Compare commits

..

6 commits

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

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

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

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

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

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-12-08 13:24:43 +02:00
Manos Pitsidianakis
c34465bab5
docs: add more doc comments to utilities::widgets mod
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-12-08 13:24:43 +02:00
Manos Pitsidianakis
1f6471556e
docs: add more doc comments to conf mod
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-12-08 13:24:43 +02:00
Manos Pitsidianakis
0cd0864fa1
docs: add more doc comments to version_migrations mod
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-12-08 11:30:06 +02:00
11 changed files with 381 additions and 15 deletions

View file

@ -25,7 +25,7 @@ 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 := `command -v printf`
# Options
PREFIX ?= /usr/local
@ -55,7 +55,8 @@ YELLOW ?= `[ -z $${NO_COLOR+x} ] && ([ -z $${TERM} ] && echo "" || tput setaf 3)
.PHONY: meli
meli: check-deps
${CARGO_BIN} build ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" ${FEATURES} --release --bin meli
@echo ${CARGO_BIN} build ${CARGO_ARGS} ${CARGO_COLOR}--target-dir=\""${CARGO_TARGET_DIR}"\" ${FEATURES} --release --bin meli
@${CARGO_BIN} build ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" ${FEATURES} --release --bin meli
.PHONY: help
help:
@ -75,7 +76,8 @@ help:
@echo " - ${BOLD}deb-dist${ANSI_RESET} (builds debian package in the parent directory)"
@echo " - ${BOLD}distclean${ANSI_RESET} (cleans distribution build artifacts)"
@echo " - ${BOLD}build-rustdoc${ANSI_RESET} (builds rustdoc documentation for all packages in \$$CARGO_TARGET_DIR)"
@echo "\nENVIRONMENT variables of interest:"
@echo ""
@echo "ENVIRONMENT variables of interest:"
@$(PRINTF) "* MELI_FEATURES "
@[ -z $${MELI_FEATURES+x} ] && echo "unset" || echo "= ${UNDERLINE}"$${MELI_FEATURES}${ANSI_RESET}
@$(PRINTF) "* PREFIX "
@ -89,20 +91,26 @@ help:
@$(PRINTF) "* NO_MAN "
@[ $${NO_MAN+x} ] && echo "set" || echo "unset"
@$(PRINTF) "* NO_COLOR "
@[ $${NO_COLOR+x} ] && echo "set" || echo "unset"
@([ $${NO_COLOR+x} ] && [ "$${NO_COLOR}" != "" ] && echo "set") || echo "unset"
@echo "* CARGO_BIN = ${UNDERLINE}${CARGO_BIN}${ANSI_RESET}"
@$(PRINTF) "* CARGO_ARGS "
@[ -z $${CARGO_ARGS+x} ] && echo "unset" || echo "= ${UNDERLINE}"$${CARGO_ARGS}${ANSI_RESET}
@([ -z "${CARGO_ARGS}" ] && echo "unset") || echo = ${UNDERLINE}${CARGO_ARGS}${ANSI_RESET}
@$(PRINTF) "* RUSTFLAGS = "
@([ -z "${RUSTFLAGS}" ] && echo "unset") || echo = ${UNDERLINE}${RUSTFLAGS}${ANSI_RESET}
@$(PRINTF) "* AUTHOR (for deb-dist) "
@[ -z $${AUTHOR+x} ] && echo "unset" || echo "= ${UNDERLINE}"$${AUTHOR}${ANSI_RESET}
@echo "* MIN_RUSTC = ${UNDERLINE}${MIN_RUSTC}${ANSI_RESET}"
@echo "* VERSION = ${UNDERLINE}${VERSION}${ANSI_RESET}"
@echo "* GIT_COMMIT = ${UNDERLINE}${GIT_COMMIT}${ANSI_RESET}"
@#@echo "* CARGO_COLOR = ${CARGO_COLOR}"
@echo "* CARGO_TARGET_DIR = ${CARGO_TARGET_DIR}"
@echo ""
@echo "Built-in/binary utilities"
@echo "* PRINTF = ${UNDERLINE}${PRINTF}${ANSI_RESET}"
.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
@echo RUSTFLAGS=\"'${RUSTFLAGS}'\" ${CARGO_BIN} check ${CARGO_ARGS} ${CARGO_COLOR}--target-dir=\""${CARGO_TARGET_DIR}"\" ${FEATURES} --all --tests --examples --benches --bins
@RUSTFLAGS='${RUSTFLAGS}' ${CARGO_BIN} check ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" ${FEATURES} --all --tests --examples --benches --bins
.PHONY: fmt
fmt:
@ -111,15 +119,18 @@ fmt:
.PHONY: lint
lint:
RUSTFLAGS='${RUSTFLAGS}' $(CARGO_BIN) clippy --no-deps ${FEATURES} --all --tests --examples --benches --bins
@echo RUSTFLAGS=\"'${RUSTFLAGS}'\" $(CARGO_BIN) clippy --no-deps ${FEATURES} --all --tests --examples --benches --bins
@RUSTFLAGS='${RUSTFLAGS}' $(CARGO_BIN) clippy --no-deps ${FEATURES} --all --tests --examples --benches --bins
.PHONY: test
test: test-docs
RUSTFLAGS='${RUSTFLAGS}' ${CARGO_BIN} test ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" ${FEATURES} --all --tests --examples --benches --bins
@echo RUSTFLAGS=\"'${RUSTFLAGS}'\" ${CARGO_BIN} test ${CARGO_ARGS} ${CARGO_COLOR}--target-dir=\""${CARGO_TARGET_DIR}"\" ${FEATURES} --all --tests --examples --benches --bins
@RUSTFLAGS='${RUSTFLAGS}' ${CARGO_BIN} test ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" ${FEATURES} --all --tests --examples --benches --bins
.PHONY: test-docs
test-docs:
RUSTFLAGS='${RUSTFLAGS}' ${CARGO_BIN} test ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" ${FEATURES} --all --doc
@echo RUSTFLAGS=\"'${RUSTFLAGS}'\" ${CARGO_BIN} test ${CARGO_ARGS} ${CARGO_COLOR}--target-dir=\""${CARGO_TARGET_DIR}"\" ${FEATURES} --all --doc
@RUSTFLAGS='${RUSTFLAGS}' ${CARGO_BIN} test ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" ${FEATURES} --all --doc
.PHONY: test-feature-permutations
test-feature-permutations:
@ -204,7 +215,8 @@ deb-dist:
.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
@echo RUSTDOCFLAGS=\""--crate-version ${VERSION}_${GIT_COMMIT}_${DATE}"\" ${CARGO_BIN} doc ${CARGO_ARGS} ${CARGO_COLOR}--target-dir=\""${CARGO_TARGET_DIR}"\" --all-features --no-deps --workspace --document-private-items --open
@RUSTDOCFLAGS="--crate-version ${VERSION}_${GIT_COMMIT}_${DATE}" ${CARGO_BIN} doc ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" --all-features --no-deps --workspace --document-private-items --open
.PHONY: check-tagrefs
check-tagrefs:

View file

@ -293,6 +293,11 @@ Path of
.Xr mutt 1
compatible alias file in the option
They are parsed and imported read-only.
.It Ic notmuch_address_book_query Ar String
.Pq Em optional
Query passed to
.Qq Li notmuch address
to import contacts into meli. Contacts are parsed and imported read-only.
.It Ic mailboxes Ar mailbox
.Pq Em optional
Configuration for each mailbox.
@ -987,7 +992,7 @@ Example:
.Bd -literal
[composing]
editor_cmd = '~/.local/bin/vim +/^$'
embed = true
embedded_pty = true
custom_compose_hooks = [ { name ="spellcheck", command="aspell --mode email --dont-suggest --ignore-case list" }]
.Ed
.\"

View file

@ -71,6 +71,13 @@ pub use themes::*;
pub use self::{composing::*, pgp::*, shortcuts::*, tags::*};
/// Utility macro to access an [`AccountConf`] setting field from
/// [`Context`](crate::Context) indexed by `$account_hash`
///
/// The value returned is the optionally overriden one in the
/// [`AccountConf::conf_override`] field, otherwise the global one.
///
/// See also the [`mailbox_settings`](crate::mailbox_settings) macro.
#[macro_export]
macro_rules! account_settings {
($context:ident[$account_hash:expr].$setting:ident.$field:ident) => {{
@ -87,6 +94,14 @@ macro_rules! account_settings {
}};
}
/// Utility macro to access an [`AccountConf`] setting field from
/// [`Context`](crate::Context) indexed by `$account_hash` and a mailbox.
///
/// The value returned is the optionally overriden one in the
/// [`FileMailboxConf::conf_override`] field, otherwise the
/// [`AccountConf::conf_override`] field, otherwise the global one.
///
/// See also the [`account_settings`] macro.
#[macro_export]
macro_rules! mailbox_settings {
($context:ident[$account_hash:expr][$mailbox_path:expr].$setting:ident.$field:ident) => {{

View file

@ -909,17 +909,41 @@ impl AutoComplete {
}
}
/// A widget that draws a scrollbar.
///
/// # Example
///
/// ```rust,no_run
/// # use meli::{Area, Component, CellBuffer, Context, utilities::ScrollBar};
/// // Mock `Component::draw` impl
/// fn draw(grid: &mut CellBuffer, area: Area, context: &mut Context) {
/// let position = 0;
/// let visible_rows = area.height();
/// let scrollbar_area = area.nth_col(area.width());
/// let total_rows = 100;
/// ScrollBar::default().set_show_arrows(true).draw(
/// grid,
/// scrollbar_area,
/// context,
/// position,
/// visible_rows,
/// total_rows,
/// );
/// }
/// ```
#[derive(Clone, Copy, Default)]
pub struct ScrollBar {
pub show_arrows: bool,
}
impl ScrollBar {
/// Update `self.show_arrows` field.
pub fn set_show_arrows(&mut self, new_val: bool) -> &mut Self {
self.show_arrows = new_val;
self
}
/// Draw `self` vertically.
pub fn draw(
&self,
grid: &mut CellBuffer,
@ -976,6 +1000,7 @@ impl ScrollBar {
}
}
/// Draw `self` horizontally.
pub fn draw_horizontal(
&self,
grid: &mut CellBuffer,
@ -1033,6 +1058,82 @@ impl ScrollBar {
}
}
/// A widget that displays a customizable progress spinner.
///
/// It uses a [`Timer`](crate::jobs::Timer) and each time its timer fires, it
/// cycles to the next stage of its `kind` sequence.
///
/// `kind` is an array of strings/string slices and an
/// [`Duration` interval](std::time::Duration), and each item represents a stage
/// or frame of the progress spinner. For example, a
/// `(Duration::from_millis(130), &["-", "\\", "|", "/"])` value would cycle
/// through the sequence `-`, `\`, `|`, `/`, `-`, `\`, `|` and so on roughly
/// every 130 milliseconds.
///
/// # Example
///
/// ```rust,no_run
/// use std::collections::HashSet;
///
/// use meli::{jobs::JobId, utilities::ProgressSpinner, Component, Context, StatusEvent, UIEvent};
///
/// struct JobMonitoringWidget {
/// progress_spinner: ProgressSpinner,
/// in_progress_jobs: HashSet<JobId>,
/// }
///
/// impl JobMonitoringWidget {
/// fn new(context: &Context, container: Box<dyn Component>) -> Self {
/// let mut progress_spinner = ProgressSpinner::new(20, context);
/// match context.settings.terminal.progress_spinner_sequence.as_ref() {
/// Some(meli::conf::terminal::ProgressSpinnerSequence::Integer(k)) => {
/// progress_spinner.set_kind(*k);
/// }
/// Some(meli::conf::terminal::ProgressSpinnerSequence::Custom {
/// ref frames,
/// ref interval_ms,
/// }) => {
/// progress_spinner.set_custom_kind(frames.clone(), *interval_ms);
/// }
/// None => {}
/// }
/// Self {
/// progress_spinner,
/// in_progress_jobs: Default::default(),
/// }
/// }
///
/// // Mock `Component::process_event` impl.
/// fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
/// match event {
/// UIEvent::StatusEvent(StatusEvent::JobCanceled(ref job_id))
/// | UIEvent::StatusEvent(StatusEvent::JobFinished(ref job_id)) => {
/// self.in_progress_jobs.remove(job_id);
/// if self.in_progress_jobs.is_empty() {
/// self.progress_spinner.stop();
/// }
/// self.progress_spinner.set_dirty(true);
/// false
/// }
/// UIEvent::StatusEvent(StatusEvent::NewJob(ref job_id)) => {
/// if self.in_progress_jobs.is_empty() {
/// self.progress_spinner.start();
/// }
/// self.progress_spinner.set_dirty(true);
/// self.in_progress_jobs.insert(*job_id);
/// false
/// }
/// UIEvent::Timer(_) => {
/// if self.progress_spinner.process_event(event, context) {
/// return true;
/// }
/// false
/// }
/// _ => false,
/// }
/// }
/// }
/// ```
#[derive(Debug)]
pub struct ProgressSpinner {
timer: crate::jobs::Timer,
@ -1120,6 +1221,7 @@ impl ProgressSpinner {
pub const INTERVAL_MS: u64 = 50;
const INTERVAL: std::time::Duration = std::time::Duration::from_millis(Self::INTERVAL_MS);
/// See source code of [`Self::KINDS`].
pub fn new(kind: usize, context: &Context) -> Self {
let kind = kind % Self::KINDS.len();
let width = Self::KINDS[kind]
@ -1151,10 +1253,12 @@ impl ProgressSpinner {
}
}
#[inline]
pub fn is_active(&self) -> bool {
self.active
}
/// See source code of [`Self::KINDS`].
pub fn set_kind(&mut self, kind: usize) {
self.stage = 0;
self.width = Self::KINDS[kind % Self::KINDS.len()]
@ -1197,11 +1301,12 @@ impl ProgressSpinner {
impl std::fmt::Display for ProgressSpinner {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "progress bar")
write!(f, "progress spinner")
}
}
impl Component for ProgressSpinner {
/// Draw current stage, if `self` is dirty.
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
if self.dirty {
grid.clear_area(area, self.theme_attr);
@ -1224,6 +1329,7 @@ impl Component for ProgressSpinner {
}
}
/// If the `event` is our timer firing, proceed to next stage.
fn process_event(&mut self, event: &mut UIEvent, _context: &mut Context) -> bool {
match event {
UIEvent::Timer(id) if *id == self.timer.id() => {

View file

@ -21,10 +21,70 @@
// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
//! Helping users move to newer `meli` versions.
//!
//! # How version information is stored in the filesystem and examined
//!
//! On start-up, `meli` checks the contents of `${XDG_DATA_HOME}/meli/.version`
//! (the "version file") to determine if there has been a version upgrade since
//! the last time it was launched, if any. Regardless of the version file
//! existence or its contents, it will write the latest version string as
//! recorded in the module `const` global [`LATEST`] **unless** the version
//! file's content match `LATEST`.
//!
//! [`LATEST`] is verified to contain the actual version at compile-time using
//! Cargo's environment variable `CARGO_PKG_VERSION`.
//!
//! If the version file does not exist, no migrations need to be performed.
//!
//! If the version file is determined to be a previous version,
//! [`calculate_migrations`] is called which examines every migration in the
//! version range starting from the previous version up to the latest. If any
//! migration is applicable, it asks the user interactively whether to perform
//! them. This happens in [`version_setup`].
//!
//! # How `meli` encodes version information statically with types and modules
//!
//! Every release **MUST** have a module associated with it. The module
//! contains:
//!
//! - a public [`VersionIdentifier`] `const` item
//! - a `struct` that represents the version, named by convention `VX_Y_Z[..]`.
//! The `struct` definition can be empty (e.g. `pub struct V0_0_1;`). The
//! `struct` **MUST** implement the [`Version`] trait, which can be used to
//! retrieve the version identifier and the migrations.
//! - Any number of structs representing migrations, which implement the
//! [`Migration`] trait, and which are returned by the [`Version::migrations`]
//! method.
//!
//! All versions must be stored in an `IndexMap` (type alias [`VersionMap`])
//! which is retrieved by the function [`versions`].
//!
//! # How migrations work
//!
//! Migrations are **not** guaranteed to be lossless; stored metadata
//! information in the filesystem may be lost.
//!
//! Migrations can optionally claim they are not applicable, which means they
//! will be skipped entirely if migrations are to be applied. The check is done
//! in the [`Migration::is_applicable`] trait method which can opt-in to make
//! checks in the configuration file.
//!
//! Migrations can be performed using the [`Migration::perform`] method, which
//! can optionally attempt to make a "dry run" application, which can check for
//! errors but not make any actual changes in the filesystem.
//!
//! If a migration can be reverted, it can implement the revert logic in
//! [`Migration::revert`] which follows the same logic as [`Migration::perform`]
//! but in reverse. It is not always possible a migration can be reverted, since
//! migrations are not necessarily lossless.
#[cfg(test)]
mod tests;
/// A container for [`Version`]s indexed by their [`VersionIdentifier`].
///
/// Internally it is returned by the [`versions`] function in this
/// module.
pub type VersionMap = IndexMap<VersionIdentifier, Box<dyn Version + Send + Sync + 'static>>;
/// Utility macro to define version module imports and a function `versions() ->
@ -80,7 +140,10 @@ pub type VersionMap = IndexMap<VersionIdentifier, Box<dyn Version + Send + Sync
#[macro_export]
macro_rules! decl_version_map {
($($version_id:path => $m:ident::$v:ident),*$(,)?) => {
fn versions() -> &'static VersionMap {
/// Return all versions in a [`VersionMap`] container.
///
/// The value is lazily initialized on first access.
pub fn versions() -> &'static VersionMap {
use std::sync::OnceLock;
#[allow(dead_code)]
const fn const_bytes_cmp(lhs: &[u8], rhs: &[u8]) -> std::cmp::Ordering {
@ -180,6 +243,8 @@ macro_rules! decl_version_map {
};
}
/// Wrapper macro over [`decl_version_map`] that also defines the arguments as
/// modules.
macro_rules! decl_version_mods {
($($version_id:path => $m:ident::$v:ident),*$(,)?) => {
$(
@ -209,9 +274,20 @@ use melib::{error::*, log};
use crate::{conf::FileSettings, terminal::Ask};
/// The latest version as defined in the Cargo manifest file of `meli`.
///
/// On compile-time if the `CARGO_PKG_VERSION` environment variable is
/// available, the macro [`decl_version_map`] asserts that it matches the actual
/// latest version string.
pub const LATEST: VersionIdentifier = v0_8_10::V0_8_10_ID;
/// An application version identifier.
/// An application version identifier with [Semantic Versioning v2.0.0]
/// semantics.
///
/// There's no support for "Build metadata" of the specification since we're not
/// using those.
///
/// [Semantic Versioning v2.0.0]: https://semver.org/spec/v2.0.0.html
#[derive(Clone, Copy, Debug, Eq)]
pub struct VersionIdentifier {
string: &'static str,
@ -222,6 +298,8 @@ pub struct VersionIdentifier {
}
impl VersionIdentifier {
/// An invalid non-existent release, `v0.0.0`, used for comparison with
/// other identifiers.
pub const NULL: Self = Self {
string: "0.0.0",
major: 0,
@ -262,6 +340,8 @@ impl std::fmt::Display for VersionIdentifier {
}
}
/// Compare `(self.major, self.minor, self.patch, self.pre)` fields with another
/// [`VersionIdentifier`].
impl Ord for VersionIdentifier {
fn cmp(&self, other: &Self) -> Ordering {
(self.major, self.minor, self.patch, self.pre).cmp(&(
@ -273,6 +353,8 @@ impl Ord for VersionIdentifier {
}
}
/// Compare `(self.major, self.minor, self.patch, self.pre)` fields with another
/// [`VersionIdentifier`].
impl PartialOrd for VersionIdentifier {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
@ -351,6 +433,8 @@ impl std::fmt::Debug for dyn Migration + Send + Sync {
/// Return the path to the `.version` file, a plain text file that contains the
/// version of meli that "owns" the configuration and data files.
///
/// The actual path examined is `${XDG_DATA_HOME}/meli/.version`.
pub fn version_file() -> Result<PathBuf> {
let xdg_dirs = xdg::BaseDirectories::with_prefix("meli")?;
Ok(xdg_dirs.place_data_file(".version")?)
@ -358,6 +442,10 @@ pub fn version_file() -> Result<PathBuf> {
/// Inspect current/previous version setup, perform migrations if necessary,
/// etc.
///
/// This function requires an interactive user session, if stdout is not an
/// interactive TTY, the process caller must ensure `stdin` contains the
/// necessary input (`y`, `n`, newline) otherwise this function _blocks_.
pub fn version_setup(
config: &Path,
writer: &mut impl std::io::Write,

View file

@ -20,8 +20,11 @@
//
// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
//! <https://release.meli-email.org/v0.8.10>
use crate::version_migrations::*;
/// <https://release.meli-email.org/v0.8.10>
pub const V0_8_10_ID: VersionIdentifier = VersionIdentifier {
string: "0.8.10",
major: 0,
@ -30,6 +33,7 @@ pub const V0_8_10_ID: VersionIdentifier = VersionIdentifier {
pre: "",
};
/// <https://release.meli-email.org/v0.8.10>
#[derive(Clone, Copy, Debug)]
pub struct V0_8_10;

View file

@ -20,8 +20,11 @@
//
// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
//! <https://release.meli-email.org/v0.8.8>
use crate::version_migrations::*;
/// <https://release.meli-email.org/v0.8.8>
pub const V0_8_8_ID: VersionIdentifier = VersionIdentifier {
string: "0.8.8",
major: 0,
@ -30,6 +33,7 @@ pub const V0_8_8_ID: VersionIdentifier = VersionIdentifier {
pre: "",
};
/// <https://release.meli-email.org/v0.8.8>
#[derive(Clone, Copy, Debug)]
pub struct V0_8_8;
@ -43,6 +47,9 @@ impl Version for V0_8_8 {
}
}
/// Rename `addressbook` to `contacts`.
///
/// "The storage file for contacts, stored in the application's data folder, was renamed from `addressbook` to `contacts` to better reflect its purpose."
#[derive(Clone, Copy, Debug)]
struct AddressbookRename;

View file

@ -20,8 +20,11 @@
//
// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
//! <https://release.meli-email.org/v0.8.9>
use crate::version_migrations::*;
/// <https://release.meli-email.org/v0.8.9>
pub const V0_8_9_ID: VersionIdentifier = VersionIdentifier {
string: "0.8.9",
major: 0,
@ -30,6 +33,7 @@ pub const V0_8_9_ID: VersionIdentifier = VersionIdentifier {
pre: "",
};
/// <https://release.meli-email.org/v0.8.9>
#[derive(Clone, Copy, Debug)]
pub struct V0_8_9;

View file

@ -97,6 +97,12 @@ impl AccountSettings {
self.extra.get("vcard_folder").map(String::as_str)
}
pub fn notmuch_address_book_query(&self) -> Option<&str> {
self.extra
.get("notmuch_address_book_query")
.map(String::as_str)
}
/// Get the server password, either directly from the `server_password`
/// settings value, or by running the `server_password_command` and reading
/// the output.
@ -152,6 +158,7 @@ impl AccountSettings {
.set_kind(ErrorKind::Configuration));
}
}
_ = self.extra.swap_remove("notmuch_address_book_query");
}
{
if let Some(mutt_alias_file) = self.extra.swap_remove("mutt_alias_file") {

View file

@ -21,6 +21,7 @@
pub mod jscontact;
pub mod mutt;
pub mod notmuchcontact;
pub mod vcard;
mod card;
@ -143,6 +144,60 @@ impl Contacts {
}
}
}
use std::process::Command;
if let Some(notmuch_addressbook_query) = s.notmuch_address_book_query() {
match Command::new("sh")
.args([
"-c",
&format!(
"notmuch address --format=json {}",
notmuch_addressbook_query
),
])
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.output()
{
Ok(notmuch_addresses) => {
if notmuch_addresses.status.success() {
match std::str::from_utf8(&notmuch_addresses.stdout) {
Ok(notmuch_address_out) => {
match notmuchcontact::parse_notmuch_contacts(notmuch_address_out) {
Ok(contacts) => {
for c in contacts {
ret.add_card(c.clone());
}
}
Err(err) => {
log::warn!(
"Unable to parse notmuch contact result into cards: {} {}",
notmuch_address_out,
err
);
}
}
}
Err(err) => {
log::warn!(
"Unable to read from notmuch address query: {} {}",
notmuch_addressbook_query,
err
);
}
}
} else {
log::warn!(
"Error ({}) running notmuch address: {} {}",
notmuch_addresses.status,
String::from_utf8_lossy(&notmuch_addresses.stdout),
String::from_utf8_lossy(&notmuch_addresses.stderr)
);
}
}
Err(e) => log::warn!("Unable to run notmuch address command: {}", e),
}
}
ret
}

View file

@ -0,0 +1,63 @@
//
// meli
//
// Copyright 2024 Emmanouil Pitsidianakis <manos@pitsidianak.is>
//
// This file is part of meli.
//
// meli is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// meli is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with meli. If not, see <http://www.gnu.org/licenses/>.
//
// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
use crate::{contacts::Card, error::Result};
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct NotmuchContact {
pub name: String,
pub address: String,
#[serde(rename = "name-addr")]
pub name_addr: String,
}
pub fn parse_notmuch_contacts(input: &str) -> Result<Vec<Card>> {
let mut cards = Vec::new();
let abook = serde_json::from_str::<Vec<NotmuchContact>>(input)?;
for c in abook.iter() {
cards.push(
Card::new()
.set_title(c.name_addr.clone())
.set_email(c.address.clone())
.set_name(c.name.clone())
.set_external_resource(true)
.clone(),
);
}
Ok(cards)
}
#[test]
fn test_addressbook_notmuchcontact() {
let cards = parse_notmuch_contacts(
r#"[{"name": "Full Name", "address": "user@example.com", "name-addr": "Full Name <user@example.com>"},
{"name": "Full2 Name", "address": "user2@example.com", "name-addr": "Full2 Name <user2@example.com>"}]"#
).unwrap();
assert_eq!(cards[0].name(), "Full Name");
assert_eq!(cards[0].title(), "Full Name <user@example.com>");
assert_eq!(cards[0].email(), "user@example.com");
assert_eq!(cards[1].name(), "Full2 Name");
assert_eq!(cards[1].title(), "Full2 Name <user2@example.com>");
assert_eq!(cards[1].email(), "user2@example.com");
}