Skip to main content

parse_net/mail/
addr.rs

1//! `addr-spec`, `mailbox`, and `msg-id` (RFC 5322 sections 3.4 and 3.6.4),
2//! with UTF-8 per RFC 6532.
3
4use super::cfws;
5use crate::{
6    class,
7    error::{ErrorKind, ParseError, Result, WriteError, err},
8    limits::Limits,
9    scan::control_kind,
10    text::{Quoted, comment_end_folded, parse_quoted, skip_ows, trim_ows},
11    write::{CountSink, Sink, reserve},
12};
13
14/// The longest local part (RFC 5321 section 4.5.3.1.1).
15const LOCAL_LIMIT: usize = 64;
16/// The longest address: the 256-octet path limit less the angle brackets
17/// (RFC 5321 section 4.5.3.1.3).
18const ADDRESS_LIMIT: usize = 254;
19
20fn refused(v: &[u8], at: usize, kind: ErrorKind) -> ParseError {
21    match v.get(at) {
22        Some(&b) if crate::scan::is_control(b) => ParseError::new(control_kind(b), at),
23        _ => ParseError::new(kind, at),
24    }
25}
26
27/// Checks `seg` as a `dot-atom-text` with RFC 6532 UTF-8: atoms of `atext` or
28/// non-ASCII UTF-8, joined by single dots. `base` offsets the errors.
29fn check_dot_atom(seg: &[u8], base: usize) -> Result<()> {
30    if seg.is_empty() {
31        return err(ErrorKind::InvalidAddrSpec, base);
32    }
33    if let Err(e) = core::str::from_utf8(seg) {
34        return err(ErrorKind::InvalidUtf8, base + e.valid_up_to());
35    }
36    let mut prev_dot = true;
37    for (i, &b) in seg.iter().enumerate() {
38        if b == b'.' {
39            if prev_dot {
40                return err(ErrorKind::InvalidAddrSpec, base + i);
41            }
42            prev_dot = true;
43        } else if class::is_atext(b) || b >= 0x80 {
44            prev_dot = false;
45        } else {
46            return Err(refused(seg, i, ErrorKind::InvalidAddrSpec).shifted(base));
47        }
48    }
49    if prev_dot {
50        return err(ErrorKind::InvalidAddrSpec, base + seg.len() - 1);
51    }
52    Ok(())
53}
54
55/// `dtext` with RFC 6532 UTF-8: `%d33-90 / %d94-126`, or non-ASCII.
56fn check_dtext(seg: &[u8], base: usize) -> Result<()> {
57    if let Err(e) = core::str::from_utf8(seg) {
58        return err(ErrorKind::InvalidUtf8, base + e.valid_up_to());
59    }
60    match seg
61        .iter()
62        .position(|&b| !matches!(b, 33..=90 | 94..=126 | 0x80..))
63    {
64        Some(i) => Err(refused(seg, i, ErrorKind::InvalidAddrSpec).shifted(base)),
65        None => Ok(()),
66    }
67}
68
69/// An address: `local-part "@" domain`.
70///
71/// # Examples
72///
73/// ```
74/// use parse_net::mail::{self, Domain, LocalPart};
75///
76/// let a = mail::parse_addr_spec(b"jane.doe@example.com")?;
77/// assert_eq!(a.local, LocalPart::DotAtom(b"jane.doe"));
78/// assert_eq!(a.domain, Domain::DotAtom(b"example.com"));
79/// # Ok::<(), parse_net::ParseError>(())
80/// ```
81#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
82pub struct AddrSpec<'a> {
83    /// The part before `@`.
84    pub local: LocalPart<'a>,
85    /// The part after `@`.
86    pub domain: Domain<'a>,
87}
88
89impl AddrSpec<'_> {
90    /// Whether the address is all ASCII; `false` means delivery needs
91    /// SMTPUTF8 (RFC 6531).
92    ///
93    /// # Examples
94    ///
95    /// ```
96    /// use parse_net::mail;
97    ///
98    /// assert!(mail::parse_addr_spec(b"info@example.com")?.is_ascii());
99    /// assert!(!mail::parse_addr_spec("info@b\u{fc}cher.example".as_bytes())?.is_ascii());
100    /// # Ok::<(), parse_net::ParseError>(())
101    /// ```
102    #[must_use]
103    pub fn is_ascii(&self) -> bool {
104        let local = match self.local {
105            LocalPart::DotAtom(a) => a,
106            LocalPart::Quoted(q) => q.raw(),
107        };
108        let domain = match self.domain {
109            Domain::DotAtom(d) | Domain::Literal(d) => d,
110        };
111        local.is_ascii() && domain.is_ascii()
112    }
113}
114
115/// A local part.
116///
117/// # Examples
118///
119/// ```
120/// use parse_net::mail::{self, LocalPart};
121///
122/// let a = mail::parse_addr_spec(b"\"john doe\"@example.com")?;
123/// assert!(matches!(a.local, LocalPart::Quoted(q) if q.eq_bytes(b"john doe")));
124/// # Ok::<(), parse_net::ParseError>(())
125/// ```
126#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
127pub enum LocalPart<'a> {
128    /// `dot-atom-text`, as sent.
129    DotAtom(&'a [u8]),
130    /// A quoted string.
131    Quoted(Quoted<'a>),
132}
133
134/// A domain.
135///
136/// # Examples
137///
138/// ```
139/// use parse_net::mail::{self, Domain};
140///
141/// assert_eq!(mail::parse_addr_spec(b"a@Example.COM")?.domain, Domain::DotAtom(b"Example.COM"));
142/// assert_eq!(mail::parse_addr_spec(b"a@[IPv6:2001:db8::1]")?.domain, Domain::Literal(b"IPv6:2001:db8::1"));
143/// # Ok::<(), parse_net::ParseError>(())
144/// ```
145#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
146pub enum Domain<'a> {
147    /// `dot-atom-text`, as sent (compare it ASCII case-insensitively).
148    DotAtom(&'a [u8]),
149    /// A domain literal: the text inside `[` and `]`.
150    Literal(&'a [u8]),
151}
152
153/// Parses an `addr-spec` (RFC 5322 section 3.4.1) without obsolete forms,
154/// comments, or folding: a `dot-atom` or quoted local part, `@`, and a
155/// `dot-atom` domain or `[domain-literal]`. The local part is at most 64
156/// octets and the address at most 254; non-ASCII is accepted only as valid
157/// UTF-8 (RFC 6532). Surrounding SP and HTAB are stripped.
158///
159/// # Errors
160///
161/// [`ErrorKind::InvalidAddrSpec`] (a leading, trailing, or doubled dot, a
162/// missing `@`, a comment, a byte outside the grammar);
163/// [`ErrorKind::TooLong`]; [`ErrorKind::InvalidUtf8`];
164/// [`ErrorKind::UnterminatedQuote`].
165///
166/// # Examples
167///
168/// ```
169/// use parse_net::mail::{self, Domain};
170///
171/// let a = mail::parse_addr_spec(b"user@[192.0.2.1]")?;
172/// assert_eq!(a.domain, Domain::Literal(b"192.0.2.1"));
173/// assert!(!mail::parse_addr_spec("j\u{f6}rg@example.de".as_bytes())?.is_ascii());
174/// assert!(mail::parse_addr_spec(b"user(comment)@example.com").is_err());
175/// # Ok::<(), parse_net::ParseError>(())
176/// ```
177pub fn parse_addr_spec(value: &[u8]) -> Result<AddrSpec<'_>> {
178    let lead = skip_ows(value);
179    let v = trim_ows(value);
180    addr_spec(v).map_err(|e| e.shifted(lead))
181}
182
183fn addr_spec(v: &[u8]) -> Result<AddrSpec<'_>> {
184    let (local, at) = if v.first() == Some(&b'"') {
185        let (q, used) = parse_quoted(v)?;
186        if let Err(e) = core::str::from_utf8(q.raw()) {
187            return err(ErrorKind::InvalidUtf8, 1 + e.valid_up_to());
188        }
189        (LocalPart::Quoted(q), used)
190    } else {
191        let at = v.iter().position(|&b| b == b'@').unwrap_or(v.len());
192        let seg = v.get(..at).unwrap_or(&[]);
193        check_dot_atom(seg, 0)?;
194        (LocalPart::DotAtom(seg), at)
195    };
196    if at > LOCAL_LIMIT {
197        return err(ErrorKind::TooLong, LOCAL_LIMIT);
198    }
199    if v.get(at) != Some(&b'@') {
200        return Err(refused(v, at, ErrorKind::InvalidAddrSpec));
201    }
202    let start = at + 1;
203    let rest = v.get(start..).unwrap_or(&[]);
204    let domain = if rest.first() == Some(&b'[') {
205        let Some((&b']', inner)) = rest
206            .split_last()
207            .map(|(l, h)| (l, h.get(1..).unwrap_or(&[])))
208        else {
209            return err(ErrorKind::InvalidAddrSpec, v.len());
210        };
211        check_dtext(inner, start + 1)?;
212        Domain::Literal(inner)
213    } else {
214        check_dot_atom(rest, start)?;
215        Domain::DotAtom(rest)
216    };
217    if v.len() > ADDRESS_LIMIT {
218        return err(ErrorKind::TooLong, ADDRESS_LIMIT);
219    }
220    Ok(AddrSpec { local, domain })
221}
222
223/// A mailbox: an optional display name and an address.
224///
225/// # Examples
226///
227/// ```
228/// use parse_net::{mail::{self, Domain}, Limits};
229///
230/// let m = mail::parse_mailbox(b"Jane Doe <jane@example.com>", &Limits::DEFAULT)?;
231/// assert_eq!(m.display_name, Some(&b"Jane Doe"[..]));
232/// assert_eq!(m.addr.domain, Domain::DotAtom(b"example.com"));
233/// # Ok::<(), parse_net::ParseError>(())
234/// ```
235#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
236pub struct Mailbox<'a> {
237    /// The display name as sent (words, quoted strings, comments, possibly
238    /// encoded words: decode with [`decode_words`](super::decode_words)).
239    pub display_name: Option<&'a [u8]>,
240    /// The address.
241    pub addr: AddrSpec<'a>,
242}
243
244/// Parses a `mailbox` (RFC 5322 section 3.4): `[display-name] "<" addr-spec
245/// ">"` or a bare `addr-spec`. The display name is checked as a phrase
246/// (atoms, dots, quoted strings, comments, UTF-8) and returned raw.
247///
248/// # Errors
249///
250/// As [`parse_addr_spec`]; [`ErrorKind::UnbalancedComment`],
251/// [`ErrorKind::CommentTooDeep`].
252///
253/// # Examples
254///
255/// ```
256/// use parse_net::{mail, Limits};
257///
258/// let m = mail::parse_mailbox(b"\"Doe, John\" <john@example.com>", &Limits::DEFAULT)?;
259/// assert_eq!(m.display_name, Some(&b"\"Doe, John\""[..]));
260/// assert!(mail::parse_mailbox(b"john@example.com", &Limits::DEFAULT)?.display_name.is_none());
261/// # Ok::<(), parse_net::ParseError>(())
262/// ```
263pub fn parse_mailbox<'a>(value: &'a [u8], limits: &Limits) -> Result<Mailbox<'a>> {
264    let lead = skip_ows(value);
265    let v = trim_ows(value);
266    // Find the `<` outside quoted strings and comments, checking the phrase.
267    // An `@` first means a bare address.
268    let (mut i, mut has_word) = (0usize, false);
269    let lt = loop {
270        match v.get(i) {
271            None | Some(b'@') => {
272                let addr = addr_spec(v).map_err(|e| e.shifted(lead))?;
273                return Ok(Mailbox {
274                    display_name: None,
275                    addr,
276                });
277            }
278            Some(b'<') => break i,
279            Some(b'"') => {
280                let (q, used) =
281                    parse_quoted(v.get(i..).unwrap_or(&[])).map_err(|e| e.shifted(lead + i))?;
282                if let Err(e) = core::str::from_utf8(q.raw()) {
283                    return err(ErrorKind::InvalidUtf8, lead + i + 1 + e.valid_up_to());
284                }
285                has_word = true;
286                i += used;
287            }
288            Some(b'(') => {
289                i = comment_end_folded(v, i, limits)
290                    .map_err(|e| e.shifted(lead))?
291                    .0;
292            }
293            Some(b' ' | b'\t') => i += 1,
294            Some(&b) if class::is_atext(b) || b == b'.' || b >= 0x80 => {
295                has_word = true;
296                i += 1;
297            }
298            Some(_) => return Err(refused(v, i, ErrorKind::InvalidAddrSpec).shifted(lead)),
299        }
300    };
301    // The closing `>`, past any quoted local part; only CFWS may follow it.
302    let mut j = lt + 1;
303    let gt = loop {
304        match v.get(j) {
305            None => return err(ErrorKind::InvalidAddrSpec, lead + j),
306            Some(b'>') => break j,
307            Some(b'"') => {
308                let (_, used) =
309                    parse_quoted(v.get(j..).unwrap_or(&[])).map_err(|e| e.shifted(lead + j))?;
310                j += used;
311            }
312            Some(_) => j += 1,
313        }
314    };
315    let end = cfws(v, gt + 1, limits).map_err(|e| e.shifted(lead))?;
316    if end < v.len() {
317        return Err(refused(v, end, ErrorKind::InvalidAddrSpec).shifted(lead));
318    }
319    let display = trim_ows(v.get(..lt).unwrap_or(&[]));
320    if let Err(e) = core::str::from_utf8(display) {
321        return err(ErrorKind::InvalidUtf8, lead + e.valid_up_to());
322    }
323    let inner = v.get(lt + 1..gt).unwrap_or(&[]);
324    let addr = addr_spec(inner).map_err(|e| e.shifted(lead + lt + 1))?;
325    Ok(Mailbox {
326        display_name: has_word.then_some(display),
327        addr,
328    })
329}
330
331/// A message identifier: `"<" id-left "@" id-right ">"`.
332///
333/// # Examples
334///
335/// ```
336/// let id = parse_net::mail::parse_msg_id(b"<abc.123@[192.0.2.1]>")?;
337/// assert_eq!((id.left, id.right), (&b"abc.123"[..], &b"[192.0.2.1]"[..]));
338/// # Ok::<(), parse_net::ParseError>(())
339/// ```
340#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
341pub struct MsgId<'a> {
342    /// The part before `@`.
343    pub left: &'a [u8],
344    /// The part after `@` (a domain literal keeps its brackets).
345    pub right: &'a [u8],
346}
347
348/// Parses a `msg-id` (RFC 5322 section 3.6.4): `dot-atom-text` on both
349/// sides of `@`, or a `no-fold-literal` (`[...]`) on the right.
350///
351/// # Errors
352///
353/// [`ErrorKind::InvalidAddrSpec`]; [`ErrorKind::InvalidUtf8`].
354///
355/// # Examples
356///
357/// ```
358/// let id = parse_net::mail::parse_msg_id(b"<1234.5678@mail.example.com>")?;
359/// assert_eq!((id.left, id.right), (&b"1234.5678"[..], &b"mail.example.com"[..]));
360/// # Ok::<(), parse_net::ParseError>(())
361/// ```
362pub fn parse_msg_id(value: &[u8]) -> Result<MsgId<'_>> {
363    let lead = skip_ows(value);
364    let v = trim_ows(value);
365    let inner = match (v.first(), v.last()) {
366        (Some(b'<'), Some(b'>')) if v.len() >= 2 => v.get(1..v.len() - 1).unwrap_or(&[]),
367        _ => return err(ErrorKind::InvalidAddrSpec, lead),
368    };
369    let at = inner.iter().position(|&b| b == b'@').unwrap_or(inner.len());
370    let left = inner.get(..at).unwrap_or(&[]);
371    check_dot_atom(left, lead + 1)?;
372    if inner.get(at) != Some(&b'@') {
373        return err(ErrorKind::InvalidAddrSpec, lead + 1 + at);
374    }
375    let right = inner.get(at + 1..).unwrap_or(&[]);
376    let base = lead + 2 + at;
377    match (right.first(), right.last()) {
378        (Some(b'['), Some(b']')) if right.len() >= 2 => {
379            check_dtext(right.get(1..right.len() - 1).unwrap_or(&[]), base + 1)?
380        }
381        _ => check_dot_atom(right, base)?,
382    }
383    Ok(MsgId { left, right })
384}
385
386fn is_ascii_dot_atom(s: &[u8]) -> Option<usize> {
387    let mut prev_dot = true;
388    for (i, &b) in s.iter().enumerate() {
389        if b == b'.' {
390            if prev_dot {
391                return Some(i);
392            }
393            prev_dot = true;
394        } else if class::is_atext(b) {
395            prev_dot = false;
396        } else {
397            return Some(i);
398        }
399    }
400    prev_dot.then_some(s.len().saturating_sub(1))
401}
402
403/// Writes `<left@right>`. `left` must be ASCII `dot-atom-text`; `right` the
404/// same or a domain literal in brackets.
405///
406/// # Errors
407///
408/// [`WriteError::Forbidden`] with `at` counting through `left` and then
409/// `right` (a byte outside the grammar, a misplaced dot, an empty side);
410/// [`WriteError::Full`] (nothing written when the sink reports its room).
411///
412/// # Examples
413///
414/// ```
415/// use parse_net::{mail, SliceSink};
416///
417/// let mut buf = [0u8; 64];
418/// let mut out = SliceSink::new(&mut buf);
419/// mail::write_msg_id("1234.5678", "mail.example.com", &mut out)?;
420/// assert_eq!(out.written(), b"<1234.5678@mail.example.com>");
421/// # Ok::<(), parse_net::WriteError>(())
422/// ```
423pub fn write_msg_id(
424    left: &str,
425    right: &str,
426    out: &mut impl Sink,
427) -> core::result::Result<(), WriteError> {
428    let (l, r) = (left.as_bytes(), right.as_bytes());
429    if let Some(i) = is_ascii_dot_atom(l) {
430        return Err(WriteError::forbidden(i));
431    }
432    let bad_right = match (r.first(), r.last()) {
433        (Some(b'['), Some(b']')) if r.len() >= 2 => r
434            .get(1..r.len() - 1)
435            .unwrap_or(&[])
436            .iter()
437            .position(|&b| !matches!(b, 33..=90 | 94..=126))
438            .map(|i| i + 1),
439        _ => is_ascii_dot_atom(r),
440    };
441    if let Some(i) = bad_right {
442        return Err(WriteError::forbidden(l.len() + i));
443    }
444    let mut count = CountSink::new();
445    put_msg_id(l, r, &mut count)?;
446    // One unfoldable line (RFC 5322 section 2.1.1).
447    if count.count() > super::HARD_LIMIT {
448        return Err(WriteError::OutOfRange);
449    }
450    reserve(out, count.count())?;
451    put_msg_id(l, r, out)
452}
453
454fn put_msg_id(l: &[u8], r: &[u8], out: &mut impl Sink) -> core::result::Result<(), WriteError> {
455    out.put(b"<")?;
456    out.put(l)?;
457    out.put(b"@")?;
458    out.put(r)?;
459    out.put(b">")?;
460    Ok(())
461}
462
463#[cfg(test)]
464mod tests {
465    use super::*;
466    use std::vec::Vec;
467
468    fn k(v: &[u8]) -> ErrorKind {
469        parse_addr_spec(v).unwrap_err().kind()
470    }
471
472    #[test]
473    fn test_addr_spec_edge_cases() {
474        let a = parse_addr_spec(b"\"john doe\"@example.com").unwrap();
475        assert!(matches!(a.local, LocalPart::Quoted(q) if q.eq_bytes(b"john doe")));
476        assert_eq!(a.domain, Domain::DotAtom(b"example.com"));
477        assert!(parse_addr_spec(b"a.b+tag@sub.example.co").is_ok());
478        assert_eq!(k(b"john..doe@example.com"), ErrorKind::InvalidAddrSpec);
479        assert_eq!(k(b".john@example.com"), ErrorKind::InvalidAddrSpec);
480        assert_eq!(k(b"john.@example.com"), ErrorKind::InvalidAddrSpec);
481        assert_eq!(k(b"john@example..com"), ErrorKind::InvalidAddrSpec);
482        assert_eq!(k(b"john@"), ErrorKind::InvalidAddrSpec);
483        assert_eq!(k(b"@example.com"), ErrorKind::InvalidAddrSpec);
484        assert_eq!(k(b"john.example.com"), ErrorKind::InvalidAddrSpec);
485        assert_eq!(k(b"john(c)@example.com"), ErrorKind::InvalidAddrSpec);
486        assert_eq!(k(b"john@example.com (c)"), ErrorKind::InvalidAddrSpec);
487        assert_eq!(k(b"a b@example.com"), ErrorKind::InvalidAddrSpec);
488        assert_eq!(k(b"a@b@example.com"), ErrorKind::InvalidAddrSpec);
489        assert_eq!(k(b"\"a@example.com"), ErrorKind::UnterminatedQuote);
490        assert_eq!(k(b"a\r\n@example.com"), ErrorKind::BareCr);
491        assert_eq!(k(b"j\xf6rg@example.de"), ErrorKind::InvalidUtf8);
492        assert!(
493            !parse_addr_spec("j\u{f6}rg@\u{e9}xample.de".as_bytes())
494                .unwrap()
495                .is_ascii()
496        );
497        let lit = parse_addr_spec(b"a@[IPv6:2001:db8::1]").unwrap();
498        assert_eq!(lit.domain, Domain::Literal(b"IPv6:2001:db8::1"));
499        assert_eq!(k(b"a@[1.2.3.4"), ErrorKind::InvalidAddrSpec);
500        assert_eq!(k(b"a@[a[b]"), ErrorKind::InvalidAddrSpec);
501    }
502
503    #[test]
504    fn test_addr_spec_lengths() {
505        let local = [b'a'; 64];
506        let ok = [&local[..], b"@example.com"].concat();
507        assert!(parse_addr_spec(&ok).is_ok());
508        let long = [&[b'a'; 65][..], b"@example.com"].concat();
509        assert_eq!(k(&long), ErrorKind::TooLong);
510        let mut domain = Vec::new();
511        while domain.len() < 250 {
512            domain.extend_from_slice(b"abcdefghi.");
513        }
514        domain.truncate(252);
515        domain.push(b'x');
516        let a = [b"a@".as_slice(), &domain[..251]].concat();
517        assert_eq!(a.len(), 253);
518        let a = [b"ab@".as_slice(), &domain[..251]].concat();
519        assert_eq!((a.len(), parse_addr_spec(&a).is_ok()), (254, true));
520        let a = [b"abc@".as_slice(), &domain[..251]].concat();
521        assert_eq!(k(&a), ErrorKind::TooLong);
522    }
523
524    #[test]
525    fn test_mailbox_forms() {
526        let l = Limits::DEFAULT;
527        let m = parse_mailbox(b"John Q. Public <jqp@example.com>", &l).unwrap();
528        assert_eq!(m.display_name, Some(&b"John Q. Public"[..]));
529        let m = parse_mailbox(b"=?UTF-8?Q?J=C3=B6rg?= (work) <j@example.de>", &l).unwrap();
530        assert_eq!(m.display_name, Some(&b"=?UTF-8?Q?J=C3=B6rg?= (work)"[..]));
531        let m = parse_mailbox(b"<a@b.c>", &l).unwrap();
532        assert!(m.display_name.is_none());
533        assert_eq!(
534            parse_mailbox(b"\"a <b@c.d>", &l).unwrap_err().kind(),
535            ErrorKind::UnterminatedQuote
536        );
537        assert_eq!(
538            parse_mailbox(b"(x <a@b.c>", &l).unwrap_err().kind(),
539            ErrorKind::UnbalancedComment
540        );
541        assert_eq!(
542            parse_mailbox(b"a@b <a@b.c>", &l).unwrap_err().kind(),
543            ErrorKind::InvalidAddrSpec
544        );
545        assert_eq!(
546            parse_mailbox(b"x <a..b@c.d>", &l).unwrap_err().kind(),
547            ErrorKind::InvalidAddrSpec
548        );
549        assert_eq!(
550            parse_mailbox(b"x a@b.c>", &l).unwrap_err().kind(),
551            ErrorKind::InvalidAddrSpec
552        );
553    }
554
555    #[test]
556    fn test_msg_id_parse_and_write() {
557        assert!(parse_msg_id(b"<a@[127.0.0.1]>").is_ok());
558        assert_eq!(
559            parse_msg_id(b"a@b").unwrap_err().kind(),
560            ErrorKind::InvalidAddrSpec
561        );
562        assert_eq!(
563            parse_msg_id(b"<ab>").unwrap_err().kind(),
564            ErrorKind::InvalidAddrSpec
565        );
566        assert_eq!(
567            parse_msg_id(b"<a..b@c>").unwrap_err().kind(),
568            ErrorKind::InvalidAddrSpec
569        );
570        assert_eq!(
571            parse_msg_id(b"<>").unwrap_err().kind(),
572            ErrorKind::InvalidAddrSpec
573        );
574        let mut buf = [0u8; 64];
575        let mut out = crate::SliceSink::new(&mut buf);
576        assert_eq!(
577            write_msg_id("a b", "c", &mut out),
578            Err(WriteError::Forbidden { at: 1 })
579        );
580        assert_eq!(
581            write_msg_id("a", "c..d", &mut out),
582            Err(WriteError::Forbidden { at: 3 })
583        );
584        assert_eq!(
585            write_msg_id("", "c", &mut out),
586            Err(WriteError::Forbidden { at: 0 })
587        );
588        assert_eq!(
589            write_msg_id("a", "[x]y]", &mut out),
590            Err(WriteError::Forbidden { at: 3 })
591        );
592        assert!(out.is_empty());
593        write_msg_id("x.y", "[192.0.2.1]", &mut out).unwrap();
594        let id = parse_msg_id(out.written()).unwrap();
595        assert_eq!((id.left, id.right), (&b"x.y"[..], &b"[192.0.2.1]"[..]));
596    }
597}