mirror of
https://github.com/pimalaya/himalaya.git
synced 2026-06-15 20:07:57 +08:00
108 lines
3.2 KiB
Rust
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(),
|
|
}
|
|
}
|