1use super::{
5 DAY_NAMES, MAX_SECS, MIN_SECS, MONTH_NAMES, SECS_PER_DAY, civil_from_days, days_from_civil,
6 days_in_month, weekday_from_days,
7};
8use crate::{
9 error::{ErrorKind, ParseError, Result, WriteError, err},
10 limits::Limits,
11 scan::control_kind,
12 text::comment_end_folded,
13 write::{Sink, reserve},
14};
15
16const MAX_OFFSET: i16 = 99 * 60 + 59;
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
38pub struct MailDate {
39 utc: i64,
40 offset: i16,
41 offset_unknown: bool,
42 leap_second: bool,
43}
44
45impl MailDate {
46 pub fn new(utc_secs: i64, offset_minutes: i16) -> Result<Self> {
65 if offset_minutes.unsigned_abs() > MAX_OFFSET.unsigned_abs() {
66 return err(ErrorKind::OutOfRange, 0);
67 }
68 let local = utc_secs.saturating_add(i64::from(offset_minutes) * 60);
69 if !(MIN_SECS..=MAX_SECS).contains(&local) {
70 return err(ErrorKind::OutOfRange, 0);
71 }
72 Ok(Self {
73 utc: utc_secs,
74 offset: offset_minutes,
75 offset_unknown: false,
76 leap_second: false,
77 })
78 }
79
80 #[must_use]
90 pub const fn utc(self) -> i64 {
91 self.utc
92 }
93
94 #[must_use]
104 pub const fn offset_minutes(self) -> i16 {
105 self.offset
106 }
107
108 #[must_use]
122 pub const fn offset_unknown(self) -> bool {
123 self.offset_unknown
124 }
125
126 #[must_use]
139 pub const fn leap_second(self) -> bool {
140 self.leap_second
141 }
142
143 #[must_use]
156 pub fn format(self) -> ([u8; 31], usize) {
157 let local = self.utc + i64::from(self.offset) * 60;
158 let days = local.div_euclid(SECS_PER_DAY);
159 let secs = local.rem_euclid(SECS_PER_DAY);
160 let (year, month, day) = civil_from_days(days);
161 let mut out = [b' '; 31];
162 let wd = usize::from(weekday_from_days(days)).saturating_sub(1);
163 put(
164 &mut out,
165 0,
166 DAY_NAMES
167 .get(wd)
168 .map_or(b"???".as_slice(), |d| d.as_slice()),
169 );
170 put(&mut out, 3, b",");
171 two(&mut out, 5, day);
172 let mon = MONTH_NAMES
173 .get(usize::from(month).saturating_sub(1))
174 .map_or(b"???".as_slice(), |m| m.as_slice());
175 put(&mut out, 8, mon);
176 let y = u32::try_from(year).unwrap_or(0);
178 two(&mut out, 12, (y / 100) as u8);
179 two(&mut out, 14, (y % 100) as u8);
180 two(&mut out, 17, (secs / 3600) as u8);
181 put(&mut out, 19, b":");
182 two(&mut out, 20, (secs / 60 % 60) as u8);
183 put(&mut out, 22, b":");
184 two(&mut out, 23, (secs % 60) as u8);
185 let (sign, abs) = if self.offset < 0 || self.offset_unknown {
186 (b"-", self.offset.unsigned_abs())
187 } else {
188 (b"+", self.offset.unsigned_abs())
189 };
190 put(&mut out, 26, sign);
191 two(&mut out, 27, (abs / 60) as u8);
192 two(&mut out, 29, (abs % 60) as u8);
193 (out, 31)
194 }
195
196 pub fn write_to(self, out: &mut impl Sink) -> core::result::Result<(), WriteError> {
217 let (bytes, len) = self.format();
218 reserve(out, len)?;
219 out.put(bytes.get(..len).unwrap_or(&bytes))?;
220 Ok(())
221 }
222
223 pub fn parse(value: &[u8], obsolete: bool) -> Result<Self> {
250 if let Some(d) = canonical(value) {
254 return Ok(d);
255 }
256 let mut c = Cur {
257 v: value,
258 i: 0,
259 limits: Limits::DEFAULT,
260 };
261 let _ = c.cfws()?;
262 let weekday_at = c.i;
263 let weekday = if c.peek().is_some_and(|b| b.is_ascii_alphabetic()) {
264 let wd = c.name(&DAY_NAMES)?;
265 let _ = c.cfws()?;
266 if c.peek() != Some(b',') {
267 return Err(c.fail(ErrorKind::InvalidDate));
268 }
269 c.i += 1;
270 let _ = c.cfws()?;
271 Some(wd + 1)
272 } else {
273 None
274 };
275 let day_at = c.i;
276 let (day, n) = c.digits()?;
277 if !(1..=2).contains(&n) {
278 return Err(c.fail_at(ErrorKind::InvalidDate, c.i - n));
279 }
280 c.space()?;
281 let month = c.name(&MONTH_NAMES)? + 1;
282 c.space()?;
283 let at = c.i;
284 let (y, n) = c.digits()?;
285 let year = match n {
286 4.. => y,
287 2 if obsolete => {
288 if y < 50 {
289 2000 + y
290 } else {
291 1900 + y
292 }
293 }
294 3 if obsolete => 1900 + y,
295 _ => return Err(c.fail_at(ErrorKind::InvalidDate, at)),
296 };
297 if !(1..=9999).contains(&year) {
298 return err(ErrorKind::OutOfRange, at);
299 }
300 c.space()?;
301 let (hour, minute, second) = c.time(obsolete)?;
302 c.space()?;
303 let (offset, unknown) = c.zone(obsolete)?;
304 let _ = c.cfws()?;
305 if c.i < value.len() {
306 return Err(c.fail(ErrorKind::TrailingData));
307 }
308 let (year, month, day) = (year as i32, month as u8, day as u8);
309 if day == 0 || day > days_in_month(year, month) {
310 return err(ErrorKind::InvalidDate, day_at);
311 }
312 let days = days_from_civil(year, month, day);
313 if weekday.is_some_and(|w| w as u8 != weekday_from_days(days)) {
314 return err(ErrorKind::WeekdayMismatch, weekday_at);
315 }
316 let leap_second = second == 60;
317 let second = if leap_second { 59 } else { second };
318 let local = days * SECS_PER_DAY + hour * 3600 + minute * 60 + second;
319 Ok(Self {
320 utc: local - i64::from(offset) * 60,
321 offset,
322 offset_unknown: unknown,
323 leap_second,
324 })
325 }
326}
327
328fn name3(b: &[u8], names: &[&[u8; 3]]) -> Option<usize> {
330 let [x, y, z] = *b else { return None };
331 if !(x.is_ascii_alphabetic() && y.is_ascii_alphabetic() && z.is_ascii_alphabetic()) {
332 return None;
333 }
334 let key = [x | 0x20, y | 0x20, z | 0x20];
336 names
337 .iter()
338 .position(|n| [n[0] | 0x20, n[1] | 0x20, n[2] | 0x20] == key)
339}
340
341fn pair(b: &[u8]) -> Option<u8> {
343 match *b {
344 [h @ b'0'..=b'9', l @ b'0'..=b'9'] => Some((h - b'0') * 10 + (l - b'0')),
345 _ => None,
346 }
347}
348
349fn canonical(v: &[u8]) -> Option<MailDate> {
352 if v.len() != 31 {
353 return None;
354 }
355 let at = |i: usize| v.get(i).copied();
356 let punct = at(3) == Some(b',')
357 && at(4) == Some(b' ')
358 && at(7) == Some(b' ')
359 && at(11) == Some(b' ')
360 && at(16) == Some(b' ')
361 && at(19) == Some(b':')
362 && at(22) == Some(b':')
363 && at(25) == Some(b' ');
364 if !punct {
365 return None;
366 }
367 let weekday = name3(v.get(0..3)?, &DAY_NAMES)? as u8 + 1;
368 let day = pair(v.get(5..7)?)?;
369 let month = name3(v.get(8..11)?, &MONTH_NAMES)? as u8 + 1;
370 let year = u16::from(pair(v.get(12..14)?)?) * 100 + u16::from(pair(v.get(14..16)?)?);
371 let (hour, minute, second) = (
372 pair(v.get(17..19)?)?,
373 pair(v.get(20..22)?)?,
374 pair(v.get(23..25)?)?,
375 );
376 let sign = at(26)?;
377 let (zh, zm) = (pair(v.get(27..29)?)?, pair(v.get(29..31)?)?);
378 if !matches!(sign, b'+' | b'-')
379 || zm > 59
380 || hour > 23
381 || minute > 59
382 || second > 60
383 || year == 0
384 {
385 return None;
386 }
387 let year = i32::from(year);
388 if day == 0 || day > days_in_month(year, month) {
389 return None;
390 }
391 let days = days_from_civil(year, month, day);
392 if weekday != weekday_from_days(days) {
393 return None;
394 }
395 let minutes = i16::from(zh) * 60 + i16::from(zm);
396 let (offset, unknown) = if sign == b'-' {
397 (-minutes, minutes == 0)
398 } else {
399 (minutes, false)
400 };
401 let leap_second = second == 60;
402 let second = i64::from(if leap_second { 59 } else { second });
403 let local = days * SECS_PER_DAY + i64::from(hour) * 3600 + i64::from(minute) * 60 + second;
404 Some(MailDate {
405 utc: local - i64::from(offset) * 60,
406 offset,
407 offset_unknown: unknown,
408 leap_second,
409 })
410}
411
412fn put(out: &mut [u8], at: usize, bytes: &[u8]) {
413 if let Some(dst) = out.get_mut(at..at + bytes.len()) {
414 dst.copy_from_slice(bytes);
415 }
416}
417
418fn two(out: &mut [u8], at: usize, v: u8) {
419 put(out, at, &[b'0' + v / 10, b'0' + v % 10]);
420}
421
422struct Cur<'a> {
424 v: &'a [u8],
425 i: usize,
426 limits: Limits,
427}
428
429impl Cur<'_> {
430 fn peek(&self) -> Option<u8> {
431 self.v.get(self.i).copied()
432 }
433
434 fn fail_at(&self, kind: ErrorKind, at: usize) -> ParseError {
435 match self.v.get(at) {
436 Some(&b) if crate::scan::is_control(b) => ParseError::new(control_kind(b), at),
437 _ => ParseError::new(kind, at),
438 }
439 }
440
441 fn fail(&self, kind: ErrorKind) -> ParseError {
442 self.fail_at(kind, self.i)
443 }
444
445 fn cfws(&mut self) -> Result<bool> {
448 let start = self.i;
449 loop {
450 match self.peek() {
451 Some(b' ' | b'\t') => self.i += 1,
452 Some(b'\r') => {
453 if self.v.get(self.i + 1) == Some(&b'\n')
454 && matches!(self.v.get(self.i + 2), Some(b' ' | b'\t'))
455 {
456 self.i += 3;
457 } else {
458 return Err(self.fail(ErrorKind::BareCr));
459 }
460 }
461 Some(b'(') => self.i = comment_end_folded(self.v, self.i, &self.limits)?.0,
462 _ => return Ok(self.i > start),
463 }
464 }
465 }
466
467 fn space(&mut self) -> Result<()> {
469 if self.cfws()? {
470 Ok(())
471 } else {
472 Err(self.fail(ErrorKind::InvalidDate))
473 }
474 }
475
476 fn digits(&mut self) -> Result<(u32, usize)> {
478 let start = self.i;
479 let mut v: u32 = 0;
480 while let Some(d) = self.peek().filter(u8::is_ascii_digit) {
481 v = v.saturating_mul(10).saturating_add(u32::from(d - b'0'));
482 self.i += 1;
483 }
484 if self.i == start {
485 return Err(self.fail(ErrorKind::InvalidDate));
486 }
487 Ok((v, self.i - start))
488 }
489
490 fn two(&mut self) -> Result<i64> {
492 let at = self.i;
493 let (v, n) = self.digits()?;
494 if n != 2 {
495 return Err(self.fail_at(ErrorKind::InvalidDate, at));
496 }
497 Ok(i64::from(v))
498 }
499
500 fn name(&mut self, names: &[&[u8; 3]]) -> Result<u32> {
502 let found = self
503 .v
504 .get(self.i..self.i + 3)
505 .and_then(|w| name3(w, names))
506 .filter(|_| !self.v.get(self.i + 3).is_some_and(u8::is_ascii_alphabetic));
507 match found {
508 Some(idx) => {
509 self.i += 3;
510 Ok(idx as u32)
511 }
512 None => Err(self.fail(ErrorKind::InvalidDate)),
513 }
514 }
515
516 fn time(&mut self, obsolete: bool) -> Result<(i64, i64, i64)> {
519 let at = self.i;
520 let hour = self.two()?;
521 let minute = {
522 self.colon(obsolete)?;
523 self.two()?
524 };
525 let mark = self.i;
526 let second = if obsolete {
527 let _ = self.cfws()?;
528 if self.peek() == Some(b':') {
529 self.colon(obsolete)?;
530 self.two()?
531 } else {
532 self.i = mark;
533 0
534 }
535 } else if self.peek() == Some(b':') {
536 self.i += 1;
537 self.two()?
538 } else {
539 0
540 };
541 if hour > 23 || minute > 59 || second > 60 {
542 return err(ErrorKind::InvalidDate, at);
543 }
544 Ok((hour, minute, second))
545 }
546
547 fn colon(&mut self, obsolete: bool) -> Result<()> {
548 if obsolete {
549 let _ = self.cfws()?;
550 }
551 if self.peek() != Some(b':') {
552 return Err(self.fail(ErrorKind::InvalidDate));
553 }
554 self.i += 1;
555 if obsolete {
556 let _ = self.cfws()?;
557 }
558 Ok(())
559 }
560
561 fn zone(&mut self, obsolete: bool) -> Result<(i16, bool)> {
564 let at = self.i;
565 match self.peek() {
566 Some(sign @ (b'+' | b'-')) => {
567 self.i += 1;
568 let (v, n) = self.digits()?;
569 if n != 4 || v % 100 > 59 {
570 return err(ErrorKind::InvalidDate, at);
571 }
572 let minutes = (v / 100 * 60 + v % 100) as i16;
573 if sign == b'-' {
574 Ok((-minutes, minutes == 0))
575 } else {
576 Ok((minutes, false))
577 }
578 }
579 Some(b) if obsolete && b.is_ascii_alphabetic() => {
580 let n = self
581 .v
582 .get(self.i..)
583 .unwrap_or(&[])
584 .iter()
585 .take_while(|b| b.is_ascii_alphabetic())
586 .count();
587 let name = self.v.get(self.i..self.i + n).unwrap_or(&[]);
588 const ZONES: [(&[u8], i16); 10] = [
589 (b"UT", 0),
590 (b"GMT", 0),
591 (b"EST", -300),
592 (b"EDT", -240),
593 (b"CST", -360),
594 (b"CDT", -300),
595 (b"MST", -420),
596 (b"MDT", -360),
597 (b"PST", -480),
598 (b"PDT", -420),
599 ];
600 let found = ZONES.iter().find(|(z, _)| z.eq_ignore_ascii_case(name));
601 self.i += n;
602 match found {
603 Some(&(_, off)) => Ok((off, false)),
604 None if n == 1 && !name.eq_ignore_ascii_case(b"j") => Ok((0, true)),
607 None => err(ErrorKind::InvalidDate, at),
608 }
609 }
610 _ => Err(self.fail(ErrorKind::InvalidDate)),
611 }
612 }
613}
614
615#[cfg(test)]
616mod tests {
617 use super::*;
618
619 fn p(v: &[u8]) -> MailDate {
620 MailDate::parse(v, false).unwrap()
621 }
622
623 fn k(v: &[u8], obsolete: bool) -> ErrorKind {
624 MailDate::parse(v, obsolete).unwrap_err().kind()
625 }
626
627 fn text(d: MailDate) -> std::string::String {
628 let (b, n) = d.format();
629 std::string::String::from_utf8(b[..n].to_vec()).unwrap()
630 }
631
632 #[test]
633 fn test_mail_date_edge_cases() {
634 let d = p(b"Wed, 21 Oct 2026 17:38:00 +0200");
635 assert_eq!(d.utc(), p(b"21 Oct 2026 15:38:00 +0000").utc());
636 assert_eq!(
637 text(p(b"21 Oct 2026 17:38 +0200")),
638 "Wed, 21 Oct 2026 17:38:00 +0200"
639 );
640 assert_eq!(
641 text(p(b"1 Jan 2026 00:00:00 +0000")),
642 "Thu, 01 Jan 2026 00:00:00 +0000"
643 );
644 assert_eq!(
645 k(b"Mon, 21 Oct 2026 17:38:00 +0200", false),
646 ErrorKind::WeekdayMismatch
647 );
648 assert_eq!(p(b"21 Oct 2026 17:38 +2400").offset_minutes(), 1440);
649 assert_eq!(k(b"21 Oct 2026 17:38 +0060", false), ErrorKind::InvalidDate);
650 let u = p(b"21 Oct 2026 17:38 -0000");
651 assert!(u.offset_unknown() && u.offset_minutes() == 0);
652 assert_eq!(text(u), "Wed, 21 Oct 2026 17:38:00 -0000");
653 assert!(!p(b"21 Oct 2026 17:38 +0000").offset_unknown());
654 let c = p(b" (a (b)) Wed , 21 (x) oct 2026 17:38:00 (y)\r\n +0200 (z) ");
655 assert_eq!(c.utc(), d.utc());
656 let l = p(b"31 Dec 2016 23:59:60 +0000");
657 assert!(l.leap_second());
658 assert_eq!(l.utc(), p(b"31 Dec 2016 23:59:59 +0000").utc());
659 assert_eq!(k(b"30 Feb 2026 00:00 +0000", false), ErrorKind::InvalidDate);
660 assert_eq!(k(b"21 Oct 2026 24:00 +0000", false), ErrorKind::InvalidDate);
661 assert_eq!(
662 k(b"21 Oct 2026 17:38 +0000 x", false),
663 ErrorKind::TrailingData
664 );
665 assert_eq!(
666 k(b"21 Oct 2026 17:38 +0000 (", false),
667 ErrorKind::UnbalancedComment
668 );
669 assert_eq!(k(b"21 Oct 2026 17:38\r\n+0000", false), ErrorKind::BareCr);
670 assert_eq!(
671 k(b"21 Octo 2026 17:38 +0000", false),
672 ErrorKind::InvalidDate
673 );
674 assert_eq!(k(b"21 Oct 0000 17:38 +0000", false), ErrorKind::OutOfRange);
675 assert_eq!(k(b"21 Oct 10000 17:38 +0000", false), ErrorKind::OutOfRange);
676 assert_eq!(k(b"21Oct 2026 17:38 +0000", false), ErrorKind::InvalidDate);
677 assert_eq!(k(b"", false), ErrorKind::InvalidDate);
678 let deep = b"21 Oct 2026 17:38 +0000 (((((((((x)))))))))";
679 assert_eq!(k(deep, false), ErrorKind::CommentTooDeep);
680 }
681
682 #[test]
683 fn test_mail_date_obsolete_forms() {
684 assert_eq!(k(b"21 Oct 26 17:38 EDT", false), ErrorKind::InvalidDate);
685 let d = MailDate::parse(b"21 Oct 26 17:38 EDT", true).unwrap();
686 assert_eq!(
687 (d.offset_minutes(), text(d).as_str()),
688 (-240, "Wed, 21 Oct 2026 17:38:00 -0400")
689 );
690 assert_eq!(
691 text(MailDate::parse(b"1 Jan 70 00:00 GMT", true).unwrap()),
692 "Thu, 01 Jan 1970 00:00:00 +0000"
693 );
694 assert_eq!(
695 text(MailDate::parse(b"1 Jan 49 00:00 UT", true).unwrap()),
696 "Fri, 01 Jan 2049 00:00:00 +0000"
697 );
698 assert_eq!(
699 text(MailDate::parse(b"1 Jan 100 00:00 PST", true).unwrap()),
700 "Sat, 01 Jan 2000 00:00:00 -0800"
701 );
702 assert!(
703 MailDate::parse(b"1 Jan 2026 00:00 Z", true)
704 .unwrap()
705 .offset_unknown()
706 );
707 assert_eq!(k(b"1 Jan 2026 00:00 J", true), ErrorKind::InvalidDate);
708 assert_eq!(k(b"1 Jan 2026 00:00 XYZ", true), ErrorKind::InvalidDate);
709 assert_eq!(
710 MailDate::parse(b"1 Jan 2026 00 : 00 : 30 +0000", true)
711 .unwrap()
712 .utc()
713 % 60,
714 30
715 );
716 assert_eq!(
717 k(b"1 Jan 2026 00 : 00 +0000", false),
718 ErrorKind::InvalidDate
719 );
720 }
721
722 #[test]
723 fn test_mail_date_new_and_write() {
724 assert_eq!(
725 MailDate::new(0, 6000).unwrap_err().kind(),
726 ErrorKind::OutOfRange
727 );
728 assert_eq!(
729 MailDate::new(0, -6000).unwrap_err().kind(),
730 ErrorKind::OutOfRange
731 );
732 assert!(MailDate::new(0, 5999).is_ok() && MailDate::new(0, -5999).is_ok());
733 assert_eq!(
734 MailDate::new(MIN_SECS, -1).unwrap_err().kind(),
735 ErrorKind::OutOfRange
736 );
737 assert_eq!(
738 MailDate::new(MAX_SECS, 1).unwrap_err().kind(),
739 ErrorKind::OutOfRange
740 );
741 assert_eq!(
742 text(MailDate::new(MIN_SECS, 0).unwrap()),
743 "Mon, 01 Jan 0001 00:00:00 +0000"
744 );
745 let mut buf = [0u8; 30];
746 let mut out = crate::SliceSink::new(&mut buf);
747 assert_eq!(
748 MailDate::new(0, 0).unwrap().write_to(&mut out),
749 Err(WriteError::Full)
750 );
751 assert!(out.is_empty());
752 for secs in [-62_135_596_800i64, -1, 0, 1_792_540_800, 253_402_300_799] {
753 for off in [-5999i16, -300, 0, 330, 5999] {
754 if let Ok(d) = MailDate::new(secs, off) {
755 let (b, n) = d.format();
756 let back = MailDate::parse(&b[..n], false).unwrap();
757 assert_eq!((back.utc(), back.offset_minutes()), (secs, off));
758 }
759 }
760 }
761 }
762}