mirror of
https://github.com/pimalaya/himalaya.git
synced 2026-06-15 20:07:57 +08:00
feat: add missing m2dir commands
This commit is contained in:
@@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
- Restored the RFC 2971 `ID`-after-auth quirk under the new shape `imap.id.{auto, fields}`. Set `imap.id.auto = true` to chain an `ID` exchange straight after IMAP authentication (required by mail.qq.com, fastmail). `imap.id.fields` is a `{ name = bool, … }` map: missing keys are not transmitted, `false` sends `NIL`, `true` sends himalaya's canned value for the well-known keys (`name`, `version`, `vendor`, `support-url`) or `NIL` (with a warning) for any other key. Replaces the v1.2.0 `imap.extensions.id.send-after-auth` flag dropped during the v2 migration.
|
- Restored the RFC 2971 `ID`-after-auth quirk under the new shape `imap.id.{auto, fields}`. Set `imap.id.auto = true` to chain an `ID` exchange straight after IMAP authentication (required by mail.qq.com, fastmail). `imap.id.fields` is a `{ name = bool, … }` map: missing keys are not transmitted, `false` sends `NIL`, `true` sends himalaya's canned value for the well-known keys (`name`, `version`, `vendor`, `support-url`) or `NIL` (with a warning) for any other key. Replaces the v1.2.0 `imap.extensions.id.send-after-auth` flag dropped during the v2 migration.
|
||||||
|
|
||||||
|
- Brought the `m2dir` backend to feature parity with `maildir` at the CLI level: `m2dir messages {save, get, read, export}`, `m2dir flags {list, add, set, remove}`, `m2dir envelopes {get, list}`. Flags are free-form UTF-8 strings persisted in the `.meta/<id>.flags` metadata file. Still missing relative to `maildir`: mailbox `rename`, message `copy` and `move` (need io-m2dir lib support first).
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Unified raw-message input across `messages add`, `messages send`, `imap message save`, `maildir message save`, `jmap email import` and `smtp message send` behind a single `MessageArg` (ported from `mml::cli::args::MessageArg`). Every command now accepts the same three forms: a positional file path, a positional inline raw message (with `\r` / `\n` literals normalized to `\r\n`), or stdin when piped. The legacy `--file <PATH>` flag on `messages add` is gone (positional path replaces it).
|
- Unified raw-message input across `messages add`, `messages send`, `imap message save`, `maildir message save`, `jmap email import` and `smtp message send` behind a single `MessageArg` (ported from `mml::cli::args::MessageArg`). Every command now accepts the same three forms: a positional file path, a positional inline raw message (with `\r` / `\n` literals normalized to `\r\n`), or stdin when piped. The legacy `--file <PATH>` flag on `messages add` is gone (positional path replaces it).
|
||||||
|
|||||||
+1
-1
@@ -22,7 +22,7 @@ imap = ["dep:io-imap", "dep:mail-parser", "dep:rfc2047-decoder", "io-email/imap"
|
|||||||
jmap = ["dep:base64", "dep:io-jmap", "dep:mail-parser", "dep:serde_json", "io-email/jmap", "io-jmap/client"]
|
jmap = ["dep:base64", "dep:io-jmap", "dep:mail-parser", "dep:serde_json", "io-email/jmap", "io-jmap/client"]
|
||||||
smtp = ["dep:io-smtp", "dep:mail-parser", "io-email/smtp"]
|
smtp = ["dep:io-smtp", "dep:mail-parser", "io-email/smtp"]
|
||||||
maildir = ["dep:convert_case", "dep:io-maildir", "dep:mail-parser", "dep:mime_guess", "io-email/maildir", "io-maildir/client"]
|
maildir = ["dep:convert_case", "dep:io-maildir", "dep:mail-parser", "dep:mime_guess", "io-email/maildir", "io-maildir/client"]
|
||||||
m2dir = ["dep:io-m2dir", "dep:mail-parser", "io-email/m2dir", "io-m2dir/client"]
|
m2dir = ["dep:convert_case", "dep:io-m2dir", "dep:mail-parser", "dep:mime_guess", "io-email/m2dir", "io-m2dir/client"]
|
||||||
native-tls = ["pimalaya-stream/native-tls", "pimconf/native-tls", "io-email/native-tls", "io-imap?/native-tls", "io-jmap?/native-tls", "io-smtp?/native-tls"]
|
native-tls = ["pimalaya-stream/native-tls", "pimconf/native-tls", "io-email/native-tls", "io-imap?/native-tls", "io-jmap?/native-tls", "io-smtp?/native-tls"]
|
||||||
rustls-aws = ["pimalaya-stream/rustls-aws", "pimconf/rustls-aws", "io-email/rustls-aws", "io-imap?/rustls-aws", "io-jmap?/rustls-aws", "io-smtp?/rustls-aws"]
|
rustls-aws = ["pimalaya-stream/rustls-aws", "pimconf/rustls-aws", "io-email/rustls-aws", "io-imap?/rustls-aws", "io-jmap?/rustls-aws", "io-smtp?/rustls-aws"]
|
||||||
rustls-ring = ["pimalaya-stream/rustls-ring", "pimconf/rustls-ring", "io-email/rustls-ring", "io-imap?/rustls-ring", "io-jmap?/rustls-ring", "io-smtp?/rustls-ring"]
|
rustls-ring = ["pimalaya-stream/rustls-ring", "pimconf/rustls-ring", "io-email/rustls-ring", "io-imap?/rustls-ring", "io-jmap?/rustls-ring", "io-smtp?/rustls-ring"]
|
||||||
|
|||||||
@@ -17,9 +17,34 @@
|
|||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
|
||||||
|
const INBOX: &str = "Inbox";
|
||||||
|
|
||||||
#[derive(Debug, Parser)]
|
#[derive(Debug, Parser)]
|
||||||
pub struct M2dirNameArg {
|
pub struct M2dirNameArg {
|
||||||
/// Name of the m2dir folder, relative to the m2store root.
|
/// Name of the m2dir folder, relative to the m2store root.
|
||||||
#[arg(name = "m2dir_name", value_name = "NAME")]
|
#[arg(name = "m2dir_name", value_name = "NAME")]
|
||||||
pub inner: String,
|
pub inner: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
pub struct M2dirNameFlag {
|
||||||
|
/// Name of the m2dir folder, relative to the m2store root.
|
||||||
|
#[arg(name = "m2dir_source_name", long = "m2dir", short = 'm')]
|
||||||
|
#[arg(value_name = "NAME", default_value = INBOX)]
|
||||||
|
pub inner: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
pub struct MessageIdArg {
|
||||||
|
/// Identifier of the message.
|
||||||
|
#[arg(name = "message_id", value_name = "ID")]
|
||||||
|
pub inner: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
pub struct MessageIdsArg {
|
||||||
|
/// Identifier(s) of message(s).
|
||||||
|
#[arg(name = "message_ids", value_name = "ID")]
|
||||||
|
#[arg(num_args = 1..)]
|
||||||
|
pub inner: Vec<String>,
|
||||||
|
}
|
||||||
|
|||||||
+18
-5
@@ -21,21 +21,30 @@ use pimalaya_cli::printer::Printer;
|
|||||||
|
|
||||||
use crate::m2dir::{
|
use crate::m2dir::{
|
||||||
client::M2dirClient, create::M2dirMailboxCreateCommand, delete::M2dirMailboxDeleteCommand,
|
client::M2dirClient, create::M2dirMailboxCreateCommand, delete::M2dirMailboxDeleteCommand,
|
||||||
list::M2dirMailboxListCommand,
|
envelope::cli::M2dirEnvelopeCommand, flag::cli::M2dirFlagCommand,
|
||||||
|
list::M2dirMailboxListCommand, message::cli::M2dirMessageCommand,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// m2dir CLI.
|
/// m2dir CLI.
|
||||||
///
|
///
|
||||||
/// Protocol-specific entry point for the m2dir backend. Wider
|
/// Protocol-specific entry point for the m2dir backend. Mailbox and
|
||||||
/// operations (envelopes, flags, messages) go through the shared
|
/// per-folder operations (messages, flags, envelopes) live here;
|
||||||
/// commands `himalaya envelopes`, `himalaya flags`, `himalaya
|
/// cross-backend shared commands also dispatch here when
|
||||||
/// messages` with `--backend m2dir`.
|
/// `--backend m2dir` is passed.
|
||||||
#[derive(Debug, Subcommand)]
|
#[derive(Debug, Subcommand)]
|
||||||
#[command(rename_all = "kebab-case")]
|
#[command(rename_all = "kebab-case")]
|
||||||
pub enum M2dirCommand {
|
pub enum M2dirCommand {
|
||||||
Create(M2dirMailboxCreateCommand),
|
Create(M2dirMailboxCreateCommand),
|
||||||
Delete(M2dirMailboxDeleteCommand),
|
Delete(M2dirMailboxDeleteCommand),
|
||||||
List(M2dirMailboxListCommand),
|
List(M2dirMailboxListCommand),
|
||||||
|
|
||||||
|
#[command(subcommand)]
|
||||||
|
#[command(aliases = ["msgs", "msg"])]
|
||||||
|
Messages(M2dirMessageCommand),
|
||||||
|
#[command(subcommand)]
|
||||||
|
Flags(M2dirFlagCommand),
|
||||||
|
#[command(subcommand)]
|
||||||
|
Envelopes(M2dirEnvelopeCommand),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl M2dirCommand {
|
impl M2dirCommand {
|
||||||
@@ -44,6 +53,10 @@ impl M2dirCommand {
|
|||||||
Self::Create(cmd) => cmd.execute(printer, client),
|
Self::Create(cmd) => cmd.execute(printer, client),
|
||||||
Self::Delete(cmd) => cmd.execute(printer, client),
|
Self::Delete(cmd) => cmd.execute(printer, client),
|
||||||
Self::List(cmd) => cmd.execute(printer, client),
|
Self::List(cmd) => cmd.execute(printer, client),
|
||||||
|
|
||||||
|
Self::Messages(cmd) => cmd.execute(printer, client),
|
||||||
|
Self::Flags(cmd) => cmd.execute(printer, client),
|
||||||
|
Self::Envelopes(cmd) => cmd.execute(printer, client),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
// This file is part of Himalaya, a CLI to manage emails.
|
||||||
|
//
|
||||||
|
// Copyright (C) 2022-2026 soywod <pimalaya.org@posteo.net>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify it under
|
||||||
|
// the terms of the GNU Affero General Public License as published by the Free
|
||||||
|
// Software Foundation, either version 3 of the License, or (at your option) any
|
||||||
|
// later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more
|
||||||
|
// details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::Subcommand;
|
||||||
|
use pimalaya_cli::printer::Printer;
|
||||||
|
|
||||||
|
use crate::m2dir::{
|
||||||
|
client::M2dirClient,
|
||||||
|
envelope::{get::M2dirEnvelopeGetCommand, list::M2dirEnvelopeListCommand},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Manage M2DIR envelopes.
|
||||||
|
///
|
||||||
|
/// An envelope contains header information about a message such as
|
||||||
|
/// date, subject, from, to, cc, bcc, etc.
|
||||||
|
#[derive(Debug, Subcommand)]
|
||||||
|
pub enum M2dirEnvelopeCommand {
|
||||||
|
Get(M2dirEnvelopeGetCommand),
|
||||||
|
List(M2dirEnvelopeListCommand),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl M2dirEnvelopeCommand {
|
||||||
|
pub fn execute(self, printer: &mut impl Printer, client: M2dirClient) -> Result<()> {
|
||||||
|
match self {
|
||||||
|
Self::Get(cmd) => cmd.execute(printer, client),
|
||||||
|
Self::List(cmd) => cmd.execute(printer, client),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
// This file is part of Himalaya, a CLI to manage emails.
|
||||||
|
//
|
||||||
|
// Copyright (C) 2022-2026 soywod <pimalaya.org@posteo.net>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify it under
|
||||||
|
// the terms of the GNU Affero General Public License as published by the Free
|
||||||
|
// Software Foundation, either version 3 of the License, or (at your option) any
|
||||||
|
// later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more
|
||||||
|
// details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use anyhow::{Result, bail};
|
||||||
|
use clap::Parser;
|
||||||
|
use comfy_table::{Cell, Row, Table};
|
||||||
|
use mail_parser::{Header, MessageParser};
|
||||||
|
use pimalaya_cli::printer::Printer;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::m2dir::{
|
||||||
|
arg::{M2dirNameFlag, MessageIdArg},
|
||||||
|
client::M2dirClient,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Get a single M2DIR envelope.
|
||||||
|
///
|
||||||
|
/// Resolves the message identified by `ID` inside the given m2dir
|
||||||
|
/// folder, parses its headers, and prints them.
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
pub struct M2dirEnvelopeGetCommand {
|
||||||
|
#[command(flatten)]
|
||||||
|
pub m2dir: M2dirNameFlag,
|
||||||
|
#[command(flatten)]
|
||||||
|
pub id: MessageIdArg,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl M2dirEnvelopeGetCommand {
|
||||||
|
pub fn execute(self, printer: &mut impl Printer, client: M2dirClient) -> Result<()> {
|
||||||
|
let store = client.open_store()?;
|
||||||
|
let path = store.resolve_folder_path(&self.m2dir.inner)?;
|
||||||
|
let m2dir = client.open_m2dir(path)?;
|
||||||
|
let (entry, bytes) = client.get(m2dir, &self.id.inner)?;
|
||||||
|
|
||||||
|
let Some(parsed) = MessageParser::new().parse_headers(&bytes) else {
|
||||||
|
let path = entry.path();
|
||||||
|
bail!("Invalid MIME message at {path}");
|
||||||
|
};
|
||||||
|
|
||||||
|
let table = EnvelopeTable {
|
||||||
|
preset: client.account.table_preset().to_string(),
|
||||||
|
headers: parsed.headers(),
|
||||||
|
};
|
||||||
|
|
||||||
|
printer.out(table)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize)]
|
||||||
|
pub struct EnvelopeTable<'a> {
|
||||||
|
#[serde(skip)]
|
||||||
|
pub preset: String,
|
||||||
|
pub headers: &'a [Header<'a>],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for EnvelopeTable<'_> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let mut table = Table::new();
|
||||||
|
|
||||||
|
table
|
||||||
|
.load_preset(&self.preset)
|
||||||
|
.set_header(Row::from([Cell::new("HEADER"), Cell::new("VALUE")]));
|
||||||
|
|
||||||
|
for header in self.headers {
|
||||||
|
writeln!(f, "{}: {:?}", header.name.as_str(), header.value)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
// This file is part of Himalaya, a CLI to manage emails.
|
||||||
|
//
|
||||||
|
// Copyright (C) 2022-2026 soywod <pimalaya.org@posteo.net>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify it under
|
||||||
|
// the terms of the GNU Affero General Public License as published by the Free
|
||||||
|
// Software Foundation, either version 3 of the License, or (at your option) any
|
||||||
|
// later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more
|
||||||
|
// details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::Parser;
|
||||||
|
use comfy_table::{Cell, Color, ContentArrangement, Row, Table};
|
||||||
|
use mail_parser::MessageParser;
|
||||||
|
use pimalaya_cli::printer::Printer;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::m2dir::{arg::M2dirNameFlag, client::M2dirClient};
|
||||||
|
|
||||||
|
/// List M2DIR envelopes from the given mailbox.
|
||||||
|
///
|
||||||
|
/// Streams every entry under the m2dir, parses each header block
|
||||||
|
/// and renders the result sorted by date.
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
pub struct M2dirEnvelopeListCommand {
|
||||||
|
#[command(flatten)]
|
||||||
|
pub m2dir: M2dirNameFlag,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl M2dirEnvelopeListCommand {
|
||||||
|
pub fn execute(self, printer: &mut impl Printer, client: M2dirClient) -> Result<()> {
|
||||||
|
let store = client.open_store()?;
|
||||||
|
let path = store.resolve_folder_path(&self.m2dir.inner)?;
|
||||||
|
let m2dir = client.open_m2dir(path)?;
|
||||||
|
|
||||||
|
let entries = client.list_entries(m2dir.clone())?;
|
||||||
|
let messages = client.read_entries_par(&m2dir, &entries)?;
|
||||||
|
|
||||||
|
let parser = MessageParser::new();
|
||||||
|
let mut envelopes = Vec::with_capacity(messages.len());
|
||||||
|
|
||||||
|
for full in messages {
|
||||||
|
let Some(parsed) = parser.parse_headers(full.contents()) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
envelopes.push(EnvelopesTableEntry {
|
||||||
|
id: full.entry().id().to_owned(),
|
||||||
|
subject: parsed.subject().unwrap_or("").to_owned(),
|
||||||
|
from: parsed
|
||||||
|
.from()
|
||||||
|
.and_then(|a| a.first())
|
||||||
|
.and_then(|addr| addr.name().or(addr.address()))
|
||||||
|
.map(str::to_owned)
|
||||||
|
.unwrap_or_default(),
|
||||||
|
date: parsed
|
||||||
|
.date()
|
||||||
|
.map(|date| date.to_rfc822())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
envelopes.sort_by(|a, b| a.date.cmp(&b.date));
|
||||||
|
|
||||||
|
let table = EnvelopesTable {
|
||||||
|
preset: client.account.table_preset().to_string(),
|
||||||
|
arrangement: client.account.table_arrangement(),
|
||||||
|
colors: EnvelopeColors {
|
||||||
|
id: client.account.envelopes_list_table_id_color(),
|
||||||
|
subject: client.account.envelopes_list_table_subject_color(),
|
||||||
|
from: client.account.envelopes_list_table_from_color(),
|
||||||
|
date: client.account.envelopes_list_table_date_color(),
|
||||||
|
},
|
||||||
|
envelopes,
|
||||||
|
};
|
||||||
|
|
||||||
|
printer.out(table)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
struct EnvelopeColors {
|
||||||
|
id: Color,
|
||||||
|
subject: Color,
|
||||||
|
from: Color,
|
||||||
|
date: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize)]
|
||||||
|
pub struct EnvelopesTable {
|
||||||
|
#[serde(skip)]
|
||||||
|
preset: String,
|
||||||
|
#[serde(skip)]
|
||||||
|
arrangement: ContentArrangement,
|
||||||
|
#[serde(skip)]
|
||||||
|
colors: EnvelopeColors,
|
||||||
|
envelopes: Vec<EnvelopesTableEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for EnvelopesTable {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let mut table = Table::new();
|
||||||
|
|
||||||
|
table
|
||||||
|
.load_preset(&self.preset)
|
||||||
|
.set_content_arrangement(self.arrangement.clone())
|
||||||
|
.set_header(Row::from([
|
||||||
|
Cell::new("ID"),
|
||||||
|
Cell::new("SUBJECT"),
|
||||||
|
Cell::new("FROM"),
|
||||||
|
Cell::new("DATE"),
|
||||||
|
]));
|
||||||
|
|
||||||
|
for entry in &self.envelopes {
|
||||||
|
let mut row = Row::new();
|
||||||
|
|
||||||
|
row.max_height(1)
|
||||||
|
.add_cell(Cell::new(&entry.id).fg(self.colors.id))
|
||||||
|
.add_cell(Cell::new(&entry.subject).fg(self.colors.subject))
|
||||||
|
.add_cell(Cell::new(&entry.from).fg(self.colors.from))
|
||||||
|
.add_cell(Cell::new(&entry.date).fg(self.colors.date));
|
||||||
|
|
||||||
|
table.add_row(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
writeln!(f)?;
|
||||||
|
writeln!(f, "{table}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Serialize)]
|
||||||
|
pub struct EnvelopesTableEntry {
|
||||||
|
pub id: String,
|
||||||
|
pub subject: String,
|
||||||
|
pub from: String,
|
||||||
|
pub date: String,
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
// This file is part of Himalaya, a CLI to manage emails.
|
||||||
|
//
|
||||||
|
// Copyright (C) 2022-2026 soywod <pimalaya.org@posteo.net>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify it under
|
||||||
|
// the terms of the GNU Affero General Public License as published by the Free
|
||||||
|
// Software Foundation, either version 3 of the License, or (at your option) any
|
||||||
|
// later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more
|
||||||
|
// details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
pub mod cli;
|
||||||
|
pub mod get;
|
||||||
|
pub mod list;
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
// This file is part of Himalaya, a CLI to manage emails.
|
||||||
|
//
|
||||||
|
// Copyright (C) 2022-2026 soywod <pimalaya.org@posteo.net>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify it under
|
||||||
|
// the terms of the GNU Affero General Public License as published by the Free
|
||||||
|
// Software Foundation, either version 3 of the License, or (at your option) any
|
||||||
|
// later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more
|
||||||
|
// details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::Parser;
|
||||||
|
use io_m2dir::flag::M2dirFlags;
|
||||||
|
use pimalaya_cli::printer::{Message, Printer};
|
||||||
|
|
||||||
|
use crate::m2dir::{
|
||||||
|
arg::{M2dirNameFlag, MessageIdsArg},
|
||||||
|
client::M2dirClient,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Add M2DIR flag(s) to message(s).
|
||||||
|
///
|
||||||
|
/// Each flag is an arbitrary UTF-8 string (e.g. `$seen`, `custom`).
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
pub struct M2dirFlagAddCommand {
|
||||||
|
#[command(flatten)]
|
||||||
|
pub ids: MessageIdsArg,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub m2dir: M2dirNameFlag,
|
||||||
|
|
||||||
|
/// Flag(s) to add to the message.
|
||||||
|
#[arg(long = "flag", short = 'f', num_args = 1..)]
|
||||||
|
pub flags: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl M2dirFlagAddCommand {
|
||||||
|
pub fn execute(self, printer: &mut impl Printer, client: M2dirClient) -> Result<()> {
|
||||||
|
let store = client.open_store()?;
|
||||||
|
let path = store.resolve_folder_path(&self.m2dir.inner)?;
|
||||||
|
let m2dir = client.open_m2dir(path)?;
|
||||||
|
let flags = M2dirFlags::from_iter(self.flags.iter().map(String::as_str));
|
||||||
|
|
||||||
|
for id in self.ids.inner {
|
||||||
|
client.add_flags(&m2dir, &id, flags.clone())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
printer.out(Message::new("M2dir flag(s) successfully added"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
// This file is part of Himalaya, a CLI to manage emails.
|
||||||
|
//
|
||||||
|
// Copyright (C) 2022-2026 soywod <pimalaya.org@posteo.net>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify it under
|
||||||
|
// the terms of the GNU Affero General Public License as published by the Free
|
||||||
|
// Software Foundation, either version 3 of the License, or (at your option) any
|
||||||
|
// later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more
|
||||||
|
// details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::Subcommand;
|
||||||
|
use pimalaya_cli::printer::Printer;
|
||||||
|
|
||||||
|
use crate::m2dir::{
|
||||||
|
client::M2dirClient,
|
||||||
|
flag::{
|
||||||
|
add::M2dirFlagAddCommand, list::M2dirFlagListCommand, remove::M2dirFlagRemoveCommand,
|
||||||
|
set::M2dirFlagSetCommand,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Manage M2DIR flags.
|
||||||
|
///
|
||||||
|
/// A flag is a free-form UTF-8 string stored in the
|
||||||
|
/// `.meta/<id>.flags` metadata file alongside the message.
|
||||||
|
#[derive(Debug, Subcommand)]
|
||||||
|
pub enum M2dirFlagCommand {
|
||||||
|
List(M2dirFlagListCommand),
|
||||||
|
Add(M2dirFlagAddCommand),
|
||||||
|
Set(M2dirFlagSetCommand),
|
||||||
|
Remove(M2dirFlagRemoveCommand),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl M2dirFlagCommand {
|
||||||
|
pub fn execute(self, printer: &mut impl Printer, client: M2dirClient) -> Result<()> {
|
||||||
|
match self {
|
||||||
|
Self::List(cmd) => cmd.execute(printer, client),
|
||||||
|
Self::Add(cmd) => cmd.execute(printer, client),
|
||||||
|
Self::Set(cmd) => cmd.execute(printer, client),
|
||||||
|
Self::Remove(cmd) => cmd.execute(printer, client),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
// This file is part of Himalaya, a CLI to manage emails.
|
||||||
|
//
|
||||||
|
// Copyright (C) 2022-2026 soywod <pimalaya.org@posteo.net>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify it under
|
||||||
|
// the terms of the GNU Affero General Public License as published by the Free
|
||||||
|
// Software Foundation, either version 3 of the License, or (at your option) any
|
||||||
|
// later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more
|
||||||
|
// details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::Parser;
|
||||||
|
use comfy_table::{Cell, ContentArrangement, Row, Table};
|
||||||
|
use pimalaya_cli::printer::Printer;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::m2dir::{
|
||||||
|
arg::{M2dirNameFlag, MessageIdArg},
|
||||||
|
client::M2dirClient,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// List flags set on an m2dir message.
|
||||||
|
///
|
||||||
|
/// Reads the `.meta/<id>.flags` metadata file and prints one flag per
|
||||||
|
/// row. Returns an empty table when the file is missing.
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
pub struct M2dirFlagListCommand {
|
||||||
|
#[command(flatten)]
|
||||||
|
pub m2dir: M2dirNameFlag,
|
||||||
|
#[command(flatten)]
|
||||||
|
pub id: MessageIdArg,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl M2dirFlagListCommand {
|
||||||
|
pub fn execute(self, printer: &mut impl Printer, client: M2dirClient) -> Result<()> {
|
||||||
|
let store = client.open_store()?;
|
||||||
|
let path = store.resolve_folder_path(&self.m2dir.inner)?;
|
||||||
|
let m2dir = client.open_m2dir(path)?;
|
||||||
|
let flags = client.read_flags(&m2dir, &self.id.inner)?;
|
||||||
|
|
||||||
|
let table = FlagsTable {
|
||||||
|
preset: client.account.table_preset().to_string(),
|
||||||
|
arrangement: client.account.table_arrangement(),
|
||||||
|
flags: flags.iter().map(str::to_owned).collect(),
|
||||||
|
};
|
||||||
|
|
||||||
|
printer.out(table)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize)]
|
||||||
|
pub struct FlagsTable {
|
||||||
|
#[serde(skip_serializing)]
|
||||||
|
preset: String,
|
||||||
|
#[serde(skip_serializing)]
|
||||||
|
arrangement: ContentArrangement,
|
||||||
|
flags: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for FlagsTable {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let mut table = Table::new();
|
||||||
|
|
||||||
|
table
|
||||||
|
.load_preset(&self.preset)
|
||||||
|
.set_content_arrangement(self.arrangement.clone())
|
||||||
|
.set_header(Row::from([Cell::new("FLAG")]));
|
||||||
|
|
||||||
|
for flag in &self.flags {
|
||||||
|
table.add_row(Row::from([Cell::new(flag)]));
|
||||||
|
}
|
||||||
|
|
||||||
|
writeln!(f)?;
|
||||||
|
writeln!(f, "{table}")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
// This file is part of Himalaya, a CLI to manage emails.
|
||||||
|
//
|
||||||
|
// Copyright (C) 2022-2026 soywod <pimalaya.org@posteo.net>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify it under
|
||||||
|
// the terms of the GNU Affero General Public License as published by the Free
|
||||||
|
// Software Foundation, either version 3 of the License, or (at your option) any
|
||||||
|
// later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more
|
||||||
|
// details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
pub mod add;
|
||||||
|
pub mod cli;
|
||||||
|
pub mod list;
|
||||||
|
pub mod remove;
|
||||||
|
pub mod set;
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
// This file is part of Himalaya, a CLI to manage emails.
|
||||||
|
//
|
||||||
|
// Copyright (C) 2022-2026 soywod <pimalaya.org@posteo.net>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify it under
|
||||||
|
// the terms of the GNU Affero General Public License as published by the Free
|
||||||
|
// Software Foundation, either version 3 of the License, or (at your option) any
|
||||||
|
// later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more
|
||||||
|
// details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::Parser;
|
||||||
|
use io_m2dir::flag::M2dirFlags;
|
||||||
|
use pimalaya_cli::printer::{Message, Printer};
|
||||||
|
|
||||||
|
use crate::m2dir::{
|
||||||
|
arg::{M2dirNameFlag, MessageIdsArg},
|
||||||
|
client::M2dirClient,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Remove M2DIR flag(s) from message(s).
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
pub struct M2dirFlagRemoveCommand {
|
||||||
|
#[command(flatten)]
|
||||||
|
pub ids: MessageIdsArg,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub m2dir: M2dirNameFlag,
|
||||||
|
|
||||||
|
/// Flag(s) to remove from the message.
|
||||||
|
#[arg(long = "flag", short = 'f', num_args = 1..)]
|
||||||
|
pub flags: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl M2dirFlagRemoveCommand {
|
||||||
|
pub fn execute(self, printer: &mut impl Printer, client: M2dirClient) -> Result<()> {
|
||||||
|
let store = client.open_store()?;
|
||||||
|
let path = store.resolve_folder_path(&self.m2dir.inner)?;
|
||||||
|
let m2dir = client.open_m2dir(path)?;
|
||||||
|
let flags = M2dirFlags::from_iter(self.flags.iter().map(String::as_str));
|
||||||
|
|
||||||
|
for id in self.ids.inner {
|
||||||
|
client.remove_flags(&m2dir, &id, flags.clone())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
printer.out(Message::new("M2dir flag(s) successfully removed"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
// This file is part of Himalaya, a CLI to manage emails.
|
||||||
|
//
|
||||||
|
// Copyright (C) 2022-2026 soywod <pimalaya.org@posteo.net>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify it under
|
||||||
|
// the terms of the GNU Affero General Public License as published by the Free
|
||||||
|
// Software Foundation, either version 3 of the License, or (at your option) any
|
||||||
|
// later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more
|
||||||
|
// details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::Parser;
|
||||||
|
use io_m2dir::flag::M2dirFlags;
|
||||||
|
use pimalaya_cli::printer::{Message, Printer};
|
||||||
|
|
||||||
|
use crate::m2dir::{
|
||||||
|
arg::{M2dirNameFlag, MessageIdsArg},
|
||||||
|
client::M2dirClient,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Set M2DIR flag(s) on message(s) (replaces any existing flags).
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
pub struct M2dirFlagSetCommand {
|
||||||
|
#[command(flatten)]
|
||||||
|
pub ids: MessageIdsArg,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub m2dir: M2dirNameFlag,
|
||||||
|
|
||||||
|
/// Flag(s) to set on the message.
|
||||||
|
#[arg(long = "flag", short = 'f', num_args = 1..)]
|
||||||
|
pub flags: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl M2dirFlagSetCommand {
|
||||||
|
pub fn execute(self, printer: &mut impl Printer, client: M2dirClient) -> Result<()> {
|
||||||
|
let store = client.open_store()?;
|
||||||
|
let path = store.resolve_folder_path(&self.m2dir.inner)?;
|
||||||
|
let m2dir = client.open_m2dir(path)?;
|
||||||
|
let flags = M2dirFlags::from_iter(self.flags.iter().map(String::as_str));
|
||||||
|
|
||||||
|
for id in self.ids.inner {
|
||||||
|
client.set_flags(&m2dir, &id, flags.clone())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
printer.out(Message::new("M2dir flag(s) successfully changed"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
// This file is part of Himalaya, a CLI to manage emails.
|
||||||
|
//
|
||||||
|
// Copyright (C) 2022-2026 soywod <pimalaya.org@posteo.net>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify it under
|
||||||
|
// the terms of the GNU Affero General Public License as published by the Free
|
||||||
|
// Software Foundation, either version 3 of the License, or (at your option) any
|
||||||
|
// later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more
|
||||||
|
// details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::Subcommand;
|
||||||
|
use pimalaya_cli::printer::Printer;
|
||||||
|
|
||||||
|
use crate::m2dir::{
|
||||||
|
client::M2dirClient,
|
||||||
|
message::{
|
||||||
|
export::M2dirMessageExportCommand, get::M2dirMessageGetCommand,
|
||||||
|
read::M2dirMessageReadCommand, save::M2dirMessageSaveCommand,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Manage M2DIR messages.
|
||||||
|
///
|
||||||
|
/// A message is a complete email including headers and body. This
|
||||||
|
/// subcommand allows you to save, get, read and export messages.
|
||||||
|
#[derive(Debug, Subcommand)]
|
||||||
|
pub enum M2dirMessageCommand {
|
||||||
|
Save(M2dirMessageSaveCommand),
|
||||||
|
Get(M2dirMessageGetCommand),
|
||||||
|
Read(M2dirMessageReadCommand),
|
||||||
|
Export(M2dirMessageExportCommand),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl M2dirMessageCommand {
|
||||||
|
pub fn execute(self, printer: &mut impl Printer, client: M2dirClient) -> Result<()> {
|
||||||
|
match self {
|
||||||
|
Self::Save(cmd) => cmd.execute(printer, client),
|
||||||
|
Self::Get(cmd) => cmd.execute(printer, client),
|
||||||
|
Self::Read(cmd) => cmd.execute(printer, client),
|
||||||
|
Self::Export(cmd) => cmd.execute(printer, client),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
// This file is part of Himalaya, a CLI to manage emails.
|
||||||
|
//
|
||||||
|
// Copyright (C) 2022-2026 soywod <pimalaya.org@posteo.net>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify it under
|
||||||
|
// the terms of the GNU Affero General Public License as published by the Free
|
||||||
|
// Software Foundation, either version 3 of the License, or (at your option) any
|
||||||
|
// later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more
|
||||||
|
// details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
use std::{fmt, fs, path::PathBuf};
|
||||||
|
|
||||||
|
use anyhow::{Result, bail};
|
||||||
|
use clap::{Parser, ValueEnum};
|
||||||
|
use convert_case::ccase;
|
||||||
|
use mail_parser::{MessageParser, MimeHeaders};
|
||||||
|
use mime_guess::{get_mime_extensions_str, mime::OCTET_STREAM};
|
||||||
|
use pimalaya_cli::printer::Printer;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::m2dir::{
|
||||||
|
arg::{M2dirNameFlag, MessageIdArg},
|
||||||
|
client::M2dirClient,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Export an m2dir message.
|
||||||
|
///
|
||||||
|
/// Two output modes are supported:
|
||||||
|
/// - raw: write the raw RFC 5322 bytes to stdout
|
||||||
|
/// - parts: explode every MIME part into a separate file under
|
||||||
|
/// `--directory <DIR>` (defaults to the message id)
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
pub struct M2dirMessageExportCommand {
|
||||||
|
#[command(flatten)]
|
||||||
|
pub m2dir: M2dirNameFlag,
|
||||||
|
#[command(flatten)]
|
||||||
|
pub id: MessageIdArg,
|
||||||
|
|
||||||
|
/// Type of the export.
|
||||||
|
#[arg(long, short, value_enum, default_value_t)]
|
||||||
|
pub r#type: ExportType,
|
||||||
|
|
||||||
|
/// Output directory (for parts type).
|
||||||
|
#[arg(long, short, value_name = "DIR")]
|
||||||
|
pub directory: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// Open exported HTML parts in the default application.
|
||||||
|
#[arg(long, short)]
|
||||||
|
pub open: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl M2dirMessageExportCommand {
|
||||||
|
pub fn execute(self, printer: &mut impl Printer, client: M2dirClient) -> Result<()> {
|
||||||
|
let store = client.open_store()?;
|
||||||
|
let path = store.resolve_folder_path(&self.m2dir.inner)?;
|
||||||
|
let m2dir = client.open_m2dir(path)?;
|
||||||
|
let (entry, bytes) = client.get(m2dir, &self.id.inner)?;
|
||||||
|
|
||||||
|
match self.r#type {
|
||||||
|
ExportType::Raw => {
|
||||||
|
let contents = String::from_utf8(bytes)?;
|
||||||
|
printer.out(ExportRaw { contents })?;
|
||||||
|
}
|
||||||
|
ExportType::Parts => {
|
||||||
|
let Some(parsed) = MessageParser::new().parse(&bytes) else {
|
||||||
|
let path = entry.path();
|
||||||
|
bail!("Invalid MIME message at {path}");
|
||||||
|
};
|
||||||
|
|
||||||
|
let dir = match self.directory {
|
||||||
|
Some(dir) => dir,
|
||||||
|
None => PathBuf::from(self.id.inner),
|
||||||
|
};
|
||||||
|
|
||||||
|
fs::create_dir_all(&dir)?;
|
||||||
|
|
||||||
|
let mut parts = Vec::new();
|
||||||
|
|
||||||
|
for (i, part) in parsed.parts.iter().enumerate() {
|
||||||
|
let cr = part.content_type().map(|ct| match &ct.c_subtype {
|
||||||
|
Some(sub) => format!("{}/{}", ct.c_type, sub),
|
||||||
|
None => ct.c_type.to_string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(ref ct) = cr {
|
||||||
|
if ct.starts_with("multipart/") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let filename = match part.attachment_name() {
|
||||||
|
Some(name) => ccase!(kebab, name),
|
||||||
|
None => {
|
||||||
|
let ext = match cr.as_deref().unwrap_or(OCTET_STREAM.as_str()) {
|
||||||
|
"text/plain" => Some(&"txt"),
|
||||||
|
"text/html" => Some(&"html"),
|
||||||
|
ct => get_mime_extensions_str(ct).and_then(|ext| ext.first()),
|
||||||
|
};
|
||||||
|
|
||||||
|
match ext {
|
||||||
|
Some(ext) => format!("part_{i}.{ext}"),
|
||||||
|
None => format!("part_{i}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let path = dir.join(&filename);
|
||||||
|
let contents = part.contents();
|
||||||
|
fs::write(&path, contents)?;
|
||||||
|
parts.push(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.open {
|
||||||
|
for path in &parts {
|
||||||
|
if let Some(ext) = path.extension() {
|
||||||
|
if ext == "html" {
|
||||||
|
open::that(path)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
printer.out(ExportParts { parts })?;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Export type for message export.
|
||||||
|
#[derive(Clone, Debug, Default, ValueEnum)]
|
||||||
|
#[clap(rename_all = "kebab-case")]
|
||||||
|
pub enum ExportType {
|
||||||
|
#[default]
|
||||||
|
/// Output raw RFC822 message to stdout.
|
||||||
|
Raw,
|
||||||
|
/// Export all MIME parts to separate files.
|
||||||
|
Parts,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct ExportRaw {
|
||||||
|
contents: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ExportRaw {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}", self.contents)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct ExportParts {
|
||||||
|
parts: Vec<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ExportParts {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
for path in &self.parts {
|
||||||
|
writeln!(f, " - {}", path.display())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeln!(f)?;
|
||||||
|
write!(f, "Exported {} part(s)", self.parts.len())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
// This file is part of Himalaya, a CLI to manage emails.
|
||||||
|
//
|
||||||
|
// Copyright (C) 2022-2026 soywod <pimalaya.org@posteo.net>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify it under
|
||||||
|
// the terms of the GNU Affero General Public License as published by the Free
|
||||||
|
// Software Foundation, either version 3 of the License, or (at your option) any
|
||||||
|
// later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more
|
||||||
|
// details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use anyhow::{Result, bail};
|
||||||
|
use clap::Parser;
|
||||||
|
use mail_parser::{Message, MessageParser};
|
||||||
|
use pimalaya_cli::printer::Printer;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::m2dir::{
|
||||||
|
arg::{M2dirNameFlag, MessageIdArg},
|
||||||
|
client::M2dirClient,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Get headers of an m2dir message.
|
||||||
|
///
|
||||||
|
/// Resolves the message identified by `ID` inside the given m2dir
|
||||||
|
/// folder, parses its headers and prints them.
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
pub struct M2dirMessageGetCommand {
|
||||||
|
#[command(flatten)]
|
||||||
|
pub m2dir: M2dirNameFlag,
|
||||||
|
#[command(flatten)]
|
||||||
|
pub id: MessageIdArg,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl M2dirMessageGetCommand {
|
||||||
|
pub fn execute(self, printer: &mut impl Printer, client: M2dirClient) -> Result<()> {
|
||||||
|
let store = client.open_store()?;
|
||||||
|
let path = store.resolve_folder_path(&self.m2dir.inner)?;
|
||||||
|
let m2dir = client.open_m2dir(path)?;
|
||||||
|
let (entry, bytes) = client.get(m2dir, &self.id.inner)?;
|
||||||
|
|
||||||
|
let Some(parsed) = MessageParser::new().parse_headers(&bytes) else {
|
||||||
|
let path = entry.path();
|
||||||
|
bail!("Invalid MIME message at {path}");
|
||||||
|
};
|
||||||
|
|
||||||
|
printer.out(MessageView(parsed))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(transparent)]
|
||||||
|
pub struct MessageView<'a>(Message<'a>);
|
||||||
|
|
||||||
|
impl fmt::Display for MessageView<'_> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
for header in self.0.headers() {
|
||||||
|
writeln!(f, "{}: {:?}", header.name(), header.value())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
// This file is part of Himalaya, a CLI to manage emails.
|
||||||
|
//
|
||||||
|
// Copyright (C) 2022-2026 soywod <pimalaya.org@posteo.net>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify it under
|
||||||
|
// the terms of the GNU Affero General Public License as published by the Free
|
||||||
|
// Software Foundation, either version 3 of the License, or (at your option) any
|
||||||
|
// later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more
|
||||||
|
// details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
pub mod cli;
|
||||||
|
pub mod export;
|
||||||
|
pub mod get;
|
||||||
|
pub mod read;
|
||||||
|
pub mod save;
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
// This file is part of Himalaya, a CLI to manage emails.
|
||||||
|
//
|
||||||
|
// Copyright (C) 2022-2026 soywod <pimalaya.org@posteo.net>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify it under
|
||||||
|
// the terms of the GNU Affero General Public License as published by the Free
|
||||||
|
// Software Foundation, either version 3 of the License, or (at your option) any
|
||||||
|
// later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more
|
||||||
|
// details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use anyhow::{Result, bail};
|
||||||
|
use clap::Parser;
|
||||||
|
use mail_parser::{Message, MessageParser};
|
||||||
|
use pimalaya_cli::printer::Printer;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::m2dir::{
|
||||||
|
arg::{M2dirNameFlag, MessageIdArg},
|
||||||
|
client::M2dirClient,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Read m2dir message content.
|
||||||
|
///
|
||||||
|
/// Fetches a message and displays its text content. By default it
|
||||||
|
/// shows plain text content; use `--html` to show HTML.
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
pub struct M2dirMessageReadCommand {
|
||||||
|
#[command(flatten)]
|
||||||
|
pub m2dir: M2dirNameFlag,
|
||||||
|
#[command(flatten)]
|
||||||
|
pub id: MessageIdArg,
|
||||||
|
|
||||||
|
/// Show HTML content instead of plain text.
|
||||||
|
#[arg(long)]
|
||||||
|
pub html: bool,
|
||||||
|
/// Terminal width for text wrapping.
|
||||||
|
#[arg(long, short, default_value = "80")]
|
||||||
|
pub width: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl M2dirMessageReadCommand {
|
||||||
|
pub fn execute(self, printer: &mut impl Printer, client: M2dirClient) -> Result<()> {
|
||||||
|
let store = client.open_store()?;
|
||||||
|
let path = store.resolve_folder_path(&self.m2dir.inner)?;
|
||||||
|
let m2dir = client.open_m2dir(path)?;
|
||||||
|
let (entry, bytes) = client.get(m2dir, &self.id.inner)?;
|
||||||
|
|
||||||
|
let Some(parsed) = MessageParser::new().parse(&bytes) else {
|
||||||
|
let path = entry.path();
|
||||||
|
bail!("Invalid MIME message at {path}");
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.html {
|
||||||
|
printer.out(MessageHtmlView { message: parsed })
|
||||||
|
} else {
|
||||||
|
printer.out(MessagePlainView { message: parsed })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(transparent)]
|
||||||
|
pub struct MessagePlainView<'a> {
|
||||||
|
message: Message<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for MessagePlainView<'_> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
for (i, part) in self.message.text_bodies().enumerate() {
|
||||||
|
if i > 0 {
|
||||||
|
writeln!(f)?;
|
||||||
|
writeln!(f)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(contents) = part.text_contents() {
|
||||||
|
write!(f, "{}", contents.trim_end())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(transparent)]
|
||||||
|
pub struct MessageHtmlView<'a> {
|
||||||
|
message: Message<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for MessageHtmlView<'_> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
for (i, part) in self.message.html_bodies().enumerate() {
|
||||||
|
if i > 0 {
|
||||||
|
writeln!(f)?;
|
||||||
|
writeln!(f)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(contents) = part.text_contents() {
|
||||||
|
write!(f, "{}", contents.trim_end())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
// This file is part of Himalaya, a CLI to manage emails.
|
||||||
|
//
|
||||||
|
// Copyright (C) 2022-2026 soywod <pimalaya.org@posteo.net>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify it under
|
||||||
|
// the terms of the GNU Affero General Public License as published by the Free
|
||||||
|
// Software Foundation, either version 3 of the License, or (at your option) any
|
||||||
|
// later version.
|
||||||
|
//
|
||||||
|
// This program 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 Affero General Public License for more
|
||||||
|
// details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::Parser;
|
||||||
|
use io_m2dir::flag::M2dirFlags;
|
||||||
|
use pimalaya_cli::printer::Printer;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
m2dir::{arg::M2dirNameFlag, client::M2dirClient},
|
||||||
|
shared::messages::arg::MessageArg,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Save a message to an m2dir folder.
|
||||||
|
///
|
||||||
|
/// Appends a message to the specified m2dir. The message can be
|
||||||
|
/// passed as a positional file path, an inline raw string, or piped
|
||||||
|
/// via stdin (see [`MessageArg`] for resolution order). When flags
|
||||||
|
/// are passed, they are written to the `.meta/<id>.flags` file
|
||||||
|
/// alongside the message.
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
pub struct M2dirMessageSaveCommand {
|
||||||
|
#[command(flatten)]
|
||||||
|
pub m2dir: M2dirNameFlag,
|
||||||
|
|
||||||
|
/// Flag(s) to write to the new message's `.flags` metadata file.
|
||||||
|
/// Each flag is an arbitrary UTF-8 string (e.g. `$seen`, `custom`).
|
||||||
|
#[arg(long = "flag", short = 'f', num_args = 0..)]
|
||||||
|
pub flags: Vec<String>,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub message: MessageArg,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl M2dirMessageSaveCommand {
|
||||||
|
pub fn execute(self, printer: &mut impl Printer, client: M2dirClient) -> Result<()> {
|
||||||
|
let store = client.open_store()?;
|
||||||
|
let path = store.resolve_folder_path(&self.m2dir.inner)?;
|
||||||
|
let m2dir = client.open_m2dir(path)?;
|
||||||
|
|
||||||
|
let msg = self.message.parse()?;
|
||||||
|
let entry = client.store(m2dir.clone(), msg.into_bytes())?;
|
||||||
|
|
||||||
|
if !self.flags.is_empty() {
|
||||||
|
let flags = M2dirFlags::from_iter(self.flags.iter().map(String::as_str));
|
||||||
|
client.set_flags(&m2dir, entry.id(), flags)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
printer.out(StoredMessage {
|
||||||
|
id: entry.id().to_owned(),
|
||||||
|
path: entry.path().as_str().to_owned(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct StoredMessage {
|
||||||
|
id: String,
|
||||||
|
path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for StoredMessage {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let id = &self.id;
|
||||||
|
let path = &self.path;
|
||||||
|
write!(f, "Message `{id}` successfully saved to {path}")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,4 +20,7 @@ pub mod cli;
|
|||||||
pub mod client;
|
pub mod client;
|
||||||
pub mod create;
|
pub mod create;
|
||||||
pub mod delete;
|
pub mod delete;
|
||||||
|
pub mod envelope;
|
||||||
|
pub mod flag;
|
||||||
pub mod list;
|
pub mod list;
|
||||||
|
pub mod message;
|
||||||
|
|||||||
Reference in New Issue
Block a user