replacing mail_parser by eml_codec, first iteration
This commit is contained in:
parent
24fed41288
commit
17fba10d8f
6 changed files with 603 additions and 675 deletions
890
Cargo.lock
generated
890
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -14,6 +14,8 @@ 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 = { path = "../eml-codec" }
|
||||||
|
eml-codec = "0.1.0"
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
im = "15"
|
im = "15"
|
||||||
|
@ -21,7 +23,6 @@ itertools = "0.10"
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
ldap3 = { version = "0.10", default-features = false, features = ["tls-rustls"] }
|
ldap3 = { version = "0.10", default-features = false, features = ["tls-rustls"] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
mail-parser = "0.8.2"
|
|
||||||
rusoto_core = { version = "0.48.0", default_features = false, features = ["rustls"] }
|
rusoto_core = { version = "0.48.0", default_features = false, features = ["rustls"] }
|
||||||
rusoto_credential = "0.48.0"
|
rusoto_credential = "0.48.0"
|
||||||
rusoto_s3 = { version = "0.48.0", default_features = false, features = ["rustls"] }
|
rusoto_s3 = { version = "0.48.0", default_features = false, features = ["rustls"] }
|
||||||
|
@ -52,7 +53,4 @@ smtp-server = { git = "http://github.com/Alexis211/kannader", branch = "feature/
|
||||||
#k2v-client = { path = "../garage/src/k2v-client" }
|
#k2v-client = { path = "../garage/src/k2v-client" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
#mail-parser-05 = { package = "mail-parser", version = "0.5" }
|
|
||||||
#mail-parser-main = { package = "mail-parser", git = "https://github.com/stalwartlabs/mail-parser", branch = "main" }
|
|
||||||
#mail-parser-superboum = { package = "mail-parser", git = "https://github.com/superboum/mail-parser", branch = "feature/no_decode" }
|
|
||||||
#mail-parser-db61a03 = { package = "mail-parser", git = "https://github.com/superboum/mail-parser", rev = "db61a03" }
|
|
||||||
|
|
|
@ -18,7 +18,13 @@ use imap_codec::types::fetch_attributes::{
|
||||||
use imap_codec::types::flag::{Flag, StoreResponse, StoreType};
|
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 mail_parser::*;
|
use eml_codec::{
|
||||||
|
imf::{self as imf},
|
||||||
|
part::{AnyPart},
|
||||||
|
part::discrete::{Text, Binary},
|
||||||
|
part::composite::{Message, Multipart},
|
||||||
|
mime,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::mail::mailbox::Mailbox;
|
use crate::mail::mailbox::Mailbox;
|
||||||
use crate::mail::uidindex::{ImapUid, ImapUidvalidity, UidIndex};
|
use crate::mail::uidindex::{ImapUid, ImapUidvalidity, UidIndex};
|
||||||
|
@ -32,6 +38,8 @@ const DEFAULT_FLAGS: [Flag; 5] = [
|
||||||
Flag::Draft,
|
Flag::Draft,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const BODY_CHECK: &str = "body attribute asked but only header is fetched, logic error";
|
||||||
|
|
||||||
/// 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
|
||||||
|
@ -283,12 +291,15 @@ 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 parsed = match &body {
|
let (parts, imf) = match &body {
|
||||||
Some(m) => {
|
Some(m) => {
|
||||||
mail_parser::Message::parse(m).ok_or_else(|| anyhow!("Invalid mail body"))?
|
let eml = eml_codec::parse_message(m).or(Err(anyhow!("Invalid mail body")))?.1;
|
||||||
|
(Some(eml.child), eml.imf)
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let imf = eml_codec::parse_imf(&meta.headers).or(Err(anyhow!("Invalid mail headers")))?.1;
|
||||||
|
(None, imf)
|
||||||
}
|
}
|
||||||
None => mail_parser::Message::parse(&meta.headers)
|
|
||||||
.ok_or_else(|| anyhow!("Invalid mail headers"))?,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
for attr in fetch_attrs.iter() {
|
for attr in fetch_attrs.iter() {
|
||||||
|
@ -308,42 +319,42 @@ impl MailboxView {
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
FetchAttribute::Rfc822Text => {
|
FetchAttribute::Rfc822Text => {
|
||||||
let rp = parsed.root_part();
|
//@FIXME this is not efficient, this is a hack as we need to patch
|
||||||
let r = parsed
|
// eml_codec to correctly implement this behavior
|
||||||
.raw_message
|
let txt = eml_codec::parse_imf(body.as_ref().expect(BODY_CHECK).as_slice())
|
||||||
.get(rp.offset_body..rp.offset_end)
|
.map(|(x, _)| x)
|
||||||
.ok_or(Error::msg(
|
.unwrap_or(b"");
|
||||||
"Unable to extract email body, cursors out of bound. This is a bug.",
|
|
||||||
))?;
|
|
||||||
|
|
||||||
attributes.push(MessageAttribute::Rfc822Text(NString(
|
attributes.push(MessageAttribute::Rfc822Text(NString(
|
||||||
r.try_into().ok().map(IString::Literal),
|
txt.try_into().ok().map(IString::Literal),
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
FetchAttribute::Rfc822 => attributes.push(MessageAttribute::Rfc822(NString(
|
FetchAttribute::Rfc822 => attributes.push(MessageAttribute::Rfc822(NString(
|
||||||
body.as_ref()
|
body.as_ref()
|
||||||
.unwrap()
|
.expect(BODY_CHECK)
|
||||||
.clone()
|
.clone()
|
||||||
.try_into()
|
.try_into()
|
||||||
.ok()
|
.ok()
|
||||||
.map(IString::Literal),
|
.map(IString::Literal),
|
||||||
))),
|
))),
|
||||||
FetchAttribute::Envelope => {
|
FetchAttribute::Envelope => {
|
||||||
attributes.push(MessageAttribute::Envelope(message_envelope(&parsed)))
|
attributes.push(MessageAttribute::Envelope(message_envelope(&imf)))
|
||||||
}
|
}
|
||||||
FetchAttribute::Body => attributes.push(MessageAttribute::Body(
|
FetchAttribute::Body => attributes.push(MessageAttribute::Body(
|
||||||
build_imap_email_struct(&parsed, parsed.root_part())?,
|
build_imap_email_struct(parts.as_ref().expect(BODY_CHECK).as_ref())?,
|
||||||
)),
|
)),
|
||||||
FetchAttribute::BodyStructure => attributes.push(MessageAttribute::Body(
|
FetchAttribute::BodyStructure => attributes.push(MessageAttribute::Body(
|
||||||
build_imap_email_struct(&parsed, parsed.root_part())?,
|
build_imap_email_struct(parts.as_ref().expect(BODY_CHECK).as_ref())?,
|
||||||
)),
|
)),
|
||||||
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(&parsed, section) {
|
/*match get_message_section(&parts.expect("body attribute asked but only header is fetched, logic error"), 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) {
|
||||||
|
@ -385,6 +396,7 @@ 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"))?;
|
||||||
|
@ -606,67 +618,48 @@ fn string_to_flag(f: &str) -> Option<Flag> {
|
||||||
|
|
||||||
//@FIXME return an error if the envelope is invalid instead of panicking
|
//@FIXME return an error if the envelope is invalid instead of panicking
|
||||||
//@FIXME some fields must be defaulted if there are not set.
|
//@FIXME some fields must be defaulted if there are not set.
|
||||||
fn message_envelope(msg: &mail_parser::Message<'_>) -> Envelope {
|
fn message_envelope(msg: &imf::Imf) -> Envelope {
|
||||||
let from = convert_addresses(msg.from()).unwrap_or(vec![]);
|
|
||||||
|
|
||||||
Envelope {
|
Envelope {
|
||||||
date: NString(
|
date: NString(
|
||||||
msg.date()
|
msg.date.as_ref()
|
||||||
.map(|d| IString::try_from(d.to_rfc3339()).unwrap()),
|
.map(|d| IString::try_from(d.to_rfc3339()).unwrap()),
|
||||||
),
|
),
|
||||||
subject: NString(
|
subject: NString(
|
||||||
msg.subject()
|
msg.subject.as_ref()
|
||||||
.map(|d| IString::try_from(d.to_string()).unwrap()),
|
.map(|d| IString::try_from(d.to_string()).unwrap()),
|
||||||
),
|
),
|
||||||
from: from.clone(),
|
from: msg.from.iter().map(convert_mbx).collect(),
|
||||||
sender: convert_addresses(msg.sender()).unwrap_or(from.clone()),
|
sender: msg.sender.iter().map(convert_mbx).collect(), //@FIXME put from[0] if empty
|
||||||
reply_to: convert_addresses(msg.reply_to()).unwrap_or(from),
|
reply_to: convert_addresses(&msg.reply_to), //@FIXME put from if empty
|
||||||
to: convert_addresses(msg.to()).unwrap_or(vec![]),
|
to: convert_addresses(&msg.to),
|
||||||
cc: convert_addresses(msg.cc()).unwrap_or(vec![]),
|
cc: convert_addresses(&msg.cc),
|
||||||
bcc: convert_addresses(msg.bcc()).unwrap_or(vec![]),
|
bcc: convert_addresses(&msg.bcc),
|
||||||
in_reply_to: NString(None), // @TODO
|
in_reply_to: NString(msg.in_reply_to.iter().next().map(|d| IString::try_from(d.to_string()).unwrap())),
|
||||||
message_id: NString(
|
message_id: NString(
|
||||||
msg.message_id()
|
msg.msg_id.as_ref().map(|d| IString::try_from(d.to_string()).unwrap()),
|
||||||
.map(|d| IString::try_from(d.to_string()).unwrap()),
|
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn convert_addresses(a: &mail_parser::HeaderValue<'_>) -> Option<Vec<Address>> {
|
fn convert_addresses(addrlist: &Vec<imf::address::AddressRef>) -> Vec<Address> {
|
||||||
match a {
|
let mut acc = vec![];
|
||||||
mail_parser::HeaderValue::Address(a) => Some(vec![convert_address(a)]),
|
for item in addrlist {
|
||||||
mail_parser::HeaderValue::AddressList(l) => {
|
match item {
|
||||||
Some(l.iter().map(convert_address).collect())
|
imf::address::AddressRef::Single(a) => acc.push(convert_mbx(a)),
|
||||||
}
|
imf::address::AddressRef::Many(l) => acc.extend(l.participants.iter().map(convert_mbx))
|
||||||
mail_parser::HeaderValue::Empty => None,
|
|
||||||
_ => {
|
|
||||||
tracing::warn!("Invalid address header");
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return acc
|
||||||
}
|
}
|
||||||
|
|
||||||
//@FIXME Remove unwrap
|
fn convert_mbx(addr: &imf::mailbox::MailboxRef) -> Address {
|
||||||
fn convert_address(a: &mail_parser::Addr<'_>) -> Address {
|
|
||||||
let (user, host) = match &a.address {
|
|
||||||
None => (None, None),
|
|
||||||
Some(x) => match x.split_once('@') {
|
|
||||||
Some((u, h)) => (Some(u.to_string()), Some(h.to_string())),
|
|
||||||
None => (Some(x.to_string()), None),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
Address::new(
|
Address::new(
|
||||||
NString(
|
NString(addr.name.as_ref().map(|x| IString::try_from(x.to_string()).unwrap())),
|
||||||
a.name
|
|
||||||
.as_ref()
|
|
||||||
.map(|x| IString::try_from(x.to_string()).unwrap()),
|
|
||||||
),
|
|
||||||
// SMTP at-domain-list (source route) seems obsolete since at least 1991
|
// SMTP at-domain-list (source route) seems obsolete since at least 1991
|
||||||
// https://www.mhonarc.org/archive/html/ietf-822/1991-06/msg00060.html
|
// https://www.mhonarc.org/archive/html/ietf-822/1991-06/msg00060.html
|
||||||
NString(None),
|
NString(None),
|
||||||
NString(user.map(|x| IString::try_from(x).unwrap())),
|
NString(Some(IString::try_from(addr.addrspec.local_part.to_string()).unwrap())),
|
||||||
NString(host.map(|x| IString::try_from(x).unwrap())),
|
NString(Some(IString::try_from(addr.addrspec.domain.to_string()).unwrap())),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -685,28 +678,19 @@ b fetch 29878:29879 (BODY)
|
||||||
b OK Fetch completed (0.001 + 0.000 secs).
|
b OK Fetch completed (0.001 + 0.000 secs).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
fn build_imap_email_struct<'a>(msg: &Message<'a>, part: &MessagePart<'a>) -> Result<BodyStructure> {
|
fn build_imap_email_struct<'a>(part: &AnyPart<'a>) -> Result<BodyStructure> {
|
||||||
match &part.body {
|
match part {
|
||||||
PartType::Multipart(parts) => {
|
AnyPart::Mult(x) => {
|
||||||
let subtype = IString::try_from(
|
let subtype = x.interpreted.parsed.ctype.as_ref()
|
||||||
part.headers
|
.map(|x| IString::try_from(String::from_utf8_lossy(x.sub).to_string()).ok())
|
||||||
.rfc(&RfcHeader::ContentType)
|
.flatten()
|
||||||
.ok_or(anyhow!("Content-Type is missing but required here."))?
|
.unwrap_or(unchecked_istring("alternative"));
|
||||||
.content_type()
|
|
||||||
.c_subtype
|
|
||||||
.as_ref()
|
|
||||||
.ok_or(anyhow!("Content-Type invalid, missing subtype"))?
|
|
||||||
.to_string(),
|
|
||||||
)
|
|
||||||
.map_err(|_| {
|
|
||||||
anyhow!("Unable to build IString from given Content-Type subtype given")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(BodyStructure::Multi {
|
Ok(BodyStructure::Multi {
|
||||||
bodies: parts
|
bodies: x.children
|
||||||
.iter()
|
.iter()
|
||||||
.map(|index| build_imap_email_struct(msg, &msg.parts[*index]))
|
.filter_map(|inner| build_imap_email_struct(&inner).ok())
|
||||||
.fold(Ok(vec![]), try_collect_shime)?,
|
.collect(),
|
||||||
subtype,
|
subtype,
|
||||||
extension_data: None,
|
extension_data: None,
|
||||||
/*Some(MultipartExtensionData {
|
/*Some(MultipartExtensionData {
|
||||||
|
@ -718,31 +702,22 @@ fn build_imap_email_struct<'a>(msg: &Message<'a>, part: &MessagePart<'a>) -> Res
|
||||||
})*/
|
})*/
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
PartType::Text(bp) | PartType::Html(bp) => {
|
AnyPart::Txt(x) => {
|
||||||
let (attrs, mut basic) = headers_to_basic_fields(part, bp.len())?;
|
//@FIXME check if we must really guess a charset if none is provided, if so we must
|
||||||
|
//update this code
|
||||||
|
let basic = basic_fields(&x.interpreted.parsed)?;
|
||||||
|
|
||||||
// If the charset is not defined, set it to "us-ascii"
|
let subtype = x.interpreted.parsed.ctype.as_ref()
|
||||||
if attrs.charset.is_none() {
|
.map(|x| IString::try_from(String::from_utf8_lossy(x.sub).to_string()).ok())
|
||||||
basic
|
.flatten()
|
||||||
.parameter_list
|
|
||||||
.push((unchecked_istring("charset"), unchecked_istring("us-ascii")));
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the subtype is not defined, set it to "plain". MIME (RFC2045) says that subtype
|
|
||||||
// MUST be defined and hence has no default. But mail-parser does not make any
|
|
||||||
// difference between MIME and raw emails, hence raw emails have no subtypes.
|
|
||||||
let subtype = part
|
|
||||||
.content_type()
|
|
||||||
.and_then(|h| h.c_subtype.as_ref())
|
|
||||||
.and_then(|st| IString::try_from(st.to_string()).ok())
|
|
||||||
.unwrap_or(unchecked_istring("plain"));
|
.unwrap_or(unchecked_istring("plain"));
|
||||||
|
|
||||||
let number_of_lines = msg
|
let number_of_lines = x.body.iter()
|
||||||
.raw_message
|
.filter(|x| **x == b'\n')
|
||||||
.get(part.offset_body..part.offset_end)
|
.count()
|
||||||
.map(|text| text.iter().filter(|x| **x == b'\n').count())
|
.try_into()
|
||||||
.unwrap_or(0)
|
.unwrap_or(0);
|
||||||
.try_into()?;
|
|
||||||
|
|
||||||
Ok(BodyStructure::Single {
|
Ok(BodyStructure::Single {
|
||||||
body: FetchBody {
|
body: FetchBody {
|
||||||
|
@ -755,26 +730,19 @@ fn build_imap_email_struct<'a>(msg: &Message<'a>, part: &MessagePart<'a>) -> Res
|
||||||
extension: None,
|
extension: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
PartType::Binary(bp) | PartType::InlineBinary(bp) => {
|
AnyPart::Bin(x) => {
|
||||||
let (_, basic) = headers_to_basic_fields(part, bp.len())?;
|
//let (_, basic) = headers_to_basic_fields(part, bp.len())?;
|
||||||
|
let basic = basic_fields(&x.interpreted.parsed)?;
|
||||||
|
|
||||||
let ct = part
|
let default = mime::r#type::NaiveType { main: &[], sub: &[], params: vec![] };
|
||||||
.content_type()
|
let ct = x.interpreted.parsed.ctype.as_ref().unwrap_or(&default);
|
||||||
.ok_or(anyhow!("Content-Type is missing but required here."))?;
|
|
||||||
|
|
||||||
let type_ = IString::try_from(ct.c_type.as_ref().to_string()).map_err(|_| {
|
let type_ = IString::try_from(String::from_utf8_lossy(ct.main).to_string())
|
||||||
anyhow!("Unable to build IString from given Content-Type type given")
|
.or(Err(anyhow!("Unable to build IString from given Content-Type type given")))?;
|
||||||
})?;
|
|
||||||
|
|
||||||
let subtype = IString::try_from(
|
|
||||||
ct.c_subtype
|
let subtype = IString::try_from(String::from_utf8_lossy(ct.sub).to_string())
|
||||||
.as_ref()
|
.or(Err(anyhow!("Unable to build IString from given Content-Type subtype given")))?;
|
||||||
.ok_or(anyhow!("Content-Type invalid, missing subtype"))?
|
|
||||||
.to_string(),
|
|
||||||
)
|
|
||||||
.map_err(|_| {
|
|
||||||
anyhow!("Unable to build IString from given Content-Type subtype given")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(BodyStructure::Single {
|
Ok(BodyStructure::Single {
|
||||||
body: FetchBody {
|
body: FetchBody {
|
||||||
|
@ -784,31 +752,22 @@ fn build_imap_email_struct<'a>(msg: &Message<'a>, part: &MessagePart<'a>) -> Res
|
||||||
extension: None,
|
extension: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
PartType::Message(inner) => {
|
AnyPart::Msg(x) => {
|
||||||
let (_, basic) = headers_to_basic_fields(part, inner.raw_message().len())?;
|
let basic = basic_fields(&x.interpreted.parsed)?;
|
||||||
|
|
||||||
// We do not count the number of lines but the number of line
|
// We do not count the number of lines but the number of line
|
||||||
// feeds to have the same behavior as Dovecot and Cyrus.
|
// feeds to have the same behavior as Dovecot and Cyrus.
|
||||||
// 2 lines = 1 line feed.
|
// 2 lines = 1 line feed.
|
||||||
let nol = inner.raw_message().iter().filter(|&c| c == &b'\n').count();
|
//let nol = inner.raw_message().iter().filter(|&c| c == &b'\n').count();
|
||||||
|
let nol = 0; // @FIXME broken for now
|
||||||
|
|
||||||
Ok(BodyStructure::Single {
|
Ok(BodyStructure::Single {
|
||||||
body: FetchBody {
|
body: FetchBody {
|
||||||
basic,
|
basic,
|
||||||
specific: SpecificFields::Message {
|
specific: SpecificFields::Message {
|
||||||
envelope: message_envelope(inner),
|
envelope: message_envelope(&x.imf),
|
||||||
body_structure: Box::new(build_imap_email_struct(
|
body_structure: Box::new(build_imap_email_struct(x.child.as_ref())?),
|
||||||
inner,
|
number_of_lines: u32::try_from(nol)?,
|
||||||
inner.root_part(),
|
|
||||||
)?),
|
|
||||||
|
|
||||||
// @FIXME This solution is bad for 2 reasons:
|
|
||||||
// - RFC2045 says line endings are CRLF but we accept LF alone with
|
|
||||||
// this method. It could be a feature (be liberal in what you
|
|
||||||
// accept) but we must be sure that we don't break things.
|
|
||||||
// - It should be done during parsing, we are iterating twice on
|
|
||||||
// the same data which results in some wastes.
|
|
||||||
number_of_lines: u32::try_from(nol)?,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
extension: None,
|
extension: None,
|
||||||
|
@ -817,103 +776,45 @@ fn build_imap_email_struct<'a>(msg: &Message<'a>, part: &MessagePart<'a>) -> Res
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn try_collect_shime<T>(acc: Result<Vec<T>>, elem: Result<T>) -> Result<Vec<T>> {
|
|
||||||
match (acc, elem) {
|
|
||||||
(Err(e), _) | (_, Err(e)) => Err(e),
|
|
||||||
(Ok(mut ac), Ok(el)) => {
|
|
||||||
ac.push(el);
|
|
||||||
Ok(ac)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// s is set to static to ensure that only compile time values
|
/// s is set to static to ensure that only compile time values
|
||||||
/// checked by developpers are passed.
|
/// checked by developpers are passed.
|
||||||
fn unchecked_istring(s: &'static str) -> IString {
|
fn unchecked_istring(s: &'static str) -> IString {
|
||||||
IString::try_from(s).expect("this value is expected to be a valid imap-codec::IString")
|
IString::try_from(s).expect("this value is expected to be a valid imap-codec::IString")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
fn basic_fields(m: &mime::NaiveMIME) -> Result<BasicFields> {
|
||||||
struct SpecialAttrs<'a> {
|
let parameter_list = m.ctype
|
||||||
charset: Option<&'a Cow<'a, str>>,
|
.as_ref()
|
||||||
boundary: Option<&'a Cow<'a, str>>,
|
.map(|x| x.params.iter()
|
||||||
}
|
.map(|p| (IString::try_from(String::from_utf8_lossy(p.name).to_string()), IString::try_from(p.value.to_string())))
|
||||||
|
.filter(|(k, v)| k.is_ok() && v.is_ok())
|
||||||
|
.map(|(k, v)| (k.unwrap(), v.unwrap()))
|
||||||
|
.collect())
|
||||||
|
.unwrap_or(vec![]);
|
||||||
|
|
||||||
/// Takes mail-parser Content-Type attributes, build imap-codec BasicFields.parameter_list and
|
Ok(BasicFields {
|
||||||
/// identify some specific attributes (charset and boundary).
|
|
||||||
fn attrs_to_params<'a>(bp: &impl MimeHeaders<'a>) -> (SpecialAttrs, Vec<(IString, IString)>) {
|
|
||||||
// Try to extract Content-Type attributes from headers
|
|
||||||
let attrs = match bp
|
|
||||||
.content_type()
|
|
||||||
.and_then(|c| c.attributes.as_ref())
|
|
||||||
{
|
|
||||||
Some(v) => v,
|
|
||||||
_ => return (SpecialAttrs::default(), vec![]),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Transform the Content-Type attributes into IMAP's parameter list
|
|
||||||
// Also collect some special attributes that might be used elsewhere
|
|
||||||
attrs.iter().fold(
|
|
||||||
(SpecialAttrs::default(), vec![]),
|
|
||||||
|(mut sa, mut param_list), (k, v)| {
|
|
||||||
let nk = k.to_lowercase();
|
|
||||||
match (IString::try_from(k.as_ref()), IString::try_from(v.as_ref())) {
|
|
||||||
(Ok(ik), Ok(iv)) => param_list.push((ik, iv)),
|
|
||||||
_ => return (sa, param_list),
|
|
||||||
};
|
|
||||||
|
|
||||||
match nk.as_str() {
|
|
||||||
"charset" => {
|
|
||||||
sa.charset = Some(v);
|
|
||||||
}
|
|
||||||
"boundary" => {
|
|
||||||
sa.boundary = Some(v);
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
};
|
|
||||||
|
|
||||||
(sa, param_list)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Takes mail-parser headers and build imap-codec BasicFields
|
|
||||||
/// Return some special informations too
|
|
||||||
fn headers_to_basic_fields<'a>(
|
|
||||||
bp: &'a MessagePart<'a>,
|
|
||||||
size: usize,
|
|
||||||
) -> Result<(SpecialAttrs<'a>, BasicFields)> {
|
|
||||||
let (attrs, parameter_list) = attrs_to_params(bp);
|
|
||||||
|
|
||||||
let bf = BasicFields {
|
|
||||||
parameter_list,
|
parameter_list,
|
||||||
|
|
||||||
id: NString(
|
id: NString(
|
||||||
bp.content_id()
|
m.id.as_ref()
|
||||||
.and_then(|ci| IString::try_from(ci.to_string()).ok()),
|
.and_then(|ci| IString::try_from(ci.to_string()).ok()),
|
||||||
),
|
),
|
||||||
|
|
||||||
description: NString(
|
description: NString(
|
||||||
bp.content_description()
|
m.description.as_ref()
|
||||||
.and_then(|cd| IString::try_from(cd.to_string()).ok()),
|
.and_then(|cd| IString::try_from(cd.to_string()).ok()),
|
||||||
),
|
),
|
||||||
|
content_transfer_encoding: match m.transfer_encoding {
|
||||||
/*
|
mime::mechanism::Mechanism::_8Bit => unchecked_istring("8bit"),
|
||||||
* RFC2045 - section 6.1
|
mime::mechanism::Mechanism::Binary => unchecked_istring("binary"),
|
||||||
* "Content-Transfer-Encoding: 7BIT" is assumed if the
|
mime::mechanism::Mechanism::QuotedPrintable => unchecked_istring("quoted-printable"),
|
||||||
* Content-Transfer-Encoding header field is not present.
|
mime::mechanism::Mechanism::Base64 => unchecked_istring("base64"),
|
||||||
*/
|
_ => unchecked_istring("7bit"),
|
||||||
content_transfer_encoding: bp
|
},
|
||||||
.content_transfer_encoding()
|
// @FIXME we can't compute the size of the message currently...
|
||||||
.and_then(|h| IString::try_from(h.to_string()).ok())
|
size: u32::try_from(0)?,
|
||||||
.unwrap_or(unchecked_istring("7bit")),
|
})
|
||||||
|
|
||||||
size: u32::try_from(size)?,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok((attrs, bf))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
fn get_message_section<'a>(
|
fn get_message_section<'a>(
|
||||||
parsed: &'a Message<'a>,
|
parsed: &'a Message<'a>,
|
||||||
section: &Option<FetchSection>,
|
section: &Option<FetchSection>,
|
||||||
|
@ -993,10 +894,10 @@ fn get_message_section<'a>(
|
||||||
}
|
}
|
||||||
Some(FetchSection::Part(part)) => map_subpart(parsed, part.0.as_slice(), |_msg, part| {
|
Some(FetchSection::Part(part)) => map_subpart(parsed, part.0.as_slice(), |_msg, part| {
|
||||||
let bytes = match &part.body {
|
let bytes = match &part.body {
|
||||||
PartType::Text(p) | PartType::Html(p) => p.as_bytes().to_vec(),
|
AnyPart::Txt(p) => p.as_bytes().to_vec(),
|
||||||
PartType::Binary(p) | PartType::InlineBinary(p) => p.to_vec(),
|
AnyPart::Bin(p) => p.to_vec(),
|
||||||
PartType::Message(p) => p.raw_message.to_vec(),
|
AnyPart::Msg(p) => p.raw_message.to_vec(),
|
||||||
PartType::Multipart(_) => bail!("Multipart part has no body"),
|
AnyPart::Multipart(_) => bail!("Multipart part has no body"),
|
||||||
};
|
};
|
||||||
Ok(bytes.into())
|
Ok(bytes.into())
|
||||||
}),
|
}),
|
||||||
|
@ -1052,7 +953,7 @@ where
|
||||||
bail!("Subpart is not a message: {}", path[0]);
|
bail!("Subpart is not a message: {}", path[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
@ -1084,7 +985,7 @@ mod tests {
|
||||||
// "tests/emails/rfc/000",
|
// "tests/emails/rfc/000",
|
||||||
|
|
||||||
// "tests/emails/rfc/001", // broken
|
// "tests/emails/rfc/001", // broken
|
||||||
// "tests/emails/rfc/002", // broken: dovecot adds \r when it is missing and count is as
|
// "tests/emails/rfc/002", // broken: dovecot adds \r when it is missing and count it as
|
||||||
// a character. Difference on how lines are counted too.
|
// a character. Difference on how lines are counted too.
|
||||||
/*"tests/emails/rfc/003", // broken for the same reason
|
/*"tests/emails/rfc/003", // broken for the same reason
|
||||||
"tests/emails/thirdparty/000",
|
"tests/emails/thirdparty/000",
|
||||||
|
@ -1097,7 +998,7 @@ mod tests {
|
||||||
println!("{}", pref);
|
println!("{}", pref);
|
||||||
let txt = fs::read(format!("{}.eml", pref))?;
|
let txt = fs::read(format!("{}.eml", pref))?;
|
||||||
let exp = fs::read(format!("{}.dovecot.body", pref))?;
|
let exp = fs::read(format!("{}.dovecot.body", pref))?;
|
||||||
let message = Message::parse(&txt).unwrap();
|
let message = eml_codec::email(&txt).unwrap();
|
||||||
|
|
||||||
let mut resp = Vec::new();
|
let mut resp = Vec::new();
|
||||||
MessageAttribute::Body(build_imap_email_struct(&message, message.root_part())?)
|
MessageAttribute::Body(build_imap_email_struct(&message, message.root_part())?)
|
||||||
|
|
|
@ -7,7 +7,7 @@ use std::sync::Arc;
|
||||||
use anyhow::{anyhow, bail, Context, Result};
|
use anyhow::{anyhow, bail, Context, Result};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use k2v_client::{
|
use k2v_client::{
|
||||||
BatchInsertOp, BatchReadOp, CausalValue, CausalityToken, Filter, K2vClient, K2vValue,
|
BatchInsertOp, BatchReadOp, CausalValue, CausalityToken, Filter, K2vClient, K2vValue, K2vClientConfig
|
||||||
};
|
};
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use rusoto_core::HttpClient;
|
use rusoto_core::HttpClient;
|
||||||
|
@ -120,19 +120,16 @@ impl Credentials {
|
||||||
|
|
||||||
impl StorageCredentials {
|
impl StorageCredentials {
|
||||||
pub fn k2v_client(&self) -> Result<K2vClient> {
|
pub fn k2v_client(&self) -> Result<K2vClient> {
|
||||||
let aws_creds = AwsCredentials::new(
|
let config = K2vClientConfig {
|
||||||
self.aws_access_key_id.clone(),
|
endpoint: self.k2v_region.endpoint.clone(),
|
||||||
self.aws_secret_access_key.clone(),
|
region: self.k2v_region.name.clone(),
|
||||||
None,
|
aws_access_key_id: self.aws_access_key_id.clone(),
|
||||||
None,
|
aws_secret_access_key: self.aws_secret_access_key.clone(),
|
||||||
);
|
bucket: self.bucket.clone(),
|
||||||
|
user_agent: None,
|
||||||
|
};
|
||||||
|
|
||||||
Ok(K2vClient::new(
|
Ok(K2vClient::new(config)?)
|
||||||
self.k2v_region.as_rusoto_region(),
|
|
||||||
self.bucket.clone(),
|
|
||||||
aws_creds,
|
|
||||||
None,
|
|
||||||
)?)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn s3_client(&self) -> Result<S3Client> {
|
pub fn s3_client(&self) -> Result<S3Client> {
|
||||||
|
|
|
@ -315,10 +315,10 @@ impl MailboxInternal {
|
||||||
},
|
},
|
||||||
async {
|
async {
|
||||||
// Save mail meta
|
// Save mail meta
|
||||||
let mail_root = mail.parsed.root_part();
|
let mail_root = mail.parsed.imf;
|
||||||
let meta = MailMeta {
|
let meta = MailMeta {
|
||||||
internaldate: now_msec(),
|
internaldate: now_msec(),
|
||||||
headers: mail.raw[..mail_root.offset_body].to_vec(),
|
headers: vec![],
|
||||||
message_key: message_key.clone(),
|
message_key: message_key.clone(),
|
||||||
rfc822_size: mail.raw.len(),
|
rfc822_size: mail.raw.len(),
|
||||||
};
|
};
|
||||||
|
@ -368,10 +368,10 @@ impl MailboxInternal {
|
||||||
},
|
},
|
||||||
async {
|
async {
|
||||||
// Save mail meta
|
// Save mail meta
|
||||||
let mail_root = mail.parsed.root_part();
|
let mail_root = mail.parsed.imf;
|
||||||
let meta = MailMeta {
|
let meta = MailMeta {
|
||||||
internaldate: now_msec(),
|
internaldate: now_msec(),
|
||||||
headers: mail.raw[..mail_root.offset_body].to_vec(),
|
headers: vec![], //@FIXME we need to put the headers part
|
||||||
message_key: message_key.clone(),
|
message_key: message_key.clone(),
|
||||||
rfc822_size: mail.raw.len(),
|
rfc822_size: mail.raw.len(),
|
||||||
};
|
};
|
||||||
|
|
|
@ -13,7 +13,7 @@ pub mod user;
|
||||||
#[allow(clippy::upper_case_acronyms)]
|
#[allow(clippy::upper_case_acronyms)]
|
||||||
pub struct IMF<'a> {
|
pub struct IMF<'a> {
|
||||||
raw: &'a [u8],
|
raw: &'a [u8],
|
||||||
parsed: mail_parser::Message<'a>,
|
parsed: eml_codec::part::composite::Message<'a>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> TryFrom<&'a [u8]> for IMF<'a> {
|
impl<'a> TryFrom<&'a [u8]> for IMF<'a> {
|
||||||
|
@ -23,7 +23,7 @@ impl<'a> TryFrom<&'a [u8]> for IMF<'a> {
|
||||||
eprintln!("---- BEGIN PARSED MESSAGE ----");
|
eprintln!("---- BEGIN PARSED MESSAGE ----");
|
||||||
let _ = std::io::stderr().write_all(body);
|
let _ = std::io::stderr().write_all(body);
|
||||||
eprintln!("---- END PARSED MESSAGE ----");
|
eprintln!("---- END PARSED MESSAGE ----");
|
||||||
let parsed = mail_parser::Message::parse(body).ok_or(())?;
|
let parsed = eml_codec::parse_message(body).or(Err(()))?.1;
|
||||||
Ok(Self { raw: body, parsed })
|
Ok(Self { raw: body, parsed })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue