use super::cfws;
use crate::{
class,
error::{ErrorKind, ParseError, Result, WriteError, err},
limits::Limits,
scan::control_kind,
text::{Quoted, comment_end_folded, parse_quoted, skip_ows, trim_ows},
write::{CountSink, Sink, reserve},
};
const LOCAL_LIMIT: usize = 64;
const ADDRESS_LIMIT: usize = 254;
fn refused(v: &[u8], at: usize, kind: ErrorKind) -> ParseError {
match v.get(at) {
Some(&b) if crate::scan::is_control(b) => ParseError::new(control_kind(b), at),
_ => ParseError::new(kind, at),
}
}
fn check_dot_atom(seg: &[u8], base: usize) -> Result<()> {
if seg.is_empty() {
return err(ErrorKind::InvalidAddrSpec, base);
}
if let Err(e) = core::str::from_utf8(seg) {
return err(ErrorKind::InvalidUtf8, base + e.valid_up_to());
}
let mut prev_dot = true;
for (i, &b) in seg.iter().enumerate() {
if b == b'.' {
if prev_dot {
return err(ErrorKind::InvalidAddrSpec, base + i);
}
prev_dot = true;
} else if class::is_atext(b) || b >= 0x80 {
prev_dot = false;
} else {
return Err(refused(seg, i, ErrorKind::InvalidAddrSpec).shifted(base));
}
}
if prev_dot {
return err(ErrorKind::InvalidAddrSpec, base + seg.len() - 1);
}
Ok(())
}
fn check_dtext(seg: &[u8], base: usize) -> Result<()> {
if let Err(e) = core::str::from_utf8(seg) {
return err(ErrorKind::InvalidUtf8, base + e.valid_up_to());
}
match seg
.iter()
.position(|&b| !matches!(b, 33..=90 | 94..=126 | 0x80..))
{
Some(i) => Err(refused(seg, i, ErrorKind::InvalidAddrSpec).shifted(base)),
None => Ok(()),
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct AddrSpec<'a> {
pub local: LocalPart<'a>,
pub domain: Domain<'a>,
}
impl AddrSpec<'_> {
#[must_use]
pub fn is_ascii(&self) -> bool {
let local = match self.local {
LocalPart::DotAtom(a) => a,
LocalPart::Quoted(q) => q.raw(),
};
let domain = match self.domain {
Domain::DotAtom(d) | Domain::Literal(d) => d,
};
local.is_ascii() && domain.is_ascii()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum LocalPart<'a> {
DotAtom(&'a [u8]),
Quoted(Quoted<'a>),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Domain<'a> {
DotAtom(&'a [u8]),
Literal(&'a [u8]),
}
pub fn parse_addr_spec(value: &[u8]) -> Result<AddrSpec<'_>> {
let lead = skip_ows(value);
let v = trim_ows(value);
addr_spec(v).map_err(|e| e.shifted(lead))
}
fn addr_spec(v: &[u8]) -> Result<AddrSpec<'_>> {
let (local, at) = if v.first() == Some(&b'"') {
let (q, used) = parse_quoted(v)?;
if let Err(e) = core::str::from_utf8(q.raw()) {
return err(ErrorKind::InvalidUtf8, 1 + e.valid_up_to());
}
(LocalPart::Quoted(q), used)
} else {
let at = v.iter().position(|&b| b == b'@').unwrap_or(v.len());
let seg = v.get(..at).unwrap_or(&[]);
check_dot_atom(seg, 0)?;
(LocalPart::DotAtom(seg), at)
};
if at > LOCAL_LIMIT {
return err(ErrorKind::TooLong, LOCAL_LIMIT);
}
if v.get(at) != Some(&b'@') {
return Err(refused(v, at, ErrorKind::InvalidAddrSpec));
}
let start = at + 1;
let rest = v.get(start..).unwrap_or(&[]);
let domain = if rest.first() == Some(&b'[') {
let Some((&b']', inner)) = rest
.split_last()
.map(|(l, h)| (l, h.get(1..).unwrap_or(&[])))
else {
return err(ErrorKind::InvalidAddrSpec, v.len());
};
check_dtext(inner, start + 1)?;
Domain::Literal(inner)
} else {
check_dot_atom(rest, start)?;
Domain::DotAtom(rest)
};
if v.len() > ADDRESS_LIMIT {
return err(ErrorKind::TooLong, ADDRESS_LIMIT);
}
Ok(AddrSpec { local, domain })
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Mailbox<'a> {
pub display_name: Option<&'a [u8]>,
pub addr: AddrSpec<'a>,
}
pub fn parse_mailbox<'a>(value: &'a [u8], limits: &Limits) -> Result<Mailbox<'a>> {
let lead = skip_ows(value);
let v = trim_ows(value);
let (mut i, mut has_word) = (0usize, false);
let lt = loop {
match v.get(i) {
None | Some(b'@') => {
let addr = addr_spec(v).map_err(|e| e.shifted(lead))?;
return Ok(Mailbox {
display_name: None,
addr,
});
}
Some(b'<') => break i,
Some(b'"') => {
let (q, used) =
parse_quoted(v.get(i..).unwrap_or(&[])).map_err(|e| e.shifted(lead + i))?;
if let Err(e) = core::str::from_utf8(q.raw()) {
return err(ErrorKind::InvalidUtf8, lead + i + 1 + e.valid_up_to());
}
has_word = true;
i += used;
}
Some(b'(') => {
i = comment_end_folded(v, i, limits)
.map_err(|e| e.shifted(lead))?
.0;
}
Some(b' ' | b'\t') => i += 1,
Some(&b) if class::is_atext(b) || b == b'.' || b >= 0x80 => {
has_word = true;
i += 1;
}
Some(_) => return Err(refused(v, i, ErrorKind::InvalidAddrSpec).shifted(lead)),
}
};
let mut j = lt + 1;
let gt = loop {
match v.get(j) {
None => return err(ErrorKind::InvalidAddrSpec, lead + j),
Some(b'>') => break j,
Some(b'"') => {
let (_, used) =
parse_quoted(v.get(j..).unwrap_or(&[])).map_err(|e| e.shifted(lead + j))?;
j += used;
}
Some(_) => j += 1,
}
};
let end = cfws(v, gt + 1, limits).map_err(|e| e.shifted(lead))?;
if end < v.len() {
return Err(refused(v, end, ErrorKind::InvalidAddrSpec).shifted(lead));
}
let display = trim_ows(v.get(..lt).unwrap_or(&[]));
if let Err(e) = core::str::from_utf8(display) {
return err(ErrorKind::InvalidUtf8, lead + e.valid_up_to());
}
let inner = v.get(lt + 1..gt).unwrap_or(&[]);
let addr = addr_spec(inner).map_err(|e| e.shifted(lead + lt + 1))?;
Ok(Mailbox {
display_name: has_word.then_some(display),
addr,
})
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct MsgId<'a> {
pub left: &'a [u8],
pub right: &'a [u8],
}
pub fn parse_msg_id(value: &[u8]) -> Result<MsgId<'_>> {
let lead = skip_ows(value);
let v = trim_ows(value);
let inner = match (v.first(), v.last()) {
(Some(b'<'), Some(b'>')) if v.len() >= 2 => v.get(1..v.len() - 1).unwrap_or(&[]),
_ => return err(ErrorKind::InvalidAddrSpec, lead),
};
let at = inner.iter().position(|&b| b == b'@').unwrap_or(inner.len());
let left = inner.get(..at).unwrap_or(&[]);
check_dot_atom(left, lead + 1)?;
if inner.get(at) != Some(&b'@') {
return err(ErrorKind::InvalidAddrSpec, lead + 1 + at);
}
let right = inner.get(at + 1..).unwrap_or(&[]);
let base = lead + 2 + at;
match (right.first(), right.last()) {
(Some(b'['), Some(b']')) if right.len() >= 2 => {
check_dtext(right.get(1..right.len() - 1).unwrap_or(&[]), base + 1)?
}
_ => check_dot_atom(right, base)?,
}
Ok(MsgId { left, right })
}
fn is_ascii_dot_atom(s: &[u8]) -> Option<usize> {
let mut prev_dot = true;
for (i, &b) in s.iter().enumerate() {
if b == b'.' {
if prev_dot {
return Some(i);
}
prev_dot = true;
} else if class::is_atext(b) {
prev_dot = false;
} else {
return Some(i);
}
}
prev_dot.then_some(s.len().saturating_sub(1))
}
pub fn write_msg_id(
left: &str,
right: &str,
out: &mut impl Sink,
) -> core::result::Result<(), WriteError> {
let (l, r) = (left.as_bytes(), right.as_bytes());
if let Some(i) = is_ascii_dot_atom(l) {
return Err(WriteError::forbidden(i));
}
let bad_right = match (r.first(), r.last()) {
(Some(b'['), Some(b']')) if r.len() >= 2 => r
.get(1..r.len() - 1)
.unwrap_or(&[])
.iter()
.position(|&b| !matches!(b, 33..=90 | 94..=126))
.map(|i| i + 1),
_ => is_ascii_dot_atom(r),
};
if let Some(i) = bad_right {
return Err(WriteError::forbidden(l.len() + i));
}
let mut count = CountSink::new();
put_msg_id(l, r, &mut count)?;
if count.count() > super::HARD_LIMIT {
return Err(WriteError::OutOfRange);
}
reserve(out, count.count())?;
put_msg_id(l, r, out)
}
fn put_msg_id(l: &[u8], r: &[u8], out: &mut impl Sink) -> core::result::Result<(), WriteError> {
out.put(b"<")?;
out.put(l)?;
out.put(b"@")?;
out.put(r)?;
out.put(b">")?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::vec::Vec;
fn k(v: &[u8]) -> ErrorKind {
parse_addr_spec(v).unwrap_err().kind()
}
#[test]
fn test_addr_spec_edge_cases() {
let a = parse_addr_spec(b"\"john doe\"@example.com").unwrap();
assert!(matches!(a.local, LocalPart::Quoted(q) if q.eq_bytes(b"john doe")));
assert_eq!(a.domain, Domain::DotAtom(b"example.com"));
assert!(parse_addr_spec(b"a.b+tag@sub.example.co").is_ok());
assert_eq!(k(b"john..doe@example.com"), ErrorKind::InvalidAddrSpec);
assert_eq!(k(b".john@example.com"), ErrorKind::InvalidAddrSpec);
assert_eq!(k(b"john.@example.com"), ErrorKind::InvalidAddrSpec);
assert_eq!(k(b"john@example..com"), ErrorKind::InvalidAddrSpec);
assert_eq!(k(b"john@"), ErrorKind::InvalidAddrSpec);
assert_eq!(k(b"@example.com"), ErrorKind::InvalidAddrSpec);
assert_eq!(k(b"john.example.com"), ErrorKind::InvalidAddrSpec);
assert_eq!(k(b"john(c)@example.com"), ErrorKind::InvalidAddrSpec);
assert_eq!(k(b"john@example.com (c)"), ErrorKind::InvalidAddrSpec);
assert_eq!(k(b"a b@example.com"), ErrorKind::InvalidAddrSpec);
assert_eq!(k(b"a@b@example.com"), ErrorKind::InvalidAddrSpec);
assert_eq!(k(b"\"a@example.com"), ErrorKind::UnterminatedQuote);
assert_eq!(k(b"a\r\n@example.com"), ErrorKind::BareCr);
assert_eq!(k(b"j\xf6rg@example.de"), ErrorKind::InvalidUtf8);
assert!(
!parse_addr_spec("j\u{f6}rg@\u{e9}xample.de".as_bytes())
.unwrap()
.is_ascii()
);
let lit = parse_addr_spec(b"a@[IPv6:2001:db8::1]").unwrap();
assert_eq!(lit.domain, Domain::Literal(b"IPv6:2001:db8::1"));
assert_eq!(k(b"a@[1.2.3.4"), ErrorKind::InvalidAddrSpec);
assert_eq!(k(b"a@[a[b]"), ErrorKind::InvalidAddrSpec);
}
#[test]
fn test_addr_spec_lengths() {
let local = [b'a'; 64];
let ok = [&local[..], b"@example.com"].concat();
assert!(parse_addr_spec(&ok).is_ok());
let long = [&[b'a'; 65][..], b"@example.com"].concat();
assert_eq!(k(&long), ErrorKind::TooLong);
let mut domain = Vec::new();
while domain.len() < 250 {
domain.extend_from_slice(b"abcdefghi.");
}
domain.truncate(252);
domain.push(b'x');
let a = [b"a@".as_slice(), &domain[..251]].concat();
assert_eq!(a.len(), 253);
let a = [b"ab@".as_slice(), &domain[..251]].concat();
assert_eq!((a.len(), parse_addr_spec(&a).is_ok()), (254, true));
let a = [b"abc@".as_slice(), &domain[..251]].concat();
assert_eq!(k(&a), ErrorKind::TooLong);
}
#[test]
fn test_mailbox_forms() {
let l = Limits::DEFAULT;
let m = parse_mailbox(b"John Q. Public <jqp@example.com>", &l).unwrap();
assert_eq!(m.display_name, Some(&b"John Q. Public"[..]));
let m = parse_mailbox(b"=?UTF-8?Q?J=C3=B6rg?= (work) <j@example.de>", &l).unwrap();
assert_eq!(m.display_name, Some(&b"=?UTF-8?Q?J=C3=B6rg?= (work)"[..]));
let m = parse_mailbox(b"<a@b.c>", &l).unwrap();
assert!(m.display_name.is_none());
assert_eq!(
parse_mailbox(b"\"a <b@c.d>", &l).unwrap_err().kind(),
ErrorKind::UnterminatedQuote
);
assert_eq!(
parse_mailbox(b"(x <a@b.c>", &l).unwrap_err().kind(),
ErrorKind::UnbalancedComment
);
assert_eq!(
parse_mailbox(b"a@b <a@b.c>", &l).unwrap_err().kind(),
ErrorKind::InvalidAddrSpec
);
assert_eq!(
parse_mailbox(b"x <a..b@c.d>", &l).unwrap_err().kind(),
ErrorKind::InvalidAddrSpec
);
assert_eq!(
parse_mailbox(b"x a@b.c>", &l).unwrap_err().kind(),
ErrorKind::InvalidAddrSpec
);
}
#[test]
fn test_msg_id_parse_and_write() {
assert!(parse_msg_id(b"<a@[127.0.0.1]>").is_ok());
assert_eq!(
parse_msg_id(b"a@b").unwrap_err().kind(),
ErrorKind::InvalidAddrSpec
);
assert_eq!(
parse_msg_id(b"<ab>").unwrap_err().kind(),
ErrorKind::InvalidAddrSpec
);
assert_eq!(
parse_msg_id(b"<a..b@c>").unwrap_err().kind(),
ErrorKind::InvalidAddrSpec
);
assert_eq!(
parse_msg_id(b"<>").unwrap_err().kind(),
ErrorKind::InvalidAddrSpec
);
let mut buf = [0u8; 64];
let mut out = crate::SliceSink::new(&mut buf);
assert_eq!(
write_msg_id("a b", "c", &mut out),
Err(WriteError::Forbidden { at: 1 })
);
assert_eq!(
write_msg_id("a", "c..d", &mut out),
Err(WriteError::Forbidden { at: 3 })
);
assert_eq!(
write_msg_id("", "c", &mut out),
Err(WriteError::Forbidden { at: 0 })
);
assert_eq!(
write_msg_id("a", "[x]y]", &mut out),
Err(WriteError::Forbidden { at: 3 })
);
assert!(out.is_empty());
write_msg_id("x.y", "[192.0.2.1]", &mut out).unwrap();
let id = parse_msg_id(out.written()).unwrap();
assert_eq!((id.left, id.right), (&b"x.y"[..], &b"[192.0.2.1]"[..]));
}
}