1use 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
788pub 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
795pub 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
802pub 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
809pub 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}