partial re-implementation of body ext #30
3 changed files with 111 additions and 104 deletions
5
Cargo.lock
generated
5
Cargo.lock
generated
|
@ -843,9 +843,8 @@ checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "eml-codec"
|
name = "eml-codec"
|
||||||
version = "0.1.1"
|
version = "0.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://git.deuxfleurs.fr/Deuxfleurs/eml-codec.git?branch=main#5cff5510acc2c414b74419e0ee636d5ab249036b"
|
||||||
checksum = "ac20cff537caf72385ffa5d9353ae63cb6c283a53665569408f040b8db36c90d"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.21.2",
|
"base64 0.21.2",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|
|
@ -14,7 +14,7 @@ backtrace = "0.3"
|
||||||
base64 = "0.13"
|
base64 = "0.13"
|
||||||
clap = { version = "3.1.18", features = ["derive", "env"] }
|
clap = { version = "3.1.18", features = ["derive", "env"] }
|
||||||
duplexify = "1.1.0"
|
duplexify = "1.1.0"
|
||||||
eml-codec = "0.1.1"
|
eml-codec = { git = "https://git.deuxfleurs.fr/Deuxfleurs/eml-codec.git", branch = "main" }
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
im = "15"
|
im = "15"
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
use std::num::NonZeroU32;
|
use std::num::NonZeroU32;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
@ -17,8 +18,8 @@ use imap_codec::types::flag::{Flag, StoreResponse, StoreType};
|
||||||
use imap_codec::types::response::{Code, Data, MessageAttribute, Status};
|
use imap_codec::types::response::{Code, Data, MessageAttribute, Status};
|
||||||
use imap_codec::types::sequence::{self, SequenceSet};
|
use imap_codec::types::sequence::{self, SequenceSet};
|
||||||
use eml_codec::{
|
use eml_codec::{
|
||||||
imf::{self as imf},
|
imf,
|
||||||
part::{AnyPart},
|
part::{AnyPart, composite::Message},
|
||||||
mime::r#type::Deductible,
|
mime::r#type::Deductible,
|
||||||
mime,
|
mime,
|
||||||
};
|
};
|
||||||
|
@ -37,6 +38,26 @@ const DEFAULT_FLAGS: [Flag; 5] = [
|
||||||
|
|
||||||
const BODY_CHECK: &str = "body attribute asked but only header is fetched, logic error";
|
const BODY_CHECK: &str = "body attribute asked but only header is fetched, logic error";
|
||||||
|
|
||||||
|
enum FetchedMail<'a> {
|
||||||
|
Partial(imf::Imf<'a>),
|
||||||
|
Full(Message<'a>),
|
||||||
|
}
|
||||||
|
impl<'a> FetchedMail<'a> {
|
||||||
|
fn as_full(&self) -> Result<&Message<'a>> {
|
||||||
|
match self {
|
||||||
|
FetchedMail::Full(x) => Ok(&x),
|
||||||
|
_ => bail!("The full message must be fetched, not only its headers: it's a logic error"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn imf(&self) -> &imf::Imf<'a> {
|
||||||
|
match self {
|
||||||
|
FetchedMail::Full(x) => &x.imf,
|
||||||
|
FetchedMail::Partial(x) => &x,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A MailboxView is responsible for giving the client the information
|
/// A MailboxView is responsible for giving the client the information
|
||||||
/// it needs about a mailbox, such as an initial summary of the mailbox's
|
/// it needs about a mailbox, such as an initial summary of the mailbox's
|
||||||
/// content and continuous updates indicating when the content
|
/// content and continuous updates indicating when the content
|
||||||
|
@ -288,14 +309,12 @@ impl MailboxView {
|
||||||
.get(&uuid)
|
.get(&uuid)
|
||||||
.ok_or_else(|| anyhow!("Mail not in uidindex table: {}", uuid))?;
|
.ok_or_else(|| anyhow!("Mail not in uidindex table: {}", uuid))?;
|
||||||
|
|
||||||
let (parts, imf) = match &body {
|
let fetched = match &body {
|
||||||
Some(m) => {
|
Some(m) => {
|
||||||
let eml = eml_codec::parse_message(m).or(Err(anyhow!("Invalid mail body")))?.1;
|
FetchedMail::Full(eml_codec::parse_message(m).or(Err(anyhow!("Invalid mail body")))?.1)
|
||||||
(Some(eml.child), eml.imf)
|
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
let imf = eml_codec::parse_imf(&meta.headers).or(Err(anyhow!("Invalid mail headers")))?.1;
|
FetchedMail::Partial(eml_codec::parse_imf(&meta.headers).or(Err(anyhow!("Invalid mail headers")))?.1)
|
||||||
(None, imf)
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -316,14 +335,8 @@ impl MailboxView {
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
FetchAttribute::Rfc822Text => {
|
FetchAttribute::Rfc822Text => {
|
||||||
//@FIXME this is not efficient, this is a hack as we need to patch
|
|
||||||
// eml_codec to correctly implement this behavior
|
|
||||||
let txt = eml_codec::parse_imf(body.as_ref().expect(BODY_CHECK).as_slice())
|
|
||||||
.map(|(x, _)| x)
|
|
||||||
.unwrap_or(b"");
|
|
||||||
|
|
||||||
attributes.push(MessageAttribute::Rfc822Text(NString(
|
attributes.push(MessageAttribute::Rfc822Text(NString(
|
||||||
txt.try_into().ok().map(IString::Literal),
|
fetched.as_full()?.raw_body.try_into().ok().map(IString::Literal),
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
FetchAttribute::Rfc822 => attributes.push(MessageAttribute::Rfc822(NString(
|
FetchAttribute::Rfc822 => attributes.push(MessageAttribute::Rfc822(NString(
|
||||||
|
@ -335,23 +348,29 @@ impl MailboxView {
|
||||||
.map(IString::Literal),
|
.map(IString::Literal),
|
||||||
))),
|
))),
|
||||||
FetchAttribute::Envelope => {
|
FetchAttribute::Envelope => {
|
||||||
attributes.push(MessageAttribute::Envelope(message_envelope(&imf)))
|
attributes.push(MessageAttribute::Envelope(message_envelope(fetched.imf())))
|
||||||
}
|
}
|
||||||
FetchAttribute::Body => attributes.push(MessageAttribute::Body(
|
FetchAttribute::Body => attributes.push(MessageAttribute::Body(
|
||||||
build_imap_email_struct(parts.as_ref().expect(BODY_CHECK).as_ref())?,
|
build_imap_email_struct(fetched.as_full()?.child.as_ref())?,
|
||||||
)),
|
)),
|
||||||
FetchAttribute::BodyStructure => attributes.push(MessageAttribute::Body(
|
FetchAttribute::BodyStructure => attributes.push(MessageAttribute::Body(
|
||||||
build_imap_email_struct(parts.as_ref().expect(BODY_CHECK).as_ref())?,
|
build_imap_email_struct(fetched.as_full()?.child.as_ref())?,
|
||||||
)),
|
)),
|
||||||
|
|
||||||
|
// maps to BODY[<section>]<<partial>> and BODY.PEEK[<section>]<<partial>>
|
||||||
|
// peek does not implicitly set the \Seen flag
|
||||||
|
//
|
||||||
|
// eg. BODY[HEADER.FIELDS (DATE FROM)]
|
||||||
|
// eg. BODY[]<0.2048>
|
||||||
FetchAttribute::BodyExt {
|
FetchAttribute::BodyExt {
|
||||||
section,
|
section,
|
||||||
partial,
|
partial,
|
||||||
peek,
|
peek,
|
||||||
} => {
|
} => {
|
||||||
// @FIXME deactivated while eml_codec is integrated
|
|
||||||
todo!();
|
|
||||||
// @TODO Add missing section specifiers
|
// @TODO Add missing section specifiers
|
||||||
/*match get_message_section(&parts.expect("body attribute asked but only header is fetched, logic error"), section) {
|
|
||||||
|
// Extract message section
|
||||||
|
match get_message_section(fetched.as_full()?, section) {
|
||||||
Ok(text) => {
|
Ok(text) => {
|
||||||
let seen_flag = Flag::Seen.to_string();
|
let seen_flag = Flag::Seen.to_string();
|
||||||
if !peek && !flags.iter().any(|x| *x == seen_flag) {
|
if !peek && !flags.iter().any(|x| *x == seen_flag) {
|
||||||
|
@ -359,6 +378,7 @@ impl MailboxView {
|
||||||
self.mailbox.add_flags(uuid, &[seen_flag]).await?;
|
self.mailbox.add_flags(uuid, &[seen_flag]).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle <<partial>> which cut the message byts
|
||||||
let (text, origin) = match partial {
|
let (text, origin) = match partial {
|
||||||
Some((begin, len)) => {
|
Some((begin, len)) => {
|
||||||
if *begin as usize > text.len() {
|
if *begin as usize > text.len() {
|
||||||
|
@ -393,7 +413,6 @@ impl MailboxView {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
FetchAttribute::InternalDate => {
|
FetchAttribute::InternalDate => {
|
||||||
let dt = Utc.fix().timestamp_opt(i64::try_from(meta.internaldate / 1000)?, 0).earliest().ok_or(anyhow!("Unable to parse internal date"))?;
|
let dt = Utc.fix().timestamp_opt(i64::try_from(meta.internaldate / 1000)?, 0).earliest().ok_or(anyhow!("Unable to parse internal date"))?;
|
||||||
|
@ -817,54 +836,61 @@ fn basic_fields(m: &mime::NaiveMIME, sz: usize) -> Result<BasicFields> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/// Extract message section for section identifier passed by the FETCH BODY[<section>]<<partial>>
|
||||||
|
/// request
|
||||||
|
///
|
||||||
|
/// Example of message sections:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// HEADER ([RFC-2822] header of the message)
|
||||||
|
/// TEXT ([RFC-2822] text body of the message) MULTIPART/MIXED
|
||||||
|
/// 1 TEXT/PLAIN
|
||||||
|
/// 2 APPLICATION/OCTET-STREAM
|
||||||
|
/// 3 MESSAGE/RFC822
|
||||||
|
/// 3.HEADER ([RFC-2822] header of the message)
|
||||||
|
/// 3.TEXT ([RFC-2822] text body of the message) MULTIPART/MIXED
|
||||||
|
/// 3.1 TEXT/PLAIN
|
||||||
|
/// 3.2 APPLICATION/OCTET-STREAM
|
||||||
|
/// 4 MULTIPART/MIXED
|
||||||
|
/// 4.1 IMAGE/GIF
|
||||||
|
/// 4.1.MIME ([MIME-IMB] header for the IMAGE/GIF)
|
||||||
|
/// 4.2 MESSAGE/RFC822
|
||||||
|
/// 4.2.HEADER ([RFC-2822] header of the message)
|
||||||
|
/// 4.2.TEXT ([RFC-2822] text body of the message) MULTIPART/MIXED
|
||||||
|
/// 4.2.1 TEXT/PLAIN
|
||||||
|
/// 4.2.2 MULTIPART/ALTERNATIVE
|
||||||
|
/// 4.2.2.1 TEXT/PLAIN
|
||||||
|
/// 4.2.2.2 TEXT/RICHTEXT
|
||||||
|
/// ```
|
||||||
fn get_message_section<'a>(
|
fn get_message_section<'a>(
|
||||||
parsed: &'a Message<'a>,
|
parsed: &'a Message<'a>,
|
||||||
section: &Option<FetchSection>,
|
section: &Option<FetchSection>,
|
||||||
) -> Result<Cow<'a, [u8]>> {
|
) -> Result<Cow<'a, [u8]>> {
|
||||||
match section {
|
match section {
|
||||||
Some(FetchSection::Text(None)) => {
|
Some(FetchSection::Text(None)) => {
|
||||||
let rp = parsed.root_part();
|
Ok(parsed.raw_body.into())
|
||||||
Ok(parsed
|
|
||||||
.raw_message
|
|
||||||
.get(rp.offset_body..rp.offset_end)
|
|
||||||
.ok_or(Error::msg(
|
|
||||||
"Unable to extract email body, cursors out of bound. This is a bug.",
|
|
||||||
))?
|
|
||||||
.into())
|
|
||||||
}
|
}
|
||||||
Some(FetchSection::Text(Some(part))) => {
|
Some(FetchSection::Text(Some(part))) => {
|
||||||
map_subpart_msg(parsed, part.0.as_slice(), |part_msg| {
|
map_subpart(parsed.child.as_ref(), part.0.as_slice(), |part_msg| {
|
||||||
let rp = part_msg.root_part();
|
Ok(part_msg.as_message().ok_or(Error::msg("Not a message/rfc822 part while expected by request (xxx.TEXT)"))?
|
||||||
Ok(part_msg
|
.raw_body
|
||||||
.raw_message
|
|
||||||
.get(rp.offset_body..rp.offset_end)
|
|
||||||
.ok_or(Error::msg(
|
|
||||||
"Unable to extract email body, cursors out of bound. This is a bug.",
|
|
||||||
))?
|
|
||||||
.to_vec()
|
|
||||||
.into())
|
.into())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Some(FetchSection::Header(part)) => map_subpart_msg(
|
Some(FetchSection::Header(part)) => map_subpart(
|
||||||
parsed,
|
parsed.child.as_ref(),
|
||||||
part.as_ref().map(|p| p.0.as_slice()).unwrap_or(&[]),
|
part.as_ref().map(|p| p.0.as_slice()).unwrap_or(&[]),
|
||||||
|part_msg| {
|
|part_msg| {
|
||||||
let rp = part_msg.root_part();
|
Ok(part_msg.as_message().ok_or(Error::msg("Not a message/rfc822 part while expected by request (xxx.TEXT)"))?
|
||||||
Ok(part_msg
|
.raw_headers
|
||||||
.raw_message
|
|
||||||
.get(..rp.offset_body)
|
|
||||||
.ok_or(Error::msg(
|
|
||||||
"Unable to extract email header, cursors out of bound. This is a bug.",
|
|
||||||
))?
|
|
||||||
.to_vec()
|
|
||||||
.into())
|
.into())
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
Some(
|
Some(
|
||||||
FetchSection::HeaderFields(part, fields) | FetchSection::HeaderFieldsNot(part, fields),
|
FetchSection::HeaderFields(part, fields) | FetchSection::HeaderFieldsNot(part, fields),
|
||||||
) => {
|
) => {
|
||||||
let invert = matches!(section, Some(FetchSection::HeaderFieldsNot(_, _)));
|
todo!();
|
||||||
|
/*let invert = matches!(section, Some(FetchSection::HeaderFieldsNot(_, _)));
|
||||||
let fields = fields
|
let fields = fields
|
||||||
.iter()
|
.iter()
|
||||||
.map(|x| match x {
|
.map(|x| match x {
|
||||||
|
@ -893,71 +919,53 @@ fn get_message_section<'a>(
|
||||||
ret.extend(b"\r\n");
|
ret.extend(b"\r\n");
|
||||||
Ok(ret.into())
|
Ok(ret.into())
|
||||||
},
|
},
|
||||||
)
|
)*/
|
||||||
}
|
}
|
||||||
Some(FetchSection::Part(part)) => map_subpart(parsed, part.0.as_slice(), |_msg, part| {
|
Some(FetchSection::Part(part)) => map_subpart(parsed.child.as_ref(), part.0.as_slice(), |part| {
|
||||||
let bytes = match &part.body {
|
let bytes = match &part {
|
||||||
AnyPart::Txt(p) => p.as_bytes().to_vec(),
|
AnyPart::Txt(p) => p.body,
|
||||||
AnyPart::Bin(p) => p.to_vec(),
|
AnyPart::Bin(p) => p.body,
|
||||||
AnyPart::Msg(p) => p.raw_message.to_vec(),
|
AnyPart::Msg(p) => p.raw_part,
|
||||||
AnyPart::Multipart(_) => bail!("Multipart part has no body"),
|
AnyPart::Mult(_) => bail!("Multipart part has no body"),
|
||||||
};
|
};
|
||||||
Ok(bytes.into())
|
Ok(bytes.to_vec().into())
|
||||||
}),
|
}),
|
||||||
Some(FetchSection::Mime(part)) => map_subpart(parsed, part.0.as_slice(), |msg, part| {
|
Some(FetchSection::Mime(part)) => map_subpart(parsed.child.as_ref(), part.0.as_slice(), |part| {
|
||||||
let mut ret = vec![];
|
let bytes = match &part {
|
||||||
for head in part.headers.iter() {
|
AnyPart::Txt(p) => p.mime.fields.raw,
|
||||||
ret.extend(head.name.as_str().as_bytes());
|
AnyPart::Bin(p) => p.mime.fields.raw,
|
||||||
ret.extend(b": ");
|
AnyPart::Msg(p) => p.mime.fields.raw,
|
||||||
ret.extend(&msg.raw_message[head.offset_start..head.offset_end]);
|
AnyPart::Mult(p) => p.mime.fields.raw,
|
||||||
}
|
};
|
||||||
ret.extend(b"\r\n");
|
Ok(bytes.to_vec().into())
|
||||||
Ok(ret.into())
|
|
||||||
}),
|
}),
|
||||||
None => Ok(parsed.raw_message.clone()),
|
None => Ok(parsed.raw_part.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_subpart_msg<F, R>(msg: &Message<'_>, path: &[NonZeroU32], f: F) -> Result<R>
|
/// Fetch a MIME SubPart
|
||||||
|
///
|
||||||
|
/// eg. FETCH BODY[4.2.2.1] -> [4, 2, 2, 1]
|
||||||
|
fn map_subpart<'a, F, R>(part: &AnyPart<'a>, path: &[NonZeroU32], f: F) -> Result<R>
|
||||||
where
|
where
|
||||||
F: FnOnce(&Message<'_>) -> Result<R>,
|
F: FnOnce(&AnyPart<'a>) -> Result<R>,
|
||||||
{
|
{
|
||||||
if path.is_empty() {
|
if path.is_empty() {
|
||||||
f(msg)
|
f(part)
|
||||||
} else {
|
} else {
|
||||||
let part = msg
|
match part {
|
||||||
.parts
|
AnyPart::Mult(x) => map_subpart(
|
||||||
.get(path[0].get() as usize - 1)
|
x.children
|
||||||
.ok_or(anyhow!("No such subpart: {}", path[0]))?;
|
.get(path[0].get() as usize - 1)
|
||||||
if let PartType::Message(msg_attach) = &part.body {
|
.ok_or(anyhow!("Unable to resolve subpath {:?}, current multipart has only {} elements", path, x.children.len()))?,
|
||||||
map_subpart_msg(msg_attach, &path[1..], f)
|
&path[1..],
|
||||||
} else {
|
f),
|
||||||
bail!("Subpart is not a message: {}", path[0]);
|
AnyPart::Msg(x) => map_subpart(x.child.as_ref(), path, f),
|
||||||
|
_ => bail!("You tried to access a subpart on an atomic part (text or binary). Unresolved subpath {:?}", path),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_subpart<F, R>(msg: &Message<'_>, path: &[NonZeroU32], f: F) -> Result<R>
|
|
||||||
where
|
|
||||||
F: FnOnce(&Message<'_>, &MessagePart<'_>) -> Result<R>,
|
|
||||||
{
|
|
||||||
if path.is_empty() {
|
|
||||||
bail!("Unexpected empty path");
|
|
||||||
} else {
|
|
||||||
let part = msg
|
|
||||||
.parts
|
|
||||||
.get(path[0].get() as usize - 1)
|
|
||||||
.ok_or(anyhow!("No such subpart: {}", path[0]))?;
|
|
||||||
if path.len() == 1 {
|
|
||||||
f(msg, part)
|
|
||||||
} else if let PartType::Message(msg_attach) = &part.body {
|
|
||||||
map_subpart(msg_attach, &path[1..], f)
|
|
||||||
} else {
|
|
||||||
bail!("Subpart is not a message: {}", path[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
Loading…
Reference in a new issue