Skip to main content

mail_builder/headers/
date.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 std::time::SystemTime;
8
9pub static DOW: &[&str] = &["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
10pub static MONTH: &[&str] = &[
11    "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
12];
13
14const DOW_BYTES: [[u8; 3]; 7] = [
15    *b"Sun", *b"Mon", *b"Tue", *b"Wed", *b"Thu", *b"Fri", *b"Sat",
16];
17const MONTH_BYTES: [[u8; 3]; 12] = [
18    *b"Jan", *b"Feb", *b"Mar", *b"Apr", *b"May", *b"Jun", *b"Jul", *b"Aug", *b"Sep", *b"Oct",
19    *b"Nov", *b"Dec",
20];
21
22const RFC822_LEN: usize = 40;
23const SECONDS_PER_DAY: i64 = 86400;
24
25use super::Header;
26use crate::writer::Writer;
27
28/// RFC5322 Date header
29#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
30pub struct Date {
31    pub date: i64,
32}
33
34#[inline(always)]
35fn push<const N: usize>(buffer: &mut [u8; RFC822_LEN], pos: &mut usize, bytes: [u8; N]) {
36    if let Some(slot) = buffer
37        .get_mut(*pos..)
38        .and_then(|tail| tail.first_chunk_mut::<N>())
39    {
40        *slot = bytes;
41        *pos += N;
42    }
43}
44
45#[inline(always)]
46const fn two_digits(value: u64) -> [u8; 2] {
47    [b'0' + (value / 10) as u8, b'0' + (value % 10) as u8]
48}
49
50#[inline(never)]
51fn push_wide_year(buffer: &mut [u8; RFC822_LEN], pos: &mut usize, year: i64) {
52    let mut digits = [0u8; 20];
53    let mut value = year.unsigned_abs();
54    let mut start = digits.len();
55
56    loop {
57        start -= 1;
58        if let Some(slot) = digits.get_mut(start) {
59            *slot = b'0' + (value % 10) as u8;
60        }
61        value /= 10;
62        if value == 0 || start == 0 {
63            break;
64        }
65    }
66
67    let mut width = digits.len() - start;
68    if year < 0 {
69        push(buffer, pos, *b"-");
70        width += 1;
71    }
72    while width < 4 {
73        push(buffer, pos, *b"0");
74        width += 1;
75    }
76    for &digit in digits.get(start..).unwrap_or_default() {
77        push(buffer, pos, [digit]);
78    }
79}
80
81impl Date {
82    /// Create a new Date header from a timestamp.
83    pub fn new(date: i64) -> Self {
84        Self { date }
85    }
86
87    /// Create a new Date header using the current time.
88    pub fn now() -> Self {
89        Self {
90            date: SystemTime::now()
91                .duration_since(SystemTime::UNIX_EPOCH)
92                .map(|d| d.as_secs())
93                .unwrap_or(0) as i64,
94        }
95    }
96
97    fn format_rfc822(&self, buffer: &mut [u8; RFC822_LEN]) -> usize {
98        let days = self.date.div_euclid(SECONDS_PER_DAY);
99        let seconds = self.date.rem_euclid(SECONDS_PER_DAY) as u64;
100
101        let z = days + 719468;
102        let era = z.div_euclid(146097);
103        let doe = (z - era * 146097) as u64;
104        let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
105        let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
106        let mp = (5 * doy + 2) / 153;
107        let day = doy - (153 * mp + 2) / 5 + 1;
108        let month = if mp < 10 { mp + 3 } else { mp - 9 };
109        let year = yoe as i64 + era * 400 + i64::from(month <= 2);
110
111        let dow = DOW_BYTES[(days + 4).rem_euclid(7) as usize];
112        let mon = MONTH_BYTES
113            .get(month as usize - 1)
114            .copied()
115            .unwrap_or(*b"   ");
116
117        let mut pos = 0;
118        push(buffer, &mut pos, [dow[0], dow[1], dow[2], b',', b' ']);
119        if day >= 10 {
120            push(buffer, &mut pos, two_digits(day));
121        } else {
122            push(buffer, &mut pos, [b'0' + day as u8]);
123        }
124        push(buffer, &mut pos, [b' ', mon[0], mon[1], mon[2], b' ']);
125
126        if (0..10000).contains(&year) {
127            let year = year as u64;
128            push(
129                buffer,
130                &mut pos,
131                [
132                    b'0' + (year / 1000) as u8,
133                    b'0' + (year / 100 % 10) as u8,
134                    b'0' + (year / 10 % 10) as u8,
135                    b'0' + (year % 10) as u8,
136                ],
137            );
138        } else {
139            push_wide_year(buffer, &mut pos, year);
140        }
141
142        let [hour, minute, second] =
143            [seconds / 3600, seconds / 60 % 60, seconds % 60].map(two_digits);
144        push(
145            buffer,
146            &mut pos,
147            [
148                b' ', hour[0], hour[1], b':', minute[0], minute[1], b':', second[0], second[1],
149                b' ',
150            ],
151        );
152        push(buffer, &mut pos, *b"+0000");
153
154        pos
155    }
156
157    /// Returns an RFC822 date.
158    pub fn to_rfc822(&self) -> String {
159        let mut buffer = [0u8; RFC822_LEN];
160        let len = self.format_rfc822(&mut buffer);
161        String::from_utf8(buffer.get(..len).unwrap_or_default().to_vec()).unwrap_or_default()
162    }
163
164    /// Writes an RFC822 date without allocating.
165    pub fn write_rfc822(&self, output: &mut impl Writer) {
166        let mut buffer = [0u8; RFC822_LEN];
167        let len = self.format_rfc822(&mut buffer);
168        output.write(buffer.get(..len).unwrap_or_default());
169    }
170}
171
172impl From<i64> for Date {
173    fn from(datetime: i64) -> Self {
174        Date::new(datetime)
175    }
176}
177
178impl From<u64> for Date {
179    fn from(datetime: u64) -> Self {
180        Date::new(datetime as i64)
181    }
182}
183
184impl Header for Date {
185    fn write_header(&self, output: &mut impl Writer, _column: usize) {
186        self.write_rfc822(output);
187        output.write(b"\r\n");
188    }
189}