Skip to main content

mail_parser/fields/
address.rs

1/*
2 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
3 *
4 * SPDX-License-Identifier: Apache-2.0 OR MIT
5 */
6
7use super::{FieldCtx, GroupMark, push_utf8_lossy};
8use crate::{
9    decoders::charsets,
10    scan::{ByteSet, Kernel},
11    store::{Str, Value},
12};
13use encodify::rfc2047::EncodedWord;
14use std::{mem, ops::Range};
15
16#[derive(Clone, Copy, PartialEq, Eq, Debug)]
17#[repr(u8)]
18enum State {
19    Name = 1,
20    Address = 2,
21    Quote = 4,
22    Comment = 8,
23}
24
25const IN_NAME: u8 = State::Name as u8;
26const IN_ADDRESS: u8 = State::Address as u8;
27const IN_QUOTE: u8 = State::Quote as u8;
28const IN_COMMENT: u8 = State::Comment as u8;
29const EVERYWHERE: u8 = IN_NAME | IN_ADDRESS | IN_QUOTE | IN_COMMENT;
30
31static STOPS: [u8; 256] = {
32    let mut table = [0; 256];
33    table[b'\n' as usize] = EVERYWHERE;
34    table[b'\r' as usize] = EVERYWHERE;
35    table[b' ' as usize] = EVERYWHERE;
36    table[b'\t' as usize] = EVERYWHERE;
37    table[b'=' as usize] = EVERYWHERE;
38    table[b'"' as usize] = IN_NAME | IN_QUOTE;
39    table[b'(' as usize] = IN_NAME | IN_ADDRESS | IN_COMMENT;
40    table[b'\\' as usize] = EVERYWHERE;
41    table[b',' as usize] = IN_NAME;
42    table[b'<' as usize] = IN_NAME;
43    table[b'@' as usize] = IN_NAME;
44    table[b':' as usize] = IN_NAME;
45    table[b';' as usize] = IN_NAME;
46    table[b'>' as usize] = IN_ADDRESS;
47    table[b')' as usize] = IN_COMMENT;
48    table
49};
50
51static SIMPLE_NAME: ByteSet = ByteSet::with_marks(b"\n\r,;<\"(:=\\", b"@");
52static SIMPLE_QUOTED: ByteSet = ByteSet::new(b"\n\r\"\\=");
53static SIMPLE_ADDRESS: ByteSet = ByteSet::new(b"\n\r>\\(=");
54
55fn is_blank(byte: u8) -> bool {
56    byte == b' ' || byte == b'\t'
57}
58
59fn skip_space(bytes: &[u8], mut at: usize) -> usize {
60    loop {
61        match bytes.get(at) {
62            Some(b' ' | b'\t' | b'\r') => at += 1,
63            Some(b'\n') if bytes.get(at + 1).copied().is_some_and(is_blank) => at += 2,
64            _ => return at,
65        }
66    }
67}
68
69fn trimmed_len(bytes: &[u8]) -> usize {
70    bytes
71        .iter()
72        .rposition(|&byte| !is_blank(byte))
73        .map_or(0, |last| last + 1)
74}
75
76type SimpleShape = (Option<Range<usize>>, Option<Range<usize>>, usize);
77
78#[derive(Clone, Copy)]
79struct Item<'x> {
80    kernel: Kernel,
81    src: &'x [u8],
82    start: usize,
83    end: usize,
84}
85
86impl Item<'_> {
87    fn bytes(&self) -> &[u8] {
88        self.src.get(self.start..self.end).unwrap_or_default()
89    }
90
91    fn find(&self, set: &ByteSet, from: usize) -> Option<usize> {
92        self.kernel
93            .first_in_set(set, self.src, self.start + from, self.end)
94            .map(|pos| pos - self.start)
95    }
96
97    fn shape(&self) -> Option<SimpleShape> {
98        let bytes = self.bytes();
99        let first = skip_space(bytes, 0);
100        let rest = bytes.get(first..)?;
101        if rest.first() == Some(&b'"') {
102            let close = self.find(&SIMPLE_QUOTED, first + 1)?;
103            if close == first + 1 || bytes.get(close) != Some(&b'"') {
104                return None;
105            }
106            let open = skip_space(bytes, close + 1);
107            if bytes.get(open) != Some(&b'<') {
108                return None;
109            }
110            let (address, after) = self.angle(open + 1)?;
111            return Some((Some(first + 1..close), Some(address), after));
112        }
113        let found = self
114            .kernel
115            .stop_in_set(&SIMPLE_NAME, self.src, self.start + first, self.end);
116        let stop = found.at.map_or(bytes.len(), |pos| pos - self.start);
117        let run = bytes.get(first..stop)?;
118        let name_len = trimmed_len(run);
119        let name = first..first + name_len;
120        match bytes.get(stop) {
121            Some(b'<') => {
122                let (address, after) = self.angle(stop + 1)?;
123                Some(((name_len > 0).then_some(name), Some(address), after))
124            }
125            None | Some(b',' | b';' | b'\r' | b'\n') if name_len > 0 => {
126                if found.marked {
127                    Some((None, Some(name), first + name_len))
128                } else {
129                    Some((Some(name), None, first + name_len))
130                }
131            }
132            _ => None,
133        }
134    }
135
136    fn angle(&self, open: usize) -> Option<(Range<usize>, usize)> {
137        let close = self.angle_end(open)?;
138        let content = self.bytes().get(open..close)?;
139        let lead = content.iter().position(|&byte| !is_blank(byte))?;
140        Some((open + lead..open + trimmed_len(content), close + 1))
141    }
142
143    fn angle_end(&self, open: usize) -> Option<usize> {
144        let bytes = self.bytes();
145        let mut from = open;
146        loop {
147            let stop = self.find(&SIMPLE_ADDRESS, from)?;
148            match bytes.get(stop) {
149                Some(b'>') => return Some(stop),
150                Some(b'=')
151                    if stop > open
152                        && stop
153                            .checked_sub(1)
154                            .and_then(|previous| bytes.get(previous))
155                            .is_some_and(|&previous| !is_blank(previous)) =>
156                {
157                    from = stop + 1
158                }
159                _ => return None,
160            }
161        }
162    }
163}
164
165#[derive(Clone, Copy, PartialEq, Eq, Debug)]
166enum List {
167    Name,
168    Mail,
169    Comment,
170}
171
172#[derive(Clone, Copy, PartialEq, Eq, Debug)]
173enum Origin {
174    Source,
175    Decoded,
176}
177
178#[derive(Clone, Copy, PartialEq, Eq, Debug)]
179struct Piece {
180    origin: Origin,
181    start: usize,
182    end: usize,
183    space: bool,
184}
185
186const RECORD_LEN: usize = 16;
187const TAG_SHIFT: u32 = 56;
188const START_MASK: u128 = (1 << TAG_SHIFT) - 1;
189const LIST_MASK: u128 = 0b11;
190const DECODED_FLAG: u128 = 0b100;
191const SPACE_FLAG: u128 = 0b1000;
192const END_SHIFT: u32 = 64;
193
194impl Piece {
195    const EMPTY: Piece = Piece {
196        origin: Origin::Source,
197        start: 0,
198        end: 0,
199        space: false,
200    };
201
202    fn record(self, list: List) -> [u8; RECORD_LEN] {
203        let mut tag = list as u128;
204        if self.origin == Origin::Decoded {
205            tag |= DECODED_FLAG;
206        }
207        if self.space {
208            tag |= SPACE_FLAG;
209        }
210        let start = self.start as u128 & START_MASK;
211        let end = (self.end as u128) << END_SHIFT;
212        (start | (tag << TAG_SHIFT) | end).to_le_bytes()
213    }
214
215    fn from_record(record: &[u8; RECORD_LEN]) -> (u128, Piece) {
216        let value = u128::from_le_bytes(*record);
217        let tag = value >> TAG_SHIFT;
218        (
219            tag & LIST_MASK,
220            Piece {
221                origin: if tag & DECODED_FLAG != 0 {
222                    Origin::Decoded
223                } else {
224                    Origin::Source
225                },
226                start: (value & START_MASK) as usize,
227                end: (value >> END_SHIFT) as usize,
228                space: tag & SPACE_FLAG != 0,
229            },
230        )
231    }
232}
233
234enum PieceText<'a> {
235    Text(&'a str),
236    Bytes(&'a [u8]),
237}
238
239#[derive(Clone, Copy, Debug)]
240struct Acc {
241    count: usize,
242    first: Piece,
243}
244
245impl Acc {
246    const EMPTY: Acc = Acc {
247        count: 0,
248        first: Piece::EMPTY,
249    };
250}
251
252#[derive(Clone, Copy, Debug)]
253struct PendingWord<'x> {
254    list: List,
255    space: bool,
256    charset: &'x [u8],
257}
258
259#[derive(Clone, Copy, Debug)]
260struct OpenGroup {
261    empty_name: Option<Str>,
262    name: Option<Str>,
263    marker: Option<GroupMark>,
264}
265
266struct Parser<'x> {
267    src: &'x [u8],
268    end: usize,
269    state: State,
270    outer: State,
271    depth: usize,
272    token: Option<(usize, usize)>,
273    email: bool,
274    token_start: bool,
275    escaped: bool,
276    glued: bool,
277    last_encoded: bool,
278    name: Acc,
279    mail: Acc,
280    comment: Acc,
281    records: usize,
282    raw: Vec<u8>,
283    text: String,
284    pending: Option<PendingWord<'x>>,
285    group: Option<OpenGroup>,
286}
287
288impl FieldCtx<'_> {
289    pub(crate) fn parse_address(&mut self, value: Range<usize>) -> Value {
290        let mark = self.address_mark();
291        let src = self.src();
292        let mut parser = Parser {
293            src,
294            end: value.end.min(src.len()),
295            state: State::Name,
296            outer: State::Name,
297            depth: 0,
298            token: None,
299            email: false,
300            token_start: true,
301            escaped: false,
302            glued: false,
303            last_encoded: true,
304            name: Acc::EMPTY,
305            mail: Acc::EMPTY,
306            comment: Acc::EMPTY,
307            records: 0,
308            raw: self.take_bytes_scratch(),
309            text: self.take_text_scratch(),
310            pending: None,
311            group: None,
312        };
313        parser.run(self, value.start);
314        parser.add_token();
315        parser.add_address(self);
316        parser.close_group(self);
317        self.put_bytes_scratch(parser.raw);
318        self.put_text_scratch(parser.text);
319        self.address_list(mark)
320    }
321}
322
323impl<'x> Parser<'x> {
324    fn run(&mut self, ctx: &mut FieldCtx<'_>, start: usize) {
325        let src = self.src;
326        let mut pos = start;
327        let mut item_start = true;
328        loop {
329            if item_start {
330                item_start = false;
331                if !self.escaped
332                    && let Some(next) = self.simple_item(ctx, pos)
333                {
334                    pos = next;
335                }
336            }
337            let rest = src.get(pos..self.end).unwrap_or_default();
338            let mask = self.state as u8;
339            let Some(run) = rest
340                .iter()
341                .position(|&byte| STOPS[usize::from(byte)] & mask != 0)
342            else {
343                if !rest.is_empty() {
344                    self.extend(pos, self.end);
345                }
346                return;
347            };
348            if run > 0 {
349                self.extend(pos, pos + run);
350                pos += run;
351            }
352            let Some((&byte, after)) = rest.get(run..).and_then(<[u8]>::split_first) else {
353                return;
354            };
355            match byte {
356                b'\n' => {
357                    self.add_token();
358                    if !matches!(after.first(), Some(b' ' | b'\t')) {
359                        return;
360                    }
361                    if self.state == State::Quote {
362                        pos += 1;
363                    } else {
364                        self.token_start = true;
365                        pos += 2;
366                    }
367                }
368                b' ' | b'\t' => {
369                    let blanks = 1 + after.iter().take_while(|&&byte| is_blank(byte)).count();
370                    self.token_start = true;
371                    self.glued &= !self.escaped;
372                    self.escaped = false;
373                    if self.state == State::Quote {
374                        let first = self.token.map_or(pos, |(first, _)| first);
375                        self.token = Some((first, pos + blanks));
376                    }
377                    pos += blanks;
378                }
379                b'\r' => pos += 1,
380                b'\\' if !self.escaped => {
381                    let glued = !self.token_start;
382                    if let Some((first, _)) = self.token {
383                        if self.state == State::Quote {
384                            self.token = Some((first, pos));
385                        }
386                        self.add_token();
387                    }
388                    self.escaped = true;
389                    self.glued = glued;
390                    pos += 1;
391                }
392                b'=' if self.token_start && !self.escaped && after.first() == Some(&b'?') => {
393                    match self.encoded_word(pos) {
394                        Some(consumed) => pos += consumed,
395                        None => {
396                            self.extend(pos, pos + 1);
397                            pos += 1;
398                        }
399                    }
400                }
401                b',' => {
402                    self.add_token();
403                    self.add_address(ctx);
404                    pos += 1;
405                    item_start = true;
406                }
407                b'<' => {
408                    self.email = false;
409                    self.add_token();
410                    self.state = State::Address;
411                    pos += 1;
412                }
413                b'>' => {
414                    self.add_token();
415                    self.state = State::Name;
416                    pos += 1;
417                }
418                b'"' if !self.escaped && self.state == State::Name => {
419                    self.state = State::Quote;
420                    self.add_token();
421                    pos += 1;
422                }
423                b'"' if !self.escaped => {
424                    self.add_token();
425                    self.state = State::Name;
426                    pos += 1;
427                }
428                b'@' => {
429                    self.email = true;
430                    self.extend(pos, pos + 1);
431                    pos += 1;
432                }
433                b'(' if !self.escaped && self.state == State::Comment => {
434                    self.depth += 1;
435                    self.extend(pos, pos + 1);
436                    pos += 1;
437                }
438                b'(' if !self.escaped => {
439                    self.add_token();
440                    self.outer = self.state;
441                    self.state = State::Comment;
442                    self.depth = 1;
443                    self.last_encoded = false;
444                    pos += 1;
445                }
446                b')' if !self.escaped && self.depth > 1 => {
447                    self.depth -= 1;
448                    self.extend(pos, pos + 1);
449                    pos += 1;
450                }
451                b')' if !self.escaped => {
452                    self.add_token();
453                    self.depth = 0;
454                    self.state = self.outer;
455                    self.last_encoded = false;
456                    pos += 1;
457                }
458                b':' if !self.escaped => {
459                    self.close_group(ctx);
460                    self.add_token();
461                    self.open_group(ctx);
462                    pos += 1;
463                    item_start = true;
464                }
465                b';' => {
466                    self.add_token();
467                    self.add_address(ctx);
468                    self.close_group(ctx);
469                    pos += 1;
470                    item_start = true;
471                }
472                _ => {
473                    self.extend(pos, pos + 1);
474                    pos += 1;
475                }
476            }
477        }
478    }
479
480    fn extend(&mut self, start: usize, end: usize) {
481        self.escaped = false;
482        self.token_start = false;
483        self.token = Some((self.token.map_or(start, |(first, _)| first), end));
484    }
485
486    fn acc(&self, list: List) -> &Acc {
487        match list {
488            List::Name => &self.name,
489            List::Mail => &self.mail,
490            List::Comment => &self.comment,
491        }
492    }
493
494    fn acc_mut(&mut self, list: List) -> &mut Acc {
495        match list {
496            List::Name => &mut self.name,
497            List::Mail => &mut self.mail,
498            List::Comment => &mut self.comment,
499        }
500    }
501
502    fn add_piece(&mut self, list: List, piece: Piece) {
503        if self.acc(list).count == 0 {
504            self.acc_mut(list).first = piece;
505        } else {
506            let at = self.records.min(self.raw.len());
507            self.raw.splice(at..at, piece.record(list));
508            self.records = at + RECORD_LEN;
509        }
510        self.acc_mut(list).count += 1;
511    }
512
513    fn add_token(&mut self) {
514        let Some((start, end)) = self.token.take() else {
515            return;
516        };
517        let (list, separated) = match self.state {
518            State::Name if !self.email => (List::Name, true),
519            State::Name | State::Address => (List::Mail, false),
520            State::Quote => (List::Name, false),
521            State::Comment => (List::Comment, true),
522        };
523        self.flush_word(self.raw.len());
524        let space = separated && !mem::take(&mut self.glued) && self.acc(list).count > 0;
525        self.add_piece(
526            list,
527            Piece {
528                origin: Origin::Source,
529                start,
530                end,
531                space,
532            },
533        );
534        self.email = false;
535        self.token_start = true;
536        self.escaped = false;
537        self.last_encoded = false;
538    }
539
540    fn encoded_word(&mut self, at: usize) -> Option<usize> {
541        let (word, consumed) = EncodedWord::parse(self.src.get(at..self.end)?)?;
542        let list = match self.state {
543            State::Comment => List::Comment,
544            _ => List::Name,
545        };
546        let joinable = self.token.is_none()
547            && self.last_encoded
548            && self.pending.is_some_and(|pending| {
549                pending.list == list && pending.charset.eq_ignore_ascii_case(word.charset)
550            });
551        let split = self.raw.len();
552        word.decode_append(&mut self.raw).ok()?;
553        if !joinable {
554            self.flush_word(split);
555            self.add_token();
556            let space =
557                !self.last_encoded && self.state != State::Quote && self.acc(list).count > 0;
558            self.pending = Some(PendingWord {
559                list,
560                space,
561                charset: word.charset,
562            });
563        }
564        self.last_encoded = true;
565        Some(consumed)
566    }
567
568    fn flush_word(&mut self, split: usize) {
569        let Some(word) = self.pending.take() else {
570            return;
571        };
572        let from = self.records.min(self.raw.len());
573        let split = split.clamp(from, self.raw.len());
574        let start = self.text.len();
575        charsets::decode_append(
576            word.charset,
577            self.raw.get(from..split).unwrap_or_default(),
578            &mut self.text,
579        );
580        let end = self.text.len();
581        self.raw.drain(from..split);
582        self.add_piece(
583            word.list,
584            Piece {
585                origin: Origin::Decoded,
586                start,
587                end,
588                space: word.space,
589            },
590        );
591    }
592
593    fn pieces(&self, list: List) -> impl Iterator<Item = Piece> {
594        let acc = self.acc(list);
595        let records: &[u8] = match acc.count {
596            0 | 1 => &[],
597            _ => self.raw.get(..self.records).unwrap_or_default(),
598        };
599        let (records, _) = records.as_chunks::<RECORD_LEN>();
600        (acc.count > 0).then_some(acc.first).into_iter().chain(
601            records
602                .iter()
603                .map(Piece::from_record)
604                .filter_map(move |(id, piece)| (id == list as u128).then_some(piece)),
605        )
606    }
607
608    fn piece_text(&self, piece: Piece) -> PieceText<'_> {
609        match piece.origin {
610            Origin::Source => {
611                PieceText::Bytes(self.src.get(piece.start..piece.end).unwrap_or_default())
612            }
613            Origin::Decoded => {
614                PieceText::Text(self.text.get(piece.start..piece.end).unwrap_or_default())
615            }
616        }
617    }
618
619    fn write_list(&self, out: &mut String, list: List) {
620        for piece in self.pieces(list) {
621            if piece.space {
622                out.push(' ');
623            }
624            match self.piece_text(piece) {
625                PieceText::Text(text) => out.push_str(text),
626                PieceText::Bytes(bytes) => push_utf8_lossy(out, bytes),
627            }
628        }
629    }
630
631    fn has_whitespace(&self, list: List) -> bool {
632        self.pieces(list).any(|piece| {
633            piece.space
634                || match self.piece_text(piece) {
635                    PieceText::Text(text) => text.chars().any(char::is_whitespace),
636                    PieceText::Bytes(bytes) => bytes
637                        .utf8_chunks()
638                        .any(|chunk| chunk.valid().chars().any(char::is_whitespace)),
639                }
640        })
641    }
642
643    fn list_str(&self, ctx: &mut FieldCtx<'_>, list: List) -> Option<Str> {
644        let acc = self.acc(list);
645        match acc.count {
646            0 => None,
647            1 => Some(match self.piece_text(acc.first) {
648                PieceText::Text(text) => ctx.push_str(text),
649                PieceText::Bytes(_) => ctx.borrow(acc.first.start..acc.first.end),
650            }),
651            _ => Some(ctx.push_with(|pool| self.write_list(pool, list))),
652        }
653    }
654
655    fn with_comment(&self, ctx: &mut FieldCtx<'_>, write: impl Fn(&Self, &mut String)) -> Str {
656        ctx.push_with(|pool| {
657            write(self, pool);
658            pool.push_str(" (");
659            self.write_list(pool, List::Comment);
660            pool.push(')');
661        })
662    }
663
664    fn reset_lists(&mut self) {
665        self.name = Acc::EMPTY;
666        self.mail = Acc::EMPTY;
667        self.comment = Acc::EMPTY;
668        self.records = 0;
669        self.raw.clear();
670        self.text.clear();
671    }
672
673    fn add_address(&mut self, ctx: &mut FieldCtx<'_>) {
674        self.flush_word(self.raw.len());
675        let name_only = |parser: &Self, pool: &mut String| parser.write_list(pool, List::Name);
676        let (name, address) = match (
677            self.mail.count > 0,
678            self.name.count > 0,
679            self.comment.count > 0,
680        ) {
681            (true, true, true) => (
682                Some(self.with_comment(ctx, name_only)),
683                self.list_str(ctx, List::Mail),
684            ),
685            (true, true, false) => (
686                self.list_str(ctx, List::Name),
687                self.list_str(ctx, List::Mail),
688            ),
689            (true, false, true) => (
690                self.list_str(ctx, List::Comment),
691                self.list_str(ctx, List::Mail),
692            ),
693            (true, false, false) => (None, self.list_str(ctx, List::Mail)),
694            (false, true, true) if self.has_whitespace(List::Name) => {
695                (Some(self.with_comment(ctx, name_only)), None)
696            }
697            (false, true, true) => (
698                self.list_str(ctx, List::Comment),
699                self.list_str(ctx, List::Name),
700            ),
701            (false, true, false) => (self.list_str(ctx, List::Name), None),
702            (false, false, true) => (self.list_str(ctx, List::Comment), None),
703            (false, false, false) => return,
704        };
705        self.reset_lists();
706        self.push_member(ctx, name, address);
707    }
708
709    fn push_member(&mut self, ctx: &mut FieldCtx<'_>, name: Option<Str>, address: Option<Str>) {
710        if let Some(group) = self.group.as_mut()
711            && group.marker.is_none()
712        {
713            group.marker = Some(ctx.push_group(group.name));
714        }
715        ctx.push_mailbox(name, address);
716    }
717
718    fn simple_item(&mut self, ctx: &mut FieldCtx<'_>, start: usize) -> Option<usize> {
719        let item = Item {
720            kernel: ctx.kernel(),
721            src: self.src,
722            start,
723            end: self.end,
724        };
725        let bytes = item.bytes();
726        let (name, address, after) = item.shape()?;
727        if !matches!(
728            bytes.get(skip_space(bytes, after)),
729            None | Some(b',' | b';' | b'\n')
730        ) {
731            return None;
732        }
733        let name = name.map(|range| ctx.borrow(start + range.start..start + range.end));
734        let address = address.map(|range| ctx.borrow(start + range.start..start + range.end));
735        self.push_member(ctx, name, address);
736        self.last_encoded = false;
737        Some(start + after)
738    }
739
740    fn write_group_name(&self, out: &mut String) {
741        self.write_list(out, List::Name);
742        if self.name.count > 0 && self.mail.count > 0 {
743            out.push(' ');
744        }
745        self.write_list(out, List::Mail);
746    }
747
748    fn open_group(&mut self, ctx: &mut FieldCtx<'_>) {
749        self.flush_word(self.raw.len());
750        let empty_name = match (self.name.count > 0, self.mail.count > 0) {
751            (true, true) => Some(ctx.push_with(|pool| self.write_group_name(pool))),
752            (true, false) => self.list_str(ctx, List::Name),
753            (false, true) => self.list_str(ctx, List::Mail),
754            (false, false) => None,
755        };
756        let name = match (empty_name, self.comment.count > 0) {
757            (Some(_), true) => Some(self.with_comment(ctx, Self::write_group_name)),
758            (Some(name), false) => Some(name),
759            (None, true) => self.list_str(ctx, List::Comment),
760            (None, false) => None,
761        };
762        self.reset_lists();
763        self.group = Some(OpenGroup {
764            empty_name,
765            name,
766            marker: None,
767        });
768    }
769
770    fn close_group(&mut self, ctx: &mut FieldCtx<'_>) {
771        match self.group.take() {
772            Some(OpenGroup {
773                marker: Some(marker),
774                ..
775            }) => ctx.end_group(marker),
776            Some(OpenGroup {
777                empty_name: Some(name),
778                ..
779            }) => {
780                let marker = ctx.push_group(Some(name));
781                ctx.end_group(marker);
782            }
783            _ => (),
784        }
785    }
786}
787
788/// The local part of an address (before the first `@`), when it is ASCII
789/// and both sides of the `@` are non-empty.
790pub fn parse_address_local_part(addr: &str) -> Option<&str> {
791    let (local, domain) = addr.split_once('@')?;
792    (!local.is_empty() && local.is_ascii() && !domain.is_empty()).then_some(local)
793}
794
795/// The domain of an address (after the first `@`), when the local part is
796/// ASCII and both sides of the `@` are non-empty.
797pub fn parse_address_domain(addr: &str) -> Option<&str> {
798    let (local, domain) = addr.split_once('@')?;
799    (!local.is_empty() && local.is_ascii() && !domain.is_empty()).then_some(domain)
800}
801
802/// The user of a subaddress (RFC 5233): the local part up to the first `+`.
803pub fn parse_address_user_part(addr: &str) -> Option<&str> {
804    let (local, domain) = addr.split_once('@')?;
805    let user = local.split_once('+').map_or(local, |(user, _)| user);
806    (!user.is_empty() && user.is_ascii() && !domain.is_empty()).then_some(user)
807}
808
809/// The detail of a subaddress (RFC 5233): the local part after its last `+`.
810pub fn parse_address_detail_part(addr: &str) -> Option<&str> {
811    let (local, domain) = addr.split_once('@')?;
812    let (_, detail) = local.rsplit_once('+')?;
813    (local.is_ascii() && !domain.is_empty()).then_some(detail)
814}
815
816#[cfg(test)]
817mod tests {
818    use super::{
819        parse_address_detail_part, parse_address_domain, parse_address_local_part,
820        parse_address_user_part,
821    };
822    use crate::{Address, HeaderForm, HeaderValue, fields::tests::load_tests};
823    use serde_json::{Value as Json, json};
824
825    fn mailbox_json(name: Option<&str>, address: Option<&str>) -> Json {
826        json!({"name": name, "address": address})
827    }
828
829    fn address_json(value: HeaderValue<'_>) -> Json {
830        match value.as_address() {
831            Some(list) => Json::Array(
832                list.iter()
833                    .map(|item| match item {
834                        Address::Mailbox(mailbox) => {
835                            json!({"mailbox": mailbox_json(mailbox.name(), mailbox.address())})
836                        }
837                        Address::Group(group) => json!({"group": {
838                            "name": group.name(),
839                            "mailboxes": group
840                                .mailboxes()
841                                .map(|mailbox| mailbox_json(mailbox.name(), mailbox.address()))
842                                .collect::<Vec<_>>(),
843                        }}),
844                    })
845                    .collect(),
846            ),
847            None => Json::Null,
848        }
849    }
850
851    fn parse(header: &str) -> Json {
852        address_json(HeaderForm::Addresses.parse(header.as_bytes()).value())
853    }
854
855    fn mailbox(name: Option<&str>, address: Option<&str>) -> Json {
856        json!({"mailbox": mailbox_json(name, address)})
857    }
858
859    #[test]
860    fn address_fixtures() {
861        let tests = load_tests("address.json");
862        assert_eq!(tests.len(), 132);
863        for (header, expected) in tests {
864            assert_eq!(parse(&header), expected, "{header:?}");
865            let crlf = header.replace("\r\n", "\n").replace('\n', "\r\n");
866            assert_eq!(parse(&crlf), expected, "{crlf:?}");
867        }
868    }
869
870    #[test]
871    fn issue_98_document_order() {
872        let parsed = HeaderForm::Addresses.parse(b" a@b.c, g: d@e.f;, h@i.j\n");
873        let list = parsed.value().as_address().expect("address list");
874        assert!(list.has_groups());
875        assert_eq!(
876            list.mailboxes()
877                .filter_map(|mailbox| mailbox.address())
878                .collect::<Vec<_>>(),
879            ["a@b.c", "d@e.f", "h@i.j"]
880        );
881        let groups: Vec<_> = list
882            .groups()
883            .map(|(name, run)| {
884                (
885                    name,
886                    run.filter_map(|mailbox| mailbox.address())
887                        .collect::<Vec<_>>(),
888                )
889            })
890            .collect();
891        assert_eq!(
892            groups,
893            [
894                (None, vec!["a@b.c"]),
895                (Some("g"), vec!["d@e.f"]),
896                (None, vec!["h@i.j"])
897            ]
898        );
899    }
900
901    #[test]
902    fn baseline_quirks_kept() {
903        let parsed = HeaderForm::Addresses.parse(b"J\xf6rg <a@b.com>\n");
904        assert_eq!(
905            address_json(parsed.value()),
906            json!([mailbox(Some("J\u{fffd}rg"), Some("a@b.com"))])
907        );
908    }
909
910    #[test]
911    fn simple_values_borrow() {
912        for header in [
913            &b"john@example.com\n"[..],
914            b" John Smith <john@example.com>\r\n",
915            b"\"Smith, John\" <john@example.com>,\n\tjane@example.com\n",
916            b"Friends: a@b.c, <d@e.f>;\n",
917            b"<mailto:list@example.com> (List)\n",
918        ] {
919            let parsed = HeaderForm::Addresses.parse(header);
920            assert!(parsed.value().as_address().is_some());
921            assert!(
922                parsed
923                    .data
924                    .as_ref()
925                    .is_some_and(|data| data.strings.is_empty()),
926                "{:?}",
927                String::from_utf8_lossy(header)
928            );
929        }
930    }
931
932    #[test]
933    fn address_parts() {
934        for (address, local, domain, user, detail) in [
935            (
936                "user@example.com",
937                Some("user"),
938                Some("example.com"),
939                Some("user"),
940                None,
941            ),
942            (
943                "user+tag@example.com",
944                Some("user+tag"),
945                Some("example.com"),
946                Some("user"),
947                Some("tag"),
948            ),
949            ("a+b+c@d", Some("a+b+c"), Some("d"), Some("a"), Some("c")),
950            ("a+@b", Some("a+"), Some("b"), Some("a"), Some("")),
951            ("+a@b", Some("+a"), Some("b"), None, Some("a")),
952            ("+@b", Some("+"), Some("b"), None, Some("")),
953            ("@example.com", None, None, None, None),
954            ("user@", None, None, None, None),
955            ("user", None, None, None, None),
956            ("", None, None, None, None),
957            ("us\u{e9}r@example.com", None, None, None, None),
958            (
959                "user@ex\u{e1}mple.com",
960                Some("user"),
961                Some("ex\u{e1}mple.com"),
962                Some("user"),
963                None,
964            ),
965            ("a+\u{e9}@b", None, None, Some("a"), None),
966            ("a@b+c@d", Some("a"), Some("b+c@d"), Some("a"), None),
967            ("a+b@@", Some("a+b"), Some("@"), Some("a"), Some("b")),
968            ("a+b", None, None, None, None),
969        ] {
970            assert_eq!(parse_address_local_part(address), local, "{address:?}");
971            assert_eq!(parse_address_domain(address), domain, "{address:?}");
972            assert_eq!(parse_address_user_part(address), user, "{address:?}");
973            assert_eq!(parse_address_detail_part(address), detail, "{address:?}");
974        }
975    }
976}