Compare commits
No commits in common. "master" and "master" have entirely different histories.
|
@ -1,2 +0,0 @@
|
|||
[env]
|
||||
PCRE2_SYS_STATIC = "1"
|
|
@ -1,2 +0,0 @@
|
|||
# Use cargo-derivefmt to sort derives alphabetically
|
||||
f900dbea468e822c5a510a72ecc6367549443927
|
|
@ -1,25 +0,0 @@
|
|||
---
|
||||
|
||||
name: "Pull Request"
|
||||
about: "Standard pull request template."
|
||||
title: "WIP: "
|
||||
ref: "master"
|
||||
|
||||
---
|
||||
|
||||
<!-- If your PR is ready to merge/review, remove the `WIP: ` prefix from the title. -->
|
||||
|
||||
### 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,65 +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:
|
||||
name: Package for debian on ${{ matrix.arch }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
build: [linux-amd64, ]
|
||||
include:
|
||||
- build: linux-amd64
|
||||
arch: amd64
|
||||
os: ubuntu-latest
|
||||
rust: stable
|
||||
artifact_name: 'linux-amd64'
|
||||
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 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,90 +0,0 @@
|
|||
# SPDX-License-Identifier: EUPL-1.2
|
||||
name: Build release binary
|
||||
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
CARGO_NET_RETRY: 10
|
||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
|
||||
RUSTFLAGS: "-D warnings -W unreachable-pub -W rust-2021-compatibility"
|
||||
RUSTUP_MAX_RETRIES: 10
|
||||
RUST_BACKTRACE: short
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build on ${{ matrix.build }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
build: [linux-amd64, ]
|
||||
include:
|
||||
- build: linux-amd64
|
||||
os: ubuntu-latest
|
||||
rust: stable
|
||||
artifact_name: 'meli-linux-amd64'
|
||||
target: x86_64-unknown-linux-gnu
|
||||
steps:
|
||||
- uses: 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: cache-rustup
|
||||
# name: Cache Rust toolchain
|
||||
# uses: https://github.com/actions/cache@v3
|
||||
# with:
|
||||
# path: ~/.rustup
|
||||
# key: toolchain-${{ matrix.os }}-${{ matrix.rust }}
|
||||
#- if: ${{ steps.cache-rustup.outputs.cache-hit != 'true' }}
|
||||
- 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
|
||||
rustup toolchain install --profile minimal ${{ matrix.rust }} --target ${{ matrix.target }}
|
||||
fi
|
||||
- name: Configure cargo data directory
|
||||
# After this point, all cargo registry and crate data is stored in
|
||||
# $GITHUB_WORKSPACE/.cargo_home. This allows us to cache only the files
|
||||
# that are needed during the build process. Additionally, this works
|
||||
# around a bug in the 'cache' action that causes directories outside of
|
||||
# the workspace dir to be saved/restored incorrectly.
|
||||
run: echo "CARGO_HOME=$(pwd)/.cargo_home" >> $GITHUB_ENV
|
||||
#- id: cache-cargo
|
||||
# name: Cache cargo configuration and installations
|
||||
# uses: https://github.com/actions/cache@v3
|
||||
# with:
|
||||
# path: ${{ env.CARGO_HOME }}
|
||||
# key: cargo-${{ matrix.os }}-${{ matrix.rust }}
|
||||
#- if: ${{ steps.cache-cargo.outputs.cache-hit != 'true' }} && matrix.target
|
||||
- name: Setup Rust target
|
||||
run: |
|
||||
mkdir -p "${{ env.CARGO_HOME }}"
|
||||
cat << EOF > "${{ env.CARGO_HOME }}"/config.toml
|
||||
[build]
|
||||
target = "${{ matrix.target }}"
|
||||
EOF
|
||||
- name: Build binary
|
||||
run: |
|
||||
make
|
||||
mkdir artifacts
|
||||
mv target/*/release/* target/ || true
|
||||
mv target/release/* target/ || true
|
||||
mv target/meli artifacts/
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ matrix.artifact_name }}
|
||||
path: artifacts/meli
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
|
@ -1,111 +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:
|
||||
test:
|
||||
name: Lint on ${{ matrix.build }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
build: [linux-amd64, ]
|
||||
include:
|
||||
- build: linux-amd64
|
||||
os: ubuntu-latest
|
||||
rust: stable
|
||||
target: x86_64-unknown-linux-gnu
|
||||
steps:
|
||||
- uses: 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: cache-rustup
|
||||
# name: Cache Rust toolchain
|
||||
# uses: https://github.com/actions/cache@v3
|
||||
# with:
|
||||
# path: ~/.rustup
|
||||
# key: toolchain-${{ matrix.os }}-${{ matrix.rust }}
|
||||
#- if: ${{ steps.cache-rustup.outputs.cache-hit != 'true' }}
|
||||
- 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
|
||||
rustup toolchain install --profile minimal --component clippy,rustfmt --target ${{ matrix.target }} -- "${{ matrix.rust }}"
|
||||
rustup default ${{ matrix.rust }}
|
||||
fi
|
||||
- name: Configure cargo data directory
|
||||
# After this point, all cargo registry and crate data is stored in
|
||||
# $GITHUB_WORKSPACE/.cargo_home. This allows us to cache only the files
|
||||
# that are needed during the build process. Additionally, this works
|
||||
# around a bug in the 'cache' action that causes directories outside of
|
||||
# the workspace dir to be saved/restored incorrectly.
|
||||
run: echo "CARGO_HOME=$(pwd)/.cargo_home" >> $GITHUB_ENV
|
||||
#- id: cache-cargo
|
||||
# name: Cache cargo configuration and installations
|
||||
# uses: https://github.com/actions/cache@v3
|
||||
# with:
|
||||
# path: ${{ env.CARGO_HOME }}
|
||||
# key: cargo-${{ matrix.os }}-${{ matrix.rust }}
|
||||
#- if: ${{ steps.cache-cargo.outputs.cache-hit != 'true' }} && matrix.target
|
||||
- name: Setup Rust target
|
||||
run: |
|
||||
mkdir -p "${{ env.CARGO_HOME }}"
|
||||
cat << EOF > "${{ env.CARGO_HOME }}"/config.toml
|
||||
[build]
|
||||
target = "${{ matrix.target }}"
|
||||
EOF
|
||||
- if: ${{ steps.cache-cargo.outputs.cache-hit != 'true' }} && matrix.target
|
||||
name: Add lint dependencies
|
||||
run: |
|
||||
cargo install --quiet --version 1.0.9 --target "${{ matrix.target }}" cargo-sort
|
||||
RUSTFLAGS="" cargo install --locked --target "${{ matrix.target }}" --git https://github.com/dcchut/cargo-derivefmt --rev 2ff93de7fb418180458dd1ba27e5655607c23ab6 --bin cargo-derivefmt
|
||||
- name: rustfmt
|
||||
if: success() || failure()
|
||||
run: |
|
||||
cargo fmt --check --all
|
||||
- name: clippy
|
||||
if: success() || failure()
|
||||
run: |
|
||||
cargo clippy --no-deps --all-features --all --tests --examples --benches --bins
|
||||
- name: cargo-derivefmt melib
|
||||
if: success() || failure()
|
||||
run: |
|
||||
cargo derivefmt --manifest-path ./melib/Cargo.toml
|
||||
- name: cargo-derivefmt meli
|
||||
if: success() || failure()
|
||||
run: |
|
||||
cargo derivefmt --manifest-path ./meli/Cargo.toml
|
||||
- name: cargo-derivefmt fuzz
|
||||
if: success() || failure()
|
||||
run: |
|
||||
cargo derivefmt --manifest-path ./fuzz/Cargo.toml
|
||||
- name: cargo-derivefmt tools
|
||||
if: success() || failure()
|
||||
run: |
|
||||
cargo derivefmt --manifest-path ./tools/Cargo.toml
|
|
@ -1,91 +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: Lint Cargo manifests on ${{ matrix.build }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
build: [linux-amd64, ]
|
||||
include:
|
||||
- build: linux-amd64
|
||||
os: ubuntu-latest
|
||||
rust: stable
|
||||
target: x86_64-unknown-linux-gnu
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- id: os-deps
|
||||
name: install OS dependencies
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y mandoc
|
||||
- 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
|
||||
- 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 }}" -- "${{ env.MELI_MSRV }}"
|
||||
rustup component add rustfmt --toolchain ${{ env.MELI_MSRV }}-${{ matrix.target }}
|
||||
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: Setup Rust target
|
||||
run: |
|
||||
mkdir -p "${{ env.CARGO_HOME }}"
|
||||
cat << EOF > "${{ env.CARGO_HOME }}"/config.toml
|
||||
[build]
|
||||
target = "${{ matrix.target }}"
|
||||
EOF
|
||||
- if: ${{ steps.cache-cargo.outputs.cache-hit != 'true' }} && matrix.target
|
||||
name: Add manifest lint dependencies
|
||||
run: |
|
||||
source "${HOME}/.cargo/env"
|
||||
cargo install --quiet --version 1.0.9 --target "${{ matrix.target }}" cargo-sort
|
||||
cargo install --quiet --version 0.15.1 --target "${{ matrix.target }}" cargo-msrv
|
||||
- name: cargo-msrv verify melib MSRV
|
||||
if: success() || failure()
|
||||
run: |
|
||||
source "${HOME}/.cargo/env"
|
||||
cargo-msrv --output-format json --log-level trace --log-target stdout --path meli verify
|
||||
cargo-msrv --output-format json --log-level trace --log-target stdout --path melib verify
|
||||
- name: cargo-sort
|
||||
if: success() || failure()
|
||||
run: |
|
||||
source "${HOME}/.cargo/env"
|
||||
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
|
||||
- name: Check debian/changelog is up-to-date.
|
||||
if: success() || failure()
|
||||
run: |
|
||||
./scripts/check_debian_changelog.sh
|
|
@ -1,103 +0,0 @@
|
|||
# SPDX-License-Identifier: EUPL-1.2
|
||||
name: 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: Test on ${{ matrix.build }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
build: [linux-amd64, ]
|
||||
include:
|
||||
- build: linux-amd64
|
||||
os: ubuntu-latest
|
||||
rust: stable
|
||||
target: x86_64-unknown-linux-gnu
|
||||
steps:
|
||||
- uses: 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
|
||||
#- id: cache-rustup
|
||||
# name: Cache Rust toolchain
|
||||
# uses: https://github.com/actions/cache@v3
|
||||
# with:
|
||||
# path: ~/.rustup
|
||||
# key: toolchain-${{ matrix.os }}-${{ matrix.rust }}
|
||||
#- if: ${{ steps.cache-rustup.outputs.cache-hit != 'true' }}
|
||||
- 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
|
||||
rustup toolchain install --profile minimal ${{ matrix.rust }} --target ${{ matrix.target }}
|
||||
fi
|
||||
- name: Configure cargo data directory
|
||||
# After this point, all cargo registry and crate data is stored in
|
||||
# $GITHUB_WORKSPACE/.cargo_home. This allows us to cache only the files
|
||||
# that are needed during the build process. Additionally, this works
|
||||
# around a bug in the 'cache' action that causes directories outside of
|
||||
# the workspace dir to be saved/restored incorrectly.
|
||||
run: echo "CARGO_HOME=$(pwd)/.cargo_home" >> $GITHUB_ENV
|
||||
#- id: cache-cargo
|
||||
# name: Cache cargo configuration and installations
|
||||
# uses: https://github.com/actions/cache@v3
|
||||
# with:
|
||||
# path: ${{ env.CARGO_HOME }}
|
||||
# key: cargo-${{ matrix.os }}-${{ matrix.rust }}
|
||||
#- if: ${{ steps.cache-cargo.outputs.cache-hit != 'true' }} && matrix.target
|
||||
- name: Setup Rust target
|
||||
run: |
|
||||
mkdir -p "${{ env.CARGO_HOME }}"
|
||||
cat << EOF > "${{ env.CARGO_HOME }}"/config.toml
|
||||
[build]
|
||||
target = "${{ matrix.target }}"
|
||||
EOF
|
||||
- if: ${{ steps.cache-cargo.outputs.cache-hit != 'true' }} && matrix.target
|
||||
name: Add test dependencies
|
||||
run: |
|
||||
cargo install --quiet --version 0.9.54 --target "${{ matrix.target }}" cargo-nextest
|
||||
- name: cargo-check
|
||||
run: |
|
||||
cargo check --all-features --all --tests --examples --benches --bins
|
||||
- name: Compile
|
||||
if: success() || failure()
|
||||
run: cargo test --all --no-fail-fast --all-features --no-run --locked
|
||||
- name: cargo test
|
||||
run: |
|
||||
cargo nextest run --all --no-fail-fast --all-features --future-incompat-report -E 'not (test(smtp::test::test_smtp))'
|
||||
#cargo test --all --no-fail-fast --all-features -- --nocapture --quiet
|
||||
- name: rustdoc build
|
||||
if: success() || failure() # always run even if other steps fail, except when cancelled <https://stackoverflow.com/questions/58858429/how-to-run-a-github-actions-step-even-if-the-previous-step-fails-while-still-f>
|
||||
run: |
|
||||
make build-rustdoc
|
||||
- name: rustdoc tests
|
||||
if: success() || failure()
|
||||
run: |
|
||||
make test-docs
|
76
BUILD.md
|
@ -1,76 +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.68.2 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. The following list explains each feature's purpose:
|
||||
|
||||
- `gpgme` enables GPG support via `libgpgme` (on by default)
|
||||
- `dbus-notifications` enables showing notifications using `dbus` (on by default)
|
||||
- `notmuch` provides support for using a notmuch database as a mail backend (on by default)
|
||||
- `jmap` provides support for connecting to a jmap server and use it as a mail backend (on by default)
|
||||
- `sqlite3` provides support for builting fast search indexes in local sqlite3 databases (on by default)
|
||||
- `cli-docs` includes the manpage documentation compiled by either `mandoc` or `man` binary to plain text in `meli`'s command line. Embedded documentation can be viewed with the subcommand `meli man [PAGE]` (on by default).
|
||||
- `regexp` provides experimental support for theming some e-mail fields based
|
||||
on regular expressions.
|
||||
It uses the `pcre2` library.
|
||||
Since it's actual use in the code is very limited, it is not recommended to use this (off by default).
|
||||
- `static` and `*-static` bundle C libraries in dependencies so that you don't need them installed in your system (on by default).
|
||||
|
||||
Though not a feature, the presence of the environment variable `UNICODE_REGENERATE_TABLES` in compile-time of the `melib` crate will force the regeneration of unicode tables.
|
||||
Otherwise the tables are included with the source code, and there's no real reason to regenerate them unless you intend to modify the code or update to a new Unicode version.
|
||||
|
||||
## 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.
|
||||
|
||||
## 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`
|
594
CHANGELOG.md
|
@ -9,559 +9,54 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
### Added
|
||||
|
||||
- [0e3a0c4b](https://git.meli-email.org/meli/meli/commit/0e3a0c4b7049139994a65c6fe914dd3587c6713e) Add safe UI widget area drawing API
|
||||
- [0114e695](https://git.meli-email.org/meli/meli/commit/0114e695428579ef4461b289d7372e3b392b5e62) Add next_search_result and previous_search_result shortcuts
|
||||
- [c4344529](https://git.meli-email.org/meli/meli/commit/c4344529e30b3385149d6dc3c1c4b34306a85491) Add .git-blame-ignore-revs file
|
||||
- Added listing configuration setting `thread_subject_pack` (see meli.conf.5)
|
||||
- Added shortcuts for focusing to sidebar menu and back to the e-mail view (`focus_left` and `focus_right`)
|
||||
- `f76f4ea3` A new manual page, `meli.7` which contains a general tutorial for using meli.
|
||||
- `cbe593cf` add configurable header preample suffix and prefix for editing
|
||||
- `a484b397` Added instructions and information to error shown when libnotmuch could not be found.
|
||||
- `a484b397` Added configuration setting `library_file_path` to notmuch backend if user wants to specify the library's location manually.
|
||||
- `aa99b0d7` Implement configurable subject prefix stripping when replying
|
||||
- `a73885ac` added RGB support to embedded terminal emulator.
|
||||
- `f4e0970d` added ability to kill embed process with Ctrl-C, or Ctrl-Z and pressing 'q'.
|
||||
- `9205f3b8` added a per account mail sort order parameter.
|
||||
- `d921b3c3` implemented sorting with user sort order parameter if defined.
|
||||
- `dc5afa13` use osascript/applescript for notifications on macos
|
||||
- `d0de0485` add {in,de}crease_sidebar shortcuts
|
||||
- `340d6451` add config setting for sidebar ratio
|
||||
- `36e29cb6` Add configurable mailbox sort order
|
||||
- `7606317f` melib/notmuch: add support for virtual mailbox hierarchy
|
||||
Add optional `parent` property to notmuch mailbox configuration.
|
||||
- `d9c07def` Add command to select charset encoding for email
|
||||
Open dialog to select charset with `d`.
|
||||
- `d679a744` melib/jmap: Implement Bearer token authentication
|
||||
Fastmail now uses an API token in a http header for authentication.
|
||||
This can be used either as a server_password or provided by a
|
||||
`server_password_command` like oauth2.
|
||||
- `47e6d5d9` add edit-config CLI subcommand that opens config files on `EDITOR`
|
||||
- `8c671935` Add compose (pre-submission) hooks for validation/linting
|
||||
compose-hooks run before submitting an e-mail.
|
||||
They perform draft validation and/or transformations.
|
||||
If a hook encounters an error or warning, it will show up as a notification.
|
||||
The currently available hooks are:
|
||||
- `past-date-warn`
|
||||
Warn if Date header value is far in the past or future.
|
||||
- `important-header-warn`
|
||||
Warn if important headers (From, Date, To, Cc, Bcc) are missing or invalid.
|
||||
- `missing-attachment-warn`
|
||||
Warn if Subject, draft body mention attachments but they are missing.
|
||||
- `empty-draft-warn`
|
||||
Warn if draft has no subject and no body.
|
||||
|
||||
### Fixed
|
||||
|
||||
- [bcec745c](https://git.meli-email.org/meli/meli/commit/bcec745c241d7ed5d7d455ccdd65c6c95e1862b0) Fix command and status bar drawing
|
||||
- [62b8465f](https://git.meli-email.org/meli/meli/commit/62b8465f2cd99789576d70008f1f321243b81fc3) Fix ThreadView for new TUI API
|
||||
- [28fa66cc](https://git.meli-email.org/meli/meli/commit/28fa66cc2ad05e67708377fc99ffd65aa1b14386) Fix ThreadedListing for new TUI API
|
||||
- [2c6f180d](https://git.meli-email.org/meli/meli/commit/2c6f180df987976c1f4cba7ceac878e697c73d27) Fix macos compilation
|
||||
- [24971d19](https://git.meli-email.org/meli/meli/commit/24971d1960418bad92d89af9eb744933445baf99) Fix compilation with 1.70.0 cargo
|
||||
They can be disabled with `[composing.disabled_compose_hooks]` setting.
|
||||
|
||||
### Changed
|
||||
|
||||
- [a1cbb198](https://git.meli-email.org/meli/meli/commit/a1cbb1988b34951046045f724f52bed2925b3880) Return Results instead of panicking
|
||||
- [7645ff1b](https://git.meli-email.org/meli/meli/commit/7645ff1b875e3920389567eb5e61d800291e8a27) Rename write_string{to_grid,}
|
||||
- [c2ae19d1](https://git.meli-email.org/meli/meli/commit/c2ae19d1208f2eb5cca341a04e019c3e285637a8) Return Option from current_pos
|
||||
- [b61fc3ab](https://git.meli-email.org/meli/meli/commit/b61fc3ab6482dcef4f5cc1c09db3539b7e401f78) Add HelpView struct for shortcuts widget
|
||||
- [ba7a97e9](https://git.meli-email.org/meli/meli/commit/ba7a97e90b4c474299a7b12fa74b7ea06c1535c8) Add x axis scroll support
|
||||
- [3495ffd6](https://git.meli-email.org/meli/meli/commit/3495ffd61b5888f8538304ecb6e441819b373bdc) Change UIEvent::Notification structure
|
||||
- [ccf6f9a2](https://git.meli-email.org/meli/meli/commit/ccf6f9a26e95437fb24464f90736c653e3f5dfed) Remember previous `set [index_style]]` preferences
|
||||
- [23c15261](https://git.meli-email.org/meli/meli/commit/23c15261e79c63791c569f225c1745df1b90ce2d) Abstract envelope view filters away
|
||||
- [031d0f7d](https://git.meli-email.org/meli/meli/commit/031d0f7dc76700ac938e1ee4a767fab8deebb9f2) Add area.is_empty() checks in cell iterators
|
||||
- [e37997d6](https://git.meli-email.org/meli/meli/commit/e37997d697f1f0b8faaa56a36f43c9f1da4bbb41) Store Link URL value in Link type
|
||||
|
||||
### Refactoring
|
||||
|
||||
- [0500e451](https://git.meli-email.org/meli/meli/commit/0500e451dab5f129d71a9279913531e77981e868) Add missing EnvelopeRemove event handler
|
||||
- [ab14f819](https://git.meli-email.org/meli/meli/commit/ab14f81900a03a07ef00a6b3232cb29d78e8edf5) Make write_string_to_grid a CellBuffer method
|
||||
- [e0adcdfe](https://git.meli-email.org/meli/meli/commit/e0adcdfe15b8a78c333de199ba734a83181f53be) Move rest of methods under CellBuffer
|
||||
- [0a74c7d0](https://git.meli-email.org/meli/meli/commit/0a74c7d0e5c318dd29c8ace01e588d441e0fcfb6) Overhaul refactor
|
||||
- [3b4acc15](https://git.meli-email.org/meli/meli/commit/3b4acc15a535c9bfd084b2e33f2cd00b5b5d4eb0) Add tests
|
||||
- [7eedd860](https://git.meli-email.org/meli/meli/commit/7eedd860518e3f7f5000a1888e4fa58ddbfb43bc) Remove address_list! macro
|
||||
- [f3e85738](https://git.meli-email.org/meli/meli/commit/f3e85738e7981755e96468213c02af78432f8cdd) Move build.rs scripts to build directory
|
||||
- [77325486](https://git.meli-email.org/meli/meli/commit/773254864bd8436712905eeb0c725d1d05277e60) Remove on-push hooks for actions w/ run on-pr
|
||||
|
||||
### Documentation
|
||||
|
||||
- [d018f07a](https://git.meli-email.org/meli/meli/commit/d018f07aa51fc293bf696fa7d7beff8e59ac91a8) Retouch manual pages
|
||||
- [3adba40e](https://git.meli-email.org/meli/meli/commit/3adba40e32a8a66271ea2a8f5ddf27858744ecd6) Add macos manpage mirror url
|
||||
|
||||
### Packaging
|
||||
|
||||
- [cd2ba80f](https://git.meli-email.org/meli/meli/commit/cd2ba80f8e5424be08421b4dcc5113977418f240) Update metadata
|
||||
- [5f8d7c80](https://git.meli-email.org/meli/meli/commit/5f8d7c8039c0623b3950fd1a8eb566f943fc309d) Update deb-dist target command with author metadata
|
||||
- [59c99fdc](https://git.meli-email.org/meli/meli/commit/59c99fdc79bb31fb42cb99d4b95613022396a499) Update debian package metadata
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- [6506fffb](https://git.meli-email.org/meli/meli/commit/6506fffb9427ba13ba4368cd6b2c0dba12e5294c) Rewrite email flag modifications
|
||||
- [23507932](https://git.meli-email.org/meli/meli/commit/23507932f94257a71f2ca8db23840ee0716072b6) Update cache on set_flags
|
||||
- [470cae6b](https://git.meli-email.org/meli/meli/commit/470cae6b885c9b4851195fbb8274b1663bfa75cb) Update thread cache on email flag modifications
|
||||
- [c1c41c91](https://git.meli-email.org/meli/meli/commit/c1c41c9126005266f00d4979777718463dddf7b2) Update README.md and add Codeberg mirror
|
||||
- [84f3641e](https://git.meli-email.org/meli/meli/commit/84f3641ec1401a0522811add0ed87a131be449b9) Re-add on-screen message display
|
||||
- [54d21f25](https://git.meli-email.org/meli/meli/commit/54d21f25fdb716d36fd3678dd149eb880e16698d) Re-add contact list and editor support
|
||||
- [458258e1](https://git.meli-email.org/meli/meli/commit/458258e1aab91f3883d6a9201a175462511349e9) Re-enable compact listing style
|
||||
- [1c1be7d6](https://git.meli-email.org/meli/meli/commit/1c1be7d6c9bfc9f14c3a62ce464e1e15f2e6c4ec) Add display_name(), display_slice(), display_name_slice() methods
|
||||
- [5dd71ef1](https://git.meli-email.org/meli/meli/commit/5dd71ef1cd93aebaadb0554eac692d0a0fa4aecd) Upgrade JobsView component to new TUI API
|
||||
- [b5cc2a09](https://git.meli-email.org/meli/meli/commit/b5cc2a095f0268bb90cab150e903b0bbaffe1479) Upgrade MailboxManager component to new TUI API
|
||||
- [ed8a5de2](https://git.meli-email.org/meli/meli/commit/ed8a5de2cb4b93ad766803d3590f7041f28cc419) Re-enable EditAttachments component
|
||||
- [77a8d9e2](https://git.meli-email.org/meli/meli/commit/77a8d9e2c2094e84e06f5d624cb6f8afda24a400) Make ModSequence publicly accessible
|
||||
- [64898a05](https://git.meli-email.org/meli/meli/commit/64898a0583e348fef3cd266a7196425e7015a871) Make UIDStore constructor pub
|
||||
- [10c3b0ea](https://git.meli-email.org/meli/meli/commit/10c3b0eabe1684699c775e03c4c58038ea7979af) Bump version to 0.8.5-rc.1
|
||||
- [71f3ffe7](https://git.meli-email.org/meli/meli/commit/71f3ffe740276087f20d85d62440ef5d3fe426f6) Update Makefile
|
||||
- [63a63253](https://git.meli-email.org/meli/meli/commit/63a63253d77f6e1b9a42ec55ecf0bbc45a011245) Use type alias for c_char
|
||||
- [c751b2e8](https://git.meli-email.org/meli/meli/commit/c751b2e8450aa83b7a8f5e8afbeccadf333f74ba) Re-enable conversations listing style
|
||||
- [d16afc7d](https://git.meli-email.org/meli/meli/commit/d16afc7d8d9e2eddb81664673e9a4ef82da2e303) Bump version to 0.8.5-rc.2
|
||||
- [da251455](https://git.meli-email.org/meli/meli/commit/da251455a0185e207e0ec2d51273f6ddbdb572a8) Bump meli version to 0.8.5-rc.2
|
||||
- [3a709794](https://git.meli-email.org/meli/meli/commit/3a7097948308981204132a0eed2d28338f9d6b33) Update minimum rust version from 1.65.0 to 1.68.2
|
||||
- [f900dbea](https://git.meli-email.org/meli/meli/commit/f900dbea468e822c5a510a72ecc6367549443927) Use cargo-derivefmt to sort derives alphabetically
|
||||
- [5ff4e8ae](https://git.meli-email.org/meli/meli/commit/5ff4e8ae68182db8d4535d8537d26a3f398c815b) Run builds.yaml when any manifest file changes
|
||||
- [0a617410](https://git.meli-email.org/meli/meli/commit/0a617410ec1ce5f6fb43772e4ad43f45f58a7f4d) Split test.yaml to test.yaml and lints.yaml
|
||||
- [3ba1603a](https://git.meli-email.org/meli/meli/commit/3ba1603af2a9e408659717b9c8dace7406a8b142) Add manifest file only lints workflow
|
||||
- [1617212c](https://git.meli-email.org/meli/meli/commit/1617212c5b0948174155ece4a9d0584764bd7dac) Add scripts/check_debian_changelog.sh lint
|
||||
- [e19f3e57](https://git.meli-email.org/meli/meli/commit/e19f3e572c0ac585a6c2023e50f8fd0bd2ea2dae) Cargo-sort all Cargo.toml files
|
||||
- [c41f35fd](https://git.meli-email.org/meli/meli/commit/c41f35fdd55bf093656b68cc69eab4cf4b9a8ec4) Use actions/checkout@v3
|
||||
- [876616d4](https://git.meli-email.org/meli/meli/commit/876616d45b7798131ecdda82bb90d1d481842f5c) Use actions/upload-artifact@v3
|
||||
- [2419f4bd](https://git.meli-email.org/meli/meli/commit/2419f4bd40fb1a732cf1df42dde48ba8ca812072) Add debian package build workflow
|
||||
|
||||
## [v0.8.4](https://git.meli-email.org/meli/meli/releases/tag/v0.8.4) - 2023-11-22
|
||||
- `f76f4ea3` Shortcut `open_thread` and `exit_thread` renamed to `open_entry` and `exit_entry`.
|
||||
- `7650805c` Binary size reduced significantly.
|
||||
|
||||
### Fixed
|
||||
|
||||
- [ef30228e](https://git.meli-email.org/meli/meli/commit/ef30228e08efe6e36ab9858a5ba32876d6d8fdae) Fix failing test
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- [f81a1e23](https://git.meli-email.org/meli/meli/commit/f81a1e23382208390394be71e3aaa27ee505cb0f) Bump version to 0.8.4
|
||||
|
||||
## [v0.8.3](https://git.meli-email.org/meli/meli/releases/tag/v0.8.3) - 2023-11-22
|
||||
|
||||
### Added
|
||||
|
||||
- [3105a037](https://git.meli-email.org/meli/meli/commit/3105a0373b8754f37b326239c1cf7129fae06e1b) Add quit command
|
||||
|
||||
### Fixed
|
||||
|
||||
- [d3cbf184](https://git.meli-email.org/meli/meli/commit/d3cbf184e606d5b7ade9cfb125db01f45d7180ae) Add extra_submission_headers fields in composer form and autocomplete for Newsgroups
|
||||
- [7aec5b8e](https://git.meli-email.org/meli/meli/commit/7aec5b8e78d80e7717a9aedd7344db6b108534f5) Fix SMTP example doc
|
||||
- [f702dc22](https://git.meli-email.org/meli/meli/commit/f702dc220c9ab97ce0fddfae194d5e2935a20193) Fix new clippy lints.
|
||||
- [688e39a6](https://git.meli-email.org/meli/meli/commit/688e39a67e6a467ca649acbe20b1f368fbc1e9f0) Fix clippy lints
|
||||
|
||||
### Changed
|
||||
|
||||
- [5a7919bb](https://git.meli-email.org/meli/meli/commit/5a7919bb03641be6d7bc5b9002d44e16ee358f12) Use ConversationsListing::format_date
|
||||
- [0f3b5294](https://git.meli-email.org/meli/meli/commit/0f3b52945959b53c8d809eb434a91ec4c561b2d4) Hoist format_date() to ListingTrait method
|
||||
|
||||
### Refactoring
|
||||
|
||||
- [e1b55340](https://git.meli-email.org/meli/meli/commit/e1b55340fa258a2a7b118fd18c11614fb2b5e173) Show error description when TIOCGWINSZ ioctl fails
|
||||
- [e95c275d](https://git.meli-email.org/meli/meli/commit/e95c275d68fe3dbd588046c110ae8b3fa966f6de) Remove duplicate end sequence
|
||||
- [8a21be21](https://git.meli-email.org/meli/meli/commit/8a21be21775cb474a6b65e1c0bffd771c0df6f2f) Replace splice with truncate
|
||||
- [2db021fa](https://git.meli-email.org/meli/meli/commit/2db021fa0a9a707cd7cdb6c8bf140bf5c8acf906) Remove regexp from default features
|
||||
- [fa33a946](https://git.meli-email.org/meli/meli/commit/fa33a9468a16c50361353efa269fca79bd58e284) Move managesieve-client binary to tools/
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- [e88957ae](https://git.meli-email.org/meli/meli/commit/e88957ae6edfee7fabb41e9210f9d906866cda8d) Add extra_submission_headers field in MailBackendCapabilities struct
|
||||
- [606f487f](https://git.meli-email.org/meli/meli/commit/606f487fc5e227f1727697a5911e27cbec174089) Add IRC channel badge
|
||||
- [0e60bdf2](https://git.meli-email.org/meli/meli/commit/0e60bdf26eb842744f59257800ca8e30b1a43836) Add "iterator" feature to signal-hook
|
||||
- [ac2a5dcd](https://git.meli-email.org/meli/meli/commit/ac2a5dcdd10d97f5ed9c8a8c83e1641b373dd31a) Add display() method for Address
|
||||
- [43bfd413](https://git.meli-email.org/meli/meli/commit/43bfd4131d5cab39319d1943bcad46e929ec4d56) Update ahash dependency
|
||||
- [af241d25](https://git.meli-email.org/meli/meli/commit/af241d25cbab20227a88ec4d557222cdeed98dde) Bump version to 0.8.3
|
||||
- [7387b67e](https://git.meli-email.org/meli/meli/commit/7387b67eeee27aefbc4d20ca2a1d503aa0fb1838) Enable "static" build for C library dependencies by default
|
||||
- [bfc78a08](https://git.meli-email.org/meli/meli/commit/bfc78a0803524e236bc883833838d3ad78918621) Replace CRLF with LF when editing
|
||||
- [111a1160](https://git.meli-email.org/meli/meli/commit/111a1160adf2e0fef00a90350784307c859a198b) Bump version to 0.8.3
|
||||
|
||||
## [v0.8.2](https://git.meli-email.org/meli/meli/releases/tag/v0.8.2) - 2023-09-22
|
||||
|
||||
### Fixed
|
||||
|
||||
- [73b3ed55](https://git.meli-email.org/meli/meli/commit/73b3ed559d21dcc7cdee7f96119461e2447c1906) Fix forward dialog not workng
|
||||
- [7888d8b2](https://git.meli-email.org/meli/meli/commit/7888d8b2a5dc977f0f18094a32dc73893a5cfc4f) Fix doc test compilation
|
||||
|
||||
### Changed
|
||||
|
||||
- [22525d40](https://git.meli-email.org/meli/meli/commit/22525d40fb48661f86657151e35fdf9c95c4b45e) Go to end when pressing next/page down for the second time
|
||||
- [71474436](https://git.meli-email.org/meli/meli/commit/714744366f5e26fc1b6609e8e785d64489f9a68d) Revert 22525d40 behavior when sidebar not focused
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- [eb5d49c4](https://git.meli-email.org/meli/meli/commit/eb5d49c41ac58c5068011620c22e21b5fa115417) Use Self in self methods
|
||||
- [3d85ca2e](https://git.meli-email.org/meli/meli/commit/3d85ca2edfca9abff4b3ffdd837b25e68c6586c2) Bump version to 0.8.2
|
||||
|
||||
## [v0.8.1](https://git.meli-email.org/meli/meli/releases/tag/v0.8.1) - 2023-09-13
|
||||
|
||||
### Added
|
||||
|
||||
- [6476985c](https://git.meli-email.org/meli/meli/commit/6476985ce6abbb9048ba5aec19f6c5144bfe89b7) Add Cross.toml for aarch64-unknown-linux-gnu builds
|
||||
- [45d4f611](https://git.meli-email.org/meli/meli/commit/45d4f611b170d7b80afca5810c51fea1bf084c10) Add install-man cli subcommand to install manpages on your system
|
||||
- [a4f0dbac](https://git.meli-email.org/meli/meli/commit/a4f0dbac26126c03886115e518b3cd2ede0b88cb) Add current working directory tracking to Context
|
||||
|
||||
### Fixed
|
||||
|
||||
- [49a38a23](https://git.meli-email.org/meli/meli/commit/49a38a23bf522a18e636385632cfe3533c4f525c) Fix invalid Type link references
|
||||
- [85af5244](https://git.meli-email.org/meli/meli/commit/85af524458bc06421ac39689469474efb8164c1c) Fix invalid mailto() results when body field exists
|
||||
- [c7825c76](https://git.meli-email.org/meli/meli/commit/c7825c76c3ac6be89f64f1f04afd9c0ca08bdf76) Handle dialog Esc in the parent component
|
||||
- [dd4d0b79](https://git.meli-email.org/meli/meli/commit/dd4d0b79721d8cd5b29cdaca9cd01412974f2e13) Fix typo
|
||||
- [c43aeb0e](https://git.meli-email.org/meli/meli/commit/c43aeb0eb103f2a8fd802f84eab56551c6e65418) Fix invalid address parse on folded values
|
||||
- [7e3e9386](https://git.meli-email.org/meli/meli/commit/7e3e9386316ef344580d9e44edb3f8b0c196c3c5) Fix out-of-bounds draw when terminal is small
|
||||
- [7e4ed2fa](https://git.meli-email.org/meli/meli/commit/7e4ed2fa107eca2ef309bcaa211440c315730b6c) Fix some out of bounds drawing.
|
||||
|
||||
### Changed
|
||||
|
||||
- [1b3bebe3](https://git.meli-email.org/meli/meli/commit/1b3bebe3049ae5c7cb2210ed95c355c9b5c709f8) Open earliest unread email instead of first in thread
|
||||
- [49c36009](https://git.meli-email.org/meli/meli/commit/49c36009cec8c88d61d796162787990216bfeeab) Don't initialize entire thread at once
|
||||
- [0a9c89b6](https://git.meli-email.org/meli/meli/commit/0a9c89b6b357fc3d002c3eb451fd67e7a49ce7f5) Add toggle_layout shortcut
|
||||
- [64ba0459](https://git.meli-email.org/meli/meli/commit/64ba0459ee3652eaf451d10222853a898d85e337) Init cursor at To: header field
|
||||
- [81974311](https://git.meli-email.org/meli/meli/commit/81974311c200b8ad66c0e626f8b8db6686e565ff) Show current number command buffer
|
||||
|
||||
### Refactoring
|
||||
|
||||
- [a337e226](https://git.meli-email.org/meli/meli/commit/a337e2269e584769314cdf325cdeb6e57cb0c622) Refactor module structure
|
||||
- [b4f2f335](https://git.meli-email.org/meli/meli/commit/b4f2f3357613729e493e5f41a48def7610dc65aa) Remove deflate feature; make it a hard dependency
|
||||
- [2dc29405](https://git.meli-email.org/meli/meli/commit/2dc29405868b9df0dfff25e341814526a478db00) Add feature to use cache instead of downloading unicode data
|
||||
- [0132677f](https://git.meli-email.org/meli/meli/commit/0132677ff54a9618d3c59b08a188b73ae0c062c7) Introduce CommandError with context
|
||||
- [3344a8db](https://git.meli-email.org/meli/meli/commit/3344a8dbf6b478a85d2b933fc1fa1a6001c600f4) Remove unnecessary Clone derives
|
||||
- [b673af02](https://git.meli-email.org/meli/meli/commit/b673af02ac9e9d4be95daa2490ce24d0bc9b10d9) Move to crate root
|
||||
- [54862f86](https://git.meli-email.org/meli/meli/commit/54862f8651cb7dfe3bca7f5924fe776b93ac6aee) Add hide_sidebar_on_launch option
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- [a615b470](https://git.meli-email.org/meli/meli/commit/a615b4701b7e852a9112b317e2e31997c6cbe82e) Embed xdg-utils crate
|
||||
- [f0075b86](https://git.meli-email.org/meli/meli/commit/f0075b86cf636a3d39d4edf1ff6d58c112bbecf7) Show descriptive tab names for composer and threads
|
||||
- [6d5ebb5b](https://git.meli-email.org/meli/meli/commit/6d5ebb5b04279fe6e4fbf598504cae2f012fa494) Split code into submodules, add better error reporting
|
||||
- [63abf1e8](https://git.meli-email.org/meli/meli/commit/63abf1e890b93fcadf35f88b3dbea473c0d8f5cd) Update README.md
|
||||
- [bb4d2000](https://git.meli-email.org/meli/meli/commit/bb4d20003690d72b62a66d46a1fc5ae914e2bf64) Unify toggle_* parsers
|
||||
- [9b9c38f7](https://git.meli-email.org/meli/meli/commit/9b9c38f769abae0ff86e4b71e4db0ad65fdacfb4) Don't flood user with sqlite3 errors if db is corrupted
|
||||
- [747e39bf](https://git.meli-email.org/meli/meli/commit/747e39bf55cfc19b6eeece3ca7c71bad98d92389) Add print-used-paths subcommand
|
||||
- [39e99770](https://git.meli-email.org/meli/meli/commit/39e99770da4b51d0986a4b561fbe36b27d04565d) Use Context::current_dir() when saving files to relative paths
|
||||
- [fe0a96f0](https://git.meli-email.org/meli/meli/commit/fe0a96f0855486207280430064a93cab94dffeb2) Update to 2021 edition
|
||||
- [3944e4e6](https://git.meli-email.org/meli/meli/commit/3944e4e60e431247eefc0b3cf35af27fb011f37b) Update to 2021 edition
|
||||
- [7eed8278](https://git.meli-email.org/meli/meli/commit/7eed82783a3dbac513e233be4f0bce06904fe8c8) Bump version to 0.8.1
|
||||
|
||||
## [v0.8.0](https://git.meli-email.org/meli/meli/releases/tag/v0.8.0) - 2023-08-29
|
||||
|
||||
### Added
|
||||
|
||||
- [36e29cb6](https://git.meli-email.org/meli/meli/commit/36e29cb6fd00c798ad83e3064e0ff78c8153dced) Add configurable mailbox sort order
|
||||
- [81184b18](https://git.meli-email.org/meli/meli/commit/81184b182c5f5d65614653b817981fddc6a84ffa) Add extra_identities configuration flag
|
||||
- [b716e438](https://git.meli-email.org/meli/meli/commit/b716e4383ea3163cabe760cd5512b7d70b218915) Add collapse option for mailboxes in sidebar menu
|
||||
- [3d92b410](https://git.meli-email.org/meli/meli/commit/3d92b41075fc16214675cf141acd9c89fb6f5c49) Add cli-docs feature to the default set
|
||||
- [104352e5](https://git.meli-email.org/meli/meli/commit/104352e5950598f4a659bd593d587910af8adc12) Add table UI widget
|
||||
- [7d9cabb0](https://git.meli-email.org/meli/meli/commit/7d9cabb023b510e6175fd6b2523f0414a6da1f3f) Add mailbox manager tab
|
||||
- [660bacb9](https://git.meli-email.org/meli/meli/commit/660bacb9262dac7457bd8c421cc70343a0db3cd5) Add `mailto` command to open composer with initial values from mailto template
|
||||
- [3adf72ae](https://git.meli-email.org/meli/meli/commit/3adf72aed0772fea39fbd6cbaec680fb2995e92d) Add support for utf-7 encoding
|
||||
- [d9c07def](https://git.meli-email.org/meli/meli/commit/d9c07def0f5db655aa11c5981d1419a336c3d91a) Add command to select charset encoding for email
|
||||
- [8c671935](https://git.meli-email.org/meli/meli/commit/8c671935f9ad5bd2894c0ecdaec9c2f378e461ca) Add compose (pre-submission) hooks for validation/linting
|
||||
- [96537e48](https://git.meli-email.org/meli/meli/commit/96537e48c5f5c8d54076ec5db76e94a499cbe1e6) Add {Timer,Component}Id wrapper types over Uuid
|
||||
- [b5f205b7](https://git.meli-email.org/meli/meli/commit/b5f205b77b8911a1fb6019767bb026e5f4a7f79e) Add availability to use server_password_command in the nntp backend like in the IMAP backend
|
||||
- [a5770c89](https://git.meli-email.org/meli/meli/commit/a5770c89f46b908d17d6eb4573c8337a952f99a8) Add Woodpecker-CI check pipeline
|
||||
- [d4e605c0](https://git.meli-email.org/meli/meli/commit/d4e605c098ba13b8bc2d9f14d07ea45da38e9a2f) Add tagref source code annotations
|
||||
- [cf9a04a5](https://git.meli-email.org/meli/meli/commit/cf9a04a5910c9d82e1acb10a2f4d40c2af0335ed) Add metadata to Jobs, and add JobManager tab
|
||||
- [bb7e119a](https://git.meli-email.org/meli/meli/commit/bb7e119ade131e8fe1bcac39b616741af817808c) Add gitea CI workflows
|
||||
- [1c79786e](https://git.meli-email.org/meli/meli/commit/1c79786ea210e53ee7d566455d83d74fe4699d28) Add scripts/make_html_manual_page.py
|
||||
- [65e82d88](https://git.meli-email.org/meli/meli/commit/65e82d8896500e8ef586656e3bde4bc102b84aba) Add meli/README.md symbolic link
|
||||
|
||||
### Fixed
|
||||
|
||||
- [ce2068d3](https://git.meli-email.org/meli/meli/commit/ce2068d36bb5d8ad0bb8f886bc19cb4aab75c4e8) Fix background watch using JSON paths incorrectly
|
||||
- [e9aaa7b0](https://git.meli-email.org/meli/meli/commit/e9aaa7b067903040acd7f3d7c685de94b3b98450) Use *const c_char instead of *const i8 for portability
|
||||
- [aa3524dd](https://git.meli-email.org/meli/meli/commit/aa3524dd305f2cf293eaaf7120b812478255f79c) Fix tag not being removed in set_flags()
|
||||
- [daa900ec](https://git.meli-email.org/meli/meli/commit/daa900ec9a566460833c020feba10933c0248162) Fix embed terminal in macos
|
||||
- [7fca5f01](https://git.meli-email.org/meli/meli/commit/7fca5f01ef53069958403dd794ee0e5c310f4e45) Fix jmap build with isahc 1.7.2
|
||||
- [ed3dbc85](https://git.meli-email.org/meli/meli/commit/ed3dbc85861ab61fee56077c7ba94306b0a96dc4) Fix crashes when listing is empty
|
||||
- [824f614a](https://git.meli-email.org/meli/meli/commit/824f614a69e55a25d67832593cb8aadb9671e306) Fix HtmlView not being redrawn when parent is dirty
|
||||
- [97ff3e78](https://git.meli-email.org/meli/meli/commit/97ff3e787fbfb5ff50e3ba787f067829509f7cd2) Only add toml files to the themes
|
||||
- [9cb66ef8](https://git.meli-email.org/meli/meli/commit/9cb66ef818f6598eb779f931e201a8d38e86a484) Fix all clippy warnings in `meli` crate
|
||||
- [0c0bee44](https://git.meli-email.org/meli/meli/commit/0c0bee4482d4fbfa675b97ca30405fdc77655936) Add missing .PHONY targets, fix missing tab indentation
|
||||
- [a73885ac](https://git.meli-email.org/meli/meli/commit/a73885acb14cd94d4a6a54ebd5b39a001d7e21e1) Improve embed terminal
|
||||
- [da9c80cc](https://git.meli-email.org/meli/meli/commit/da9c80ccfd7aa87842c2c3c089ba2b784a583ab6) Enhance SubjectPrefix with strip_prefixes_from_list() method
|
||||
- [aa99b0d7](https://git.meli-email.org/meli/meli/commit/aa99b0d787463be4267913b801117bd4d2ea5003) Implement configurable subject prefix stripping when replying
|
||||
- [cbe593cf](https://git.meli-email.org/meli/meli/commit/cbe593cf31308dcf549d7880eea2d82e5024dd73) Add configurable header preample suffix and prefix for editing
|
||||
- [2de69d17](https://git.meli-email.org/meli/meli/commit/2de69d17f14e79ce2a35564d278b5e895d16a48f) Fix erroneous placement of newlnes for wrap_header_preamble suffix
|
||||
- [94bd84b4](https://git.meli-email.org/meli/meli/commit/94bd84b45d53b0e0fae52198fbdc05179b87cccc) Fix clippy lints for `meli` crate
|
||||
- [b138d9bc](https://git.meli-email.org/meli/meli/commit/b138d9bc6166b763febf035b50109d810e3c18c9) Fix some clippy lints
|
||||
- [c6bdda03](https://git.meli-email.org/meli/meli/commit/c6bdda03cf451ab52a3d414cad1344bb32c82879) Fix notmuch error shown on any missing backend
|
||||
- [16646976](https://git.meli-email.org/meli/meli/commit/16646976d75284665c1fa0d7b7e3e3cde3531d66) Fix reply subject prefixes stripping original prefix
|
||||
- [88a1f0d4](https://git.meli-email.org/meli/meli/commit/88a1f0d4bc17b60f8f23ea71f33a81aee78f8769) Fix FETCH response parsing bug
|
||||
- [59b95f83](https://git.meli-email.org/meli/meli/commit/59b95f83d2b388b30a3a855f68bf5952355597d7) Fix docs
|
||||
- [282af86e](https://git.meli-email.org/meli/meli/commit/282af86e83807772f042b115af24ffe2e0575b9e) Fix NAME sections manual pages for correct whatis(1) parsing
|
||||
- [bd22f986](https://git.meli-email.org/meli/meli/commit/bd22f986f0c06f6dae535733d484aa89f610ed46) Fix clippy lints
|
||||
- [5ba7b2cd](https://git.meli-email.org/meli/meli/commit/5ba7b2cd7bb07abe8faafe5e45db6145b3f90bc9) Fix clippy lints for meli binary
|
||||
- [7924aa8b](https://git.meli-email.org/meli/meli/commit/7924aa8bfe8f0fbcd557bb8bb3a9d3ebeab2220a) Fix compilation
|
||||
- [b9030a68](https://git.meli-email.org/meli/meli/commit/b9030a684c0ad64951a388e49d5825c12b483fb4) Fix selection not appearing immediately and invalid motions
|
||||
- [4f45b109](https://git.meli-email.org/meli/meli/commit/4f45b109745ebc29febc452b9bcb0cd88f131ffc) Fix tag updates not showing up right away
|
||||
- [abc56eae](https://git.meli-email.org/meli/meli/commit/abc56eae431153d2e48f8b1eb3e0d2a140b600d8) Fix SEEN flag update hiding mail view momentarily
|
||||
- [40c6647d](https://git.meli-email.org/meli/meli/commit/40c6647db83c5137b79c9bec233972a8a78aeb76) Fix multipart/related with main text/html part not displayed correctly
|
||||
- [11140b4a](https://git.meli-email.org/meli/meli/commit/11140b4a76419a6f8c83db38823e83aeac8fbb98) Fix test output
|
||||
- [3a10953f](https://git.meli-email.org/meli/meli/commit/3a10953f05ea4944a8a20c2c5d647d5862dca907) Update fix-prefix-for-debian.patch
|
||||
- [939dc15e](https://git.meli-email.org/meli/meli/commit/939dc15e289e06a0fad72e44f9e91133892a4ec0) Fix melib tests
|
||||
- [39d9c2af](https://git.meli-email.org/meli/meli/commit/39d9c2af3b7daf39c6aa7eab5f2d95f1b9c3a562) Fix test smtp server logic
|
||||
- [34bb532e](https://git.meli-email.org/meli/meli/commit/34bb532e8d91c5f35bdc058821da63ac543ecfa6) Mention w3m dependency
|
||||
- [b1a71887](https://git.meli-email.org/meli/meli/commit/b1a71887710153f0f98b25b2f224fbe37f7a6889) Clippy fixes
|
||||
- [1f8ac228](https://git.meli-email.org/meli/meli/commit/1f8ac2287b960e0ed5c44dadbf68b924f035d321) Fix ftplugin location and add example mail.vim file
|
||||
- [1eea8bab](https://git.meli-email.org/meli/meli/commit/1eea8bab77cc20fb911f13aa16322a217b36b06b) Fix `test_imap_fetch_response`.
|
||||
- [daf42fd4](https://git.meli-email.org/meli/meli/commit/daf42fd456bad5ddf65ac515c2fb277896d1fea3) Fix build error with quote 1.0.28
|
||||
- [6388bea9](https://git.meli-email.org/meli/meli/commit/6388bea9a063f776398ffc503fdb0789ce9af9f1) Fix &[u8] index in HeaderMap
|
||||
- [c5ecacea](https://git.meli-email.org/meli/meli/commit/c5ecaceae1ab50a1c337f5cab9e97c0b061cb2d5) Fix some search criteria in Query type
|
||||
- [27a4dcb9](https://git.meli-email.org/meli/meli/commit/27a4dcb916e0bed723490df9d82bfd7c83f10a83) Fix some rustdoc lints
|
||||
- [fdc0861a](https://git.meli-email.org/meli/meli/commit/fdc0861ac0ac725e6e5031d120bd4682752c0267) Fix expanded_hash argument off by one error
|
||||
- [0c0a678c](https://git.meli-email.org/meli/meli/commit/0c0a678cffec73940065923bb3837deb85075f9f) Fix overlay widgets not being reaped after Unrealize event
|
||||
- [65179d48](https://git.meli-email.org/meli/meli/commit/65179d4816a39b0c92e9c6a981b491c60313634f) Fix cursor/widget focus scrolling logic
|
||||
- [e64923ee](https://git.meli-email.org/meli/meli/commit/e64923eeaaf1fdf0ee485cceff0c57b2d43f165a) Fix debug_assert condition
|
||||
- [5f29faa6](https://git.meli-email.org/meli/meli/commit/5f29faa640ebe7b14e76e56227a482207b8d952e) Clippy lint fixes
|
||||
- [0b258a1f](https://git.meli-email.org/meli/meli/commit/0b258a1f058fa08b143a8e573883a4abe89dc7e1) Clippy lint fixes
|
||||
- [ba7f5dce](https://git.meli-email.org/meli/meli/commit/ba7f5dce1c37c04768aa060b35f3803e6db3840e) Fix display of threaded conversations tree structure
|
||||
- [1dc1d868](https://git.meli-email.org/meli/meli/commit/1dc1d86848eb6d187120bcaa00296f2b4e2025ca) Fix infinite loop bug
|
||||
- [e8e49e74](https://git.meli-email.org/meli/meli/commit/e8e49e741b0f888d44da69f52aa3fff2e03e7ced) Fix wrong per message offset
|
||||
- [e3dfeaad](https://git.meli-email.org/meli/meli/commit/e3dfeaad7e4f838af5fb2e6e398d3e1aa37fe511) Fix compilation error when building without `gpgme` feature
|
||||
- [7998e1e7](https://git.meli-email.org/meli/meli/commit/7998e1e77ef057bab28434edefb79d7be6a4de33) Add missing LC libc constants for openbsd target_os
|
||||
- [b5657201](https://git.meli-email.org/meli/meli/commit/b5657201db4828c6e61c52e7ce338ac1a6e6f9fc) Fix doctest compilation errors
|
||||
- [c2ed3e28](https://git.meli-email.org/meli/meli/commit/c2ed3e283f6729ac7e112d00ae54dd99a2ada5e6) Fix Source::* view showing only envelope body
|
||||
- [d93ee413](https://git.meli-email.org/meli/meli/commit/d93ee413a766f35a4ef88d9fc3ace9cf37d28dd1) Add timestamp_to_string_utc
|
||||
- [6086a378](https://git.meli-email.org/meli/meli/commit/6086a3789d4d01818322dab1f1a9eb4c1f6a2b25) Fix libgpgme segfault error and re-enable gpg
|
||||
- [ab418c1d](https://git.meli-email.org/meli/meli/commit/ab418c1d39d02840bc5c61996c1a5416e2f35464) Refresh documentation, fix encryption/signing
|
||||
- [0219dc87](https://git.meli-email.org/meli/meli/commit/0219dc870798a16fd4d9f546d14c115f9e2c6bd8) Respect max_objects_in_get when fetching email
|
||||
- [6280bc75](https://git.meli-email.org/meli/meli/commit/6280bc75e550332a73c1a51dd46475cd54cc0a34) Fix blob download URL formatting
|
||||
- [2df73547](https://git.meli-email.org/meli/meli/commit/2df73547515fd3464e1fc2b88aa67462f583a8ec) Fix overflow substracts
|
||||
- [8e698cab](https://git.meli-email.org/meli/meli/commit/8e698cabcfe58ddd566133ba2c33249c23180a74) Fix unreachable-pub and disjoint-capture lint errors
|
||||
- [40d4ecef](https://git.meli-email.org/meli/meli/commit/40d4ecefa013caaa13af493233c693fb495360ca) Accept invalid (non-ascii) address comment text
|
||||
- [4e654d2d](https://git.meli-email.org/meli/meli/commit/4e654d2d02044be7340b63f1250d37b2ca57b221) Limit LIST ACTIVE command length to 512 octets
|
||||
- [84081f4e](https://git.meli-email.org/meli/meli/commit/84081f4ed7570dd8bcc23d90b9c4cbff55620636) Minor style fix
|
||||
- [97d36868](https://git.meli-email.org/meli/meli/commit/97d3686815c011bb8f1d4e448f12b2294693730d) Use Happy Eyeballs algorithm Ꙭ
|
||||
- [96f0b3e6](https://git.meli-email.org/meli/meli/commit/96f0b3e6b484c9cbb7eaddcaad2b59811b733545) Fix shortcut section order
|
||||
- [64982b4c](https://git.meli-email.org/meli/meli/commit/64982b4cab0b0c2d396cb5dcf7add6f268fd4551) Fix page{up,down} event bubbling up
|
||||
- [8551e1ba](https://git.meli-email.org/meli/meli/commit/8551e1ba0b4fa6d9587bbb249f11e9b80d24e4d3) Fix new 1.72 default clippy lints
|
||||
|
||||
### Changed
|
||||
|
||||
- [8563bccd](https://git.meli-email.org/meli/meli/commit/8563bccd1b6d48dc06dd521f77228c3cbecf7613) Don't cache CellBuffer, only row info
|
||||
- [0f6f3e30](https://git.meli-email.org/meli/meli/commit/0f6f3e30c67f209e0b5e03d2dd2e1e48180d9855) Add IMAP config in config parse test
|
||||
- [ce269c64](https://git.meli-email.org/meli/meli/commit/ce269c64e16db344f0e65461e56dbced2f1a4d64) Don't fail on `server_password_command`
|
||||
- [9dc4d405](https://git.meli-email.org/meli/meli/commit/9dc4d4055cb2f854e835748315677bf4a2db2012) Add focus_{left,right} shortcuts to switch focus
|
||||
- [4b96bd59](https://git.meli-email.org/meli/meli/commit/4b96bd591f18bf7c8a3c922d469b81072d1782a2) Add ColorCache constructor to deduplicate code
|
||||
- [c06c3f58](https://git.meli-email.org/meli/meli/commit/c06c3f589315f017a412f31d80559a5a734d7b89) Draw gap between list and mail view
|
||||
- [c9d26bb4](https://git.meli-email.org/meli/meli/commit/c9d26bb4158e2f423c795f82bcb2c91a0f0c46ec) Add configurable custom hooks with shell commands
|
||||
- [02e86d1f](https://git.meli-email.org/meli/meli/commit/02e86d1fade9faefc14b890e3cec8ed2255bb839) Check for subject overflow on draw
|
||||
- [8cab9d9d](https://git.meli-email.org/meli/meli/commit/8cab9d9da8710257f2b62832bfac802c2a35b368) Add option to hide consecutive identical From values inside a thread
|
||||
- [363f4930](https://git.meli-email.org/meli/meli/commit/363f4930994d1d2e88220878b3848f176b8c5f97) Add {previous,next}_entry shortcuts to quickly open other mail entries
|
||||
- [342df091](https://git.meli-email.org/meli/meli/commit/342df091a076bce1f8477dabbad193312d8cdd67) Don't set all thread to seen when opening a thread entry
|
||||
- [74e15316](https://git.meli-email.org/meli/meli/commit/74e15316dbbf67254023e619924e522f80e77cb9) Open message/rfc822 attachments in subview instead of new tab
|
||||
- [369c1dbd](https://git.meli-email.org/meli/meli/commit/369c1dbdac9842746270a3d3c5bf7ed2205cb644) Show `open` command in status bar
|
||||
- [519257b0](https://git.meli-email.org/meli/meli/commit/519257b08f7029fe71efd2f61ab3a29a4b43b862) Add relative_menu_indices setting for menubar
|
||||
- [8abc9358](https://git.meli-email.org/meli/meli/commit/8abc9358a70465b12a11168be1718ab06479d6e2) Add newline after Version: 1 header
|
||||
- [561ba9c8](https://git.meli-email.org/meli/meli/commit/561ba9c87b57e1012ad89bde08506a2beacb7fff) Add relative_list_indices setting for thread listing
|
||||
- [52874f9a](https://git.meli-email.org/meli/meli/commit/52874f9a97a4799fcff2e14c43cafe9692f21cb6) Cancel previous jobs on MailView drop/update
|
||||
- [9037f084](https://git.meli-email.org/meli/meli/commit/9037f08495894c15a7817594ba91e0d5561c6e69) Replace hardcoded Key::{Home,End} values with shortcut values
|
||||
- [31aa9ad2](https://git.meli-email.org/meli/meli/commit/31aa9ad29e33f285314d0d320a02f00071f61282) Autogen mbox filename when exporting mail to directories
|
||||
|
||||
### Refactoring
|
||||
|
||||
- [330a2b20](https://git.meli-email.org/meli/meli/commit/330a2b20ed492f6b6ea86c196d43d67430487faa) Flush stdout in Ask() after printing
|
||||
- [340d6451](https://git.meli-email.org/meli/meli/commit/340d6451a330861af09fd02231c17ba4168d9654) Add config setting for sidebar ratio
|
||||
- [d0de0485](https://git.meli-email.org/meli/meli/commit/d0de04854ec4770b54e4d8303a9b8ab9eb5d68b0) Add {in,de}crease_sidebar shortcuts
|
||||
- [f5dc25ae](https://git.meli-email.org/meli/meli/commit/f5dc25ae0d5b8d6fb15a534fa49557385d6894d0) Check that all conf flags are recognized in validation
|
||||
- [d3e62e3d](https://git.meli-email.org/meli/meli/commit/d3e62e3d74bdc55872bbdf92c01d18aa00b0affd) Use conf shortcuts for scroll {up, down}
|
||||
- [23c23556](https://git.meli-email.org/meli/meli/commit/23c2355662d589c091dd3c86c8d91c7988eb941c) Fill and align shortcut table columns
|
||||
- [5823178c](https://git.meli-email.org/meli/meli/commit/5823178cc26f66ba902a901522f0506b4348b22e) Add test that looks in source code for invalid theme key references
|
||||
- [9205f3b8](https://git.meli-email.org/meli/meli/commit/9205f3b8afe28ef3a68959d590ed967946a5d622) Handle a per account mail order parameter
|
||||
- [d921b3c3](https://git.meli-email.org/meli/meli/commit/d921b3c3209ff7fe865b5a3b90e20098b3ff211f) Use mail sorting parameters from config
|
||||
- [f4e0970d](https://git.meli-email.org/meli/meli/commit/f4e0970d46e3ec73d684e2ddcc5011f61e87314d) Add ability to kill embed process
|
||||
- [bde87af3](https://git.meli-email.org/meli/meli/commit/bde87af3877d4a0b071e331c93a07e0acf51bf7a) Refactor filter() method in Listing trait
|
||||
- [a42a6ca8](https://git.meli-email.org/meli/meli/commit/a42a6ca868e4590a8b93560737173e80993ecaec) Show notifications in terminal if no alternative
|
||||
- [eb5949dc](https://git.meli-email.org/meli/meli/commit/eb5949dc9bbcf05f86c58b3c93d1066204313e2a) Switch summary<->details identifiers
|
||||
- [8c7b001a](https://git.meli-email.org/meli/meli/commit/8c7b001aa5d4cb6bbaf438f3f47cd91cc2fd6833) Add `thread_subject_pack` command to pack different inner thread subjects in entry title
|
||||
- [388d4e35](https://git.meli-email.org/meli/meli/commit/388d4e35d65f8f770526c4c5f44767c55eda23f8) Add in-progress messages while connecting in IMAP
|
||||
- [787c64c2](https://git.meli-email.org/meli/meli/commit/787c64c2da8af5cc0dafcb92c1d3bea6b54f3659) Remove expect()s from create_config_file()
|
||||
- [b87d54ea](https://git.meli-email.org/meli/meli/commit/b87d54ea3f3f077b6330e798263be6a3d33b3b9c) Impl Into<BTreeSet<EnvelopeHash>> for EnvelopeHashBatch
|
||||
- [e450ad0f](https://git.meli-email.org/meli/meli/commit/e450ad0f9cbc2d215a8f03d2d39260abe19fb5af) Remove unused struct
|
||||
- [c54a31f7](https://git.meli-email.org/meli/meli/commit/c54a31f7cca728eec87f7cd670a4baec37dc919a) Break line for error messages
|
||||
- [7935e49a](https://git.meli-email.org/meli/meli/commit/7935e49a00190cc7f2057abe353739c8dad4f74d) Check properly if mailbox request is an error
|
||||
- [117d7fbe](https://git.meli-email.org/meli/meli/commit/117d7fbe046fe23c400a925ccba7317d8a1d3f08) Make private fields public
|
||||
- [ffb12c6d](https://git.meli-email.org/meli/meli/commit/ffb12c6d1ae9a774de22a25d38bc6714a435c7ad) Make all public struct fields public
|
||||
- [46a038dc](https://git.meli-email.org/meli/meli/commit/46a038dc68093b28b69c3af38de4dd09431efae2) Remove interactive messages when #[cfg(test)]
|
||||
- [803d3414](https://git.meli-email.org/meli/meli/commit/803d3414fd73743ff5bfc0fefe5e3d76d88e58cb) Implement some rfc5804 commands
|
||||
- [b776409d](https://git.meli-email.org/meli/meli/commit/b776409d6c9caec3732bada9e25637c2676af3b8) Add thread, env hash index fields
|
||||
- [cc439b23](https://git.meli-email.org/meli/meli/commit/cc439b239ae27ae84fbcf50fbd82ec591c147c94) Add RowsState struct
|
||||
- [db227dea](https://git.meli-email.org/meli/meli/commit/db227dea34caa747e136500356fddf95a91002e6) Add error messages if `mandoc`,`man` binaries are missing
|
||||
- [ee9d458b](https://git.meli-email.org/meli/meli/commit/ee9d458b05ffa0214a4526daf1423916830526bc) Implement mailbox {un,}sub actions
|
||||
- [7af89359](https://git.meli-email.org/meli/meli/commit/7af893597f5a3f3261bfff47dae0723bf1b17e53) Replace use of Self::DESCRIPTION with Shortcuts struct consts
|
||||
- [eaecc5ea](https://git.meli-email.org/meli/meli/commit/eaecc5ea12f4a5ebe309d5654509c0771bbdc2f1) Remove hardcoded major .so version for non linux/macos target_os
|
||||
- [f63ce388](https://git.meli-email.org/meli/meli/commit/f63ce388f7774ea015fdaa2362202c33f3ddacd4) Move ManageMailboxes to Tab Actions
|
||||
- [3c847ad2](https://git.meli-email.org/meli/meli/commit/3c847ad26afcc4a4cdcfbdbf70f35be57d0da1ab) Add beginning of sieve parser
|
||||
- [5443b7e8](https://git.meli-email.org/meli/meli/commit/5443b7e8f300a0084abde7354360ecbe909178bb) Remove literal_map() parse combinator
|
||||
- [12cb717b](https://git.meli-email.org/meli/meli/commit/12cb717bda186b0ebdda18e2215e30b1426fb08a) Add server_password_command to jmap
|
||||
- [428f752b](https://git.meli-email.org/meli/meli/commit/428f752b20cdb1c8ab01e7f3119001cfafca8ef1) Remove obsolete crate::components::mail::get_display_name()
|
||||
- [91557c2c](https://git.meli-email.org/meli/meli/commit/91557c2c4366b481e80943e94f661c8b47150571) Prevent list blank when refreshing account
|
||||
- [d332e457](https://git.meli-email.org/meli/meli/commit/d332e4578d69c4371418fb2bb3c0d75e1960e01f) Add proper Display impl for HeaderName
|
||||
- [f537c249](https://git.meli-email.org/meli/meli/commit/f537c24909d13a53a95b43e265e4cb4c013334ac) Move text field to its own module
|
||||
- [d33f9d54](https://git.meli-email.org/meli/meli/commit/d33f9d54c708699386a3f32e4056ccab6c68528b) Remove unreachable!() in Key::serialize
|
||||
- [330887c4](https://git.meli-email.org/meli/meli/commit/330887c4f5bad5357508b9fa6f723e45ab307d2a) Introduce imap-codec.
|
||||
- [4da53669](https://git.meli-email.org/meli/meli/commit/4da5366959145e166c40297abfdf1876e5addc50) Remove bincode dep, use serde_json for sqlite3 values
|
||||
- [155fb41b](https://git.meli-email.org/meli/meli/commit/155fb41b93708ef8793250f9dea611bc317a86d5) Remove unused Component::set_id method
|
||||
- [575509f1](https://git.meli-email.org/meli/meli/commit/575509f1edc756ad218bb76cf74460d83009c851) Move mail view to listing parent component
|
||||
- [6858ee1f](https://git.meli-email.org/meli/meli/commit/6858ee1fab3bcddbda7335f49c30f36153e8d4b7) Move subcommand handling to its own module
|
||||
- [b0e867eb](https://git.meli-email.org/meli/meli/commit/b0e867eb68dc3dba96de79f7481989187fa12df4) Move src to meli/src
|
||||
- [48a10f72](https://git.meli-email.org/meli/meli/commit/48a10f724171bfae702b7b40438189adbbe75079) Remove unused BackendOp::fetch_flags() method
|
||||
- [073d43b9](https://git.meli-email.org/meli/meli/commit/073d43b9b869fc9d46c5195c31ad6e7806cf486c) Move data files to data subdir
|
||||
- [1e084c1d](https://git.meli-email.org/meli/meli/commit/1e084c1d854ed7efb2254f9e8d52ac13d8badffa) Move backends out of the backends module
|
||||
- [a5446975](https://git.meli-email.org/meli/meli/commit/a5446975c2423654dea9551474a880e94ebdc006) Move braille and screen to their own module files
|
||||
- [005bf388](https://git.meli-email.org/meli/meli/commit/005bf3881ec59d53e4f16473fb3b1857487dae23) Move components/utilities -> utilities
|
||||
- [64ab65dd](https://git.meli-email.org/meli/meli/commit/64ab65ddffe3341bca775acb2289ee00e771fdb0) Move components/contacts -> contacts
|
||||
- [7c9a4b4b](https://git.meli-email.org/meli/meli/commit/7c9a4b4b7c366c967a3378098d210124712fd293) Move components/mail -> mail
|
||||
- [df638cce](https://git.meli-email.org/meli/meli/commit/df638cceec6016760037b650a77143a07cd1e738) Remove stale failing doc code example
|
||||
- [da8e8104](https://git.meli-email.org/meli/meli/commit/da8e81044833975cadb08db836795a389c142e9c) Remove leftover debug prints
|
||||
- [a1e70061](https://git.meli-email.org/meli/meli/commit/a1e7006186474f55cf4a14f53dbd32bdf8ca5993) Move Sort{Order,Field} to utils mod
|
||||
- [66c21ab1](https://git.meli-email.org/meli/meli/commit/66c21ab1734bfbf4e604da505f6b6109008fd7c2) Move StandardHeader to its own module
|
||||
- [946309c6](https://git.meli-email.org/meli/meli/commit/946309c6f3bbc59b53dc2b05732b40f3d445fd9f) Do some small parser refactoring
|
||||
- [b95f7783](https://git.meli-email.org/meli/meli/commit/b95f778335bebd480f69fe66fabec4f8a6e2b587) Move JmapSession to its own module
|
||||
|
||||
### Documentation
|
||||
|
||||
- [a866b294](https://git.meli-email.org/meli/meli/commit/a866b29499b44032545df4941b6cfec4ee2db8bb) Update valid shortcut entries from src/conf/shortcuts.rs
|
||||
- [f76f4ea3](https://git.meli-email.org/meli/meli/commit/f76f4ea3f7416a4a641d5891f19927aa354a3247) Add meli.7, a general tutorial document
|
||||
- [5fa4b626](https://git.meli-email.org/meli/meli/commit/5fa4b6260c60409579fe964970719f9ab60482cc) Add more screenshots
|
||||
- [7c711542](https://git.meli-email.org/meli/meli/commit/7c7115427dd5f6320a4305df3dc88a8567829720) Complete guide document
|
||||
- [30cc5d3d](https://git.meli-email.org/meli/meli/commit/30cc5d3d0220452630780c3238f393b9e1f2b93a) Add edit-config in manpages
|
||||
- [24103f33](https://git.meli-email.org/meli/meli/commit/24103f3310ca533791bdd07643fdb23a10c6031d) Add external-tools.md document
|
||||
- [b6c93e49](https://git.meli-email.org/meli/meli/commit/b6c93e49f2af3001b206a288edea02c58e14aa5b) Add use_tls option in IMAP connection settings
|
||||
- [34a54d3c](https://git.meli-email.org/meli/meli/commit/34a54d3c05efc3b56154179111c3e39e0f3fd8b1) Add some `TODO([#222](https://git.meli-email.org/meli/meli/issues/222))`s.
|
||||
|
||||
### Packaging
|
||||
|
||||
- [671ce9f6](https://git.meli-email.org/meli/meli/commit/671ce9f694a8e941826472caad8051998540bb1f) Add missing build dependencies
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- [25805229](https://git.meli-email.org/meli/meli/commit/2580522931fb29442598ac8932a13eaeb577bace) Log vcard parsing failures
|
||||
- [5f003a31](https://git.meli-email.org/meli/meli/commit/5f003a31be95a3877d1006f8a22e424a1183163d) Parse vCards with just LF instead of CRLF line endings
|
||||
- [d8e9a005](https://git.meli-email.org/meli/meli/commit/d8e9a00563c023abb0ff75aaa4ba3fa92626c5ce) Add quoted REFERENCES field in parsing of responses
|
||||
- [81d12656](https://git.meli-email.org/meli/meli/commit/81d1265601c299dee6405f3f9b4e81f89d3cfe29) Escape IMAP passwords properly
|
||||
- [0d8bedd2](https://git.meli-email.org/meli/meli/commit/0d8bedd2d5d3eb8eee831e75d1e14d45beefb847) Make is_online() await for connection
|
||||
- [d4b690d5](https://git.meli-email.org/meli/meli/commit/d4b690d5d3a7f6a6b57afd7a6177db0db20a9c94) Send password as byte literal on LOGIN
|
||||
- [2eb22a29](https://git.meli-email.org/meli/meli/commit/2eb22a290abb3f37bc77c3bc2771edfb60a1c314) Stop hardcoding certain component colors
|
||||
- [2c23ca34](https://git.meli-email.org/meli/meli/commit/2c23ca34cdee769a0f78a0b0ef934e5f20dd9567) Update most Cargo dependencies
|
||||
- [721891c2](https://git.meli-email.org/meli/meli/commit/721891c2955e9f5e223949bde2dd43604cec8390) Update nom dependency
|
||||
- [4fdc90b3](https://git.meli-email.org/meli/meli/commit/4fdc90b31ea56c046dfe5bf9bee0a118f9c03db1) Use `open` instead of `xdg-open` in macos
|
||||
- [9558b2ae](https://git.meli-email.org/meli/meli/commit/9558b2ae921aa35076f58d68b5898334a2797685) Parse Cp1253 as windows1253 encoding
|
||||
- [6a843d49](https://git.meli-email.org/meli/meli/commit/6a843d49830f8c70f510c4232ea63eb204d35319) Export list_mail_in_maildir_fs() function
|
||||
- [d6355a30](https://git.meli-email.org/meli/meli/commit/d6355a3043ec0b4b2a3e1c3fbb0ed66d2e87e7f4) Impl Debug for ParsingError
|
||||
- [dc5afa13](https://git.meli-email.org/meli/meli/commit/dc5afa13dbea4da042c35e12291c5b5a2846c3ff) Use osascript/applescript for notifications on macos
|
||||
- [e6d6e1f5](https://git.meli-email.org/meli/meli/commit/e6d6e1f588db9793e822cdbb1ce2edb2959170c6) Don't unwrap if pseudoterminal creation fails
|
||||
- [ca84906d](https://git.meli-email.org/meli/meli/commit/ca84906d7ddb1351643998efaa56086e3ba9cf8e) Escape all quotes in applescript on macos
|
||||
- [4a79b202](https://git.meli-email.org/meli/meli/commit/4a79b2021d2fb3edd046197b44b702bdb468fc5e) Update dependency versions
|
||||
- [e29041f7](https://git.meli-email.org/meli/meli/commit/e29041f73354c59ef95916edd75e6ca7876e3c3a) Rename src/bin.rs to src/main.rs
|
||||
- [7650805c](https://git.meli-email.org/meli/meli/commit/7650805c60cec2fe09cd2a59cb665731f5cca140) Bring stripped binary size down to 7MiB
|
||||
- [ca488968](https://git.meli-email.org/meli/meli/commit/ca48896865778df2c79bc1d13f03b5f56136304c) Add strip option to profile.release
|
||||
- [10497952](https://git.meli-email.org/meli/meli/commit/10497952f718b49f3a247741a64361f855b2d4f7) Wrap stdout in BufWriter
|
||||
- [29042aba](https://git.meli-email.org/meli/meli/commit/29042aba593210f3be73010908d5092951b3b1a1) Add mbox date format parse
|
||||
- [480000eb](https://git.meli-email.org/meli/meli/commit/480000ebbb67a80181fd27762ca649acf13df0f3) Show error if account directory does not contain ".notmuch" subdirectory
|
||||
- [a484b397](https://git.meli-email.org/meli/meli/commit/a484b397c68fd126c17073ac9c9f02432c413341) Show informative error messages if libloading fails
|
||||
- [4a20fc42](https://git.meli-email.org/meli/meli/commit/4a20fc42e1f5cad325d5aa439d1baab210aceed8) Update CHANGELOG.md
|
||||
- [a72c96a2](https://git.meli-email.org/meli/meli/commit/a72c96a26afe9e54a0fcadb8c43448f1fdc09ce9) Add 8BITMIME support to smtp client
|
||||
- [3c0f5d82](https://git.meli-email.org/meli/meli/commit/3c0f5d8274d8039b1a2c928f99194835bca7b83a) Add BINARYMIME support to smtp client
|
||||
- [36883692](https://git.meli-email.org/meli/meli/commit/36883692782ed2355a0ec12ccf9f82aa2edcc8c1) Add smtp test
|
||||
- [9cbbf71e](https://git.meli-email.org/meli/meli/commit/9cbbf71e0f8f9115e9e043982f20045cfc550eb7) Add DecodeOptions struct for decoding
|
||||
- [0df46a63](https://git.meli-email.org/meli/meli/commit/0df46a63ec6e30983480f0eb50c8da3f74b4f0b3) Show error if sqlite3 search backend is set but doesn't exist
|
||||
- [a7a50d30](https://git.meli-email.org/meli/meli/commit/a7a50d3078cb7466ab341ddfc30a80c7b1f8dfdb) Box<_> some large fields in biggest types
|
||||
- [d8d43a16](https://git.meli-email.org/meli/meli/commit/d8d43a16fef045a2116ff126e7b6e27817b526fc) Add html_open config setting
|
||||
- [0ed10711](https://git.meli-email.org/meli/meli/commit/0ed10711ef542cc13eaaef809fa557468b3d6696) Add new_mail_script option
|
||||
- [c3fdafde](https://git.meli-email.org/meli/meli/commit/c3fdafde3b69c0abc78a62926e0c32fc3dd602d6) Documentation touchups
|
||||
- [347be543](https://git.meli-email.org/meli/meli/commit/347be54305c60350b055a1da3a1abfa4d33d3f22) Add NetworkErrorKind enum
|
||||
- [0c08cb73](https://git.meli-email.org/meli/meli/commit/0c08cb737ceaa5c738712905c7d57f956d449ed0) Mark mailboxes as subscribed on personal accounts
|
||||
- [129573e0](https://git.meli-email.org/meli/meli/commit/129573e0fd9b42ebf14c2de176e65b92bf8479bd) Rename root_path to root_mailbox
|
||||
- [7e09b180](https://git.meli-email.org/meli/meli/commit/7e09b1807ffa9bae54da35b02c83b5aaee455819) Replace _Ref deref unwraps with expect()
|
||||
- [55ed9624](https://git.meli-email.org/meli/meli/commit/55ed962425ba25d2317946705ff6861a77eb770f) Use server_url instead of server_hostname + server_port in config
|
||||
- [0ef4dde9](https://git.meli-email.org/meli/meli/commit/0ef4dde9392452f7cf7f18294f747fc6e0babb8d) Wrap serde_json deserialize errors in human readable errors
|
||||
- [dd0baa82](https://git.meli-email.org/meli/meli/commit/dd0baa82e9789da23c8f9b06925776c7f80e2568) Spawn user-given command strings with sh -c ".."
|
||||
- [3697b7d9](https://git.meli-email.org/meli/meli/commit/3697b7d960cc9dbe602fa84f861cea854b600b73) Don't use LC_ category in place of LC_ masks in libc calls
|
||||
- [6d20abdd](https://git.meli-email.org/meli/meli/commit/6d20abdde7b4cec6ec1af7c097f01042ea05cfbb) Add #[allow(deref_nullptr)] in bindgen tests
|
||||
- [17b42b1a](https://git.meli-email.org/meli/meli/commit/17b42b1a6c721fb2e369c2a300867c8db2beb959) Add json deserialization tests
|
||||
- [64346dd3](https://git.meli-email.org/meli/meli/commit/64346dd3fe0ef40025ec6fdb01d18eb38f7e7f65) Add map_res, quoted_slice, is_a, alt, take, take_literal
|
||||
- [56fc43bc](https://git.meli-email.org/meli/meli/commit/56fc43bcf869a867455b44d007b9d3d17422bc8d) Add As{Ref,Mut} impls for RwRef{,Mut}
|
||||
- [63179841](https://git.meli-email.org/meli/meli/commit/631798413659a320dcd9574e0bca7b7d75cc8d6c) Add --bin flag to meli cargo build target
|
||||
- [ded9adde](https://git.meli-email.org/meli/meli/commit/ded9adde614ac3d38045fa97a0f5144b80855fe7) More descriptive "Unimplemented" messages
|
||||
- [2224a710](https://git.meli-email.org/meli/meli/commit/2224a7100f9bc6c44bc66117a88556003e74186e) Reset imap cache on init error
|
||||
- [252d2bdf](https://git.meli-email.org/meli/meli/commit/252d2bdf2f12c8954f8b299000bbde6219d25335) Replace hardcoded /bin/false with 'false'
|
||||
- [2427b097](https://git.meli-email.org/meli/meli/commit/2427b097c5c40f3212a105cb40f913c9860ae2a8) Make tag_default background lighter on light theme
|
||||
- [7382e301](https://git.meli-email.org/meli/meli/commit/7382e30160a934ce97dd73c1be44640d5b4a4c75) Convert EnvelopeHash from typedef to wrapper struct
|
||||
- [259aeb00](https://git.meli-email.org/meli/meli/commit/259aeb00877557ee85b5cc555d50e605b85b3109) Convert {Account,Mailbox}Hash from typedef to wrapper struct
|
||||
- [5634f955](https://git.meli-email.org/meli/meli/commit/5634f9555315deb2d39ed8fce577a35f4d535ac1) Rename MeliError struct to Error
|
||||
- [7606317f](https://git.meli-email.org/meli/meli/commit/7606317f24d076bdc7db873c2b15811728ed946a) Add support for virtual mailbox hierarchy
|
||||
- [2878bbb8](https://git.meli-email.org/meli/meli/commit/2878bbb8c887275d26264bf7201a632161c4048a) Add parser for mutt alias file
|
||||
- [de2f46fe](https://git.meli-email.org/meli/meli/commit/de2f46fe611726a445c1e06cbc35343e716aa335) Rustfmt changes
|
||||
- [f9ac9b60](https://git.meli-email.org/meli/meli/commit/f9ac9b607a2bd01e42c81cfab3c933df28ff1676) Temporarily disable libgpgme functions because of a bug
|
||||
- [256a3e25](https://git.meli-email.org/meli/meli/commit/256a3e252e2e4db9af9a04c7df1a52eeaf2bbfc9) Update minimum supported rust version
|
||||
- [fbc1007f](https://git.meli-email.org/meli/meli/commit/fbc1007ff4f41bac888a1b53c156feec4f795403) Deserialize `null` to empty vec for messageId
|
||||
- [d7ec97f0](https://git.meli-email.org/meli/meli/commit/d7ec97f03bc0e815e160a142f871dc764d416af1) Small rustfmt change
|
||||
- [2447a2cb](https://git.meli-email.org/meli/meli/commit/2447a2cbfeaa8d6f7ec11a2a8a6f3be1ff2fea58) Avoid relying on hardcoded hash values
|
||||
- [d679a744](https://git.meli-email.org/meli/meli/commit/d679a74450b35724301c81da1644bcedb1c54045) Implement Bearer token authentication
|
||||
- [47e6d5d9](https://git.meli-email.org/meli/meli/commit/47e6d5d935a2b5124efbe847dac885b859200469) Add edit-config CLI subcommand that opens config files on EDITOR
|
||||
- [3a02b6fb](https://git.meli-email.org/meli/meli/commit/3a02b6fb8024e6bb046fc167e7527aad1b192202) Mention how to override w3m with html_filter
|
||||
- [85d4316a](https://git.meli-email.org/meli/meli/commit/85d4316a6a8703ac3e4923cf99ce8c4bb22bb4ae) Replace old logging module with the `log` create
|
||||
- [1f1ea307](https://git.meli-email.org/meli/meli/commit/1f1ea307698a5a7f62f5ab2ea1594aef4d8f48a8) On draw() set dirty on return
|
||||
- [77020e0c](https://git.meli-email.org/meli/meli/commit/77020e0c19873b8053321132ff5b58181c567fcd) Update CHANGELOG.md
|
||||
- [682ea554](https://git.meli-email.org/meli/meli/commit/682ea5547e380deeb215503b39c8aa66c65b3cac) Add `.idea` (CLion) to `.gitignore`.
|
||||
- [f63f6445](https://git.meli-email.org/meli/meli/commit/f63f6445addeccee1a6b830f1c101a043612ea4e) Improve error message when `m4` executable is missing.
|
||||
- [cc27639f](https://git.meli-email.org/meli/meli/commit/cc27639fca0dcb3a5ff9fceef8666dbbf047adaa) Use Envelope attachments when editing and don't add already existing headers
|
||||
- [30866f75](https://git.meli-email.org/meli/meli/commit/30866f752b21802b64ce7d2e02c9962c1091c9d8) Bypass rustfmt bug.
|
||||
- [235fceaf](https://git.meli-email.org/meli/meli/commit/235fceaf2168af50c3804cecfbf69e64ff42598c) Add standard heeder constants in email::headers
|
||||
- [aebff3d3](https://git.meli-email.org/meli/meli/commit/aebff3d3d9864b8854aba5e7f43a61d515e8057f) Implement mailto RFC properly
|
||||
- [954329d8](https://git.meli-email.org/meli/meli/commit/954329d848a5b3e73fca50ed1db9859118bed6dd) Set file extensions to temp files, use `open` in macos
|
||||
- [58889bca](https://git.meli-email.org/meli/meli/commit/58889bcadd44d6aec2eddd17cf5ecb1e07531cbe) Add show_extra_headers option
|
||||
- [23d95973](https://git.meli-email.org/meli/meli/commit/23d95973d4f574fe431441df97ceaef0e3e4762f) Add search.rs module
|
||||
- [6bf1756d](https://git.meli-email.org/meli/meli/commit/6bf1756de844386ba312d15109ae29951896147b) Implement more search criteria in Query type
|
||||
- [299c8e0f](https://git.meli-email.org/meli/meli/commit/299c8e0f993c4ac88005a5c9e708d9e214b20ac1) Restructure pub use melib::* imports
|
||||
- [f8623d4b](https://git.meli-email.org/meli/meli/commit/f8623d4b2c386f51f1d11a23900503d8165ac9f3) Implement more ResponseCode cases
|
||||
- [b92a80a2](https://git.meli-email.org/meli/meli/commit/b92a80a23afb96fbd63031704e4656cc8a00526c) Resync even if UIDVALIDITY is missing from cache
|
||||
- [bf615e7d](https://git.meli-email.org/meli/meli/commit/bf615e7d933b474942d421eafc1015aeb28f8516) Check for case when envelope has its own message id in References and In-Reply-To
|
||||
- [e0257c9d](https://git.meli-email.org/meli/meli/commit/e0257c9d8d6f234f71852a0080d443b063d5e6d7) Run cargo-sort
|
||||
- [d7e6b40b](https://git.meli-email.org/meli/meli/commit/d7e6b40b7e1f501fdaaba54880e9c7a4b0e01288) Auto re-index sqlite3 database if it's missing
|
||||
- [cd85d833](https://git.meli-email.org/meli/meli/commit/cd85d83324a009ea4b86ac22af395145a9e999ab) Replace timestamp with Date value in message/rfc822 Display
|
||||
- [579372b4](https://git.meli-email.org/meli/meli/commit/579372b4a75e39c9e84010de16d7d46294bed04a) Improve readability of `Envelope`.
|
||||
- [6c6d9f4b](https://git.meli-email.org/meli/meli/commit/6c6d9f4b4e0d16b5a73ae8e2a2fb2a6f124df7e6) Improve ordering of `flag_impl!`s.
|
||||
- [8f14a237](https://git.meli-email.org/meli/meli/commit/8f14a2373e16b9b4af22f9388fae84235dd08123) Put imap-codec logic under the imap_backend feature
|
||||
- [fd0faade](https://git.meli-email.org/meli/meli/commit/fd0faade066a18466e683361211bba569956bf63) Add connection instance id string for debugging in logs
|
||||
- [5c9b3fb0](https://git.meli-email.org/meli/meli/commit/5c9b3fb0448fa3689ff33faba3dde03c49347f61) Impl Component for Box<dyn Component>
|
||||
- [45bac6eb](https://git.meli-email.org/meli/meli/commit/45bac6eb16a5a093193d5beb4d80040ce161304a) Tidy up use of debug!
|
||||
- [5699baec](https://git.meli-email.org/meli/meli/commit/5699baecfba9cb15aac04a6b400cfb6bc881e2c5) Add utils::{futures, random}
|
||||
- [b05d9299](https://git.meli-email.org/meli/meli/commit/b05d92997546e438b202d336fc581c2514c63b9f) Impl exponential backoff when retrying connection
|
||||
- [f5cfbd32](https://git.meli-email.org/meli/meli/commit/f5cfbd32e6ebbe83ad7e84d048f1fbf2e51ca605) On set_flags, update {un,}seen sets in all mailboxes
|
||||
- [f0d88005](https://git.meli-email.org/meli/meli/commit/f0d88005fbabcd552593ba0fe785e89a3560ac1c) Change message/rfc822 Display repr
|
||||
- [f98e36ce](https://git.meli-email.org/meli/meli/commit/f98e36cee514f643e0fe256857cf31e2e0f24080) Replace old-style /*! module doc comments with //!
|
||||
- [1bcc0bbe](https://git.meli-email.org/meli/meli/commit/1bcc0bbece2f479950e8811261befedc0199dab9) Add mbox parsing test
|
||||
- [619fbef1](https://git.meli-email.org/meli/meli/commit/619fbef129e249489e64a26e1d0dfbd02db2516a) Recursively calculate update_show_subject()
|
||||
- [957abf4e](https://git.meli-email.org/meli/meli/commit/957abf4e7238ec74b2194a21533b69dd1a58c0a8) Update cargo dependencies
|
||||
- [9d51b6bd](https://git.meli-email.org/meli/meli/commit/9d51b6bd525784bc108959519c8dd21d30a8b020) Update `imap-codec`.
|
||||
- [7c33f899](https://git.meli-email.org/meli/meli/commit/7c33f8999b6a5efd911680f2b83a3ff3a682a715) Use published imap-codec 0.10.0.
|
||||
- [3803d788](https://git.meli-email.org/meli/meli/commit/3803d788abc5157b9cc6368da7e54aced9604aec) If auth is false checks if config has password entry
|
||||
- [866166eb](https://git.meli-email.org/meli/meli/commit/866166eb8e8b994c8c87aad92a3303f9f6449b2d) Don't print parsing error for empty bytes
|
||||
- [5b5869a2](https://git.meli-email.org/meli/meli/commit/5b5869a2ec3fce2fc69aa5c83fbda7a767f2a402) Re-enable print to stderr ifdef MELI_DEBUG_STDERR
|
||||
- [13fe64a0](https://git.meli-email.org/meli/meli/commit/13fe64a027895780efdb6bfee246d562741a4be1) Cache pgp signature verification results
|
||||
- [5ceddf41](https://git.meli-email.org/meli/meli/commit/5ceddf412e3b215b712e55aea8e18887d2d39f1a) Update CHANGELOG.md
|
||||
- [4e55fbc9](https://git.meli-email.org/meli/meli/commit/4e55fbc90d8b105788c7c5998cb26b2829ac87a2) Add SEEN flag to all envs, since NNTP has no flags
|
||||
- [e9cd800f](https://git.meli-email.org/meli/meli/commit/e9cd800f49e2d0e155d434ff8e91462e20b9d4f5) Add support for storing read status locally
|
||||
- [53cba4be](https://git.meli-email.org/meli/meli/commit/53cba4beee4f774b548881c1a3f207ca391d3df3) Update README.md relative file paths
|
||||
- [c4c245ee](https://git.meli-email.org/meli/meli/commit/c4c245ee19137f64d836401f7c1de17c9eb42b6e) Respect danger_accept_invalid_certs setting
|
||||
- [29b43e2c](https://git.meli-email.org/meli/meli/commit/29b43e2c88edcfdecffd076fbb773c8547425f12) Replace mktime with timegm
|
||||
- [4874e30f](https://git.meli-email.org/meli/meli/commit/4874e30f3ce9b186ac7cd427cba4a8542bd5048e) Add smtp-trace feature
|
||||
- [51e9fbe8](https://git.meli-email.org/meli/meli/commit/51e9fbe8f2c380f3c9ee6a9ee65e638c169b43ef) Add account_name identifier to sqlite3 index database name
|
||||
- [129f1091](https://git.meli-email.org/meli/meli/commit/129f10911b01641940801586bfa5286307e4342f) Rename `imap_backend` feature to `imap`
|
||||
- [fe027fa3](https://git.meli-email.org/meli/meli/commit/fe027fa300a9882730a558fffe6000527ef08ff8) Rename `maildir_backend` feature to `maildir`
|
||||
- [fe7dcc50](https://git.meli-email.org/meli/meli/commit/fe7dcc508ee51f492df2de3884147531fada6f4e) Rename `notmuch_backend` feature to `notmuch`
|
||||
- [e9f09a15](https://git.meli-email.org/meli/meli/commit/e9f09a153ca0a1a023efe924b314ea977ccc3c25) Rename `mbox_backend` feature to `mbox`
|
||||
- [7db930ca](https://git.meli-email.org/meli/meli/commit/7db930cabd295e888f4f106d5e7ea411521340ff) Rename `jmap_backend` feature to `jmap`
|
||||
- [89c90f22](https://git.meli-email.org/meli/meli/commit/89c90f224a68ec524f7dc7033955ce7b8196f493) Add `nntp` feature
|
||||
- [b65934fa](https://git.meli-email.org/meli/meli/commit/b65934facc7aeeb8ab30603e16cef2b747f9a0e5) Add nntp-trace feature
|
||||
- [8ecdb6df](https://git.meli-email.org/meli/meli/commit/8ecdb6df3189cae4b6fa21a177bde756cc4407cf) Add imap-trace feature
|
||||
- [9216e7bc](https://git.meli-email.org/meli/meli/commit/9216e7bc657738ae9861583a837c1326398197e4) Add opt id string for tracing
|
||||
- [ae25ffba](https://git.meli-email.org/meli/meli/commit/ae25ffba430572efe73fde05eaf8111453f814cf) Don't do plain EHLO before starting Tls connection
|
||||
- [8cb2a515](https://git.meli-email.org/meli/meli/commit/8cb2a515e1ba31efe914db67504993bc081ed7f3) Use localhost in lieu of 127.0.0.1 for CI
|
||||
- [0ee1b6e0](https://git.meli-email.org/meli/meli/commit/0ee1b6e01830c01871e93e27d735a39792202325) Start background watch job in init
|
||||
- [448e0635](https://git.meli-email.org/meli/meli/commit/448e0635e00b533a4d9dc15ba65982097649b397) Log error when command length exceeds 512 octets
|
||||
- [bf543855](https://git.meli-email.org/meli/meli/commit/bf543855dc143b25344b79303f017380c9773793) Add PartialEq<str> for MessageID
|
||||
- [7c7f6e19](https://git.meli-email.org/meli/meli/commit/7c7f6e1923e8b3127cf7cbd4b18f1db3ed9c6583) Don't increase Thread length for duplicates
|
||||
- [5c2b0471](https://git.meli-email.org/meli/meli/commit/5c2b04719b953373c6a657f22db295d08b94685e) Normalize std::fmt::* imports
|
||||
- [0f60009e](https://git.meli-email.org/meli/meli/commit/0f60009ea909adfb8f4e85d942decb8bc60f7539) Add RUSTFLAGS with -D warnings
|
||||
- [6578a566](https://git.meli-email.org/meli/meli/commit/6578a5666889434ed6ca2f276e365633956fe3d3) Update cargo install directions
|
||||
- [4f6081b6](https://git.meli-email.org/meli/meli/commit/4f6081b6633aed1eeafd99c24aa2dc64397043ca) Update to `imap-codec 1.0.0-beta`.
|
||||
- [dc2b0044](https://git.meli-email.org/meli/meli/commit/dc2b00442b04c21455a6fda59b4729d0cbd04eff) Run rustfmt and cargo-sort
|
||||
- [b3858de2](https://git.meli-email.org/meli/meli/commit/b3858de2f4e12723ee922174c79cc36062bed54e) Impl From<io::ErrorKind> for ErrorKind
|
||||
- [f93adb68](https://git.meli-email.org/meli/meli/commit/f93adb683a562f25e40ffa03f80d04d5ad8ca34f) Replace change_color uses with change_theme
|
||||
- [f193bdf6](https://git.meli-email.org/meli/meli/commit/f193bdf685e06652ab5b2da2a9a01fa56620cda6) Add column headers and sorting
|
||||
- [095d24f9](https://git.meli-email.org/meli/meli/commit/095d24f91447a2ecab6d6bc78e1705ea4394e9bd) Add PULL_REQUEST_TEMPLATE.md
|
||||
- [ab57e942](https://git.meli-email.org/meli/meli/commit/ab57e9420db29efd42773e970f33751b7b3f6f26) Add delete_contact shortcut
|
||||
- [3963103d](https://git.meli-email.org/meli/meli/commit/3963103d55db28f789fe39f0dd80cd0d57792b5d) Prevent duplicate contact creation
|
||||
- [f162239f](https://git.meli-email.org/meli/meli/commit/f162239fcc87d9c4f8aba8c33a9812a5e691c8d9) Change `on:` conditions for test.yaml
|
||||
- [974b3a53](https://git.meli-email.org/meli/meli/commit/974b3a53058181e3df992a2105abcbf1c392fc19) Update bitflags, rusqlite dependencies
|
||||
- [4d22b669](https://git.meli-email.org/meli/meli/commit/4d22b669bf330f8f3168fc2f704ad63c21c5e821) Update dependencies
|
||||
- [ffba203a](https://git.meli-email.org/meli/meli/commit/ffba203a3b7070cc9e71d9444556e108ff0e18ea) Add support for Home and End key navigation
|
||||
- [3433f7c4](https://git.meli-email.org/meli/meli/commit/3433f7c41e0d0cbb48af821280537da41b9e53d0) Update PULL_REQUEST_TEMPLATE.md
|
||||
- [f7a4741b](https://git.meli-email.org/meli/meli/commit/f7a4741bf1622ae60042fb6ab0a906fe50fb1e06) Add jmap-trace feature
|
||||
- [c875dda4](https://git.meli-email.org/meli/meli/commit/c875dda4960e5688b17176ba82ad1e5da38b883b) Add last_method_response field to Connection
|
||||
- [37a787e6](https://git.meli-email.org/meli/meli/commit/37a787e6bb5abd34fae2888944537dec1ee3842f) Use IndexMap instead of HashMap
|
||||
- [6ebdc7f9](https://git.meli-email.org/meli/meli/commit/6ebdc7f9aec5531c2b562a4e0cfd320ead6a4c01) Add Id<_>::empty() contructor
|
||||
- [4f9b9773](https://git.meli-email.org/meli/meli/commit/4f9b97736a4af8b8b4ba0017ad1175a1c2352db6) Rename EmailImport to EmailImportObject
|
||||
- [11432ba2](https://git.meli-email.org/meli/meli/commit/11432ba2c381b07bb540f7f92664b3c351e3cf62) Make `null` fields into Option<_>s
|
||||
- [d9467d5f](https://git.meli-email.org/meli/meli/commit/d9467d5fcd9543611ec8a034eb7e25d12a3dcc45) Save all core capabilities to session store
|
||||
- [31982931](https://git.meli-email.org/meli/meli/commit/31982931f5f472717b4c3d900f16c0588682f48e) Use Argument<OBJ> (value or resultreference) where appropriate
|
||||
- [29fd8522](https://git.meli-email.org/meli/meli/commit/29fd8522e6bc2b0b6196cb97c8868dc34c2ba2f0) Implement Backend::create_mailbox()
|
||||
- [5d8f07c8](https://git.meli-email.org/meli/meli/commit/5d8f07c8058261c7c251b3fb010ad866110e91df) Rename some objects better
|
||||
- [38bc1369](https://git.meli-email.org/meli/meli/commit/38bc1369cc136c482f48d1ed3172b7f510ff7762) Add an Identity type.
|
||||
- [59513b26](https://git.meli-email.org/meli/meli/commit/59513b267097cac8fe757c6198f26e0179014604) Implement Backend::submit(), server-side submission
|
||||
- [5459a84f](https://git.meli-email.org/meli/meli/commit/5459a84f3d2b4c91a89252fba63f4ef12d965b9b) Update to imap-codec 1.0.0 (w/o `-beta`)
|
||||
- [290cfb86](https://git.meli-email.org/meli/meli/commit/290cfb86c0c942690c48a0d3298e9d2de3ec4d94) Add a highlighted_selected theme key
|
||||
- [46636d87](https://git.meli-email.org/meli/meli/commit/46636d8748f2779f38a10c6bf38c4e07acf16f8a) Bump version to 0.8.0
|
||||
|
||||
### Continuous Integration
|
||||
|
||||
- [1d0405ed](https://git.meli-email.org/meli/meli/commit/1d0405ed5b5cd76f4fe79e73fb30f4d4dce1d441) Add env vars
|
||||
- [6e27edcb](https://git.meli-email.org/meli/meli/commit/6e27edcb775ce831b784d2040672f2d2af2c020f) Use cargo-nextest
|
||||
- [67d2da0f](https://git.meli-email.org/meli/meli/commit/67d2da0f88b0e7b9b74c5d05c6c17a45057b094a) Disable smtp::test::test_smtp in test.yaml
|
||||
- `a42a6ca8` show notifications in terminal if there is no other alternative.
|
||||
|
||||
## [alpha-0.7.2] - 2021-10-15
|
||||
|
||||
|
@ -591,7 +86,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- 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
|
||||
|
@ -726,8 +221,3 @@ Notable changes:
|
|||
[alpha-0.7.0]: https://github.com/meli/meli/releases/tag/alpha-0.7.0
|
||||
[alpha-0.7.1]: https://github.com/meli/meli/releases/tag/alpha-0.7.1
|
||||
[alpha-0.7.2]: https://github.com/meli/meli/releases/tag/alpha-0.7.2
|
||||
[v0.8.0]: https://git.meli-email.org/meli/meli/releases/tag/v0.8.0
|
||||
[v0.8.1]: https://git.meli-email.org/meli/meli/releases/tag/v0.8.1
|
||||
[v0.8.2]: https://git.meli-email.org/meli/meli/releases/tag/v0.8.2
|
||||
[v0.8.3]: https://git.meli-email.org/meli/meli/releases/tag/v0.8.3
|
||||
[v0.8.4]: https://git.meli-email.org/meli/meli/releases/tag/v0.8.4
|
||||
|
|
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 -->
|
||||
<!-- ``` -->
|
133
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 = /usr/bin/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
|
||||
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,7 +48,6 @@ 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
|
||||
|
@ -59,12 +55,12 @@ meli: check-deps
|
|||
|
||||
.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)"
|
||||
|
@ -77,31 +73,29 @@ help:
|
|||
@echo " - ${BOLD}build-rustdoc${ANSI_RESET} (builds rustdoc documentation for all packages in \$$CARGO_TARGET_DIR)"
|
||||
@echo "\nENVIRONMENT variables of interest:"
|
||||
@echo "* PREFIX = ${UNDERLINE}${EXPANDED_PREFIX}${ANSI_RESET}"
|
||||
@echo "* MELI_FEATURES = ${UNDERLINE}\n"
|
||||
@[ -z $${MELI_FEATURES+x} ] && echo "unset\c" || echo ${MELI_FEATURES}'\c'
|
||||
@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 "* MANPATH = ${UNDERLINE}\c"
|
||||
@[ $${MANPATH+x} ] && echo $${MANPATH}'\c' || echo "unset\c"
|
||||
@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}"
|
||||
@echo "* NO_MAN ${UNDERLINE}\c"
|
||||
@[ $${NO_MAN+x} ] && echo "set\c" || echo "unset\c"
|
||||
@echo -n "* NO_MAN ${UNDERLINE}"
|
||||
@[ $${NO_MAN+x} ] && echo -n "set" || echo -n "unset"
|
||||
@echo ${ANSI_RESET}
|
||||
@echo "* NO_COLOR ${UNDERLINE}\c"
|
||||
@[ $${NO_COLOR+x} ] && echo "set\c" || echo "unset\c"
|
||||
@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}"
|
||||
@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_COLOR = ${CARGO_COLOR}"
|
||||
|
||||
.PHONY: check
|
||||
check: check-tagrefs
|
||||
@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:
|
||||
|
@ -110,19 +104,11 @@ fmt:
|
|||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
@RUSTFLAGS='${RUSTFLAGS}' $(CARGO_BIN) clippy --no-deps --all-features --all --tests --examples --benches --bins
|
||||
@$(CARGO_BIN) clippy --no-deps --all-features --all --tests --examples --benches --bins
|
||||
|
||||
.PHONY: test
|
||||
test: test-docs
|
||||
@RUSTFLAGS='${RUSTFLAGS}' ${CARGO_BIN} test ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" --all --tests --examples --benches --bins
|
||||
|
||||
.PHONY: test-docs
|
||||
test-docs:
|
||||
@RUSTFLAGS='${RUSTFLAGS}' ${CARGO_BIN} test ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" --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:
|
||||
|
@ -135,32 +121,30 @@ clean:
|
|||
-rm -rf ./${CARGO_TARGET_DIR}/
|
||||
|
||||
.PHONY: distclean
|
||||
distclean:
|
||||
distclean: clean
|
||||
@rm -f meli-${VERSION}.tar.gz
|
||||
@rm -rf .pc # rm debian stuff
|
||||
|
||||
.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)
|
||||
|
@ -170,7 +154,7 @@ install-bin: meli
|
|||
@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}
|
||||
|
@ -183,11 +167,10 @@ install-bin: meli
|
|||
.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:
|
||||
|
@ -196,53 +179,9 @@ dist:
|
|||
|
||||
.PHONY: deb-dist
|
||||
deb-dist:
|
||||
@author=$(grep -m1 authors meli/Cargo.toml | head -n1 | cut -d'"' -f 2 | head -n1)
|
||||
@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:
|
||||
@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-)
|
||||
#}
|
||||
|
|
214
README.md
|
@ -1,153 +1,137 @@
|
|||
# meli ![Established, created in 2017](https://img.shields.io/badge/Est.-2017-blue) ![Minimum Supported Rust Version](https://img.shields.io/badge/MSRV-1.68.2-blue) [![GitHub license](https://img.shields.io/github/license/meli/meli)](https://github.com/meli/meli/blob/master/COPYING) [![Crates.io](https://img.shields.io/crates/v/meli)](https://crates.io/crates/meli) [![IRC channel](https://img.shields.io/badge/irc.oftc.net-%23meli-blue)](ircs://irc.oftc.net:6697/%23meli)
|
||||
# meli [![GitHub license](https://img.shields.io/github/license/meli/meli)](https://github.com/meli/meli/blob/master/COPYING) [![Crates.io](https://img.shields.io/crates/v/meli)](https://crates.io/crates/meli)
|
||||
|
||||
**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 online 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/)
|
||||
* Repository:
|
||||
- Main <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")
|
||||
- Official mirror <https://codeberg.org/meli/meli>
|
||||
- Official mirror <https://github.com/meli/meli>
|
||||
| | | |
|
||||
:---:|:---:|:---:
|
||||
![Main view screenshot](./docs/screenshots/main.webp "mail meli view screenshot") | ![Compact main view screenshot](./docs/screenshots/compact.webp "compact main view screenshot") | ![Compose with embed terminal editor screenshot](./docs/screenshots/compose.webp "composing view screenshot")
|
||||
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'")
|
||||
|
||||
- [pkgsrc](https://pkgsrc.se/mail/meli)
|
||||
- [openbsd ports](https://openports.pl/path/mail/meli)
|
||||
- `cargo install meli` or `cargo install --git https://git.meli-email.org/meli/meli.git meli`
|
||||
- [Pre-built debian package, static binaries](https://github.com/meli/meli/releases/ "github releases for meli")
|
||||
- [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'")
|
||||
## Documentation
|
||||
|
||||
## Build
|
||||
See a comprehensive tour of `meli` in the manual page [`meli(7)`](./docs/meli.7).
|
||||
|
||||
Run `cargo build --release --bin meli` or `make`.
|
||||
See also the [Quickstart tutorial](https://meli.delivery/documentation.html#quick-start) online.
|
||||
|
||||
For detailed building instructions, see [`BUILD.md`](./BUILD.md)
|
||||
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").
|
||||
|
||||
## Quick start
|
||||
`meli` by default looks for a configuration file in this location: `$XDG_CONFIG_HOME/meli/config.toml`
|
||||
|
||||
<table>
|
||||
<tr><td>
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
</td><td>
|
||||
|
||||
See a comprehensive tour of `meli` in the manual page [`meli(7)`](./meli/docs/meli.7).
|
||||
|
||||
See also the [Quickstart tutorial](https://meli-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, 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
|
||||
```
|
||||
|
||||
</td></tr>
|
||||
</table>
|
||||
## Build
|
||||
For a quick start, build and install locally:
|
||||
|
||||
See [`meli(7)`](./meli/docs/meli.7) for an extensive tutorial and [`meli.conf(5)`](./meli/docs/meli.conf.5) for all configuration values.
|
||||
```sh
|
||||
PREFIX=~/.local make install
|
||||
```
|
||||
|
||||
| | | |
|
||||
:---:|:---:|:---:
|
||||
![Main view screenshot](./meli/docs/screenshots/main.webp "mail meli view screenshot") | ![Compact main view screenshot](./meli/docs/screenshots/compact.webp "compact main view screenshot") | ![Compose with embed terminal editor screenshot](./meli/docs/screenshots/compose.webp "composing view screenshot")
|
||||
Main view | Compact main view | Compose with embed terminal editor
|
||||
Available subcommands for `make` are listed with `make help`. The Makefile *should* be POSIX portable and not require a specific `make` version.
|
||||
|
||||
### Supported E-mail backends
|
||||
`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>
|
||||
|
||||
| Protocol | Support |
|
||||
|:------------:|:----------------|
|
||||
| IMAP | full |
|
||||
| Maildir | full |
|
||||
| notmuch | full[^0] |
|
||||
| mbox | read-only |
|
||||
| JMAP | functional |
|
||||
| NNTP / Usenet| functional |
|
||||
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`.
|
||||
|
||||
[^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.
|
||||
You can build and run `meli` with one command: `cargo run --release`.
|
||||
|
||||
### E-mail submission backends
|
||||
### Build features
|
||||
|
||||
- SMTP
|
||||
- Pipe to shell script
|
||||
- Server-side submission when supported
|
||||
Some functionality is held behind "feature gates", or compile-time flags. The following list explains each feature's purpose:
|
||||
|
||||
### Non-exhaustive list of features
|
||||
- `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)
|
||||
|
||||
- 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
|
||||
### Build Debian package (*deb*)
|
||||
|
||||
## HTML Rendering
|
||||
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,47 +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-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-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");
|
||||
}
|
||||
}
|
123
cliff.toml
|
@ -1,123 +0,0 @@
|
|||
# configuration for https://github.com/orhun/git-cliff
|
||||
|
||||
[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 %}
|
||||
{% 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 | upper_first }}{% 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]
|
||||
# parse the commits based on https://www.conventionalcommits.org
|
||||
conventional_commits = true
|
||||
# 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}))" },
|
||||
]
|
||||
# regex for parsing and grouping commits
|
||||
commit_parsers = [
|
||||
{ message = "^feat", group = "<!-- 00 -->Added" },
|
||||
{ message = "^[aA]dd", group = "<!-- 00 -->Added" },
|
||||
{ message = "[fF]ix", group = "<!-- 01 -->Bug Fixes" },
|
||||
{ message = "[rR]efactor", group = "<!-- 02 -->Refactoring" },
|
||||
{ message = "[mM]ove", group = "<!-- 02 -->Refactoring" },
|
||||
{ message = "[rR]emove", group = "<!-- 02 -->Refactoring" },
|
||||
{ message = "^refactor", group = "<!-- 02 -->Refactoring" },
|
||||
{ message = "^[^.]*.rs:", group = "<!-- 02 -->Refactoring" },
|
||||
{ message = "^meli", group = "<!-- 07 -->Miscellaneous Tasks" },
|
||||
{ message = "^melib", group = "<!-- 07 -->Miscellaneous Tasks" },
|
||||
{ message = "^imap", group = "<!-- 07 -->Miscellaneous Tasks" },
|
||||
{ message = "^jmap", group = "<!-- 07 -->Miscellaneous Tasks" },
|
||||
{ message = "^notmuch", group = "<!-- 07 -->Miscellaneous Tasks" },
|
||||
{ message = "^mbox", group = "<!-- 07 -->Miscellaneous Tasks" },
|
||||
{ message = "^smtp", group = "<!-- 07 -->Miscellaneous Tasks" },
|
||||
{ message = "^mbox", group = "<!-- 07 -->Miscellaneous Tasks" },
|
||||
{ message = "^doc", group = "<!-- 03 -->Documentation" },
|
||||
{ message = "[mM]anual", group = "<!-- 03 -->Documentation" },
|
||||
{ message = "[mM]anpage", group = "<!-- 03 -->Documentation" },
|
||||
{ message = "[rR]eadme", group = "<!-- 03 -->Documentation" },
|
||||
{ message = "^perf", group = "<!-- 04 -->Performance" },
|
||||
{ message = "^style", group = "<!-- 05 -->Styling" },
|
||||
{ message = "^test", group = "<!-- 06 -->Testing" },
|
||||
{ message = "^debian", group = "<!-- 06 -->Packaging" },
|
||||
{ message = "^mail/view", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^view", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^utilities", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^mail", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^listing", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^terminal", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^types", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^conf", group = "<!-- 02 -->Changes" },
|
||||
{ message = "^chore\\(release\\): prepare for", skip = true },
|
||||
{ message = "^chore\\(pr\\)", skip = true },
|
||||
{ message = "^chore\\(pull\\)", skip = true },
|
||||
{ message = "^chore\\(deps\\)", skip = true },
|
||||
{ message = "^chore\\(changelog\\)", skip = true },
|
||||
{ message = "^[cC]hore", group = "<!-- 07 -->Miscellaneous Tasks" },
|
||||
{ message = "^scripts", group = "<!-- 07 -->Miscellaneous Tasks" },
|
||||
{ body = ".*security", group = "<!-- 08 -->Security" },
|
||||
{ message = "^build", group = "<!-- 09 -->Build" },
|
||||
{ message = "^ci", group = "<!-- 10 -->Continuous Integration" },
|
||||
{ message = "^revert", group = "<!-- 11 -->Reverted Commits" },
|
||||
{ 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,62 +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": "2023-12-11",
|
||||
"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.5-rc.3.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://codeberg.org/meli/meli",
|
||||
"https://github.com/meli/meli",
|
||||
"https://lists.meli-email.org/"
|
||||
],
|
||||
"version": "0.8.5-rc3",
|
||||
"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/commit/dedee908d1e0b42773bade8e0604e94b14810e2d/README.md",
|
||||
"buildInstructions": "https://git.meli-email.org/meli/meli/raw/commit/dedee908d1e0b42773bade8e0604e94b14810e2d/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,7 +56,7 @@ 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;
|
||||
|
@ -192,7 +192,7 @@ use melib::HeaderName;
|
|||
#(#attrs_tokens)*
|
||||
impl Default for #override_ident {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
#override_ident {
|
||||
#(#field_idents: None),*
|
||||
}
|
||||
}
|
|
@ -1,21 +1,3 @@
|
|||
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
|
||||
|
||||
|
@ -43,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
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
11
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
[Desktop Entry]
|
||||
Name=meli
|
||||
Exec=meli
|
||||
Categories=Office;Network;Email;
|
||||
Comment=Terminal mail client
|
||||
NoDisplay=false
|
||||
Terminal=true
|
||||
Type=Application
|
|
@ -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/
|
|
@ -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
|
|
@ -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
|
|
@ -1,4 +1,3 @@
|
|||
meli/docs/meli.1
|
||||
meli/docs/meli.7
|
||||
meli/docs/meli.conf.5
|
||||
meli/docs/meli-themes.5
|
||||
docs/meli.1
|
||||
docs/meli.conf.5
|
||||
docs/meli-themes.5
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
meli/docs/samples/sample-config.toml
|
||||
meli/docs/samples/themes
|
|
@ -1,2 +1 @@
|
|||
fix-prefix-for-debian.patch
|
||||
usr_bin_editor.patch
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
From: Manos Pitsidianakis <manos@pitsidianak.is>
|
||||
Date: Thu, 27 Feb 2014 16:06:15 +0100
|
||||
Subject: usr_bin_editor
|
||||
|
||||
If EDITOR or VISUAL is not set, fall back to /usr/bin/editor,
|
||||
which is set by update-alternatives.
|
||||
---
|
||||
meli/src/subcommands.rs | 1 +---
|
||||
1 file changed, 1 insertion(+), 3 deletions(-)
|
||||
|
||||
--- a/meli/src/subcommands.rs
|
||||
+++ b/meli/src/subcommands.rs
|
||||
@@ -52,9 +52,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 = crate::conf::get_config_file()?;
|
||||
|
||||
let mut cmd = Command::new(editor);
|
|
@ -1,21 +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 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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
@ -138,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
|
||||
|
@ -151,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
|
||||
.Tn 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
|
||||
|
@ -164,22 +157,17 @@ option (section
|
|||
.It
|
||||
The
|
||||
.Ev NO_COLOR
|
||||
environmental variable, when present (regardless of its value), prevents the addition of
|
||||
.Tn 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
|
||||
.Tn 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
|
||||
|
@ -249,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
|
||||
|
@ -269,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
|
||||
|
@ -287,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
|
||||
|
@ -324,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
|
||||
|
@ -360,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
|
||||
|
@ -396,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
|
||||
|
@ -432,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
|
||||
|
@ -468,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
|
||||
|
@ -504,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
|
||||
|
@ -540,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
|
||||
|
@ -576,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
|
||||
|
@ -612,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
|
||||
|
@ -633,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
|
||||
|
@ -70,44 +65,28 @@ Create configuration file in
|
|||
.Pa path
|
||||
if given, or at
|
||||
.Pa $XDG_CONFIG_HOME/meli/config.toml
|
||||
.It Cm test-config Op Ar path
|
||||
Test a configuration file for syntax issues or missing options.
|
||||
.It Cm man Op Ar page
|
||||
Print documentation page and exit (Piping to a pager is recommended).
|
||||
.It Cm install-man Op Ar path
|
||||
Install manual pages to the first location provided by
|
||||
.Ev MANPATH
|
||||
or
|
||||
.Xr manpath 1 ,
|
||||
unless you specify the directory as an argument.
|
||||
.It Cm compiled-with
|
||||
Print compile time feature flags of this binary.
|
||||
.It Cm edit-config
|
||||
Edit configuration files with
|
||||
.Ev EDITOR
|
||||
or
|
||||
.Ev VISUAL Ns
|
||||
\&.
|
||||
.It Cm 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
|
||||
^^ .-=-=-=-. ^^
|
||||
^^ (`-=-=-=-=-`) ^^
|
||||
|
@ -141,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"
|
||||
|
@ -178,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
|
||||
|
@ -207,7 +165,7 @@ If the path provided is a directory, the attachment is saved with its filename s
|
|||
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
|
||||
|
@ -256,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
|
||||
|
@ -266,10 +225,9 @@ 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 | alladdresses | subject | flags | has_attachments | query \&"or\&" query | query \&"and\&" query | not query
|
||||
.It
|
||||
|
@ -299,23 +257,10 @@ Sqlite3 on the contrary at reasonable mailbox sizes should have a non noticeable
|
|||
.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
|
||||
|
@ -336,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
|
||||
|
@ -380,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
|
||||
|
@ -405,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
|
||||
|
@ -417,7 +358,8 @@ 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/addressbook Ns
|
||||
|
@ -439,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
|
||||
|
@ -459,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
|
||||
|
@ -496,8 +437,6 @@ Escape exits search results.
|
|||
select threads matching
|
||||
.Ar STRING
|
||||
query.
|
||||
.It Cm clear-selection
|
||||
Clear current selection.
|
||||
.It Cm set seen, set unseen
|
||||
Set seen status of message.
|
||||
.It Cm import Ar FILEPATH Ar MAILBOX_PATH
|
||||
|
@ -505,26 +444,25 @@ Import mail from file into given mailbox.
|
|||
.It Cm copyto, moveto Ar MAILBOX_PATH
|
||||
Copy or move to other mailbox.
|
||||
.It Cm copyto, moveto Ar ACCOUNT Ar MAILBOX_PATH
|
||||
Copy or move to another account's mailbox.
|
||||
Copy or move to another account's mailbox.
|
||||
.It Cm delete
|
||||
Delete selected threads.
|
||||
.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 Ar ARGS
|
||||
pipe pager contents to binary
|
||||
|
@ -538,8 +476,7 @@ 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
|
||||
|
@ -574,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
|
||||
|
@ -599,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.
|
||||
|
@ -616,9 +556,7 @@ 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
|
||||
.Tn ANSI
|
||||
color.
|
||||
When present (regardless of its value), prevents the addition of ANSI color.
|
||||
The configuration value
|
||||
.Ic use_color
|
||||
overrides this.
|
||||
|
@ -675,265 +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 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 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\&.
|
||||
|
@ -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_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,25 +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 stderr, separated by NULL bytes.
|
||||
.It Cm remove\-attachment Ar INDEX
|
||||
.It Cm remove-attachment Ar INDEX
|
||||
remove attachment with given index
|
||||
.It Cm toggle sign
|
||||
toggle between signing and not signing this message.
|
||||
|
@ -722,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
|
||||
|
@ -739,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
|
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.5-rc.3"
|
||||
authors = ["Manos Pitsidianakis <manos@pitsidianak.is>"]
|
||||
edition = "2021"
|
||||
rust-version = "1.68.2"
|
||||
license = "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"
|
||||
|
||||
[[bin]]
|
||||
name = "meli"
|
||||
path = "src/main.rs"
|
||||
|
||||
[lib]
|
||||
name = "meli"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
async-task = "^4.2.0"
|
||||
bitflags = { version = "2.4", features = ["serde"] }
|
||||
crossbeam = { version = "^0.8" }
|
||||
flate2 = { version = "1", optional = true }
|
||||
futures = "0.3.5"
|
||||
indexmap = { version = "^1.6", features = ["serde-1"] }
|
||||
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.5-rc.3", features = [] }
|
||||
nix = { version = "0.27", default-features = false, features = ["signal", "poll", "term", "ioctl", "process"] }
|
||||
serde = "1.0.71"
|
||||
serde_derive = "1.0.71"
|
||||
serde_json = "1.0"
|
||||
signal-hook = { version = "^0.3", default-features = false, features = ["iterator"] }
|
||||
signal-hook-registry = { version = "1.2.0", default-features = false }
|
||||
smallvec = { version = "^1.5.0", features = ["serde"] }
|
||||
structopt = { version = "0.3.14", default-features = false }
|
||||
svg_crate = { version = "^0.13", optional = true, package = "svg" }
|
||||
termion = { version = "1.5.1", default-features = false }
|
||||
toml = { version = "0.8", default-features = false, features = ["display","preserve_order","parse"] }
|
||||
xdg = "2.1.0"
|
||||
|
||||
[dependencies.pcre2]
|
||||
# An [env] entry in .cargo/config.toml should force a static build instead of
|
||||
# looking for a system library.
|
||||
version = "0.2.3"
|
||||
optional = true
|
||||
|
||||
[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"]
|
||||
regexp = ["dep:pcre2"]
|
||||
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 = "1.0.37"
|
||||
quote = "^1.0"
|
||||
regex = "1"
|
||||
syn = { version = "1", features = [] }
|
||||
|
||||
[dev-dependencies]
|
||||
flate2 = { version = "1" }
|
||||
regex = "1"
|
||||
tempfile = "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,354 +0,0 @@
|
|||
'\" t
|
||||
.\"<!-- Copyright 1998 - 2007 Double Precision, Inc. See COPYING for -->
|
||||
.\"<!-- distribution information. -->
|
||||
.\" Title: maildir
|
||||
.\" Author: Sam Varshavchik
|
||||
.\" Generator: DocBook XSL Stylesheets vsnapshot <http://docbook.sf.net/>
|
||||
.\" Date: 07/24/2017
|
||||
.\" Manual: Double Precision, Inc.
|
||||
.\" Source: Courier Mail Server
|
||||
.\" Language: English
|
||||
.\"
|
||||
.TH "MAILDIR" "5" "07/24/2017" "Courier Mail Server" "Double Precision, Inc\&."
|
||||
.\" -----------------------------------------------------------------
|
||||
.\" * Define some portability stuff
|
||||
.\" -----------------------------------------------------------------
|
||||
.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
.\" http://bugs.debian.org/507673
|
||||
.\" http://lists.gnu.org/archive/html/groff/2009-02/msg00013.html
|
||||
.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
.ie \n(.g .ds Aq \(aq
|
||||
.el .ds Aq '
|
||||
.\" -----------------------------------------------------------------
|
||||
.\" * set default formatting
|
||||
.\" -----------------------------------------------------------------
|
||||
.\" disable hyphenation
|
||||
.nh
|
||||
.\" disable justification (adjust text to left margin only)
|
||||
.ad l
|
||||
.\" -----------------------------------------------------------------
|
||||
.\" * MAIN CONTENT STARTS HERE *
|
||||
.\" -----------------------------------------------------------------
|
||||
.SH "NAME"
|
||||
maildir \- E\-mail directory
|
||||
.SH "SYNOPSIS"
|
||||
.sp
|
||||
$HOME/Maildir
|
||||
.SH "DESCRIPTION"
|
||||
.PP
|
||||
A
|
||||
\(lqMaildir\(rq
|
||||
is a structured directory that holds E\-mail messages\&. Maildirs were first implemented by the
|
||||
Qmail
|
||||
mail server\&. Qmail\*(Aqs maildirs were a simple data structure, nothing more than a single collection of E\-mail messages\&. The
|
||||
Courier
|
||||
mail server builds upon
|
||||
Qmail\*(Aqs maildirs to provide extended functionality, such as folders and quotas\&. This document describes the
|
||||
Courier
|
||||
mail server\*(Aqs extended maildirs, without explicitly identifying The
|
||||
Courier
|
||||
mail server\-specific extensions\&. See
|
||||
\fBmaildir\fR(5)
|
||||
in Qmail\*(Aqs documentation for the original definition of maildirs\&.
|
||||
.PP
|
||||
Traditionally, E\-mail folders were saved as plain text files, called
|
||||
\(lqmboxes\(rq\&. Mboxes have known limitations\&. Only one application can use an mbox at the same time\&. Locking is required in order to allow simultaneous concurrent access by different applications\&. Locking is often problematic, and not very reliable in network\-based filesystem requirements\&. Some network\-based filesystems don\*(Aqt offer any reliable locking mechanism at all\&. Furthermore, even bulletproof locking won\*(Aqt prevent occasional mbox corruption\&. A process can be killed or terminated in the middle of updating an mbox\&. This will likely result in corruption, and a loss of most messages in the mbox\&.
|
||||
.PP
|
||||
Maildirs allow multiple concurrent access by different applications\&. Maildirs do not require locking\&. Multiple applications can update a maildir at the same time, without stepping on each other\*(Aqs feet\&.
|
||||
.SS "Maildir contents"
|
||||
.PP
|
||||
A
|
||||
\(lqmaildir\(rq
|
||||
is a directory that\*(Aqs created by
|
||||
\m[blue]\fB\fBmaildirmake\fR(1)\fR\m[]\&\s-2\u[1]\d\s+2\&. Naturally, maildirs should not have any group or world permissions, unless you want other people to read your mail\&. A maildir contains three subdirectories:
|
||||
tmp,
|
||||
new, and
|
||||
cur\&. These three subdirectories comprise the primary folder, where new mail is delivered by the system\&.
|
||||
.PP
|
||||
Folders are additional subdirectories in the maildir whose names begin with a period: such as
|
||||
\&.Drafts
|
||||
or
|
||||
\&.Sent\&. Each folder itself contains the same three subdirectories,
|
||||
tmp,
|
||||
new, and
|
||||
cur, and an additional zero\-length file named
|
||||
maildirfolder, whose purpose is to inform any mail delivery agent that it\*(Aqs really delivering to a folder, and that the mail delivery agent should look in the parent directory for any maildir\-related information\&.
|
||||
.PP
|
||||
Folders are not physically nested\&. A folder subdirectory, such as
|
||||
\&.Sent
|
||||
does not itself contain any subfolders\&. The main maildir contains a single, flat list of subfolders\&. These folders are logically nested, and periods serve to separate folder hierarchies\&. For example,
|
||||
\&.Sent\&.2002
|
||||
is considered to be a subfolder called
|
||||
\(lq2002\(rq
|
||||
which is a subfolder of
|
||||
\(lqSent\(rq\&.
|
||||
.sp
|
||||
.it 1 an-trap
|
||||
.nr an-no-space-flag 1
|
||||
.nr an-break-flag 1
|
||||
.br
|
||||
.ps +1
|
||||
\fBFolder name encoding\fR
|
||||
.RS 4
|
||||
.PP
|
||||
Folder names can contain any Unicode character, except for control characters\&. US\-ASCII characters, U+0x0020 \- U+0x007F, except for the period, forward\-slash, and ampersand characters (U+0x002E, U+0x002F, and U+0x0026) represent themselves\&. The ampersand is represent by the two character sequence
|
||||
\(lq&\-\(rq\&. The period, forward slash, and non US\-ASCII Unicode characters are represented using the UTF\-7 character set, and encoded with a modified form of base64\-encoding\&.
|
||||
.PP
|
||||
The
|
||||
\(lq&\(rq
|
||||
character starts the modified base64\-encoded sequence; the sequence is terminated by the
|
||||
\(lq\-\(rq
|
||||
character\&. The sequence of 16\-bit Unicode characters is written in big\-endian order, and encoded using the base64\-encoding method described in section 5\&.2 of
|
||||
\m[blue]\fBRFC 1521\fR\m[]\&\s-2\u[2]\d\s+2, with the following modifications:
|
||||
.sp
|
||||
.RS 4
|
||||
.ie n \{\
|
||||
\h'-04'\(bu\h'+03'\c
|
||||
.\}
|
||||
.el \{\
|
||||
.sp -1
|
||||
.IP \(bu 2.3
|
||||
.\}
|
||||
The
|
||||
\(lq=\(rq
|
||||
padding character is omitted\&. When decoding, an incomplete 16\-bit character is discarded\&.
|
||||
.RE
|
||||
.sp
|
||||
.RS 4
|
||||
.ie n \{\
|
||||
\h'-04'\(bu\h'+03'\c
|
||||
.\}
|
||||
.el \{\
|
||||
.sp -1
|
||||
.IP \(bu 2.3
|
||||
.\}
|
||||
The comma character,
|
||||
\(lq,\(rq
|
||||
is used in place of the
|
||||
\(lq/\(rq
|
||||
character in the base64 alphabet\&.
|
||||
.RE
|
||||
.PP
|
||||
For example, the word
|
||||
\(lqResume\(rq
|
||||
with both "e"s being the e\-acute character, U+0x00e9, is encoded as
|
||||
\(lqR&AOk\-sum&AOk\-\(rq
|
||||
(so a folder of that name would be a maildir subdirectory called
|
||||
\(lq\&.R&AOk\-sum&AOk\-\(rq)\&.
|
||||
.RE
|
||||
.sp
|
||||
.it 1 an-trap
|
||||
.nr an-no-space-flag 1
|
||||
.nr an-break-flag 1
|
||||
.br
|
||||
.ps +1
|
||||
\fBOther maildir contents\fR
|
||||
.RS 4
|
||||
.PP
|
||||
Software that uses maildirs may also create additional files besides the
|
||||
tmp,
|
||||
new, and
|
||||
cur
|
||||
subdirectories \-\- in the main maildir or a subfolder \-\- for its own purposes\&.
|
||||
.RE
|
||||
.SS "Messages"
|
||||
.PP
|
||||
E\-mail messages are stored in separate, individual files, one E\-mail message per file\&. The
|
||||
tmp
|
||||
subdirectory temporarily stores E\-mail messages that are in the process of being delivered to this maildir\&.
|
||||
tmp
|
||||
may also store other kinds of temporary files, as long as they are created in the same way that message files are created in
|
||||
tmp\&. The
|
||||
new
|
||||
subdirectory stores messages that have been delivered to this maildir, but have not yet been seen by any mail application\&. The
|
||||
cur
|
||||
subdirectory stores messages that have already been seen by mail applications\&.
|
||||
.SS "Adding new mail to maildirs"
|
||||
.PP
|
||||
The following process delivers a new message to the maildir:
|
||||
.PP
|
||||
A new unique filename is created using one of two possible forms:
|
||||
\(lqtime\&.MusecPpid\&.host\(rq, or
|
||||
\(lqtime\&.MusecPpid_unique\&.host\(rq\&.
|
||||
\(lqtime\(rq
|
||||
and
|
||||
\(lqusec\(rq
|
||||
is the current system time, obtained from
|
||||
\fBgettimeofday\fR(2)\&.
|
||||
\(lqpid\(rq
|
||||
is the process number of the process that is delivering this message to the maildir\&.
|
||||
\(lqhost\(rq
|
||||
is the name of the machine where the mail is being delivered\&. In the event that the same process creates multiple messages, a suffix unique to each message is appended to the process id; preferrably an underscore, followed by an increasing counter\&. This applies whether messages created by a process are all added to the same, or different, maildirs\&. This protocol allows multiple processes running on multiple machines on the same network to simultaneously create new messages without stomping on each other\&.
|
||||
.PP
|
||||
The filename created in the previous step is checked for existence by executing the
|
||||
\fBstat\fR(2)
|
||||
system call\&. If
|
||||
\fBstat\fR(2)
|
||||
results in ANYTHING OTHER than the system error
|
||||
ENOENT, the process must sleep for two seconds, then go back and create another unique filename\&. This is an extra step to insure that each new message has a completely unique filename\&.
|
||||
.PP
|
||||
Other applications that wish to use
|
||||
tmp
|
||||
for temporary storage should observe the same protocol (but see READING MAIL FROM MAILDIRS below, because old files in
|
||||
tmp
|
||||
will be eventually deleted)\&.
|
||||
.PP
|
||||
If the
|
||||
\fBstat\fR(2)
|
||||
system call returned
|
||||
ENOENT, the process may proceed to create the file in the
|
||||
tmp
|
||||
subdirectory, and save the entire message in the new file\&. The message saved MUST NOT have the
|
||||
\(lqFrom_\(rq
|
||||
header that is used to mboxes\&. The message also MUST NOT have any
|
||||
\(lqFrom_\(rq
|
||||
lines in the contents of the message prefixed by the
|
||||
\(lq>\(rq
|
||||
character\&.
|
||||
.PP
|
||||
When saving the message, the number of bytes returned by the
|
||||
\fBwrite\fR(2)
|
||||
system call must be checked, in order to make sure that the complete message has been written out\&.
|
||||
.PP
|
||||
After the message is saved, the file descriptor is
|
||||
\fBfstat\fR(2)\-ed\&. The file\*(Aqs device number, inode number, and the its byte size, are saved\&. The file is closed and is then immediately moved/renamed into the
|
||||
new
|
||||
subdirectory\&. The name of the file in
|
||||
new
|
||||
should be
|
||||
\(lqtime\&.MusecPpidVdevIino\&.host,S=\fIcnt\fR\(rq, or
|
||||
\(lqtime\&.MusecPpidVdevIino_unique\&.host,S=\fIcnt\fR\(rq\&.
|
||||
\(lqdev\(rq
|
||||
is the message\*(Aqs device number,
|
||||
\(lqino\(rq
|
||||
is the message\*(Aqs inode number (from the previous
|
||||
\fBfstat\fR(2)
|
||||
call); and
|
||||
\(lqcnt\(rq
|
||||
is the message\*(Aqs size, in bytes\&.
|
||||
.PP
|
||||
The
|
||||
\(lq,S=\fIcnt\fR\(rq
|
||||
part optimizes the
|
||||
\m[blue]\fBCourier\fR\m[]\&\s-2\u[3]\d\s+2
|
||||
mail server\*(Aqs maildir quota enhancement; it allows the size of all the mail stored in the maildir to be added up without issuing the
|
||||
\fBstat\fR(2)
|
||||
system call for each individual message (this can be quite a performance drain with certain network filesystems)\&.
|
||||
.SS "READING MAIL FROM MAILDIRS"
|
||||
.PP
|
||||
Applications that read mail from maildirs should do it in the following order:
|
||||
.PP
|
||||
When opening a maildir or a maildir folder, read the
|
||||
tmp
|
||||
subdirectory and delete any files in there that are at least 36 hours old\&.
|
||||
.PP
|
||||
Look for new messages in the
|
||||
new
|
||||
subdirectory\&. Rename
|
||||
\fInew/filename\fR, as
|
||||
\fIcur/filename:2,info\fR\&. Here,
|
||||
\fIinfo\fR
|
||||
represents the state of the message, and it consists of zero or more boolean flags chosen from the following:
|
||||
\(lqD\(rq
|
||||
\- this is a \*(Aqdraft\*(Aq message,
|
||||
\(lqR\(rq
|
||||
\- this message has been replied to,
|
||||
\(lqS\(rq
|
||||
\- this message has been viewed (seen),
|
||||
\(lqT\(rq
|
||||
\- this message has been marked to be deleted (trashed), but is not yet removed (messages are removed from maildirs simply by deleting their file),
|
||||
\(lqF\(rq
|
||||
\- this message has been marked by the user, for some purpose\&. These flags must be stored in alphabetical order\&. New messages contain only the
|
||||
:2,
|
||||
suffix, with no flags, indicating that the messages were not seen, replied, marked, or deleted\&.
|
||||
.PP
|
||||
Maildirs may have maximum size quotas defined, but these quotas are purely voluntary\&. If you need to implement mandatory quotas, you should use any quota facilities provided by the underlying filesystem that is used to store the maildirs\&. The maildir quota enhancement is designed to be used in certain situations where filesystem\-based quotas cannot be used for some reason\&. The implementation is designed to avoid the use of any locking\&. As such, at certain times the calculated quota may be imprecise, and certain anomalous situations may result in the maildir actually going over the stated quota\&. One such situation would be when applications create messages without updating the quota estimate for the maildir\&. Eventually it will be precisely recalculated, but wherever possible new messages should be created in compliance with the voluntary quota protocol\&.
|
||||
.PP
|
||||
The voluntary quota protocol involves some additional procedures that must be followed when creating or deleting messages within a given maildir or its subfolders\&. The
|
||||
\m[blue]\fB\fBdeliverquota\fR(8)\fR\m[]\&\s-2\u[4]\d\s+2
|
||||
command is a tiny application that delivers a single message to a maildir using the voluntary quota protocol, and hopefully it can be used as a measure of last resort\&. Alternatively, applications can use the
|
||||
libmaildir\&.a
|
||||
library to handle all the low\-level dirty details for them\&. The voluntary quota enhancement is described in the
|
||||
\m[blue]\fB\fBmaildirquota\fR(7)\fR\m[]\&\s-2\u[5]\d\s+2
|
||||
man page\&.
|
||||
.SS "Maildir Quotas"
|
||||
.PP
|
||||
This is a voluntary mechanism for enforcing "loose" quotas on the maximum sizes of maildirs\&. This mechanism is enforced in software, and not by the operating system\&. Therefore it is only effective as long as the maildirs themselves are not directly accessible by their users, since this mechanism is trivially disabled\&.
|
||||
.PP
|
||||
If possible, operating system\-enforced quotas are preferrable\&. Where operating system quota enforcement is not available, or not possible, this voluntary quota enforcement mechanism might be an acceptable compromise\&. Since it\*(Aqs enforced in software, all software that modifies or accesses the maildirs is required to voluntary obey and enforce a quota\&. The voluntary quota implementation is flexible enough to allow non quota\-aware applications to also access the maildirs, without any drastic consequences\&. There will be some non\-drastic consequences, though\&. Of course, non quota\-aware applications will not enforce any defined quotas\&. Furthermore, this voluntary maildir quota mechanism works by estimating the current size of the maildir, with periodic exact recalculation\&. Obviously non quota\-aware maildir applications will not update the maildir size estimation, so the estimate will be thrown off for some period of time, until the next recalculation\&.
|
||||
.PP
|
||||
This voluntary quota mechanism is designed to be a reasonable compromise between effectiveness, and performance\&. The entire purpose of using maildir\-based mail storage is to avoid any kind of locking, and to permit parallel access to mail by multiple applications\&. In order to compute the exact size of a maildir, the maildir must be locked somehow to prevent any modifications while its contents are added up\&. Obviously something like that defeats the original purpose of using maildirs, therefore the voluntary quota mechanism does not use locking, and that\*(Aqs why the current recorded maildir size is always considered to be an estimate\&. Regular size recalculations will compensate for any occasional race conditions that result in the estimate to be thrown off\&.
|
||||
.PP
|
||||
A quota for an existing maildir is installed by running maildirmake with the
|
||||
\-q
|
||||
option, and naming an existing maildir\&. The
|
||||
\-q
|
||||
option takes a parameter,
|
||||
\fIquota\fR, which is a comma\-separated list of quota specifications\&. A quota specification consists of a number followed by either \*(AqS\*(Aq, indicating the maximum message size in bytes, or \*(AqC\*(Aq, maximum number of messages\&. For example:
|
||||
.sp
|
||||
.if n \{\
|
||||
.RS 4
|
||||
.\}
|
||||
.nf
|
||||
\fBmaildirmake \-q 5000000S,1000C \&./Maildir\fR
|
||||
.fi
|
||||
.if n \{\
|
||||
.RE
|
||||
.\}
|
||||
.PP
|
||||
This sets the quota to 5,000,000 bytes or 1000 messages, whichever comes first\&.
|
||||
.sp
|
||||
.if n \{\
|
||||
.RS 4
|
||||
.\}
|
||||
.nf
|
||||
\fBmaildirmake \-q 1000000S \&./Maildir\fR
|
||||
.fi
|
||||
.if n \{\
|
||||
.RE
|
||||
.\}
|
||||
.PP
|
||||
This sets the quota to 1,000,000 bytes, without limiting the number of messages\&.
|
||||
.PP
|
||||
A quota of an existing maildir can be changed by rerunning the
|
||||
\fBmaildirmake\fR
|
||||
command with a new
|
||||
\-q
|
||||
option\&. To delete a quota entirely, delete the
|
||||
\fIMaildir\fR/maildirsize
|
||||
file\&.
|
||||
.SH "SEE ALSO"
|
||||
.PP
|
||||
\m[blue]\fB\fBmaildirmake\fR(1)\fR\m[]\&\s-2\u[1]\d\s+2\&.
|
||||
.SH "AUTHOR"
|
||||
.PP
|
||||
\fBSam Varshavchik\fR
|
||||
.RS 4
|
||||
Author
|
||||
.RE
|
||||
.SH "NOTES"
|
||||
.IP " 1." 4
|
||||
\fBmaildirmake\fR(1)
|
||||
.RS 4
|
||||
\%http://www.courier-mta.org/maildirmake.html
|
||||
.RE
|
||||
.IP " 2." 4
|
||||
RFC 1521
|
||||
.RS 4
|
||||
\%http://www.rfc-editor.org/rfc/rfc1521.txt
|
||||
.RE
|
||||
.IP " 3." 4
|
||||
Courier
|
||||
.RS 4
|
||||
\%http://www.courier-mta.org
|
||||
.RE
|
||||
.IP " 4." 4
|
||||
\fBdeliverquota\fR(8)
|
||||
.RS 4
|
||||
\%http://www.courier-mta.org/deliverquota.html
|
||||
.RE
|
||||
.IP " 5." 4
|
||||
\fBmaildirquota\fR(7)
|
||||
.RS 4
|
||||
\%http://www.courier-mta.org/maildirquota.html
|
||||
.RE
|
|
@ -1,187 +0,0 @@
|
|||
'\" t
|
||||
.\" -*-nroff-*-
|
||||
.\"
|
||||
.\" Copyright (C) 2000 Thomas Roessler <roessler@does-not-exist.org>
|
||||
.\"
|
||||
.\" This document is in the public domain and may be distributed and
|
||||
.\" changed arbitrarily.
|
||||
.\"
|
||||
.TH mbox 5 "February 19th, 2002" Unix "User Manuals"
|
||||
.\"
|
||||
.SH NAME
|
||||
mbox \- Format for mail message storage.
|
||||
.\"
|
||||
.SH DESCRIPTION
|
||||
This document describes the format traditionally used by Unix hosts
|
||||
to store mail messages locally.
|
||||
.B mbox
|
||||
files typically reside in the system's mail spool, under various
|
||||
names in users' Mail directories, and under the name
|
||||
.B mbox
|
||||
in users' home directories.
|
||||
.PP
|
||||
An
|
||||
.B mbox
|
||||
is a text file containing an arbitrary number of e-mail messages.
|
||||
Each message consists of a postmark, followed by an e-mail message
|
||||
formatted according to \fBRFC822\fP, \fBRFC2822\fP. The file format
|
||||
is line-oriented. Lines are separated by line feed characters (ASCII 10).
|
||||
.PP
|
||||
A postmark line consists of the four characters "From", followed by
|
||||
a space character, followed by the message's envelope sender
|
||||
address, followed by whitespace, and followed by a time stamp. This
|
||||
line is often called From_ line.
|
||||
.PP
|
||||
The sender address is expected to be
|
||||
.B addr-spec
|
||||
as defined in \fBRFC2822\fP 3.4.1. The date is expected to be
|
||||
.B date-time
|
||||
as output by
|
||||
.BR asctime (3).
|
||||
For compatibility reasons with legacy software, two-digit years
|
||||
greater than or equal to 70 should be interpreted as the years
|
||||
1970+, while two-digit years less than 70 should be interpreted as
|
||||
the years 2000-2069. Software reading files in this format should
|
||||
also be prepared to accept non-numeric timezone information such as
|
||||
"CET DST" for Central European Time, daylight saving time.
|
||||
.PP
|
||||
Example:
|
||||
.IP "" 1
|
||||
>From example@example.com Fri Jun 23 02:56:55 2000
|
||||
.PP
|
||||
In order to avoid misinterpretation of lines in message bodies
|
||||
which begin with the four characters "From", followed by a space
|
||||
character, the mail delivery agent must quote any occurrence
|
||||
of "From " at the start of a body line.
|
||||
.sp
|
||||
There are two different quoting schemes, the first (\fBMBOXO\fP) only
|
||||
quotes plain "From " lines in the body by prepending a '>' to the
|
||||
line; the second (\fBMBOXRD\fP) also quotes already quoted "From "
|
||||
lines by prepending a '>' (i.e. ">From ", ">>From ", ...). The later
|
||||
has the advantage that lines like
|
||||
.IP "" 1
|
||||
>From the command line you can use the '\-p' option
|
||||
.PP
|
||||
aren't dequoted wrongly as a \fBMBOXRD\fP-MDA would turn the line
|
||||
into
|
||||
.IP "" 1
|
||||
>>From the command line you can use the '\-p' option
|
||||
.PP
|
||||
before storing it. Besides \fBMBOXO\fP and \fBMBOXRD\fP there is also
|
||||
\fBMBOXCL\fP which is \fBMBOXO\fP with a "Content-Length:"\-field with the
|
||||
number of bytes in the message body; some MUAs (like
|
||||
.BR mutt (1))
|
||||
do automatically transform \fBMBOXO\fP mailboxes into \fBMBOXCL\fP ones when
|
||||
ever they write them back as \fBMBOXCL\fP can be read by any \fBMBOXO\fP-MUA
|
||||
without any problems.
|
||||
.PP
|
||||
If the modification-time (usually determined via
|
||||
.BR stat (2))
|
||||
of a nonempty
|
||||
.B mbox
|
||||
file is greater than the access-time the file has new mail. Many MUAs
|
||||
place a Status: header in each message to indicate which messages have
|
||||
already been read.
|
||||
.\"
|
||||
.SH LOCKING
|
||||
Since
|
||||
.B mbox
|
||||
files are frequently accessed by multiple programs in parallel,
|
||||
.B mbox
|
||||
files should generally not be accessed without locking.
|
||||
.PP
|
||||
Three different locking mechanisms (and combinations thereof) are in
|
||||
general use:
|
||||
.IP "\(bu"
|
||||
.BR fcntl (2)
|
||||
locking is mostly used on recent, POSIX-compliant systems. Use of
|
||||
this locking method is, in particular, advisable if
|
||||
.B mbox
|
||||
files are accessed through the Network File System (NFS), since it
|
||||
seems the only way to reliably invalidate NFS clients' caches.
|
||||
.IP "\(bu"
|
||||
.BR flock (2)
|
||||
locking is mostly used on BSD-based systems.
|
||||
.IP "\(bu"
|
||||
Dotlocking is used on all kinds of systems. In order to lock an
|
||||
.B mbox
|
||||
file named \fIfolder\fR, an application first creates a temporary file
|
||||
with a unique name in the directory in which the
|
||||
\fIfolder\fR resides. The application then tries to use the
|
||||
.BR link (2)
|
||||
system call to create a hard link named \fIfolder.lock\fR
|
||||
to the temporary file. The success of the
|
||||
.BR link (2)
|
||||
system call should be additionally verified using
|
||||
.BR stat (2)
|
||||
calls. If the link has succeeded, the mail folder is considered
|
||||
dotlocked. The temporary file can then safely be unlinked.
|
||||
.IP ""
|
||||
In order to release the lock, an application just unlinks the
|
||||
\fIfolder.lock\fR file.
|
||||
.PP
|
||||
If multiple methods are combined, implementors should make sure to
|
||||
use the non-blocking variants of the
|
||||
.BR fcntl (2)
|
||||
and
|
||||
.BR flock (2)
|
||||
system calls in order to avoid deadlocks.
|
||||
.PP
|
||||
If multiple methods are combined, an
|
||||
.B mbox
|
||||
file must not be considered to have been successfully locked before
|
||||
all individual locks were obtained. When one of the individual
|
||||
locking methods fails, an application should release all locks it
|
||||
acquired successfully, and restart the entire locking procedure from
|
||||
the beginning, after a suitable delay.
|
||||
.PP
|
||||
The locking mechanism used on a particular system is a matter of
|
||||
local policy, and should be consistently used by all applications
|
||||
installed on the system which access
|
||||
.B mbox
|
||||
files. Failure to do so may result in loss of e-mail data, and in
|
||||
corrupted
|
||||
.B mbox
|
||||
files.
|
||||
.\"
|
||||
.SH FILES
|
||||
.IR /var/spool/mail/$LOGNAME
|
||||
.RS
|
||||
\fB$LOGNAME\fP's incoming mail folder.
|
||||
.RE
|
||||
.PP
|
||||
.IR $HOME/mbox
|
||||
.RS
|
||||
user's archived mail messages, in his \fB$HOME\fP directory.
|
||||
.RE
|
||||
.PP
|
||||
.IR $HOME/Mail/
|
||||
.RS
|
||||
A directory in user's \fB$HOME\fP directory which is commonly used to hold
|
||||
.B mbox
|
||||
format folders.
|
||||
.RE
|
||||
.PP
|
||||
.\"
|
||||
.SH "SEE ALSO"
|
||||
.BR mutt (1),
|
||||
.BR fcntl (2),
|
||||
.BR flock (2),
|
||||
.BR link (2),
|
||||
.BR stat (2),
|
||||
.BR asctime (3),
|
||||
.BR maildir (5),
|
||||
.BR mmdf (5),
|
||||
.BR RFC822 ,
|
||||
.BR RFC976 ,
|
||||
.BR RFC2822
|
||||
.\"
|
||||
.SH AUTHOR
|
||||
Thomas Roessler <roessler@does-not-exist.org>, Urs Janssen <urs@tin.org>
|
||||
.\"
|
||||
.SH HISTORY
|
||||
The
|
||||
.B mbox
|
||||
format occurred in Version 6 AT&T Unix.
|
||||
.br
|
||||
A variant of this format was documented in \fBRFC976\fP.
|
|
@ -1,235 +0,0 @@
|
|||
.TH mbox 5
|
||||
.SH "NAME"
|
||||
mbox \- file containing mail messages
|
||||
.SH "INTRODUCTION"
|
||||
The most common format for storage of mail messages is
|
||||
.I mbox
|
||||
format.
|
||||
An
|
||||
.I mbox
|
||||
is a single file containing zero or more mail messages.
|
||||
.SH "MESSAGE FORMAT"
|
||||
A message encoded in
|
||||
.I mbox
|
||||
format begins with a
|
||||
.B From_
|
||||
line, continues with a series of
|
||||
.B \fRnon-\fBFrom_
|
||||
lines,
|
||||
and ends with a blank line.
|
||||
A
|
||||
.B From_
|
||||
line means any line that begins with the characters
|
||||
F, r, o, m, space:
|
||||
|
||||
.EX
|
||||
From god@heaven.af.mil Sat Jan 3 01:05:34 1996
|
||||
.br
|
||||
Return-Path: <god@heaven.af.mil>
|
||||
.br
|
||||
Delivered-To: djb@silverton.berkeley.edu
|
||||
.br
|
||||
Date: 3 Jan 1996 01:05:34 -0000
|
||||
.br
|
||||
From: God <god@heaven.af.mil>
|
||||
.br
|
||||
To: djb@silverton.berkeley.edu (D. J. Bernstein)
|
||||
.br
|
||||
|
||||
.br
|
||||
How's that mail system project coming along?
|
||||
.br
|
||||
|
||||
.EE
|
||||
|
||||
The final line is a completely blank line (no spaces or tabs).
|
||||
Notice that blank lines may also appear elsewhere in the message.
|
||||
|
||||
The
|
||||
.B From_
|
||||
line always looks like
|
||||
.B From
|
||||
.I envsender
|
||||
.I date
|
||||
.IR moreinfo .
|
||||
.I envsender
|
||||
is one word, without spaces or tabs;
|
||||
it is usually the envelope sender of the message.
|
||||
.I date
|
||||
is the delivery date of the message.
|
||||
It always contains exactly 24 characters in
|
||||
.B asctime
|
||||
format.
|
||||
.I moreinfo
|
||||
is optional; it may contain arbitrary information.
|
||||
|
||||
Between the
|
||||
.B From_
|
||||
line and the blank line is a message in RFC 822 format,
|
||||
as described in
|
||||
.BR qmail-header(5) ,
|
||||
subject to
|
||||
.B >From quoting
|
||||
as described below.
|
||||
.SH "HOW A MESSAGE IS DELIVERED"
|
||||
Here is how a program appends a message to an
|
||||
.I mbox
|
||||
file.
|
||||
|
||||
It first creates a
|
||||
.B From_
|
||||
line given the message's envelope sender and the current date.
|
||||
If the envelope sender is empty (i.e., if this is a bounce message),
|
||||
the program uses
|
||||
.B MAILER-DAEMON
|
||||
instead.
|
||||
If the envelope sender contains spaces, tabs, or newlines,
|
||||
the program replaces them with hyphens.
|
||||
|
||||
The program then copies the message, applying
|
||||
.B >From quoting
|
||||
to each line.
|
||||
.B >From quoting
|
||||
ensures that the resulting lines are not
|
||||
.B From_
|
||||
lines:
|
||||
the program prepends a
|
||||
.B >
|
||||
to any
|
||||
.B From_
|
||||
line,
|
||||
.B >From_
|
||||
line,
|
||||
.B >>From_
|
||||
line,
|
||||
.B >>>From_
|
||||
line,
|
||||
etc.
|
||||
|
||||
Finally the program appends a blank line to the message.
|
||||
If the last line of the message was a partial line,
|
||||
it writes two newlines;
|
||||
otherwise it writes one.
|
||||
.SH "HOW A MESSAGE IS READ"
|
||||
A reader scans through an
|
||||
.I mbox
|
||||
file looking for
|
||||
.B From_
|
||||
lines.
|
||||
Any
|
||||
.B From_
|
||||
line marks the beginning of a message.
|
||||
The reader should not attempt to take advantage of the fact that every
|
||||
.B From_
|
||||
line (past the beginning of the file)
|
||||
is preceded by a blank line.
|
||||
|
||||
Once the reader finds a message,
|
||||
it extracts a (possibly corrupted) envelope sender
|
||||
and delivery date out of the
|
||||
.B From_
|
||||
line.
|
||||
It then reads until the next
|
||||
.B From_
|
||||
line or end of file, whichever comes first.
|
||||
It strips off the final blank line
|
||||
and
|
||||
deletes the
|
||||
quoting of
|
||||
.B >From_
|
||||
lines and
|
||||
.B >>From_
|
||||
lines and so on.
|
||||
The result is an RFC 822 message.
|
||||
.SH "COMMON MBOX VARIANTS"
|
||||
There are many variants of
|
||||
.I mbox
|
||||
format.
|
||||
The variant described above is
|
||||
.I mboxrd
|
||||
format, popularized by Rahul Dhesi in June 1995.
|
||||
|
||||
The original
|
||||
.I mboxo
|
||||
format quotes only
|
||||
.B From_
|
||||
lines, not
|
||||
.B >From_
|
||||
lines.
|
||||
As a result it is impossible to tell whether
|
||||
|
||||
.EX
|
||||
From: djb@silverton.berkeley.edu (D. J. Bernstein)
|
||||
.br
|
||||
To: god@heaven.af.mil
|
||||
.br
|
||||
|
||||
.br
|
||||
>From now through August I'll be doing beta testing.
|
||||
.br
|
||||
Thanks for your interest.
|
||||
.EE
|
||||
|
||||
was quoted in the original message.
|
||||
An
|
||||
.I mboxrd
|
||||
reader will always strip off the quoting.
|
||||
|
||||
.I mboxcl
|
||||
format is like
|
||||
.I mboxo
|
||||
format, but includes a Content-Length field with the
|
||||
number of bytes in the message.
|
||||
.I mboxcl2
|
||||
format is like
|
||||
.I mboxcl
|
||||
but has no
|
||||
.B >From
|
||||
quoting.
|
||||
These formats are used by SVR4 mailers.
|
||||
.I mboxcl2
|
||||
cannot be read safely by
|
||||
.I mboxrd
|
||||
readers.
|
||||
.SH "UNSPECIFIED DETAILS"
|
||||
There are many locking mechanisms for
|
||||
.I mbox
|
||||
files.
|
||||
.B qmail-local
|
||||
always uses
|
||||
.B flock
|
||||
on systems that have it, otherwise
|
||||
.BR lockf .
|
||||
|
||||
The delivery date in a
|
||||
.B From_
|
||||
line does not specify a time zone.
|
||||
.B qmail-local
|
||||
always creates the delivery date in GMT
|
||||
so that
|
||||
.I mbox
|
||||
files can be safely transported from one time zone to another.
|
||||
|
||||
If the mtime on a nonempty
|
||||
.I mbox
|
||||
file is greater than the atime,
|
||||
the file has new mail.
|
||||
If the mtime is smaller than the atime,
|
||||
the new mail has been read.
|
||||
If the atime equals the mtime,
|
||||
there is no way to tell whether the file has new mail,
|
||||
since
|
||||
.B qmail-local
|
||||
takes much less than a second to run.
|
||||
One solution is for a mail reader to artificially set the
|
||||
atime to the mtime plus 1.
|
||||
Then the file has new mail if and only if the atime is
|
||||
less than or equal to the mtime.
|
||||
|
||||
Some mail readers place
|
||||
.B Status
|
||||
fields in each message to indicate which messages have been read.
|
||||
.SH "SEE ALSO"
|
||||
maildir(5),
|
||||
qmail-header(5),
|
||||
qmail-local(8)
|
|
@ -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,90 +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_specialized("set_flags".into(), fut);
|
||||
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 == crate::conf::SearchBackend::Sqlite3 {
|
||||
let msg_id = env.message_id_display().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_specialized("sqlite3::remove".into(), fut);
|
||||
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,222 +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};
|
||||
|
||||
pub enum JobRequest {
|
||||
Mailboxes {
|
||||
handle: JoinHandle<Result<HashMap<MailboxHash, Mailbox>>>,
|
||||
},
|
||||
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<()>>,
|
||||
},
|
||||
CreateMailbox {
|
||||
path: String,
|
||||
handle: JoinHandle<Result<(MailboxHash, HashMap<MailboxHash, Mailbox>)>>,
|
||||
},
|
||||
DeleteMailbox {
|
||||
mailbox_hash: MailboxHash,
|
||||
handle: JoinHandle<Result<HashMap<MailboxHash, Mailbox>>>,
|
||||
},
|
||||
//RenameMailbox,
|
||||
SetMailboxPermissions {
|
||||
mailbox_hash: MailboxHash,
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
SetMailboxSubscription {
|
||||
mailbox_hash: MailboxHash,
|
||||
new_value: bool,
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
Watch {
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Drop for JobRequest {
|
||||
fn drop(&mut self) {
|
||||
match self {
|
||||
Self::Generic { handle, .. } |
|
||||
Self::IsOnline { handle, .. } |
|
||||
Self::Refresh { handle, .. } |
|
||||
Self::SetFlags { handle, .. } |
|
||||
Self::SaveMessage { handle, .. } |
|
||||
//JobRequest::RenameMailbox,
|
||||
Self::SetMailboxPermissions { handle, .. } |
|
||||
Self::SetMailboxSubscription { handle, .. } |
|
||||
Self::Watch { handle, .. } |
|
||||
Self::SendMessageBackground { handle, .. } => {
|
||||
handle.cancel();
|
||||
}
|
||||
Self::DeleteMessages { handle, .. } => {
|
||||
handle.cancel();
|
||||
}
|
||||
Self::CreateMailbox { handle, .. } => {
|
||||
handle.cancel();
|
||||
}
|
||||
Self::DeleteMailbox { handle, .. } => {
|
||||
handle.cancel();
|
||||
}
|
||||
Self::Fetch { handle, .. } => {
|
||||
handle.cancel();
|
||||
}
|
||||
Self::Mailboxes { handle, .. } => {
|
||||
handle.cancel();
|
||||
}
|
||||
Self::SendMessage => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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::Mailboxes { .. } => write!(f, "JobRequest::Mailboxes"),
|
||||
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::CreateMailbox { .. } => write!(f, "JobRequest::CreateMailbox"),
|
||||
Self::DeleteMailbox { mailbox_hash, .. } => {
|
||||
write!(f, "JobRequest::DeleteMailbox({})", mailbox_hash)
|
||||
}
|
||||
//JobRequest::RenameMailbox,
|
||||
Self::SetMailboxPermissions { .. } => {
|
||||
write!(f, "JobRequest::SetMailboxPermissions")
|
||||
}
|
||||
Self::SetMailboxSubscription { .. } => {
|
||||
write!(f, "JobRequest::SetMailboxSubscription")
|
||||
}
|
||||
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::Mailboxes { .. } => write!(f, "Get mailbox list"),
|
||||
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::CreateMailbox { path, .. } => write!(f, "Create mailbox {}", path),
|
||||
Self::DeleteMailbox { .. } => write!(f, "Delete mailbox"),
|
||||
//JobRequest::RenameMailbox,
|
||||
Self::SetMailboxPermissions { .. } => write!(f, "Set mailbox permissions"),
|
||||
Self::SetMailboxSubscription { .. } => write!(f, "Set mailbox subscription"),
|
||||
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 { .. } }
|
||||
|
||||
pub fn is_fetch(&self, mailbox_hash: MailboxHash) -> bool {
|
||||
matches!(self, Self::Fetch {
|
||||
mailbox_hash: h, ..
|
||||
} if *h == mailbox_hash)
|
||||
}
|
||||
}
|
|
@ -1,347 +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);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use melib::{
|
||||
backends::{Mailbox, MailboxHash},
|
||||
error::Result,
|
||||
MailboxPermissions, SpecialUsageMailbox,
|
||||
};
|
||||
|
||||
use crate::accounts::{FileMailboxConf, MailboxEntry, MailboxStatus};
|
||||
|
||||
#[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!()
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
229
meli/src/args.rs
|
@ -1,229 +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 super::*;
|
||||
|
||||
#[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
|
||||
#[structopt(display_order = 1)]
|
||||
CreateConfig {
|
||||
#[structopt(value_name = "NEW_CONFIG_PATH", parse(from_os_str))]
|
||||
path: Option<PathBuf>,
|
||||
},
|
||||
/// test a configuration file for syntax issues or missing options.
|
||||
#[structopt(display_order = 2)]
|
||||
TestConfig {
|
||||
#[structopt(value_name = "CONFIG_PATH", parse(from_os_str))]
|
||||
path: Option<PathBuf>,
|
||||
},
|
||||
#[structopt(visible_alias="docs", aliases=&["docs", "manpage", "manpages"])]
|
||||
#[structopt(display_order = 3)]
|
||||
/// print documentation page and exit (Piping to a pager is recommended.).
|
||||
Man(ManOpt),
|
||||
|
||||
#[structopt(display_order = 4)]
|
||||
/// Install manual pages to the first location provided by $MANPATH /
|
||||
/// manpath(1), unless you specify the directory as an argument.
|
||||
InstallMan {
|
||||
#[structopt(value_name = "DESTINATION_PATH", parse(from_os_str))]
|
||||
destination_path: Option<PathBuf>,
|
||||
},
|
||||
#[structopt(display_order = 5)]
|
||||
/// Print compile time feature flags of this binary
|
||||
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 {
|
||||
#[cfg(feature = "cli-docs")]
|
||||
#[cfg_attr(feature = "cli-docs", structopt(default_value = "meli", possible_values=manpages::POSSIBLE_VALUES, value_name="PAGE", parse(try_from_str = manpages::parse_manpage)))]
|
||||
/// Name of manual page.
|
||||
pub page: manpages::ManPages,
|
||||
/// If true, output text in stdout instead of spawning $PAGER.
|
||||
#[cfg(feature = "cli-docs")]
|
||||
#[cfg_attr(
|
||||
feature = "cli-docs",
|
||||
structopt(long = "no-raw", alias = "no-raw", value_name = "bool")
|
||||
)]
|
||||
pub no_raw: Option<Option<bool>>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "cli-docs")]
|
||||
pub mod manpages {
|
||||
use std::{
|
||||
env, fs,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use melib::log;
|
||||
|
||||
use crate::{Error, Result};
|
||||
|
||||
pub const POSSIBLE_VALUES: &[&str] = &[
|
||||
"meli",
|
||||
"meli.1",
|
||||
"conf",
|
||||
"meli.conf",
|
||||
"meli.conf.5",
|
||||
"themes",
|
||||
"meli-themes",
|
||||
"meli-themes.5",
|
||||
"guide",
|
||||
"meli.7",
|
||||
];
|
||||
|
||||
pub fn parse_manpage(src: &str) -> Result<ManPages> {
|
||||
match src {
|
||||
"" | "meli" | "meli.1" | "main" => Ok(ManPages::Main),
|
||||
"meli.7" | "guide" => Ok(ManPages::Guide),
|
||||
"meli.conf" | "meli.conf.5" | "conf" | "config" | "configuration" => Ok(ManPages::Conf),
|
||||
"meli-themes" | "meli-themes.5" | "themes" | "theming" | "theme" => {
|
||||
Ok(ManPages::Themes)
|
||||
}
|
||||
_ => Err(Error::new(format!("Invalid documentation page: {src}",))),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
/// Choose manpage
|
||||
pub enum ManPages {
|
||||
/// meli(1)
|
||||
Main = 0,
|
||||
/// meli.conf(5)
|
||||
Conf = 1,
|
||||
/// meli-themes(5)
|
||||
Themes = 2,
|
||||
/// meli(7)
|
||||
Guide = 3,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ManPages {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(
|
||||
fmt,
|
||||
"{}",
|
||||
match self {
|
||||
Self::Main => "meli.1",
|
||||
Self::Conf => "meli.conf.5",
|
||||
Self::Themes => "meli-themes.5",
|
||||
Self::Guide => "meli.7",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ManPages {
|
||||
pub fn install(destination: Option<PathBuf>) -> Result<PathBuf> {
|
||||
fn path_valid(p: &Path, tries: &mut Vec<PathBuf>) -> bool {
|
||||
tries.push(p.into());
|
||||
p.exists()
|
||||
&& p.is_dir()
|
||||
&& fs::metadata(p)
|
||||
.ok()
|
||||
.map(|m| !m.permissions().readonly())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
let mut tries = vec![];
|
||||
let Some(mut path) = destination
|
||||
.filter(|p| path_valid(p, &mut tries))
|
||||
.or_else(|| {
|
||||
if let Some(paths) = env::var_os("MANPATH") {
|
||||
if let Some(path) =
|
||||
env::split_paths(&paths).find(|p| path_valid(p, &mut tries))
|
||||
{
|
||||
return Some(path);
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
.or_else(|| {
|
||||
#[allow(deprecated)]
|
||||
env::home_dir()
|
||||
.map(|p| p.join(".local").join("share").join("man"))
|
||||
.filter(|p| path_valid(p, &mut tries))
|
||||
})
|
||||
else {
|
||||
return Err(format!("Could not write to any of these paths: {:?}", tries).into());
|
||||
};
|
||||
|
||||
for (p, dir) in [
|
||||
(Self::Main, "man1"),
|
||||
(Self::Conf, "man5"),
|
||||
(Self::Themes, "man5"),
|
||||
(Self::Guide, "man7"),
|
||||
] {
|
||||
let text = crate::subcommands::man(p, true)?;
|
||||
path.push(dir);
|
||||
std::fs::create_dir_all(&path).map_err(|err| {
|
||||
Error::new(format!("Could not create {} directory.", path.display()))
|
||||
.set_source(Some(Arc::new(err)))
|
||||
})?;
|
||||
path.push(&p.to_string());
|
||||
|
||||
fs::write(&path, text.as_bytes()).map_err(|err| {
|
||||
Error::new(format!("Could not write to {}", path.display()))
|
||||
.set_source(Some(Arc::new(err)))
|
||||
})?;
|
||||
log::trace!("Installed {} to {}", p, path.display());
|
||||
path.pop();
|
||||
path.pop();
|
||||
}
|
||||
|
||||
Ok(path)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,688 +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,
|
||||
};
|
||||
|
||||
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, *},
|
||||
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);
|
||||
}
|
||||
}
|
||||
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 = "";
|
||||
}
|
||||
}
|
||||
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]),
|
||||
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: ["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: ["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).into_iter());
|
||||
}
|
||||
}
|
||||
sugg.into_iter()
|
||||
.map(|s| format!("{}{}", input, s.as_str()))
|
||||
.collect::<Vec<String>>()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_command_parser() {
|
||||
let mut input = "sort".to_string();
|
||||
macro_rules! match_input {
|
||||
($input:expr) => {{
|
||||
let mut sugg: 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_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: "Bad argument for `set`. Accepted arguments are [seen, unseen, plain, \
|
||||
threaded, compact, conversations]."
|
||||
.into(),
|
||||
}
|
||||
.to_string(),
|
||||
);
|
||||
assert_eq!(
|
||||
parse_command(b"moveto ").unwrap_err().to_string(),
|
||||
WrongNumberOfArguments {
|
||||
too_many: false,
|
||||
takes: (1, Some(1)),
|
||||
given: 0,
|
||||
__func__: "moveto",
|
||||
inner: "".into(),
|
||||
}
|
||||
.to_string(),
|
||||
);
|
||||
assert_eq!(
|
||||
parse_command(b"reindex 1 2 3").unwrap_err().to_string(),
|
||||
WrongNumberOfArguments {
|
||||
too_many: true,
|
||||
takes: (1, Some(1)),
|
||||
given: 2,
|
||||
__func__: "reindex",
|
||||
inner: "".into(),
|
||||
}
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,123 +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>,
|
||||
},
|
||||
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 } => {
|
||||
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,43 +0,0 @@
|
|||
// @generated
|
||||
/*
|
||||
* meli - conf/overrides.rs
|
||||
*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
#![allow(clippy::derivable_impls)]
|
||||
|
||||
//! This module is automatically generated by `config_macros.rs`.
|
||||
|
||||
use super::*;
|
||||
use melib::HeaderName;
|
||||
|
||||
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct PagerSettingsOverride { # [doc = " Number of context lines when going to next page."] # [doc = " Default: 0"] # [serde (alias = "pager-context")] # [serde (default)] pub pager_context : Option < usize > , # [doc = " Stop at the end instead of displaying next mail."] # [doc = " Default: false"] # [serde (alias = "pager-stop")] # [serde (default)] pub pager_stop : Option < bool > , # [doc = " Always show headers when scrolling."] # [doc = " Default: true"] # [serde (alias = "sticky-headers" , alias = "headers-sticky" , alias = "headers_sticky")] # [serde (default)] pub sticky_headers : Option < bool > , # [doc = " The height of the pager in mail view, in percent."] # [doc = " Default: 80"] # [serde (alias = "pager-ratio")] # [serde (default)] pub pager_ratio : Option < usize > , # [doc = " A command to pipe mail output through for viewing in pager."] # [doc = " Default: None"] # [serde (deserialize_with = "non_empty_opt_string")] # [serde (default)] pub filter : Option < Option < String > > , # [doc = " A command to pipe html output before displaying it in a pager"] # [doc = " Default: None"] # [serde (deserialize_with = "non_empty_opt_string" , alias = "html-filter")] # [serde (default)] pub html_filter : Option < Option < String > > , # [doc = " Respect \"format=flowed\""] # [doc = " Default: true"] # [serde (alias = "format-flowed")] # [serde (default)] pub format_flowed : Option < bool > , # [doc = " Split long lines that would overflow on the x axis."] # [doc = " Default: true"] # [serde (alias = "split-long-lines")] # [serde (default)] pub split_long_lines : Option < bool > , # [doc = " Minimum text width in columns."] # [doc = " Default: 80"] # [serde (alias = "minimum-width")] # [serde (default)] pub minimum_width : Option < usize > , # [doc = " Choose `text/html` alternative if `text/plain` is empty in"] # [doc = " `multipart/alternative` attachments."] # [doc = " Default: true"] # [serde (alias = "auto-choose-multipart-alternative")] # [serde (default)] pub auto_choose_multipart_alternative : Option < ToggleFlag > , # [doc = " Show Date: in my timezone"] # [doc = " Default: true"] # [serde (alias = "show-date-in-my-timezone")] # [serde (default)] pub show_date_in_my_timezone : Option < ToggleFlag > , # [doc = " A command to launch URLs with. The URL will be given as the first"] # [doc = " argument of the command. Default: None"] # [serde (deserialize_with = "non_empty_opt_string")] # [serde (default)] pub url_launcher : Option < Option < String > > , # [doc = " A command to open html files."] # [doc = " Default: None"] # [serde (deserialize_with = "non_empty_opt_string" , alias = "html-open")] # [serde (default)] pub html_open : Option < Option < String > > , # [doc = " Extra headers to display, if present, in the default header preamble."] # [doc = " Default: []"] # [serde (alias = "show-extra-headers")] # [serde (default)] pub show_extra_headers : Option < Vec < String > > } impl Default for PagerSettingsOverride { fn default () -> Self { Self { pager_context : None , pager_stop : None , sticky_headers : None , pager_ratio : None , filter : None , html_filter : None , format_flowed : None , split_long_lines : None , minimum_width : None , auto_choose_multipart_alternative : None , show_date_in_my_timezone : None , url_launcher : None , html_open : None , show_extra_headers : None } } }
|
||||
|
||||
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct ListingSettingsOverride { # [doc = " Number of context lines when going to next page."] # [doc = " Default: 0"] # [serde (alias = "context-lines")] # [serde (default)] pub context_lines : Option < usize > , # [doc = " Show auto-hiding scrollbar in accounts sidebar menu."] # [doc = " Default: True"] # [serde (default)] pub show_menu_scrollbar : Option < bool > , # [doc = " Datetime formatting passed verbatim to strftime(3)."] # [doc = " Default: %Y-%m-%d %T"] # [serde (alias = "datetime-fmt")] # [serde (default)] pub datetime_fmt : Option < Option < String > > , # [doc = " Show recent dates as `X {minutes,hours,days} ago`, up to 7 days."] # [doc = " Default: true"] # [serde (alias = "recent-dates")] # [serde (default)] pub recent_dates : Option < bool > , # [doc = " Show only envelopes that match this query"] # [doc = " Default: None"] # [serde (default)] pub filter : Option < Option < Query > > , # [serde (alias = "index-style")] # [serde (default)] pub index_style : Option < IndexStyle > , # [doc = " Default: \" \""] # [serde (default)] pub sidebar_mailbox_tree_has_sibling : Option < Option < String > > , # [doc = " Default: \" \""] # [serde (default)] pub sidebar_mailbox_tree_no_sibling : Option < Option < String > > , # [doc = " Default: \" \""] # [serde (default)] pub sidebar_mailbox_tree_has_sibling_leaf : Option < Option < String > > , # [doc = " Default: \" \""] # [serde (default)] pub sidebar_mailbox_tree_no_sibling_leaf : Option < Option < String > > , # [doc = " Default: ' '"] # [serde (default)] pub sidebar_divider : Option < char > , # [doc = " Default: 90"] # [serde (default)] pub sidebar_ratio : Option < usize > , # [doc = " Flag to show if thread entry contains unseen mail."] # [doc = " Default: \"●\""] # [serde (default)] pub unseen_flag : Option < Option < String > > , # [doc = " Flag to show if thread has been snoozed."] # [doc = " Default: \"💤\""] # [serde (default)] pub thread_snoozed_flag : Option < Option < String > > , # [doc = " Flag to show if thread entry has been selected."] # [doc = " Default: \"☑\u{fe0f}\""] # [serde (default)] pub selected_flag : Option < Option < String > > , # [doc = " Flag to show if thread entry contains attachments."] # [doc = " Default: \"📎\""] # [serde (default)] pub attachment_flag : Option < Option < String > > , # [doc = " Flag to show if any thread entry contains your address as a receiver."] # [doc = " Useful to make mailing list threads that CC you stand out."] # [doc = " Default: \"✸\""] # [serde (default)] pub highlight_self_flag : Option < Option < String > > , # [doc = " Show `highlight_self_flag` or not."] # [doc = " Default: false"] # [serde (default)] pub highlight_self : Option < ToggleFlag > , # [doc = " Should threads with different Subjects show a list of those"] # [doc = " subjects on the entry title?"] # [doc = " Default: \"true\""] # [serde (default)] pub thread_subject_pack : Option < bool > , # [doc = " In threaded listing style, repeat identical From column values within a"] # [doc = " thread. Not repeating adds empty space in the From column which"] # [doc = " might result in less visual clutter."] # [doc = " Default: \"false\""] # [serde (default)] pub threaded_repeat_identical_from_values : Option < bool > , # [doc = " Show relative indices in menu mailboxes to quickly help with jumping to"] # [doc = " them. Default: \"true\""] # [serde (alias = "relative-menu-indices")] # [serde (default)] pub relative_menu_indices : Option < bool > , # [doc = " Show relative indices in listings to quickly help with jumping to"] # [doc = " them. Default: \"true\""] # [serde (alias = "relative-list-indices")] # [serde (default)] pub relative_list_indices : Option < bool > , # [doc = " Hide sidebar on launch. Default: \"false\""] # [serde (alias = "hide-sidebar-on-launch")] # [serde (default)] pub hide_sidebar_on_launch : Option < bool > } impl Default for ListingSettingsOverride { fn default () -> Self { Self { context_lines : None , show_menu_scrollbar : None , datetime_fmt : None , recent_dates : None , filter : None , index_style : None , sidebar_mailbox_tree_has_sibling : None , sidebar_mailbox_tree_no_sibling : None , sidebar_mailbox_tree_has_sibling_leaf : None , sidebar_mailbox_tree_no_sibling_leaf : None , sidebar_divider : None , sidebar_ratio : None , unseen_flag : None , thread_snoozed_flag : None , selected_flag : None , attachment_flag : None , highlight_self_flag : None , highlight_self : None , thread_subject_pack : None , threaded_repeat_identical_from_values : None , relative_menu_indices : None , relative_list_indices : None , hide_sidebar_on_launch : None } } }
|
||||
|
||||
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct NotificationsSettingsOverride { # [doc = " Enable notifications."] # [doc = " Default: True"] # [serde (default)] pub enable : Option < bool > , # [doc = " A command to pipe notifications through."] # [doc = " Default: None"] # [serde (default)] pub script : Option < Option < String > > , # [doc = " A command to pipe new mail notifications through (preferred over"] # [doc = " `script`). Default: None"] # [serde (default)] pub new_mail_script : Option < Option < String > > , # [doc = " A file location which has its size changed when new mail arrives (max"] # [doc = " 128 bytes). Can be used to trigger new mail notifications eg with"] # [doc = " `xbiff(1)`. Default: None"] # [serde (alias = "xbiff-file-path")] # [serde (default)] pub xbiff_file_path : Option < Option < String > > , # [serde (alias = "play-sound")] # [serde (default)] pub play_sound : Option < ToggleFlag > , # [serde (alias = "sound-file")] # [serde (default)] pub sound_file : Option < Option < String > > } impl Default for NotificationsSettingsOverride { fn default () -> Self { Self { enable : None , script : None , new_mail_script : None , xbiff_file_path : None , play_sound : None , sound_file : None } } }
|
||||
|
||||
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct ShortcutsOverride { # [serde (default)] pub general : Option < GeneralShortcuts > , # [serde (default)] pub listing : Option < ListingShortcuts > , # [serde (default)] pub composing : Option < ComposingShortcuts > , # [serde (alias = "contact-list")] # [serde (default)] pub contact_list : Option < ContactListShortcuts > , # [serde (alias = "envelope-view")] # [serde (default)] pub envelope_view : Option < EnvelopeViewShortcuts > , # [serde (alias = "thread-view")] # [serde (default)] pub thread_view : Option < ThreadViewShortcuts > , # [serde (default)] pub pager : Option < PagerShortcuts > } impl Default for ShortcutsOverride { fn default () -> Self { Self { general : None , listing : None , composing : None , contact_list : None , envelope_view : None , thread_view : None , pager : None } } }
|
||||
|
||||
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct ComposingSettingsOverride { # [doc = " A command to pipe new emails to"] # [doc = " Required"] # [serde (default)] pub send_mail : Option < SendMail > , # [doc = " Command to launch editor. Can have arguments. Draft filename is given as"] # [doc = " the last argument. If it's missing, the environment variable $EDITOR is"] # [doc = " looked up."] # [serde (alias = "editor-command" , alias = "editor-cmd" , alias = "editor_cmd")] # [serde (default)] pub editor_command : Option < Option < String > > , # [doc = " Embedded editor (for terminal interfaces) instead of forking and"] # [doc = " waiting."] # [serde (alias = "embed")] # [serde (default)] pub embedded_pty : Option < bool > , # [doc = " Set \"format=flowed\" in plain text attachments."] # [doc = " Default: true"] # [serde (alias = "format-flowed")] # [serde (default)] pub format_flowed : Option < bool > , # [doc = " Set User-Agent"] # [doc = " Default: empty"] # [serde (alias = "insert_user_agent")] # [serde (default)] pub insert_user_agent : Option < bool > , # [doc = " Set default header values for new drafts"] # [doc = " Default: empty"] # [serde (alias = "default-header-values")] # [serde (default)] pub default_header_values : Option < HashMap < HeaderName , String > > , # [doc = " Wrap header preamble when editing a draft in an editor. This allows you"] # [doc = " to write non-plain text email without the preamble creating syntax"] # [doc = " errors. They are stripped when you return from the editor. The"] # [doc = " values should be a two element array of strings, a prefix and suffix."] # [doc = " Default: None"] # [serde (alias = "wrap-header-preamble")] # [serde (default)] pub wrap_header_preamble : Option < Option < (String , String) > > , # [doc = " Store sent mail after successful submission. This setting is meant to be"] # [doc = " disabled for non-standard behaviour in gmail, which auto-saves sent"] # [doc = " mail on its own. Default: true"] # [serde (default)] pub store_sent_mail : Option < bool > , # [doc = " The attribution line appears above the quoted reply text."] # [doc = " The format specifiers for the replied address are:"] # [doc = " - `%+f` — the sender's name and email address."] # [doc = " - `%+n` — the sender's name (or email address, if no name is included)."] # [doc = " - `%+a` — the sender's email address."] # [doc = " The format string is passed to strftime(3) with the replied envelope's"] # [doc = " date. Default: \"On %a, %0e %b %Y %H:%M, %+f wrote:%n\""] # [serde (default)] pub attribution_format_string : Option < Option < String > > , # [doc = " Whether the strftime call for the attribution string uses the POSIX"] # [doc = " locale instead of the user's active locale"] # [doc = " Default: true"] # [serde (default)] pub attribution_use_posix_locale : Option < bool > , # [doc = " Forward emails as attachment? (Alternative is inline)"] # [doc = " Default: ask"] # [serde (alias = "forward-as-attachment")] # [serde (default)] pub forward_as_attachment : Option < ActionFlag > , # [doc = " Alternative lists of reply prefixes (etc. [\"Re:\", \"RE:\", ...]) to strip"] # [doc = " Default: `[\"Re:\", \"RE:\", \"Fwd:\", \"Fw:\", \"回复:\", \"回覆:\", \"SV:\", \"Sv:\","] # [doc = " \"VS:\", \"Antw:\", \"Doorst:\", \"VS:\", \"VL:\", \"REF:\", \"TR:\", \"TR:\", \"AW:\","] # [doc = " \"WG:\", \"ΑΠ:\", \"Απ:\", \"απ:\", \"ΠΡΘ:\", \"Πρθ:\", \"πρθ:\", \"ΣΧΕΤ:\", \"Σχετ:\","] # [doc = " \"σχετ:\", \"ΠΡΘ:\", \"Πρθ:\", \"πρθ:\", \"Vá:\", \"Továbbítás:\", \"R:\", \"I:\","] # [doc = " \"RIF:\", \"FS:\", \"BLS:\", \"TRS:\", \"VS:\", \"VB:\", \"RV:\", \"RES:\", \"Res\","] # [doc = " \"ENC:\", \"Odp:\", \"PD:\", \"YNT:\", \"İLT:\", \"ATB:\", \"YML:\"]`"] # [serde (alias = "reply-prefix-list-to-strip")] # [serde (default)] pub reply_prefix_list_to_strip : Option < Option < Vec < String > > > , # [doc = " The prefix to use in reply subjects. The de facto prefix is \"Re:\"."] # [serde (alias = "reply-prefix")] # [serde (default)] pub reply_prefix : Option < String > , # [doc = " Custom `compose-hooks`."] # [serde (alias = "custom-compose-hooks")] # [serde (default)] pub custom_compose_hooks : Option < Vec < ComposeHook > > , # [doc = " Disabled `compose-hooks`."] # [serde (alias = "disabled-compose-hooks")] # [serde (default)] pub disabled_compose_hooks : Option < Vec < String > > } impl Default for ComposingSettingsOverride { fn default () -> Self { Self { send_mail : None , editor_command : None , embedded_pty : None , format_flowed : None , insert_user_agent : None , default_header_values : None , wrap_header_preamble : None , store_sent_mail : None , attribution_format_string : None , attribution_use_posix_locale : None , forward_as_attachment : None , reply_prefix_list_to_strip : None , reply_prefix : None , custom_compose_hooks : None , disabled_compose_hooks : None } } }
|
||||
|
||||
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct TagsSettingsOverride { # [serde (deserialize_with = "tag_color_de")] # [serde (default)] pub colors : Option < HashMap < TagHash , Color > > , # [serde (deserialize_with = "tag_set_de" , alias = "ignore-tags")] # [serde (default)] pub ignore_tags : Option < HashSet < TagHash > > } impl Default for TagsSettingsOverride { fn default () -> Self { Self { colors : None , ignore_tags : None } } }
|
||||
|
||||
# [derive (Debug , Serialize , Deserialize , Clone)] # [serde (deny_unknown_fields)] pub struct PGPSettingsOverride { # [doc = " auto verify signed e-mail according to RFC3156"] # [doc = " Default: true"] # [serde (alias = "auto-verify-signatures")] # [serde (default)] pub auto_verify_signatures : Option < ActionFlag > , # [doc = " auto decrypt encrypted e-mail"] # [doc = " Default: true"] # [serde (alias = "auto-decrypt")] # [serde (default)] pub auto_decrypt : Option < ActionFlag > , # [doc = " always sign sent e-mail"] # [doc = " Default: false"] # [serde (alias = "auto-sign")] # [serde (default)] pub auto_sign : Option < ActionFlag > , # [doc = " Auto encrypt sent e-mail"] # [doc = " Default: false"] # [serde (alias = "auto-encrypt")] # [serde (default)] pub auto_encrypt : Option < ActionFlag > , # [doc = " Default: None"] # [serde (alias = "sign-key")] # [serde (default)] pub sign_key : Option < Option < String > > , # [doc = " Default: None"] # [serde (alias = "decrypt-key")] # [serde (default)] pub decrypt_key : Option < Option < String > > , # [doc = " Default: None"] # [serde (alias = "encrypt-key")] # [serde (default)] pub encrypt_key : Option < Option < String > > , # [doc = " Allow remote lookups"] # [doc = " Default: False"] # [serde (alias = "allow-remote-lookups")] # [serde (default)] pub allow_remote_lookup : Option < ActionFlag > , # [doc = " Remote lookup mechanisms."] # [doc = " Default: \"local,wkd\""] # [cfg_attr (feature = "gpgme" , serde (alias = "remote-lookup-mechanisms"))] # [cfg (feature = "gpgme")] # [serde (default)] pub remote_lookup_mechanisms : Option < melib :: gpgme :: LocateKey > , # [cfg (not (feature = "gpgme"))] # [cfg_attr (not (feature = "gpgme") , serde (alias = "remote-lookup-mechanisms"))] # [serde (default)] pub remote_lookup_mechanisms : Option < String > } impl Default for PGPSettingsOverride { fn default () -> Self { Self { auto_verify_signatures : None , auto_decrypt : None , auto_sign : None , auto_encrypt : None , sign_key : None , decrypt_key : None , encrypt_key : None , allow_remote_lookup : None , remote_lookup_mechanisms : None } } }
|
||||
|
|
@ -1,23 +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/>.
|
||||
*/
|
||||
|
||||
pub mod editor;
|
||||
pub mod list;
|
|
@ -1,638 +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::borrow::Cow;
|
||||
|
||||
use indexmap::IndexMap;
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
jobs::{JobId, JobMetadata},
|
||||
melib::{
|
||||
utils::datetime::{self, formats::RFC3339_DATETIME_AND_SPACE},
|
||||
SortOrder,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
#[repr(u8)]
|
||||
enum Column {
|
||||
_0 = 0,
|
||||
_1,
|
||||
_2,
|
||||
_3,
|
||||
_4,
|
||||
}
|
||||
|
||||
const fn _assert_len() {
|
||||
if JobManager::HEADERS.len() != Column::_4 as usize + 1 {
|
||||
panic!("JobManager::HEADERS length changed, please update Column enum accordingly.");
|
||||
}
|
||||
}
|
||||
|
||||
const _: () = _assert_len();
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct JobManager {
|
||||
cursor_pos: usize,
|
||||
new_cursor_pos: usize,
|
||||
length: usize,
|
||||
data_columns: DataColumns<5>,
|
||||
min_width: [usize; 5],
|
||||
sort_col: Column,
|
||||
sort_order: SortOrder,
|
||||
entries: IndexMap<JobId, JobMetadata>,
|
||||
|
||||
initialized: bool,
|
||||
theme_default: ThemeAttribute,
|
||||
highlight_theme: ThemeAttribute,
|
||||
|
||||
dirty: bool,
|
||||
|
||||
movement: Option<PageMovement>,
|
||||
id: ComponentId,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for JobManager {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(f, "jobs")
|
||||
}
|
||||
}
|
||||
|
||||
impl JobManager {
|
||||
const HEADERS: [&'static str; 5] = ["id", "desc", "started", "finished", "succeeded"];
|
||||
|
||||
pub fn new(context: &Context) -> Self {
|
||||
let theme_default = crate::conf::value(context, "theme_default");
|
||||
let highlight_theme = if context.settings.terminal.use_color() {
|
||||
crate::conf::value(context, "highlight")
|
||||
} else {
|
||||
ThemeAttribute {
|
||||
attrs: Attr::REVERSE,
|
||||
..ThemeAttribute::default()
|
||||
}
|
||||
};
|
||||
let mut data_columns = DataColumns::default();
|
||||
data_columns.theme_config.set_single_theme(theme_default);
|
||||
Self {
|
||||
cursor_pos: 0,
|
||||
new_cursor_pos: 0,
|
||||
entries: IndexMap::default(),
|
||||
length: 0,
|
||||
data_columns,
|
||||
min_width: [0; 5],
|
||||
sort_col: Column::_2,
|
||||
sort_order: SortOrder::Desc,
|
||||
theme_default,
|
||||
highlight_theme,
|
||||
initialized: false,
|
||||
dirty: true,
|
||||
movement: None,
|
||||
id: ComponentId::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn initialize(&mut self, context: &Context) {
|
||||
self.set_dirty(true);
|
||||
|
||||
let mut entries = (*context.main_loop_handler.job_executor.jobs.lock().unwrap()).clone();
|
||||
|
||||
self.length = entries.len();
|
||||
entries.sort_by(|_, a, _, b| match (self.sort_col, self.sort_order) {
|
||||
(Column::_0, SortOrder::Asc) => a.id.cmp(&b.id),
|
||||
(Column::_0, SortOrder::Desc) => b.id.cmp(&b.id),
|
||||
(Column::_1, SortOrder::Asc) => a.desc.cmp(&b.desc),
|
||||
(Column::_1, SortOrder::Desc) => b.desc.cmp(&a.desc),
|
||||
(Column::_2, SortOrder::Asc) => a.started.cmp(&b.started),
|
||||
(Column::_2, SortOrder::Desc) => b.started.cmp(&a.started),
|
||||
(Column::_3, SortOrder::Asc) => a.finished.cmp(&b.finished),
|
||||
(Column::_3, SortOrder::Desc) => b.finished.cmp(&a.finished),
|
||||
(Column::_4, SortOrder::Asc) if a.finished.is_some() && b.finished.is_some() => {
|
||||
a.succeeded.cmp(&b.succeeded)
|
||||
}
|
||||
(Column::_4, SortOrder::Desc) if a.finished.is_some() && b.finished.is_some() => {
|
||||
b.succeeded.cmp(&a.succeeded)
|
||||
}
|
||||
(Column::_4, SortOrder::Asc) if a.finished.is_none() => std::cmp::Ordering::Less,
|
||||
(Column::_4, SortOrder::Asc) => std::cmp::Ordering::Greater,
|
||||
(Column::_4, SortOrder::Desc) if a.finished.is_none() => std::cmp::Ordering::Greater,
|
||||
(Column::_4, SortOrder::Desc) => std::cmp::Ordering::Less,
|
||||
});
|
||||
self.entries = entries;
|
||||
|
||||
macro_rules! hdr {
|
||||
($idx:literal) => {{
|
||||
Self::HEADERS[$idx].len() + if self.sort_col as u8 == $idx { 1 } else { 0 }
|
||||
}};
|
||||
}
|
||||
self.min_width = [hdr!(0), hdr!(1), hdr!(2), hdr!(3), hdr!(4)];
|
||||
|
||||
for c in self.entries.values() {
|
||||
// title
|
||||
self.min_width[0] = self.min_width[0].max(c.id.to_string().len());
|
||||
// desc
|
||||
self.min_width[1] = self.min_width[1].max(c.desc.len());
|
||||
}
|
||||
self.min_width[2] = "1970-01-01 00:00:00".len();
|
||||
self.min_width[3] = self.min_width[2];
|
||||
|
||||
// name column
|
||||
_ = self.data_columns.columns[0].resize_with_context(
|
||||
self.min_width[0],
|
||||
self.length,
|
||||
context,
|
||||
);
|
||||
self.data_columns.columns[0].grid_mut().clear(None);
|
||||
// path column
|
||||
_ = self.data_columns.columns[1].resize_with_context(
|
||||
self.min_width[1],
|
||||
self.length,
|
||||
context,
|
||||
);
|
||||
self.data_columns.columns[1].grid_mut().clear(None);
|
||||
// size column
|
||||
_ = self.data_columns.columns[2].resize_with_context(
|
||||
self.min_width[2],
|
||||
self.length,
|
||||
context,
|
||||
);
|
||||
self.data_columns.columns[2].grid_mut().clear(None);
|
||||
// subscribed column
|
||||
_ = self.data_columns.columns[3].resize_with_context(
|
||||
self.min_width[3],
|
||||
self.length,
|
||||
context,
|
||||
);
|
||||
self.data_columns.columns[3].grid_mut().clear(None);
|
||||
_ = self.data_columns.columns[4].resize_with_context(
|
||||
self.min_width[4],
|
||||
self.length,
|
||||
context,
|
||||
);
|
||||
self.data_columns.columns[4].grid_mut().clear(None);
|
||||
|
||||
for (idx, e) in self.entries.values().enumerate() {
|
||||
{
|
||||
let area = self.data_columns.columns[0].area().nth_row(idx);
|
||||
self.data_columns.columns[0].grid_mut().write_string(
|
||||
&e.id.to_string(),
|
||||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let area = self.data_columns.columns[1].area().nth_row(idx);
|
||||
self.data_columns.columns[1].grid_mut().write_string(
|
||||
&e.desc,
|
||||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let area = self.data_columns.columns[2].area().nth_row(idx);
|
||||
self.data_columns.columns[2].grid_mut().write_string(
|
||||
&datetime::timestamp_to_string(
|
||||
e.started,
|
||||
Some(RFC3339_DATETIME_AND_SPACE),
|
||||
true,
|
||||
),
|
||||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let area = self.data_columns.columns[3].area().nth_row(idx);
|
||||
self.data_columns.columns[3].grid_mut().write_string(
|
||||
&if let Some(t) = e.finished {
|
||||
Cow::Owned(datetime::timestamp_to_string(
|
||||
t,
|
||||
Some(RFC3339_DATETIME_AND_SPACE),
|
||||
true,
|
||||
))
|
||||
} else {
|
||||
Cow::Borrowed("null")
|
||||
},
|
||||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let area = self.data_columns.columns[4].area().nth_row(idx);
|
||||
self.data_columns.columns[4].grid_mut().write_string(
|
||||
&if e.finished.is_some() {
|
||||
Cow::Owned(format!("{:?}", e.succeeded))
|
||||
} else {
|
||||
Cow::Borrowed("-")
|
||||
},
|
||||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if self.length == 0 {
|
||||
let message = "No jobs.".to_string();
|
||||
if self.data_columns.columns[0].resize_with_context(message.len(), self.length, context)
|
||||
{
|
||||
let area = self.data_columns.columns[0].area();
|
||||
self.data_columns.columns[0].grid_mut().write_string(
|
||||
&message,
|
||||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs,
|
||||
area,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_list(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
|
||||
let rows = area.height();
|
||||
if rows == 0 {
|
||||
return;
|
||||
}
|
||||
if self.length == 0 {
|
||||
grid.clear_area(area, self.theme_default);
|
||||
|
||||
grid.copy_area(
|
||||
self.data_columns.columns[0].grid(),
|
||||
area,
|
||||
self.data_columns.columns[0].area(),
|
||||
);
|
||||
context.dirty_areas.push_back(area);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(mvm) = self.movement.take() {
|
||||
match mvm {
|
||||
PageMovement::Up(amount) => {
|
||||
self.new_cursor_pos = self.new_cursor_pos.saturating_sub(amount);
|
||||
}
|
||||
PageMovement::PageUp(multiplier) => {
|
||||
self.new_cursor_pos = self.new_cursor_pos.saturating_sub(rows * multiplier);
|
||||
}
|
||||
PageMovement::Down(amount) => {
|
||||
if self.new_cursor_pos + amount < self.length {
|
||||
self.new_cursor_pos += amount;
|
||||
} else {
|
||||
self.new_cursor_pos = self.length - 1;
|
||||
}
|
||||
}
|
||||
PageMovement::PageDown(multiplier) => {
|
||||
#[allow(clippy::comparison_chain)]
|
||||
if self.new_cursor_pos + rows * multiplier < self.length {
|
||||
self.new_cursor_pos += rows * multiplier;
|
||||
} else if self.new_cursor_pos + rows * multiplier > self.length {
|
||||
self.new_cursor_pos = self.length - 1;
|
||||
} else {
|
||||
self.new_cursor_pos = (self.length / rows) * rows;
|
||||
}
|
||||
}
|
||||
PageMovement::Right(amount) => {
|
||||
self.data_columns.x_offset += amount;
|
||||
self.data_columns.x_offset = self.data_columns.x_offset.min(
|
||||
self.data_columns
|
||||
.widths
|
||||
.iter()
|
||||
.map(|w| w + 2)
|
||||
.sum::<usize>()
|
||||
.saturating_sub(2),
|
||||
);
|
||||
}
|
||||
PageMovement::Left(amount) => {
|
||||
self.data_columns.x_offset = self.data_columns.x_offset.saturating_sub(amount);
|
||||
}
|
||||
PageMovement::Home => {
|
||||
self.new_cursor_pos = 0;
|
||||
}
|
||||
PageMovement::End => {
|
||||
self.new_cursor_pos = self.length - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let prev_page_no = (self.cursor_pos).wrapping_div(rows);
|
||||
let page_no = (self.new_cursor_pos).wrapping_div(rows);
|
||||
|
||||
let top_idx = page_no * rows;
|
||||
|
||||
if self.length >= rows {
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
|
||||
ScrollUpdate::Update {
|
||||
id: self.id,
|
||||
context: ScrollContext {
|
||||
shown_lines: top_idx + rows,
|
||||
total_lines: self.length,
|
||||
has_more_lines: false,
|
||||
},
|
||||
},
|
||||
)));
|
||||
} else {
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
|
||||
ScrollUpdate::End(self.id),
|
||||
)));
|
||||
}
|
||||
|
||||
// If cursor position has changed, remove the highlight from the previous
|
||||
// position and apply it in the new one.
|
||||
if self.cursor_pos != self.new_cursor_pos && prev_page_no == page_no {
|
||||
let old_cursor_pos = self.cursor_pos;
|
||||
self.cursor_pos = self.new_cursor_pos;
|
||||
for &(idx, highlight) in &[(old_cursor_pos, false), (self.new_cursor_pos, true)] {
|
||||
if idx >= self.length {
|
||||
continue; //bounds check
|
||||
}
|
||||
let new_area = area.nth_row(idx % rows);
|
||||
self.data_columns
|
||||
.draw(grid, idx, self.cursor_pos, grid.bounds_iter(new_area));
|
||||
let row_attr = if highlight {
|
||||
self.highlight_theme
|
||||
} else {
|
||||
self.theme_default
|
||||
};
|
||||
grid.change_theme(new_area, row_attr);
|
||||
context.dirty_areas.push_back(new_area);
|
||||
}
|
||||
return;
|
||||
} else if self.cursor_pos != self.new_cursor_pos {
|
||||
self.cursor_pos = self.new_cursor_pos;
|
||||
}
|
||||
if self.new_cursor_pos >= self.length {
|
||||
self.new_cursor_pos = self.length - 1;
|
||||
self.cursor_pos = self.new_cursor_pos;
|
||||
}
|
||||
// Page_no has changed, so draw new page
|
||||
_ = self
|
||||
.data_columns
|
||||
.recalc_widths((area.width(), area.height()), top_idx);
|
||||
grid.clear_area(area, self.theme_default);
|
||||
// copy table columns
|
||||
self.data_columns
|
||||
.draw(grid, top_idx, self.cursor_pos, grid.bounds_iter(area));
|
||||
|
||||
// highlight cursor
|
||||
grid.change_theme(area.nth_row(self.cursor_pos % rows), self.highlight_theme);
|
||||
|
||||
// clear gap if available height is more than count of entries
|
||||
if top_idx + rows > self.length {
|
||||
grid.change_theme(area.skip_rows(self.length - top_idx), self.theme_default);
|
||||
}
|
||||
context.dirty_areas.push_back(area);
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for JobManager {
|
||||
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
|
||||
if !self.is_dirty() {
|
||||
return;
|
||||
}
|
||||
if !self.initialized {
|
||||
self.initialize(context);
|
||||
}
|
||||
if self.dirty {
|
||||
let area = area.nth_row(0);
|
||||
// Draw column headers.
|
||||
grid.clear_area(area, self.theme_default);
|
||||
let mut x_offset = 0;
|
||||
for (i, (h, w)) in Self::HEADERS.iter().zip(self.min_width).enumerate() {
|
||||
grid.write_string(
|
||||
h,
|
||||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs | Attr::BOLD,
|
||||
area.skip_cols(x_offset),
|
||||
None,
|
||||
);
|
||||
if self.sort_col as usize == i {
|
||||
use SortOrder::*;
|
||||
let arrow = match (grid.ascii_drawing, self.sort_order) {
|
||||
(true, Asc) => DataColumns::<5>::ARROW_UP_ASCII,
|
||||
(true, Desc) => DataColumns::<5>::ARROW_DOWN_ASCII,
|
||||
(false, Asc) => DataColumns::<5>::ARROW_UP,
|
||||
(false, Desc) => DataColumns::<5>::ARROW_DOWN,
|
||||
};
|
||||
grid.write_string(
|
||||
arrow,
|
||||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs,
|
||||
area.skip_cols(x_offset + h.len()),
|
||||
None,
|
||||
);
|
||||
}
|
||||
x_offset += w + 2;
|
||||
}
|
||||
context.dirty_areas.push_back(area);
|
||||
}
|
||||
|
||||
self.draw_list(grid, area.skip_rows(1), context);
|
||||
self.dirty = false;
|
||||
}
|
||||
|
||||
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
|
||||
if let UIEvent::ConfigReload { old_settings: _ } = event {
|
||||
self.theme_default = crate::conf::value(context, "theme_default");
|
||||
self.initialized = false;
|
||||
self.set_dirty(true);
|
||||
}
|
||||
|
||||
let shortcuts = self.shortcuts(context);
|
||||
match event {
|
||||
UIEvent::StatusEvent(
|
||||
StatusEvent::JobFinished(_) | StatusEvent::JobCanceled(_) | StatusEvent::NewJob(_),
|
||||
) => {
|
||||
self.initialized = false;
|
||||
self.set_dirty(true);
|
||||
return false;
|
||||
}
|
||||
UIEvent::Action(Action::SortColumn(column, order)) => {
|
||||
let column = match *column {
|
||||
0 => Column::_0,
|
||||
1 => Column::_1,
|
||||
2 => Column::_2,
|
||||
3 => Column::_3,
|
||||
4 => Column::_4,
|
||||
other => {
|
||||
context.replies.push_back(UIEvent::StatusEvent(
|
||||
StatusEvent::DisplayMessage(format!(
|
||||
"Invalid column index `{}`: there are {} columns.",
|
||||
other,
|
||||
Self::HEADERS.len()
|
||||
)),
|
||||
));
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
if (self.sort_col, self.sort_order) != (column, *order) {
|
||||
self.sort_col = column;
|
||||
self.sort_order = *order;
|
||||
self.initialized = false;
|
||||
self.set_dirty(true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(Key::Char(ref c)) if c.is_ascii_digit() => {
|
||||
let n = *c as u8 - b'0'; // safe cast because of is_ascii_digit() check;
|
||||
let column = match n {
|
||||
1 => Column::_0,
|
||||
2 => Column::_1,
|
||||
3 => Column::_2,
|
||||
4 => Column::_3,
|
||||
5 => Column::_4,
|
||||
_ => {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
if self.sort_col == column {
|
||||
self.sort_order = !self.sort_order;
|
||||
} else {
|
||||
self.sort_col = column;
|
||||
self.sort_order = SortOrder::default();
|
||||
}
|
||||
self.initialized = false;
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(ref key)
|
||||
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_up"]) =>
|
||||
{
|
||||
let amount = 1;
|
||||
self.movement = Some(PageMovement::Up(amount));
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(ref key)
|
||||
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_down"])
|
||||
&& self.cursor_pos < self.length.saturating_sub(1) =>
|
||||
{
|
||||
let amount = 1;
|
||||
self.set_dirty(true);
|
||||
self.movement = Some(PageMovement::Down(amount));
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(ref key)
|
||||
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["prev_page"]) =>
|
||||
{
|
||||
let mult = 1;
|
||||
self.set_dirty(true);
|
||||
self.movement = Some(PageMovement::PageUp(mult));
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(ref key)
|
||||
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["next_page"]) =>
|
||||
{
|
||||
let mult = 1;
|
||||
self.set_dirty(true);
|
||||
self.movement = Some(PageMovement::PageDown(mult));
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(ref key)
|
||||
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["home_page"]) =>
|
||||
{
|
||||
self.set_dirty(true);
|
||||
self.movement = Some(PageMovement::Home);
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(ref key)
|
||||
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["end_page"]) =>
|
||||
{
|
||||
self.set_dirty(true);
|
||||
self.movement = Some(PageMovement::End);
|
||||
return true;
|
||||
}
|
||||
UIEvent::Resize => {
|
||||
self.set_dirty(true);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn is_dirty(&self) -> bool {
|
||||
self.dirty
|
||||
}
|
||||
|
||||
fn set_dirty(&mut self, value: bool) {
|
||||
self.dirty = value;
|
||||
}
|
||||
|
||||
fn kill(&mut self, uuid: ComponentId, context: &mut Context) {
|
||||
debug_assert!(uuid == self.id);
|
||||
context.replies.push_back(UIEvent::Action(Tab(Kill(uuid))));
|
||||
}
|
||||
|
||||
fn shortcuts(&self, context: &Context) -> ShortcutMaps {
|
||||
let mut map = ShortcutMaps::default();
|
||||
|
||||
map.insert(
|
||||
Shortcuts::GENERAL,
|
||||
context.settings.shortcuts.general.key_values(),
|
||||
);
|
||||
map[Shortcuts::GENERAL].insert("sort by 1st column", Key::Char('1'));
|
||||
map[Shortcuts::GENERAL].insert("sort by 2nd column", Key::Char('2'));
|
||||
map[Shortcuts::GENERAL].insert("sort by 3rd column", Key::Char('3'));
|
||||
map[Shortcuts::GENERAL].insert("sort by 4th column", Key::Char('4'));
|
||||
map[Shortcuts::GENERAL].insert("sort by 5th column", Key::Char('5'));
|
||||
|
||||
map
|
||||
}
|
||||
|
||||
fn id(&self) -> ComponentId {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn can_quit_cleanly(&mut self, _context: &Context) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn status(&self, _context: &Context) -> String {
|
||||
format!(
|
||||
"{} entries. Use `sort <n> [asc/desc]` command or press column index number key \
|
||||
(twice to toggle asc/desc) to sort",
|
||||
self.entries.len()
|
||||
)
|
||||
}
|
||||
}
|
161
meli/src/lib.rs
|
@ -1,161 +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,
|
||||
/* 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::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;
|
||||
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 mailbox_management;
|
||||
pub use mailbox_management::*;
|
||||
|
||||
pub mod jobs_view;
|
||||
pub use jobs_view::*;
|
||||
|
||||
#[cfg(feature = "svgscreenshot")]
|
||||
pub mod svg;
|
||||
|
||||
#[macro_use]
|
||||
pub mod conf;
|
||||
pub use crate::conf::{
|
||||
DotAddressable, IndexStyle, SearchBackend, Settings, Shortcuts, ThemeAttribute,
|
||||
};
|
||||
|
||||
#[cfg(feature = "sqlite3")]
|
||||
pub mod sqlite3;
|
||||
|
||||
pub mod jobs;
|
||||
pub mod mailcap;
|
||||
|
||||
pub mod accounts;
|
||||
pub use self::accounts::Account;
|
|
@ -1,395 +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::*,
|
||||
parser::BytesExt,
|
||||
text::Truncate,
|
||||
utils::xdg::query_default_app,
|
||||
Attachment, Result,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
components::*,
|
||||
desktop_exec_to_command,
|
||||
terminal::{Area, CellBuffer},
|
||||
Context, ErrorKind, File, StatusEvent, UIEvent,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ViewFilter {
|
||||
pub filter_invocation: String,
|
||||
pub content_type: ContentType,
|
||||
pub notice: Option<Cow<'static, str>>,
|
||||
pub body_text: String,
|
||||
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(stringify!(ViewFilter))
|
||||
.field("filter_invocation", &self.filter_invocation)
|
||||
.field("content_type", &self.content_type)
|
||||
.field("notice", &self.notice)
|
||||
.field("body_text", &self.body_text.trim_at_boundary(18))
|
||||
.field("body_text_len", &self.body_text.len())
|
||||
.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, 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 bytes: Vec<u8> = att.decode(Default::default());
|
||||
|
||||
let settings = &context.settings;
|
||||
if let Some(filter_invocation) = settings.pager.html_filter.as_ref() {
|
||||
match run("sh", &["-c", filter_invocation], &bytes) {
|
||||
Err(err) => {
|
||||
return Err(Error::new(format!(
|
||||
"Failed to start html filter process `{}`",
|
||||
filter_invocation,
|
||||
))
|
||||
.set_source(Some(Arc::new(err)))
|
||||
.set_kind(ErrorKind::External));
|
||||
}
|
||||
Ok(body_text) => {
|
||||
let notice =
|
||||
Some(format!("Text piped through `{}`.\n\n", filter_invocation).into());
|
||||
return Ok(Self {
|
||||
filter_invocation: filter_invocation.clone(),
|
||||
content_type: att.content_type.clone(),
|
||||
notice,
|
||||
body_text,
|
||||
unfiltered: bytes,
|
||||
event_handler: Some(Self::html_process_event),
|
||||
id: ComponentId::default(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Ok(body_text) = run("w3m", &["-I", "utf-8", "-T", "text/html"], &bytes) {
|
||||
return Ok(Self {
|
||||
filter_invocation: "w3m -I utf-8 -T text/html".into(),
|
||||
content_type: att.content_type.clone(),
|
||||
notice: Some("Text piped through `w3m -I utf-8 -T text/html`.\n\n".into()),
|
||||
body_text,
|
||||
unfiltered: bytes,
|
||||
event_handler: Some(Self::html_process_event),
|
||||
id: ComponentId::default(),
|
||||
});
|
||||
}
|
||||
|
||||
Err(
|
||||
Error::new("Failed to find any application to use as html filter")
|
||||
.set_kind(ErrorKind::Configuration),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn new_attachment(att: &Attachment, context: &mut 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, context))
|
||||
{
|
||||
return Ok(v);
|
||||
}
|
||||
} else if let ContentType::Multipart {
|
||||
kind: MultipartType::Related,
|
||||
ref parts,
|
||||
..
|
||||
} = att.content_type
|
||||
{
|
||||
if let Some(v @ Ok(_)) = parts.iter().find_map(|p| {
|
||||
if let v @ Ok(_) = Self::new_attachment(p, context) {
|
||||
Some(v)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
if att.is_html() {
|
||||
return Self::new_html(att, 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: String::new(),
|
||||
unfiltered: vec![],
|
||||
event_handler: None,
|
||||
id: ComponentId::default(),
|
||||
});
|
||||
}
|
||||
if let ContentType::Multipart {
|
||||
kind: MultipartType::Mixed,
|
||||
ref parts,
|
||||
..
|
||||
} = att.content_type
|
||||
{
|
||||
if let Some(Ok(res)) =
|
||||
parts
|
||||
.iter()
|
||||
.find_map(|part| match Self::new_attachment(part, context) {
|
||||
v @ Ok(_) => Some(v),
|
||||
Err(_) => None,
|
||||
})
|
||||
{
|
||||
return Ok(res);
|
||||
}
|
||||
}
|
||||
let notice = Some("Viewing attachment.\n\n".into());
|
||||
Ok(Self {
|
||||
filter_invocation: String::new(),
|
||||
content_type: att.content_type.clone(),
|
||||
notice,
|
||||
body_text: att.text(),
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
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");
|
||||
}
|
|
@ -1,596 +0,0 @@
|
|||
/*
|
||||
* meli - sqlite3.rs
|
||||
*
|
||||
* 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 an sqlite3 database for fast searching.
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
||||
use melib::{
|
||||
backends::MailBackend,
|
||||
email::{Envelope, EnvelopeHash},
|
||||
log,
|
||||
search::{
|
||||
escape_double_quote,
|
||||
Query::{self, *},
|
||||
},
|
||||
smol,
|
||||
utils::sqlite3::{rusqlite::params, DatabaseDescription},
|
||||
Error, Result, ResultIntoError, SortField, SortOrder,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
const DB: DatabaseDescription = DatabaseDescription {
|
||||
name: "index.db",
|
||||
identifier: None,
|
||||
application_prefix: "meli",
|
||||
init_script: Some(
|
||||
"CREATE TABLE IF NOT EXISTS envelopes (
|
||||
id INTEGER PRIMARY KEY,
|
||||
account_id INTEGER REFERENCES accounts ON UPDATE CASCADE,
|
||||
hash BLOB NOT NULL UNIQUE,
|
||||
date TEXT NOT NULL,
|
||||
_from TEXT NOT NULL,
|
||||
_to TEXT NOT NULL,
|
||||
cc TEXT NOT NULL,
|
||||
bcc TEXT NOT NULL,
|
||||
subject TEXT NOT NULL,
|
||||
message_id TEXT NOT NULL,
|
||||
in_reply_to TEXT NOT NULL,
|
||||
_references TEXT NOT NULL,
|
||||
flags INTEGER NOT NULL,
|
||||
has_attachments BOOLEAN NOT NULL,
|
||||
body_text TEXT NOT NULL,
|
||||
timestamp BLOB NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS folders (
|
||||
id INTEGER PRIMARY KEY,
|
||||
account_id INTEGER NOT NULL REFERENCES accounts ON UPDATE CASCADE,
|
||||
hash BLOB NOT NULL,
|
||||
date TEXT NOT NULL,
|
||||
name TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS accounts (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS folder_and_envelope (
|
||||
folder_id INTEGER NOT NULL,
|
||||
envelope_id INTEGER NOT NULL,
|
||||
PRIMARY KEY (folder_id, envelope_id),
|
||||
FOREIGN KEY(folder_id) REFERENCES folders(id) ON UPDATE CASCADE,
|
||||
FOREIGN KEY(envelope_id) REFERENCES envelopes(id) ON UPDATE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS folder_env_idx ON folder_and_envelope(folder_id);
|
||||
CREATE INDEX IF NOT EXISTS env_folder_idx ON folder_and_envelope(envelope_id);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS acc_idx ON accounts(name);
|
||||
|
||||
|
||||
CREATE INDEX IF NOT EXISTS envelope_timestamp_index ON envelopes (timestamp);
|
||||
CREATE INDEX IF NOT EXISTS envelope__from_index ON envelopes (_from);
|
||||
CREATE INDEX IF NOT EXISTS envelope__to_index ON envelopes (_to);
|
||||
CREATE INDEX IF NOT EXISTS envelope_cc_index ON envelopes (cc);
|
||||
CREATE INDEX IF NOT EXISTS envelope_bcc_index ON envelopes (bcc);
|
||||
CREATE INDEX IF NOT EXISTS envelope_message_id_index ON envelopes (message_id);
|
||||
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS fts USING fts5(subject, body_text, content=envelopes, \
|
||||
content_rowid=id);
|
||||
|
||||
-- Triggers to keep the FTS index up to date.
|
||||
CREATE TRIGGER IF NOT EXISTS envelopes_ai AFTER INSERT ON envelopes BEGIN
|
||||
INSERT INTO fts(rowid, subject, body_text) VALUES (new.id, new.subject, new.body_text);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS envelopes_ad AFTER DELETE ON envelopes BEGIN
|
||||
INSERT INTO fts(fts, rowid, subject, body_text) VALUES('delete', old.id, old.subject, \
|
||||
old.body_text);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS envelopes_au AFTER UPDATE ON envelopes BEGIN
|
||||
INSERT INTO fts(fts, rowid, subject, body_text) VALUES('delete', old.id, old.subject, \
|
||||
old.body_text);
|
||||
INSERT INTO fts(rowid, subject, body_text) VALUES (new.id, new.subject, new.body_text);
|
||||
END; ",
|
||||
),
|
||||
version: 1,
|
||||
};
|
||||
|
||||
//#[inline(always)]
|
||||
//fn fts5_bareword(w: &str) -> Cow<str> {
|
||||
// if w == "AND" || w == "OR" || w == "NOT" {
|
||||
// Cow::from(w)
|
||||
// } else {
|
||||
// if !w.is_ascii() {
|
||||
// Cow::from(format!("\"{}\"", escape_double_quote(w)))
|
||||
// } else {
|
||||
// for &b in w.as_bytes() {
|
||||
// if !(b > 0x2f && b < 0x3a)
|
||||
// || !(b > 0x40 && b < 0x5b)
|
||||
// || !(b > 0x60 && b < 0x7b)
|
||||
// || b != 0x60
|
||||
// || b != 26
|
||||
// {
|
||||
// return Cow::from(format!("\"{}\"",
|
||||
// escape_double_quote(w))); }
|
||||
// }
|
||||
// Cow::from(w)
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
//
|
||||
|
||||
pub struct AccountCache;
|
||||
|
||||
impl AccountCache {
|
||||
pub async fn insert(
|
||||
envelope: Envelope,
|
||||
backend: Arc<RwLock<Box<dyn MailBackend>>>,
|
||||
acc_name: String,
|
||||
) -> Result<()> {
|
||||
let db_desc = DatabaseDescription {
|
||||
identifier: Some(acc_name.clone().into()),
|
||||
..DB.clone()
|
||||
};
|
||||
|
||||
if !db_desc.exists().unwrap_or(false) {
|
||||
return Err(Error::new(format!(
|
||||
"Database hasn't been initialised. Run `reindex {acc_name}` command"
|
||||
)));
|
||||
}
|
||||
|
||||
let op = backend
|
||||
.read()
|
||||
.unwrap()
|
||||
.operation(envelope.hash())?
|
||||
.as_bytes()?;
|
||||
|
||||
let body = match op.await.map(|bytes| envelope.body_bytes(&bytes)) {
|
||||
Ok(body) => body.text(),
|
||||
Err(err) => {
|
||||
log::error!(
|
||||
"Failed to open envelope {}: {err}",
|
||||
envelope.message_id_display(),
|
||||
);
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
smol::unblock(move || {
|
||||
let mut conn = db_desc.open_or_create_db()?;
|
||||
|
||||
let tx =
|
||||
conn.transaction_with_behavior(melib::rusqlite::TransactionBehavior::Immediate)?;
|
||||
if let Err(err) = tx.execute(
|
||||
"INSERT OR IGNORE INTO accounts (name) VALUES (?1)",
|
||||
params![acc_name,],
|
||||
) {
|
||||
log::error!(
|
||||
"Failed to insert envelope {}: {err}",
|
||||
envelope.message_id_display(),
|
||||
);
|
||||
return Err(Error::new(format!(
|
||||
"Failed to insert envelope {}: {err}",
|
||||
envelope.message_id_display(),
|
||||
)));
|
||||
}
|
||||
let account_id: i32 = {
|
||||
let mut stmt = tx
|
||||
.prepare("SELECT id FROM accounts WHERE name = ?")
|
||||
.unwrap();
|
||||
let x = stmt
|
||||
.query_map(params![acc_name], |row| row.get(0))
|
||||
.unwrap()
|
||||
.next()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
x
|
||||
};
|
||||
if let Err(err) = tx
|
||||
.execute(
|
||||
"INSERT OR REPLACE INTO envelopes (account_id, hash, date, _from, _to, cc, \
|
||||
bcc, subject, message_id, in_reply_to, _references, flags, has_attachments, \
|
||||
body_text, timestamp)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)",
|
||||
params![
|
||||
account_id,
|
||||
envelope.hash().to_be_bytes().to_vec(),
|
||||
envelope.date_as_str(),
|
||||
envelope.field_from_to_string(),
|
||||
envelope.field_to_to_string(),
|
||||
envelope.field_cc_to_string(),
|
||||
envelope.field_bcc_to_string(),
|
||||
envelope.subject().into_owned().trim_end_matches('\u{0}'),
|
||||
envelope.message_id_display().to_string(),
|
||||
envelope
|
||||
.in_reply_to_display()
|
||||
.map(|f| f.to_string())
|
||||
.unwrap_or_default(),
|
||||
envelope.field_references_to_string(),
|
||||
i64::from(envelope.flags().bits()),
|
||||
i32::from(envelope.has_attachments()),
|
||||
body,
|
||||
envelope.date().to_be_bytes().to_vec()
|
||||
],
|
||||
)
|
||||
.map_err(|e| Error::new(e.to_string()))
|
||||
{
|
||||
drop(tx);
|
||||
log::error!(
|
||||
"Failed to insert envelope {}: {err}",
|
||||
envelope.message_id_display(),
|
||||
);
|
||||
} else {
|
||||
tx.commit()?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn remove(acc_name: String, env_hash: EnvelopeHash) -> Result<()> {
|
||||
let db_desc = DatabaseDescription {
|
||||
identifier: Some(acc_name.clone().into()),
|
||||
..DB.clone()
|
||||
};
|
||||
let db_path = db_desc.db_path()?;
|
||||
if !db_path.exists() {
|
||||
return Err(Error::new(format!(
|
||||
"Database hasn't been initialised. Run `reindex {acc_name}` command"
|
||||
)));
|
||||
}
|
||||
|
||||
smol::unblock(move || {
|
||||
let mut conn = db_desc.open_or_create_db()?;
|
||||
let tx =
|
||||
conn.transaction_with_behavior(melib::rusqlite::TransactionBehavior::Immediate)?;
|
||||
if let Err(err) = tx.execute(
|
||||
"DELETE FROM envelopes WHERE hash = ?",
|
||||
params![env_hash.to_be_bytes().to_vec(),],
|
||||
) {
|
||||
drop(tx);
|
||||
log::error!("Failed to remove envelope {env_hash}: {err}");
|
||||
return Err(Error::new(format!(
|
||||
"Failed to remove envelope {env_hash}: {err}"
|
||||
)));
|
||||
}
|
||||
tx.commit()?;
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn index(
|
||||
acc_name: Arc<String>,
|
||||
collection: melib::Collection,
|
||||
backend_mutex: Arc<RwLock<Box<dyn MailBackend>>>,
|
||||
) -> Result<()> {
|
||||
let acc_mutex = collection.envelopes.clone();
|
||||
let db_desc = Arc::new(DatabaseDescription {
|
||||
identifier: Some(acc_name.to_string().into()),
|
||||
..DB.clone()
|
||||
});
|
||||
let env_hashes = acc_mutex
|
||||
.read()
|
||||
.unwrap()
|
||||
.keys()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
/* Sleep, index and repeat in order not to block the main process */
|
||||
let account_id: i32 = {
|
||||
let acc_name = Arc::clone(&acc_name);
|
||||
let db_desc = Arc::clone(&db_desc);
|
||||
smol::unblock(move || {
|
||||
let mut conn = db_desc.open_or_create_db()?;
|
||||
let tx = conn
|
||||
.transaction_with_behavior(melib::rusqlite::TransactionBehavior::Immediate)?;
|
||||
tx.execute(
|
||||
"INSERT OR REPLACE INTO accounts (name) VALUES (?1)",
|
||||
params![acc_name.as_str(),],
|
||||
)
|
||||
.chain_err_summary(|| "Failed to update index:")?;
|
||||
let account_id = {
|
||||
let mut stmt = tx
|
||||
.prepare("SELECT id FROM accounts WHERE name = ?")
|
||||
.unwrap();
|
||||
let x = stmt
|
||||
.query_map(params![acc_name.as_str()], |row| row.get(0))
|
||||
.unwrap()
|
||||
.next()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
x
|
||||
};
|
||||
tx.commit()?;
|
||||
Ok::<i32, Error>(account_id)
|
||||
})
|
||||
.await?
|
||||
};
|
||||
let mut ctr = 0;
|
||||
log::trace!(
|
||||
"Rebuilding {} index. {}/{}",
|
||||
acc_name,
|
||||
ctr,
|
||||
env_hashes.len()
|
||||
);
|
||||
for chunk in env_hashes.chunks(200) {
|
||||
ctr += chunk.len();
|
||||
let mut chunk_bytes = Vec::with_capacity(chunk.len());
|
||||
for &env_hash in chunk {
|
||||
let mut op = backend_mutex.read().unwrap().operation(env_hash)?;
|
||||
let bytes = op
|
||||
.as_bytes()?
|
||||
.await
|
||||
.chain_err_summary(|| format!("Failed to open envelope {}", env_hash))?;
|
||||
chunk_bytes.push((env_hash, bytes));
|
||||
}
|
||||
{
|
||||
let acc_mutex = acc_mutex.clone();
|
||||
let db_desc = Arc::clone(&db_desc);
|
||||
smol::unblock(move || {
|
||||
let mut conn = db_desc.open_or_create_db()?;
|
||||
let tx = conn.transaction_with_behavior(
|
||||
melib::rusqlite::TransactionBehavior::Immediate,
|
||||
)?;
|
||||
let envelopes_lck = acc_mutex.read().unwrap();
|
||||
for (env_hash, bytes) in chunk_bytes {
|
||||
if let Some(e) = envelopes_lck.get(&env_hash) {
|
||||
let body = e.body_bytes(&bytes).text().replace('\0', "");
|
||||
tx.execute(
|
||||
"INSERT OR REPLACE INTO envelopes (account_id, hash, date, _from, \
|
||||
_to, cc, bcc, subject, message_id, in_reply_to, _references, \
|
||||
flags, has_attachments, body_text, timestamp)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)",
|
||||
params![
|
||||
account_id,
|
||||
e.hash().to_be_bytes().to_vec(),
|
||||
e.date_as_str(),
|
||||
e.field_from_to_string(),
|
||||
e.field_to_to_string(),
|
||||
e.field_cc_to_string(),
|
||||
e.field_bcc_to_string(),
|
||||
e.subject().into_owned().trim_end_matches('\u{0}'),
|
||||
e.message_id_display().to_string(),
|
||||
e.in_reply_to_display()
|
||||
.map(|f| f.to_string())
|
||||
.unwrap_or_default(),
|
||||
e.field_references_to_string(),
|
||||
i64::from(e.flags().bits()),
|
||||
i32::from(e.has_attachments()),
|
||||
body,
|
||||
e.date().to_be_bytes().to_vec()
|
||||
],
|
||||
)
|
||||
.chain_err_summary(|| {
|
||||
format!("Failed to insert envelope {}", e.message_id_display())
|
||||
})?;
|
||||
}
|
||||
}
|
||||
tx.commit()?;
|
||||
Ok::<(), Error>(())
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
let sleep_dur = std::time::Duration::from_millis(50);
|
||||
smol::Timer::after(sleep_dur).await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn search(
|
||||
acc_name: String,
|
||||
query: Query,
|
||||
(sort_field, sort_order): (SortField, SortOrder),
|
||||
) -> Result<SmallVec<[EnvelopeHash; 512]>> {
|
||||
let db_desc = DatabaseDescription {
|
||||
identifier: Some(acc_name.clone().into()),
|
||||
..DB.clone()
|
||||
};
|
||||
|
||||
if !db_desc.exists().unwrap_or(false) {
|
||||
return Err(Error::new(format!(
|
||||
"Database hasn't been initialised for account `{}`. Run `reindex` command to \
|
||||
build an index.",
|
||||
acc_name
|
||||
)));
|
||||
}
|
||||
let query = query_to_sql(&query);
|
||||
|
||||
smol::unblock(move || {
|
||||
let mut conn = db_desc.open_or_create_db()?;
|
||||
|
||||
let sort_field = match sort_field {
|
||||
SortField::Subject => "subject",
|
||||
SortField::Date => "timestamp",
|
||||
};
|
||||
|
||||
let sort_order = match sort_order {
|
||||
SortOrder::Asc => "ASC",
|
||||
SortOrder::Desc => "DESC",
|
||||
};
|
||||
|
||||
let tx = conn.transaction()?;
|
||||
let mut stmt = tx
|
||||
.prepare(&format!(
|
||||
"SELECT hash FROM envelopes WHERE {} ORDER BY {} {};",
|
||||
query, sort_field, sort_order
|
||||
))
|
||||
.map_err(|e| Error::new(e.to_string()))?;
|
||||
|
||||
#[allow(clippy::let_and_return)] // false positive, the let binding is needed
|
||||
// for the temporary to live long enough
|
||||
let x = stmt
|
||||
.query_map([], |row| row.get::<_, EnvelopeHash>(0))
|
||||
.map_err(Error::from)?
|
||||
.map(|item| item.map_err(Error::from))
|
||||
.collect::<Result<SmallVec<[EnvelopeHash; 512]>>>();
|
||||
x
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub fn db_path(acc_name: &str) -> Result<Option<PathBuf>> {
|
||||
let db_desc = DatabaseDescription {
|
||||
identifier: Some(acc_name.to_string().into()),
|
||||
..DB.clone()
|
||||
};
|
||||
let db_path = db_desc.db_path()?;
|
||||
if !db_path.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(db_path))
|
||||
}
|
||||
}
|
||||
|
||||
/// Translates a `Query` to an Sqlite3 expression in a `String`.
|
||||
pub fn query_to_sql(q: &Query) -> String {
|
||||
fn rec(q: &Query, s: &mut String) {
|
||||
match q {
|
||||
Subject(t) => {
|
||||
s.push_str("subject LIKE \"%");
|
||||
s.extend(escape_double_quote(t).chars());
|
||||
s.push_str("%\" ");
|
||||
}
|
||||
From(t) => {
|
||||
s.push_str("_from LIKE \"%");
|
||||
s.extend(escape_double_quote(t).chars());
|
||||
s.push_str("%\" ");
|
||||
}
|
||||
To(t) => {
|
||||
s.push_str("_to LIKE \"%");
|
||||
s.extend(escape_double_quote(t).chars());
|
||||
s.push_str("%\" ");
|
||||
}
|
||||
Cc(t) => {
|
||||
s.push_str("cc LIKE \"%");
|
||||
s.extend(escape_double_quote(t).chars());
|
||||
s.push_str("%\" ");
|
||||
}
|
||||
Bcc(t) => {
|
||||
s.push_str("bcc LIKE \"%");
|
||||
s.extend(escape_double_quote(t).chars());
|
||||
s.push_str("%\" ");
|
||||
}
|
||||
AllText(t) => {
|
||||
s.push_str("body_text LIKE \"%");
|
||||
s.extend(escape_double_quote(t).chars());
|
||||
s.push_str("%\" ");
|
||||
}
|
||||
And(q1, q2) => {
|
||||
s.push('(');
|
||||
rec(q1, s);
|
||||
s.push_str(") AND (");
|
||||
rec(q2, s);
|
||||
s.push_str(") ");
|
||||
}
|
||||
Or(q1, q2) => {
|
||||
s.push('(');
|
||||
rec(q1, s);
|
||||
s.push_str(") OR (");
|
||||
rec(q2, s);
|
||||
s.push_str(") ");
|
||||
}
|
||||
Not(q) => {
|
||||
s.push_str("NOT (");
|
||||
rec(q, s);
|
||||
s.push_str(") ");
|
||||
}
|
||||
Flags(v) => {
|
||||
let total = v.len();
|
||||
if total > 1 {
|
||||
s.push('(');
|
||||
}
|
||||
for (i, f) in v.iter().enumerate() {
|
||||
match f.as_str() {
|
||||
"draft" => {
|
||||
s.push_str(" (flags & 8 > 0) ");
|
||||
}
|
||||
"deleted" | "trashed" => {
|
||||
s.push_str(" (flags & 6 > 0) ");
|
||||
}
|
||||
"flagged" => {
|
||||
s.push_str(" (flags & 16 > 0) ");
|
||||
}
|
||||
"recent" => {
|
||||
s.push_str(" (flags & 4 == 0) ");
|
||||
}
|
||||
"seen" | "read" => {
|
||||
s.push_str(" (flags & 4 > 0) ");
|
||||
}
|
||||
"unseen" | "unread" => {
|
||||
s.push_str(" (flags & 4 == 0) ");
|
||||
}
|
||||
"answered" | "replied" => {
|
||||
s.push_str(" (flags & 2 > 0) ");
|
||||
}
|
||||
"unanswered" => {
|
||||
s.push_str(" (flags & 2 == 0) ");
|
||||
}
|
||||
_ => {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if total > 1 && i != total - 1 {
|
||||
s.push_str(" AND ");
|
||||
}
|
||||
}
|
||||
if total > 1 {
|
||||
s.push_str(") ");
|
||||
}
|
||||
}
|
||||
HasAttachment => {
|
||||
s.push_str("has_attachments == 1 ");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let mut ret = String::new();
|
||||
rec(q, &mut ret);
|
||||
ret
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_query_to_sql() {
|
||||
use melib::{search::query, utils::parsec::Parser};
|
||||
assert_eq!(
|
||||
"(subject LIKE \"%test%\" ) AND (body_text LIKE \"%i%\" ) ",
|
||||
&query_to_sql(&query().parse_complete("subject:test and i").unwrap().1)
|
||||
);
|
||||
assert_eq!(
|
||||
"(subject LIKE \"%github%\" ) OR ((_from LIKE \"%epilys%\" ) AND ((subject LIKE \
|
||||
\"%lib%\" ) OR (subject LIKE \"%meli%\" ) ) ) ",
|
||||
&query_to_sql(
|
||||
&query()
|
||||
.parse_complete(
|
||||
"subject:github or (from:epilys and (subject:lib or subject:meli))"
|
||||
)
|
||||
.unwrap()
|
||||
.1
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,183 +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/>.
|
||||
*/
|
||||
|
||||
//! Terminal grid cells, keys, colors, etc.
|
||||
|
||||
use serde::{de, de::Visitor, Deserialize, Deserializer};
|
||||
|
||||
mod braille;
|
||||
mod color;
|
||||
mod screen;
|
||||
pub use color::*;
|
||||
#[macro_use]
|
||||
pub mod cells;
|
||||
#[macro_use]
|
||||
pub mod keys;
|
||||
pub mod embedded;
|
||||
pub mod text_editing;
|
||||
|
||||
use std::io::{BufRead, Write};
|
||||
|
||||
pub use braille::BraillePixelIter;
|
||||
pub use screen::{Area, Screen, ScreenGeneration, StateStdout, Tty, Virtual};
|
||||
|
||||
pub use self::{cells::*, keys::*, text_editing::*};
|
||||
|
||||
/// A type alias for a `(x, y)` position on screen.
|
||||
pub type Pos = (usize, usize);
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub enum Alignment {
|
||||
/// Stretch to fill all space if possible, center if no meaningful way to
|
||||
/// stretch.
|
||||
Fill,
|
||||
/// Snap to left or top side, leaving space on right or bottom.
|
||||
Start,
|
||||
/// Snap to right or bottom side, leaving space on left or top.
|
||||
End,
|
||||
/// Center natural width of widget inside the allocation.
|
||||
#[default]
|
||||
Center,
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! emoji_text_presentation_selector {
|
||||
() => {
|
||||
'\u{FE0E}'
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* CSI events we use
|
||||
*/
|
||||
|
||||
pub const BRACKET_PASTE_START: &[u8] = b"\x1B[200~";
|
||||
pub const BRACKET_PASTE_END: &[u8] = b"\x1B[201~";
|
||||
|
||||
// Some macros taken from termion:
|
||||
/// Create a CSI-introduced sequence.
|
||||
macro_rules! csi {
|
||||
($( $l:expr ),*) => { concat!("\x1b[", $( $l ),*) };
|
||||
}
|
||||
|
||||
/// Derive a CSI sequence struct.
|
||||
macro_rules! derive_csi_sequence {
|
||||
($(#[$outer:meta])*
|
||||
($name:ident, $value:expr)) => {
|
||||
$(#[$outer])*
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct $name;
|
||||
|
||||
impl std::fmt::Display for $name {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(f, csi!($value))
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for $name {
|
||||
fn as_ref(&self) -> &'static [u8] {
|
||||
csi!($value).as_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for $name {
|
||||
fn as_ref(&self) -> &'static str {
|
||||
csi!($value)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
derive_csi_sequence!(
|
||||
///Ps = 1 0 0 2 ⇒ Don't use Cell Motion Mouse Tracking, xterm
|
||||
(DisableMouse, "?1002l")
|
||||
);
|
||||
|
||||
derive_csi_sequence!(
|
||||
///Ps = 1 0 0 2 ⇒ Use Cell Motion Mouse Tracking, xterm
|
||||
(EnableMouse, "?1002h")
|
||||
);
|
||||
|
||||
derive_csi_sequence!(
|
||||
///Ps = 1 0 0 6 Enable SGR Mouse Mode, xterm.
|
||||
(EnableSGRMouse, "?1006h")
|
||||
);
|
||||
|
||||
derive_csi_sequence!(
|
||||
///Ps = 1 0 0 6 Disable SGR Mouse Mode, xterm.
|
||||
(DisableSGRMouse, "?1006l")
|
||||
);
|
||||
|
||||
derive_csi_sequence!(
|
||||
#[doc = "`CSI Ps ; Ps ; Ps t`, where `Ps = 2 2 ; 0` -> Save xterm icon and window title on \
|
||||
stack."]
|
||||
(SaveWindowTitleIconToStack, "22;0t")
|
||||
);
|
||||
|
||||
derive_csi_sequence!(
|
||||
#[doc = "Restore window title and icon from terminal's title stack. `CSI Ps ; Ps ; Ps t`, \
|
||||
where `Ps = 2 3 ; 0` -> Restore xterm icon and window title from stack."]
|
||||
(RestoreWindowTitleIconFromStack, "23;0t")
|
||||
);
|
||||
|
||||
derive_csi_sequence!(
|
||||
#[doc = "Empty struct with a Display implementation that returns the byte sequence to start [Bracketed Paste Mode](http://www.xfree86.org/current/ctlseqs.html#Bracketed%20Paste%20Mode)"]
|
||||
(BracketModeStart, "?2004h")
|
||||
);
|
||||
|
||||
derive_csi_sequence!(
|
||||
#[doc = "Empty struct with a Display implementation that returns the byte sequence to end [Bracketed Paste Mode](http://www.xfree86.org/current/ctlseqs.html#Bracketed%20Paste%20Mode)"]
|
||||
(BracketModeEnd, "?2004l")
|
||||
);
|
||||
|
||||
pub struct Ask {
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
impl Ask {
|
||||
pub fn run(self) -> bool {
|
||||
let mut buffer = String::new();
|
||||
let stdin = std::io::stdin();
|
||||
let mut handle = stdin.lock();
|
||||
|
||||
print!("{} [Y/n] ", &self.message);
|
||||
let _ = std::io::stdout().flush();
|
||||
loop {
|
||||
buffer.clear();
|
||||
handle
|
||||
.read_line(&mut buffer)
|
||||
.expect("Could not read from stdin.");
|
||||
|
||||
match buffer.trim() {
|
||||
"" | "Y" | "y" | "yes" | "YES" | "Yes" => {
|
||||
return true;
|
||||
}
|
||||
"n" | "N" | "no" | "No" | "NO" => {
|
||||
return false;
|
||||
}
|
||||
_ => {
|
||||
print!("\n{} [Y/n] ", &self.message);
|
||||
let _ = std::io::stdout().flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,488 +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/>.
|
||||
*/
|
||||
|
||||
struct Braille16bitColumn {
|
||||
// each u16 in the tuple is one line ( first_line, second_line, third_line, fourth line) */
|
||||
bitmaps: (u16, u16, u16, u16),
|
||||
// reverse 1-indexing, so column: 1 means the left-most column in 16bit word */
|
||||
bitcolumn: u32,
|
||||
}
|
||||
|
||||
/*
|
||||
impl std::fmt::Debug for Braille16bitColumn {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"Braille16bitColumn {{\n bitmaps: \n{:x}\n{:x}\n{:x}\n{:x},\n bitcolumn: {}\n\n{}\n{:016b}\n{:016b}\n{:016b}\n{:016b},\n
|
||||
}}",self.bitmaps.0, self.bitmaps.1, self.bitmaps.2, self.bitmaps.3, self.bitcolumn, format!("{:016b}", 0x0001_u16.rotate_left(self.bitcolumn)).replace("0"," ").replace("1", "v"), self.bitmaps.0, self.bitmaps.1, self.bitmaps.2, self.bitmaps.3,
|
||||
)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/// Iterate on 2x4 pixel blocks from a bitmap and return a unicode braille
|
||||
/// character for each block. The iterator holds four lines of bitmaps
|
||||
/// encoded as `u16` numbers in swapped bit order, like the `xbm`
|
||||
/// graphics format. The bitmap is split to `u16` columns.
|
||||
///
|
||||
/// ## Usage
|
||||
/// ```no_run
|
||||
/// /* BEE is the contents of a 48x48 xbm file. xbm is a C-like array of 8bit values, and
|
||||
/// * each pair was manually (macro-ually?) condensed into a single 16bit value. Each 3 items
|
||||
/// * represent one pixel row.
|
||||
/// */
|
||||
/// const BEE: [u16; 3 * 48] = [
|
||||
/// 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
|
||||
/// 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
|
||||
/// 0x0000, 0x0002, 0x0000, 0x0000, 0xe003, 0x0000, 0x0000, 0xfc00, 0x0000, 0x0000, 0x3f00,
|
||||
/// 0x0000, 0x00e0, 0x0f00, 0x0000, 0x00f8, 0x0300, 0x0000, 0x00fe, 0x0000, 0x0080, 0x8f0d,
|
||||
/// 0x0000, 0x00e0, 0xff7f, 0x0000, 0x00f8, 0xffff, 0x0300, 0x00fc, 0xffff, 0x0f00, 0x00fe,
|
||||
/// 0xffff, 0x3f00, 0x00ff, 0xffff, 0xff00, 0xc0ff, 0xffff, 0xff01, 0xc0ff, 0xff77, 0xff07,
|
||||
/// 0xf0f9, 0xffff, 0xff07, 0xf0f0, 0xffef, 0xfd0f, 0xf0e0, 0xffff, 0xfb1f, 0xf0e1, 0xffc1,
|
||||
/// 0xfb0f, 0xe0f3, 0xffc3, 0xf307, 0xc0f7, 0xffc0, 0xe100, 0xc0ff, 0xd9e0, 0x3f00, 0x803e,
|
||||
/// 0xc1f8, 0x5f00, 0x8076, 0x43f4, 0xbf18, 0x806c, 0x43fc, 0xf325, 0x0009, 0xc3df, 0x4326,
|
||||
/// 0x001a, 0xcf3f, 0x622d, 0x0034, 0xff01, 0x2224, 0x00f0, 0xff00, 0x8312, 0x00a0, 0x5700,
|
||||
/// 0x0309, 0x00f8, 0x1b00, 0x8f06, 0x0048, 0x6000, 0xcd03, 0x0018, 0x6624, 0xdf00, 0x0030,
|
||||
/// 0x820f, 0x3f00, 0x00c0, 0xf0ff, 0x3f00, 0x0080, 0x03fe, 0x7f00, 0x0000, 0x7ce0, 0x0f00,
|
||||
/// 0x0000, 0x809f, 0x1c00, 0x0000, 0x0000, 0x3800, 0x0000, 0x0000, 0x7000, 0x0000, 0x0000,
|
||||
/// 0xe000,
|
||||
/// ];
|
||||
///
|
||||
/// for lines in BEE.chunks(12) {
|
||||
/// let iter = meli::BraillePixelIter::from(lines);
|
||||
/// for b in iter {
|
||||
/// print!("{}", b);
|
||||
/// }
|
||||
/// println!("");
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Output:
|
||||
///
|
||||
/// ```text
|
||||
/// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠀⠀⠀
|
||||
/// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣤⣶⠾⠛⠉⠀⠀⠀
|
||||
/// ⠀⠀⠀⠀⠀⠀⢀⣠⣤⣤⣀⣠⣔⣾⣛⡛⠉⠀⠀⠀⠀⠀⠀⠀
|
||||
/// ⠀⠀⠀⠀⣠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣤⣀⠀⠀⠀⠀
|
||||
/// ⠀⠀⣤⣿⠟⠻⣿⣿⣿⣿⣿⣿⣿⣯⢿⣯⡿⣿⣿⣿⣷⣆⠀⠀
|
||||
/// ⠀⠀⠻⣿⣦⡀⣼⣿⣿⣿⣿⣿⠯⠉⠉⣿⡿⠘⢿⣿⠿⠟⠁⠀
|
||||
/// ⠀⠀⠀⢹⠹⣟⢿⡍⣧⠈⠁⡟⠀⣔⣾⣿⣿⠿⣯⣢⡀⡠⢄⠀
|
||||
/// ⠀⠀⠀⠀⠑⠜⣦⣀⣿⣶⣤⣿⠟⠛⠓⠉⣹⠀⠰⢃⢊⠗⡸⠀
|
||||
/// ⠀⠀⠀⠀⠀⢰⡚⠞⢛⡑⢣⡅⠀⡀⢀⠀⣟⣶⡀⣴⠵⠊⠀⠀
|
||||
/// ⠀⠀⠀⠀⠀⠀⠉⠲⠬⣀⣒⡚⠻⠿⢶⣶⣿⣿⠿⠄⠀⠀⠀⠀
|
||||
/// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠉⠉⠁⠈⠀⠙⢷⣄⠀⠀⠀
|
||||
/// ```
|
||||
///
|
||||
///
|
||||
/// ## Explanation:
|
||||
/// For the following bitmap:
|
||||
///
|
||||
/// ```text
|
||||
/// ◻◼◻◻◼◻◻◼◻◻◻◼◼◼◼◼
|
||||
/// ◼◼◼◼◼◼◻◻◼◻◼◻◻◼◼◼
|
||||
/// ◻◼◼◼◼◼◼◼◻◻◻◻◼◼◻◻
|
||||
/// ◼◻◼◼◻◼◼◻◼◼◼◻◻◻◻◻
|
||||
/// ```
|
||||
///
|
||||
/// Iteration on each step examines two columns:
|
||||
///
|
||||
/// ```text
|
||||
/// ⇩⇩
|
||||
/// ◻◼┆◻◻┆◼◻┆◻◼┆◻◻┆◻◼┆◼◼┆◼◼
|
||||
/// ◼◼┆◼◼┆◼◼┆◻◻┆◼◻┆◼◻┆◻◼┆◼◼
|
||||
/// ◻◼┆◼◼┆◼◼┆◼◼┆◻◻┆◻◻┆◼◼┆◻◻
|
||||
/// ◼◻┆◼◼┆◻◼┆◼◻┆◼◼┆◼◻┆◻◻┆◻◻
|
||||
/// ```
|
||||
///
|
||||
/// The first two columns are encoded as:
|
||||
///
|
||||
/// ```text
|
||||
/// ┏━━━━━━┳━━━━┓
|
||||
/// ┃pixels┃bits┃
|
||||
/// ┡━━━━━━╇━━━━┩
|
||||
/// │ ◻◼ │ 14 │
|
||||
/// │ ◼◼ │ 25 │
|
||||
/// │ ◻◻ │ 36 │
|
||||
/// │ ◼◻ │ 78 │
|
||||
/// └──────┴────┘
|
||||
/// =
|
||||
/// braille bitmap is
|
||||
/// ◻◼◻◼◼◻◼◻ = 0b01011010 = 0x5A
|
||||
/// 12345678
|
||||
/// ```
|
||||
///
|
||||
/// and braille character is bitmap + 0x2800 (Braille block's position in
|
||||
/// Unicode)
|
||||
///
|
||||
/// ```text
|
||||
/// 0x5A + 0x2800 = 0x285A = '⡚'
|
||||
/// ```
|
||||
///
|
||||
/// Why three columns? I originally wrote this for X-Face bitmaps, which are
|
||||
/// 48x48 pixels.
|
||||
pub struct BraillePixelIter {
|
||||
columns: [Braille16bitColumn; 3],
|
||||
column_ptr: usize,
|
||||
}
|
||||
|
||||
impl From<&[u16]> for BraillePixelIter {
|
||||
fn from(from: &[u16]) -> Self {
|
||||
Self {
|
||||
columns: [
|
||||
Braille16bitColumn {
|
||||
bitmaps: (
|
||||
from[0].swap_bytes().reverse_bits(),
|
||||
from[3].swap_bytes().reverse_bits(),
|
||||
from[6].swap_bytes().reverse_bits(),
|
||||
from[9].swap_bytes().reverse_bits(),
|
||||
),
|
||||
bitcolumn: 1,
|
||||
},
|
||||
Braille16bitColumn {
|
||||
bitmaps: (
|
||||
from[1].swap_bytes().reverse_bits(),
|
||||
from[4].swap_bytes().reverse_bits(),
|
||||
from[7].swap_bytes().reverse_bits(),
|
||||
from[10].swap_bytes().reverse_bits(),
|
||||
),
|
||||
bitcolumn: 1,
|
||||
},
|
||||
Braille16bitColumn {
|
||||
bitmaps: (
|
||||
from[2].swap_bytes().reverse_bits(),
|
||||
from[5].swap_bytes().reverse_bits(),
|
||||
from[8].swap_bytes().reverse_bits(),
|
||||
from[11].swap_bytes().reverse_bits(),
|
||||
),
|
||||
bitcolumn: 1,
|
||||
},
|
||||
],
|
||||
column_ptr: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for BraillePixelIter {
|
||||
type Item = char;
|
||||
fn next(&mut self) -> Option<char> {
|
||||
if self.columns[self.column_ptr].bitcolumn == 17 {
|
||||
if self.column_ptr == 2 {
|
||||
return None;
|
||||
}
|
||||
self.column_ptr += 1;
|
||||
return self.next();
|
||||
}
|
||||
let Braille16bitColumn {
|
||||
ref bitmaps,
|
||||
ref mut bitcolumn,
|
||||
} = &mut self.columns[self.column_ptr];
|
||||
/* First bitcolumn out of two (braille is 2x4) */
|
||||
let mut bits: u16 = 0x1 & (bitmaps.0.rotate_left(*bitcolumn)); // * 0x1;
|
||||
bits += (0x1 & (bitmaps.1.rotate_left(*bitcolumn))) * 0x2;
|
||||
bits += (0x1 & (bitmaps.2.rotate_left(*bitcolumn))) * 0x4;
|
||||
bits += (0x1 & (bitmaps.3.rotate_left(*bitcolumn))) * 0x40;
|
||||
/* Second bitcolumn */
|
||||
*bitcolumn += 1;
|
||||
bits += (0x1 & (bitmaps.0.rotate_left(*bitcolumn))) * 0x8;
|
||||
bits += (0x1 & (bitmaps.1.rotate_left(*bitcolumn))) * 0x10;
|
||||
bits += (0x1 & (bitmaps.2.rotate_left(*bitcolumn))) * 0x20;
|
||||
bits += (0x1 & (bitmaps.3.rotate_left(*bitcolumn))) * 0x80;
|
||||
*bitcolumn += 1;
|
||||
|
||||
/* The Braille Patterns block spans the entire [U+2800, U+28FF] range and
|
||||
* bits is a 16bit integer ∈ [0x00, 0xFF] so this is guaranteed
|
||||
* to be a Unicode char */
|
||||
Some(unsafe { std::char::from_u32_unchecked(bits as u32 + 0x2800) })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
type Xbm = [u16; 3 * 48];
|
||||
|
||||
const _X_QRCODE: (Xbm, &str) = (
|
||||
[
|
||||
0xff3f, 0x3cf3, 0xff03, 0xff3f, 0x3cf3, 0xff03, 0x0330, 0x0333, 0x0003, 0x0330, 0x0333,
|
||||
0x0003, 0xf333, 0xf030, 0x3f03, 0xf333, 0xf030, 0x3f03, 0xf333, 0xfc33, 0x3f03, 0xf333,
|
||||
0xfc33, 0x3f03, 0xf333, 0x0f33, 0x3f03, 0xf333, 0x0f33, 0x3f03, 0x0330, 0x3033, 0x0003,
|
||||
0x0330, 0x3033, 0x0003, 0xff3f, 0x33f3, 0xff03, 0xff3f, 0x33f3, 0xff03, 0x0000, 0xc003,
|
||||
0x0000, 0x0000, 0xc003, 0x0000, 0x3333, 0xfc00, 0xc300, 0x3333, 0xfc00, 0xc300, 0xc3c0,
|
||||
0x3f30, 0x0c00, 0xc3c0, 0x3f30, 0x0c00, 0xcff0, 0x3f03, 0xcf00, 0xcff0, 0x3f03, 0xcf00,
|
||||
0x0ccf, 0x0f30, 0xcc00, 0x0ccf, 0x0f30, 0xcc00, 0x0033, 0x3033, 0xf300, 0x0033, 0x3033,
|
||||
0xf300, 0x0000, 0xffcc, 0x0c00, 0x0000, 0xffcc, 0x0c00, 0xff3f, 0xccfc, 0x3000, 0xff3f,
|
||||
0xccfc, 0x3000, 0x0330, 0xf0cf, 0x0f00, 0x0330, 0xf0cf, 0x0f00, 0xf333, 0xcffc, 0x3003,
|
||||
0xf333, 0xcffc, 0x3003, 0xf333, 0x0030, 0xf000, 0xf333, 0x0030, 0xf000, 0xf333, 0x3f03,
|
||||
0x0303, 0xf333, 0x3f03, 0x0303, 0x0330, 0x3030, 0xf003, 0x0330, 0x3030, 0xf003, 0xff3f,
|
||||
0x0333, 0x3303, 0xff3f, 0x0333, 0x3303, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
|
||||
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
|
||||
0x0000,
|
||||
],
|
||||
r#"
|
||||
⣿⠛⠛⠛⠛⠛⣿⠀⣤⠛⠛⠀⣿⠀⣿⠛⠛⠛⠛⠛⣿⠀⠀⠀
|
||||
⣿⠀⣿⣿⣿⠀⣿⠀⠀⣤⣿⣿⣤⠀⣿⠀⣿⣿⣿⠀⣿⠀⠀⠀
|
||||
⣿⠀⠛⠛⠛⠀⣿⠀⠛⠛⣤⠀⣿⠀⣿⠀⠛⠛⠛⠀⣿⠀⠀⠀
|
||||
⠛⠛⠛⠛⠛⠛⠛⠀⠛⠀⠛⣤⣿⠀⠛⠛⠛⠛⠛⠛⠛⠀⠀⠀
|
||||
⣿⠀⠛⣤⠛⠀⠛⣤⣤⣿⣿⠛⠀⠀⣤⠀⠛⣤⠀⠛⠀⠀⠀⠀
|
||||
⠛⣿⠀⠛⣤⣤⠛⣿⣿⣿⠛⠀⠛⠀⣤⠀⠛⣿⠀⣿⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠛⠀⠛⠀⣤⣤⣿⣤⠛⣤⠛⣤⠛⣤⠛⠛⠀⠀⠀⠀
|
||||
⣿⠛⠛⠛⠛⠛⣿⠀⠀⠛⣤⣿⣤⣿⠛⣿⣤⣤⠛⠀⠀⠀⠀⠀
|
||||
⣿⠀⣿⣿⣿⠀⣿⠀⠛⠛⠀⠛⠀⠛⣿⠛⠀⠀⣿⣤⠛⠀⠀⠀
|
||||
⣿⠀⠛⠛⠛⠀⣿⠀⠛⠛⣿⠀⠛⠀⣤⠀⠛⠀⣤⣤⣿⠀⠀⠀
|
||||
⠛⠛⠛⠛⠛⠛⠛⠀⠛⠀⠀⠀⠛⠀⠛⠀⠛⠀⠛⠀⠛⠀⠀⠀
|
||||
"#,
|
||||
);
|
||||
|
||||
const XTTHOMAS: (Xbm, &str) = (
|
||||
[
|
||||
0xFFFF, 0xFF0F, 0x0000, 0xFFFF, 0xFF0F, 0x0000, 0x1FFC, 0xC10F, 0x0000, 0x0FFC, 0x810F,
|
||||
0x0000, 0x07FC, 0x010F, 0x0000, 0x07FC, 0x010F, 0x0000, 0x03FC, 0x010E, 0x0000, 0x03FC,
|
||||
0x010E, 0x0000, 0x01FC, 0x010C, 0x0000, 0x00FC, 0x0100, 0x0000, 0x00FC, 0x0100, 0x0000,
|
||||
0x00FC, 0x0100, 0x0000, 0x00FC, 0x0100, 0x0000, 0x00FC, 0x0100, 0x0000, 0x00FC, 0x0100,
|
||||
0x0000, 0x00FC, 0x0100, 0x0000, 0x00FC, 0x0100, 0x0000, 0x00FC, 0xFDFF, 0xFF7F, 0x00FC,
|
||||
0xFDFF, 0xFF7F, 0x00FC, 0xFDE0, 0x0F7E, 0x00FC, 0x7DE0, 0x0F7C, 0x00FC, 0x3DE0, 0x0F78,
|
||||
0x00FC, 0x3DE0, 0x0F78, 0x00FC, 0x1DE0, 0x0F70, 0x00FC, 0x1DE0, 0x0F70, 0x00FC, 0x0DE0,
|
||||
0x0F60, 0x00FC, 0x01E0, 0x0F00, 0x00FE, 0x07E0, 0x0F00, 0xC0FF, 0x1FE0, 0x0F00, 0xC0FF,
|
||||
0x1FE0, 0x0F00, 0x0000, 0x00E0, 0x0F00, 0x0000, 0x00E0, 0x0F00, 0x0000, 0x00E0, 0x0F00,
|
||||
0x0000, 0x00E0, 0x0F00, 0x0000, 0x00E0, 0x0F00, 0x0000, 0x00E0, 0x0F00, 0x0000, 0x00E0,
|
||||
0x0F00, 0x0000, 0x00E0, 0x0F00, 0x0000, 0x00E0, 0x0F00, 0x0000, 0x00E0, 0x0F00, 0x0000,
|
||||
0x00E0, 0x0F00, 0x0000, 0x00E0, 0x0F00, 0x0000, 0x00E0, 0x0F00, 0x0000, 0x00E0, 0x0F00,
|
||||
0x0000, 0x00F0, 0x3F00, 0x0000, 0x00FE, 0xFF00, 0x0000, 0x00FE, 0xFF00, 0x0000, 0x0000,
|
||||
0x0000,
|
||||
],
|
||||
r#"⣿⣿⠟⠛⠛⣿⣿⣿⡟⠛⠛⢿⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⣿⠃⠀⠀⠀⣿⣿⣿⡇⠀⠀⠀⢻⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠁⠀⠀⠀⠀⣿⣿⣿⡇⠀⠀⠀⠀⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⣿⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⣿⣿⣿⡇⣶⣶⣶⠶⠶⢶⣶⣶⣶⠶⠶⢶⣶⣶⡆
|
||||
⠀⠀⠀⠀⠀⣿⣿⣿⡇⣿⡿⠁⠀⠀⢸⣿⣿⣿⠀⠀⠀⠹⣿⡇
|
||||
⠀⠀⠀⠀⢀⣿⣿⣿⣇⡛⠁⠀⠀⠀⢸⣿⣿⣿⠀⠀⠀⠀⠙⠃
|
||||
⠀⠀⠀⠛⠛⠛⠛⠛⠛⠛⠃⠀⠀⠀⢸⣿⣿⣿⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠰⠶⠿⠿⠿⠿⠿⠶⠀⠀⠀⠀
|
||||
"#,
|
||||
);
|
||||
|
||||
const FBIRD_SCALED_DOWN: (Xbm, &str) = (
|
||||
[
|
||||
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
|
||||
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
|
||||
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
|
||||
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
|
||||
0x0000, 0x00e0, 0x0100, 0x0000, 0x0000, 0x0f00, 0x0000, 0x0000, 0x3c00, 0x0000, 0x00e0,
|
||||
0x790c, 0x0000, 0x00fc, 0xff3f, 0x0000, 0x0043, 0xffff, 0x0300, 0x0000, 0xfc0f, 0x0000,
|
||||
0x0000, 0xf00f, 0x0000, 0x0000, 0xf00f, 0x0000, 0x0000, 0xf00f, 0x0000, 0x0000, 0xfc07,
|
||||
0x0000, 0x0000, 0xfc03, 0x0000, 0x0000, 0xfc00, 0x0000, 0x0000, 0x0e00, 0x0000, 0x0000,
|
||||
0x0700, 0x0000, 0x0000, 0x0700, 0x0000, 0x0040, 0x0300, 0x0000, 0x0000, 0x0200, 0x0000,
|
||||
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
|
||||
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
|
||||
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
|
||||
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
|
||||
0x0000,
|
||||
],
|
||||
r#"⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⢀⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⣀⣠⣤⣍⣻⣶⣄⣀⣤⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠉⠀⠀⠁⠉⠛⣿⣿⣿⣿⠉⠉⠉⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⣶⣿⣿⠿⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠀⡀⣾⠏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠀⠀⠈⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
"#,
|
||||
);
|
||||
|
||||
const SYS: (Xbm, &str) = (
|
||||
[
|
||||
0x0000, 0x0000, 0x0000, 0x0000, 0x0002, 0x0000, 0x0000, 0x0000, 0x0100, 0x0000, 0x8080,
|
||||
0x0200, 0x0000, 0x0088, 0x2200, 0x0000, 0x00b4, 0x5a00, 0x0000, 0x0248, 0x2400, 0x0000,
|
||||
0x0008, 0x2000, 0x0020, 0x0090, 0x1300, 0x0000, 0x10ce, 0xe700, 0x0000, 0x28c1, 0x0701,
|
||||
0x0088, 0x28ce, 0xe700, 0x0040, 0xab95, 0x1300, 0x0080, 0x440a, 0x2000, 0x0080, 0x004a,
|
||||
0x2400, 0x0000, 0x39b5, 0x5a04, 0x00e0, 0x7c8e, 0x2200, 0x0012, 0x7c90, 0x0200, 0x00e0,
|
||||
0x7c0e, 0x0101, 0x0000, 0x3901, 0x0000, 0x0080, 0x0002, 0x2000, 0x0080, 0x4412, 0x0000,
|
||||
0x0040, 0xab2d, 0x0000, 0x0080, 0xa82a, 0x0200, 0x0000, 0x68ab, 0x0500, 0x0000, 0x9044,
|
||||
0x4200, 0x0000, 0x8000, 0x0200, 0x0020, 0x0039, 0x0100, 0x0002, 0xe07c, 0x8e00, 0x0080,
|
||||
0x107c, 0x1000, 0x8040, 0xe17c, 0x0e00, 0x0044, 0x1139, 0x0100, 0x405a, 0xad00, 0x8200,
|
||||
0x0024, 0x9244, 0x0200, 0x0004, 0x50ab, 0x0500, 0x20c8, 0x8928, 0x0200, 0x00e7, 0x7328,
|
||||
0x2000, 0x80e0, 0x8310, 0x0000, 0x00e7, 0x7300, 0x0000, 0x00c8, 0x0900, 0x0000, 0x0004,
|
||||
0x1000, 0x0000, 0x0024, 0x1202, 0x0000, 0x005a, 0x2d00, 0x0000, 0x0044, 0x1100, 0x0000,
|
||||
0x0040, 0x8100, 0x0000, 0x0080, 0x0000, 0x0000, 0x0000, 0x1000, 0x0000, 0x0000, 0x0000,
|
||||
0x0000,
|
||||
],
|
||||
r#"⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠐⠀⠀⢀⢄⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠀⠀⠠⠀⠀⠀⠀⢪⠒⠜⠘⠔⢪⠂⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⢀⠈⢀⠀⢠⢢⠀⢔⣒⠁⣾⣿⡆⢑⣒⠄⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠀⠱⡉⢊⣈⠊⡱⡱⣁⢌⢉⢄⡱⡀⠀⡀⠀⠀
|
||||
⠀⠀⠀⠀⠐⠀⠪⠭⡀⢿⣿⠇⡨⠭⠂⠘⠜⠀⠈⠀⠄⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠀⢜⠤⢢⢠⢢⢜⢤⢢⠀⢀⠀⠈⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⢀⠀⠀⠈⠊⠱⡉⢊⣈⠊⡱⠁⠀⠂⠀⠀⠀⠀
|
||||
⠀⠀⠀⠠⠈⡀⠀⡔⡄⠀⡪⠭⡀⢿⣿⠇⡨⠭⠂⠈⠀⠀⠀⠀
|
||||
⠀⠀⢀⠁⠈⢎⠑⣁⡑⢉⠎⢜⠤⢢⢠⠢⢜⠄⠀⠈⠀⠀⠀⠀
|
||||
⠀⠀⠀⠐⠭⢅⠸⣿⡿⢀⠭⠕⠀⠈⠊⠀⠀⠀⠈⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠠⡣⠔⡄⡔⠤⡣⠀⠐⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠀⠑⠁⠀⠄⠈⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
"#,
|
||||
);
|
||||
|
||||
const FBIRD: (Xbm, &str) = (
|
||||
[
|
||||
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
|
||||
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
|
||||
0x0000, 0x0000, 0x007E, 0x0000, 0x0000, 0x00E0, 0x0700, 0x0000, 0x0080, 0x1F00, 0x0000,
|
||||
0x0000, 0x7F00, 0x0000, 0x007E, 0xFCE0, 0x0000, 0xC0FF, 0xFFF9, 0x0300, 0xF0FF, 0xFFFF,
|
||||
0x0700, 0x0CE8, 0xFFFF, 0xFF00, 0x0080, 0xFFFF, 0x0103, 0x0000, 0xFEFF, 0x0000, 0x0000,
|
||||
0xF8FF, 0x0000, 0x0000, 0xF07F, 0x0000, 0x0000, 0xF07F, 0x0000, 0x0000, 0xF87F, 0x0000,
|
||||
0x0000, 0xFC3F, 0x0000, 0x0000, 0xFE3F, 0x0000, 0x0000, 0xFE1F, 0x0000, 0x0000, 0xFF07,
|
||||
0x0000, 0x0000, 0xFF01, 0x0000, 0x0080, 0x0F00, 0x0000, 0x0080, 0x0700, 0x0000, 0x00C0,
|
||||
0x0300, 0x0000, 0x00E0, 0x0300, 0x0000, 0x00F0, 0x0300, 0x0000, 0x00E8, 0x0100, 0x0000,
|
||||
0x0080, 0x0100, 0x0000, 0x0000, 0x0100, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
|
||||
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
|
||||
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
|
||||
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
|
||||
0x0000,
|
||||
],
|
||||
r#"⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠈⠉⠙⠳⣶⣦⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⣀⠤⠶⠾⢿⢿⣷⣶⣿⣿⣿⣦⣴⣾⣿⣶⣄⣀⣀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠀⠈⠙⠻⣿⣿⣿⣿⣿⡿⠁⠀⠀⠀⠉⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⣿⣿⣿⣿⠃⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠀⢀⣾⣿⠿⠿⠟⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⣠⣾⣿⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠈⠈⠙⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
"#,
|
||||
);
|
||||
|
||||
const XFACE: (Xbm, &str) = (
|
||||
[
|
||||
0xAAAA, 0xAAAA, 0xAAAA, 0x0000, 0x0000, 0x0000, 0xAAAA, 0xAAAA, 0xAAA8, 0x0000, 0x0000,
|
||||
0x0000, 0xA222, 0x2222, 0x222A, 0x0000, 0x0000, 0x0000, 0xAAAA, 0xAAAA, 0xAAA2, 0x0000,
|
||||
0x0000, 0x0000, 0xA222, 0x2222, 0x2228, 0x0000, 0x0000, 0x0000, 0xAAAA, 0xAAAA, 0xAAAA,
|
||||
0x0000, 0x0000, 0x0000, 0xA222, 0x2222, 0x2222, 0x0000, 0x0000, 0x0000, 0xAAAA, 0xAAAA,
|
||||
0xAAA8, 0x0000, 0x0000, 0x0000, 0xA222, 0x2222, 0x222A, 0x0000, 0x0000, 0x0000, 0xAAAA,
|
||||
0xAAAA, 0xAAA2, 0x0000, 0x0000, 0x0000, 0xA222, 0x2222, 0x2228, 0x0000, 0x0000, 0x0000,
|
||||
0xAAAA, 0xAAAA, 0xAAAA, 0x0000, 0x0000, 0x0000, 0xA222, 0x2222, 0x2222, 0x0000, 0x0000,
|
||||
0x0000, 0xAAAA, 0xAAAA, 0xAAA8, 0x0000, 0x0000, 0x0000, 0xA222, 0x2222, 0x222A, 0x0000,
|
||||
0x0000, 0x0000, 0xAAAA, 0xAAAA, 0xAAA2, 0x0000, 0x0000, 0x0000, 0xA222, 0x2222, 0x2228,
|
||||
0x0000, 0x0000, 0x0000, 0xAAAA, 0xAAAA, 0xAAAA, 0x0000, 0x0000, 0x0000, 0xA222, 0x2222,
|
||||
0x2222, 0x0000, 0x0000, 0x0000, 0xAAAA, 0xAAAA, 0xAAA8, 0x0000, 0x0000, 0x0000, 0xA222,
|
||||
0x2222, 0x222A, 0x0000, 0x0000, 0x0000, 0xAAAA, 0xAAAA, 0xAAA2, 0x0000, 0x0000, 0x0000,
|
||||
0xA222, 0x2222, 0x2228, 0x0000, 0x0000, 0x0000, 0xAAAA, 0xAAAA, 0xAAAA, 0x0000, 0x0000,
|
||||
0x0000,
|
||||
],
|
||||
r#"⠨⠨⠨⠨⠨⠨⠨⠨⠨⠨⠨⠨⠨⠨⠨⠨⠨⠨⠨⠨⠈⠨⠨⠨
|
||||
⠨⠠⠨⠨⠨⠠⠨⠠⠨⠠⠨⠠⠨⠠⠨⠠⠨⠠⠨⠠⠨⠈⠨⠠
|
||||
⠨⠠⠨⠨⠨⠠⠨⠠⠨⠠⠨⠠⠨⠠⠨⠠⠨⠠⠨⠠⠠⠨⠨⠠
|
||||
⠨⠠⠨⠨⠨⠠⠨⠠⠨⠠⠨⠠⠨⠠⠨⠠⠨⠠⠨⠠⠈⠠⠨⠠
|
||||
⠨⠠⠨⠨⠨⠠⠨⠠⠨⠠⠨⠠⠨⠠⠨⠠⠨⠠⠨⠠⠨⠈⠨⠠
|
||||
⠨⠠⠨⠨⠨⠠⠨⠠⠨⠠⠨⠠⠨⠠⠨⠠⠨⠠⠨⠠⠠⠨⠨⠠
|
||||
⠨⠠⠨⠨⠨⠠⠨⠠⠨⠠⠨⠠⠨⠠⠨⠠⠨⠠⠨⠠⠈⠠⠨⠠
|
||||
⠨⠠⠨⠨⠨⠠⠨⠠⠨⠠⠨⠠⠨⠠⠨⠠⠨⠠⠨⠠⠨⠈⠨⠠
|
||||
⠨⠠⠨⠨⠨⠠⠨⠠⠨⠠⠨⠠⠨⠠⠨⠠⠨⠠⠨⠠⠠⠨⠨⠠
|
||||
⠨⠠⠨⠨⠨⠠⠨⠠⠨⠠⠨⠠⠨⠠⠨⠠⠨⠠⠨⠠⠈⠠⠨⠠
|
||||
⠨⠠⠨⠨⠨⠠⠨⠠⠨⠠⠨⠠⠨⠠⠨⠠⠨⠠⠨⠠⠨⠈⠨⠠
|
||||
⠨⠠⠨⠨⠨⠠⠨⠠⠨⠠⠨⠠⠨⠠⠨⠠⠨⠠⠨⠠⠠⠨⠨⠠
|
||||
"#,
|
||||
);
|
||||
|
||||
const MAILQ: (Xbm, &str) = (
|
||||
[
|
||||
0x0000, 0x0000, 0x0000, 0xffff, 0xff00, 0x0000, 0x0100, 0x8000, 0x0000, 0x5501, 0xb800,
|
||||
0x0000, 0xa900, 0xb800, 0x0000, 0x5501, 0x8000, 0x0000, 0x0100, 0x8000, 0x0000, 0x01f0,
|
||||
0xffff, 0x0f00, 0x0110, 0x0000, 0x0800, 0x0150, 0x1580, 0x0b00, 0xff9f, 0x0a80, 0x0b00,
|
||||
0x0050, 0x1500, 0x0800, 0x0010, 0x0000, 0x0800, 0x0010, 0x00ff, 0xffff, 0x0010, 0x0001,
|
||||
0x0080, 0x0010, 0x0055, 0x01b8, 0x00f0, 0xffa9, 0x00b8, 0x0000, 0x0055, 0x0180, 0x0000,
|
||||
0x0001, 0x4095, 0x00f0, 0xffff, 0x8f8a, 0x0010, 0x0000, 0x4895, 0x0050, 0x1580, 0x0b80,
|
||||
0x0090, 0x0a80, 0xfbff, 0x0050, 0x1500, 0x0800, 0x0010, 0x0054, 0x0900, 0xffff, 0xffa8,
|
||||
0x0800, 0x0100, 0x8054, 0x0900, 0x5501, 0xb800, 0x0800, 0xa900, 0xb8ff, 0x0f00, 0x5501,
|
||||
0x8000, 0x0000, 0x0100, 0x8000, 0x0000, 0x01f0, 0xffff, 0x0f00, 0x0110, 0x0000, 0x0800,
|
||||
0x0150, 0x1580, 0x0b00, 0xff9f, 0x0a80, 0x0b00, 0x0050, 0x1500, 0x0800, 0x0010, 0x0000,
|
||||
0x0800, 0x0010, 0x00ff, 0xffff, 0x0010, 0x0001, 0x0080, 0x0010, 0x0055, 0x01b8, 0x00f0,
|
||||
0xffa9, 0x00b8, 0x0000, 0x0055, 0x0180, 0x0000, 0x0001, 0x4095, 0x0000, 0x0001, 0x808a,
|
||||
0x0000, 0x0001, 0x4095, 0x0000, 0x0001, 0x0080, 0x0000, 0x00ff, 0xffff, 0x0000, 0x0000,
|
||||
0x0000,
|
||||
],
|
||||
r#"⡖⡒⡒⡒⡒⠒⠒⠒⠒⢒⣒⢲⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⡇⠊⠊⠊⠂⠀⣀⣀⣀⣈⣉⣸⣀⣀⣀⣀⣀⣀⠀⠀⠀⠀⠀⠀
|
||||
⠧⠤⠤⠤⠤⠤⡇⡢⡢⡢⡂⠀⠀⠀⠀⠰⠶⢸⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⡖⡒⡒⡒⡒⠚⠒⠒⠒⢒⣒⢲
|
||||
⠀⠀⠀⠀⠀⠀⣉⣉⣉⣉⣉⣉⣇⣊⣊⣊⣂⣀⠀⢄⢄⢌⠍⢸
|
||||
⠀⠀⠀⠀⠀⠀⡇⡢⡢⡢⡂⠀⠀⠀⠀⠰⠶⢸⠤⠥⠥⠥⠥⠼
|
||||
⡖⡒⡒⡒⡒⠒⠓⠒⠒⢒⣒⢲⠀⠕⠕⠕⠅⢸⠀⠀⠀⠀⠀⠀
|
||||
⡇⠊⠊⠊⠂⠀⣀⣀⣀⣈⣉⣸⣉⣉⣉⣉⣉⣉⠀⠀⠀⠀⠀⠀
|
||||
⠧⠤⠤⠤⠤⠤⡇⡢⡢⡢⡂⠀⠀⠀⠀⠰⠶⢸⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⡖⡒⡒⡒⡒⠚⠒⠒⠒⢒⣒⢲
|
||||
⠀⠀⠀⠀⠀⠀⠉⠉⠉⠉⠉⠉⡇⠊⠊⠊⠂⠀⠀⢄⢄⢌⠍⢸
|
||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠧⠤⠤⠤⠤⠤⠤⠥⠥⠥⠥⠼
|
||||
"#,
|
||||
);
|
||||
|
||||
macro_rules! add_to_all {
|
||||
($($xbm:ident),*$(,)?) => {
|
||||
&[
|
||||
$((stringify!($xbm), $xbm),)*
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
const ALL: &[(&str, (Xbm, &str))] = add_to_all! {
|
||||
FBIRD,
|
||||
FBIRD_SCALED_DOWN,
|
||||
MAILQ,
|
||||
SYS,
|
||||
XFACE,
|
||||
XTTHOMAS,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_braille_xface() {
|
||||
/* lines has 12 bitmaps, with 3 bitmap making each line that is 4 lines.
|
||||
* lines = [
|
||||
* a_0, a_1, a_2,
|
||||
* b_0, b_1, b_2,
|
||||
* c_0, c_1, c_2,
|
||||
* d_0, d_1, d_2,
|
||||
* ];
|
||||
*/
|
||||
for (name, (face, output)) in ALL {
|
||||
println!("{name}: \n--------------------------------------");
|
||||
let mut printed = String::new();
|
||||
for lines in face.chunks(12) {
|
||||
let iter = super::BraillePixelIter::from(lines);
|
||||
print!("`");
|
||||
for b in iter {
|
||||
print!("{b}");
|
||||
printed.push(b);
|
||||
}
|
||||
printed.push('\n');
|
||||
println!("`");
|
||||
}
|
||||
assert_eq!(printed.as_bytes(), output.as_bytes());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,194 +0,0 @@
|
|||
/*
|
||||
* meli
|
||||
*
|
||||
* Copyright 2017-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/>.
|
||||
*/
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use std::os::fd::OwnedFd;
|
||||
use std::{
|
||||
ffi::{CString, OsStr},
|
||||
os::unix::{
|
||||
ffi::OsStrExt,
|
||||
io::{AsRawFd, FromRawFd, IntoRawFd},
|
||||
},
|
||||
};
|
||||
|
||||
use melib::{error::*, log};
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
use nix::{
|
||||
fcntl::{open, OFlag},
|
||||
pty::{grantpt, posix_openpt, ptsname, unlockpt},
|
||||
sys::stat,
|
||||
};
|
||||
use nix::{
|
||||
ioctl_none_bad, ioctl_write_ptr_bad,
|
||||
libc::{STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO},
|
||||
pty::Winsize,
|
||||
unistd::{dup2, fork, ForkResult},
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
pub mod escape_codes;
|
||||
pub mod terminal;
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
use std::path::Path;
|
||||
use std::{
|
||||
convert::TryFrom,
|
||||
io::{Read, Write},
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
/// ioctl request code to "Make the given terminal the controlling terminal of
|
||||
/// the calling process"
|
||||
use libc::TIOCSCTTY;
|
||||
/// ioctl request code to set window size of pty:
|
||||
use libc::TIOCSWINSZ;
|
||||
pub use terminal::{EmbeddedGrid, ScreenBuffer, Terminal};
|
||||
|
||||
ioctl_write_ptr_bad!(
|
||||
/// Macro generated function that calls ioctl to set window size of backend
|
||||
/// PTY end.
|
||||
set_window_size,
|
||||
TIOCSWINSZ,
|
||||
Winsize
|
||||
);
|
||||
|
||||
ioctl_none_bad!(
|
||||
/// Set controlling terminal fd for current session.
|
||||
set_controlling_terminal,
|
||||
TIOCSCTTY
|
||||
);
|
||||
|
||||
/// Create a new pseudoterminal (PTY) with given width, size and execute
|
||||
/// `command` in it.
|
||||
pub fn create_pty(width: usize, height: usize, command: &str) -> Result<Arc<Mutex<Terminal>>> {
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let (frontend_fd, backend_name): (nix::pty::PtyMaster, String) = {
|
||||
// Open a new PTY frontend
|
||||
let frontend_fd = posix_openpt(OFlag::O_RDWR)?;
|
||||
|
||||
// Allow a backend to be generated for it
|
||||
grantpt(&frontend_fd)?;
|
||||
unlockpt(&frontend_fd)?;
|
||||
|
||||
// Get the name of the backend
|
||||
let backend_name = unsafe { ptsname(&frontend_fd) }?;
|
||||
|
||||
{
|
||||
let winsize = Winsize {
|
||||
ws_row: <u16>::try_from(height).unwrap(),
|
||||
ws_col: <u16>::try_from(width).unwrap(),
|
||||
ws_xpixel: 0,
|
||||
ws_ypixel: 0,
|
||||
};
|
||||
|
||||
let frontend_fd = frontend_fd.as_raw_fd();
|
||||
unsafe { set_window_size(frontend_fd, &winsize)? };
|
||||
}
|
||||
(frontend_fd, backend_name)
|
||||
};
|
||||
#[cfg(target_os = "macos")]
|
||||
let (frontend_fd, backend_fd): (OwnedFd, OwnedFd) = {
|
||||
let winsize = Winsize {
|
||||
ws_row: <u16>::try_from(height).unwrap(),
|
||||
ws_col: <u16>::try_from(width).unwrap(),
|
||||
ws_xpixel: 0,
|
||||
ws_ypixel: 0,
|
||||
};
|
||||
|
||||
let ends = nix::pty::openpty(Some(&winsize), None)?;
|
||||
(ends.master, ends.slave)
|
||||
};
|
||||
|
||||
let child_pid = match unsafe { fork()? } {
|
||||
ForkResult::Child => {
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
/* Open backend end for pseudoterminal */
|
||||
let backend_fd = open(Path::new(&backend_name), OFlag::O_RDWR, stat::Mode::empty())?;
|
||||
|
||||
// assign stdin, stdout, stderr to the pty
|
||||
dup2(backend_fd.as_raw_fd(), STDIN_FILENO).unwrap();
|
||||
dup2(backend_fd.as_raw_fd(), STDOUT_FILENO).unwrap();
|
||||
dup2(backend_fd.as_raw_fd(), STDERR_FILENO).unwrap();
|
||||
/* Become session leader */
|
||||
nix::unistd::setsid().unwrap();
|
||||
match unsafe { set_controlling_terminal(backend_fd.as_raw_fd()) } {
|
||||
Ok(c) if c < 0 => {
|
||||
log::error!(
|
||||
"Could not execute `{command}`: ioctl(fd, TIOCSCTTY, NULL) returned {c}",
|
||||
);
|
||||
std::process::exit(c);
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
log::error!(
|
||||
"Could not execute `{command}`: ioctl(fd, TIOCSCTTY, NULL) returned {err}",
|
||||
);
|
||||
std::process::exit(-1);
|
||||
}
|
||||
}
|
||||
/* Find posix sh location, because POSIX shell is not always at /bin/sh */
|
||||
let path_var = std::process::Command::new("getconf")
|
||||
.args(["PATH"])
|
||||
.output()?
|
||||
.stdout;
|
||||
for mut p in std::env::split_paths(&OsStr::from_bytes(&path_var[..])) {
|
||||
p.push("sh");
|
||||
if p.exists() {
|
||||
if let Err(e) = nix::unistd::execv(
|
||||
&CString::new(p.as_os_str().as_bytes()).unwrap(),
|
||||
&[
|
||||
&CString::new("sh").unwrap(),
|
||||
&CString::new("-c").unwrap(),
|
||||
&CString::new(command.as_bytes()).unwrap(),
|
||||
],
|
||||
) {
|
||||
log::error!("Could not execute `{command}`: {e}");
|
||||
std::process::exit(-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
log::error!(
|
||||
"Could not execute `{command}`: did not find the standard POSIX sh shell in PATH \
|
||||
= {}",
|
||||
String::from_utf8_lossy(&path_var),
|
||||
);
|
||||
// We are in a separate process, so doing exit(-1) here won't affect the parent
|
||||
// process.
|
||||
std::process::exit(-1);
|
||||
}
|
||||
ForkResult::Parent { child } => child,
|
||||
};
|
||||
|
||||
let stdin = unsafe { std::fs::File::from_raw_fd(frontend_fd.as_raw_fd()) };
|
||||
let mut embedded_pty = Terminal::new(stdin, child_pid);
|
||||
embedded_pty.set_terminal_size((width, height));
|
||||
let pty = Arc::new(Mutex::new(embedded_pty));
|
||||
let pty_ = pty.clone();
|
||||
|
||||
std::thread::Builder::new()
|
||||
.spawn(move || {
|
||||
let frontend_fd = frontend_fd.into_raw_fd();
|
||||
let frontend_file = unsafe { std::fs::File::from_raw_fd(frontend_fd) };
|
||||
Terminal::forward_pty_translate_escape_codes(pty_, frontend_file);
|
||||
})
|
||||
.unwrap();
|
||||
Ok(pty)
|
||||
}
|
|
@ -1,183 +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/>.
|
||||
*/
|
||||
|
||||
use std::{
|
||||
fs,
|
||||
fs::OpenOptions,
|
||||
io::{Read, Write},
|
||||
os::{
|
||||
fd::{FromRawFd, OwnedFd},
|
||||
unix::fs::PermissionsExt,
|
||||
},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use melib::{error::*, uuid::Uuid};
|
||||
|
||||
/// Temporary file that can optionally cleaned up when it is dropped.
|
||||
#[derive(Debug)]
|
||||
pub struct File {
|
||||
/// File's path.
|
||||
path: PathBuf,
|
||||
/// Delete file when it is dropped.
|
||||
delete_on_drop: bool,
|
||||
}
|
||||
|
||||
impl Drop for File {
|
||||
fn drop(&mut self) {
|
||||
if self.delete_on_drop {
|
||||
let _ = std::fs::remove_file(self.path());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl File {
|
||||
/// Open as a standard library file type.
|
||||
pub fn as_std_file(&self) -> Result<std::fs::File> {
|
||||
OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(false)
|
||||
.open(&self.path)
|
||||
.chain_err_summary(|| format!("Could not create/open path {}", self.path.display()))
|
||||
}
|
||||
|
||||
/// The file's path.
|
||||
pub fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
|
||||
/// Convenience method to read `File` to `String`.
|
||||
pub fn read_to_string(&self) -> Result<String> {
|
||||
fn inner(path: &Path) -> Result<String> {
|
||||
let mut buf = Vec::new();
|
||||
let mut f = fs::File::open(path)?;
|
||||
f.read_to_end(&mut buf)?;
|
||||
Ok(String::from_utf8(buf)?)
|
||||
}
|
||||
inner(&self.path).chain_err_summary(|| format!("Can't read {}", self.path.display()))
|
||||
}
|
||||
|
||||
/// Returned `File` will be deleted when dropped if `delete_on_drop` is set,
|
||||
/// so make sure to add it on `context.temp_files` to reap it later.
|
||||
pub fn create_temp_file(
|
||||
bytes: &[u8],
|
||||
filename: Option<&str>,
|
||||
path: Option<&mut PathBuf>,
|
||||
extension: Option<&str>,
|
||||
delete_on_drop: bool,
|
||||
) -> Result<Self> {
|
||||
let mut dir = std::env::temp_dir();
|
||||
|
||||
let path = if let Some(p) = path {
|
||||
p
|
||||
} else {
|
||||
dir.push("meli");
|
||||
std::fs::DirBuilder::new().recursive(true).create(&dir)?;
|
||||
if let Some(filename) = filename {
|
||||
dir.push(filename)
|
||||
} else {
|
||||
let u = Uuid::new_v4();
|
||||
dir.push(u.as_simple().to_string());
|
||||
}
|
||||
&mut dir
|
||||
};
|
||||
if let Some(ext) = extension {
|
||||
path.set_extension(ext);
|
||||
}
|
||||
fn inner(path: &Path, bytes: &[u8], delete_on_drop: bool) -> Result<File> {
|
||||
let mut f = std::fs::File::create(path)?;
|
||||
let metadata = f.metadata()?;
|
||||
let mut permissions = metadata.permissions();
|
||||
|
||||
permissions.set_mode(0o600); // Read/write for owner only.
|
||||
f.set_permissions(permissions)?;
|
||||
|
||||
f.write_all(bytes)?;
|
||||
f.flush()?;
|
||||
Ok(File {
|
||||
path: path.to_path_buf(),
|
||||
delete_on_drop,
|
||||
})
|
||||
}
|
||||
inner(path, bytes, delete_on_drop)
|
||||
.chain_err_summary(|| format!("Could not create file at path {}", path.display()))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pipe() -> Result<(OwnedFd, OwnedFd)> {
|
||||
nix::unistd::pipe()
|
||||
.map(|(fd1, fd2)| unsafe { (OwnedFd::from_raw_fd(fd1), OwnedFd::from_raw_fd(fd2)) })
|
||||
.map_err(|err| {
|
||||
Error::new("Could not create pipe")
|
||||
.set_source(Some(
|
||||
(Box::new(err) as Box<dyn std::error::Error + Send + Sync + 'static>).into(),
|
||||
))
|
||||
.set_kind(ErrorKind::OSError)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_file_invalid_path() {
|
||||
let f = File {
|
||||
path: PathBuf::from("//////"),
|
||||
delete_on_drop: true,
|
||||
};
|
||||
f.as_std_file().unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_delete_on_drop() {
|
||||
const S: &str = "hello world";
|
||||
let tempdir = tempfile::tempdir().unwrap();
|
||||
|
||||
let delete_on_drop = File::create_temp_file(
|
||||
S.as_bytes(),
|
||||
None,
|
||||
Some(&mut tempdir.path().join("test")),
|
||||
None,
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&delete_on_drop.read_to_string().unwrap(), S);
|
||||
drop(delete_on_drop);
|
||||
assert!(!tempdir.path().join("test").try_exists().unwrap());
|
||||
|
||||
let persist = File::create_temp_file(
|
||||
S.as_bytes(),
|
||||
None,
|
||||
Some(&mut tempdir.path().join("test")),
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(&persist.read_to_string().unwrap(), S);
|
||||
drop(persist);
|
||||
assert!(tempdir.path().join("test").try_exists().unwrap());
|
||||
|
||||
_ = tempdir.close();
|
||||
}
|
||||
}
|
|
@ -1,865 +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::*;
|
||||
|
||||
const OK: &str = "OK";
|
||||
const CANCEL: &str = "Cancel";
|
||||
const CANCEL_OFFSET: usize = "OK ".len();
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum SelectorCursor {
|
||||
Unfocused,
|
||||
/// Cursor is at an entry
|
||||
Entry(usize),
|
||||
/// Cursor is located on the Ok button
|
||||
Ok,
|
||||
/// Cursor is located on the Cancel button
|
||||
Cancel,
|
||||
}
|
||||
|
||||
/// Shows a little window with options for user to select.
|
||||
///
|
||||
/// Instantiate with `Selector::new()`. Set `single_only` to true if user should
|
||||
/// only choose one of the options. After passing input events to this
|
||||
/// component, check `Selector::is_done` to see if the user has finalised their
|
||||
/// choices. Collect the choices by consuming the `Selector` with
|
||||
/// `Selector::collect()`
|
||||
pub struct Selector<
|
||||
T: 'static + PartialEq + std::fmt::Debug + Clone + Sync + Send,
|
||||
F: 'static + Sync + Send,
|
||||
> {
|
||||
/// allow only one selection
|
||||
single_only: bool,
|
||||
entries: Vec<(T, bool)>,
|
||||
entry_titles: Vec<String>,
|
||||
theme_default: ThemeAttribute,
|
||||
|
||||
cursor: SelectorCursor,
|
||||
scroll_x_cursor: usize,
|
||||
movement: Option<PageMovement>,
|
||||
vertical_alignment: Alignment,
|
||||
horizontal_alignment: Alignment,
|
||||
title: String,
|
||||
content: Screen<Virtual>,
|
||||
initialized: bool,
|
||||
/// If `true`, user has finished their selection
|
||||
done: bool,
|
||||
done_fn: F,
|
||||
dirty: bool,
|
||||
id: ComponentId,
|
||||
}
|
||||
|
||||
pub type UIConfirmationDialog = Selector<
|
||||
bool,
|
||||
Option<Box<dyn FnOnce(ComponentId, bool) -> Option<UIEvent> + 'static + Sync + Send>>,
|
||||
>;
|
||||
|
||||
pub type UIDialog<T> = Selector<
|
||||
T,
|
||||
Option<Box<dyn FnOnce(ComponentId, &[T]) -> Option<UIEvent> + 'static + Sync + Send>>,
|
||||
>;
|
||||
|
||||
impl<T: 'static + PartialEq + std::fmt::Debug + Clone + Sync + Send, F: 'static + Sync + Send>
|
||||
std::fmt::Debug for Selector<T, F>
|
||||
{
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
std::fmt::Display::fmt("Selector", f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static + PartialEq + std::fmt::Debug + Clone + Sync + Send, F: 'static + Sync + Send>
|
||||
std::fmt::Display for Selector<T, F>
|
||||
{
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
std::fmt::Display::fmt("Selector", f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static + PartialEq + std::fmt::Debug + Clone + Sync + Send, F: 'static + Sync + Send>
|
||||
PartialEq for Selector<T, F>
|
||||
{
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.entries == other.entries
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static + PartialEq + std::fmt::Debug + Clone + Sync + Send> Component for UIDialog<T> {
|
||||
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
|
||||
Selector::draw(self, grid, area, context);
|
||||
}
|
||||
|
||||
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
|
||||
if let UIEvent::ConfigReload { old_settings: _ } = event {
|
||||
self.initialise(context);
|
||||
self.set_dirty(true);
|
||||
return false;
|
||||
}
|
||||
|
||||
let shortcuts = self.shortcuts(context);
|
||||
match (event, self.cursor) {
|
||||
(UIEvent::Input(Key::Char('\n')), _) if self.single_only => {
|
||||
/* User can only select one entry, so Enter key finalises the selection */
|
||||
self.done = true;
|
||||
if let Some(event) = self.done() {
|
||||
context.replies.push_back(event);
|
||||
self.unrealize(context);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(Key::Char('\n')), SelectorCursor::Entry(c)) if !self.single_only => {
|
||||
/* User can select multiple entries, so Enter key toggles the entry under the
|
||||
* cursor */
|
||||
self.entries[c].1 = !self.entries[c].1;
|
||||
self.dirty = true;
|
||||
self.initialized = false;
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(Key::Char('\n')), SelectorCursor::Ok) if !self.single_only => {
|
||||
self.done = true;
|
||||
if let Some(event) = self.done() {
|
||||
context.replies.push_back(event);
|
||||
self.unrealize(context);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(Key::Esc), _) => {
|
||||
for e in self.entries.iter_mut() {
|
||||
e.1 = false;
|
||||
}
|
||||
if !self.done {
|
||||
self.unrealize(context);
|
||||
}
|
||||
self.done = true;
|
||||
_ = self.done();
|
||||
self.cancel(context);
|
||||
self.set_dirty(true);
|
||||
return false;
|
||||
}
|
||||
(UIEvent::Input(Key::Char('\n')), SelectorCursor::Cancel) if !self.single_only => {
|
||||
for e in self.entries.iter_mut() {
|
||||
e.1 = false;
|
||||
}
|
||||
self.done = true;
|
||||
if let Some(event) = self.done() {
|
||||
context.replies.push_back(event);
|
||||
self.unrealize(context);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(ref key), SelectorCursor::Unfocused)
|
||||
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_down"]) =>
|
||||
{
|
||||
if self.single_only {
|
||||
self.entries[0].1 = true;
|
||||
}
|
||||
self.cursor = SelectorCursor::Entry(0);
|
||||
self.dirty = true;
|
||||
self.initialized = false;
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(ref key), SelectorCursor::Entry(c))
|
||||
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_up"]) && c > 0 =>
|
||||
{
|
||||
if self.single_only {
|
||||
// Redraw selection
|
||||
self.entries[c].1 = false;
|
||||
self.entries[c - 1].1 = true;
|
||||
}
|
||||
self.cursor = SelectorCursor::Entry(c - 1);
|
||||
self.dirty = true;
|
||||
self.initialized = false;
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(ref key), SelectorCursor::Ok)
|
||||
| (UIEvent::Input(ref key), SelectorCursor::Cancel)
|
||||
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_up"]) =>
|
||||
{
|
||||
let c = self.entries.len().saturating_sub(1);
|
||||
self.cursor = SelectorCursor::Entry(c);
|
||||
self.dirty = true;
|
||||
self.initialized = false;
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(ref key), SelectorCursor::Entry(c))
|
||||
if c < self.entries.len().saturating_sub(1)
|
||||
&& shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_down"]) =>
|
||||
{
|
||||
if self.single_only {
|
||||
// Redraw selection
|
||||
self.entries[c].1 = false;
|
||||
self.entries[c + 1].1 = true;
|
||||
}
|
||||
self.cursor = SelectorCursor::Entry(c + 1);
|
||||
self.dirty = true;
|
||||
self.initialized = false;
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(ref key), SelectorCursor::Entry(_))
|
||||
if !self.single_only
|
||||
&& shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_down"]) =>
|
||||
{
|
||||
self.cursor = SelectorCursor::Ok;
|
||||
self.set_dirty(true);
|
||||
self.initialized = false;
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(ref key), SelectorCursor::Ok)
|
||||
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_right"]) =>
|
||||
{
|
||||
self.cursor = SelectorCursor::Cancel;
|
||||
self.set_dirty(true);
|
||||
self.initialized = false;
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(ref key), SelectorCursor::Cancel)
|
||||
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_left"]) =>
|
||||
{
|
||||
self.cursor = SelectorCursor::Ok;
|
||||
self.set_dirty(true);
|
||||
self.initialized = false;
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(ref key), _)
|
||||
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_left"]) =>
|
||||
{
|
||||
self.movement = Some(PageMovement::Left(1));
|
||||
self.set_dirty(true);
|
||||
self.initialized = false;
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(ref key), _)
|
||||
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_right"]) =>
|
||||
{
|
||||
self.movement = Some(PageMovement::Right(1));
|
||||
self.set_dirty(true);
|
||||
self.initialized = false;
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(ref key), _)
|
||||
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["prev_page"]) =>
|
||||
{
|
||||
self.movement = Some(PageMovement::PageUp(1));
|
||||
self.set_dirty(true);
|
||||
self.initialized = false;
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(ref key), _)
|
||||
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["next_page"]) =>
|
||||
{
|
||||
self.movement = Some(PageMovement::PageDown(1));
|
||||
self.set_dirty(true);
|
||||
self.initialized = false;
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(ref key), _)
|
||||
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["home_page"]) =>
|
||||
{
|
||||
self.movement = Some(PageMovement::Home);
|
||||
self.set_dirty(true);
|
||||
self.initialized = false;
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(ref key), _)
|
||||
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["end_page"]) =>
|
||||
{
|
||||
self.movement = Some(PageMovement::End);
|
||||
self.set_dirty(true);
|
||||
self.initialized = false;
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(ref key), _)
|
||||
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_up"])
|
||||
|| shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_down"]) =>
|
||||
{
|
||||
return true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn shortcuts(&self, context: &Context) -> ShortcutMaps {
|
||||
let mut map = ShortcutMaps::default();
|
||||
map.insert(
|
||||
Shortcuts::GENERAL,
|
||||
context.settings.shortcuts.general.key_values(),
|
||||
);
|
||||
map
|
||||
}
|
||||
|
||||
fn is_dirty(&self) -> bool {
|
||||
self.dirty
|
||||
}
|
||||
|
||||
fn set_dirty(&mut self, value: bool) {
|
||||
self.dirty = value;
|
||||
self.initialized = false;
|
||||
}
|
||||
|
||||
fn id(&self) -> ComponentId {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for UIConfirmationDialog {
|
||||
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
|
||||
Selector::draw(self, grid, area, context);
|
||||
}
|
||||
|
||||
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
|
||||
if let UIEvent::ConfigReload { old_settings: _ } = event {
|
||||
self.initialise(context);
|
||||
self.set_dirty(true);
|
||||
return false;
|
||||
}
|
||||
|
||||
let shortcuts = self.shortcuts(context);
|
||||
match (event, self.cursor) {
|
||||
(UIEvent::Input(Key::Char('\n')), _) if self.single_only => {
|
||||
/* User can only select one entry, so Enter key finalises the selection */
|
||||
self.done = true;
|
||||
if let Some(event) = self.done() {
|
||||
context.replies.push_back(event);
|
||||
self.unrealize(context);
|
||||
}
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(Key::Char('\n')), SelectorCursor::Entry(c)) if !self.single_only => {
|
||||
/* User can select multiple entries, so Enter key toggles the entry under the
|
||||
* cursor */
|
||||
self.entries[c].1 = !self.entries[c].1;
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(Key::Char('\n')), SelectorCursor::Ok) if !self.single_only => {
|
||||
self.done = true;
|
||||
if let Some(event) = self.done() {
|
||||
context.replies.push_back(event);
|
||||
self.unrealize(context);
|
||||
}
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(Key::Esc), _) => {
|
||||
for e in self.entries.iter_mut() {
|
||||
e.1 = false;
|
||||
}
|
||||
if !self.done {
|
||||
self.unrealize(context);
|
||||
}
|
||||
self.done = true;
|
||||
_ = self.done();
|
||||
self.cancel(context);
|
||||
self.set_dirty(true);
|
||||
return false;
|
||||
}
|
||||
(UIEvent::Input(Key::Char('\n')), SelectorCursor::Cancel) if !self.single_only => {
|
||||
for e in self.entries.iter_mut() {
|
||||
e.1 = false;
|
||||
}
|
||||
self.done = true;
|
||||
if let Some(event) = self.done() {
|
||||
context.replies.push_back(event);
|
||||
self.unrealize(context);
|
||||
}
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(ref key), SelectorCursor::Entry(c))
|
||||
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_up"]) && c > 0 =>
|
||||
{
|
||||
if self.single_only {
|
||||
// Redraw selection
|
||||
self.entries[c].1 = false;
|
||||
self.entries[c - 1].1 = true;
|
||||
}
|
||||
self.cursor = SelectorCursor::Entry(c - 1);
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(ref key), SelectorCursor::Ok)
|
||||
| (UIEvent::Input(ref key), SelectorCursor::Cancel)
|
||||
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_up"]) =>
|
||||
{
|
||||
let c = self.entries.len().saturating_sub(1);
|
||||
self.cursor = SelectorCursor::Entry(c);
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(ref key), SelectorCursor::Unfocused)
|
||||
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_down"]) =>
|
||||
{
|
||||
if self.single_only {
|
||||
self.entries[0].1 = true;
|
||||
}
|
||||
self.cursor = SelectorCursor::Entry(0);
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(ref key), SelectorCursor::Entry(c))
|
||||
if c < self.entries.len().saturating_sub(1)
|
||||
&& shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_down"]) =>
|
||||
{
|
||||
if self.single_only {
|
||||
// Redraw selection
|
||||
self.entries[c].1 = false;
|
||||
self.entries[c + 1].1 = true;
|
||||
}
|
||||
self.cursor = SelectorCursor::Entry(c + 1);
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(ref key), SelectorCursor::Entry(_))
|
||||
if !self.single_only
|
||||
&& shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_down"]) =>
|
||||
{
|
||||
self.cursor = SelectorCursor::Ok;
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(ref key), SelectorCursor::Ok)
|
||||
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_right"]) =>
|
||||
{
|
||||
self.cursor = SelectorCursor::Cancel;
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(ref key), SelectorCursor::Cancel)
|
||||
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_left"]) =>
|
||||
{
|
||||
self.cursor = SelectorCursor::Ok;
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
(UIEvent::Input(ref key), _)
|
||||
if shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_left"])
|
||||
|| shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_right"])
|
||||
|| shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_up"])
|
||||
|| shortcut!(key == shortcuts[Shortcuts::GENERAL]["scroll_down"]) =>
|
||||
{
|
||||
return true
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn shortcuts(&self, context: &Context) -> ShortcutMaps {
|
||||
let mut map = ShortcutMaps::default();
|
||||
map.insert(
|
||||
Shortcuts::GENERAL,
|
||||
context.settings.shortcuts.general.key_values(),
|
||||
);
|
||||
map
|
||||
}
|
||||
|
||||
fn is_dirty(&self) -> bool {
|
||||
self.dirty
|
||||
}
|
||||
|
||||
fn set_dirty(&mut self, value: bool) {
|
||||
self.dirty = value;
|
||||
}
|
||||
|
||||
fn id(&self) -> ComponentId {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PartialEq + std::fmt::Debug + Clone + Sync + Send, F: 'static + Sync + Send>
|
||||
Selector<T, F>
|
||||
{
|
||||
pub fn new(
|
||||
title: &str,
|
||||
mut entries: Vec<(T, String)>,
|
||||
single_only: bool,
|
||||
done_fn: F,
|
||||
context: &Context,
|
||||
) -> Self {
|
||||
let entry_titles = entries
|
||||
.iter_mut()
|
||||
.map(|(_id, ref mut title)| std::mem::take(title))
|
||||
.collect::<Vec<String>>();
|
||||
let mut identifiers: Vec<(T, bool)> =
|
||||
entries.into_iter().map(|(id, _)| (id, false)).collect();
|
||||
if single_only {
|
||||
/* set default option */
|
||||
identifiers[0].1 = true;
|
||||
}
|
||||
|
||||
let mut ret = Self {
|
||||
single_only,
|
||||
entries: identifiers,
|
||||
entry_titles,
|
||||
cursor: SelectorCursor::Unfocused,
|
||||
scroll_x_cursor: 0,
|
||||
movement: None,
|
||||
vertical_alignment: Alignment::Center,
|
||||
horizontal_alignment: Alignment::Center,
|
||||
title: title.to_string(),
|
||||
content: Screen::<Virtual>::new(),
|
||||
initialized: false,
|
||||
done: false,
|
||||
done_fn,
|
||||
dirty: true,
|
||||
theme_default: Default::default(),
|
||||
id: ComponentId::default(),
|
||||
};
|
||||
ret.initialise(context);
|
||||
ret
|
||||
}
|
||||
|
||||
fn initialise(&mut self, context: &Context) {
|
||||
self.theme_default = crate::conf::value(context, "theme_default");
|
||||
}
|
||||
|
||||
pub fn is_done(&self) -> bool {
|
||||
self.done
|
||||
}
|
||||
|
||||
pub fn collect(self) -> Vec<T> {
|
||||
self.entries
|
||||
.into_iter()
|
||||
.filter(|v| v.1)
|
||||
.map(|(id, _)| id)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn initialize(&mut self, context: &Context) {
|
||||
let mut highlighted_attrs = crate::conf::value(context, "widgets.options.highlighted");
|
||||
if !context.settings.terminal.use_color() {
|
||||
highlighted_attrs.attrs |= Attr::REVERSE;
|
||||
}
|
||||
let shortcuts = context.settings.shortcuts.general.key_values();
|
||||
let navigate_help_string = format!(
|
||||
"Navigate options with {} to go down, {} to go up, select with {}",
|
||||
shortcuts["scroll_down"],
|
||||
shortcuts["scroll_up"],
|
||||
Key::Char('\n')
|
||||
);
|
||||
let width = std::cmp::max(
|
||||
self.entry_titles.iter().map(|e| e.len()).max().unwrap_or(0) + 3,
|
||||
std::cmp::max(self.title.len(), navigate_help_string.len()) + 3,
|
||||
) + 3;
|
||||
let height = self.entries.len()
|
||||
// padding
|
||||
+ 3
|
||||
// buttons row
|
||||
+ if self.single_only { 1 } else { 5 };
|
||||
if !self.content.resize_with_context(width, height, context) {
|
||||
self.dirty = false;
|
||||
return;
|
||||
}
|
||||
|
||||
let inner_area = self.content.area();
|
||||
let (_, y) = self.content.grid_mut().write_string(
|
||||
&self.title,
|
||||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs | Attr::BOLD,
|
||||
inner_area.skip_cols(2),
|
||||
None,
|
||||
);
|
||||
|
||||
let y = self
|
||||
.content
|
||||
.grid_mut()
|
||||
.write_string(
|
||||
&navigate_help_string,
|
||||
self.theme_default.fg,
|
||||
self.theme_default.bg,
|
||||
self.theme_default.attrs | Attr::ITALICS,
|
||||
inner_area.skip_cols(2).skip_rows(y + 2),
|
||||
None,
|
||||
)
|
||||
.1
|
||||
+ y
|
||||
+ 2;
|
||||
|
||||
let inner_area = inner_area.skip_cols(1).skip_rows(y + 2);
|
||||
|
||||
/* Extra room for buttons Okay/Cancel */
|
||||
if self.single_only {
|
||||
for (i, e) in self.entry_titles.iter().enumerate() {
|
||||
let attr = if matches!(self.cursor, SelectorCursor::Entry(e) if e == i) {
|
||||
highlighted_attrs
|
||||
} else {
|
||||
self.theme_default
|
||||
};
|
||||
self.content.grid_mut().write_string(
|
||||
e,
|
||||
attr.fg,
|
||||
attr.bg,
|
||||
attr.attrs,
|
||||
inner_area.nth_row(i),
|
||||
None,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
for (i, e) in self.entry_titles.iter().enumerate() {
|
||||
let attr = if matches!(self.cursor, SelectorCursor::Entry(e) if e == i) {
|
||||
highlighted_attrs
|
||||
} else {
|
||||
self.theme_default
|
||||
};
|
||||
self.content.grid_mut().write_string(
|
||||
&format!("[{}] {}", if self.entries[i].1 { "x" } else { " " }, e),
|
||||
attr.fg,
|
||||
attr.bg,
|
||||
attr.attrs,
|
||||
inner_area.nth_row(i),
|
||||
None,
|
||||
);
|
||||
}
|
||||
let inner_area = inner_area.nth_row(self.entry_titles.len() + 2).skip_cols(2);
|
||||
let attr = if matches!(self.cursor, SelectorCursor::Ok) {
|
||||
highlighted_attrs
|
||||
} else {
|
||||
self.theme_default
|
||||
};
|
||||
let (x, y) = self.content.grid_mut().write_string(
|
||||
OK,
|
||||
attr.fg,
|
||||
attr.bg,
|
||||
attr.attrs | Attr::BOLD,
|
||||
inner_area,
|
||||
None,
|
||||
);
|
||||
let attr = if matches!(self.cursor, SelectorCursor::Cancel) {
|
||||
highlighted_attrs
|
||||
} else {
|
||||
self.theme_default
|
||||
};
|
||||
self.content.grid_mut().write_string(
|
||||
CANCEL,
|
||||
attr.fg,
|
||||
attr.bg,
|
||||
attr.attrs,
|
||||
inner_area.skip(CANCEL_OFFSET + x, y),
|
||||
None,
|
||||
);
|
||||
}
|
||||
self.initialized = true;
|
||||
}
|
||||
|
||||
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
|
||||
let mut highlighted_attrs = crate::conf::value(context, "widgets.options.highlighted");
|
||||
if !context.settings.terminal.use_color() {
|
||||
highlighted_attrs.attrs |= Attr::REVERSE;
|
||||
}
|
||||
if !self.initialized {
|
||||
// [ref:FIXME]: don't re-initialize when the only change is highlight index.
|
||||
self.initialize(context);
|
||||
}
|
||||
let (width, height) = self.content.area().size();
|
||||
let dialog_area = area.align_inside(
|
||||
(width + 2, height + 2),
|
||||
self.horizontal_alignment,
|
||||
self.vertical_alignment,
|
||||
);
|
||||
let inner_area = create_box(grid, dialog_area);
|
||||
let rows = inner_area.height();
|
||||
if let Some(mvm) = self.movement.take() {
|
||||
match mvm {
|
||||
PageMovement::Up(_) | PageMovement::Down(_) => {}
|
||||
PageMovement::Right(amount) => {
|
||||
self.scroll_x_cursor = self.scroll_x_cursor.saturating_add(amount);
|
||||
}
|
||||
PageMovement::Left(amount) => {
|
||||
self.scroll_x_cursor = self.scroll_x_cursor.saturating_sub(amount);
|
||||
}
|
||||
PageMovement::PageUp(multiplier) => match self.cursor {
|
||||
SelectorCursor::Unfocused => {
|
||||
self.cursor = SelectorCursor::Entry(0);
|
||||
self.initialize(context);
|
||||
}
|
||||
SelectorCursor::Entry(c) => {
|
||||
self.cursor = SelectorCursor::Entry(c.saturating_sub(multiplier * rows));
|
||||
self.initialize(context);
|
||||
}
|
||||
SelectorCursor::Ok | SelectorCursor::Cancel
|
||||
if !self.entry_titles.is_empty() =>
|
||||
{
|
||||
self.cursor = SelectorCursor::Entry(
|
||||
self.entry_titles.len().saturating_sub(multiplier * rows),
|
||||
);
|
||||
self.initialize(context);
|
||||
}
|
||||
SelectorCursor::Ok | SelectorCursor::Cancel => {}
|
||||
},
|
||||
PageMovement::PageDown(multiplier) => match self.cursor {
|
||||
SelectorCursor::Unfocused => {
|
||||
self.cursor = SelectorCursor::Entry(
|
||||
self.entry_titles
|
||||
.len()
|
||||
.saturating_sub(1)
|
||||
.min(multiplier * rows),
|
||||
);
|
||||
self.initialize(context);
|
||||
}
|
||||
SelectorCursor::Entry(c)
|
||||
if c.saturating_add(multiplier * rows) < self.entry_titles.len()
|
||||
&& !self.entry_titles.is_empty() =>
|
||||
{
|
||||
self.cursor = SelectorCursor::Entry(
|
||||
self.entry_titles
|
||||
.len()
|
||||
.saturating_sub(1)
|
||||
.min(c.saturating_add(multiplier * rows)),
|
||||
);
|
||||
self.initialize(context);
|
||||
}
|
||||
SelectorCursor::Entry(_) => {
|
||||
self.cursor = SelectorCursor::Ok;
|
||||
self.initialize(context);
|
||||
}
|
||||
SelectorCursor::Ok | SelectorCursor::Cancel => {}
|
||||
},
|
||||
PageMovement::Home if !self.entry_titles.is_empty() => {
|
||||
self.cursor = SelectorCursor::Entry(0);
|
||||
self.initialize(context);
|
||||
}
|
||||
PageMovement::End
|
||||
if matches!(self.cursor, SelectorCursor::Ok | SelectorCursor::Cancel) => {}
|
||||
PageMovement::End
|
||||
if !matches!(self.cursor, SelectorCursor::Entry(c) if c +1 == self.entry_titles.len())
|
||||
&& !self.entry_titles.is_empty() =>
|
||||
{
|
||||
self.cursor = SelectorCursor::Entry(self.entry_titles.len().saturating_sub(1));
|
||||
self.initialize(context);
|
||||
}
|
||||
PageMovement::Home | PageMovement::End => {}
|
||||
}
|
||||
}
|
||||
let skip_rows = match self.cursor {
|
||||
SelectorCursor::Unfocused => 0,
|
||||
SelectorCursor::Entry(e) if e >= rows => e.min(height.saturating_sub(rows)),
|
||||
SelectorCursor::Entry(_) => 0,
|
||||
SelectorCursor::Ok | SelectorCursor::Cancel => height.saturating_sub(rows),
|
||||
};
|
||||
|
||||
self.scroll_x_cursor = self
|
||||
.scroll_x_cursor
|
||||
.min(width.saturating_sub(inner_area.width()));
|
||||
grid.copy_area(
|
||||
self.content.grid(),
|
||||
inner_area,
|
||||
self.content
|
||||
.area()
|
||||
.skip_cols(self.scroll_x_cursor)
|
||||
.skip_rows(skip_rows),
|
||||
);
|
||||
|
||||
if height > dialog_area.height() {
|
||||
let inner_area = inner_area.skip_rows(1);
|
||||
ScrollBar::default().set_show_arrows(true).draw(
|
||||
grid,
|
||||
inner_area.nth_col(inner_area.width().saturating_sub(1)),
|
||||
context,
|
||||
// position
|
||||
skip_rows,
|
||||
// visible_rows
|
||||
inner_area.height(),
|
||||
// length
|
||||
height,
|
||||
);
|
||||
}
|
||||
if width > dialog_area.width() {
|
||||
let inner_area = inner_area.skip_cols(1);
|
||||
ScrollBar::default().set_show_arrows(true).draw_horizontal(
|
||||
grid,
|
||||
inner_area.nth_row(inner_area.height().saturating_sub(1)),
|
||||
context,
|
||||
// position
|
||||
self.scroll_x_cursor,
|
||||
// visible_cols
|
||||
inner_area.width(),
|
||||
// length
|
||||
width,
|
||||
);
|
||||
}
|
||||
context.dirty_areas.push_back(dialog_area);
|
||||
self.dirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static + PartialEq + std::fmt::Debug + Clone + Sync + Send> UIDialog<T> {
|
||||
fn done(&mut self) -> Option<UIEvent> {
|
||||
let Self {
|
||||
ref mut done_fn,
|
||||
ref mut entries,
|
||||
ref id,
|
||||
..
|
||||
} = self;
|
||||
done_fn.take().and_then(|done_fn| {
|
||||
done_fn(
|
||||
*id,
|
||||
entries
|
||||
.iter()
|
||||
.filter(|v| v.1)
|
||||
.map(|(id, _)| id)
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
.as_slice(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn cancel(&mut self, context: &mut Context) {
|
||||
context.unrealized.insert(self.id());
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::ComponentUnrealize(self.id()));
|
||||
}
|
||||
}
|
||||
|
||||
impl UIConfirmationDialog {
|
||||
fn done(&mut self) -> Option<UIEvent> {
|
||||
let Self {
|
||||
ref mut done_fn,
|
||||
ref mut entries,
|
||||
ref id,
|
||||
..
|
||||
} = self;
|
||||
done_fn.take().and_then(|done_fn| {
|
||||
done_fn(
|
||||
*id,
|
||||
entries
|
||||
.iter()
|
||||
.filter(|v| v.1)
|
||||
.map(|(id, _)| id)
|
||||
.cloned()
|
||||
.any(std::convert::identity),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn cancel(&mut self, context: &mut Context) {
|
||||
context.unrealized.insert(self.id());
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::ComponentUnrealize(self.id()));
|
||||
}
|
||||
}
|
104
melib/Cargo.toml
|
@ -1,14 +1,15 @@
|
|||
[package]
|
||||
name = "melib"
|
||||
version = "0.8.5-rc.3"
|
||||
authors = ["Manos Pitsidianakis <manos@pitsidianak.is>"]
|
||||
edition = "2021"
|
||||
version = "0.7.2"
|
||||
authors = ["Manos Pitsidianakis <epilys@nessuent.xyz>"]
|
||||
workspace = ".."
|
||||
edition = "2018"
|
||||
build = "build.rs"
|
||||
rust-version = "1.68.2"
|
||||
rust-version = "1.65.0"
|
||||
|
||||
homepage = "https://meli-email.org"
|
||||
repository = "https://git.meli-email.org/meli/meli.git"
|
||||
description = "library for e-mail clients and other e-mail applications"
|
||||
homepage = "https://meli.delivery"
|
||||
repository = "https://git.meli.delivery/meli/meli.git"
|
||||
description = "mail library"
|
||||
keywords = ["mail", "mua", "maildir", "imap", "jmap"]
|
||||
categories = ["email", "parser-implementations"]
|
||||
license = "GPL-3.0-or-later"
|
||||
|
@ -21,67 +22,68 @@ path = "src/lib.rs"
|
|||
[dependencies]
|
||||
async-stream = "^0.3"
|
||||
base64 = { version = "^0.13", optional = true }
|
||||
bitflags = { version = "2.4", features = ["serde"] }
|
||||
bitflags = "1.0"
|
||||
data-encoding = { version = "2.1.1" }
|
||||
encoding = { version = "0.2.33", default-features = false }
|
||||
encoding_rs = { version = "^0.8" }
|
||||
flate2 = { version = "1.0.16" }
|
||||
flate2 = { version = "1.0.16", optional = true }
|
||||
futures = "0.3.5"
|
||||
|
||||
imap-codec = { version = "1.0.0", features = ["ext_condstore_qresync"], optional = true }
|
||||
|
||||
indexmap = { version = "^1.5", default-features = false, features = ["serde-1"] }
|
||||
indexmap = { version = "^1.5", default-features = false, features = ["serde-1", ] }
|
||||
isahc = { version = "^1.7.2", optional = true, default-features = false, features = ["http2", "json", "text-decoding"] }
|
||||
libc = { version = "0.2.125", features = ["extra_traits"] }
|
||||
libc = { version = "0.2.125", features = ["extra_traits",] }
|
||||
|
||||
libloading = "^0.7"
|
||||
log = { version = "0.4", features = ["std"] }
|
||||
native-tls = { version = "0.2.3", default-features = false, optional = true }
|
||||
nix = { version = "0.27", default-features = false, features = ["fs", "socket", "dir", "hostname"] }
|
||||
nix = "^0.24"
|
||||
nom = { version = "7" }
|
||||
notify = { version = "6.1.1", optional = true }
|
||||
polling = "2.8"
|
||||
notify = { version = "4.0.15", optional = true }
|
||||
regex = { version = "1" }
|
||||
rusqlite = { version = "^0.29", default-features = false, features = ["array"], optional = true }
|
||||
serde = { version = "1.0", features = ["rc"] }
|
||||
serde_derive = "1.0"
|
||||
serde_json = { version = "1.0", features = ["raw_value"] }
|
||||
serde_path_to_error = { version = "0.1" }
|
||||
smallvec = { version = "^1.5.0", features = ["serde"] }
|
||||
rusqlite = { version = "^0.28", default-features = false, optional = true }
|
||||
serde = { version = "1.0.71", features = ["rc", ] }
|
||||
serde_derive = "1.0.71"
|
||||
serde_json = { version = "1.0", features = ["raw_value",] }
|
||||
smallvec = { version = "^1.5.0", features = ["serde", ] }
|
||||
smol = "1.0.0"
|
||||
socket2 = { version = "0.5", features = [] }
|
||||
unicode-segmentation = { version = "1.2.1", default-features = false }
|
||||
url = { version = "2.4", optional = true }
|
||||
|
||||
unicode-segmentation = { version = "1.2.1", default-features = false, optional = true }
|
||||
uuid = { version = "^1", features = ["serde", "v4", "v5"] }
|
||||
xdg = "2.1.0"
|
||||
xdg-utils = "^0.4.0"
|
||||
|
||||
[features]
|
||||
default = ["imap", "nntp", "maildir", "mbox", "vcard", "smtp"]
|
||||
|
||||
debug-tracing = []
|
||||
gpgme = []
|
||||
http = ["isahc"]
|
||||
http-static = ["isahc", "isahc/static-curl"]
|
||||
imap = ["imap-codec", "tls"]
|
||||
imap-trace = ["imap"]
|
||||
jmap = ["http", "url/serde"]
|
||||
jmap-trace = ["jmap"]
|
||||
nntp = ["tls"]
|
||||
nntp-trace = ["nntp"]
|
||||
maildir = ["notify"]
|
||||
mbox = ["notify"]
|
||||
notmuch = ["notify"]
|
||||
smtp = ["tls", "base64"]
|
||||
smtp-trace = ["smtp"]
|
||||
sqlite3 = ["rusqlite"]
|
||||
sqlite3-static = ["sqlite3", "rusqlite/bundled-full"]
|
||||
tls = ["native-tls"]
|
||||
tls-static = ["tls", "native-tls/vendored"]
|
||||
vcard = []
|
||||
|
||||
[build-dependencies]
|
||||
flate2 = { version = "1.0.16" }
|
||||
[dependencies.imap-codec]
|
||||
version = "0.10.0"
|
||||
features = [
|
||||
"ext_condstore_qresync",
|
||||
"ext_enable",
|
||||
"ext_idle",
|
||||
"ext_literal",
|
||||
"ext_move",
|
||||
"ext_sasl_ir",
|
||||
"ext_unselect"
|
||||
]
|
||||
optional = true
|
||||
|
||||
[dev-dependencies]
|
||||
mailin-embedded = { version = "0.7", features = ["rtls"] }
|
||||
stderrlog = "^0.5"
|
||||
|
||||
[features]
|
||||
default = ["unicode_algorithms", "imap_backend", "maildir_backend", "mbox_backend", "vcard", "sqlite3", "smtp", "deflate_compression"]
|
||||
|
||||
debug-tracing = []
|
||||
deflate_compression = ["flate2", "imap-codec/ext_compress"]
|
||||
gpgme = []
|
||||
http = ["isahc"]
|
||||
http-static = ["isahc", "isahc/static-curl"]
|
||||
imap_backend = ["imap-codec", "tls"]
|
||||
jmap_backend = ["http"]
|
||||
maildir_backend = ["notify"]
|
||||
mbox_backend = ["notify"]
|
||||
notmuch_backend = []
|
||||
smtp = ["tls", "base64"]
|
||||
sqlite3 = ["rusqlite", ]
|
||||
tls = ["native-tls"]
|
||||
unicode_algorithms = ["unicode-segmentation"]
|
||||
vcard = []
|
||||
|
|
|
@ -6,26 +6,19 @@ Library for handling mail.
|
|||
|
||||
## optional features
|
||||
|
||||
| feature flag | dependencies | notes |
|
||||
|------------------------------|-------------------------------------|--------------------------|
|
||||
| `smtp` | `native-tls`, `base64` | async SMTP communication |
|
||||
|------------------------------|-------------------------------------|--------------------------|
|
||||
| `imap` | `native-tls` | |
|
||||
|------------------------------|-------------------------------------|--------------------------|
|
||||
| `jmap` | `isahc`, `native-tls`, `serde_json` | |
|
||||
|------------------------------|-------------------------------------|--------------------------|
|
||||
| `maildir` | `notify` | |
|
||||
|------------------------------|-------------------------------------|--------------------------|
|
||||
| `mbox` | `notify` | |
|
||||
|------------------------------|-------------------------------------|--------------------------|
|
||||
| `notmuch` | `notify` | |
|
||||
|------------------------------|-------------------------------------|--------------------------|
|
||||
| `sqlite` | `rusqlite` | Used in IMAP cache. |
|
||||
|------------------------------|-------------------------------------|--------------------------|
|
||||
| `vcard` | | vcard parsing |
|
||||
|------------------------------|-------------------------------------|--------------------------|
|
||||
| `gpgme` | | GPG use with libgpgme |
|
||||
|------------------------------|-------------------------------------|--------------------------|
|
||||
| feature flag | dependencies | notes |
|
||||
| ---------------------- | ----------------------------------- | ------------------------ |
|
||||
| `imap_backend` | `native-tls` | |
|
||||
| `deflate_compression` | `flate2` | for use with IMAP |
|
||||
| `jmap_backend` | `isahc`, `native-tls`, `serde_json` | |
|
||||
| `maildir_backend` | `notify` | |
|
||||
| `mbox_backend` | `notify` | |
|
||||
| `notmuch_backend` | `notify` | |
|
||||
| `sqlite` | `rusqlite` | used in IMAP cache |
|
||||
| `unicode_algorithms` | `unicode-segmentation` | linebreaking algo etc |
|
||||
| `vcard` | | vcard parsing |
|
||||
| `gpgme` | | GPG use with libgpgme |
|
||||
| `smtp` | `native-tls`, `base64` | async SMTP communication |
|
||||
|
||||
## Example: Parsing bytes into an `Envelope`
|
||||
|
||||
|
|
|
@ -21,14 +21,15 @@
|
|||
|
||||
#![allow(clippy::needless_range_loop)]
|
||||
|
||||
include!("src/text/types.rs");
|
||||
#[cfg(feature = "unicode_algorithms")]
|
||||
include!("src/text_processing/types.rs");
|
||||
|
||||
fn main() -> Result<(), std::io::Error> {
|
||||
#[cfg(feature = "unicode_algorithms")]
|
||||
{
|
||||
const MOD_PATH: &str = "src/text/tables.rs";
|
||||
println!("cargo:rerun-if-env-changed=UNICODE_REGENERATE_TABLES");
|
||||
const MOD_PATH: &str = "src/text_processing/tables.rs";
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
println!("cargo:rerun-if-changed={MOD_PATH}");
|
||||
println!("cargo:rerun-if-changed={}", MOD_PATH);
|
||||
/* Line break tables */
|
||||
use std::{
|
||||
fs::File,
|
||||
|
@ -51,24 +52,7 @@ fn main() -> Result<(), std::io::Error> {
|
|||
"{} already exists, delete it if you want to replace it.",
|
||||
mod_path.display()
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
if std::env::var("UNICODE_REGENERATE_TABLES").is_err() {
|
||||
const CACHED_MODULE: &[u8] = include_bytes!(concat!("./src/text/tables.rs.gz"));
|
||||
|
||||
let mut gz = GzDecoder::new(CACHED_MODULE);
|
||||
use flate2::bufread::GzDecoder;
|
||||
let mut v = String::with_capacity(
|
||||
8, /*
|
||||
str::parse::<usize>(unsafe {
|
||||
std::str::from_utf8_unchecked(gz.header().unwrap().comment().unwrap())
|
||||
})
|
||||
.unwrap_or_else(|_| panic!("was not compressed with size comment header",)),*/
|
||||
);
|
||||
gz.read_to_string(&mut v)?;
|
||||
let mut file = File::create(mod_path)?;
|
||||
file.write_all(v.as_bytes())?;
|
||||
return Ok(());
|
||||
std::process::exit(0);
|
||||
}
|
||||
let mut child = Command::new("curl")
|
||||
.args(["-o", "-", LINE_BREAK_TABLE_URL])
|
||||
|
@ -348,7 +332,7 @@ fn main() -> Result<(), std::io::Error> {
|
|||
let mut file = File::create(mod_path)?;
|
||||
file.write_all(
|
||||
br#"/*
|
||||
* meli - text crate.
|
||||
* meli - text_processing crate.
|
||||
*
|
||||
* Copyright 2017-2020 Manos Pitsidianakis
|
||||
*
|
||||
|
|
|
@ -19,25 +19,21 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
pub mod mutt;
|
||||
#[cfg(feature = "vcard")]
|
||||
pub mod vcard;
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
hash::{Hash, Hasher},
|
||||
ops::Deref,
|
||||
};
|
||||
pub mod mutt;
|
||||
|
||||
use std::{collections::HashMap, ops::Deref};
|
||||
|
||||
use indexmap::IndexMap;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::utils::{
|
||||
datetime::{now, timestamp_to_string, UnixTimestamp},
|
||||
datetime::{self, UnixTimestamp},
|
||||
parsec::Parser,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
||||
#[derive(Hash, Debug, PartialEq, Eq, Clone, Copy, Deserialize, Serialize)]
|
||||
#[serde(from = "String")]
|
||||
#[serde(into = "String")]
|
||||
pub enum CardId {
|
||||
|
@ -62,7 +58,11 @@ impl From<CardId> for String {
|
|||
|
||||
impl From<String> for CardId {
|
||||
fn from(s: String) -> Self {
|
||||
use std::{collections::hash_map::DefaultHasher, str::FromStr};
|
||||
use std::{
|
||||
collections::hash_map::DefaultHasher,
|
||||
hash::{Hash, Hasher},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
if let Ok(u) = Uuid::parse_str(s.as_str()) {
|
||||
Self::Uuid(u)
|
||||
|
@ -76,15 +76,15 @@ impl From<String> for CardId {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub struct AddressBook {
|
||||
display_name: String,
|
||||
created: UnixTimestamp,
|
||||
last_edited: UnixTimestamp,
|
||||
pub cards: IndexMap<CardId, Card>,
|
||||
pub cards: HashMap<CardId, Card>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub struct Card {
|
||||
id: CardId,
|
||||
title: String,
|
||||
|
@ -92,48 +92,28 @@ pub struct Card {
|
|||
additionalname: String,
|
||||
name_prefix: String,
|
||||
name_suffix: String,
|
||||
//address
|
||||
birthday: Option<UnixTimestamp>,
|
||||
email: String,
|
||||
url: String,
|
||||
key: String,
|
||||
|
||||
color: u8,
|
||||
last_edited: UnixTimestamp,
|
||||
extra_properties: IndexMap<String, String>,
|
||||
/// If `true`, we can't make any changes because we do not manage this
|
||||
extra_properties: HashMap<String, String>,
|
||||
|
||||
/// If true, we can't make any changes because we do not manage this
|
||||
/// resource.
|
||||
external_resource: bool,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Card {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
if !self.name.is_empty() {
|
||||
self.name.fmt(fmt)
|
||||
} else if !self.email.is_empty() {
|
||||
self.email.fmt(fmt)
|
||||
} else {
|
||||
"empty contact".fmt(fmt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for Card {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
let mut serialized = serde_json::json! { self };
|
||||
// The following anonymous let bind is just to make sure at compile-time that
|
||||
// `id` field is present in Self and serialized["id"] will be a valid access.
|
||||
let _: &CardId = &self.id;
|
||||
serialized["id"] = serde_json::json! { CardId::Hash(0) };
|
||||
serialized.to_string().hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl AddressBook {
|
||||
pub fn new(display_name: String) -> Self {
|
||||
Self {
|
||||
display_name,
|
||||
created: now(),
|
||||
last_edited: now(),
|
||||
cards: IndexMap::default(),
|
||||
created: datetime::now(),
|
||||
last_edited: datetime::now(),
|
||||
cards: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -182,65 +162,29 @@ impl AddressBook {
|
|||
pub fn add_card(&mut self, card: Card) {
|
||||
self.cards.insert(card.id, card);
|
||||
}
|
||||
|
||||
pub fn remove_card(&mut self, card_id: CardId) {
|
||||
self.cards.remove(&card_id);
|
||||
}
|
||||
|
||||
pub fn card_exists(&self, card_id: CardId) -> bool {
|
||||
self.cards.contains_key(&card_id)
|
||||
}
|
||||
|
||||
pub fn search(&self, term: &str) -> Vec<String> {
|
||||
self.cards
|
||||
.values()
|
||||
.filter(|c| c.email.contains(term) || c.name.contains(term))
|
||||
.map(|c| {
|
||||
crate::email::Address::new(
|
||||
if c.name.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(c.name.clone())
|
||||
},
|
||||
c.email.clone(),
|
||||
)
|
||||
.to_string()
|
||||
})
|
||||
.filter(|c| c.email.contains(term))
|
||||
.map(|c| format!("{} <{}>", &c.name, &c.email))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for AddressBook {
|
||||
type Target = IndexMap<CardId, Card>;
|
||||
type Target = HashMap<CardId, Card>;
|
||||
|
||||
fn deref(&self) -> &IndexMap<CardId, Card> {
|
||||
fn deref(&self) -> &HashMap<CardId, Card> {
|
||||
&self.cards
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! get_fn {
|
||||
($fn:tt str) => {
|
||||
pub fn $fn(&self) -> &str {
|
||||
self.$fn.as_str()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! set_fn {
|
||||
($n:ident, $fn:tt) => {
|
||||
pub fn $n(&mut self, val: String) -> &mut Self {
|
||||
self.$fn = val;
|
||||
self
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl Default for Card {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Card {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
|
@ -256,9 +200,9 @@ impl Card {
|
|||
url: String::new(),
|
||||
key: String::new(),
|
||||
|
||||
last_edited: now(),
|
||||
last_edited: datetime::now(),
|
||||
external_resource: false,
|
||||
extra_properties: IndexMap::default(),
|
||||
extra_properties: HashMap::default(),
|
||||
color: 0,
|
||||
}
|
||||
}
|
||||
|
@ -267,29 +211,32 @@ impl Card {
|
|||
&self.id
|
||||
}
|
||||
|
||||
get_fn! { title str }
|
||||
get_fn! { name str }
|
||||
get_fn! { additionalname str }
|
||||
get_fn! { name_prefix str }
|
||||
get_fn! { name_suffix str }
|
||||
get_fn! { email str }
|
||||
get_fn! { url str }
|
||||
get_fn! { key str }
|
||||
|
||||
pub fn title(&self) -> &str {
|
||||
self.title.as_str()
|
||||
}
|
||||
pub fn name(&self) -> &str {
|
||||
self.name.as_str()
|
||||
}
|
||||
pub fn additionalname(&self) -> &str {
|
||||
self.additionalname.as_str()
|
||||
}
|
||||
pub fn name_prefix(&self) -> &str {
|
||||
self.name_prefix.as_str()
|
||||
}
|
||||
pub fn name_suffix(&self) -> &str {
|
||||
self.name_suffix.as_str()
|
||||
}
|
||||
pub fn email(&self) -> &str {
|
||||
self.email.as_str()
|
||||
}
|
||||
pub fn url(&self) -> &str {
|
||||
self.url.as_str()
|
||||
}
|
||||
pub fn key(&self) -> &str {
|
||||
self.key.as_str()
|
||||
}
|
||||
pub fn last_edited(&self) -> String {
|
||||
timestamp_to_string(self.last_edited, None, false)
|
||||
}
|
||||
|
||||
pub fn extra_property(&self, key: &str) -> Option<&str> {
|
||||
self.extra_properties.get(key).map(String::as_str)
|
||||
}
|
||||
|
||||
pub fn extra_properties(&self) -> &IndexMap<String, String> {
|
||||
&self.extra_properties
|
||||
}
|
||||
|
||||
pub fn external_resource(&self) -> bool {
|
||||
self.external_resource
|
||||
datetime::timestamp_to_string(self.last_edited, None, false)
|
||||
}
|
||||
|
||||
pub fn set_id(&mut self, new_val: CardId) -> &mut Self {
|
||||
|
@ -297,52 +244,104 @@ impl Card {
|
|||
self
|
||||
}
|
||||
|
||||
set_fn! { set_title, title }
|
||||
set_fn! { set_name, name }
|
||||
set_fn! { set_additionalname, additionalname }
|
||||
set_fn! { set_name_prefix, name_prefix }
|
||||
set_fn! { set_name_suffix, name_suffix }
|
||||
set_fn! { set_email, email }
|
||||
set_fn! { set_url, url }
|
||||
set_fn! { set_key, key }
|
||||
pub fn set_title(&mut self, new: String) -> &mut Self {
|
||||
self.title = new;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_name(&mut self, new: String) -> &mut Self {
|
||||
self.name = new;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_additionalname(&mut self, new: String) -> &mut Self {
|
||||
self.additionalname = new;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_name_prefix(&mut self, new: String) -> &mut Self {
|
||||
self.name_prefix = new;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_name_suffix(&mut self, new: String) -> &mut Self {
|
||||
self.name_suffix = new;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_email(&mut self, new: String) -> &mut Self {
|
||||
self.email = new;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_url(&mut self, new: String) -> &mut Self {
|
||||
self.url = new;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_key(&mut self, new: String) -> &mut Self {
|
||||
self.key = new;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_extra_property(&mut self, key: &str, value: String) -> &mut Self {
|
||||
self.extra_properties.insert(key.to_string(), value);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn extra_property(&self, key: &str) -> Option<&str> {
|
||||
self.extra_properties.get(key).map(String::as_str)
|
||||
}
|
||||
|
||||
pub fn extra_properties(&self) -> &HashMap<String, String> {
|
||||
&self.extra_properties
|
||||
}
|
||||
|
||||
pub fn set_external_resource(&mut self, new_val: bool) -> &mut Self {
|
||||
self.external_resource = new_val;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn external_resource(&self) -> bool {
|
||||
self.external_resource
|
||||
}
|
||||
}
|
||||
|
||||
impl From<IndexMap<String, String>> for Card {
|
||||
fn from(mut map: IndexMap<String, String>) -> Self {
|
||||
impl From<HashMap<String, String>> for Card {
|
||||
fn from(mut map: HashMap<String, String>) -> Self {
|
||||
let mut card = Self::new();
|
||||
macro_rules! get {
|
||||
($key:literal, $field:tt) => {
|
||||
if let Some(val) = map.remove($key) {
|
||||
card.$field = val;
|
||||
}
|
||||
};
|
||||
if let Some(val) = map.remove("TITLE") {
|
||||
card.title = val;
|
||||
}
|
||||
if let Some(val) = map.remove("NAME") {
|
||||
card.name = val;
|
||||
}
|
||||
if let Some(val) = map.remove("ADDITIONAL NAME") {
|
||||
card.additionalname = val;
|
||||
}
|
||||
if let Some(val) = map.remove("NAME PREFIX") {
|
||||
card.name_prefix = val;
|
||||
}
|
||||
if let Some(val) = map.remove("NAME SUFFIX") {
|
||||
card.name_suffix = val;
|
||||
}
|
||||
|
||||
if let Some(val) = map.remove("E-MAIL") {
|
||||
card.email = val;
|
||||
}
|
||||
if let Some(val) = map.remove("URL") {
|
||||
card.url = val;
|
||||
}
|
||||
if let Some(val) = map.remove("KEY") {
|
||||
card.key = val;
|
||||
}
|
||||
get! { "TITLE", title };
|
||||
get! { "NAME", name };
|
||||
get! { "ADDITIONAL NAME", additionalname };
|
||||
get! { "NAME PREFIX", name_prefix };
|
||||
get! { "NAME SUFFIX", name_suffix };
|
||||
get! { "E-MAIL", email };
|
||||
get! { "URL", url };
|
||||
get! { "KEY", key };
|
||||
card.extra_properties = map;
|
||||
card
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HashMap<String, String>> for Card {
|
||||
fn from(map: HashMap<String, String>) -> Self {
|
||||
let map: IndexMap<String, String> = map.into_iter().collect();
|
||||
Self::from(map)
|
||||
impl Default for Card {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,12 +31,12 @@ use std::{collections::HashMap, convert::TryInto};
|
|||
|
||||
use super::*;
|
||||
use crate::{
|
||||
error::{Error, ErrorKind, Result},
|
||||
error::{Error, Result},
|
||||
utils::parsec::{match_literal_anycase, one_or_more, peek, prefix, take_until, Parser},
|
||||
};
|
||||
|
||||
/* Supported vcard versions */
|
||||
pub trait VCardVersion: std::fmt::Debug {}
|
||||
pub trait VCardVersion: core::fmt::Debug {}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct VCardVersionUnknown;
|
||||
|
@ -76,7 +76,7 @@ impl<V: VCardVersion> VCard<V> {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct ContentLine {
|
||||
group: Option<String>,
|
||||
params: Vec<String>,
|
||||
|
@ -85,21 +85,14 @@ pub struct ContentLine {
|
|||
|
||||
impl CardDeserializer {
|
||||
pub fn try_from_str(mut input: &str) -> Result<VCard<impl VCardVersion>> {
|
||||
input = if (!input.starts_with(HEADER_CRLF)
|
||||
|| (!input.ends_with(FOOTER_CRLF) && !input.ends_with(FOOTER)))
|
||||
&& (!input.starts_with(HEADER_LF)
|
||||
|| (!input.ends_with(FOOTER_LF) && !input.ends_with(FOOTER)))
|
||||
input = if (!input.starts_with(HEADER_CRLF) || !input.ends_with(FOOTER_CRLF))
|
||||
&& (!input.starts_with(HEADER_LF) || !input.ends_with(FOOTER_LF))
|
||||
{
|
||||
return Err(Error::new(format!(
|
||||
"Error while parsing vcard: input does not start or end with correct header and \
|
||||
footer: {:?}",
|
||||
footer. input is:\n{:?}",
|
||||
input
|
||||
))
|
||||
.set_kind(ErrorKind::ValueError)
|
||||
.set_details(
|
||||
"vcard file entries are expected to start with a `BEGIN:VCARD` line and end with \
|
||||
a `END:VCARD` line.",
|
||||
));
|
||||
)));
|
||||
} else if input.starts_with(HEADER_CRLF) {
|
||||
&input[HEADER_CRLF.len()..input.len() - FOOTER_CRLF.len()]
|
||||
} else {
|
||||
|
@ -163,15 +156,13 @@ impl CardDeserializer {
|
|||
return Err(Error::new(format!(
|
||||
"Error while parsing vcard: error at line {}, no colon. {:?}",
|
||||
l, el
|
||||
))
|
||||
.set_kind(ErrorKind::ValueError));
|
||||
)));
|
||||
}
|
||||
if name.is_empty() {
|
||||
return Err(Error::new(format!(
|
||||
"Error while parsing vcard: error at line {}, no name for content line. {:?}",
|
||||
l, el
|
||||
))
|
||||
.set_kind(ErrorKind::ValueError));
|
||||
)));
|
||||
}
|
||||
el.value = l[value_start..].replace("\\:", ":");
|
||||
ret.insert(name, el);
|
||||
|
@ -186,6 +177,7 @@ impl<V: VCardVersion> TryInto<Card> for VCard<V> {
|
|||
fn try_into(mut self) -> crate::error::Result<Card> {
|
||||
let mut card = Card::new();
|
||||
card.set_id(CardId::Hash({
|
||||
use std::hash::Hasher;
|
||||
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
||||
if let Some(val) = self.0.get("FN") {
|
||||
hasher.write(val.value.as_bytes());
|
||||
|
@ -201,7 +193,7 @@ impl<V: VCardVersion> TryInto<Card> for VCard<V> {
|
|||
if let Some(val) = self.0.remove("FN") {
|
||||
card.set_name(val.value);
|
||||
} else {
|
||||
return Err(Error::new("FN entry missing in VCard.").set_kind(ErrorKind::ValueError));
|
||||
return Err(Error::new("FN entry missing in VCard."));
|
||||
}
|
||||
if let Some(val) = self.0.remove("NICKNAME") {
|
||||
card.set_additionalname(val.value);
|
||||
|
|
|
@ -20,24 +20,49 @@
|
|||
*/
|
||||
|
||||
pub mod utf7;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
#[cfg(feature = "imap_backend")]
|
||||
pub mod imap;
|
||||
#[cfg(feature = "imap_backend")]
|
||||
pub mod nntp;
|
||||
#[cfg(feature = "notmuch_backend")]
|
||||
pub mod notmuch;
|
||||
#[cfg(feature = "notmuch_backend")]
|
||||
pub use self::notmuch::NotmuchDb;
|
||||
#[cfg(feature = "jmap_backend")]
|
||||
pub mod jmap;
|
||||
#[cfg(feature = "maildir_backend")]
|
||||
pub mod maildir;
|
||||
#[cfg(feature = "mbox_backend")]
|
||||
pub mod mbox;
|
||||
use std::{
|
||||
any::Any,
|
||||
borrow::Cow,
|
||||
collections::{BTreeSet, HashMap},
|
||||
fmt,
|
||||
fmt::Debug,
|
||||
future::Future,
|
||||
ops::Deref,
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
||||
use futures::stream::Stream;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
#[cfg(feature = "imap_backend")]
|
||||
pub use self::imap::ImapType;
|
||||
#[cfg(feature = "maildir_backend")]
|
||||
use self::maildir::MaildirType;
|
||||
#[cfg(feature = "mbox_backend")]
|
||||
use self::mbox::MboxType;
|
||||
#[cfg(feature = "imap_backend")]
|
||||
pub use self::nntp::NntpType;
|
||||
use super::email::{Envelope, EnvelopeHash, Flag};
|
||||
use crate::{
|
||||
conf::AccountSettings,
|
||||
error::{Error, ErrorKind, Result},
|
||||
HeaderName, LogLevel,
|
||||
LogLevel,
|
||||
};
|
||||
|
||||
#[macro_export]
|
||||
|
@ -80,19 +105,19 @@ impl Default for Backends {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "notmuch")]
|
||||
#[cfg(feature = "notmuch_backend")]
|
||||
pub const NOTMUCH_ERROR_MSG: &str = "libnotmuch5 was not found in your system. Make sure it is \
|
||||
installed and in the library paths. For a custom file path, \
|
||||
use `library_file_path` setting in your notmuch account.\n";
|
||||
#[cfg(not(feature = "notmuch"))]
|
||||
#[cfg(not(feature = "notmuch_backend"))]
|
||||
pub const NOTMUCH_ERROR_MSG: &str = "this version of meli is not compiled with notmuch support. \
|
||||
Use an appropriate version and make sure libnotmuch5 is \
|
||||
installed and in the library paths.\n";
|
||||
|
||||
#[cfg(not(feature = "notmuch"))]
|
||||
#[cfg(not(feature = "notmuch_backend"))]
|
||||
pub const NOTMUCH_ERROR_DETAILS: &str = "";
|
||||
|
||||
#[cfg(all(feature = "notmuch", target_os = "unix"))]
|
||||
#[cfg(all(feature = "notmuch_backend", target_os = "unix"))]
|
||||
pub const NOTMUCH_ERROR_DETAILS: &str = r#"If you have installed the library manually, try setting the `LD_LIBRARY_PATH` environment variable to its `lib` directory. Otherwise, set it to the location of libnotmuch.5.so. Example:
|
||||
|
||||
LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/path/to/notmuch/lib" meli
|
||||
|
@ -103,7 +128,7 @@ export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/path/to/notmuch/lib"
|
|||
|
||||
You can also set any location by specifying the library file path with the configuration flag `library_file_path`."#;
|
||||
|
||||
#[cfg(all(feature = "notmuch", target_os = "macos"))]
|
||||
#[cfg(all(feature = "notmuch_backend", target_os = "macos"))]
|
||||
pub const NOTMUCH_ERROR_DETAILS: &str = r#"If you have installed the library via homebrew, try setting the `DYLD_LIBRARY_PATH` environment variable to its `lib` directory. Otherwise, set it to the location of libnotmuch.5.dylib. Example:
|
||||
|
||||
DYLD_LIBRARY_PATH="$(brew --prefix)/lib" meli
|
||||
|
@ -118,7 +143,10 @@ export DYLD_LIBRARY_PATH="$DYLD_LIBRARY_PATH:$(brew --prefix)/lib"
|
|||
|
||||
You can also set any location by specifying the library file path with the configuration flag `library_file_path`."#;
|
||||
|
||||
#[cfg(all(feature = "notmuch", not(any(target_os = "unix", target_os = "macos"))))]
|
||||
#[cfg(all(
|
||||
feature = "notmuch_backend",
|
||||
not(any(target_os = "unix", target_os = "macos"))
|
||||
))]
|
||||
pub const NOTMUCH_ERROR_DETAILS: &str = r#"If notmuch is installed but the library isn't found, consult your system's documentation on how to make dynamic libraries discoverable."#;
|
||||
|
||||
impl Backends {
|
||||
|
@ -126,10 +154,8 @@ impl Backends {
|
|||
let mut b = Self {
|
||||
map: HashMap::with_capacity_and_hasher(1, Default::default()),
|
||||
};
|
||||
#[cfg(feature = "maildir")]
|
||||
#[cfg(feature = "maildir_backend")]
|
||||
{
|
||||
use crate::maildir::MaildirType;
|
||||
|
||||
b.register(
|
||||
"maildir".to_string(),
|
||||
Backend {
|
||||
|
@ -138,10 +164,8 @@ impl Backends {
|
|||
},
|
||||
);
|
||||
}
|
||||
#[cfg(feature = "mbox")]
|
||||
#[cfg(feature = "mbox_backend")]
|
||||
{
|
||||
use crate::mbox::MboxType;
|
||||
|
||||
b.register(
|
||||
"mbox".to_string(),
|
||||
Backend {
|
||||
|
@ -150,34 +174,25 @@ impl Backends {
|
|||
},
|
||||
);
|
||||
}
|
||||
#[cfg(feature = "imap")]
|
||||
#[cfg(feature = "imap_backend")]
|
||||
{
|
||||
use crate::imap::ImapType;
|
||||
|
||||
b.register(
|
||||
"imap".to_string(),
|
||||
Backend {
|
||||
create_fn: Box::new(|| Box::new(|f, i, ev| ImapType::new(f, i, ev))),
|
||||
validate_conf_fn: Box::new(ImapType::validate_config),
|
||||
create_fn: Box::new(|| Box::new(|f, i, ev| imap::ImapType::new(f, i, ev))),
|
||||
validate_conf_fn: Box::new(imap::ImapType::validate_config),
|
||||
},
|
||||
);
|
||||
}
|
||||
#[cfg(feature = "nntp")]
|
||||
{
|
||||
use crate::nntp::NntpType;
|
||||
|
||||
b.register(
|
||||
"nntp".to_string(),
|
||||
Backend {
|
||||
create_fn: Box::new(|| Box::new(|f, i, ev| NntpType::new(f, i, ev))),
|
||||
validate_conf_fn: Box::new(NntpType::validate_config),
|
||||
create_fn: Box::new(|| Box::new(|f, i, ev| nntp::NntpType::new(f, i, ev))),
|
||||
validate_conf_fn: Box::new(nntp::NntpType::validate_config),
|
||||
},
|
||||
);
|
||||
}
|
||||
#[cfg(feature = "notmuch")]
|
||||
#[cfg(feature = "notmuch_backend")]
|
||||
{
|
||||
use crate::notmuch::NotmuchDb;
|
||||
|
||||
b.register(
|
||||
"notmuch".to_string(),
|
||||
Backend {
|
||||
|
@ -186,15 +201,13 @@ impl Backends {
|
|||
},
|
||||
);
|
||||
}
|
||||
#[cfg(feature = "jmap")]
|
||||
#[cfg(feature = "jmap_backend")]
|
||||
{
|
||||
use crate::jmap::JmapType;
|
||||
|
||||
b.register(
|
||||
"jmap".to_string(),
|
||||
Backend {
|
||||
create_fn: Box::new(|| Box::new(|f, i, ev| JmapType::new(f, i, ev))),
|
||||
validate_conf_fn: Box::new(JmapType::validate_config),
|
||||
create_fn: Box::new(|| Box::new(|f, i, ev| jmap::JmapType::new(f, i, ev))),
|
||||
validate_conf_fn: Box::new(jmap::JmapType::validate_config),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -205,7 +218,7 @@ impl Backends {
|
|||
if !self.map.contains_key(key) {
|
||||
if key == "notmuch" {
|
||||
eprint!("{}", NOTMUCH_ERROR_MSG);
|
||||
#[cfg(feature = "notmuch")]
|
||||
#[cfg(feature = "notmuch_backend")]
|
||||
{
|
||||
eprint!("{}", NOTMUCH_ERROR_DETAILS);
|
||||
}
|
||||
|
@ -235,7 +248,7 @@ impl Backends {
|
|||
""
|
||||
},
|
||||
key,
|
||||
if cfg!(feature = "notmuch") && key == "notmuch" {
|
||||
if cfg!(feature = "notmuch_backend") && key == "notmuch" {
|
||||
NOTMUCH_ERROR_DETAILS
|
||||
} else {
|
||||
""
|
||||
|
@ -246,7 +259,7 @@ impl Backends {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum BackendEvent {
|
||||
Notice {
|
||||
description: String,
|
||||
|
@ -270,7 +283,7 @@ impl From<Error> for BackendEvent {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum RefreshEventKind {
|
||||
Update(EnvelopeHash, Box<Envelope>),
|
||||
/// Rename(old_hash, new_hash)
|
||||
|
@ -290,7 +303,7 @@ pub enum RefreshEventKind {
|
|||
MailboxUnsubscribe(MailboxHash),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RefreshEvent {
|
||||
pub mailbox_hash: MailboxHash,
|
||||
pub account_hash: AccountHash,
|
||||
|
@ -306,8 +319,8 @@ impl BackendEventConsumer {
|
|||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for BackendEventConsumer {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
impl fmt::Debug for BackendEventConsumer {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "BackendEventConsumer")
|
||||
}
|
||||
}
|
||||
|
@ -320,38 +333,7 @@ impl Deref for BackendEventConsumer {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum FlagOp {
|
||||
Set(Flag),
|
||||
SetTag(String),
|
||||
UnSet(Flag),
|
||||
UnSetTag(String),
|
||||
}
|
||||
|
||||
impl From<&FlagOp> for bool {
|
||||
fn from(val: &FlagOp) -> Self {
|
||||
matches!(val, FlagOp::Set(_) | FlagOp::SetTag(_))
|
||||
}
|
||||
}
|
||||
|
||||
impl FlagOp {
|
||||
#[inline]
|
||||
pub fn is_flag(&self) -> bool {
|
||||
matches!(self, Self::Set(_) | Self::UnSet(_))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_tag(&self) -> bool {
|
||||
matches!(self, Self::SetTag(_) | Self::UnSetTag(_))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn as_bool(&self) -> bool {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MailBackendCapabilities {
|
||||
pub is_async: bool,
|
||||
pub is_remote: bool,
|
||||
|
@ -359,10 +341,9 @@ pub struct MailBackendCapabilities {
|
|||
pub supports_search: bool,
|
||||
pub supports_tags: bool,
|
||||
pub supports_submission: bool,
|
||||
pub extra_submission_headers: &'static [HeaderName],
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum MailBackendExtensionStatus {
|
||||
Unsupported { comment: Option<&'static str> },
|
||||
Supported { comment: Option<&'static str> },
|
||||
|
@ -407,7 +388,7 @@ pub trait MailBackend: ::std::fmt::Debug + Send + Sync {
|
|||
&mut self,
|
||||
env_hashes: EnvelopeHashBatch,
|
||||
mailbox_hash: MailboxHash,
|
||||
flags: SmallVec<[FlagOp; 8]>,
|
||||
flags: SmallVec<[(std::result::Result<Flag, String>, bool); 8]>,
|
||||
) -> ResultFuture<()>;
|
||||
|
||||
fn delete_messages(
|
||||
|
@ -494,15 +475,19 @@ pub trait MailBackend: ::std::fmt::Debug + Send + Sync {
|
|||
/// fn as_bytes(&mut self) -> Result<&[u8]> {
|
||||
/// unimplemented!()
|
||||
/// }
|
||||
/// fn fetch_flags(&self) -> Result<Flag> {
|
||||
/// unimplemented!()
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// let operation = Box::new(FooOp {});
|
||||
/// ```
|
||||
pub trait BackendOp: ::std::fmt::Debug + ::std::marker::Send {
|
||||
fn as_bytes(&mut self) -> ResultFuture<Vec<u8>>;
|
||||
fn fetch_flags(&self) -> ResultFuture<Flag>;
|
||||
}
|
||||
|
||||
/// Wrapper for [`BackendOp`] that are to be set read-only.
|
||||
/// Wrapper for BackendOps that are to be set read-only.
|
||||
///
|
||||
/// Warning: Backend implementations may still cause side-effects (for example
|
||||
/// IMAP can set the Seen flag when fetching an envelope)
|
||||
|
@ -522,9 +507,12 @@ impl BackendOp for ReadOnlyOp {
|
|||
fn as_bytes(&mut self) -> ResultFuture<Vec<u8>> {
|
||||
self.op.as_bytes()
|
||||
}
|
||||
fn fetch_flags(&self) -> ResultFuture<Flag> {
|
||||
self.op.fetch_flags()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
||||
#[derive(Default, Debug, Copy, Hash, Eq, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum SpecialUsageMailbox {
|
||||
#[default]
|
||||
Normal,
|
||||
|
@ -577,7 +565,7 @@ impl SpecialUsageMailbox {
|
|||
}
|
||||
}
|
||||
|
||||
pub trait BackendMailbox: std::fmt::Debug {
|
||||
pub trait BackendMailbox: Debug {
|
||||
fn hash(&self) -> MailboxHash;
|
||||
/// Final component of `path`.
|
||||
fn name(&self) -> &str;
|
||||
|
@ -606,7 +594,7 @@ impl Clone for Mailbox {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
|
||||
pub struct MailboxPermissions {
|
||||
pub create_messages: bool,
|
||||
pub remove_messages: bool,
|
||||
|
@ -639,7 +627,7 @@ impl std::fmt::Display for MailboxPermissions {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct EnvelopeHashBatch {
|
||||
pub first: EnvelopeHash,
|
||||
pub rest: SmallVec<[EnvelopeHash; 64]>,
|
||||
|
@ -692,14 +680,14 @@ impl EnvelopeHashBatch {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
#[derive(Default, Clone)]
|
||||
pub struct LazyCountSet {
|
||||
pub not_yet_seen: usize,
|
||||
pub set: BTreeSet<EnvelopeHash>,
|
||||
not_yet_seen: usize,
|
||||
set: BTreeSet<EnvelopeHash>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for LazyCountSet {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
impl fmt::Debug for LazyCountSet {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.debug_struct("LazyCountSet")
|
||||
.field("not_yet_seen", &self.not_yet_seen)
|
||||
.field("set", &self.set.len())
|
||||
|
@ -709,10 +697,6 @@ impl std::fmt::Debug for LazyCountSet {
|
|||
}
|
||||
|
||||
impl LazyCountSet {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn set_not_yet_seen(&mut self, new_val: usize) {
|
||||
self.not_yet_seen = new_val;
|
||||
}
|
||||
|
@ -731,7 +715,7 @@ impl LazyCountSet {
|
|||
|
||||
pub fn insert_existing_set(&mut self, set: BTreeSet<EnvelopeHash>) {
|
||||
let old_len = self.set.len();
|
||||
self.set.extend(set);
|
||||
self.set.extend(set.into_iter());
|
||||
self.not_yet_seen = self.not_yet_seen.saturating_sub(self.set.len() - old_len);
|
||||
}
|
||||
|
||||
|
@ -755,20 +739,29 @@ impl LazyCountSet {
|
|||
}
|
||||
|
||||
pub fn insert_set(&mut self, set: BTreeSet<EnvelopeHash>) {
|
||||
self.set.extend(set);
|
||||
self.set.extend(set.into_iter());
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, env_hash: EnvelopeHash) -> bool {
|
||||
self.set.remove(&env_hash)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn contains(&self, value: &EnvelopeHash) -> bool {
|
||||
self.set.contains(value)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct IsSubscribedFn(pub Box<dyn Fn(&str) -> bool + Send + Sync>);
|
||||
#[test]
|
||||
fn test_lazy_count_set() {
|
||||
let mut new = LazyCountSet::default();
|
||||
assert_eq!(new.len(), 0);
|
||||
new.set_not_yet_seen(10);
|
||||
assert_eq!(new.len(), 10);
|
||||
for i in 0..10 {
|
||||
assert!(new.insert_existing(EnvelopeHash(i)));
|
||||
}
|
||||
assert_eq!(new.len(), 10);
|
||||
assert!(!new.insert_existing(EnvelopeHash(10)));
|
||||
assert_eq!(new.len(), 10);
|
||||
}
|
||||
|
||||
pub struct IsSubscribedFn(Box<dyn Fn(&str) -> bool + Send + Sync>);
|
||||
|
||||
impl std::fmt::Debug for IsSubscribedFn {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
|
@ -782,22 +775,3 @@ impl std::ops::Deref for IsSubscribedFn {
|
|||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_lazy_count_set() {
|
||||
let mut new = LazyCountSet::default();
|
||||
assert_eq!(new.len(), 0);
|
||||
new.set_not_yet_seen(10);
|
||||
assert_eq!(new.len(), 10);
|
||||
for i in 0..10 {
|
||||
assert!(new.insert_existing(EnvelopeHash(i)));
|
||||
}
|
||||
assert_eq!(new.len(), 10);
|
||||
assert!(!new.insert_existing(EnvelopeHash(10)));
|
||||
assert_eq!(new.len(), 10);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,9 +19,6 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// In case we forget to wait some future.
|
||||
#![deny(unused_must_use)]
|
||||
|
||||
use smallvec::SmallVec;
|
||||
#[macro_use]
|
||||
mod protocol_parser;
|
||||
|
@ -37,10 +34,11 @@ mod watch;
|
|||
pub use watch::*;
|
||||
mod search;
|
||||
pub use search::*;
|
||||
pub mod cache;
|
||||
mod cache;
|
||||
use cache::{ImapCacheReset, ModSequence};
|
||||
pub mod error;
|
||||
pub mod managesieve;
|
||||
pub mod untagged;
|
||||
mod untagged;
|
||||
|
||||
use std::{
|
||||
collections::{hash_map::DefaultHasher, BTreeSet, HashMap, HashSet},
|
||||
|
@ -52,9 +50,8 @@ use std::{
|
|||
time::{Duration, SystemTime},
|
||||
};
|
||||
|
||||
pub use cache::ModSequence;
|
||||
use futures::{lock::Mutex as FutureMutex, stream::Stream};
|
||||
use imap_codec::imap_types::{
|
||||
use imap_codec::{
|
||||
command::CommandBody,
|
||||
core::Literal,
|
||||
flag::{Flag as ImapCodecFlag, StoreResponse, StoreType},
|
||||
|
@ -80,6 +77,7 @@ pub type MessageSequenceNumber = ImapNum;
|
|||
|
||||
pub static SUPPORTED_CAPABILITIES: &[&str] = &[
|
||||
"AUTH=OAUTH2",
|
||||
#[cfg(feature = "deflate_compression")]
|
||||
"COMPRESS=DEFLATE",
|
||||
"CONDSTORE",
|
||||
"ENABLE",
|
||||
|
@ -101,7 +99,7 @@ pub struct EnvelopeCache {
|
|||
flags: Option<Flag>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ImapServerConf {
|
||||
pub server_hostname: String,
|
||||
pub server_username: String,
|
||||
|
@ -147,31 +145,31 @@ macro_rules! get_conf_val {
|
|||
|
||||
#[derive(Debug)]
|
||||
pub struct UIDStore {
|
||||
pub account_hash: AccountHash,
|
||||
pub account_name: Arc<str>,
|
||||
pub keep_offline_cache: Arc<Mutex<bool>>,
|
||||
pub capabilities: Arc<Mutex<Capabilities>>,
|
||||
pub hash_index: Arc<Mutex<HashMap<EnvelopeHash, (UID, MailboxHash)>>>,
|
||||
pub uid_index: Arc<Mutex<HashMap<(MailboxHash, UID), EnvelopeHash>>>,
|
||||
pub msn_index: Arc<Mutex<HashMap<MailboxHash, Vec<UID>>>>,
|
||||
account_hash: AccountHash,
|
||||
account_name: Arc<str>,
|
||||
keep_offline_cache: bool,
|
||||
capabilities: Arc<Mutex<Capabilities>>,
|
||||
hash_index: Arc<Mutex<HashMap<EnvelopeHash, (UID, MailboxHash)>>>,
|
||||
uid_index: Arc<Mutex<HashMap<(MailboxHash, UID), EnvelopeHash>>>,
|
||||
msn_index: Arc<Mutex<HashMap<MailboxHash, Vec<UID>>>>,
|
||||
|
||||
pub byte_cache: Arc<Mutex<HashMap<UID, EnvelopeCache>>>,
|
||||
pub collection: Collection,
|
||||
byte_cache: Arc<Mutex<HashMap<UID, EnvelopeCache>>>,
|
||||
collection: Collection,
|
||||
|
||||
// Offline caching
|
||||
pub uidvalidity: Arc<Mutex<HashMap<MailboxHash, UID>>>,
|
||||
pub envelopes: Arc<Mutex<HashMap<EnvelopeHash, cache::CachedEnvelope>>>,
|
||||
pub max_uids: Arc<Mutex<HashMap<MailboxHash, UID>>>,
|
||||
pub modseq: Arc<Mutex<HashMap<EnvelopeHash, ModSequence>>>,
|
||||
pub highestmodseqs: Arc<Mutex<HashMap<MailboxHash, std::result::Result<ModSequence, ()>>>>,
|
||||
pub mailboxes: Arc<FutureMutex<HashMap<MailboxHash, ImapMailbox>>>,
|
||||
pub is_online: Arc<Mutex<(SystemTime, Result<()>)>>,
|
||||
pub event_consumer: BackendEventConsumer,
|
||||
pub timeout: Option<Duration>,
|
||||
/* Offline caching */
|
||||
uidvalidity: Arc<Mutex<HashMap<MailboxHash, UID>>>,
|
||||
envelopes: Arc<Mutex<HashMap<EnvelopeHash, cache::CachedEnvelope>>>,
|
||||
max_uids: Arc<Mutex<HashMap<MailboxHash, UID>>>,
|
||||
modseq: Arc<Mutex<HashMap<EnvelopeHash, ModSequence>>>,
|
||||
highestmodseqs: Arc<Mutex<HashMap<MailboxHash, std::result::Result<ModSequence, ()>>>>,
|
||||
mailboxes: Arc<FutureMutex<HashMap<MailboxHash, ImapMailbox>>>,
|
||||
is_online: Arc<Mutex<(SystemTime, Result<()>)>>,
|
||||
event_consumer: BackendEventConsumer,
|
||||
timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
impl UIDStore {
|
||||
pub fn new(
|
||||
fn new(
|
||||
account_hash: AccountHash,
|
||||
account_name: Arc<str>,
|
||||
event_consumer: BackendEventConsumer,
|
||||
|
@ -180,7 +178,7 @@ impl UIDStore {
|
|||
Self {
|
||||
account_hash,
|
||||
account_name,
|
||||
keep_offline_cache: Arc::new(Mutex::new(false)),
|
||||
keep_offline_cache: false,
|
||||
capabilities: Default::default(),
|
||||
uidvalidity: Default::default(),
|
||||
envelopes: Default::default(),
|
||||
|
@ -201,38 +199,14 @@ impl UIDStore {
|
|||
timeout,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cache_handle(self: &Arc<Self>) -> Result<Option<Box<dyn cache::ImapCache>>> {
|
||||
if !*self.keep_offline_cache.lock().unwrap() {
|
||||
return Ok(None);
|
||||
}
|
||||
#[cfg(not(feature = "sqlite3"))]
|
||||
return Ok(None);
|
||||
#[cfg(feature = "sqlite3")]
|
||||
return Ok(Some(cache::sqlite3_cache::Sqlite3Cache::get(Arc::clone(
|
||||
self,
|
||||
))?));
|
||||
}
|
||||
|
||||
pub fn reset_db(self: &Arc<Self>) -> Result<()> {
|
||||
if !*self.keep_offline_cache.lock().unwrap() {
|
||||
return Ok(());
|
||||
}
|
||||
#[cfg(not(feature = "sqlite3"))]
|
||||
return Ok(());
|
||||
#[cfg(feature = "sqlite3")]
|
||||
use crate::imap::cache::ImapCacheReset;
|
||||
#[cfg(feature = "sqlite3")]
|
||||
return cache::sqlite3_cache::Sqlite3Cache::reset_db(self);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ImapType {
|
||||
pub _is_subscribed: Arc<IsSubscribedFn>,
|
||||
pub connection: Arc<FutureMutex<ImapConnection>>,
|
||||
pub server_conf: ImapServerConf,
|
||||
pub uid_store: Arc<UIDStore>,
|
||||
_is_subscribed: Arc<IsSubscribedFn>,
|
||||
connection: Arc<FutureMutex<ImapConnection>>,
|
||||
server_conf: ImapServerConf,
|
||||
uid_store: Arc<UIDStore>,
|
||||
}
|
||||
|
||||
impl MailBackend for ImapType {
|
||||
|
@ -254,6 +228,7 @@ impl MailBackend for ImapType {
|
|||
extension_use:
|
||||
ImapExtensionUse {
|
||||
idle,
|
||||
#[cfg(feature = "deflate_compression")]
|
||||
deflate,
|
||||
condstore,
|
||||
oauth2,
|
||||
|
@ -272,11 +247,20 @@ impl MailBackend for ImapType {
|
|||
}
|
||||
}
|
||||
"COMPRESS=DEFLATE" => {
|
||||
if deflate {
|
||||
*status = MailBackendExtensionStatus::Enabled { comment: None };
|
||||
} else {
|
||||
*status = MailBackendExtensionStatus::Supported {
|
||||
comment: Some("Disabled by user configuration"),
|
||||
#[cfg(feature = "deflate_compression")]
|
||||
{
|
||||
if deflate {
|
||||
*status = MailBackendExtensionStatus::Enabled { comment: None };
|
||||
} else {
|
||||
*status = MailBackendExtensionStatus::Supported {
|
||||
comment: Some("Disabled by user configuration"),
|
||||
};
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "deflate_compression"))]
|
||||
{
|
||||
*status = MailBackendExtensionStatus::Unsupported {
|
||||
comment: Some("melib not compiled with DEFLATE."),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -317,7 +301,6 @@ impl MailBackend for ImapType {
|
|||
extensions: Some(extensions),
|
||||
supports_tags: true,
|
||||
supports_submission: false,
|
||||
extra_submission_headers: &[],
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -326,35 +309,40 @@ impl MailBackend for ImapType {
|
|||
mailbox_hash: MailboxHash,
|
||||
) -> Result<Pin<Box<dyn Stream<Item = Result<Vec<Envelope>>> + Send + 'static>>> {
|
||||
let cache_handle = {
|
||||
match self.uid_store.cache_handle().chain_err_summary(|| {
|
||||
format!(
|
||||
"Could not initialize cache for IMAP account {}. Resetting database.",
|
||||
self.uid_store.account_name
|
||||
)
|
||||
}) {
|
||||
Ok(Some(v)) => Some(v),
|
||||
Ok(None) => None,
|
||||
Err(err) => {
|
||||
(self.uid_store.event_consumer)(self.uid_store.account_hash, err.into());
|
||||
match self
|
||||
.uid_store
|
||||
.reset_db()
|
||||
.and_then(|()| self.uid_store.cache_handle())
|
||||
.chain_err_summary(|| "Could not reset IMAP cache database.")
|
||||
{
|
||||
Ok(Some(v)) => Some(v),
|
||||
Ok(None) => None,
|
||||
Err(err) => {
|
||||
*self.uid_store.keep_offline_cache.lock().unwrap() = false;
|
||||
log::trace!("{}: cache error: {}", self.uid_store.account_name, err);
|
||||
None
|
||||
#[cfg(feature = "sqlite3")]
|
||||
if self.uid_store.keep_offline_cache {
|
||||
match cache::Sqlite3Cache::get(self.uid_store.clone()).chain_err_summary(|| {
|
||||
format!(
|
||||
"Could not initialize cache for IMAP account {}. Resetting database.",
|
||||
self.uid_store.account_name
|
||||
)
|
||||
}) {
|
||||
Ok(v) => Some(v),
|
||||
Err(err) => {
|
||||
(self.uid_store.event_consumer)(self.uid_store.account_hash, err.into());
|
||||
match cache::Sqlite3Cache::reset_db(&self.uid_store)
|
||||
.and_then(|()| cache::Sqlite3Cache::get(self.uid_store.clone()))
|
||||
.chain_err_summary(|| "Could not reset IMAP cache database.")
|
||||
{
|
||||
Ok(v) => Some(v),
|
||||
Err(err) => {
|
||||
(self.uid_store.event_consumer)(
|
||||
self.uid_store.account_hash,
|
||||
err.into(),
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
#[cfg(not(feature = "sqlite3"))]
|
||||
None
|
||||
};
|
||||
let mut state = FetchState {
|
||||
stage: if *self.uid_store.keep_offline_cache.lock().unwrap() && cache_handle.is_some() {
|
||||
stage: if self.uid_store.keep_offline_cache && cache_handle.is_some() {
|
||||
FetchStage::InitialCache
|
||||
} else {
|
||||
FetchStage::InitialFresh
|
||||
|
@ -381,6 +369,7 @@ impl MailBackend for ImapType {
|
|||
}
|
||||
};
|
||||
Ok(Box::pin(async_stream::try_stream! {
|
||||
#[cfg(debug_assertions)]
|
||||
let id = state.connection.lock().await.id.clone();
|
||||
{
|
||||
let f = &state.uid_store.mailboxes.lock().await[&mailbox_hash];
|
||||
|
@ -392,7 +381,8 @@ impl MailBackend for ImapType {
|
|||
};
|
||||
loop {
|
||||
let res = fetch_hlpr(&mut state).await.map_err(|err| {
|
||||
log::trace!("{} fetch_hlpr at stage {:?} err {:?}", id, state.stage, &err);
|
||||
#[cfg(debug_assertions)]
|
||||
log::trace!("{} fetch_hlpr err {:?}", id, &err);
|
||||
err
|
||||
})?;
|
||||
yield res;
|
||||
|
@ -411,7 +401,7 @@ impl MailBackend for ImapType {
|
|||
let inbox = timeout(uid_store.timeout, uid_store.mailboxes.lock())
|
||||
.await?
|
||||
.get(&mailbox_hash)
|
||||
.cloned()
|
||||
.map(std::clone::Clone::clone)
|
||||
.unwrap();
|
||||
let mut conn = timeout(uid_store.timeout, main_conn.lock()).await?;
|
||||
watch::examine_updates(inbox, &mut conn, &uid_store).await?;
|
||||
|
@ -474,7 +464,7 @@ impl MailBackend for ImapType {
|
|||
Ok(Box::pin(async move {
|
||||
match timeout(timeout_dur, connection.lock()).await {
|
||||
Ok(mut conn) => {
|
||||
imap_log!(trace, conn, "is_online");
|
||||
imap_trace!(conn, "is_online");
|
||||
match timeout(timeout_dur, conn.connect()).await {
|
||||
Ok(Ok(())) => Ok(()),
|
||||
Err(err) | Ok(Err(err)) => {
|
||||
|
@ -510,6 +500,7 @@ impl MailBackend for ImapType {
|
|||
idle(ImapWatchKit {
|
||||
conn: ImapConnection::new_connection(
|
||||
&server_conf,
|
||||
#[cfg(debug_assertions)]
|
||||
"watch()::idle".into(),
|
||||
uid_store.clone(),
|
||||
),
|
||||
|
@ -521,6 +512,7 @@ impl MailBackend for ImapType {
|
|||
poll_with_examine(ImapWatchKit {
|
||||
conn: ImapConnection::new_connection(
|
||||
&server_conf,
|
||||
#[cfg(debug_assertions)]
|
||||
"watch()::poll_with_examine".into(),
|
||||
uid_store.clone(),
|
||||
),
|
||||
|
@ -553,7 +545,7 @@ impl MailBackend for ImapType {
|
|||
}
|
||||
Ok(()) => {
|
||||
log::trace!(
|
||||
"{} Watch reconnect attempt successful",
|
||||
"{} Watch reconnect attempt succesful",
|
||||
uid_store.account_name
|
||||
);
|
||||
continue;
|
||||
|
@ -715,7 +707,7 @@ impl MailBackend for ImapType {
|
|||
&mut self,
|
||||
env_hashes: EnvelopeHashBatch,
|
||||
mailbox_hash: MailboxHash,
|
||||
flags: SmallVec<[FlagOp; 8]>,
|
||||
flags: SmallVec<[(std::result::Result<Flag, String>, bool); 8]>,
|
||||
) -> ResultFuture<()> {
|
||||
let connection = self.connection.clone();
|
||||
let uid_store = self.uid_store.clone();
|
||||
|
@ -738,7 +730,7 @@ impl MailBackend for ImapType {
|
|||
let mut conn = connection.lock().await;
|
||||
conn.select_mailbox(mailbox_hash, &mut response, false)
|
||||
.await?;
|
||||
if flags.iter().any(<bool>::from) {
|
||||
if flags.iter().any(|(_, b)| *b) {
|
||||
/* Set flags/tags to true */
|
||||
let mut set_seen = false;
|
||||
let command = {
|
||||
|
@ -748,25 +740,28 @@ impl MailBackend for ImapType {
|
|||
cmd = format!("{},{}", cmd, uid);
|
||||
}
|
||||
cmd = format!("{} +FLAGS (", cmd);
|
||||
for op in flags.iter().filter(|op| <bool>::from(*op)) {
|
||||
match op {
|
||||
FlagOp::Set(flag) if *flag == Flag::REPLIED => {
|
||||
for (f, v) in flags.iter() {
|
||||
if !*v {
|
||||
continue;
|
||||
}
|
||||
match f {
|
||||
Ok(flag) if *flag == Flag::REPLIED => {
|
||||
cmd.push_str("\\Answered ");
|
||||
}
|
||||
FlagOp::Set(flag) if *flag == Flag::FLAGGED => {
|
||||
Ok(flag) if *flag == Flag::FLAGGED => {
|
||||
cmd.push_str("\\Flagged ");
|
||||
}
|
||||
FlagOp::Set(flag) if *flag == Flag::TRASHED => {
|
||||
Ok(flag) if *flag == Flag::TRASHED => {
|
||||
cmd.push_str("\\Deleted ");
|
||||
}
|
||||
FlagOp::Set(flag) if *flag == Flag::SEEN => {
|
||||
Ok(flag) if *flag == Flag::SEEN => {
|
||||
cmd.push_str("\\Seen ");
|
||||
set_seen = true;
|
||||
}
|
||||
FlagOp::Set(flag) if *flag == Flag::DRAFT => {
|
||||
Ok(flag) if *flag == Flag::DRAFT => {
|
||||
cmd.push_str("\\Draft ");
|
||||
}
|
||||
FlagOp::Set(_) => {
|
||||
Ok(_) => {
|
||||
log::error!(
|
||||
"Application error: more than one flag bit set in set_flags: \
|
||||
{:?}",
|
||||
|
@ -779,13 +774,12 @@ impl MailBackend for ImapType {
|
|||
))
|
||||
.set_kind(crate::ErrorKind::Bug));
|
||||
}
|
||||
FlagOp::SetTag(tag) => {
|
||||
Err(tag) => {
|
||||
let hash = TagHash::from_bytes(tag.as_bytes());
|
||||
tag_lck.entry(hash).or_insert_with(|| tag.to_string());
|
||||
cmd.push_str(tag);
|
||||
cmd.push(' ');
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
// pop last space
|
||||
|
@ -806,7 +800,7 @@ impl MailBackend for ImapType {
|
|||
}
|
||||
}
|
||||
}
|
||||
if flags.iter().any(|b| !<bool>::from(b)) {
|
||||
if flags.iter().any(|(_, b)| !*b) {
|
||||
let mut set_unseen = false;
|
||||
/* Set flags/tags to false */
|
||||
let command = {
|
||||
|
@ -815,25 +809,28 @@ impl MailBackend for ImapType {
|
|||
cmd = format!("{},{}", cmd, uid);
|
||||
}
|
||||
cmd = format!("{} -FLAGS (", cmd);
|
||||
for op in flags.iter().filter(|op| !<bool>::from(*op)) {
|
||||
match op {
|
||||
FlagOp::UnSet(flag) if *flag == Flag::REPLIED => {
|
||||
for (f, v) in flags.iter() {
|
||||
if *v {
|
||||
continue;
|
||||
}
|
||||
match f {
|
||||
Ok(flag) if *flag == Flag::REPLIED => {
|
||||
cmd.push_str("\\Answered ");
|
||||
}
|
||||
FlagOp::UnSet(flag) if *flag == Flag::FLAGGED => {
|
||||
Ok(flag) if *flag == Flag::FLAGGED => {
|
||||
cmd.push_str("\\Flagged ");
|
||||
}
|
||||
FlagOp::UnSet(flag) if *flag == Flag::TRASHED => {
|
||||
Ok(flag) if *flag == Flag::TRASHED => {
|
||||
cmd.push_str("\\Deleted ");
|
||||
}
|
||||
FlagOp::UnSet(flag) if *flag == Flag::SEEN => {
|
||||
Ok(flag) if *flag == Flag::SEEN => {
|
||||
cmd.push_str("\\Seen ");
|
||||
set_unseen = true;
|
||||
}
|
||||
FlagOp::UnSet(flag) if *flag == Flag::DRAFT => {
|
||||
Ok(flag) if *flag == Flag::DRAFT => {
|
||||
cmd.push_str("\\Draft ");
|
||||
}
|
||||
FlagOp::UnSet(_) => {
|
||||
Ok(_) => {
|
||||
log::error!(
|
||||
"Application error: more than one flag bit set in set_flags: \
|
||||
{:?}",
|
||||
|
@ -845,14 +842,12 @@ impl MailBackend for ImapType {
|
|||
flags
|
||||
)));
|
||||
}
|
||||
FlagOp::UnSetTag(tag) => {
|
||||
Err(tag) => {
|
||||
cmd.push_str(tag);
|
||||
cmd.push(' ');
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
// [ref:TODO] there should be a check that cmd is not empty here.
|
||||
// pop last space
|
||||
cmd.pop();
|
||||
cmd.push(')');
|
||||
|
@ -863,18 +858,14 @@ impl MailBackend for ImapType {
|
|||
.await?;
|
||||
if set_unseen {
|
||||
for f in uid_store.mailboxes.lock().await.values() {
|
||||
if let (Ok(mut unseen), Ok(exists)) = (f.unseen.lock(), f.exists.lock()) {
|
||||
for env_hash in env_hashes.iter().filter(|h| exists.contains(h)) {
|
||||
if let Ok(mut unseen) = f.unseen.lock() {
|
||||
for env_hash in env_hashes.iter() {
|
||||
unseen.insert_new(env_hash);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(mut cache_handle) = uid_store.cache_handle()? {
|
||||
let res = cache_handle.update_flags(env_hashes, mailbox_hash, flags);
|
||||
log::trace!("update_flags in cache: {:?}", res);
|
||||
}
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
|
@ -887,7 +878,7 @@ impl MailBackend for ImapType {
|
|||
let flag_future = self.set_flags(
|
||||
env_hashes,
|
||||
mailbox_hash,
|
||||
smallvec::smallvec![FlagOp::Set(Flag::TRASHED)],
|
||||
smallvec::smallvec![(Ok(Flag::TRASHED), true)],
|
||||
)?;
|
||||
let connection = self.connection.clone();
|
||||
Ok(Box::pin(async move {
|
||||
|
@ -897,8 +888,7 @@ impl MailBackend for ImapType {
|
|||
conn.send_command(CommandBody::Expunge).await?;
|
||||
conn.read_response(&mut response, RequiredResponses::empty())
|
||||
.await?;
|
||||
imap_log!(
|
||||
trace,
|
||||
imap_trace!(
|
||||
conn,
|
||||
"EXPUNGE response: {}",
|
||||
&String::from_utf8_lossy(&response)
|
||||
|
@ -907,11 +897,11 @@ impl MailBackend for ImapType {
|
|||
}))
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -929,11 +919,11 @@ impl MailBackend for ImapType {
|
|||
Ok(Box::pin(async move {
|
||||
/* Must transform path to something the IMAP server will accept
|
||||
*
|
||||
* Each root mailbox has a hierarchy delimiter reported by the LIST entry.
|
||||
* All paths must use this delimiter to indicate children of this
|
||||
* Each root mailbox has a hierarchy delimeter reported by the LIST entry.
|
||||
* All paths must use this delimeter to indicate children of this
|
||||
* mailbox.
|
||||
*
|
||||
* A new root mailbox should have the default delimiter, which can be found
|
||||
* A new root mailbox should have the default delimeter, which can be found
|
||||
* out by issuing an empty LIST command as described in RFC3501:
|
||||
* C: A101 LIST "" ""
|
||||
* S: * LIST (\Noselect) "/" ""
|
||||
|
@ -966,9 +956,8 @@ impl MailBackend for ImapType {
|
|||
}
|
||||
}
|
||||
|
||||
/* [ref:FIXME] Do not try to CREATE a sub-mailbox in a
|
||||
* mailbox that has the \Noinferiors flag
|
||||
* set. */
|
||||
/* FIXME Do not try to CREATE a sub-mailbox in a mailbox
|
||||
* that has the \Noinferiors flag set. */
|
||||
}
|
||||
|
||||
let mut response = Vec::with_capacity(8 * 1024);
|
||||
|
@ -991,13 +980,13 @@ impl MailBackend for ImapType {
|
|||
}
|
||||
let ret: Result<()> = ImapResponse::try_from(response.as_slice())?.into();
|
||||
ret?;
|
||||
let new_hash = MailboxHash::from_bytes(path.as_bytes());
|
||||
let new_hash = MailboxHash::from_bytes(path.as_str().as_bytes());
|
||||
uid_store.mailboxes.lock().await.clear();
|
||||
Ok((
|
||||
new_hash,
|
||||
new_mailbox_fut?.await.map_err(|err| {
|
||||
Error::new(format!(
|
||||
"Mailbox create was successful (returned `{}`) but listing mailboxes \
|
||||
"Mailbox create was succesful (returned `{}`) but listing mailboxes \
|
||||
afterwards returned `{}`",
|
||||
String::from_utf8_lossy(&response),
|
||||
err
|
||||
|
@ -1058,7 +1047,7 @@ impl MailBackend for ImapType {
|
|||
uid_store.mailboxes.lock().await.clear();
|
||||
new_mailbox_fut?.await.map_err(|err| {
|
||||
format!(
|
||||
"Mailbox delete was successful (returned `{}`) but listing mailboxes \
|
||||
"Mailbox delete was succesful (returned `{}`) but listing mailboxes \
|
||||
afterwards returned `{}`",
|
||||
String::from_utf8_lossy(&response),
|
||||
err
|
||||
|
@ -1157,13 +1146,13 @@ impl MailBackend for ImapType {
|
|||
.read_response(&mut response, RequiredResponses::empty())
|
||||
.await?;
|
||||
}
|
||||
let new_hash = MailboxHash::from_bytes(new_path.as_bytes());
|
||||
let new_hash = MailboxHash::from_bytes(new_path.as_str().as_bytes());
|
||||
let ret: Result<()> = ImapResponse::try_from(response.as_slice())?.into();
|
||||
ret?;
|
||||
uid_store.mailboxes.lock().await.clear();
|
||||
new_mailbox_fut?.await.map_err(|err| {
|
||||
format!(
|
||||
"Mailbox rename was successful (returned `{}`) but listing mailboxes \
|
||||
"Mailbox rename was succesful (returned `{}`) but listing mailboxes \
|
||||
afterwards returned `{}`",
|
||||
String::from_utf8_lossy(&response),
|
||||
err
|
||||
|
@ -1224,8 +1213,7 @@ impl MailBackend for ImapType {
|
|||
.await?;
|
||||
conn.read_response(&mut response, RequiredResponses::SEARCH)
|
||||
.await?;
|
||||
imap_log!(
|
||||
trace,
|
||||
imap_trace!(
|
||||
conn,
|
||||
"searching for {} returned: {}",
|
||||
query_str,
|
||||
|
@ -1234,6 +1222,7 @@ impl MailBackend for ImapType {
|
|||
|
||||
for l in response.split_rn() {
|
||||
if l.starts_with(b"* SEARCH") {
|
||||
use std::iter::FromIterator;
|
||||
let uid_index = uid_store.uid_index.lock()?;
|
||||
return Ok(SmallVec::from_iter(
|
||||
String::from_utf8_lossy(l[b"* SEARCH".len()..].trim())
|
||||
|
@ -1304,6 +1293,7 @@ impl ImapType {
|
|||
extension_use: ImapExtensionUse {
|
||||
idle: get_conf_val!(s["use_idle"], true)?,
|
||||
condstore: get_conf_val!(s["use_condstore"], true)?,
|
||||
#[cfg(feature = "deflate_compression")]
|
||||
deflate: get_conf_val!(s["use_deflate"], true)?,
|
||||
oauth2: use_oauth2,
|
||||
},
|
||||
|
@ -1313,7 +1303,7 @@ impl ImapType {
|
|||
let account_hash = AccountHash::from_bytes(s.name.as_bytes());
|
||||
let account_name = s.name.to_string().into();
|
||||
let uid_store: Arc<UIDStore> = Arc::new(UIDStore {
|
||||
keep_offline_cache: Arc::new(Mutex::new(keep_offline_cache)),
|
||||
keep_offline_cache,
|
||||
..UIDStore::new(
|
||||
account_hash,
|
||||
account_name,
|
||||
|
@ -1321,8 +1311,12 @@ impl ImapType {
|
|||
server_conf.timeout,
|
||||
)
|
||||
});
|
||||
let connection =
|
||||
ImapConnection::new_connection(&server_conf, "ImapType::new".into(), uid_store.clone());
|
||||
let connection = ImapConnection::new_connection(
|
||||
&server_conf,
|
||||
#[cfg(debug_assertions)]
|
||||
"ImapType::new".into(),
|
||||
uid_store.clone(),
|
||||
);
|
||||
|
||||
Ok(Box::new(Self {
|
||||
server_conf,
|
||||
|
@ -1335,6 +1329,7 @@ impl ImapType {
|
|||
pub fn shell(&mut self) {
|
||||
let mut conn = ImapConnection::new_connection(
|
||||
&self.server_conf,
|
||||
#[cfg(debug_assertions)]
|
||||
"ImapType::shell".into(),
|
||||
self.uid_store.clone(),
|
||||
);
|
||||
|
@ -1382,7 +1377,7 @@ impl ImapType {
|
|||
if input.trim() == "IDLE" {
|
||||
let mut iter = ImapBlockingConnection::from(conn);
|
||||
while let Some(line) = iter.next() {
|
||||
imap_log!(trace, "out: {}", unsafe { std::str::from_utf8_unchecked(&line) });
|
||||
imap_trace!("out: {}", unsafe { std::str::from_utf8_unchecked(&line) });
|
||||
}
|
||||
conn = iter.into_conn();
|
||||
}
|
||||
|
@ -1408,7 +1403,7 @@ impl ImapType {
|
|||
.iter()
|
||||
.any(|cap| cap.eq_ignore_ascii_case(b"LIST-STATUS"));
|
||||
if has_list_status {
|
||||
// [ref:TODO]: (#222) imap-codec does not support "LIST Command Extensions" currently.
|
||||
// TODO(#222): imap-codec does not support "LIST Command Extensions" currently.
|
||||
conn.send_command_raw(b"LIST \"\" \"*\" RETURN (STATUS (MESSAGES UNSEEN))")
|
||||
.await?;
|
||||
conn.read_response(
|
||||
|
@ -1421,7 +1416,7 @@ impl ImapType {
|
|||
conn.read_response(&mut res, RequiredResponses::LIST_REQUIRED)
|
||||
.await?;
|
||||
}
|
||||
imap_log!(trace, conn, "LIST reply: {}", String::from_utf8_lossy(&res));
|
||||
imap_trace!(conn, "LIST reply: {}", String::from_utf8_lossy(&res));
|
||||
for l in res.split_rn() {
|
||||
if !l.starts_with(b"*") {
|
||||
continue;
|
||||
|
@ -1463,14 +1458,14 @@ impl ImapType {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
imap_log!(trace, conn, "parse error for {:?}", l);
|
||||
imap_trace!(conn, "parse error for {:?}", l);
|
||||
}
|
||||
}
|
||||
mailboxes.retain(|_, v| !v.hash.is_null());
|
||||
conn.send_command(CommandBody::lsub("", "*")?).await?;
|
||||
conn.read_response(&mut res, RequiredResponses::LSUB_REQUIRED)
|
||||
.await?;
|
||||
imap_log!(trace, conn, "LSUB reply: {}", String::from_utf8_lossy(&res));
|
||||
imap_trace!(conn, "LSUB reply: {}", String::from_utf8_lossy(&res));
|
||||
for l in res.split_rn() {
|
||||
if !l.starts_with(b"*") {
|
||||
continue;
|
||||
|
@ -1485,7 +1480,7 @@ impl ImapType {
|
|||
f.is_subscribed = true;
|
||||
}
|
||||
} else {
|
||||
imap_log!(trace, conn, "parse error for {:?}", l);
|
||||
imap_trace!(conn, "parse error for {:?}", l);
|
||||
}
|
||||
}
|
||||
Ok(mailboxes)
|
||||
|
@ -1568,7 +1563,16 @@ impl ImapType {
|
|||
}
|
||||
get_conf_val!(s["use_idle"], true)?;
|
||||
get_conf_val!(s["use_condstore"], true)?;
|
||||
#[cfg(feature = "deflate_compression")]
|
||||
get_conf_val!(s["use_deflate"], true)?;
|
||||
#[cfg(not(feature = "deflate_compression"))]
|
||||
if s.extra.contains_key("use_deflate") {
|
||||
return Err(Error::new(format!(
|
||||
"Configuration error ({}): setting `use_deflate` is set but this version of meli \
|
||||
isn't compiled with DEFLATE support.",
|
||||
s.name.as_str(),
|
||||
)));
|
||||
}
|
||||
let _timeout = get_conf_val!(s["timeout"], 16_u64)?;
|
||||
let extra_keys = s
|
||||
.extra
|
||||
|
@ -1598,7 +1602,7 @@ impl ImapType {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
#[derive(Debug, PartialEq, Copy, Clone)]
|
||||
enum FetchStage {
|
||||
InitialFresh,
|
||||
InitialCache,
|
||||
|
@ -1617,8 +1621,7 @@ struct FetchState {
|
|||
}
|
||||
|
||||
async fn fetch_hlpr(state: &mut FetchState) -> Result<Vec<Envelope>> {
|
||||
imap_log!(
|
||||
trace,
|
||||
imap_trace!(
|
||||
state.connection.lock().await,
|
||||
"fetch_hlpr mailbox: {:?} stage: {:?}",
|
||||
state.mailbox_hash,
|
||||
|
@ -1680,8 +1683,7 @@ async fn fetch_hlpr(state: &mut FetchState) -> Result<Vec<Envelope>> {
|
|||
}
|
||||
Ok(Some(cached_payload)) => {
|
||||
state.stage = FetchStage::ResyncCache;
|
||||
imap_log!(
|
||||
trace,
|
||||
imap_trace!(
|
||||
state.connection.lock().await,
|
||||
"fetch_hlpr fetch_cached_envs payload {} len for mailbox_hash {}",
|
||||
cached_payload.len(),
|
||||
|
@ -1753,13 +1755,7 @@ async fn fetch_hlpr(state: &mut FetchState) -> Result<Vec<Envelope>> {
|
|||
conn.examine_mailbox(mailbox_hash, &mut response, false)
|
||||
.await?;
|
||||
if max_uid_left > 0 {
|
||||
imap_log!(
|
||||
trace,
|
||||
conn,
|
||||
"{} max_uid_left= {}",
|
||||
mailbox_hash,
|
||||
max_uid_left
|
||||
);
|
||||
imap_trace!(conn, "{} max_uid_left= {}", mailbox_hash, max_uid_left);
|
||||
let sequence_set = if max_uid_left == 1 {
|
||||
SequenceSet::from(ONE)
|
||||
} else {
|
||||
|
@ -1783,8 +1779,7 @@ async fn fetch_hlpr(state: &mut FetchState) -> Result<Vec<Envelope>> {
|
|||
)
|
||||
})?;
|
||||
let (_, mut v, _) = protocol_parser::fetch_responses(&response)?;
|
||||
imap_log!(
|
||||
trace,
|
||||
imap_trace!(
|
||||
conn,
|
||||
"fetch response is {} bytes and {} lines and has {} parsed Envelopes",
|
||||
response.len(),
|
||||
|
@ -1801,8 +1796,7 @@ async fn fetch_hlpr(state: &mut FetchState) -> Result<Vec<Envelope>> {
|
|||
} in v.iter_mut()
|
||||
{
|
||||
if uid.is_none() || envelope.is_none() || flags.is_none() {
|
||||
imap_log!(
|
||||
trace,
|
||||
imap_trace!(
|
||||
conn,
|
||||
"BUG? something in fetch is none. UID: {:?}, envelope: {:?} \
|
||||
flags: {:?}",
|
||||
|
@ -1810,8 +1804,7 @@ async fn fetch_hlpr(state: &mut FetchState) -> Result<Vec<Envelope>> {
|
|||
envelope,
|
||||
flags
|
||||
);
|
||||
imap_log!(
|
||||
trace,
|
||||
imap_trace!(
|
||||
conn,
|
||||
"response was: {}",
|
||||
String::from_utf8_lossy(&response)
|
||||
|
@ -1834,7 +1827,7 @@ async fn fetch_hlpr(state: &mut FetchState) -> Result<Vec<Envelope>> {
|
|||
for f in keywords {
|
||||
let hash = TagHash::from_bytes(f.as_bytes());
|
||||
tag_lck.entry(hash).or_insert_with(|| f.to_string());
|
||||
env.tags_mut().insert(hash);
|
||||
env.tags_mut().push(hash);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1865,7 +1858,7 @@ async fn fetch_hlpr(state: &mut FetchState) -> Result<Vec<Envelope>> {
|
|||
let uid = uid.unwrap();
|
||||
let env = envelope.unwrap();
|
||||
/*
|
||||
imap_log!(trace,
|
||||
imap_trace!(
|
||||
"env hash {} {} UID = {} MSN = {}",
|
||||
env.hash(),
|
||||
env.subject(),
|
|
@ -0,0 +1,820 @@
|
|||
/*
|
||||
* meli - imap melib
|
||||
*
|
||||
* 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::*;
|
||||
mod sync;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use crate::{
|
||||
backends::MailboxHash,
|
||||
email::{Envelope, EnvelopeHash},
|
||||
error::*,
|
||||
};
|
||||
|
||||
#[derive(Debug, PartialEq, Hash, Eq, Ord, PartialOrd, Copy, Clone)]
|
||||
pub struct ModSequence(pub std::num::NonZeroU64);
|
||||
|
||||
impl TryFrom<i64> for ModSequence {
|
||||
type Error = ();
|
||||
fn try_from(val: i64) -> std::result::Result<Self, ()> {
|
||||
std::num::NonZeroU64::new(val as u64)
|
||||
.map(|u| Ok(Self(u)))
|
||||
.unwrap_or(Err(()))
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for ModSequence {
|
||||
fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
write!(fmt, "{}", &self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CachedEnvelope {
|
||||
pub inner: Envelope,
|
||||
pub uid: UID,
|
||||
pub mailbox_hash: MailboxHash,
|
||||
pub modsequence: Option<ModSequence>,
|
||||
}
|
||||
|
||||
pub trait ImapCache: Send + core::fmt::Debug {
|
||||
fn reset(&mut self) -> Result<()>;
|
||||
fn mailbox_state(&mut self, mailbox_hash: MailboxHash) -> Result<Option<()>>;
|
||||
|
||||
fn find_envelope(
|
||||
&mut self,
|
||||
identifier: std::result::Result<UID, EnvelopeHash>,
|
||||
mailbox_hash: MailboxHash,
|
||||
) -> Result<Option<CachedEnvelope>>;
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
mailbox_hash: MailboxHash,
|
||||
refresh_events: &[(UID, RefreshEvent)],
|
||||
) -> Result<()>;
|
||||
|
||||
fn update_mailbox(
|
||||
&mut self,
|
||||
mailbox_hash: MailboxHash,
|
||||
select_response: &SelectResponse,
|
||||
) -> Result<()>;
|
||||
|
||||
fn insert_envelopes(
|
||||
&mut self,
|
||||
mailbox_hash: MailboxHash,
|
||||
fetches: &[FetchResponse<'_>],
|
||||
) -> Result<()>;
|
||||
|
||||
fn envelopes(&mut self, mailbox_hash: MailboxHash) -> Result<Option<Vec<EnvelopeHash>>>;
|
||||
|
||||
fn clear(&mut self, mailbox_hash: MailboxHash, select_response: &SelectResponse) -> Result<()>;
|
||||
|
||||
fn rfc822(
|
||||
&mut self,
|
||||
identifier: std::result::Result<UID, EnvelopeHash>,
|
||||
mailbox_hash: MailboxHash,
|
||||
) -> Result<Option<Vec<u8>>>;
|
||||
}
|
||||
|
||||
pub trait ImapCacheReset: Send + core::fmt::Debug {
|
||||
fn reset_db(uid_store: &UIDStore) -> Result<()>
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
#[cfg(feature = "sqlite3")]
|
||||
pub use sqlite3_m::*;
|
||||
|
||||
#[cfg(feature = "sqlite3")]
|
||||
mod sqlite3_m {
|
||||
use super::*;
|
||||
use crate::utils::sqlite3::{
|
||||
self,
|
||||
rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSql, ToSqlOutput},
|
||||
Connection, DatabaseDescription,
|
||||
};
|
||||
|
||||
type Sqlite3UID = i32;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Sqlite3Cache {
|
||||
connection: Connection,
|
||||
loaded_mailboxes: BTreeSet<MailboxHash>,
|
||||
uid_store: Arc<UIDStore>,
|
||||
}
|
||||
|
||||
const DB_DESCRIPTION: DatabaseDescription = DatabaseDescription {
|
||||
name: "header_cache.db",
|
||||
init_script: Some(
|
||||
"PRAGMA foreign_keys = true;
|
||||
PRAGMA encoding = 'UTF-8';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS envelopes (
|
||||
hash INTEGER NOT NULL,
|
||||
mailbox_hash INTEGER NOT NULL,
|
||||
uid INTEGER NOT NULL,
|
||||
modsequence INTEGER,
|
||||
rfc822 BLOB,
|
||||
envelope BLOB NOT NULL,
|
||||
PRIMARY KEY (mailbox_hash, uid),
|
||||
FOREIGN KEY (mailbox_hash) REFERENCES mailbox(mailbox_hash) ON DELETE CASCADE
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS mailbox (
|
||||
mailbox_hash INTEGER UNIQUE,
|
||||
uidvalidity INTEGER,
|
||||
flags BLOB NOT NULL,
|
||||
highestmodseq INTEGER,
|
||||
PRIMARY KEY (mailbox_hash)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS envelope_uid_idx ON envelopes(mailbox_hash, uid);
|
||||
CREATE INDEX IF NOT EXISTS envelope_idx ON envelopes(hash);
|
||||
CREATE INDEX IF NOT EXISTS mailbox_idx ON mailbox(mailbox_hash);",
|
||||
),
|
||||
version: 3,
|
||||
};
|
||||
|
||||
impl ToSql for ModSequence {
|
||||
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput> {
|
||||
Ok(ToSqlOutput::from(self.0.get() as i64))
|
||||
}
|
||||
}
|
||||
|
||||
impl FromSql for ModSequence {
|
||||
fn column_result(value: rusqlite::types::ValueRef) -> FromSqlResult<Self> {
|
||||
let i: i64 = FromSql::column_result(value)?;
|
||||
if i == 0 {
|
||||
return Err(FromSqlError::OutOfRange(0));
|
||||
}
|
||||
Ok(Self::try_from(i).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
impl Sqlite3Cache {
|
||||
pub fn get(uid_store: Arc<UIDStore>) -> Result<Box<dyn ImapCache>> {
|
||||
Ok(Box::new(Self {
|
||||
connection: sqlite3::open_or_create_db(
|
||||
&DB_DESCRIPTION,
|
||||
Some(&uid_store.account_name),
|
||||
)?,
|
||||
loaded_mailboxes: BTreeSet::default(),
|
||||
uid_store,
|
||||
}))
|
||||
}
|
||||
|
||||
fn max_uid(&self, mailbox_hash: MailboxHash) -> Result<UID> {
|
||||
let mut stmt = self
|
||||
.connection
|
||||
.prepare("SELECT MAX(uid) FROM envelopes WHERE mailbox_hash = ?1;")?;
|
||||
|
||||
let mut ret: Vec<UID> = stmt
|
||||
.query_map(sqlite3::params![mailbox_hash], |row| {
|
||||
row.get(0).map(|i: Sqlite3UID| i as UID)
|
||||
})?
|
||||
.collect::<std::result::Result<_, _>>()?;
|
||||
Ok(ret.pop().unwrap_or(0))
|
||||
}
|
||||
}
|
||||
|
||||
impl ImapCacheReset for Sqlite3Cache {
|
||||
fn reset_db(uid_store: &UIDStore) -> Result<()> {
|
||||
sqlite3::reset_db(&DB_DESCRIPTION, Some(&uid_store.account_name))
|
||||
}
|
||||
}
|
||||
|
||||
impl ImapCache for Sqlite3Cache {
|
||||
fn reset(&mut self) -> Result<()> {
|
||||
Self::reset_db(&self.uid_store)
|
||||
}
|
||||
|
||||
fn mailbox_state(&mut self, mailbox_hash: MailboxHash) -> Result<Option<()>> {
|
||||
if self.loaded_mailboxes.contains(&mailbox_hash) {
|
||||
return Ok(Some(()));
|
||||
}
|
||||
debug!("loading mailbox state {} from cache", mailbox_hash);
|
||||
let mut stmt = self.connection.prepare(
|
||||
"SELECT uidvalidity, flags, highestmodseq FROM mailbox WHERE mailbox_hash = ?1;",
|
||||
)?;
|
||||
|
||||
let mut ret = stmt.query_map(sqlite3::params![mailbox_hash], |row| {
|
||||
Ok((
|
||||
row.get(0).map(|u: Sqlite3UID| u as UID)?,
|
||||
row.get(1)?,
|
||||
row.get(2)?,
|
||||
))
|
||||
})?;
|
||||
if let Some(v) = ret.next() {
|
||||
let (uidvalidity, flags, highestmodseq): (
|
||||
UIDVALIDITY,
|
||||
Vec<u8>,
|
||||
Option<ModSequence>,
|
||||
) = v?;
|
||||
debug!(
|
||||
"mailbox state {} in cache uidvalidity {}",
|
||||
mailbox_hash, uidvalidity
|
||||
);
|
||||
debug!(
|
||||
"mailbox state {} in cache highestmodseq {:?}",
|
||||
mailbox_hash, &highestmodseq
|
||||
);
|
||||
debug!(
|
||||
"mailbox state {} inserting flags: {:?}",
|
||||
mailbox_hash,
|
||||
to_str!(&flags)
|
||||
);
|
||||
self.uid_store
|
||||
.highestmodseqs
|
||||
.lock()
|
||||
.unwrap()
|
||||
.entry(mailbox_hash)
|
||||
.and_modify(|entry| *entry = highestmodseq.ok_or(()))
|
||||
.or_insert_with(|| highestmodseq.ok_or(()));
|
||||
self.uid_store
|
||||
.uidvalidity
|
||||
.lock()
|
||||
.unwrap()
|
||||
.entry(mailbox_hash)
|
||||
.and_modify(|entry| *entry = uidvalidity)
|
||||
.or_insert(uidvalidity);
|
||||
let mut tag_lck = self.uid_store.collection.tag_index.write().unwrap();
|
||||
for f in to_str!(&flags).split('\0') {
|
||||
let hash = TagHash::from_bytes(f.as_bytes());
|
||||
tag_lck.entry(hash).or_insert_with(|| f.to_string());
|
||||
}
|
||||
self.loaded_mailboxes.insert(mailbox_hash);
|
||||
Ok(Some(()))
|
||||
} else {
|
||||
debug!("mailbox state {} not in cache", mailbox_hash);
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn clear(
|
||||
&mut self,
|
||||
mailbox_hash: MailboxHash,
|
||||
select_response: &SelectResponse,
|
||||
) -> Result<()> {
|
||||
debug!("clear mailbox_hash {} {:?}", mailbox_hash, select_response);
|
||||
self.loaded_mailboxes.remove(&mailbox_hash);
|
||||
self.connection
|
||||
.execute(
|
||||
"DELETE FROM mailbox WHERE mailbox_hash = ?1",
|
||||
sqlite3::params![mailbox_hash],
|
||||
)
|
||||
.chain_err_summary(|| {
|
||||
format!(
|
||||
"Could not clear cache of mailbox {} account {}",
|
||||
mailbox_hash, self.uid_store.account_name
|
||||
)
|
||||
})?;
|
||||
|
||||
if let Some(Ok(highestmodseq)) = select_response.highestmodseq {
|
||||
self.connection
|
||||
.execute(
|
||||
"INSERT OR IGNORE INTO mailbox (uidvalidity, flags, highestmodseq, \
|
||||
mailbox_hash) VALUES (?1, ?2, ?3, ?4)",
|
||||
sqlite3::params![
|
||||
select_response.uidvalidity as Sqlite3UID,
|
||||
select_response
|
||||
.flags
|
||||
.1
|
||||
.iter()
|
||||
.map(|s| s.as_str())
|
||||
.collect::<Vec<&str>>()
|
||||
.join("\0")
|
||||
.as_bytes(),
|
||||
highestmodseq,
|
||||
mailbox_hash
|
||||
],
|
||||
)
|
||||
.chain_err_summary(|| {
|
||||
format!(
|
||||
"Could not insert uidvalidity {} in header_cache of account {}",
|
||||
select_response.uidvalidity, self.uid_store.account_name
|
||||
)
|
||||
})?;
|
||||
} else {
|
||||
self.connection
|
||||
.execute(
|
||||
"INSERT OR IGNORE INTO mailbox (uidvalidity, flags, mailbox_hash) VALUES \
|
||||
(?1, ?2, ?3)",
|
||||
sqlite3::params![
|
||||
select_response.uidvalidity as Sqlite3UID,
|
||||
select_response
|
||||
.flags
|
||||
.1
|
||||
.iter()
|
||||
.map(|s| s.as_str())
|
||||
.collect::<Vec<&str>>()
|
||||
.join("\0")
|
||||
.as_bytes(),
|
||||
mailbox_hash
|
||||
],
|
||||
)
|
||||
.chain_err_summary(|| {
|
||||
format!(
|
||||
"Could not insert mailbox {} in header_cache of account {}",
|
||||
select_response.uidvalidity, self.uid_store.account_name
|
||||
)
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_mailbox(
|
||||
&mut self,
|
||||
mailbox_hash: MailboxHash,
|
||||
select_response: &SelectResponse,
|
||||
) -> Result<()> {
|
||||
if self.mailbox_state(mailbox_hash)?.is_none() {
|
||||
return self.clear(mailbox_hash, select_response);
|
||||
}
|
||||
|
||||
if let Some(Ok(highestmodseq)) = select_response.highestmodseq {
|
||||
self.connection
|
||||
.execute(
|
||||
"UPDATE mailbox SET flags=?1, highestmodseq =?2 where mailbox_hash = ?3;",
|
||||
sqlite3::params![
|
||||
select_response
|
||||
.flags
|
||||
.1
|
||||
.iter()
|
||||
.map(|s| s.as_str())
|
||||
.collect::<Vec<&str>>()
|
||||
.join("\0")
|
||||
.as_bytes(),
|
||||
highestmodseq,
|
||||
mailbox_hash
|
||||
],
|
||||
)
|
||||
.chain_err_summary(|| {
|
||||
format!(
|
||||
"Could not update mailbox {} in header_cache of account {}",
|
||||
mailbox_hash, self.uid_store.account_name
|
||||
)
|
||||
})?;
|
||||
} else {
|
||||
self.connection
|
||||
.execute(
|
||||
"UPDATE mailbox SET flags=?1 where mailbox_hash = ?2;",
|
||||
sqlite3::params![
|
||||
select_response
|
||||
.flags
|
||||
.1
|
||||
.iter()
|
||||
.map(|s| s.as_str())
|
||||
.collect::<Vec<&str>>()
|
||||
.join("\0")
|
||||
.as_bytes(),
|
||||
mailbox_hash
|
||||
],
|
||||
)
|
||||
.chain_err_summary(|| {
|
||||
format!(
|
||||
"Could not update mailbox {} in header_cache of account {}",
|
||||
mailbox_hash, self.uid_store.account_name
|
||||
)
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn envelopes(&mut self, mailbox_hash: MailboxHash) -> Result<Option<Vec<EnvelopeHash>>> {
|
||||
debug!("envelopes mailbox_hash {}", mailbox_hash);
|
||||
if self.mailbox_state(mailbox_hash)?.is_none() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let ret: Vec<(UID, Envelope, Option<ModSequence>)> = match {
|
||||
let mut stmt = self.connection.prepare(
|
||||
"SELECT uid, envelope, modsequence FROM envelopes WHERE mailbox_hash = ?1;",
|
||||
)?;
|
||||
|
||||
#[allow(clippy::let_and_return)] // false positive, the let binding is needed
|
||||
// for the temporary to live long enough
|
||||
let x = stmt
|
||||
.query_map(sqlite3::params![mailbox_hash], |row| {
|
||||
Ok((
|
||||
row.get(0).map(|i: Sqlite3UID| i as UID)?,
|
||||
row.get(1)?,
|
||||
row.get(2)?,
|
||||
))
|
||||
})?
|
||||
.collect::<std::result::Result<_, _>>();
|
||||
x
|
||||
} {
|
||||
Err(err) if matches!(&err, rusqlite::Error::FromSqlConversionFailure(_, _, _)) => {
|
||||
drop(err);
|
||||
self.reset()?;
|
||||
return Ok(None);
|
||||
}
|
||||
Err(err) => return Err(err.into()),
|
||||
Ok(v) => v,
|
||||
};
|
||||
let mut max_uid = 0;
|
||||
let mut env_lck = self.uid_store.envelopes.lock().unwrap();
|
||||
let mut hash_index_lck = self.uid_store.hash_index.lock().unwrap();
|
||||
let mut uid_index_lck = self.uid_store.uid_index.lock().unwrap();
|
||||
let mut env_hashes = Vec::with_capacity(ret.len());
|
||||
for (uid, env, modseq) in ret {
|
||||
env_hashes.push(env.hash());
|
||||
max_uid = std::cmp::max(max_uid, uid);
|
||||
hash_index_lck.insert(env.hash(), (uid, mailbox_hash));
|
||||
uid_index_lck.insert((mailbox_hash, uid), env.hash());
|
||||
env_lck.insert(
|
||||
env.hash(),
|
||||
CachedEnvelope {
|
||||
inner: env,
|
||||
uid,
|
||||
mailbox_hash,
|
||||
modsequence: modseq,
|
||||
},
|
||||
);
|
||||
}
|
||||
self.uid_store
|
||||
.max_uids
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(mailbox_hash, max_uid);
|
||||
Ok(Some(env_hashes))
|
||||
}
|
||||
|
||||
fn insert_envelopes(
|
||||
&mut self,
|
||||
mailbox_hash: MailboxHash,
|
||||
fetches: &[FetchResponse<'_>],
|
||||
) -> Result<()> {
|
||||
debug!(
|
||||
"insert_envelopes mailbox_hash {} len {}",
|
||||
mailbox_hash,
|
||||
fetches.len()
|
||||
);
|
||||
let mut max_uid = self
|
||||
.uid_store
|
||||
.max_uids
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get(&mailbox_hash)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
if self.mailbox_state(mailbox_hash)?.is_none() {
|
||||
return Err(Error::new("Mailbox is not in cache").set_kind(ErrorKind::Bug));
|
||||
}
|
||||
let Self {
|
||||
ref mut connection,
|
||||
ref uid_store,
|
||||
loaded_mailboxes: _,
|
||||
} = self;
|
||||
let tx = connection.transaction()?;
|
||||
for item in fetches {
|
||||
if let FetchResponse {
|
||||
uid: Some(uid),
|
||||
message_sequence_number: _,
|
||||
modseq,
|
||||
flags: _,
|
||||
body: _,
|
||||
references: _,
|
||||
envelope: Some(envelope),
|
||||
raw_fetch_value: _,
|
||||
} = item
|
||||
{
|
||||
max_uid = std::cmp::max(max_uid, *uid);
|
||||
tx.execute(
|
||||
"INSERT OR REPLACE INTO envelopes (hash, uid, mailbox_hash, modsequence, \
|
||||
envelope) VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||
sqlite3::params![
|
||||
envelope.hash(),
|
||||
*uid as Sqlite3UID,
|
||||
mailbox_hash,
|
||||
modseq,
|
||||
&envelope
|
||||
],
|
||||
)
|
||||
.chain_err_summary(|| {
|
||||
format!(
|
||||
"Could not insert envelope {} {} in header_cache of account {}",
|
||||
envelope.message_id(),
|
||||
envelope.hash(),
|
||||
uid_store.account_name
|
||||
)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
tx.commit()?;
|
||||
self.uid_store
|
||||
.max_uids
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(mailbox_hash, max_uid);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
mailbox_hash: MailboxHash,
|
||||
refresh_events: &[(UID, RefreshEvent)],
|
||||
) -> Result<()> {
|
||||
if self.mailbox_state(mailbox_hash)?.is_none() {
|
||||
return Err(Error::new("Mailbox is not in cache").set_kind(ErrorKind::Bug));
|
||||
}
|
||||
let Self {
|
||||
ref mut connection,
|
||||
ref uid_store,
|
||||
loaded_mailboxes: _,
|
||||
} = self;
|
||||
let tx = connection.transaction()?;
|
||||
let mut hash_index_lck = uid_store.hash_index.lock().unwrap();
|
||||
for (uid, event) in refresh_events {
|
||||
match &event.kind {
|
||||
RefreshEventKind::Remove(env_hash) => {
|
||||
hash_index_lck.remove(env_hash);
|
||||
tx.execute(
|
||||
"DELETE FROM envelopes WHERE mailbox_hash = ?1 AND uid = ?2;",
|
||||
sqlite3::params![mailbox_hash, *uid as Sqlite3UID],
|
||||
)
|
||||
.chain_err_summary(|| {
|
||||
format!(
|
||||
"Could not remove envelope {} uid {} from mailbox {} account {}",
|
||||
env_hash, *uid, mailbox_hash, uid_store.account_name
|
||||
)
|
||||
})?;
|
||||
}
|
||||
RefreshEventKind::NewFlags(env_hash, (flags, tags)) => {
|
||||
let mut stmt = tx.prepare(
|
||||
"SELECT envelope FROM envelopes WHERE mailbox_hash = ?1 AND uid = ?2;",
|
||||
)?;
|
||||
|
||||
let mut ret: Vec<Envelope> = stmt
|
||||
.query_map(sqlite3::params![mailbox_hash, *uid as Sqlite3UID], |row| {
|
||||
row.get(0)
|
||||
})?
|
||||
.collect::<std::result::Result<_, _>>()?;
|
||||
if let Some(mut env) = ret.pop() {
|
||||
env.set_flags(*flags);
|
||||
env.tags_mut().clear();
|
||||
env.tags_mut()
|
||||
.extend(tags.iter().map(|t| TagHash::from_bytes(t.as_bytes())));
|
||||
tx.execute(
|
||||
"UPDATE envelopes SET envelope = ?1 WHERE mailbox_hash = ?2 AND \
|
||||
uid = ?3;",
|
||||
sqlite3::params![&env, mailbox_hash, *uid as Sqlite3UID],
|
||||
)
|
||||
.chain_err_summary(|| {
|
||||
format!(
|
||||
"Could not update envelope {} uid {} from mailbox {} account \
|
||||
{}",
|
||||
env_hash, *uid, mailbox_hash, uid_store.account_name
|
||||
)
|
||||
})?;
|
||||
uid_store
|
||||
.envelopes
|
||||
.lock()
|
||||
.unwrap()
|
||||
.entry(*env_hash)
|
||||
.and_modify(|entry| {
|
||||
entry.inner = env;
|
||||
});
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
tx.commit()?;
|
||||
let new_max_uid = self.max_uid(mailbox_hash).unwrap_or(0);
|
||||
self.uid_store
|
||||
.max_uids
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(mailbox_hash, new_max_uid);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn find_envelope(
|
||||
&mut self,
|
||||
identifier: std::result::Result<UID, EnvelopeHash>,
|
||||
mailbox_hash: MailboxHash,
|
||||
) -> Result<Option<CachedEnvelope>> {
|
||||
let mut ret: Vec<(UID, Envelope, Option<ModSequence>)> = match identifier {
|
||||
Ok(uid) => {
|
||||
let mut stmt = self.connection.prepare(
|
||||
"SELECT uid, envelope, modsequence FROM envelopes WHERE mailbox_hash = ?1 \
|
||||
AND uid = ?2;",
|
||||
)?;
|
||||
|
||||
#[allow(clippy::let_and_return)] // false positive, the let binding is needed
|
||||
// for the temporary to live long enough
|
||||
let x = stmt
|
||||
.query_map(sqlite3::params![mailbox_hash, uid as Sqlite3UID], |row| {
|
||||
Ok((
|
||||
row.get(0).map(|u: Sqlite3UID| u as UID)?,
|
||||
row.get(1)?,
|
||||
row.get(2)?,
|
||||
))
|
||||
})?
|
||||
.collect::<std::result::Result<_, _>>()?;
|
||||
x
|
||||
}
|
||||
Err(env_hash) => {
|
||||
let mut stmt = self.connection.prepare(
|
||||
"SELECT uid, envelope, modsequence FROM envelopes WHERE mailbox_hash = ?1 \
|
||||
AND hash = ?2;",
|
||||
)?;
|
||||
|
||||
#[allow(clippy::let_and_return)] // false positive, the let binding is needed
|
||||
// for the temporary to live long enough
|
||||
let x = stmt
|
||||
.query_map(sqlite3::params![mailbox_hash, env_hash], |row| {
|
||||
Ok((
|
||||
row.get(0).map(|u: Sqlite3UID| u as UID)?,
|
||||
row.get(1)?,
|
||||
row.get(2)?,
|
||||
))
|
||||
})?
|
||||
.collect::<std::result::Result<_, _>>()?;
|
||||
x
|
||||
}
|
||||
};
|
||||
if ret.len() != 1 {
|
||||
return Ok(None);
|
||||
}
|
||||
let (uid, inner, modsequence) = ret.pop().unwrap();
|
||||
Ok(Some(CachedEnvelope {
|
||||
inner,
|
||||
uid,
|
||||
mailbox_hash,
|
||||
modsequence,
|
||||
}))
|
||||
}
|
||||
|
||||
fn rfc822(
|
||||
&mut self,
|
||||
identifier: std::result::Result<UID, EnvelopeHash>,
|
||||
mailbox_hash: MailboxHash,
|
||||
) -> Result<Option<Vec<u8>>> {
|
||||
let mut ret: Vec<Option<Vec<u8>>> = match identifier {
|
||||
Ok(uid) => {
|
||||
let mut stmt = self.connection.prepare(
|
||||
"SELECT rfc822 FROM envelopes WHERE mailbox_hash = ?1 AND uid = ?2;",
|
||||
)?;
|
||||
#[allow(clippy::let_and_return)] // false positive, the let binding is needed
|
||||
// for the temporary to live long enough
|
||||
let x = stmt
|
||||
.query_map(sqlite3::params![mailbox_hash, uid as Sqlite3UID], |row| {
|
||||
row.get(0)
|
||||
})?
|
||||
.collect::<std::result::Result<_, _>>()?;
|
||||
x
|
||||
}
|
||||
Err(env_hash) => {
|
||||
let mut stmt = self.connection.prepare(
|
||||
"SELECT rfc822 FROM envelopes WHERE mailbox_hash = ?1 AND hash = ?2;",
|
||||
)?;
|
||||
#[allow(clippy::let_and_return)] // false positive, the let binding is needed
|
||||
// for the temporary to live long enough
|
||||
let x = stmt
|
||||
.query_map(sqlite3::params![mailbox_hash, env_hash], |row| row.get(0))?
|
||||
.collect::<std::result::Result<_, _>>()?;
|
||||
x
|
||||
}
|
||||
};
|
||||
|
||||
if ret.len() != 1 {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(ret.pop().unwrap())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn fetch_cached_envs(state: &mut FetchState) -> Result<Option<Vec<Envelope>>> {
|
||||
let FetchState {
|
||||
stage: _,
|
||||
ref mut connection,
|
||||
mailbox_hash,
|
||||
ref uid_store,
|
||||
cache_handle: _,
|
||||
} = state;
|
||||
let mailbox_hash = *mailbox_hash;
|
||||
if !uid_store.keep_offline_cache {
|
||||
return Ok(None);
|
||||
}
|
||||
{
|
||||
let mut conn = connection.lock().await;
|
||||
match conn.load_cache(mailbox_hash).await {
|
||||
None => Ok(None),
|
||||
Some(Ok(env_hashes)) => {
|
||||
let env_lck = uid_store.envelopes.lock().unwrap();
|
||||
Ok(Some(
|
||||
env_hashes
|
||||
.into_iter()
|
||||
.filter_map(|env_hash| {
|
||||
env_lck.get(&env_hash).map(|c_env| c_env.inner.clone())
|
||||
})
|
||||
.collect::<Vec<Envelope>>(),
|
||||
))
|
||||
}
|
||||
Some(Err(err)) => Err(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "sqlite3"))]
|
||||
pub use default_m::*;
|
||||
|
||||
#[cfg(not(feature = "sqlite3"))]
|
||||
mod default_m {
|
||||
use super::*;
|
||||
#[derive(Debug)]
|
||||
pub struct DefaultCache;
|
||||
|
||||
impl DefaultCache {
|
||||
pub fn get(_uid_store: Arc<UIDStore>) -> Result<Box<dyn ImapCache>> {
|
||||
Ok(Box::new(Self))
|
||||
}
|
||||
}
|
||||
|
||||
impl ImapCacheReset for DefaultCache {
|
||||
fn reset_db(uid_store: &UIDStore) -> Result<()> {
|
||||
Err(Error::new("melib is not built with any imap cache").set_kind(ErrorKind::Bug))
|
||||
}
|
||||
}
|
||||
|
||||
impl ImapCache for DefaultCache {
|
||||
fn reset(&mut self) -> Result<()> {
|
||||
DefaultCache::reset_db(&self.uid_store)
|
||||
}
|
||||
|
||||
fn mailbox_state(&mut self, _mailbox_hash: MailboxHash) -> Result<Option<()>> {
|
||||
Err(Error::new("melib is not built with any imap cache").set_kind(ErrorKind::Bug))
|
||||
}
|
||||
|
||||
fn clear(
|
||||
&mut self,
|
||||
_mailbox_hash: MailboxHash,
|
||||
_select_response: &SelectResponse,
|
||||
) -> Result<()> {
|
||||
Err(Error::new("melib is not built with any imap cache").set_kind(ErrorKind::Bug))
|
||||
}
|
||||
|
||||
fn envelopes(&mut self, _mailbox_hash: MailboxHash) -> Result<Option<Vec<EnvelopeHash>>> {
|
||||
Err(Error::new("melib is not built with any imap cache").set_kind(ErrorKind::Bug))
|
||||
}
|
||||
|
||||
fn insert_envelopes(
|
||||
&mut self,
|
||||
_mailbox_hash: MailboxHash,
|
||||
_fetches: &[FetchResponse<'_>],
|
||||
) -> Result<()> {
|
||||
Err(Error::new("melib is not built with any imap cache").set_kind(ErrorKind::Bug))
|
||||
}
|
||||
|
||||
fn update_mailbox(
|
||||
&mut self,
|
||||
_mailbox_hash: MailboxHash,
|
||||
_select_response: &SelectResponse,
|
||||
) -> Result<()> {
|
||||
Err(Error::new("melib is not built with any imap cache").set_kind(ErrorKind::Bug))
|
||||
}
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
_mailbox_hash: MailboxHash,
|
||||
_refresh_events: &[(UID, RefreshEvent)],
|
||||
) -> Result<()> {
|
||||
Err(Error::new("melib is not built with any imap cache").set_kind(ErrorKind::Bug))
|
||||
}
|
||||
|
||||
fn find_envelope(
|
||||
&mut self,
|
||||
_identifier: std::result::Result<UID, EnvelopeHash>,
|
||||
_mailbox_hash: MailboxHash,
|
||||
) -> Result<Option<CachedEnvelope>> {
|
||||
Err(Error::new("melib is not built with any imap cache").set_kind(ErrorKind::Bug))
|
||||
}
|
||||
|
||||
fn rfc822(
|
||||
&mut self,
|
||||
_identifier: std::result::Result<UID, EnvelopeHash>,
|
||||
_mailbox_hash: MailboxHash,
|
||||
) -> Result<Option<Vec<u8>>> {
|
||||
Err(Error::new("melib is not built with any imap cache").set_kind(ErrorKind::Bug))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,13 +19,12 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use imap_codec::imap_types::{
|
||||
use imap_codec::{
|
||||
fetch::{MacroOrMessageDataItemNames, MessageDataItemName},
|
||||
search::SearchKey,
|
||||
sequence::SequenceSet,
|
||||
status::StatusDataItemName,
|
||||
};
|
||||
use indexmap::IndexSet;
|
||||
|
||||
use super::*;
|
||||
|
||||
|
@ -37,22 +36,22 @@ impl ImapConnection {
|
|||
return Ok(None);
|
||||
}
|
||||
|
||||
if let Some(mut cache_handle) = self.uid_store.cache_handle()? {
|
||||
if cache_handle.mailbox_state(mailbox_hash)?.is_none() {
|
||||
return Ok(None);
|
||||
}
|
||||
#[cfg(not(feature = "sqlite3"))]
|
||||
let mut cache_handle = DefaultCache::get(self.uid_store.clone())?;
|
||||
#[cfg(feature = "sqlite3")]
|
||||
let mut cache_handle = Sqlite3Cache::get(self.uid_store.clone())?;
|
||||
if cache_handle.mailbox_state(mailbox_hash)?.is_none() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
match self.sync_policy {
|
||||
SyncPolicy::None => Ok(None),
|
||||
SyncPolicy::Basic => self.resync_basic(cache_handle, mailbox_hash).await,
|
||||
SyncPolicy::Condstore => self.resync_condstore(cache_handle, mailbox_hash).await,
|
||||
SyncPolicy::CondstoreQresync => {
|
||||
self.resync_condstoreqresync(cache_handle, mailbox_hash)
|
||||
.await
|
||||
}
|
||||
match self.sync_policy {
|
||||
SyncPolicy::None => Ok(None),
|
||||
SyncPolicy::Basic => self.resync_basic(cache_handle, mailbox_hash).await,
|
||||
SyncPolicy::Condstore => self.resync_condstore(cache_handle, mailbox_hash).await,
|
||||
SyncPolicy::CondstoreQresync => {
|
||||
self.resync_condstoreqresync(cache_handle, mailbox_hash)
|
||||
.await
|
||||
}
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -61,8 +60,14 @@ impl ImapConnection {
|
|||
mailbox_hash: MailboxHash,
|
||||
) -> Option<Result<Vec<EnvelopeHash>>> {
|
||||
debug!("load_cache {}", mailbox_hash);
|
||||
let mut cache_handle = match self.uid_store.cache_handle() {
|
||||
Ok(v) => v?,
|
||||
#[cfg(not(feature = "sqlite3"))]
|
||||
let mut cache_handle = match DefaultCache::get(self.uid_store.clone()) {
|
||||
Ok(v) => v,
|
||||
Err(err) => return Some(Err(err)),
|
||||
};
|
||||
#[cfg(feature = "sqlite3")]
|
||||
let mut cache_handle = match Sqlite3Cache::get(self.uid_store.clone()) {
|
||||
Ok(v) => v,
|
||||
Err(err) => return Some(Err(err)),
|
||||
};
|
||||
match cache_handle.mailbox_state(mailbox_hash) {
|
||||
|
@ -79,7 +84,7 @@ impl ImapConnection {
|
|||
}
|
||||
}
|
||||
|
||||
/// RFC4549 Synchronization Operations for Disconnected IMAP4 Clients
|
||||
//rfc4549_Synchronization_Operations_for_Disconnected_IMAP4_Clients
|
||||
pub async fn resync_basic(
|
||||
&mut self,
|
||||
mut cache_handle: Box<dyn ImapCache>,
|
||||
|
@ -168,7 +173,7 @@ impl ImapConnection {
|
|||
for f in keywords {
|
||||
let hash = TagHash::from_bytes(f.as_bytes());
|
||||
tag_lck.entry(hash).or_insert_with(|| f.to_string());
|
||||
env.tags_mut().insert(hash);
|
||||
env.tags_mut().push(hash);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -261,7 +266,7 @@ impl ImapConnection {
|
|||
!= &tags
|
||||
.iter()
|
||||
.map(|t| TagHash::from_bytes(t.as_bytes()))
|
||||
.collect::<IndexSet<TagHash>>()
|
||||
.collect::<SmallVec<[TagHash; 8]>>()
|
||||
{
|
||||
env_lck.entry(env_hash).and_modify(|entry| {
|
||||
entry.inner.set_flags(flags);
|
||||
|
@ -311,8 +316,8 @@ impl ImapConnection {
|
|||
Ok(Some(payload.into_iter().map(|(_, env)| env).collect()))
|
||||
}
|
||||
|
||||
/// RFC4549 Synchronization Operations for Disconnected IMAP4 Clients
|
||||
/// > Section 6.1
|
||||
//rfc4549_Synchronization_Operations_for_Disconnected_IMAP4_Clients
|
||||
//Section 6.1
|
||||
pub async fn resync_condstore(
|
||||
&mut self,
|
||||
mut cache_handle: Box<dyn ImapCache>,
|
||||
|
@ -418,7 +423,7 @@ impl ImapConnection {
|
|||
// "SEARCH MODSEQ <cached-value>".
|
||||
|
||||
// 2. tag1 UID FETCH <lastseenuid+1>:* <descriptors>
|
||||
// [ref:TODO]: (#222) imap-codec does not support "CONDSTORE/QRESYNC" currently.
|
||||
// TODO(#222): imap-codec does not support "CONDSTORE/QRESYNC" currently.
|
||||
self.send_command_raw(
|
||||
format!(
|
||||
"UID FETCH {}:* (UID FLAGS ENVELOPE BODY.PEEK[HEADER.FIELDS (REFERENCES)] \
|
||||
|
@ -461,7 +466,7 @@ impl ImapConnection {
|
|||
for f in keywords {
|
||||
let hash = TagHash::from_bytes(f.as_bytes());
|
||||
tag_lck.entry(hash).or_insert_with(|| f.to_string());
|
||||
env.tags_mut().insert(hash);
|
||||
env.tags_mut().push(hash);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -514,7 +519,7 @@ impl ImapConnection {
|
|||
mailbox_exists.lock().unwrap().insert_set(payload_hash_set);
|
||||
// 3. tag2 UID FETCH 1:<lastseenuid> FLAGS
|
||||
if cached_max_uid == 0 {
|
||||
// [ref:TODO]: (#222) imap-codec does not support "CONDSTORE/QRESYNC" currently.
|
||||
// TODO(#222): imap-codec does not support "CONDSTORE/QRESYNC" currently.
|
||||
self.send_command_raw(
|
||||
format!(
|
||||
"UID FETCH 1:* FLAGS (CHANGEDSINCE {})",
|
||||
|
@ -524,7 +529,7 @@ impl ImapConnection {
|
|||
)
|
||||
.await?;
|
||||
} else {
|
||||
// [ref:TODO]: (#222) imap-codec does not support "CONDSTORE/QRESYNC" currently.
|
||||
// TODO(#222): imap-codec does not support "CONDSTORE/QRESYNC" currently.
|
||||
self.send_command_raw(
|
||||
format!(
|
||||
"UID FETCH 1:{} FLAGS (CHANGEDSINCE {})",
|
||||
|
@ -551,7 +556,7 @@ impl ImapConnection {
|
|||
!= &tags
|
||||
.iter()
|
||||
.map(|t| TagHash::from_bytes(t.as_bytes()))
|
||||
.collect::<IndexSet<TagHash>>()
|
||||
.collect::<SmallVec<[TagHash; 8]>>()
|
||||
{
|
||||
env_lck.entry(env_hash).and_modify(|entry| {
|
||||
entry.inner.set_flags(flags);
|
||||
|
@ -621,8 +626,8 @@ impl ImapConnection {
|
|||
Ok(Some(payload.into_iter().map(|(_, env)| env).collect()))
|
||||
}
|
||||
|
||||
/// RFC7162 Quick Flag Changes Resynchronization (CONDSTORE) and Quick
|
||||
/// Mailbox Resynchronization (QRESYNC)
|
||||
//rfc7162_Quick Flag Changes Resynchronization (CONDSTORE)_and Quick Mailbox
|
||||
// Resynchronization (QRESYNC)
|
||||
pub async fn resync_condstoreqresync(
|
||||
&mut self,
|
||||
_cache_handle: Box<dyn ImapCache>,
|
|
@ -19,24 +19,21 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use super::protocol_parser::{ImapLineSplit, ImapResponse, RequiredResponses, SelectResponse};
|
||||
use crate::{
|
||||
backends::{BackendEvent, MailboxHash, RefreshEvent},
|
||||
email::parser::BytesExt,
|
||||
error::*,
|
||||
imap::{
|
||||
protocol_parser::{self, ImapLineSplit, ImapResponse, RequiredResponses, SelectResponse},
|
||||
Capabilities, ImapServerConf, UIDStore,
|
||||
},
|
||||
text::Truncate,
|
||||
utils::{
|
||||
connections::{std_net::connect as tcp_stream_connect, Connection},
|
||||
connections::{lookup_ipv4, Connection},
|
||||
futures::timeout,
|
||||
},
|
||||
LogLevel,
|
||||
};
|
||||
extern crate native_tls;
|
||||
#[cfg(debug_assertions)]
|
||||
use std::borrow::Cow;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::HashSet,
|
||||
convert::TryFrom,
|
||||
future::Future,
|
||||
|
@ -47,36 +44,39 @@ use std::{
|
|||
};
|
||||
|
||||
use futures::io::{AsyncReadExt, AsyncWriteExt};
|
||||
#[cfg(feature = "deflate_compression")]
|
||||
use imap_codec::extensions::compress::CompressionAlgorithm;
|
||||
use imap_codec::{
|
||||
encode::{Encoder, Fragment},
|
||||
imap_types::{
|
||||
auth::AuthMechanism,
|
||||
command::{Command, CommandBody},
|
||||
core::{AString, LiteralMode, NonEmptyVec, Tag},
|
||||
extensions::{compress::CompressionAlgorithm, enable::CapabilityEnable},
|
||||
mailbox::Mailbox,
|
||||
search::SearchKey,
|
||||
secret::Secret,
|
||||
sequence::SequenceSet,
|
||||
status::StatusDataItemName,
|
||||
},
|
||||
CommandCodec,
|
||||
auth::AuthMechanism,
|
||||
codec::{Encode, Fragment},
|
||||
command::{Command, CommandBody},
|
||||
core::{AString, LiteralMode, NonEmptyVec, Tag},
|
||||
extensions::enable::CapabilityEnable,
|
||||
mailbox::Mailbox,
|
||||
search::SearchKey,
|
||||
secret::Secret,
|
||||
sequence::SequenceSet,
|
||||
status::StatusDataItemName,
|
||||
};
|
||||
use native_tls::TlsConnector;
|
||||
pub use smol::Async as AsyncWrapper;
|
||||
|
||||
const IMAP_PROTOCOL_TIMEOUT: Duration = Duration::from_secs(60 * 28);
|
||||
|
||||
macro_rules! imap_log {
|
||||
($fn:ident, $conn:expr, $fmt:literal, $($t:tt)*) => {
|
||||
log::$fn!(std::concat!("{} ", $fmt), $conn.id, $($t)*);
|
||||
macro_rules! imap_trace {
|
||||
($conn:expr, $fmt:literal, $($t:tt)*) => {
|
||||
#[cfg(debug_assertions)]
|
||||
log::trace!(std::concat!("{} ", $fmt), $conn.id, $($t)*);
|
||||
};
|
||||
($fn:ident, $conn:expr, $fmt:literal) => {
|
||||
log::$fn!(std::concat!("{} ", $fmt), $conn.id);
|
||||
($conn:expr, $fmt:literal) => {
|
||||
#[cfg(debug_assertions)]
|
||||
log::trace!(std::concat!("{} ", $fmt), $conn.id);
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
use super::{protocol_parser, Capabilities, ImapServerConf, UIDStore};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum SyncPolicy {
|
||||
None,
|
||||
/// RFC4549 `Synch Ops for Disconnected IMAP4 Clients` <https://tools.ietf.org/html/rfc4549>
|
||||
|
@ -87,16 +87,17 @@ pub enum SyncPolicy {
|
|||
CondstoreQresync,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ImapProtocol {
|
||||
IMAP { extension_use: ImapExtensionUse },
|
||||
ManageSieve,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct ImapExtensionUse {
|
||||
pub condstore: bool,
|
||||
pub idle: bool,
|
||||
#[cfg(feature = "deflate_compression")]
|
||||
pub deflate: bool,
|
||||
pub oauth2: bool,
|
||||
}
|
||||
|
@ -106,6 +107,7 @@ impl Default for ImapExtensionUse {
|
|||
Self {
|
||||
condstore: true,
|
||||
idle: true,
|
||||
#[cfg(feature = "deflate_compression")]
|
||||
deflate: true,
|
||||
oauth2: false,
|
||||
}
|
||||
|
@ -115,6 +117,7 @@ impl Default for ImapExtensionUse {
|
|||
#[derive(Debug)]
|
||||
pub struct ImapStream {
|
||||
pub cmd_id: usize,
|
||||
#[cfg(debug_assertions)]
|
||||
pub id: Cow<'static, str>,
|
||||
pub stream: AsyncWrapper<Connection>,
|
||||
pub protocol: ImapProtocol,
|
||||
|
@ -122,7 +125,7 @@ pub struct ImapStream {
|
|||
pub timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
pub enum MailboxSelection {
|
||||
None,
|
||||
Select(MailboxHash),
|
||||
|
@ -141,6 +144,7 @@ async fn try_await(cl: impl Future<Output = Result<()>> + Send) -> Result<()> {
|
|||
|
||||
#[derive(Debug)]
|
||||
pub struct ImapConnection {
|
||||
#[cfg(debug_assertions)]
|
||||
pub id: Cow<'static, str>,
|
||||
pub stream: Result<ImapStream>,
|
||||
pub server_conf: ImapServerConf,
|
||||
|
@ -151,9 +155,10 @@ pub struct ImapConnection {
|
|||
impl ImapStream {
|
||||
pub async fn new_connection(
|
||||
server_conf: &ImapServerConf,
|
||||
id: Cow<'static, str>,
|
||||
#[cfg(debug_assertions)] id: Cow<'static, str>,
|
||||
uid_store: &UIDStore,
|
||||
) -> Result<(Capabilities, Self)> {
|
||||
use std::net::TcpStream;
|
||||
let path = &server_conf.server_hostname;
|
||||
|
||||
let cmd_id = 1;
|
||||
|
@ -172,21 +177,15 @@ impl ImapStream {
|
|||
.build()
|
||||
.chain_err_kind(ErrorKind::Network(NetworkErrorKind::InvalidTLSConnection))?;
|
||||
|
||||
let mut socket = AsyncWrapper::new({
|
||||
let addr = (path.clone(), server_conf.server_port);
|
||||
let timeout = server_conf.timeout;
|
||||
let conn = Connection::new_tcp(
|
||||
smol::unblock(move || tcp_stream_connect(addr, timeout)).await?,
|
||||
);
|
||||
#[cfg(feature = "imap-trace")]
|
||||
{
|
||||
conn.trace(true).with_id("imap")
|
||||
}
|
||||
#[cfg(not(feature = "imap-trace"))]
|
||||
{
|
||||
conn
|
||||
}
|
||||
})?;
|
||||
let addr = lookup_ipv4(path, server_conf.server_port)?;
|
||||
|
||||
let mut socket = AsyncWrapper::new(Connection::Tcp(
|
||||
if let Some(timeout) = server_conf.timeout {
|
||||
TcpStream::connect_timeout(&addr, timeout)?
|
||||
} else {
|
||||
TcpStream::connect(addr)?
|
||||
},
|
||||
))?;
|
||||
if server_conf.use_starttls {
|
||||
let err_fn = || {
|
||||
if server_conf.server_port == 993 {
|
||||
|
@ -248,59 +247,44 @@ impl ImapStream {
|
|||
}
|
||||
|
||||
{
|
||||
let path = Arc::new(path.to_string());
|
||||
let conn = smol::unblock({
|
||||
let socket = socket.into_inner()?;
|
||||
let path = Arc::clone(&path);
|
||||
move || {
|
||||
let conn_result = connector.connect(&path, socket);
|
||||
if let Err(native_tls::HandshakeError::WouldBlock(midhandshake_stream)) =
|
||||
conn_result
|
||||
{
|
||||
let mut midhandshake_stream = Some(midhandshake_stream);
|
||||
loop {
|
||||
match midhandshake_stream.take().unwrap().handshake() {
|
||||
Ok(r) => {
|
||||
return Ok(r);
|
||||
}
|
||||
Err(native_tls::HandshakeError::WouldBlock(stream)) => {
|
||||
midhandshake_stream = Some(stream);
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(Error::from(err).set_kind(ErrorKind::Network(
|
||||
NetworkErrorKind::InvalidTLSConnection,
|
||||
)));
|
||||
}
|
||||
}
|
||||
// FIXME: This is blocking
|
||||
let socket = socket.into_inner()?;
|
||||
let mut conn_result = connector.connect(path, socket);
|
||||
if let Err(native_tls::HandshakeError::WouldBlock(midhandshake_stream)) =
|
||||
conn_result
|
||||
{
|
||||
let mut midhandshake_stream = Some(midhandshake_stream);
|
||||
loop {
|
||||
match midhandshake_stream.take().unwrap().handshake() {
|
||||
Ok(r) => {
|
||||
conn_result = Ok(r);
|
||||
break;
|
||||
}
|
||||
Err(native_tls::HandshakeError::WouldBlock(stream)) => {
|
||||
midhandshake_stream = Some(stream);
|
||||
}
|
||||
p => {
|
||||
p.chain_err_kind(ErrorKind::Network(
|
||||
NetworkErrorKind::InvalidTLSConnection,
|
||||
))?;
|
||||
}
|
||||
}
|
||||
conn_result.chain_err_kind(ErrorKind::Network(
|
||||
NetworkErrorKind::InvalidTLSConnection,
|
||||
))
|
||||
}
|
||||
})
|
||||
.await
|
||||
.chain_err_summary(|| format!("Could not initiate TLS negotiation to {}.", path))?;
|
||||
AsyncWrapper::new(Connection::new_tls(conn))
|
||||
.chain_err_summary(|| format!("{} connection failed.", path))
|
||||
.chain_err_kind(ErrorKind::OSError)?
|
||||
}
|
||||
AsyncWrapper::new(Connection::Tls(conn_result.chain_err_summary(|| {
|
||||
format!("Could not initiate TLS negotiation to {}.", path)
|
||||
})?))
|
||||
.chain_err_summary(|| format!("Could not initiate TLS negotiation to {}.", path))?
|
||||
}
|
||||
} else {
|
||||
AsyncWrapper::new({
|
||||
let addr = (path.clone(), server_conf.server_port);
|
||||
let timeout = server_conf.timeout;
|
||||
let conn = Connection::new_tcp(
|
||||
smol::unblock(move || tcp_stream_connect(addr, timeout)).await?,
|
||||
);
|
||||
#[cfg(feature = "imap-trace")]
|
||||
{
|
||||
conn.trace(true).with_id("imap")
|
||||
}
|
||||
#[cfg(not(feature = "imap-trace"))]
|
||||
{
|
||||
conn
|
||||
}
|
||||
})?
|
||||
let addr = lookup_ipv4(path, server_conf.server_port)?;
|
||||
AsyncWrapper::new(Connection::Tcp(
|
||||
if let Some(timeout) = server_conf.timeout {
|
||||
TcpStream::connect_timeout(&addr, timeout)?
|
||||
} else {
|
||||
TcpStream::connect(addr)?
|
||||
},
|
||||
))?
|
||||
};
|
||||
if let Err(err) = stream
|
||||
.get_ref()
|
||||
|
@ -311,6 +295,7 @@ impl ImapStream {
|
|||
let mut res = Vec::with_capacity(8 * 1024);
|
||||
let mut ret = Self {
|
||||
cmd_id,
|
||||
#[cfg(debug_assertions)]
|
||||
id,
|
||||
stream,
|
||||
protocol: server_conf.protocol,
|
||||
|
@ -324,7 +309,7 @@ impl ImapStream {
|
|||
&server_conf.server_username, &server_conf.server_password
|
||||
);
|
||||
ret.send_command(CommandBody::authenticate_with_ir(
|
||||
AuthMechanism::Plain,
|
||||
AuthMechanism::PLAIN,
|
||||
credentials.as_bytes(),
|
||||
))
|
||||
.await?;
|
||||
|
@ -350,24 +335,16 @@ impl ImapStream {
|
|||
.map(|(_, v)| v)
|
||||
});
|
||||
|
||||
let capabilities = match capabilities {
|
||||
Err(_err) => {
|
||||
log::debug!(
|
||||
"Could not connect to {}: expected CAPABILITY response but got: {} `{}`",
|
||||
&server_conf.server_hostname,
|
||||
_err,
|
||||
String::from_utf8_lossy(&res)
|
||||
);
|
||||
return Err(Error::new(format!(
|
||||
"Could not connect to {}: expected CAPABILITY response but got: `{}`",
|
||||
&server_conf.server_hostname,
|
||||
String::from_utf8_lossy(&res).as_ref().trim_at_boundary(40)
|
||||
))
|
||||
.set_kind(ErrorKind::ProtocolError));
|
||||
}
|
||||
Ok(v) => v,
|
||||
};
|
||||
if capabilities.is_err() {
|
||||
return Err(Error::new(format!(
|
||||
"Could not connect to {}: expected CAPABILITY response but got:{}",
|
||||
&server_conf.server_hostname,
|
||||
String::from_utf8_lossy(&res)
|
||||
))
|
||||
.set_kind(ErrorKind::ProtocolError));
|
||||
}
|
||||
|
||||
let capabilities = capabilities.unwrap();
|
||||
if !capabilities
|
||||
.iter()
|
||||
.any(|cap| cap.eq_ignore_ascii_case(b"IMAP4rev1"))
|
||||
|
@ -385,7 +362,7 @@ impl ImapStream {
|
|||
"Could not connect to {}: server does not accept logins [LOGINDISABLED]",
|
||||
&server_conf.server_hostname
|
||||
))
|
||||
.set_kind(ErrorKind::Authentication));
|
||||
.set_err_kind(ErrorKind::Authentication));
|
||||
}
|
||||
|
||||
(uid_store.event_consumer)(
|
||||
|
@ -412,7 +389,7 @@ impl ImapStream {
|
|||
.collect::<Vec<String>>()
|
||||
.join(" ")
|
||||
))
|
||||
.set_kind(ErrorKind::Authentication));
|
||||
.set_err_kind(ErrorKind::Authentication));
|
||||
}
|
||||
let xoauth2 = base64::decode(&server_conf.server_password)
|
||||
.chain_err_summary(|| {
|
||||
|
@ -420,7 +397,7 @@ impl ImapStream {
|
|||
})
|
||||
.chain_err_kind(ErrorKind::Configuration)?;
|
||||
ret.send_command(CommandBody::authenticate_with_ir(
|
||||
AuthMechanism::XOAuth2,
|
||||
AuthMechanism::XOAUTH2,
|
||||
&xoauth2,
|
||||
))
|
||||
.await?;
|
||||
|
@ -459,7 +436,7 @@ impl ImapStream {
|
|||
"Could not connect. Server replied with '{}'",
|
||||
String::from_utf8_lossy(l[tag_start.len()..].trim())
|
||||
))
|
||||
.set_kind(ErrorKind::Authentication));
|
||||
.set_err_kind(ErrorKind::Authentication));
|
||||
}
|
||||
should_break = true;
|
||||
}
|
||||
|
@ -469,16 +446,17 @@ impl ImapStream {
|
|||
}
|
||||
}
|
||||
|
||||
if let Some(capabilities) = capabilities {
|
||||
Ok((capabilities, ret))
|
||||
} else {
|
||||
if capabilities.is_none() {
|
||||
/* sending CAPABILITY after LOGIN automatically is an RFC recommendation, so
|
||||
* check for lazy servers */
|
||||
ret.send_command(CommandBody::Capability).await?;
|
||||
ret.read_response(&mut res).await?;
|
||||
ret.read_response(&mut res).await.unwrap();
|
||||
let capabilities = protocol_parser::capabilities(&res)?.1;
|
||||
let capabilities = HashSet::from_iter(capabilities.into_iter().map(|s| s.to_vec()));
|
||||
Ok((capabilities, ret))
|
||||
} else {
|
||||
let capabilities = capabilities.unwrap();
|
||||
Ok((capabilities, ret))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -520,7 +498,7 @@ impl ImapStream {
|
|||
&& ret[last_line_idx..].starts_with(termination_string)
|
||||
{
|
||||
if !keep_termination_string {
|
||||
ret.truncate(last_line_idx);
|
||||
ret.splice(last_line_idx.., std::iter::empty::<u8>());
|
||||
}
|
||||
break;
|
||||
} else if termination_string.is_empty() {
|
||||
|
@ -535,7 +513,7 @@ impl ImapStream {
|
|||
}
|
||||
}
|
||||
}
|
||||
//imap_log!(trace, self, "returning IMAP response:\n{:?}", &ret);
|
||||
//imap_trace!(self, "returning IMAP response:\n{:?}", &ret);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -556,15 +534,15 @@ impl ImapStream {
|
|||
match self.protocol {
|
||||
ImapProtocol::IMAP { .. } => {
|
||||
if matches!(command.body, CommandBody::Login { .. }) {
|
||||
imap_log!(trace, self, "sent: M{} LOGIN ..", self.cmd_id);
|
||||
imap_trace!(self, "sent: M{} LOGIN ..", self.cmd_id - 1);
|
||||
} else {
|
||||
imap_log!(trace, self, "sent: M{} {:?}", self.cmd_id, command.body);
|
||||
imap_trace!(self, "sent: M{} {:?}", self.cmd_id - 1, command.body);
|
||||
}
|
||||
}
|
||||
ImapProtocol::ManageSieve => {}
|
||||
}
|
||||
|
||||
for action in CommandCodec::default().encode(&command) {
|
||||
for action in command.encode() {
|
||||
match action {
|
||||
Fragment::Line { data } => {
|
||||
self.stream.write_all(&data).await?;
|
||||
|
@ -612,11 +590,11 @@ impl ImapStream {
|
|||
match self.protocol {
|
||||
ImapProtocol::IMAP { .. } => {
|
||||
if !command.starts_with(b"LOGIN") {
|
||||
imap_log!(trace, self, "sent: M{} {}", self.cmd_id - 1, unsafe {
|
||||
imap_trace!(self, "sent: M{} {}", self.cmd_id - 1, unsafe {
|
||||
std::str::from_utf8_unchecked(command)
|
||||
});
|
||||
} else {
|
||||
imap_log!(trace, self, "sent: M{} LOGIN ..", self.cmd_id - 1);
|
||||
imap_trace!(self, "sent: M{} LOGIN ..", self.cmd_id - 1);
|
||||
}
|
||||
}
|
||||
ImapProtocol::ManageSieve => {}
|
||||
|
@ -646,14 +624,15 @@ impl ImapStream {
|
|||
impl ImapConnection {
|
||||
pub fn new_connection(
|
||||
server_conf: &ImapServerConf,
|
||||
id: Cow<'static, str>,
|
||||
#[cfg(debug_assertions)] id: Cow<'static, str>,
|
||||
uid_store: Arc<UIDStore>,
|
||||
) -> Self {
|
||||
Self {
|
||||
stream: Err(Error::new("Offline".to_string())),
|
||||
#[cfg(debug_assertions)]
|
||||
id,
|
||||
server_conf: server_conf.clone(),
|
||||
sync_policy: if *uid_store.keep_offline_cache.lock().unwrap() {
|
||||
sync_policy: if uid_store.keep_offline_cache {
|
||||
SyncPolicy::Basic
|
||||
} else {
|
||||
SyncPolicy::None
|
||||
|
@ -672,7 +651,7 @@ impl ImapConnection {
|
|||
"Connection timed out after {} seconds",
|
||||
IMAP_PROTOCOL_TIMEOUT.as_secs()
|
||||
))
|
||||
.set_kind(ErrorKind::TimedOut);
|
||||
.set_kind(ErrorKind::Timeout);
|
||||
*status = Err(err.clone());
|
||||
self.stream = Err(err);
|
||||
}
|
||||
|
@ -686,15 +665,9 @@ impl ImapConnection {
|
|||
})
|
||||
.await
|
||||
{
|
||||
imap_log!(
|
||||
trace,
|
||||
self,
|
||||
"connect(): connection is probably dead: {:?}",
|
||||
&_err
|
||||
);
|
||||
imap_trace!(self, "connect(): connection is probably dead: {:?}", &_err);
|
||||
} else {
|
||||
imap_log!(
|
||||
trace,
|
||||
imap_trace!(
|
||||
self,
|
||||
"connect(): connection is probably alive, NOOP returned {:?}",
|
||||
&String::from_utf8_lossy(&ret)
|
||||
|
@ -702,9 +675,13 @@ impl ImapConnection {
|
|||
return Ok(());
|
||||
}
|
||||
}
|
||||
let new_stream =
|
||||
ImapStream::new_connection(&self.server_conf, self.id.clone(), &self.uid_store)
|
||||
.await;
|
||||
let new_stream = ImapStream::new_connection(
|
||||
&self.server_conf,
|
||||
#[cfg(debug_assertions)]
|
||||
self.id.clone(),
|
||||
&self.uid_store,
|
||||
)
|
||||
.await;
|
||||
if let Err(err) = new_stream.as_ref() {
|
||||
self.uid_store.is_online.lock().unwrap().1 = Err(err.clone());
|
||||
} else {
|
||||
|
@ -717,6 +694,7 @@ impl ImapConnection {
|
|||
extension_use:
|
||||
ImapExtensionUse {
|
||||
condstore,
|
||||
#[cfg(feature = "deflate_compression")]
|
||||
deflate,
|
||||
idle: _idle,
|
||||
oauth2: _,
|
||||
|
@ -757,6 +735,7 @@ impl ImapConnection {
|
|||
}
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "deflate_compression")]
|
||||
if capabilities.contains(&b"COMPRESS=DEFLATE"[..]) && deflate {
|
||||
let mut ret = Vec::new();
|
||||
self.send_command(CommandBody::compress(CompressionAlgorithm::Deflate))
|
||||
|
@ -778,6 +757,7 @@ impl ImapConnection {
|
|||
ImapResponse::Ok(_) => {
|
||||
let ImapStream {
|
||||
cmd_id,
|
||||
#[cfg(debug_assertions)]
|
||||
id,
|
||||
stream,
|
||||
protocol,
|
||||
|
@ -787,6 +767,7 @@ impl ImapConnection {
|
|||
let stream = stream.into_inner()?;
|
||||
self.stream = Ok(ImapStream {
|
||||
cmd_id,
|
||||
#[cfg(debug_assertions)]
|
||||
id,
|
||||
stream: AsyncWrapper::new(stream.deflate())?,
|
||||
protocol,
|
||||
|
@ -830,8 +811,7 @@ impl ImapConnection {
|
|||
ImapResponse::No(ref _response_code)
|
||||
if required_responses.intersects(RequiredResponses::NO_REQUIRED) =>
|
||||
{
|
||||
imap_log!(
|
||||
trace,
|
||||
imap_trace!(
|
||||
self,
|
||||
"Received expected NO response: {:?} {:?}",
|
||||
_response_code,
|
||||
|
@ -839,8 +819,7 @@ impl ImapConnection {
|
|||
);
|
||||
}
|
||||
ImapResponse::No(ref response_code) => {
|
||||
imap_log!(
|
||||
trace,
|
||||
imap_trace!(
|
||||
self,
|
||||
"Received NO response: {:?} {:?}",
|
||||
response_code,
|
||||
|
@ -858,8 +837,7 @@ impl ImapConnection {
|
|||
return r.into();
|
||||
}
|
||||
ImapResponse::Bad(ref response_code) => {
|
||||
imap_log!(
|
||||
trace,
|
||||
imap_trace!(
|
||||
self,
|
||||
"Received BAD response: {:?} {:?}",
|
||||
response_code,
|
||||
|
@ -878,12 +856,12 @@ impl ImapConnection {
|
|||
}
|
||||
_ => {}
|
||||
}
|
||||
/* imap_log!(trace, self,
|
||||
/* imap_trace!(self,
|
||||
"check every line for required_responses: {:#?}",
|
||||
&required_responses
|
||||
);*/
|
||||
for l in response.split_rn() {
|
||||
/* imap_log!(trace, self, "check line: {}", &l); */
|
||||
/* imap_trace!(self, "check line: {}", &l); */
|
||||
if required_responses.check(l) || !self.process_untagged(l).await? {
|
||||
ret.extend_from_slice(l);
|
||||
}
|
||||
|
@ -1002,8 +980,7 @@ impl ImapConnection {
|
|||
.await?;
|
||||
self.read_response(ret, RequiredResponses::SELECT_REQUIRED)
|
||||
.await?;
|
||||
imap_log!(
|
||||
trace,
|
||||
imap_trace!(
|
||||
self,
|
||||
"{} SELECT response {}",
|
||||
imap_path,
|
||||
|
@ -1013,7 +990,11 @@ impl ImapConnection {
|
|||
format!("Could not parse select response for mailbox {}", imap_path)
|
||||
})?;
|
||||
{
|
||||
if let Some(mut cache_handle) = self.uid_store.cache_handle()? {
|
||||
if self.uid_store.keep_offline_cache {
|
||||
#[cfg(not(feature = "sqlite3"))]
|
||||
let mut cache_handle = super::cache::DefaultCache::get(self.uid_store.clone())?;
|
||||
#[cfg(feature = "sqlite3")]
|
||||
let mut cache_handle = super::cache::Sqlite3Cache::get(self.uid_store.clone())?;
|
||||
if let Err(err) = cache_handle.mailbox_state(mailbox_hash).and_then(|r| {
|
||||
if r.is_none() {
|
||||
cache_handle.clear(mailbox_hash, &select_response)
|
||||
|
@ -1087,12 +1068,7 @@ impl ImapConnection {
|
|||
self.read_response(ret, RequiredResponses::EXAMINE_REQUIRED)
|
||||
.await?;
|
||||
|
||||
imap_log!(
|
||||
trace,
|
||||
self,
|
||||
"EXAMINE response {}",
|
||||
String::from_utf8_lossy(ret)
|
||||
);
|
||||
imap_trace!(self, "EXAMINE response {}", String::from_utf8_lossy(ret));
|
||||
let select_response = protocol_parser::select_response(ret).chain_err_summary(|| {
|
||||
format!("Could not parse select response for mailbox {}", imap_path)
|
||||
})?;
|
|
@ -19,19 +19,20 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::{fmt, sync::Arc};
|
||||
|
||||
use imap_codec::imap_types::{
|
||||
command::error::{AppendError, CopyError, ListError},
|
||||
error::ValidationError,
|
||||
extensions::r#move::error::MoveError,
|
||||
use imap_codec::{
|
||||
command::{AppendError, CopyError, ListError},
|
||||
core::LiteralError,
|
||||
extensions::r#move::MoveError,
|
||||
sequence::SequenceSetError,
|
||||
};
|
||||
|
||||
use crate::error::{Error, ErrorKind};
|
||||
|
||||
impl From<ValidationError> for Error {
|
||||
impl From<LiteralError> for Error {
|
||||
#[inline]
|
||||
fn from(error: ValidationError) -> Self {
|
||||
fn from(error: LiteralError) -> Self {
|
||||
Self {
|
||||
summary: error.to_string().into(),
|
||||
details: None,
|
||||
|
@ -41,9 +42,21 @@ impl From<ValidationError> for Error {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<SequenceSetError> for Error {
|
||||
#[inline]
|
||||
fn from(error: SequenceSetError) -> Self {
|
||||
Self {
|
||||
summary: error.to_string().into(),
|
||||
details: None,
|
||||
source: Some(Arc::new(error)),
|
||||
kind: ErrorKind::Bug,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, L> From<AppendError<S, L>> for Error
|
||||
where
|
||||
AppendError<S, L>: std::fmt::Debug + std::fmt::Display + Sync + Send + 'static,
|
||||
AppendError<S, L>: fmt::Debug + fmt::Display + Sync + Send + 'static,
|
||||
{
|
||||
#[inline]
|
||||
fn from(error: AppendError<S, L>) -> Self {
|
||||
|
@ -58,7 +71,7 @@ where
|
|||
|
||||
impl<S, L> From<CopyError<S, L>> for Error
|
||||
where
|
||||
CopyError<S, L>: std::fmt::Debug + std::fmt::Display + Sync + Send + 'static,
|
||||
CopyError<S, L>: fmt::Debug + fmt::Display + Sync + Send + 'static,
|
||||
{
|
||||
#[inline]
|
||||
fn from(error: CopyError<S, L>) -> Self {
|
||||
|
@ -73,7 +86,7 @@ where
|
|||
|
||||
impl<S, M> From<MoveError<S, M>> for Error
|
||||
where
|
||||
MoveError<S, M>: std::fmt::Debug + std::fmt::Display + Sync + Send + 'static,
|
||||
MoveError<S, M>: fmt::Debug + fmt::Display + Sync + Send + 'static,
|
||||
{
|
||||
#[inline]
|
||||
fn from(error: MoveError<S, M>) -> Self {
|
||||
|
@ -88,7 +101,7 @@ where
|
|||
|
||||
impl<L1, L2> From<ListError<L1, L2>> for Error
|
||||
where
|
||||
ListError<L1, L2>: std::fmt::Debug + std::fmt::Display + Sync + Send + 'static,
|
||||
ListError<L1, L2>: fmt::Debug + fmt::Display + Sync + Send + 'static,
|
||||
{
|
||||
#[inline]
|
||||
fn from(error: ListError<L1, L2>) -> Self {
|
|
@ -29,7 +29,7 @@ use crate::{
|
|||
error::*,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct ImapMailbox {
|
||||
pub hash: MailboxHash,
|
||||
pub imap_path: String,
|
|
@ -25,6 +25,11 @@ use std::{
|
|||
time::SystemTime,
|
||||
};
|
||||
|
||||
use nom::{
|
||||
branch::alt, bytes::complete::tag, combinator::map, multi::separated_list1,
|
||||
sequence::separated_pair,
|
||||
};
|
||||
|
||||
use super::{ImapConnection, ImapProtocol, ImapServerConf, UIDStore};
|
||||
use crate::{
|
||||
conf::AccountSettings,
|
||||
|
@ -38,7 +43,18 @@ pub struct ManageSieveConnection {
|
|||
pub inner: ImapConnection,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub fn managesieve_capabilities(input: &[u8]) -> Result<Vec<(&[u8], &[u8])>> {
|
||||
let (_, ret) = separated_list1(
|
||||
tag(b"\r\n"),
|
||||
alt((
|
||||
separated_pair(quoted_raw, tag(b" "), quoted_raw),
|
||||
map(quoted_raw, |q| (q, &b""[..])),
|
||||
)),
|
||||
)(input)?;
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
||||
pub enum ManageSieveResponse<'a> {
|
||||
Ok {
|
||||
code: Option<&'a [u8]>,
|
||||
|
@ -50,6 +66,231 @@ pub enum ManageSieveResponse<'a> {
|
|||
},
|
||||
}
|
||||
|
||||
mod parser {
|
||||
use nom::{
|
||||
bytes::complete::tag,
|
||||
character::complete::crlf,
|
||||
combinator::{iterator, map, opt},
|
||||
};
|
||||
pub use nom::{
|
||||
bytes::complete::{is_not, tag_no_case},
|
||||
sequence::{delimited, pair, preceded, terminated},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
pub fn sieve_name(input: &[u8]) -> IResult<&[u8], &[u8]> {
|
||||
crate::backends::imap::protocol_parser::string_token(input)
|
||||
}
|
||||
|
||||
// *(sieve-name [SP "ACTIVE"] CRLF)
|
||||
// response-oknobye
|
||||
pub fn listscripts(input: &[u8]) -> IResult<&[u8], Vec<(&[u8], bool)>> {
|
||||
let mut it = iterator(
|
||||
input,
|
||||
alt((
|
||||
terminated(
|
||||
map(terminated(sieve_name, tag_no_case(b" ACTIVE")), |r| {
|
||||
(r, true)
|
||||
}),
|
||||
crlf,
|
||||
),
|
||||
terminated(map(sieve_name, |r| (r, false)), crlf),
|
||||
)),
|
||||
);
|
||||
|
||||
let parsed = (&mut it).collect::<Vec<(&[u8], bool)>>();
|
||||
let res: IResult<_, _> = it.finish();
|
||||
let (rest, _) = res?;
|
||||
Ok((rest, parsed))
|
||||
}
|
||||
|
||||
// response-getscript = (sieve-script CRLF response-ok) /
|
||||
// response-nobye
|
||||
pub fn getscript(input: &[u8]) -> IResult<&[u8], &[u8]> {
|
||||
sieve_name(input)
|
||||
}
|
||||
|
||||
pub fn response_oknobye(input: &[u8]) -> IResult<&[u8], ManageSieveResponse> {
|
||||
alt((
|
||||
map(
|
||||
terminated(
|
||||
pair(
|
||||
preceded(
|
||||
tag_no_case(b"ok"),
|
||||
opt(preceded(
|
||||
tag(b" "),
|
||||
delimited(tag(b"("), is_not(")"), tag(b")")),
|
||||
)),
|
||||
),
|
||||
opt(preceded(tag(b" "), sieve_name)),
|
||||
),
|
||||
crlf,
|
||||
),
|
||||
|(code, message)| ManageSieveResponse::Ok { code, message },
|
||||
),
|
||||
map(
|
||||
terminated(
|
||||
pair(
|
||||
preceded(
|
||||
alt((tag_no_case(b"no"), tag_no_case(b"bye"))),
|
||||
opt(preceded(
|
||||
tag(b" "),
|
||||
delimited(tag(b"("), is_not(")"), tag(b")")),
|
||||
)),
|
||||
),
|
||||
opt(preceded(tag(b" "), sieve_name)),
|
||||
),
|
||||
crlf,
|
||||
),
|
||||
|(code, message)| ManageSieveResponse::NoBye { code, message },
|
||||
),
|
||||
))(input)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_managesieve_listscripts() {
|
||||
let input_1 = b"\"summer_script\"\r\n\"vacation_script\"\r\n{13}\r\nclever\"script\r\n\"main_script\" ACTIVE\r\nOK";
|
||||
assert_eq!(
|
||||
terminated(listscripts, tag_no_case(b"OK"))(input_1),
|
||||
Ok((
|
||||
&b""[..],
|
||||
vec![
|
||||
(&b"summer_script"[..], false),
|
||||
(&b"vacation_script"[..], false),
|
||||
(&b"clever\"script"[..], false),
|
||||
(&b"main_script"[..], true)
|
||||
]
|
||||
))
|
||||
);
|
||||
|
||||
let input_2 = b"\"summer_script\"\r\n\"main_script\" active\r\nok";
|
||||
assert_eq!(
|
||||
terminated(listscripts, tag_no_case(b"OK"))(input_2),
|
||||
Ok((
|
||||
&b""[..],
|
||||
vec![(&b"summer_script"[..], false), (&b"main_script"[..], true)]
|
||||
))
|
||||
);
|
||||
let input_3 = b"ok";
|
||||
assert_eq!(
|
||||
terminated(listscripts, tag_no_case(b"OK"))(input_3),
|
||||
Ok((&b""[..], vec![]))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_managesieve_general() {
|
||||
assert_eq!(managesieve_capabilities(b"\"IMPLEMENTATION\" \"Dovecot Pigeonhole\"\r\n\"SIEVE\" \"fileinto reject envelope encoded-character vacation subaddress comparator-i;ascii-numeric relational regex imap4flags copy include variables body enotify environment mailbox date index ihave duplicate mime foreverypart extracttext\"\r\n\"NOTIFY\" \"mailto\"\r\n\"SASL\" \"PLAIN\"\r\n\"STARTTLS\"\r\n\"VERSION\" \"1.0\"\r\n").unwrap(), vec![
|
||||
(&b"IMPLEMENTATION"[..],&b"Dovecot Pigeonhole"[..]),
|
||||
(&b"SIEVE"[..],&b"fileinto reject envelope encoded-character vacation subaddress comparator-i;ascii-numeric relational regex imap4flags copy include variables body enotify environment mailbox date index ihave duplicate mime foreverypart extracttext"[..]),
|
||||
(&b"NOTIFY"[..],&b"mailto"[..]),
|
||||
(&b"SASL"[..],&b"PLAIN"[..]),
|
||||
(&b"STARTTLS"[..], &b""[..]),
|
||||
(&b"VERSION"[..],&b"1.0"[..])]
|
||||
|
||||
);
|
||||
|
||||
let response_ok = b"OK (WARNINGS) \"line 8: server redirect action limit is 2, this redirect might be ignored\"\r\n";
|
||||
assert_eq!(
|
||||
response_oknobye(response_ok),
|
||||
Ok((
|
||||
&b""[..],
|
||||
ManageSieveResponse::Ok {
|
||||
code: Some(&b"WARNINGS"[..]),
|
||||
message: Some(&b"line 8: server redirect action limit is 2, this redirect might be ignored"[..]),
|
||||
}
|
||||
))
|
||||
);
|
||||
let response_ok = b"OK (WARNINGS)\r\n";
|
||||
assert_eq!(
|
||||
response_oknobye(response_ok),
|
||||
Ok((
|
||||
&b""[..],
|
||||
ManageSieveResponse::Ok {
|
||||
code: Some(&b"WARNINGS"[..]),
|
||||
message: None,
|
||||
}
|
||||
))
|
||||
);
|
||||
let response_ok =
|
||||
b"OK \"line 8: server redirect action limit is 2, this redirect might be ignored\"\r\n";
|
||||
assert_eq!(
|
||||
response_oknobye(response_ok),
|
||||
Ok((
|
||||
&b""[..],
|
||||
ManageSieveResponse::Ok {
|
||||
code: None,
|
||||
message: Some(&b"line 8: server redirect action limit is 2, this redirect might be ignored"[..]),
|
||||
}
|
||||
))
|
||||
);
|
||||
let response_ok = b"Ok\r\n";
|
||||
assert_eq!(
|
||||
response_oknobye(response_ok),
|
||||
Ok((
|
||||
&b""[..],
|
||||
ManageSieveResponse::Ok {
|
||||
code: None,
|
||||
message: None,
|
||||
}
|
||||
))
|
||||
);
|
||||
|
||||
let response_nobye = b"No (NONEXISTENT) \"There is no script by that name\"\r\n";
|
||||
assert_eq!(
|
||||
response_oknobye(response_nobye),
|
||||
Ok((
|
||||
&b""[..],
|
||||
ManageSieveResponse::NoBye {
|
||||
code: Some(&b"NONEXISTENT"[..]),
|
||||
message: Some(&b"There is no script by that name"[..]),
|
||||
}
|
||||
))
|
||||
);
|
||||
let response_nobye = b"No (NONEXISTENT) {31}\r\nThere is no script by that name\r\n";
|
||||
assert_eq!(
|
||||
response_oknobye(response_nobye),
|
||||
Ok((
|
||||
&b""[..],
|
||||
ManageSieveResponse::NoBye {
|
||||
code: Some(&b"NONEXISTENT"[..]),
|
||||
message: Some(&b"There is no script by that name"[..]),
|
||||
}
|
||||
))
|
||||
);
|
||||
|
||||
let response_nobye = b"No\r\n";
|
||||
assert_eq!(
|
||||
response_oknobye(response_nobye),
|
||||
Ok((
|
||||
&b""[..],
|
||||
ManageSieveResponse::NoBye {
|
||||
code: None,
|
||||
message: None,
|
||||
}
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Return a byte sequence surrounded by "s and decoded if necessary
|
||||
pub fn quoted_raw(input: &[u8]) -> IResult<&[u8], &[u8]> {
|
||||
if input.is_empty() || input[0] != b'"' {
|
||||
return Err(nom::Err::Error((input, "empty").into()));
|
||||
}
|
||||
|
||||
let mut i = 1;
|
||||
while i < input.len() {
|
||||
if input[i] == b'\"' && input[i - 1] != b'\\' {
|
||||
return Ok((&input[i + 1..], &input[1..i]));
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
Err(nom::Err::Error((input, "no quotes").into()))
|
||||
}
|
||||
|
||||
impl ManageSieveConnection {
|
||||
pub fn new(
|
||||
account_hash: crate::backends::AccountHash,
|
||||
|
@ -95,6 +336,7 @@ impl ManageSieveConnection {
|
|||
Ok(Self {
|
||||
inner: ImapConnection::new_connection(
|
||||
&server_conf,
|
||||
#[cfg(debug_assertions)]
|
||||
"ManageSieveConnection::new()".into(),
|
||||
uid_store,
|
||||
),
|
||||
|
@ -237,247 +479,3 @@ impl ManageSieveConnection {
|
|||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub mod parser {
|
||||
use nom::{
|
||||
branch::alt,
|
||||
bytes::complete::tag,
|
||||
character::complete::crlf,
|
||||
combinator::{iterator, map, opt},
|
||||
multi::separated_list1,
|
||||
sequence::separated_pair,
|
||||
};
|
||||
pub use nom::{
|
||||
bytes::complete::{is_not, tag_no_case},
|
||||
sequence::{delimited, pair, preceded, terminated},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Return a byte sequence surrounded by "s and decoded if necessary
|
||||
pub fn quoted_raw(input: &[u8]) -> IResult<&[u8], &[u8]> {
|
||||
if input.is_empty() || input[0] != b'"' {
|
||||
return Err(nom::Err::Error((input, "empty").into()));
|
||||
}
|
||||
|
||||
let mut i = 1;
|
||||
while i < input.len() {
|
||||
if input[i] == b'\"' && input[i - 1] != b'\\' {
|
||||
return Ok((&input[i + 1..], &input[1..i]));
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
Err(nom::Err::Error((input, "no quotes").into()))
|
||||
}
|
||||
|
||||
pub fn managesieve_capabilities(input: &[u8]) -> Result<Vec<(&[u8], &[u8])>> {
|
||||
let (_, ret) = separated_list1(
|
||||
tag(b"\r\n"),
|
||||
alt((
|
||||
separated_pair(quoted_raw, tag(b" "), quoted_raw),
|
||||
map(quoted_raw, |q| (q, &b""[..])),
|
||||
)),
|
||||
)(input)?;
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
pub fn sieve_name(input: &[u8]) -> IResult<&[u8], &[u8]> {
|
||||
crate::imap::protocol_parser::string_token(input)
|
||||
}
|
||||
|
||||
// *(sieve-name [SP "ACTIVE"] CRLF)
|
||||
// response-oknobye
|
||||
pub fn listscripts(input: &[u8]) -> IResult<&[u8], Vec<(&[u8], bool)>> {
|
||||
let mut it = iterator(
|
||||
input,
|
||||
alt((
|
||||
terminated(
|
||||
map(terminated(sieve_name, tag_no_case(b" ACTIVE")), |r| {
|
||||
(r, true)
|
||||
}),
|
||||
crlf,
|
||||
),
|
||||
terminated(map(sieve_name, |r| (r, false)), crlf),
|
||||
)),
|
||||
);
|
||||
|
||||
let parsed = (&mut it).collect::<Vec<(&[u8], bool)>>();
|
||||
let res: IResult<_, _> = it.finish();
|
||||
let (rest, _) = res?;
|
||||
Ok((rest, parsed))
|
||||
}
|
||||
|
||||
// response-getscript = (sieve-script CRLF response-ok) /
|
||||
// response-nobye
|
||||
pub fn getscript(input: &[u8]) -> IResult<&[u8], &[u8]> {
|
||||
sieve_name(input)
|
||||
}
|
||||
|
||||
pub fn response_oknobye(input: &[u8]) -> IResult<&[u8], ManageSieveResponse> {
|
||||
alt((
|
||||
map(
|
||||
terminated(
|
||||
pair(
|
||||
preceded(
|
||||
tag_no_case(b"ok"),
|
||||
opt(preceded(
|
||||
tag(b" "),
|
||||
delimited(tag(b"("), is_not(")"), tag(b")")),
|
||||
)),
|
||||
),
|
||||
opt(preceded(tag(b" "), sieve_name)),
|
||||
),
|
||||
crlf,
|
||||
),
|
||||
|(code, message)| ManageSieveResponse::Ok { code, message },
|
||||
),
|
||||
map(
|
||||
terminated(
|
||||
pair(
|
||||
preceded(
|
||||
alt((tag_no_case(b"no"), tag_no_case(b"bye"))),
|
||||
opt(preceded(
|
||||
tag(b" "),
|
||||
delimited(tag(b"("), is_not(")"), tag(b")")),
|
||||
)),
|
||||
),
|
||||
opt(preceded(tag(b" "), sieve_name)),
|
||||
),
|
||||
crlf,
|
||||
),
|
||||
|(code, message)| ManageSieveResponse::NoBye { code, message },
|
||||
),
|
||||
))(input)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_managesieve_listscripts() {
|
||||
let input_1 = b"\"summer_script\"\r\n\"vacation_script\"\r\n{13}\r\nclever\"script\r\n\"main_script\" ACTIVE\r\nOK";
|
||||
assert_eq!(
|
||||
terminated(listscripts, tag_no_case(b"OK"))(input_1),
|
||||
Ok((
|
||||
&b""[..],
|
||||
vec![
|
||||
(&b"summer_script"[..], false),
|
||||
(&b"vacation_script"[..], false),
|
||||
(&b"clever\"script"[..], false),
|
||||
(&b"main_script"[..], true)
|
||||
]
|
||||
))
|
||||
);
|
||||
|
||||
let input_2 = b"\"summer_script\"\r\n\"main_script\" active\r\nok";
|
||||
assert_eq!(
|
||||
terminated(listscripts, tag_no_case(b"OK"))(input_2),
|
||||
Ok((
|
||||
&b""[..],
|
||||
vec![(&b"summer_script"[..], false), (&b"main_script"[..], true)]
|
||||
))
|
||||
);
|
||||
let input_3 = b"ok";
|
||||
assert_eq!(
|
||||
terminated(listscripts, tag_no_case(b"OK"))(input_3),
|
||||
Ok((&b""[..], vec![]))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_managesieve_general() {
|
||||
assert_eq!(managesieve_capabilities(b"\"IMPLEMENTATION\" \"Dovecot Pigeonhole\"\r\n\"SIEVE\" \"fileinto reject envelope encoded-character vacation subaddress comparator-i;ascii-numeric relational regex imap4flags copy include variables body enotify environment mailbox date index ihave duplicate mime foreverypart extracttext\"\r\n\"NOTIFY\" \"mailto\"\r\n\"SASL\" \"PLAIN\"\r\n\"STARTTLS\"\r\n\"VERSION\" \"1.0\"\r\n").unwrap(), vec![
|
||||
(&b"IMPLEMENTATION"[..],&b"Dovecot Pigeonhole"[..]),
|
||||
(&b"SIEVE"[..],&b"fileinto reject envelope encoded-character vacation subaddress comparator-i;ascii-numeric relational regex imap4flags copy include variables body enotify environment mailbox date index ihave duplicate mime foreverypart extracttext"[..]),
|
||||
(&b"NOTIFY"[..],&b"mailto"[..]),
|
||||
(&b"SASL"[..],&b"PLAIN"[..]),
|
||||
(&b"STARTTLS"[..], &b""[..]),
|
||||
(&b"VERSION"[..],&b"1.0"[..])]
|
||||
|
||||
);
|
||||
|
||||
let response_ok = b"OK (WARNINGS) \"line 8: server redirect action limit is 2, this redirect might be ignored\"\r\n";
|
||||
assert_eq!(
|
||||
response_oknobye(response_ok),
|
||||
Ok((
|
||||
&b""[..],
|
||||
ManageSieveResponse::Ok {
|
||||
code: Some(&b"WARNINGS"[..]),
|
||||
message: Some(&b"line 8: server redirect action limit is 2, this redirect might be ignored"[..]),
|
||||
}
|
||||
))
|
||||
);
|
||||
let response_ok = b"OK (WARNINGS)\r\n";
|
||||
assert_eq!(
|
||||
response_oknobye(response_ok),
|
||||
Ok((
|
||||
&b""[..],
|
||||
ManageSieveResponse::Ok {
|
||||
code: Some(&b"WARNINGS"[..]),
|
||||
message: None,
|
||||
}
|
||||
))
|
||||
);
|
||||
let response_ok =
|
||||
b"OK \"line 8: server redirect action limit is 2, this redirect might be ignored\"\r\n";
|
||||
assert_eq!(
|
||||
response_oknobye(response_ok),
|
||||
Ok((
|
||||
&b""[..],
|
||||
ManageSieveResponse::Ok {
|
||||
code: None,
|
||||
message: Some(&b"line 8: server redirect action limit is 2, this redirect might be ignored"[..]),
|
||||
}
|
||||
))
|
||||
);
|
||||
let response_ok = b"Ok\r\n";
|
||||
assert_eq!(
|
||||
response_oknobye(response_ok),
|
||||
Ok((
|
||||
&b""[..],
|
||||
ManageSieveResponse::Ok {
|
||||
code: None,
|
||||
message: None,
|
||||
}
|
||||
))
|
||||
);
|
||||
|
||||
let response_nobye = b"No (NONEXISTENT) \"There is no script by that name\"\r\n";
|
||||
assert_eq!(
|
||||
response_oknobye(response_nobye),
|
||||
Ok((
|
||||
&b""[..],
|
||||
ManageSieveResponse::NoBye {
|
||||
code: Some(&b"NONEXISTENT"[..]),
|
||||
message: Some(&b"There is no script by that name"[..]),
|
||||
}
|
||||
))
|
||||
);
|
||||
let response_nobye = b"No (NONEXISTENT) {31}\r\nThere is no script by that name\r\n";
|
||||
assert_eq!(
|
||||
response_oknobye(response_nobye),
|
||||
Ok((
|
||||
&b""[..],
|
||||
ManageSieveResponse::NoBye {
|
||||
code: Some(&b"NONEXISTENT"[..]),
|
||||
message: Some(&b"There is no script by that name"[..]),
|
||||
}
|
||||
))
|
||||
);
|
||||
|
||||
let response_nobye = b"No\r\n";
|
||||
assert_eq!(
|
||||
response_oknobye(response_nobye),
|
||||
Ok((
|
||||
&b""[..],
|
||||
ManageSieveResponse::NoBye {
|
||||
code: None,
|
||||
message: None,
|
||||
}
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -21,13 +21,13 @@
|
|||
|
||||
use std::sync::Arc;
|
||||
|
||||
use imap_codec::imap_types::fetch::MessageDataItemName;
|
||||
use imap_codec::fetch::MessageDataItemName;
|
||||
|
||||
use super::*;
|
||||
use crate::{backends::*, error::Error};
|
||||
use crate::{backends::*, email::*, error::Error};
|
||||
|
||||
/// `BackendOp` implementor for Imap
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ImapOp {
|
||||
uid: UID,
|
||||
mailbox_hash: MailboxHash,
|
||||
|
@ -114,4 +114,65 @@ impl BackendOp for ImapOp {
|
|||
Ok(ret)
|
||||
}))
|
||||
}
|
||||
|
||||
fn fetch_flags(&self) -> ResultFuture<Flag> {
|
||||
let mut response = Vec::with_capacity(8 * 1024);
|
||||
let connection = self.connection.clone();
|
||||
let mailbox_hash = self.mailbox_hash;
|
||||
let uid = self.uid;
|
||||
let uid_store = self.uid_store.clone();
|
||||
|
||||
Ok(Box::pin(async move {
|
||||
let exists_in_cache = {
|
||||
let mut bytes_cache = uid_store.byte_cache.lock()?;
|
||||
let cache = bytes_cache.entry(uid).or_default();
|
||||
cache.flags.is_some()
|
||||
};
|
||||
if !exists_in_cache {
|
||||
let mut conn = connection.lock().await;
|
||||
conn.connect().await?;
|
||||
conn.examine_mailbox(mailbox_hash, &mut response, false)
|
||||
.await?;
|
||||
conn.send_command(CommandBody::fetch(
|
||||
uid,
|
||||
vec![MessageDataItemName::Flags],
|
||||
true,
|
||||
)?)
|
||||
.await?;
|
||||
conn.read_response(&mut response, RequiredResponses::FETCH_REQUIRED)
|
||||
.await?;
|
||||
debug!(
|
||||
"fetch response is {} bytes and {} lines",
|
||||
response.len(),
|
||||
String::from_utf8_lossy(&response).lines().count()
|
||||
);
|
||||
let v = protocol_parser::uid_fetch_flags_responses(&response)
|
||||
.map(|(_, v)| v)
|
||||
.map_err(Error::from)?;
|
||||
if v.len() != 1 {
|
||||
debug!("responses len is {}", v.len());
|
||||
debug!(String::from_utf8_lossy(&response));
|
||||
/* TODO: Trigger cache invalidation here. */
|
||||
debug!("message with UID {} was not found", uid);
|
||||
return Err(
|
||||
Error::new(format!("Invalid/unexpected response: {:?}", response))
|
||||
.set_summary(format!("message with UID {} was not found?", uid)),
|
||||
);
|
||||
}
|
||||
let (_uid, (_flags, _)) = v[0];
|
||||
assert_eq!(_uid, uid);
|
||||
let mut bytes_cache = uid_store.byte_cache.lock()?;
|
||||
let cache = bytes_cache.entry(uid).or_default();
|
||||
cache.flags = Some(_flags);
|
||||
}
|
||||
{
|
||||
let val = {
|
||||
let mut bytes_cache = uid_store.byte_cache.lock()?;
|
||||
let cache = bytes_cache.entry(uid).or_default();
|
||||
cache.flags
|
||||
};
|
||||
Ok(val.unwrap())
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
|
@ -42,14 +42,10 @@ use crate::{
|
|||
},
|
||||
},
|
||||
error::ResultIntoError,
|
||||
text::Truncate,
|
||||
utils::parsec::CRLF,
|
||||
};
|
||||
|
||||
const UNTAGGED_PREFIX: &[u8] = b"* ";
|
||||
|
||||
bitflags! {
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
pub struct RequiredResponses: u64 {
|
||||
const CAPABILITY = 0b0000_0000_0000_0001;
|
||||
const BYE = 0b0000_0000_0000_0010;
|
||||
|
@ -67,22 +63,22 @@ bitflags! {
|
|||
const SEARCH = 0b0010_0000_0000_0000;
|
||||
const FETCH = 0b0100_0000_0000_0000;
|
||||
const NO_REQUIRED = 0b1000_0000_0000_0000;
|
||||
const CAPABILITY_REQUIRED = Self::CAPABILITY.bits();
|
||||
const LOGOUT_REQUIRED = Self::BYE.bits();
|
||||
const SELECT_REQUIRED = Self::FLAGS.bits() | Self::EXISTS.bits() | Self::RECENT.bits() | Self::UNSEEN.bits() | Self::PERMANENTFLAGS.bits() | Self::UIDNEXT.bits() | Self::UIDVALIDITY.bits();
|
||||
const EXAMINE_REQUIRED = Self::FLAGS.bits() | Self::EXISTS.bits() | Self::RECENT.bits() | Self::UNSEEN.bits() | Self::PERMANENTFLAGS.bits() | Self::UIDNEXT.bits() | Self::UIDVALIDITY.bits();
|
||||
const LIST_REQUIRED = Self::LIST.bits();
|
||||
const LSUB_REQUIRED = Self::LSUB.bits();
|
||||
const FETCH_REQUIRED = Self::FETCH.bits();
|
||||
const CAPABILITY_REQUIRED = Self::CAPABILITY.bits;
|
||||
const LOGOUT_REQUIRED = Self::BYE.bits;
|
||||
const SELECT_REQUIRED = Self::FLAGS.bits | Self::EXISTS.bits | Self::RECENT.bits | Self::UNSEEN.bits | Self::PERMANENTFLAGS.bits | Self::UIDNEXT.bits | Self::UIDVALIDITY.bits;
|
||||
const EXAMINE_REQUIRED = Self::FLAGS.bits | Self::EXISTS.bits | Self::RECENT.bits | Self::UNSEEN.bits | Self::PERMANENTFLAGS.bits | Self::UIDNEXT.bits | Self::UIDVALIDITY.bits;
|
||||
const LIST_REQUIRED = Self::LIST.bits;
|
||||
const LSUB_REQUIRED = Self::LSUB.bits;
|
||||
const FETCH_REQUIRED = Self::FETCH.bits;
|
||||
}
|
||||
}
|
||||
|
||||
impl RequiredResponses {
|
||||
pub fn check(&self, line: &[u8]) -> bool {
|
||||
if !line.starts_with(UNTAGGED_PREFIX) {
|
||||
if !line.starts_with(b"* ") {
|
||||
return false;
|
||||
}
|
||||
let line = &line[UNTAGGED_PREFIX.len()..];
|
||||
let line = &line[b"* ".len()..];
|
||||
let mut ret = false;
|
||||
if self.intersects(Self::CAPABILITY) {
|
||||
ret |= line.starts_with(b"CAPABILITY");
|
||||
|
@ -360,7 +356,7 @@ impl<'a> Iterator for ImapLineIterator<'a> {
|
|||
let mut i = 0;
|
||||
loop {
|
||||
let cur_slice = &self.slice[i..];
|
||||
if let Some(pos) = cur_slice.find(CRLF) {
|
||||
if let Some(pos) = cur_slice.find(b"\r\n") {
|
||||
/* Skip literal continuation line */
|
||||
if cur_slice.get(pos.saturating_sub(1)) == Some(&b'}') {
|
||||
i += pos + 2;
|
||||
|
@ -428,7 +424,7 @@ pub fn list_mailbox_result(input: &[u8]) -> IResult<&[u8], ImapMailbox> {
|
|||
let (input, separator) = delimited(tag(b"\""), take(1_u32), tag(b"\""))(input)?;
|
||||
let (input, _) = take(1_u32)(input)?;
|
||||
let (input, path) = mailbox_token(input)?;
|
||||
let (input, _) = tag(CRLF)(input)?;
|
||||
let (input, _) = tag("\r\n")(input)?;
|
||||
Ok((
|
||||
input,
|
||||
({
|
||||
|
@ -480,12 +476,12 @@ pub fn list_mailbox_result(input: &[u8]) -> IResult<&[u8], ImapMailbox> {
|
|||
};
|
||||
f.separator = separator;
|
||||
|
||||
f
|
||||
debug!(f)
|
||||
}),
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct FetchResponse<'a> {
|
||||
pub uid: Option<UID>,
|
||||
pub message_sequence_number: MessageSequenceNumber,
|
||||
|
@ -499,7 +495,7 @@ pub struct FetchResponse<'a> {
|
|||
|
||||
pub fn fetch_response(input: &[u8]) -> ImapParseResult<FetchResponse<'_>> {
|
||||
macro_rules! should_start_with {
|
||||
($input:expr, $tag:tt) => {
|
||||
($input:expr, $tag:literal) => {
|
||||
if !$input.starts_with($tag) {
|
||||
return Err(Error::new(format!(
|
||||
"Expected `{}` but got `{:.50}`",
|
||||
|
@ -509,9 +505,9 @@ pub fn fetch_response(input: &[u8]) -> ImapParseResult<FetchResponse<'_>> {
|
|||
}
|
||||
};
|
||||
}
|
||||
should_start_with!(input, UNTAGGED_PREFIX);
|
||||
should_start_with!(input, b"* ");
|
||||
|
||||
let mut i = UNTAGGED_PREFIX.len();
|
||||
let mut i = b"* ".len();
|
||||
macro_rules! bounds {
|
||||
() => {
|
||||
if i == input.len() {
|
||||
|
@ -579,14 +575,10 @@ pub fn fetch_response(input: &[u8]) -> ImapParseResult<FetchResponse<'_>> {
|
|||
ret.uid =
|
||||
Some(UID::from_str(unsafe { std::str::from_utf8_unchecked(uid) }).unwrap());
|
||||
} else {
|
||||
log::debug!(
|
||||
"Unexpected input while parsing UID FETCH response. Got: `{}`",
|
||||
return debug!(Err(Error::new(format!(
|
||||
"Unexpected input while parsing UID FETCH response. Got: `{:.40}`",
|
||||
String::from_utf8_lossy(input)
|
||||
);
|
||||
return Err(Error::new(format!(
|
||||
"Unexpected input while parsing UID FETCH response. Got: `{}`",
|
||||
String::from_utf8_lossy(input).as_ref().trim_at_boundary(40)
|
||||
)));
|
||||
))));
|
||||
}
|
||||
} else if input[i..].starts_with(b"FLAGS (") {
|
||||
i += b"FLAGS (".len();
|
||||
|
@ -594,17 +586,11 @@ pub fn fetch_response(input: &[u8]) -> ImapParseResult<FetchResponse<'_>> {
|
|||
ret.flags = Some(flags);
|
||||
i += (input.len() - i - rest.len()) + 1;
|
||||
} else {
|
||||
log::debug!(
|
||||
"Unexpected input while parsing UID FETCH response. Could not parse FLAGS: {}.",
|
||||
String::from_utf8_lossy(&input[i..])
|
||||
);
|
||||
return Err(Error::new(format!(
|
||||
return debug!(Err(Error::new(format!(
|
||||
"Unexpected input while parsing UID FETCH response. Could not parse FLAGS: \
|
||||
`{}`.",
|
||||
{:.40}.",
|
||||
String::from_utf8_lossy(&input[i..])
|
||||
.as_ref()
|
||||
.trim_at_boundary(40)
|
||||
)));
|
||||
))));
|
||||
}
|
||||
} else if input[i..].starts_with(b"MODSEQ (") {
|
||||
i += b"MODSEQ (".len();
|
||||
|
@ -617,14 +603,10 @@ pub fn fetch_response(input: &[u8]) -> ImapParseResult<FetchResponse<'_>> {
|
|||
.and_then(std::num::NonZeroU64::new)
|
||||
.map(ModSequence);
|
||||
} else {
|
||||
log::debug!(
|
||||
"Unexpected input while parsing MODSEQ in UID FETCH response. Got: `{}`",
|
||||
return debug!(Err(Error::new(format!(
|
||||
"Unexpected input while parsing MODSEQ in UID FETCH response. Got: `{:.40}`",
|
||||
String::from_utf8_lossy(input)
|
||||
);
|
||||
return Err(Error::new(format!(
|
||||
"Unexpected input while parsing MODSEQ in UID FETCH response. Got: `{}`",
|
||||
String::from_utf8_lossy(input).as_ref().trim_at_boundary(40)
|
||||
)));
|
||||
))));
|
||||
}
|
||||
} else if input[i..].starts_with(b"RFC822 {") {
|
||||
i += b"RFC822 ".len();
|
||||
|
@ -640,16 +622,11 @@ pub fn fetch_response(input: &[u8]) -> ImapParseResult<FetchResponse<'_>> {
|
|||
ret.body = Some(body);
|
||||
i += input.len() - i - rest.len();
|
||||
} else {
|
||||
log::debug!(
|
||||
"Unexpected input while parsing UID FETCH response. Could not parse RFC822: {}",
|
||||
return debug!(Err(Error::new(format!(
|
||||
"Unexpected input while parsing UID FETCH response. Could not parse RFC822: \
|
||||
{:.40}",
|
||||
String::from_utf8_lossy(&input[i..])
|
||||
);
|
||||
return Err(Error::new(format!(
|
||||
"Unexpected input while parsing UID FETCH response. Could not parse RFC822: {}",
|
||||
String::from_utf8_lossy(&input[i..])
|
||||
.as_ref()
|
||||
.trim_at_boundary(40)
|
||||
)));
|
||||
))));
|
||||
}
|
||||
} else if input[i..].starts_with(b"ENVELOPE (") {
|
||||
i += b"ENVELOPE ".len();
|
||||
|
@ -657,18 +634,11 @@ pub fn fetch_response(input: &[u8]) -> ImapParseResult<FetchResponse<'_>> {
|
|||
ret.envelope = Some(envelope);
|
||||
i += input.len() - i - rest.len();
|
||||
} else {
|
||||
log::debug!(
|
||||
return debug!(Err(Error::new(format!(
|
||||
"Unexpected input while parsing UID FETCH response. Could not parse ENVELOPE: \
|
||||
{}",
|
||||
{:.40}",
|
||||
String::from_utf8_lossy(&input[i..])
|
||||
);
|
||||
return Err(Error::new(format!(
|
||||
"Unexpected input while parsing UID FETCH response. Could not parse ENVELOPE: \
|
||||
{}",
|
||||
String::from_utf8_lossy(&input[i..])
|
||||
.as_ref()
|
||||
.trim_at_boundary(40)
|
||||
)));
|
||||
))));
|
||||
}
|
||||
} else if input[i..].starts_with(b"BODYSTRUCTURE ") {
|
||||
i += b"BODYSTRUCTURE ".len();
|
||||
|
@ -687,18 +657,11 @@ pub fn fetch_response(input: &[u8]) -> ImapParseResult<FetchResponse<'_>> {
|
|||
}
|
||||
i += input.len() - i - rest.len();
|
||||
} else {
|
||||
log::debug!(
|
||||
return debug!(Err(Error::new(format!(
|
||||
"Unexpected input while parsing UID FETCH response. Could not parse \
|
||||
BODY[HEADER.FIELDS (REFERENCES)]: {}",
|
||||
BODY[HEADER.FIELDS (REFERENCES)]: {:.40}",
|
||||
String::from_utf8_lossy(&input[i..])
|
||||
);
|
||||
return Err(Error::new(format!(
|
||||
"Unexpected input while parsing UID FETCH response. Could not parse \
|
||||
BODY[HEADER.FIELDS (REFERENCES)]: {}",
|
||||
String::from_utf8_lossy(&input[i..])
|
||||
.as_ref()
|
||||
.trim_at_boundary(40)
|
||||
)));
|
||||
))));
|
||||
}
|
||||
} else if input[i..].starts_with(b"BODY[HEADER.FIELDS (\"REFERENCES\")] ") {
|
||||
i += b"BODY[HEADER.FIELDS (\"REFERENCES\")] ".len();
|
||||
|
@ -711,33 +674,24 @@ pub fn fetch_response(input: &[u8]) -> ImapParseResult<FetchResponse<'_>> {
|
|||
}
|
||||
i += input.len() - i - rest.len();
|
||||
} else {
|
||||
log::debug!(
|
||||
return debug!(Err(Error::new(format!(
|
||||
"Unexpected input while parsing UID FETCH response. Could not parse \
|
||||
BODY[HEADER.FIELDS (\"REFERENCES\"): {}",
|
||||
BODY[HEADER.FIELDS (\"REFERENCES\"): {:.40}",
|
||||
String::from_utf8_lossy(&input[i..])
|
||||
);
|
||||
return Err(Error::new(format!(
|
||||
"Unexpected input while parsing UID FETCH response. Could not parse \
|
||||
BODY[HEADER.FIELDS (\"REFERENCES\"): {}",
|
||||
String::from_utf8_lossy(&input[i..])
|
||||
.as_ref()
|
||||
.trim_at_boundary(40)
|
||||
)));
|
||||
))));
|
||||
}
|
||||
} else if input[i..].starts_with(b")\r\n") {
|
||||
i += b")\r\n".len();
|
||||
break;
|
||||
} else {
|
||||
log::debug!(
|
||||
debug!(
|
||||
"Got unexpected token while parsing UID FETCH response:\n`{}`\n",
|
||||
String::from_utf8_lossy(input)
|
||||
);
|
||||
return Err(Error::new(format!(
|
||||
"Got unexpected token while parsing UID FETCH response: `{}`",
|
||||
return debug!(Err(Error::new(format!(
|
||||
"Got unexpected token while parsing UID FETCH response: `{:.40}`",
|
||||
String::from_utf8_lossy(&input[i..])
|
||||
.as_ref()
|
||||
.trim_at_boundary(40)
|
||||
)));
|
||||
))));
|
||||
}
|
||||
}
|
||||
ret.raw_fetch_value = &input[..i];
|
||||
|
@ -753,7 +707,7 @@ pub fn fetch_responses(mut input: &[u8]) -> ImapParseResult<Vec<FetchResponse<'_
|
|||
let mut ret = Vec::new();
|
||||
let mut alert: Option<Alert> = None;
|
||||
|
||||
while input.starts_with(UNTAGGED_PREFIX) {
|
||||
while input.starts_with(b"* ") {
|
||||
let next_response = fetch_response(input);
|
||||
match next_response {
|
||||
Ok((rest, el, el_alert)) => {
|
||||
|
@ -830,8 +784,8 @@ pub fn capabilities(input: &[u8]) -> IResult<&[u8], Vec<&[u8]>> {
|
|||
let (input, _) = take_until("CAPABILITY ")(input)?;
|
||||
let (input, _) = tag("CAPABILITY ")(input)?;
|
||||
let (input, ret) = separated_list1(tag(" "), is_not(" ]\r\n"))(input)?;
|
||||
let (input, _) = take_until(CRLF)(input)?;
|
||||
let (input, _) = tag(CRLF)(input)?;
|
||||
let (input, _) = take_until("\r\n")(input)?;
|
||||
let (input, _) = tag("\r\n")(input)?;
|
||||
Ok((input, ret))
|
||||
}
|
||||
|
||||
|
@ -906,14 +860,15 @@ pub enum UntaggedResponse<'s> {
|
|||
|
||||
pub fn untagged_responses(input: &[u8]) -> ImapParseResult<Option<UntaggedResponse<'_>>> {
|
||||
let orig_input = input;
|
||||
let (input, _) = tag::<_, &[u8], (&[u8], nom::error::ErrorKind)>(UNTAGGED_PREFIX)(input)?;
|
||||
let (input, _) = tag::<_, &[u8], (&[u8], nom::error::ErrorKind)>(b"* ")(input)?;
|
||||
let (input, num) = map_res::<_, _, _, (&[u8], nom::error::ErrorKind), _, _, _>(digit1, |s| {
|
||||
ImapNum::from_str(unsafe { std::str::from_utf8_unchecked(s) })
|
||||
})(input)?;
|
||||
let (input, _) = tag::<_, &[u8], (&[u8], nom::error::ErrorKind)>(b" ")(input)?;
|
||||
let (input, _tag) = take_until::<_, &[u8], (&[u8], nom::error::ErrorKind)>(CRLF)(input)?;
|
||||
let (input, _) = tag::<_, &[u8], (&[u8], nom::error::ErrorKind)>(CRLF)(input)?;
|
||||
log::trace!(
|
||||
let (input, _tag) =
|
||||
take_until::<_, &[u8], (&[u8], nom::error::ErrorKind)>(&b"\r\n"[..])(input)?;
|
||||
let (input, _) = tag::<_, &[u8], (&[u8], nom::error::ErrorKind)>(b"\r\n")(input)?;
|
||||
debug!(
|
||||
"Parse untagged response from {:?}",
|
||||
String::from_utf8_lossy(orig_input)
|
||||
);
|
||||
|
@ -927,10 +882,9 @@ pub fn untagged_responses(input: &[u8]) -> ImapParseResult<Option<UntaggedRespon
|
|||
b"RECENT" => Some(Recent(num)),
|
||||
_ if _tag.starts_with(b"FETCH ") => Some(Fetch(fetch_response(orig_input)?.1)),
|
||||
_ => {
|
||||
log::error!(
|
||||
"unknown untagged_response: {}, message was {:?}",
|
||||
String::from_utf8_lossy(_tag),
|
||||
String::from_utf8_lossy(orig_input)
|
||||
debug!(
|
||||
"unknown untagged_response: {}",
|
||||
String::from_utf8_lossy(_tag)
|
||||
);
|
||||
None
|
||||
}
|
||||
|
@ -950,7 +904,7 @@ pub fn search_results<'a>(input: &'a [u8]) -> IResult<&'a [u8], Vec<ImapNum>> {
|
|||
ImapNum::from_str(unsafe { std::str::from_utf8_unchecked(s) })
|
||||
}),
|
||||
)(input)?;
|
||||
let (input, _) = tag(CRLF)(input)?;
|
||||
let (input, _) = tag("\r\n")(input)?;
|
||||
Ok((input, list))
|
||||
},
|
||||
|input: &'a [u8]| -> IResult<&'a [u8], Vec<ImapNum>> {
|
||||
|
@ -964,8 +918,8 @@ pub fn search_results_raw<'a>(input: &'a [u8]) -> IResult<&'a [u8], &'a [u8]> {
|
|||
alt((
|
||||
|input: &'a [u8]| -> IResult<&'a [u8], &'a [u8]> {
|
||||
let (input, _) = tag("* SEARCH ")(input)?;
|
||||
let (input, list) = take_until(CRLF)(input)?;
|
||||
let (input, _) = tag(CRLF)(input)?;
|
||||
let (input, list) = take_until("\r\n")(input)?;
|
||||
let (input, _) = tag("\r\n")(input)?;
|
||||
Ok((input, list))
|
||||
},
|
||||
|input: &'a [u8]| -> IResult<&'a [u8], &'a [u8]> {
|
||||
|
@ -975,7 +929,7 @@ pub fn search_results_raw<'a>(input: &'a [u8]) -> IResult<&'a [u8], &'a [u8]> {
|
|||
))(input)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
#[derive(Debug, Default, Eq, PartialEq, Clone)]
|
||||
pub struct SelectResponse {
|
||||
pub exists: ImapNum,
|
||||
pub recent: ImapNum,
|
||||
|
@ -984,7 +938,7 @@ pub struct SelectResponse {
|
|||
pub uidvalidity: UIDVALIDITY,
|
||||
pub uidnext: UID,
|
||||
pub permanentflags: (Flag, Vec<String>),
|
||||
/// if SELECT returns \* we can set arbitrary flags permanently.
|
||||
/// if SELECT returns \* we can set arbritary flags permanently.
|
||||
pub can_create_flags: bool,
|
||||
pub read_only: bool,
|
||||
pub highestmodseq: Option<std::result::Result<ModSequence, ()>>,
|
||||
|
@ -1017,13 +971,13 @@ pub fn select_response(input: &[u8]) -> Result<SelectResponse> {
|
|||
if input.contains_subsequence(b"* OK") {
|
||||
let mut ret = SelectResponse::default();
|
||||
for l in input.split_rn() {
|
||||
if l.starts_with(UNTAGGED_PREFIX) && l.ends_with(b" EXISTS\r\n") {
|
||||
if l.starts_with(b"* ") && l.ends_with(b" EXISTS\r\n") {
|
||||
ret.exists = ImapNum::from_str(&String::from_utf8_lossy(
|
||||
&l[UNTAGGED_PREFIX.len()..l.len() - b" EXISTS\r\n".len()],
|
||||
&l[b"* ".len()..l.len() - b" EXISTS\r\n".len()],
|
||||
))?;
|
||||
} else if l.starts_with(UNTAGGED_PREFIX) && l.ends_with(b" RECENT\r\n") {
|
||||
} else if l.starts_with(b"* ") && l.ends_with(b" RECENT\r\n") {
|
||||
ret.recent = ImapNum::from_str(&String::from_utf8_lossy(
|
||||
&l[UNTAGGED_PREFIX.len()..l.len() - b" RECENT\r\n".len()],
|
||||
&l[b"* ".len()..l.len() - b" RECENT\r\n".len()],
|
||||
))?;
|
||||
} else if l.starts_with(b"* FLAGS (") {
|
||||
ret.flags = flags(&l[b"* FLAGS (".len()..l.len() - b")".len()]).map(|(_, v)| v)?;
|
||||
|
@ -1062,13 +1016,13 @@ pub fn select_response(input: &[u8]) -> Result<SelectResponse> {
|
|||
} else if l.starts_with(b"* OK [NOMODSEQ") {
|
||||
ret.highestmodseq = Some(Err(()));
|
||||
} else if !l.is_empty() {
|
||||
log::trace!("select response: {}", String::from_utf8_lossy(l));
|
||||
debug!("select response: {}", String::from_utf8_lossy(l));
|
||||
}
|
||||
}
|
||||
Ok(ret)
|
||||
} else {
|
||||
let ret = String::from_utf8_lossy(input).to_string();
|
||||
log::error!("BAD/NO response in select: {}", &ret);
|
||||
debug!("BAD/NO response in select: {}", &ret);
|
||||
Err(Error::new(ret))
|
||||
}
|
||||
}
|
||||
|
@ -1156,29 +1110,28 @@ pub fn byte_flags(input: &[u8]) -> IResult<&[u8], (Flag, Vec<String>)> {
|
|||
*/
|
||||
|
||||
pub fn envelope(input: &[u8]) -> IResult<&[u8], Envelope> {
|
||||
const WS: &[u8] = b"\r\n\t ";
|
||||
let (input, _) = tag("(")(input)?;
|
||||
let (input, _) = opt(is_a(WS))(input)?;
|
||||
let (input, _) = opt(is_a("\r\n\t "))(input)?;
|
||||
let (input, date) = quoted_or_nil(input)?;
|
||||
let (input, _) = opt(is_a(WS))(input)?;
|
||||
let (input, _) = opt(is_a("\r\n\t "))(input)?;
|
||||
let (input, subject) = quoted_or_nil(input)?;
|
||||
let (input, _) = opt(is_a(WS))(input)?;
|
||||
let (input, _) = opt(is_a("\r\n\t "))(input)?;
|
||||
let (input, from) = envelope_addresses(input)?;
|
||||
let (input, _) = opt(is_a(WS))(input)?;
|
||||
let (input, _) = opt(is_a("\r\n\t "))(input)?;
|
||||
let (input, _sender) = envelope_addresses(input)?;
|
||||
let (input, _) = opt(is_a(WS))(input)?;
|
||||
let (input, _) = opt(is_a("\r\n\t "))(input)?;
|
||||
let (input, _reply_to) = envelope_addresses(input)?;
|
||||
let (input, _) = opt(is_a(WS))(input)?;
|
||||
let (input, _) = opt(is_a("\r\n\t "))(input)?;
|
||||
let (input, to) = envelope_addresses(input)?;
|
||||
let (input, _) = opt(is_a(WS))(input)?;
|
||||
let (input, _) = opt(is_a("\r\n\t "))(input)?;
|
||||
let (input, cc) = envelope_addresses(input)?;
|
||||
let (input, _) = opt(is_a(WS))(input)?;
|
||||
let (input, _) = opt(is_a("\r\n\t "))(input)?;
|
||||
let (input, bcc) = envelope_addresses(input)?;
|
||||
let (input, _) = opt(is_a(WS))(input)?;
|
||||
let (input, _) = opt(is_a("\r\n\t "))(input)?;
|
||||
let (input, in_reply_to) = quoted_or_nil(input)?;
|
||||
let (input, _) = opt(is_a(WS))(input)?;
|
||||
let (input, _) = opt(is_a("\r\n\t "))(input)?;
|
||||
let (input, message_id) = quoted_or_nil(input)?;
|
||||
let (input, _) = opt(is_a(WS))(input)?;
|
||||
let (input, _) = opt(is_a("\r\n\t "))(input)?;
|
||||
let (input, _) = tag(")")(input)?;
|
||||
Ok((
|
||||
input,
|
||||
|
@ -1262,14 +1215,13 @@ pub fn envelope_addresses<'a>(
|
|||
// Parse an address in the format of the ENVELOPE structure eg
|
||||
// ("Terry Gray" NIL "gray" "cac.washington.edu")
|
||||
pub fn envelope_address(input: &[u8]) -> IResult<&[u8], Address> {
|
||||
const WS: &[u8] = b"\r\n\t ";
|
||||
let (input, name) = alt((quoted, map(tag("NIL"), |_| Vec::new())))(input)?;
|
||||
let (input, _) = is_a(WS)(input)?;
|
||||
let (input, _) = is_a("\r\n\t ")(input)?;
|
||||
let (input, _) = alt((quoted, map(tag("NIL"), |_| Vec::new())))(input)?;
|
||||
let (input, _) = is_a(WS)(input)?;
|
||||
let (input, _) = is_a("\r\n\t ")(input)?;
|
||||
let (input, mailbox_name) = alt((quoted, map(tag("NIL"), |_| Vec::new())))(input)?;
|
||||
let (input, host_name) = opt(preceded(
|
||||
is_a(WS),
|
||||
is_a("\r\n\t "),
|
||||
alt((quoted, map(tag("NIL"), |_| Vec::new()))),
|
||||
))(input)?;
|
||||
Ok((
|
||||
|
@ -1417,7 +1369,7 @@ fn eat_whitespace(mut input: &[u8]) -> IResult<&[u8], ()> {
|
|||
while !input.is_empty() {
|
||||
if input[0] == b' ' || input[0] == b'\n' || input[0] == b'\t' {
|
||||
input = &input[1..];
|
||||
} else if input.starts_with(CRLF) {
|
||||
} else if input.starts_with(b"\r\n") {
|
||||
input = &input[2..];
|
||||
} else {
|
||||
break;
|
||||
|
@ -1426,7 +1378,7 @@ fn eat_whitespace(mut input: &[u8]) -> IResult<&[u8], ()> {
|
|||
Ok((input, ()))
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct StatusResponse {
|
||||
pub mailbox: Option<MailboxHash>,
|
||||
pub messages: Option<ImapNum>,
|
||||
|
@ -1636,6 +1588,7 @@ mod tests {
|
|||
let response =
|
||||
&b"* 1040 FETCH (UID 1064 FLAGS ())\r\nM15 OK Fetch completed (0.001 + 0.299 secs).\r\n"[..];
|
||||
for l in response.split_rn() {
|
||||
/* debug!("check line: {}", &l); */
|
||||
if required_responses.check(l) {
|
||||
ret.extend_from_slice(l);
|
||||
}
|
|
@ -25,7 +25,7 @@ use std::collections::VecDeque;
|
|||
|
||||
use crate::{
|
||||
search::*,
|
||||
utils::datetime::{formats::IMAP_DATE, timestamp_to_string_utc},
|
||||
utils::datetime::{formats::IMAP_DATE, timestamp_to_string},
|
||||
};
|
||||
|
||||
mod private {
|
||||
|
@ -165,25 +165,25 @@ impl ToImapSearch for Query {
|
|||
Q(Before(t)) => {
|
||||
space_pad!(s);
|
||||
s.push_str("BEFORE ");
|
||||
s.push_str(×tamp_to_string_utc(*t, Some(IMAP_DATE), true));
|
||||
s.push_str(×tamp_to_string(*t, Some(IMAP_DATE), true));
|
||||
}
|
||||
Q(After(t)) => {
|
||||
space_pad!(s);
|
||||
s.push_str("SINCE ");
|
||||
s.push_str(×tamp_to_string_utc(*t, Some(IMAP_DATE), true));
|
||||
s.push_str(×tamp_to_string(*t, Some(IMAP_DATE), true));
|
||||
}
|
||||
Q(Between(t1, t2)) => {
|
||||
space_pad!(s);
|
||||
s.push_str("(SINCE ");
|
||||
s.push_str(×tamp_to_string_utc(*t1, Some(IMAP_DATE), true));
|
||||
s.push_str(×tamp_to_string(*t1, Some(IMAP_DATE), true));
|
||||
s.push_str(" BEFORE ");
|
||||
s.push_str(×tamp_to_string_utc(*t2, Some(IMAP_DATE), true));
|
||||
s.push_str(×tamp_to_string(*t2, Some(IMAP_DATE), true));
|
||||
s.push(')');
|
||||
}
|
||||
Q(On(t)) => {
|
||||
space_pad!(s);
|
||||
s.push_str("ON ");
|
||||
s.push_str(×tamp_to_string_utc(*t, Some(IMAP_DATE), true));
|
||||
s.push_str(×tamp_to_string(*t, Some(IMAP_DATE), true));
|
||||
}
|
||||
Q(InReplyTo(t)) => {
|
||||
space_pad!(s);
|
||||
|
@ -281,8 +281,8 @@ mod tests {
|
|||
);
|
||||
|
||||
assert_eq!(
|
||||
×tamp_to_string_utc(1685739600, Some(IMAP_DATE), true),
|
||||
"02-Jun-2023"
|
||||
×tamp_to_string(1685739600, Some(IMAP_DATE), true),
|
||||
"03-Jun-2023"
|
||||
);
|
||||
|
||||
let (_, q) = query()
|
|
@ -21,23 +21,21 @@
|
|||
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
|
||||
use imap_codec::imap_types::{command::CommandBody, search::SearchKey, sequence::SequenceSet};
|
||||
use imap_codec::{command::CommandBody, search::SearchKey, sequence::SequenceSet};
|
||||
|
||||
use super::{ImapConnection, MailboxSelection, UID};
|
||||
use crate::{
|
||||
backends::{
|
||||
imap::protocol_parser::{
|
||||
generate_envelope_hash, FetchResponse, ImapLineSplit, RequiredResponses,
|
||||
UntaggedResponse,
|
||||
},
|
||||
BackendMailbox, RefreshEvent,
|
||||
RefreshEventKind::{self, *},
|
||||
TagHash,
|
||||
},
|
||||
email::common_attributes,
|
||||
error::*,
|
||||
imap::{
|
||||
protocol_parser::{
|
||||
generate_envelope_hash, FetchResponse, ImapLineSplit, RequiredResponses,
|
||||
UntaggedResponse,
|
||||
},
|
||||
ImapConnection, MailboxSelection, UID,
|
||||
},
|
||||
};
|
||||
|
||||
impl ImapConnection {
|
||||
|
@ -46,8 +44,7 @@ impl ImapConnection {
|
|||
($mailbox_hash: expr, $($result:expr $(,)*)+) => {
|
||||
$(if let Err(err) = $result {
|
||||
self.uid_store.is_online.lock().unwrap().1 = Err(err.clone());
|
||||
imap_log!(trace, self, "failure: {}", err.to_string());
|
||||
log::debug!("failure: {}", err.to_string());
|
||||
debug!("failure: {}", err.to_string());
|
||||
self.add_refresh_event(RefreshEvent {
|
||||
account_hash: self.uid_store.account_hash,
|
||||
mailbox_hash: $mailbox_hash,
|
||||
|
@ -64,7 +61,10 @@ impl ImapConnection {
|
|||
let mailbox =
|
||||
std::clone::Clone::clone(&self.uid_store.mailboxes.lock().await[&mailbox_hash]);
|
||||
|
||||
let mut cache_handle = self.uid_store.cache_handle();
|
||||
#[cfg(not(feature = "sqlite3"))]
|
||||
let mut cache_handle = super::cache::DefaultCache::get(self.uid_store.clone())?;
|
||||
#[cfg(feature = "sqlite3")]
|
||||
let mut cache_handle = super::cache::Sqlite3Cache::get(self.uid_store.clone())?;
|
||||
let mut response = Vec::with_capacity(8 * 1024);
|
||||
let untagged_response =
|
||||
match super::protocol_parser::untagged_responses(line).map(|(_, v, _)| v) {
|
||||
|
@ -121,46 +121,35 @@ impl ImapConnection {
|
|||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter(|((_, u), _)| !results.contains(u))
|
||||
.filter(|((mailbox_hash_, u), _)| {
|
||||
*mailbox_hash_ == mailbox_hash && !results.contains(u)
|
||||
})
|
||||
.map(|((_, uid), hash)| (*uid, *hash))
|
||||
.collect::<Vec<(UID, crate::email::EnvelopeHash)>>();
|
||||
let mut mboxes_to_update = vec![];
|
||||
for (deleted_uid, deleted_hash) in deleteds {
|
||||
for (mailbox_hash, mbx) in self.uid_store.mailboxes.lock().await.iter_mut()
|
||||
{
|
||||
mbx.exists.lock().unwrap().remove(deleted_hash);
|
||||
mbx.unseen.lock().unwrap().remove(deleted_hash);
|
||||
let mut existed = self
|
||||
.uid_store
|
||||
.uid_index
|
||||
.lock()
|
||||
.unwrap()
|
||||
.remove(&(*mailbox_hash, deleted_uid))
|
||||
.is_some();
|
||||
existed |= self
|
||||
.uid_store
|
||||
.hash_index
|
||||
.lock()
|
||||
.unwrap()
|
||||
.remove(&deleted_hash)
|
||||
.is_some();
|
||||
if existed {
|
||||
mboxes_to_update.push(*mailbox_hash);
|
||||
events.push((
|
||||
deleted_uid,
|
||||
RefreshEvent {
|
||||
account_hash: self.uid_store.account_hash,
|
||||
mailbox_hash: *mailbox_hash,
|
||||
kind: Remove(deleted_hash),
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
mailbox.exists.lock().unwrap().remove(deleted_hash);
|
||||
mailbox.unseen.lock().unwrap().remove(deleted_hash);
|
||||
self.uid_store
|
||||
.uid_index
|
||||
.lock()
|
||||
.unwrap()
|
||||
.remove(&(mailbox_hash, deleted_uid));
|
||||
self.uid_store
|
||||
.hash_index
|
||||
.lock()
|
||||
.unwrap()
|
||||
.remove(&deleted_hash);
|
||||
events.push((
|
||||
deleted_uid,
|
||||
RefreshEvent {
|
||||
account_hash: self.uid_store.account_hash,
|
||||
mailbox_hash,
|
||||
kind: Remove(deleted_hash),
|
||||
},
|
||||
));
|
||||
}
|
||||
if let Ok(Some(ref mut cache_handle)) = cache_handle {
|
||||
for mailbox_hash in mboxes_to_update {
|
||||
cache_handle.update(mailbox_hash, &events)?;
|
||||
}
|
||||
if self.uid_store.keep_offline_cache {
|
||||
cache_handle.update(mailbox_hash, &events)?;
|
||||
}
|
||||
for (_, event) in events {
|
||||
self.add_refresh_event(event);
|
||||
|
@ -176,7 +165,7 @@ impl ImapConnection {
|
|||
.entry(mailbox_hash)
|
||||
.or_default()
|
||||
.remove(TryInto::<usize>::try_into(n).unwrap().saturating_sub(1));
|
||||
imap_log!(trace, self, "expunge {}, UID = {}", n, deleted_uid);
|
||||
debug!("expunge {}, UID = {}", n, deleted_uid);
|
||||
let deleted_hash: crate::email::EnvelopeHash = match self
|
||||
.uid_store
|
||||
.uid_index
|
||||
|
@ -187,44 +176,35 @@ impl ImapConnection {
|
|||
Some(v) => v,
|
||||
None => return Ok(true),
|
||||
};
|
||||
let mut mboxes_to_update = vec![];
|
||||
for (mailbox_hash, mbx) in self.uid_store.mailboxes.lock().await.iter_mut() {
|
||||
if mbx.exists.lock().unwrap().remove(deleted_hash) {
|
||||
mboxes_to_update.push(*mailbox_hash);
|
||||
}
|
||||
mbx.unseen.lock().unwrap().remove(deleted_hash);
|
||||
}
|
||||
mailbox.exists.lock().unwrap().remove(deleted_hash);
|
||||
mailbox.unseen.lock().unwrap().remove(deleted_hash);
|
||||
self.uid_store
|
||||
.hash_index
|
||||
.lock()
|
||||
.unwrap()
|
||||
.remove(&deleted_hash);
|
||||
let events = mboxes_to_update
|
||||
.into_iter()
|
||||
.map(|mailbox_hash| {
|
||||
(
|
||||
mailbox_hash,
|
||||
[(
|
||||
deleted_uid,
|
||||
RefreshEvent {
|
||||
account_hash: self.uid_store.account_hash,
|
||||
mailbox_hash,
|
||||
kind: Remove(deleted_hash),
|
||||
},
|
||||
)],
|
||||
)
|
||||
})
|
||||
.collect::<Vec<(_, [(UID, RefreshEvent); 1])>>();
|
||||
for (mailbox_hash, pair) in events {
|
||||
if let Ok(Some(ref mut cache_handle)) = cache_handle {
|
||||
cache_handle.update(mailbox_hash, &pair)?;
|
||||
}
|
||||
let [(_, event)] = pair;
|
||||
self.add_refresh_event(event);
|
||||
let mut event: [(UID, RefreshEvent); 1] = [(
|
||||
deleted_uid,
|
||||
RefreshEvent {
|
||||
account_hash: self.uid_store.account_hash,
|
||||
mailbox_hash,
|
||||
kind: Remove(deleted_hash),
|
||||
},
|
||||
)];
|
||||
if self.uid_store.keep_offline_cache {
|
||||
cache_handle.update(mailbox_hash, &event)?;
|
||||
}
|
||||
self.add_refresh_event(std::mem::replace(
|
||||
&mut event[0].1,
|
||||
RefreshEvent {
|
||||
account_hash: self.uid_store.account_hash,
|
||||
mailbox_hash,
|
||||
kind: Rescan,
|
||||
},
|
||||
));
|
||||
}
|
||||
UntaggedResponse::Exists(n) => {
|
||||
imap_log!(trace, self, "exists {}", n);
|
||||
debug!("exists {}", n);
|
||||
try_fail!(
|
||||
mailbox_hash,
|
||||
self.send_command(CommandBody::fetch(n, common_attributes(), false)?).await
|
||||
|
@ -233,16 +213,14 @@ impl ImapConnection {
|
|||
let mut v = match super::protocol_parser::fetch_responses(&response) {
|
||||
Ok((_, v, _)) => v,
|
||||
Err(err) => {
|
||||
imap_log!(
|
||||
trace,
|
||||
self,
|
||||
debug!(
|
||||
"Error when parsing FETCH response after untagged exists {:?}",
|
||||
err
|
||||
);
|
||||
return Ok(true);
|
||||
}
|
||||
};
|
||||
imap_log!(trace, self, "responses len is {}", v.len());
|
||||
debug!("responses len is {}", v.len());
|
||||
for FetchResponse {
|
||||
ref uid,
|
||||
ref mut envelope,
|
||||
|
@ -269,7 +247,7 @@ impl ImapConnection {
|
|||
for f in keywords {
|
||||
let hash = TagHash::from_bytes(f.as_bytes());
|
||||
tag_lck.entry(hash).or_insert_with(|| f.to_string());
|
||||
env.tags_mut().insert(hash);
|
||||
env.tags_mut().push(hash);
|
||||
}
|
||||
}
|
||||
mailbox.exists.lock().unwrap().insert_new(env.hash());
|
||||
|
@ -298,16 +276,14 @@ impl ImapConnection {
|
|||
.lock()
|
||||
.unwrap()
|
||||
.insert((mailbox_hash, uid), env.hash());
|
||||
imap_log!(
|
||||
trace,
|
||||
self,
|
||||
debug!(
|
||||
"Create event {} {} {}",
|
||||
env.hash(),
|
||||
env.subject(),
|
||||
mailbox.path(),
|
||||
);
|
||||
}
|
||||
if let Ok(Some(ref mut cache_handle)) = cache_handle {
|
||||
if self.uid_store.keep_offline_cache {
|
||||
if let Err(err) = cache_handle
|
||||
.insert_envelopes(mailbox_hash, &v)
|
||||
.chain_err_summary(|| {
|
||||
|
@ -317,7 +293,7 @@ impl ImapConnection {
|
|||
)
|
||||
})
|
||||
{
|
||||
imap_log!(info, self, "{}", err);
|
||||
log::info!("{err}");
|
||||
}
|
||||
}
|
||||
for response in v {
|
||||
|
@ -345,7 +321,7 @@ impl ImapConnection {
|
|||
.map_err(Error::from)
|
||||
{
|
||||
Ok(&[]) => {
|
||||
imap_log!(trace, self, "UID SEARCH RECENT returned no results");
|
||||
debug!("UID SEARCH RECENT returned no results");
|
||||
}
|
||||
Ok(v) => {
|
||||
let command = {
|
||||
|
@ -370,16 +346,14 @@ impl ImapConnection {
|
|||
let mut v = match super::protocol_parser::fetch_responses(&response) {
|
||||
Ok((_, v, _)) => v,
|
||||
Err(err) => {
|
||||
imap_log!(
|
||||
debug,
|
||||
self,
|
||||
debug!(
|
||||
"Error when parsing FETCH response after untagged recent {:?}",
|
||||
err
|
||||
);
|
||||
return Ok(true);
|
||||
}
|
||||
};
|
||||
imap_log!(trace, self, "responses len is {}", v.len());
|
||||
debug!("responses len is {}", v.len());
|
||||
for FetchResponse {
|
||||
ref uid,
|
||||
ref mut envelope,
|
||||
|
@ -406,12 +380,12 @@ impl ImapConnection {
|
|||
for f in keywords {
|
||||
let hash = TagHash::from_bytes(f.as_bytes());
|
||||
tag_lck.entry(hash).or_insert_with(|| f.to_string());
|
||||
env.tags_mut().insert(hash);
|
||||
env.tags_mut().push(hash);
|
||||
}
|
||||
}
|
||||
mailbox.exists.lock().unwrap().insert_new(env.hash());
|
||||
}
|
||||
if let Ok(Some(ref mut cache_handle)) = cache_handle {
|
||||
if self.uid_store.keep_offline_cache {
|
||||
if let Err(err) = cache_handle
|
||||
.insert_envelopes(mailbox_hash, &v)
|
||||
.chain_err_summary(|| {
|
||||
|
@ -532,15 +506,9 @@ impl ImapConnection {
|
|||
temp
|
||||
} {
|
||||
if !flags.0.intersects(crate::email::Flag::SEEN) {
|
||||
for mbx in self.uid_store.mailboxes.lock().await.values_mut() {
|
||||
if mbx.exists.lock().unwrap().contains(&env_hash) {
|
||||
mbx.unseen.lock().unwrap().insert_new(env_hash);
|
||||
}
|
||||
}
|
||||
mailbox.unseen.lock().unwrap().insert_new(env_hash);
|
||||
} else {
|
||||
for mbx in self.uid_store.mailboxes.lock().await.values_mut() {
|
||||
mbx.unseen.lock().unwrap().remove(env_hash);
|
||||
}
|
||||
mailbox.unseen.lock().unwrap().remove(env_hash);
|
||||
}
|
||||
mailbox.exists.lock().unwrap().insert_new(env_hash);
|
||||
if let Some(modseq) = modseq {
|
||||
|
@ -558,7 +526,7 @@ impl ImapConnection {
|
|||
kind: NewFlags(env_hash, flags),
|
||||
},
|
||||
)];
|
||||
if let Ok(Some(ref mut cache_handle)) = cache_handle {
|
||||
if self.uid_store.keep_offline_cache {
|
||||
cache_handle.update(mailbox_hash, &event)?;
|
||||
}
|
||||
self.add_refresh_event(std::mem::replace(
|
|
@ -20,7 +20,7 @@
|
|||
*/
|
||||
use std::sync::Arc;
|
||||
|
||||
use imap_codec::imap_types::search::SearchKey;
|
||||
use imap_codec::search::SearchKey;
|
||||
|
||||
use super::*;
|
||||
use crate::backends::SpecialUsageMailbox;
|
||||
|
@ -48,7 +48,7 @@ pub async fn poll_with_examine(kit: ImapWatchKit) -> Result<()> {
|
|||
for (_, mailbox) in mailboxes.clone() {
|
||||
examine_updates(mailbox, &mut conn, &uid_store).await?;
|
||||
}
|
||||
//[ref:FIXME]: make sleep duration configurable
|
||||
//FIXME: make sleep duration configurable
|
||||
smol::Timer::after(std::time::Duration::from_secs(3 * 60)).await;
|
||||
}
|
||||
}
|
||||
|
@ -69,7 +69,7 @@ pub async fn idle(kit: ImapWatchKit) -> Result<()> {
|
|||
.await
|
||||
.values()
|
||||
.find(|f| f.parent.is_none() && (f.special_usage() == SpecialUsageMailbox::Inbox))
|
||||
.cloned()
|
||||
.map(std::clone::Clone::clone)
|
||||
{
|
||||
Some(mailbox) => mailbox,
|
||||
None => {
|
||||
|
@ -90,7 +90,11 @@ pub async fn idle(kit: ImapWatchKit) -> Result<()> {
|
|||
|
||||
if let Some(v) = uidvalidities.get(&mailbox_hash) {
|
||||
if *v != select_response.uidvalidity {
|
||||
if let Ok(Some(mut cache_handle)) = uid_store.cache_handle() {
|
||||
if uid_store.keep_offline_cache {
|
||||
#[cfg(not(feature = "sqlite3"))]
|
||||
let mut cache_handle = super::cache::DefaultCache::get(uid_store.clone())?;
|
||||
#[cfg(feature = "sqlite3")]
|
||||
let mut cache_handle = super::cache::Sqlite3Cache::get(uid_store.clone())?;
|
||||
cache_handle.clear(mailbox_hash, &select_response)?;
|
||||
}
|
||||
conn.add_refresh_event(RefreshEvent {
|
||||
|
@ -98,6 +102,11 @@ pub async fn idle(kit: ImapWatchKit) -> Result<()> {
|
|||
mailbox_hash,
|
||||
kind: RefreshEventKind::Rescan,
|
||||
});
|
||||
/*
|
||||
uid_store.uid_index.lock().unwrap().clear();
|
||||
uid_store.hash_index.lock().unwrap().clear();
|
||||
uid_store.byte_cache.lock().unwrap().clear();
|
||||
*/
|
||||
}
|
||||
} else {
|
||||
uidvalidities.insert(mailbox_hash, select_response.uidvalidity);
|
||||
|
@ -209,7 +218,10 @@ pub async fn examine_updates(
|
|||
});
|
||||
}
|
||||
} else {
|
||||
let cache_handle = uid_store.cache_handle();
|
||||
#[cfg(not(feature = "sqlite3"))]
|
||||
let mut cache_handle = super::cache::DefaultCache::get(uid_store.clone())?;
|
||||
#[cfg(feature = "sqlite3")]
|
||||
let mut cache_handle = super::cache::Sqlite3Cache::get(uid_store.clone())?;
|
||||
let mut response = Vec::with_capacity(8 * 1024);
|
||||
let select_response = conn
|
||||
.examine_mailbox(mailbox_hash, &mut response, true)
|
||||
|
@ -220,7 +232,7 @@ pub async fn examine_updates(
|
|||
|
||||
if let Some(v) = uidvalidities.get(&mailbox_hash) {
|
||||
if *v != select_response.uidvalidity {
|
||||
if let Ok(Some(mut cache_handle)) = cache_handle {
|
||||
if uid_store.keep_offline_cache {
|
||||
cache_handle.clear(mailbox_hash, &select_response)?;
|
||||
}
|
||||
conn.add_refresh_event(RefreshEvent {
|
||||
|
@ -249,7 +261,7 @@ pub async fn examine_updates(
|
|||
.iter()
|
||||
.any(|cap| cap.eq_ignore_ascii_case(b"LIST-STATUS"));
|
||||
if has_list_status {
|
||||
// [ref:TODO]: (#222) imap-codec does not support "LIST Command Extensions" currently.
|
||||
// TODO(#222): imap-codec does not support "LIST Command Extensions" currently.
|
||||
conn.send_command_raw(
|
||||
format!(
|
||||
"LIST \"{}\" \"\" RETURN (STATUS (MESSAGES UNSEEN))",
|
||||
|
@ -367,21 +379,19 @@ pub async fn examine_updates(
|
|||
for f in keywords {
|
||||
let hash = TagHash::from_bytes(f.as_bytes());
|
||||
tag_lck.entry(hash).or_insert_with(|| f.to_string());
|
||||
env.tags_mut().insert(hash);
|
||||
env.tags_mut().push(hash);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Ok(Some(mut cache_handle)) = cache_handle {
|
||||
if cache_handle.mailbox_state(mailbox_hash)?.is_some() {
|
||||
cache_handle
|
||||
.insert_envelopes(mailbox_hash, &v)
|
||||
.chain_err_summary(|| {
|
||||
format!(
|
||||
"Could not save envelopes in cache for mailbox {}",
|
||||
mailbox.imap_path()
|
||||
)
|
||||
})?;
|
||||
}
|
||||
if uid_store.keep_offline_cache && cache_handle.mailbox_state(mailbox_hash)?.is_some() {
|
||||
cache_handle
|
||||
.insert_envelopes(mailbox_hash, &v)
|
||||
.chain_err_summary(|| {
|
||||
format!(
|
||||
"Could not save envelopes in cache for mailbox {}",
|
||||
mailbox.imap_path()
|
||||
)
|
||||
})?;
|
||||
}
|
||||
|
||||
for FetchResponse { uid, envelope, .. } in v {
|
|
@ -0,0 +1,420 @@
|
|||
/*
|
||||
* meli - jmap module.
|
||||
*
|
||||
* 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::sync::MutexGuard;
|
||||
|
||||
use isahc::config::Configurable;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct JmapConnection {
|
||||
pub session: Arc<Mutex<JmapSession>>,
|
||||
pub request_no: Arc<Mutex<usize>>,
|
||||
pub client: Arc<HttpClient>,
|
||||
pub server_conf: JmapServerConf,
|
||||
pub store: Arc<Store>,
|
||||
}
|
||||
|
||||
impl JmapConnection {
|
||||
pub fn new(server_conf: &JmapServerConf, store: Arc<Store>) -> Result<Self> {
|
||||
let client = HttpClient::builder()
|
||||
.timeout(std::time::Duration::from_secs(10))
|
||||
.connection_cache_size(8)
|
||||
.connection_cache_ttl(std::time::Duration::from_secs(30 * 60))
|
||||
.default_header("Content-Type", "application/json")
|
||||
.redirect_policy(RedirectPolicy::Limit(10));
|
||||
let client = if server_conf.use_token {
|
||||
client
|
||||
.authentication(isahc::auth::Authentication::none())
|
||||
.default_header(
|
||||
"Authorization",
|
||||
format!("Bearer {}", &server_conf.server_password),
|
||||
)
|
||||
} else {
|
||||
client
|
||||
.authentication(isahc::auth::Authentication::basic())
|
||||
.credentials(isahc::auth::Credentials::new(
|
||||
&server_conf.server_username,
|
||||
&server_conf.server_password,
|
||||
))
|
||||
};
|
||||
let client = client.build()?;
|
||||
let server_conf = server_conf.clone();
|
||||
Ok(Self {
|
||||
session: Arc::new(Mutex::new(Default::default())),
|
||||
request_no: Arc::new(Mutex::new(0)),
|
||||
client: Arc::new(client),
|
||||
server_conf,
|
||||
store,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn connect(&mut self) -> Result<()> {
|
||||
if self.store.online_status.lock().await.1.is_ok() {
|
||||
return Ok(());
|
||||
}
|
||||
let mut jmap_session_resource_url = self.server_conf.server_url.to_string();
|
||||
jmap_session_resource_url.push_str("/.well-known/jmap");
|
||||
|
||||
let mut req = self
|
||||
.client
|
||||
.get_async(&jmap_session_resource_url)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
//*self.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
|
||||
Error::new(format!(
|
||||
"Could not connect to JMAP server endpoint for {}. Is your server url setting \
|
||||
correct? (i.e. \"jmap.mailserver.org\") (Note: only session resource \
|
||||
discovery via /.well-known/jmap is supported. DNS SRV records are not \
|
||||
suppported.)\nError connecting to server: {}",
|
||||
&self.server_conf.server_url, &err
|
||||
))
|
||||
.set_source(Some(Arc::new(err)))
|
||||
})?;
|
||||
|
||||
if !req.status().is_success() {
|
||||
let kind: crate::error::NetworkErrorKind = req.status().into();
|
||||
let res_text = req.text().await.unwrap_or_default();
|
||||
let err = Error::new(format!(
|
||||
"Could not connect to JMAP server endpoint for {}. Reply from server: {}",
|
||||
&self.server_conf.server_url, res_text
|
||||
))
|
||||
.set_kind(kind.into());
|
||||
*self.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
let res_text = req.text().await?;
|
||||
|
||||
let session: JmapSession = match serde_json::from_str(&res_text) {
|
||||
Err(err) => {
|
||||
let err = Error::new(format!(
|
||||
"Could not connect to JMAP server endpoint for {}. Is your server url setting \
|
||||
correct? (i.e. \"jmap.mailserver.org\") (Note: only session resource \
|
||||
discovery via /.well-known/jmap is supported. DNS SRV records are not \
|
||||
suppported.)\nReply from server: {}",
|
||||
&self.server_conf.server_url, &res_text
|
||||
))
|
||||
.set_source(Some(Arc::new(err)));
|
||||
*self.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
|
||||
return Err(err);
|
||||
}
|
||||
Ok(s) => s,
|
||||
};
|
||||
if !session
|
||||
.capabilities
|
||||
.contains_key("urn:ietf:params:jmap:core")
|
||||
{
|
||||
let err = Error::new(format!(
|
||||
"Server {} did not return JMAP Core capability (urn:ietf:params:jmap:core). \
|
||||
Returned capabilities were: {}",
|
||||
&self.server_conf.server_url,
|
||||
session
|
||||
.capabilities
|
||||
.keys()
|
||||
.map(String::as_str)
|
||||
.collect::<Vec<&str>>()
|
||||
.join(", ")
|
||||
));
|
||||
*self.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
|
||||
return Err(err);
|
||||
}
|
||||
if !session
|
||||
.capabilities
|
||||
.contains_key("urn:ietf:params:jmap:mail")
|
||||
{
|
||||
let err = Error::new(format!(
|
||||
"Server {} does not support JMAP Mail capability (urn:ietf:params:jmap:mail). \
|
||||
Returned capabilities were: {}",
|
||||
&self.server_conf.server_url,
|
||||
session
|
||||
.capabilities
|
||||
.keys()
|
||||
.map(String::as_str)
|
||||
.collect::<Vec<&str>>()
|
||||
.join(", ")
|
||||
));
|
||||
*self.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
*self.store.online_status.lock().await = (Instant::now(), Ok(()));
|
||||
*self.session.lock().unwrap() = session;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn mail_account_id(&self) -> Id<Account> {
|
||||
self.session.lock().unwrap().primary_accounts["urn:ietf:params:jmap:mail"].clone()
|
||||
}
|
||||
|
||||
pub fn session_guard(&'_ self) -> MutexGuard<'_, JmapSession> {
|
||||
self.session.lock().unwrap()
|
||||
}
|
||||
|
||||
pub fn add_refresh_event(&self, event: RefreshEvent) {
|
||||
(self.store.event_consumer)(self.store.account_hash, BackendEvent::Refresh(event));
|
||||
}
|
||||
|
||||
pub async fn email_changes(&self, mailbox_hash: MailboxHash) -> Result<()> {
|
||||
let mut current_state: State<EmailObject> = if let Some(s) = self
|
||||
.store
|
||||
.mailboxes
|
||||
.read()
|
||||
.unwrap()
|
||||
.get(&mailbox_hash)
|
||||
.and_then(|mbox| mbox.email_state.lock().unwrap().clone())
|
||||
{
|
||||
s
|
||||
} else {
|
||||
return Ok(());
|
||||
};
|
||||
loop {
|
||||
let email_changes_call: EmailChanges = EmailChanges::new(
|
||||
Changes::<EmailObject>::new()
|
||||
.account_id(self.mail_account_id().clone())
|
||||
.since_state(current_state.clone()),
|
||||
);
|
||||
|
||||
let mut req = Request::new(self.request_no.clone());
|
||||
let prev_seq = req.add_call(&email_changes_call);
|
||||
let email_get_call: EmailGet = EmailGet::new(
|
||||
Get::new()
|
||||
.ids(Some(JmapArgument::reference(
|
||||
prev_seq,
|
||||
ResultField::<EmailChanges, EmailObject>::new("/created"),
|
||||
)))
|
||||
.account_id(self.mail_account_id().clone()),
|
||||
);
|
||||
|
||||
req.add_call(&email_get_call);
|
||||
if let Some(mailbox) = self.store.mailboxes.read().unwrap().get(&mailbox_hash) {
|
||||
if let Some(email_query_state) = mailbox.email_query_state.lock().unwrap().clone() {
|
||||
let email_query_changes_call = EmailQueryChanges::new(
|
||||
QueryChanges::new(self.mail_account_id().clone(), email_query_state)
|
||||
.filter(Some(Filter::Condition(
|
||||
EmailFilterCondition::new()
|
||||
.in_mailbox(Some(mailbox.id.clone()))
|
||||
.into(),
|
||||
))),
|
||||
);
|
||||
let seq_no = req.add_call(&email_query_changes_call);
|
||||
let email_get_call: EmailGet = EmailGet::new(
|
||||
Get::new()
|
||||
.ids(Some(JmapArgument::reference(
|
||||
seq_no,
|
||||
ResultField::<EmailQueryChanges, EmailObject>::new("/removed"),
|
||||
)))
|
||||
.account_id(self.mail_account_id().clone())
|
||||
.properties(Some(vec![
|
||||
"keywords".to_string(),
|
||||
"mailboxIds".to_string(),
|
||||
])),
|
||||
);
|
||||
req.add_call(&email_get_call);
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
let api_url = self.session.lock().unwrap().api_url.clone();
|
||||
let mut res = self
|
||||
.client
|
||||
.post_async(api_url.as_str(), serde_json::to_string(&req)?)
|
||||
.await?;
|
||||
|
||||
let res_text = res.text().await?;
|
||||
debug!(&res_text);
|
||||
let mut v: MethodResponse = match serde_json::from_str(&res_text) {
|
||||
Err(err) => {
|
||||
let err = Error::new(format!(
|
||||
"BUG: Could not deserialize {} server JSON response properly, please \
|
||||
report this!\nReply from server: {}",
|
||||
&self.server_conf.server_url, &res_text
|
||||
))
|
||||
.set_source(Some(Arc::new(err)))
|
||||
.set_kind(ErrorKind::Bug);
|
||||
*self.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
|
||||
return Err(err);
|
||||
}
|
||||
Ok(s) => s,
|
||||
};
|
||||
let changes_response =
|
||||
ChangesResponse::<EmailObject>::try_from(v.method_responses.remove(0))?;
|
||||
if changes_response.new_state == current_state {
|
||||
return Ok(());
|
||||
}
|
||||
let get_response = GetResponse::<EmailObject>::try_from(v.method_responses.remove(0))?;
|
||||
|
||||
{
|
||||
/* process get response */
|
||||
let GetResponse::<EmailObject> { list, .. } = get_response;
|
||||
|
||||
let mut mailbox_hashes: Vec<SmallVec<[MailboxHash; 8]>> =
|
||||
Vec::with_capacity(list.len());
|
||||
for envobj in &list {
|
||||
let v = self
|
||||
.store
|
||||
.mailboxes
|
||||
.read()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter(|(_, m)| envobj.mailbox_ids.contains_key(&m.id))
|
||||
.map(|(k, _)| *k)
|
||||
.collect::<SmallVec<[MailboxHash; 8]>>();
|
||||
mailbox_hashes.push(v);
|
||||
}
|
||||
for (env, mailbox_hashes) in list
|
||||
.into_iter()
|
||||
.map(|obj| self.store.add_envelope(obj))
|
||||
.zip(mailbox_hashes)
|
||||
{
|
||||
for mailbox_hash in mailbox_hashes.iter().skip(1).cloned() {
|
||||
let mut mailboxes_lck = self.store.mailboxes.write().unwrap();
|
||||
mailboxes_lck.entry(mailbox_hash).and_modify(|mbox| {
|
||||
if !env.is_seen() {
|
||||
mbox.unread_emails.lock().unwrap().insert_new(env.hash());
|
||||
}
|
||||
mbox.total_emails.lock().unwrap().insert_new(env.hash());
|
||||
});
|
||||
self.add_refresh_event(RefreshEvent {
|
||||
account_hash: self.store.account_hash,
|
||||
mailbox_hash,
|
||||
kind: RefreshEventKind::Create(Box::new(env.clone())),
|
||||
});
|
||||
}
|
||||
if let Some(mailbox_hash) = mailbox_hashes.first().cloned() {
|
||||
let mut mailboxes_lck = self.store.mailboxes.write().unwrap();
|
||||
mailboxes_lck.entry(mailbox_hash).and_modify(|mbox| {
|
||||
if !env.is_seen() {
|
||||
mbox.unread_emails.lock().unwrap().insert_new(env.hash());
|
||||
}
|
||||
mbox.total_emails.lock().unwrap().insert_new(env.hash());
|
||||
});
|
||||
self.add_refresh_event(RefreshEvent {
|
||||
account_hash: self.store.account_hash,
|
||||
mailbox_hash,
|
||||
kind: RefreshEventKind::Create(Box::new(env)),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
let reverse_id_store_lck = self.store.reverse_id_store.lock().unwrap();
|
||||
let response = v.method_responses.remove(0);
|
||||
match EmailQueryChangesResponse::try_from(response) {
|
||||
Ok(EmailQueryChangesResponse {
|
||||
collapse_threads: _,
|
||||
query_changes_response:
|
||||
QueryChangesResponse {
|
||||
account_id: _,
|
||||
old_query_state,
|
||||
new_query_state,
|
||||
total: _,
|
||||
removed,
|
||||
added,
|
||||
},
|
||||
}) if old_query_state != new_query_state => {
|
||||
self.store
|
||||
.mailboxes
|
||||
.write()
|
||||
.unwrap()
|
||||
.entry(mailbox_hash)
|
||||
.and_modify(|mbox| {
|
||||
*mbox.email_query_state.lock().unwrap() = Some(new_query_state);
|
||||
});
|
||||
/* If the "filter" or "sort" includes a mutable property, the server
|
||||
MUST include all Foos in the current results for which this
|
||||
property may have changed. The position of these may have moved
|
||||
in the results, so they must be reinserted by the client to ensure
|
||||
its query cache is correct. */
|
||||
for email_obj_id in removed
|
||||
.into_iter()
|
||||
.filter(|id| !added.iter().any(|item| item.id == *id))
|
||||
{
|
||||
if let Some(env_hash) = reverse_id_store_lck.get(&email_obj_id) {
|
||||
let mut mailboxes_lck = self.store.mailboxes.write().unwrap();
|
||||
mailboxes_lck.entry(mailbox_hash).and_modify(|mbox| {
|
||||
mbox.unread_emails.lock().unwrap().remove(*env_hash);
|
||||
mbox.total_emails.lock().unwrap().insert_new(*env_hash);
|
||||
});
|
||||
self.add_refresh_event(RefreshEvent {
|
||||
account_hash: self.store.account_hash,
|
||||
mailbox_hash,
|
||||
kind: RefreshEventKind::Remove(*env_hash),
|
||||
});
|
||||
}
|
||||
}
|
||||
for AddedItem {
|
||||
id: _email_obj_id,
|
||||
index: _,
|
||||
} in added
|
||||
{
|
||||
// FIXME
|
||||
}
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
debug!(mailbox_hash);
|
||||
debug!(err);
|
||||
}
|
||||
}
|
||||
let GetResponse::<EmailObject> { list, .. } =
|
||||
GetResponse::<EmailObject>::try_from(v.method_responses.remove(0))?;
|
||||
let mut mailboxes_lck = self.store.mailboxes.write().unwrap();
|
||||
for envobj in list {
|
||||
if let Some(env_hash) = reverse_id_store_lck.get(&envobj.id) {
|
||||
let new_flags =
|
||||
protocol::keywords_to_flags(envobj.keywords().keys().cloned().collect());
|
||||
mailboxes_lck.entry(mailbox_hash).and_modify(|mbox| {
|
||||
if new_flags.0.contains(Flag::SEEN) {
|
||||
mbox.unread_emails.lock().unwrap().remove(*env_hash);
|
||||
} else {
|
||||
mbox.unread_emails.lock().unwrap().insert_new(*env_hash);
|
||||
}
|
||||
});
|
||||
self.add_refresh_event(RefreshEvent {
|
||||
account_hash: self.store.account_hash,
|
||||
mailbox_hash,
|
||||
kind: RefreshEventKind::NewFlags(*env_hash, new_flags),
|
||||
});
|
||||
}
|
||||
}
|
||||
drop(mailboxes_lck);
|
||||
if changes_response.has_more_changes {
|
||||
current_state = changes_response.new_state;
|
||||
} else {
|
||||
self.store
|
||||
.mailboxes
|
||||
.write()
|
||||
.unwrap()
|
||||
.entry(mailbox_hash)
|
||||
.and_modify(|mbox| {
|
||||
*mbox.email_state.lock().unwrap() = Some(changes_response.new_state);
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|