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