Files
himalaya/src/attachments/list.rs
T
Clément DOUIN 8416a41f99 use std clients
2026-05-20 00:54:16 +02:00

108 lines
3.2 KiB
Rust

use anyhow::{bail, Result};
use clap::Parser;
use mail_parser::{MessageParser, MessagePart, MimeHeaders};
use pimalaya_cli::printer::Printer;
use crate::{
account::Account,
attachments::table::{AttachmentEntry, AttachmentsTable},
cli::BackendArg,
config::{AccountConfig, Config},
};
/// List the attachments carried by a single message in the active
/// account.
///
/// "Attachment" follows mail_parser's classification: parts with
/// `Content-Disposition: attachment`, or any non-body part with a
/// `filename`/`name` parameter. Inline parts (e.g. embedded images
/// referenced by HTML bodies) are skipped by default; pass
/// `--include-inline` to surface them too.
#[derive(Debug, Parser)]
pub struct AttachmentsListCommand {
/// Identifier of the message (IMAP UID, JMAP email id, or Maildir
/// filename id).
#[arg(value_name = "ID")]
pub id: String,
/// Mailbox name or path (IMAP/Maildir). Ignored for JMAP.
#[arg(
long = "mailbox",
short = 'm',
value_name = "NAME",
default_value = "Inbox"
)]
pub mailbox: String,
/// Include parts with `Content-Disposition: inline`.
#[arg(long = "include-inline")]
pub include_inline: bool,
}
impl AttachmentsListCommand {
pub fn execute(
self,
printer: &mut impl Printer,
config: Config,
account_config: AccountConfig,
backend: BackendArg,
) -> Result<()> {
let raw = crate::messages::fetch::fetch_raw(
&config,
&account_config,
backend,
&self.mailbox,
&self.id,
)?;
let Some(message) = MessageParser::new().parse(&raw) else {
bail!("Failed to parse RFC 5322 message");
};
let mut attachments = Vec::new();
for (index, part) in message.attachments().enumerate() {
let inline = is_inline(part);
if inline && !self.include_inline {
continue;
}
attachments.push(AttachmentEntry {
index,
filename: part
.attachment_name()
.map(str::to_owned)
.unwrap_or_else(|| format!("attachment-{index}")),
mime: mime_string(part),
size: part.contents().len(),
inline,
});
}
// Reuse the active account's table styling. Constructing
// an `Account<()>` is enough to read the preset/arrangement.
let account = Account::new(config, account_config, ())?;
printer.out(AttachmentsTable {
preset: account.table_preset,
arrangement: account.table_arrangement,
attachments,
})
}
}
fn is_inline(part: &MessagePart<'_>) -> bool {
part.content_disposition()
.map(|cd| cd.c_type.eq_ignore_ascii_case("inline"))
.unwrap_or(false)
}
fn mime_string(part: &MessagePart<'_>) -> String {
let Some(ct) = part.content_type() else {
return "application/octet-stream".to_string();
};
match ct.c_subtype.as_deref() {
Some(sub) => format!("{}/{}", ct.c_type, sub),
None => ct.c_type.to_string(),
}
}