use alloc::{
collections::BTreeSet,
format,
string::{String, ToString},
vec,
vec::Vec,
};
use core::{mem, str::from_utf8};
use chrono::{DateTime, FixedOffset};
use io_imap::{
codec::fragmentizer::Fragmentizer,
coroutine::{ImapCoroutine, ImapCoroutineState, ImapYield},
rfc3501::{
fetch::{ImapMessageFetch, ImapMessageFetchError, ImapMessageFetchOptions},
select::{ImapMailboxSelect, ImapMailboxSelectError, ImapMailboxSelectOptions},
},
types::{
body::BodyStructure,
envelope::Address as ImapAddress,
fetch::{MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName},
flag::FlagFetch,
},
};
use log::trace;
use rfc2047_decoder::{Decoder, RecoverStrategy};
use thiserror::Error;
use crate::{
address::Address,
envelope::types::{Envelope, normalize_message_id},
flag::types::Flag,
imap::convert::{InvalidMailboxName, parse_mailbox},
};
#[derive(Debug, Error)]
pub enum ImapEnvelopeListError {
#[error(transparent)]
Select(#[from] ImapMailboxSelectError),
#[error(transparent)]
Fetch(#[from] ImapMessageFetchError),
#[error("invalid IMAP mailbox `{0}`")]
InvalidMailbox(String),
#[error("computed sequence-set window {0:?} is invalid")]
InvalidWindow(String),
#[error("coroutine was resumed after completion")]
ResumedAfterDone,
}
impl From<InvalidMailboxName> for ImapEnvelopeListError {
fn from(err: InvalidMailboxName) -> Self {
Self::InvalidMailbox(err.0)
}
}
pub struct ImapEnvelopeList {
state: State,
}
impl ImapEnvelopeList {
pub fn new(
mailbox: &str,
page: Option<u32>,
page_size: Option<u32>,
with_attachment: bool,
) -> Result<Self, ImapEnvelopeListError> {
trace!("prepare IMAP envelope listing");
let mbox = parse_mailbox(mailbox)?;
Ok(Self {
state: State::Selecting {
select: ImapMailboxSelect::new(mbox, ImapMailboxSelectOptions::default()),
page,
page_size,
item_names: build_item_names(with_attachment),
},
})
}
}
enum State {
Selecting {
select: ImapMailboxSelect,
page: Option<u32>,
page_size: Option<u32>,
item_names: MacroOrMessageDataItemNames<'static>,
},
Fetching(ImapMessageFetch),
Done,
}
pub(crate) fn build_item_names(with_attachment: bool) -> MacroOrMessageDataItemNames<'static> {
let mut names = vec![
MessageDataItemName::Uid,
MessageDataItemName::Flags,
MessageDataItemName::Envelope,
MessageDataItemName::Rfc822Size,
];
if with_attachment {
names.push(MessageDataItemName::BodyStructure);
}
MacroOrMessageDataItemNames::MessageDataItemNames(names)
}
pub(crate) fn compute_window(
exists: u32,
page: Option<u32>,
page_size: Option<u32>,
) -> Option<String> {
if exists == 0 {
return None;
}
let page = page.unwrap_or(1).max(1);
let Some(size) = page_size else {
return Some("1:*".to_string());
};
if size == 0 {
return None;
}
let skip = (page - 1).saturating_mul(size);
if skip >= exists {
return None;
}
let end = exists - skip;
let start = end.saturating_sub(size - 1).max(1);
Some(format!("{start}:{end}"))
}
pub(crate) fn envelope_from(seq: u32, items: Vec<MessageDataItem<'static>>) -> Envelope {
let mut id = String::new();
let mut message_id: Option<String> = None;
let mut flags = BTreeSet::new();
let mut subject = String::new();
let mut from = Vec::new();
let mut to = Vec::new();
let mut date: Option<DateTime<FixedOffset>> = None;
let mut size: u64 = 0;
let mut has_attachment: Option<bool> = None;
for item in items {
match item {
MessageDataItem::Uid(uid) => {
id = uid.get().to_string();
}
MessageDataItem::Flags(fs) => {
flags = fs.into_iter().filter_map(flag_from_fetch).collect();
}
MessageDataItem::Envelope(env) => {
if let Some(s) = env.subject.into_option() {
subject = decode_mime_bytes(s.as_ref());
}
if let Some(d) = env.date.into_option() {
let raw = bytes_to_string(d.as_ref());
date = parse_rfc2822_date(&raw);
}
if let Some(m) = env.message_id.into_option() {
let raw = bytes_to_string(m.as_ref());
message_id = normalize_message_id(&raw);
}
from = env.from.iter().map(address_from).collect();
to = env.to.iter().map(address_from).collect();
}
MessageDataItem::Rfc822Size(n) => {
size = u64::from(n);
}
MessageDataItem::BodyStructure(structure) => {
has_attachment = Some(body_structure_has_attachment(&structure));
}
_ => {}
}
}
if id.is_empty() {
id = seq.to_string();
}
Envelope {
id,
message_id,
flags,
subject,
from,
to,
date,
size,
has_attachment,
}
}
fn flag_from_fetch(fetch: FlagFetch<'_>) -> Option<Flag> {
let FlagFetch::Flag(flag) = fetch else {
return None;
};
Some(Flag::from_raw(flag.to_string()))
}
fn address_from(addr: &ImapAddress<'_>) -> Address {
let name = addr
.name
.0
.as_ref()
.map(|s| decode_mime_bytes(s.as_ref()))
.filter(|s| !s.is_empty());
let mailbox = addr
.mailbox
.0
.as_ref()
.map(|s| bytes_to_string(s.as_ref()))
.unwrap_or_default();
let host = addr
.host
.0
.as_ref()
.map(|s| bytes_to_string(s.as_ref()))
.unwrap_or_default();
let email = if mailbox.is_empty() {
host
} else if host.is_empty() {
mailbox
} else {
let mut s = String::with_capacity(mailbox.len() + 1 + host.len());
s.push_str(&mailbox);
s.push('@');
s.push_str(&host);
s
};
Address { name, email }
}
fn body_structure_has_attachment(structure: &BodyStructure<'_>) -> bool {
match structure {
BodyStructure::Single { extension_data, .. } => {
let Some(ext) = extension_data.as_ref() else {
return false;
};
let Some(disposition) = ext.tail.as_ref() else {
return false;
};
let Some((kind, _)) = disposition.disposition.as_ref() else {
return false;
};
kind.as_ref().eq_ignore_ascii_case(b"attachment")
}
BodyStructure::Multi { bodies, .. } => {
bodies.as_ref().iter().any(body_structure_has_attachment)
}
}
}
fn parse_rfc2822_date(raw: &str) -> Option<DateTime<FixedOffset>> {
let trimmed = raw.trim();
if trimmed.is_empty() {
return None;
}
DateTime::parse_from_rfc2822(trimmed).ok()
}
fn bytes_to_string(bytes: &[u8]) -> String {
from_utf8(bytes)
.map(ToString::to_string)
.unwrap_or_else(|_| {
let mut out = String::with_capacity(bytes.len());
for b in bytes {
out.push(*b as char);
}
out
})
}
fn decode_mime_bytes(bytes: &[u8]) -> String {
let decoder = Decoder::new().too_long_encoded_word_strategy(RecoverStrategy::Decode);
match decoder.decode(bytes) {
Ok(s) => s,
Err(err) => {
trace!("cannot decode RFC 2047 bytes: {err}");
bytes_to_string(bytes)
}
}
}
impl ImapCoroutine for ImapEnvelopeList {
type Yield = ImapYield;
type Return = Result<Vec<Envelope>, ImapEnvelopeListError>;
fn resume(
&mut self,
fragmentizer: &mut Fragmentizer,
mut bytes: Option<&[u8]>,
) -> ImapCoroutineState<Self::Yield, Self::Return> {
loop {
match mem::replace(&mut self.state, State::Done) {
State::Selecting {
mut select,
page,
page_size,
item_names,
} => match select.resume(fragmentizer, bytes.take()) {
ImapCoroutineState::Yielded(yielded) => {
self.state = State::Selecting {
select,
page,
page_size,
item_names,
};
return ImapCoroutineState::Yielded(yielded);
}
ImapCoroutineState::Complete(Ok(data)) => {
let exists = data.exists.unwrap_or(0);
let Some(window) = compute_window(exists, page, page_size) else {
return ImapCoroutineState::Complete(Ok(Vec::new()));
};
let sequence_set = match window.as_str().try_into() {
Ok(set) => set,
Err(_) => {
return ImapCoroutineState::Complete(Err(
ImapEnvelopeListError::InvalidWindow(window),
));
}
};
self.state = State::Fetching(ImapMessageFetch::new(
sequence_set,
item_names,
ImapMessageFetchOptions::default(),
));
}
ImapCoroutineState::Complete(Err(err)) => {
return ImapCoroutineState::Complete(Err(err.into()));
}
},
State::Fetching(mut fetch) => match fetch.resume(fragmentizer, bytes.take()) {
ImapCoroutineState::Yielded(yielded) => {
self.state = State::Fetching(fetch);
return ImapCoroutineState::Yielded(yielded);
}
ImapCoroutineState::Complete(Ok(data)) => {
let envelopes = data
.into_iter()
.rev()
.map(|(seq, items)| envelope_from(seq.get(), items.into_inner()))
.collect();
return ImapCoroutineState::Complete(Ok(envelopes));
}
ImapCoroutineState::Complete(Err(err)) => {
return ImapCoroutineState::Complete(Err(err.into()));
}
},
State::Done => {
return ImapCoroutineState::Complete(Err(
ImapEnvelopeListError::ResumedAfterDone,
));
}
}
}
}
}