Skip to main content

mail_parser/
header_name.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::{
8    borrow::Cow,
9    cmp::Ordering,
10    fmt,
11    hash::{Hash, Hasher},
12};
13
14macro_rules! header_names {
15    ($($variant:ident, $name:literal, $lc:literal;)+) => {
16        /// A header field name: one of the known names, or any other name
17        /// with its original spelling.
18        ///
19        /// Names compare ASCII case-insensitively, so `Other("x-custom")`
20        /// equals `Other("X-Custom")`, and an `Other` spelled like a known
21        /// name equals, hashes and orders as that name. Known names are
22        /// parsed into their variant by [`HeaderName::parse`], by the `From`
23        /// conversions (which keep any other string as written in `Other`)
24        /// and by the message parser, and keep their canonical spelling in
25        /// [`HeaderName::as_str`]; [`crate::Header::raw_name`] gives the
26        /// spelling used in a message.
27        ///
28        /// ```
29        /// use mail_parser::HeaderName;
30        ///
31        /// assert_eq!(HeaderName::parse("content-type"), Some(HeaderName::ContentType));
32        /// assert_eq!(HeaderName::ContentType.as_str(), "Content-Type");
33        /// let custom = HeaderName::parse("X-Custom").expect("valid name");
34        /// assert!(custom.is_other());
35        /// assert_eq!(custom, HeaderName::from("x-custom"));
36        /// ```
37        #[derive(Debug, Clone)]
38        #[non_exhaustive]
39        pub enum HeaderName<'x> {
40            $(
41                #[doc = concat!("`", $name, "`.")]
42                $variant,
43            )+
44            /// Any other name, with its original spelling.
45            Other(Cow<'x, str>),
46        }
47
48        #[derive(Debug, Clone, Copy, PartialEq, Eq)]
49        #[repr(u16)]
50        pub(crate) enum HeaderId {
51            $($variant,)+
52        }
53
54        pub(crate) const KNOWN_NAMES: usize = [$($lc,)+].len();
55
56        static CANONICAL: [&str; KNOWN_NAMES] = [$($name,)+];
57
58        const LOWERCASE: [&str; KNOWN_NAMES] = [$($lc,)+];
59
60        static VARIANTS: [HeaderName<'static>; KNOWN_NAMES] = [$(HeaderName::$variant,)+];
61
62        impl HeaderName<'_> {
63            pub(crate) fn id(&self) -> u16 {
64                match self {
65                    $(HeaderName::$variant => HeaderId::$variant as u16,)+
66                    HeaderName::Other(_) => OTHER_ID,
67                }
68            }
69        }
70
71        pub(crate) fn lookup_lowercase(name: &[u8]) -> Option<u16> {
72            hashify::fnc_map!(name,
73                $($lc => Some(HeaderId::$variant as u16),)+
74                _ => None
75            )
76        }
77    };
78}
79
80pub(crate) const OTHER_ID: u16 = u16::MAX;
81const CONTENT_PREFIX: &[u8] = b"content-";
82pub(crate) const FIRST_VENDOR: u16 = HeaderId::XOriginalTo as u16;
83
84header_names! {
85    Subject, "Subject", "subject";
86    From, "From", "from";
87    To, "To", "to";
88    Cc, "Cc", "cc";
89    Date, "Date", "date";
90    Bcc, "Bcc", "bcc";
91    ReplyTo, "Reply-To", "reply-to";
92    Sender, "Sender", "sender";
93    Comments, "Comments", "comments";
94    InReplyTo, "In-Reply-To", "in-reply-to";
95    Keywords, "Keywords", "keywords";
96    Received, "Received", "received";
97    MessageId, "Message-ID", "message-id";
98    References, "References", "references";
99    ReturnPath, "Return-Path", "return-path";
100    MimeVersion, "MIME-Version", "mime-version";
101    ContentDescription, "Content-Description", "content-description";
102    ContentId, "Content-ID", "content-id";
103    ContentLanguage, "Content-Language", "content-language";
104    ContentLocation, "Content-Location", "content-location";
105    ContentTransferEncoding, "Content-Transfer-Encoding", "content-transfer-encoding";
106    ContentType, "Content-Type", "content-type";
107    ContentDisposition, "Content-Disposition", "content-disposition";
108    ResentTo, "Resent-To", "resent-to";
109    ResentFrom, "Resent-From", "resent-from";
110    ResentBcc, "Resent-Bcc", "resent-bcc";
111    ResentCc, "Resent-Cc", "resent-cc";
112    ResentSender, "Resent-Sender", "resent-sender";
113    ResentDate, "Resent-Date", "resent-date";
114    ResentMessageId, "Resent-Message-ID", "resent-message-id";
115    ListArchive, "List-Archive", "list-archive";
116    ListHelp, "List-Help", "list-help";
117    ListId, "List-ID", "list-id";
118    ListOwner, "List-Owner", "list-owner";
119    ListPost, "List-Post", "list-post";
120    ListSubscribe, "List-Subscribe", "list-subscribe";
121    ListUnsubscribe, "List-Unsubscribe", "list-unsubscribe";
122    DkimSignature, "DKIM-Signature", "dkim-signature";
123    ArcAuthenticationResults, "ARC-Authentication-Results", "arc-authentication-results";
124    ArcMessageSignature, "ARC-Message-Signature", "arc-message-signature";
125    ArcSeal, "ARC-Seal", "arc-seal";
126    Dkim2Signature, "DKIM2-Signature", "dkim2-signature";
127    MessageInstance, "Message-Instance", "message-instance";
128    AcceptLanguage, "Accept-Language", "accept-language";
129    AlternateRecipient, "Alternate-Recipient", "alternate-recipient";
130    ArchivedAt, "Archived-At", "archived-at";
131    AuthenticationResults, "Authentication-Results", "authentication-results";
132    AutoSubmitted, "Auto-Submitted", "auto-submitted";
133    Autoforwarded, "Autoforwarded", "autoforwarded";
134    Autosubmitted, "Autosubmitted", "autosubmitted";
135    ContentAlternative, "Content-Alternative", "content-alternative";
136    ContentDuration, "Content-Duration", "content-duration";
137    ContentFeatures, "Content-features", "content-features";
138    ContentMd5, "Content-MD5", "content-md5";
139    ContentTranslationType, "Content-Translation-Type", "content-translation-type";
140    Conversion, "Conversion", "conversion";
141    ConversionWithLoss, "Conversion-With-Loss", "conversion-with-loss";
142    DlExpansionHistory, "DL-Expansion-History", "dl-expansion-history";
143    DeferredDelivery, "Deferred-Delivery", "deferred-delivery";
144    DeliveryDate, "Delivery-Date", "delivery-date";
145    DiscardedX400IpmsExtensions, "Discarded-X400-IPMS-Extensions", "discarded-x400-ipms-extensions";
146    DiscardedX400MtsExtensions, "Discarded-X400-MTS-Extensions", "discarded-x400-mts-extensions";
147    DiscloseRecipients, "Disclose-Recipients", "disclose-recipients";
148    DispositionNotificationOptions, "Disposition-Notification-Options", "disposition-notification-options";
149    DispositionNotificationTo, "Disposition-Notification-To", "disposition-notification-to";
150    DowngradedFinalRecipient, "Downgraded-Final-Recipient", "downgraded-final-recipient";
151    DowngradedInReplyTo, "Downgraded-In-Reply-To", "downgraded-in-reply-to";
152    DowngradedMessageId, "Downgraded-Message-Id", "downgraded-message-id";
153    DowngradedOriginalRecipient, "Downgraded-Original-Recipient", "downgraded-original-recipient";
154    DowngradedReferences, "Downgraded-References", "downgraded-references";
155    Encoding, "Encoding", "encoding";
156    Expires, "Expires", "expires";
157    GenerateDeliveryReport, "Generate-Delivery-Report", "generate-delivery-report";
158    HpOuter, "HP-Outer", "hp-outer";
159    Importance, "Importance", "importance";
160    IncompleteCopy, "Incomplete-Copy", "incomplete-copy";
161    Language, "Language", "language";
162    LatestDeliveryTime, "Latest-Delivery-Time", "latest-delivery-time";
163    ListUnsubscribePost, "List-Unsubscribe-Post", "list-unsubscribe-post";
164    MessageContext, "Message-Context", "message-context";
165    MessageType, "Message-Type", "message-type";
166    MmhsExemptedAddress, "MMHS-Exempted-Address", "mmhs-exempted-address";
167    MmhsExtendedAuthorisationInfo, "MMHS-Extended-Authorisation-Info", "mmhs-extended-authorisation-info";
168    MmhsSubjectIndicatorCodes, "MMHS-Subject-Indicator-Codes", "mmhs-subject-indicator-codes";
169    MmhsHandlingInstructions, "MMHS-Handling-Instructions", "mmhs-handling-instructions";
170    MmhsMessageInstructions, "MMHS-Message-Instructions", "mmhs-message-instructions";
171    MmhsCodressMessageIndicator, "MMHS-Codress-Message-Indicator", "mmhs-codress-message-indicator";
172    MmhsOriginatorReference, "MMHS-Originator-Reference", "mmhs-originator-reference";
173    MmhsPrimaryPrecedence, "MMHS-Primary-Precedence", "mmhs-primary-precedence";
174    MmhsCopyPrecedence, "MMHS-Copy-Precedence", "mmhs-copy-precedence";
175    MmhsMessageType, "MMHS-Message-Type", "mmhs-message-type";
176    MmhsOtherRecipientsIndicatorTo, "MMHS-Other-Recipients-Indicator-To", "mmhs-other-recipients-indicator-to";
177    MmhsOtherRecipientsIndicatorCc, "MMHS-Other-Recipients-Indicator-CC", "mmhs-other-recipients-indicator-cc";
178    MmhsAcp127MessageIdentifier, "MMHS-Acp127-Message-Identifier", "mmhs-acp127-message-identifier";
179    MmhsOriginatorPlad, "MMHS-Originator-PLAD", "mmhs-originator-plad";
180    MtPriority, "MT-Priority", "mt-priority";
181    Organization, "Organization", "organization";
182    OriginalEncodedInformationTypes, "Original-Encoded-Information-Types", "original-encoded-information-types";
183    OriginalFrom, "Original-From", "original-from";
184    OriginalMessageId, "Original-Message-ID", "original-message-id";
185    OriginalRecipient, "Original-Recipient", "original-recipient";
186    OriginatorReturnAddress, "Originator-Return-Address", "originator-return-address";
187    OriginalSubject, "Original-Subject", "original-subject";
188    PicsLabel, "PICS-Label", "pics-label";
189    PreventNonDeliveryReport, "Prevent-NonDelivery-Report", "prevent-nondelivery-report";
190    Priority, "Priority", "priority";
191    ReceivedSpf, "Received-SPF", "received-spf";
192    ReplyBy, "Reply-By", "reply-by";
193    RequireRecipientValidSince, "Require-Recipient-Valid-Since", "require-recipient-valid-since";
194    Sensitivity, "Sensitivity", "sensitivity";
195    Solicitation, "Solicitation", "solicitation";
196    Supersedes, "Supersedes", "supersedes";
197    TlsReportDomain, "TLS-Report-Domain", "tls-report-domain";
198    TlsReportSubmitter, "TLS-Report-Submitter", "tls-report-submitter";
199    TlsRequired, "TLS-Required", "tls-required";
200    VbrInfo, "VBR-Info", "vbr-info";
201    X400ContentIdentifier, "X400-Content-Identifier", "x400-content-identifier";
202    X400ContentReturn, "X400-Content-Return", "x400-content-return";
203    X400ContentType, "X400-Content-Type", "x400-content-type";
204    X400MtsIdentifier, "X400-MTS-Identifier", "x400-mts-identifier";
205    X400Originator, "X400-Originator", "x400-originator";
206    X400Received, "X400-Received", "x400-received";
207    X400Recipients, "X400-Recipients", "x400-recipients";
208    X400Trace, "X400-Trace", "x400-trace";
209    ApparentlyTo, "Apparently-To", "apparently-to";
210    Author, "Author", "author";
211    CfblAddress, "CFBL-Address", "cfbl-address";
212    CfblFeedbackId, "CFBL-Feedback-ID", "cfbl-feedback-id";
213    DeliveredTo, "Delivered-To", "delivered-to";
214    EdiintFeatures, "EDIINT-Features", "ediint-features";
215    EesstVersion, "Eesst-Version", "eesst-version";
216    ErrorsTo, "Errors-To", "errors-to";
217    Face, "Face", "face";
218    FormSub, "Form-Sub", "form-sub";
219    JabberId, "Jabber-ID", "jabber-id";
220    MmhsAuthorizingUsers, "MMHS-Authorizing-Users", "mmhs-authorizing-users";
221    Privicon, "Privicon", "privicon";
222    SioLabel, "SIO-Label", "sio-label";
223    SioLabelHistory, "SIO-Label-History", "sio-label-history";
224    WrongRecipient, "Wrong-Recipient", "wrong-recipient";
225    XOriginalTo, "X-Original-To", "x-original-to";
226    ReturnReceiptTo, "Return-Receipt-To", "return-receipt-to";
227    XSpamStatus, "X-Spam-Status", "x-spam-status";
228    XSpamScore, "X-Spam-Score", "x-spam-score";
229    XSpamFlag, "X-Spam-Flag", "x-spam-flag";
230    XSpamResult, "X-Spam-Result", "x-spam-result";
231    XPriority, "X-Priority", "x-priority";
232    XMSMailPriority, "X-MSMail-Priority", "x-msmail-priority";
233    XMailer, "X-Mailer", "x-mailer";
234    UserAgent, "User-Agent", "user-agent";
235    XMimeOLE, "X-MimeOLE", "x-mimeole";
236    XOriginatingIp, "X-Originating-IP", "x-originating-ip";
237    XForwardedTo, "X-Forwarded-To", "x-forwarded-to";
238    XForwardedFor, "X-Forwarded-For", "x-forwarded-for";
239    XAutoResponseSuppress, "X-Auto-Response-Suppress", "x-auto-response-suppress";
240    Precedence, "Precedence", "precedence";
241    ThreadIndex, "Thread-Index", "thread-index";
242    ThreadTopic, "Thread-Topic", "thread-topic";
243    FeedbackId, "Feedback-ID", "feedback-id";
244    XUniversallyUniqueIdentifier, "X-Universally-Unique-Identifier", "x-universally-unique-identifier";
245    XVirusScanned, "X-Virus-Scanned", "x-virus-scanned";
246    XMailFrom, "X-MailFrom", "x-mailfrom";
247    XMailmanVersion, "X-Mailman-Version", "x-mailman-version";
248    MessageIdHash, "Message-ID-Hash", "message-id-hash";
249    XMessageIdHash, "X-Message-ID-Hash", "x-message-id-hash";
250    XMailmanRuleMisses, "X-Mailman-Rule-Misses", "x-mailman-rule-misses";
251    XSpamLevel, "X-Spam-Level", "x-spam-level";
252    XReceived, "X-Received", "x-received";
253    XGmMessageState, "X-Gm-Message-State", "x-gm-message-state";
254    XGoogleDkimSignature, "X-Google-DKIM-Signature", "x-google-dkim-signature";
255    XMailboxLine, "X-Mailbox-Line", "x-mailbox-line";
256    XGmGg, "X-Gm-Gg", "x-gm-gg";
257    XOriginalFrom, "X-Original-From", "x-original-from";
258    XGoogleSmtpSource, "X-Google-Smtp-Source", "x-google-smtp-source";
259    XMsTnefCorrelator, "X-MS-TNEF-Correlator", "x-ms-tnef-correlator";
260    XMsHasAttach, "X-MS-Has-Attach", "x-ms-has-attach";
261    XMeProxy, "X-ME-Proxy", "x-me-proxy";
262    XMeProxyCause, "X-ME-Proxy-Cause", "x-me-proxy-cause";
263    XMeSender, "X-ME-Sender", "x-me-sender";
264    XGmFeatures, "X-Gm-Features", "x-gm-features";
265    XMsExchangeTransportCrossTenantHeadersStamped, "X-MS-Exchange-Transport-CrossTenantHeadersStamped", "x-ms-exchange-transport-crosstenantheadersstamped";
266    XMsPublicTrafficType, "X-MS-PublicTrafficType", "x-ms-publictraffictype";
267    XOriginatorOrg, "X-OriginatorOrg", "x-originatororg";
268    XMsExchangeCrossTenantNetworkMessageId, "X-MS-Exchange-CrossTenant-Network-Message-Id", "x-ms-exchange-crosstenant-network-message-id";
269    XMsExchangeCrossTenantAuthSource, "X-MS-Exchange-CrossTenant-AuthSource", "x-ms-exchange-crosstenant-authsource";
270    XMsExchangeCrossTenantAuthAs, "X-MS-Exchange-CrossTenant-AuthAs", "x-ms-exchange-crosstenant-authas";
271    XMsExchangeCrossTenantOriginalArrivalTime, "X-MS-Exchange-CrossTenant-OriginalArrivalTime", "x-ms-exchange-crosstenant-originalarrivaltime";
272    XMicrosoftAntispam, "X-Microsoft-Antispam", "x-microsoft-antispam";
273    XMsExchangeCrossTenantId, "X-MS-Exchange-CrossTenant-Id", "x-ms-exchange-crosstenant-id";
274    XMsTrafficTypeDiagnostic, "X-MS-TrafficTypeDiagnostic", "x-ms-traffictypediagnostic";
275    XMicrosoftAntispamMessageInfo, "X-Microsoft-Antispam-Message-Info", "x-microsoft-antispam-message-info";
276    XMsOffice365FilteringCorrelationId, "X-MS-Office365-Filtering-Correlation-Id", "x-ms-office365-filtering-correlation-id";
277    XMsExchangeCrossTenantFromEntityHeader, "X-MS-Exchange-CrossTenant-FromEntityHeader", "x-ms-exchange-crosstenant-fromentityheader";
278    XMsExchangeAntiSpamMessageDataChunkCount, "X-MS-Exchange-AntiSpam-MessageData-ChunkCount", "x-ms-exchange-antispam-messagedata-chunkcount";
279    XMsExchangeAntiSpamMessageData0, "X-MS-Exchange-AntiSpam-MessageData-0", "x-ms-exchange-antispam-messagedata-0";
280    XMsExchangeSenderADCheck, "X-MS-Exchange-SenderADCheck", "x-ms-exchange-senderadcheck";
281    XForefrontAntispamReport, "X-Forefront-Antispam-Report", "x-forefront-antispam-report";
282    XMsExchangeAntiSpamRelay, "X-MS-Exchange-AntiSpam-Relay", "x-ms-exchange-antispam-relay";
283    XMsExchangeCrossTenantMailboxType, "X-MS-Exchange-CrossTenant-MailboxType", "x-ms-exchange-crosstenant-mailboxtype";
284    XMsExchangeCrossTenantUserPrincipalName, "X-MS-Exchange-CrossTenant-UserPrincipalName", "x-ms-exchange-crosstenant-userprincipalname";
285    XGitHubReason, "X-GitHub-Reason", "x-github-reason";
286    XGitHubRecipientAddress, "X-GitHub-Recipient-Address", "x-github-recipient-address";
287    XGitHubRecipient, "X-GitHub-Recipient", "x-github-recipient";
288    Destinations, "Destinations", "destinations";
289    XGitHubSender, "X-GitHub-Sender", "x-github-sender";
290    XGitHubNotifyPlatform, "X-GitHub-Notify-Platform", "x-github-notify-platform";
291    XThreadId, "X-ThreadId", "x-threadid";
292    XSesOutgoing, "X-SES-Outgoing", "x-ses-outgoing";
293    XProofpointVirusVersion, "X-Proofpoint-Virus-Version", "x-proofpoint-virus-version";
294    XGitHubAssignees, "X-GitHub-Assignees", "x-github-assignees";
295    XGitHubLabels, "X-GitHub-Labels", "x-github-labels";
296    BimiSelector, "BIMI-Selector", "bimi-selector";
297    XStripeEid, "X-Stripe-EID", "x-stripe-eid";
298    XAttachmentId, "X-Attachment-Id", "x-attachment-id";
299    XTestIDTracker, "X-Test-IDTracker", "x-test-idtracker";
300    XIetfIDTracker, "X-IETF-IDTracker", "x-ietf-idtracker";
301    XPmMessageId, "X-Pm-Message-ID", "x-pm-message-id";
302    XAntiAbuse, "X-AntiAbuse", "x-antiabuse";
303    Autocrypt, "Autocrypt", "autocrypt";
304    XComplaintsTo, "X-Complaints-To", "x-complaints-to";
305    XProofpointOrigGuid, "X-Proofpoint-ORIG-GUID", "x-proofpoint-orig-guid";
306    XProofpointGuid, "X-Proofpoint-GUID", "x-proofpoint-guid";
307    XGitHubIssueState, "X-GitHub-IssueState", "x-github-issuestate";
308    XMsReactions, "X-MS-Reactions", "x-ms-reactions";
309    XPmMtaPool, "X-Pm-MTA-Pool", "x-pm-mta-pool";
310    XPmRcpt, "X-Pm-RCPT", "x-pm-rcpt";
311    XProofpointSpamDetailsEnc, "X-Proofpoint-Spam-Details-Enc", "x-proofpoint-spam-details-enc";
312    XPmMessageOptions, "X-Pm-Message-Options", "x-pm-message-options";
313    XSenderId, "X-Sender-ID", "x-sender-id";
314    XGitHubPullRequestStatus, "X-GitHub-PullRequestStatus", "x-github-pullrequeststatus";
315    XAuthorityAnalysis, "X-Authority-Analysis", "x-authority-analysis";
316    XForwardedEncrypted, "X-Forwarded-Encrypted", "x-forwarded-encrypted";
317    XMsExchangeAntiSpamExternalHopMessageDataChunkCount, "X-MS-Exchange-AntiSpam-ExternalHop-MessageData-ChunkCount", "x-ms-exchange-antispam-externalhop-messagedata-chunkcount";
318    XMsExchangeAntiSpamExternalHopMessageData0, "X-MS-Exchange-AntiSpam-ExternalHop-MessageData-0", "x-ms-exchange-antispam-externalhop-messagedata-0";
319    XMsExchangeCrossTenantRmsPersistedConsumerOrg, "X-MS-Exchange-CrossTenant-RMS-PersistedConsumerOrg", "x-ms-exchange-crosstenant-rms-persistedconsumerorg";
320    XExchangeRoutingPolicyChecked, "X-Exchange-RoutingPolicyChecked", "x-exchange-routingpolicychecked";
321    XMsExchangeCrossTenantOriginalAttributedTenantConnectingIp, "X-MS-Exchange-CrossTenant-OriginalAttributedTenantConnectingIp", "x-ms-exchange-crosstenant-originalattributedtenantconnectingip";
322    XMsExchangeMessageSentRepresentingType, "X-MS-Exchange-MessageSentRepresentingType", "x-ms-exchange-messagesentrepresentingtype";
323    XMsExchangeTransportCrossTenantHeadersStripped, "X-MS-Exchange-Transport-CrossTenantHeadersStripped", "x-ms-exchange-transport-crosstenantheadersstripped";
324    XMsOffice365FilteringCorrelationIdPrvs, "X-MS-Office365-Filtering-Correlation-Id-Prvs", "x-ms-office365-filtering-correlation-id-prvs";
325    XMicrosoftAntispamMessageInfoOriginal, "X-Microsoft-Antispam-Message-Info-Original", "x-microsoft-antispam-message-info-original";
326    XMsExchangeAuthenticationResults, "X-MS-Exchange-Authentication-Results", "x-ms-exchange-authentication-results";
327    XForefrontAntispamReportUntrusted, "X-Forefront-Antispam-Report-Untrusted", "x-forefront-antispam-report-untrusted";
328    XMsExchangeAtpMessageProperties, "X-MS-Exchange-AtpMessageProperties", "x-ms-exchange-atpmessageproperties";
329    XMicrosoftAntispamUntrusted, "X-Microsoft-Antispam-Untrusted", "x-microsoft-antispam-untrusted";
330    XMsExchangeAntiSpamMessageData1, "X-MS-Exchange-AntiSpam-MessageData-1", "x-ms-exchange-antispam-messagedata-1";
331    XMissiveId, "X-Missive-Id", "x-missive-id";
332    XIadbIpReverse, "X-IADB-IP-Reverse", "x-iadb-ip-reverse";
333    XReportAbuse, "X-Report-Abuse", "x-report-abuse";
334    DkimFilter, "DKIM-Filter", "dkim-filter";
335    XMeReceived, "X-ME-Received", "x-me-received";
336    XProofpointSpamDetails, "X-Proofpoint-Spam-Details", "x-proofpoint-spam-details";
337}
338
339const fn longest(names: &[&str]) -> usize {
340    let mut index = 0;
341    let mut max = 0;
342    while index < names.len() {
343        if names[index].len() > max {
344            max = names[index].len();
345        }
346        index += 1;
347    }
348    max
349}
350
351pub(crate) const MAX_NAME_LEN: usize = longest(&LOWERCASE);
352
353pub(crate) fn known(id: u16) -> Option<&'static HeaderName<'static>> {
354    VARIANTS.get(id as usize)
355}
356
357pub(crate) fn canonical(id: u16) -> &'static str {
358    CANONICAL.get(id as usize).copied().unwrap_or_default()
359}
360
361pub(crate) fn lookup(name: &[u8]) -> Option<u16> {
362    let mut buf = [0u8; MAX_NAME_LEN];
363    let lowercase = buf.get_mut(..name.len())?;
364    lowercase
365        .iter_mut()
366        .zip(name)
367        .for_each(|(out, byte)| *out = byte.to_ascii_lowercase());
368    lookup_lowercase(lowercase)
369}
370
371pub(crate) fn trim_blank_end(name: &[u8]) -> &[u8] {
372    let len = name
373        .iter()
374        .rposition(|&byte| !matches!(byte, b' ' | b'\t'))
375        .map_or(0, |last| last + 1);
376    name.get(..len).unwrap_or_default()
377}
378
379#[inline(never)]
380fn other_id(name: &str) -> u16 {
381    lookup(name.as_bytes()).unwrap_or(OTHER_ID)
382}
383
384fn is_ftext(byte: u8) -> bool {
385    matches!(byte, b'!'..=b'9' | b';'..=b'~')
386}
387
388impl<'x> HeaderName<'x> {
389    /// Parses a header name. Returns `None` for an empty name or one with a
390    /// byte that RFC 5322 does not allow in a field name (anything but
391    /// printable US-ASCII other than the colon). Never allocates.
392    pub fn parse(name: impl Into<Cow<'x, str>>) -> Option<HeaderName<'x>> {
393        let name = name.into();
394        (!name.is_empty() && name.bytes().all(is_ftext)).then(|| HeaderName::resolve(name))
395    }
396
397    fn resolve(name: Cow<'x, str>) -> HeaderName<'x> {
398        match lookup(name.as_bytes()).and_then(known) {
399            Some(known) => known.clone(),
400            None => HeaderName::Other(name),
401        }
402    }
403
404    /// Resolves the name once, for code that looks the same name up in many
405    /// parts or messages: [`crate::Headers::all_key`] and
406    /// [`crate::Headers::get_key`] then skip the name lookup that
407    /// [`crate::Headers::all`] and [`crate::Headers::get`] do on every call.
408    ///
409    /// ```
410    /// use mail_parser::{HeaderName, MessageParser};
411    ///
412    /// let message = MessageParser::new()
413    ///     .parse(b"X-Tag: a\r\nX-Tag: b\r\n\r\nbody")
414    ///     .expect("message");
415    /// let name = HeaderName::from("x-tag");
416    /// let key = name.key();
417    /// assert_eq!(message.headers().all_key(key).count(), 2);
418    /// assert_eq!(
419    ///     message.headers().get_key(key).map(|header| header.raw_value()),
420    ///     Some(&b" b\r\n"[..])
421    /// );
422    /// ```
423    #[inline]
424    pub fn key(&self) -> HeaderKey<'_> {
425        HeaderKey {
426            id: self.resolved_id(),
427            name: self.as_str(),
428        }
429    }
430
431    #[inline]
432    pub(crate) fn resolved_id(&self) -> u16 {
433        match self {
434            HeaderName::Other(name) => other_id(name),
435            known => known.id(),
436        }
437    }
438
439    /// Converts the name into one that owns its spelling.
440    pub fn into_owned(self) -> HeaderName<'static> {
441        match self {
442            HeaderName::Other(name) => HeaderName::Other(name.into_owned().into()),
443            known => known
444                .known_static()
445                .cloned()
446                .unwrap_or(HeaderName::Other(Cow::Borrowed(""))),
447        }
448    }
449
450    fn known_static(&self) -> Option<&'static HeaderName<'static>> {
451        match self {
452            HeaderName::Other(_) => None,
453            known => VARIANTS.get(known.id() as usize),
454        }
455    }
456
457    /// Canonical spelling of a known name; empty for other names.
458    pub fn as_static_str(&self) -> &'static str {
459        match self {
460            HeaderName::Other(_) => "",
461            known => CANONICAL
462                .get(known.id() as usize)
463                .copied()
464                .unwrap_or_default(),
465        }
466    }
467
468    /// Canonical spelling of a known name, or the original spelling of any
469    /// other name.
470    pub fn as_str(&self) -> &str {
471        match self {
472            HeaderName::Other(name) => name.as_ref(),
473            known => known.as_static_str(),
474        }
475    }
476
477    /// Whether this is not one of the known names.
478    pub fn is_other(&self) -> bool {
479        matches!(self, HeaderName::Other(_))
480    }
481
482    /// Whether this is one of the seven `Content-*` MIME header fields.
483    pub fn is_mime_header(&self) -> bool {
484        matches!(
485            self,
486            HeaderName::ContentDescription
487                | HeaderName::ContentId
488                | HeaderName::ContentLanguage
489                | HeaderName::ContentLocation
490                | HeaderName::ContentTransferEncoding
491                | HeaderName::ContentType
492                | HeaderName::ContentDisposition
493        )
494    }
495
496    /// Whether this is any `Content-*` header field, known or not: the
497    /// fields that describe a part's content and do not survive replacing it.
498    pub fn is_content_header(&self) -> bool {
499        match self {
500            HeaderName::ContentDescription
501            | HeaderName::ContentId
502            | HeaderName::ContentLanguage
503            | HeaderName::ContentLocation
504            | HeaderName::ContentTransferEncoding
505            | HeaderName::ContentType
506            | HeaderName::ContentDisposition
507            | HeaderName::ContentAlternative
508            | HeaderName::ContentDuration
509            | HeaderName::ContentFeatures
510            | HeaderName::ContentMd5
511            | HeaderName::ContentTranslationType => true,
512            HeaderName::Other(name) => name
513                .as_bytes()
514                .get(..CONTENT_PREFIX.len())
515                .is_some_and(|prefix| prefix.eq_ignore_ascii_case(CONTENT_PREFIX)),
516            _ => false,
517        }
518    }
519
520    /// Whether the default parser yields a structured value for this name.
521    pub fn is_structured(&self) -> bool {
522        matches!(
523            self,
524            HeaderName::Subject
525                | HeaderName::Comments
526                | HeaderName::ContentDescription
527                | HeaderName::ContentLocation
528                | HeaderName::ContentTransferEncoding
529                | HeaderName::From
530                | HeaderName::To
531                | HeaderName::Cc
532                | HeaderName::Bcc
533                | HeaderName::ReplyTo
534                | HeaderName::Sender
535                | HeaderName::ResentTo
536                | HeaderName::ResentFrom
537                | HeaderName::ResentBcc
538                | HeaderName::ResentCc
539                | HeaderName::ResentSender
540                | HeaderName::ListArchive
541                | HeaderName::ListHelp
542                | HeaderName::ListId
543                | HeaderName::ListOwner
544                | HeaderName::ListPost
545                | HeaderName::ListSubscribe
546                | HeaderName::ListUnsubscribe
547                | HeaderName::Date
548                | HeaderName::ResentDate
549                | HeaderName::MessageId
550                | HeaderName::References
551                | HeaderName::InReplyTo
552                | HeaderName::ReturnPath
553                | HeaderName::ContentId
554                | HeaderName::ResentMessageId
555                | HeaderName::Keywords
556                | HeaderName::ContentLanguage
557                | HeaderName::Received
558                | HeaderName::ContentType
559                | HeaderName::ContentDisposition
560        )
561    }
562}
563
564impl PartialEq for HeaderName<'_> {
565    fn eq(&self, other: &Self) -> bool {
566        match (self, other) {
567            (HeaderName::Other(a), HeaderName::Other(b)) => a.eq_ignore_ascii_case(b),
568            (HeaderName::Other(name), known) | (known, HeaderName::Other(name)) => {
569                name.eq_ignore_ascii_case(known.as_static_str())
570            }
571            _ => self.id() == other.id(),
572        }
573    }
574}
575
576impl Eq for HeaderName<'_> {}
577
578impl Hash for HeaderName<'_> {
579    fn hash<H: Hasher>(&self, state: &mut H) {
580        match (self, self.resolved_id()) {
581            (HeaderName::Other(name), OTHER_ID) => name
582                .bytes()
583                .for_each(|byte| byte.to_ascii_lowercase().hash(state)),
584            (_, id) => id.hash(state),
585        }
586    }
587}
588
589impl PartialOrd for HeaderName<'_> {
590    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
591        Some(self.cmp(other))
592    }
593}
594
595impl Ord for HeaderName<'_> {
596    fn cmp(&self, other: &Self) -> Ordering {
597        match (self.resolved_id(), other.resolved_id()) {
598            (OTHER_ID, OTHER_ID) => self
599                .as_str()
600                .bytes()
601                .map(|byte| byte.to_ascii_lowercase())
602                .cmp(other.as_str().bytes().map(|byte| byte.to_ascii_lowercase())),
603            (a, b) => a.cmp(&b),
604        }
605    }
606}
607
608impl fmt::Display for HeaderName<'_> {
609    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
610        f.write_str(self.as_str())
611    }
612}
613
614impl<'x> From<&'x str> for HeaderName<'x> {
615    fn from(value: &'x str) -> Self {
616        HeaderName::resolve(Cow::Borrowed(value))
617    }
618}
619
620impl<'x> From<Cow<'x, str>> for HeaderName<'x> {
621    fn from(value: Cow<'x, str>) -> Self {
622        HeaderName::resolve(value)
623    }
624}
625
626impl From<String> for HeaderName<'_> {
627    fn from(value: String) -> Self {
628        HeaderName::resolve(Cow::Owned(value))
629    }
630}
631
632impl<'x> From<&HeaderName<'x>> for HeaderName<'x> {
633    fn from(value: &HeaderName<'x>) -> Self {
634        value.clone()
635    }
636}
637
638impl<'x> From<HeaderName<'x>> for Cow<'x, str> {
639    fn from(name: HeaderName<'x>) -> Self {
640        match name {
641            HeaderName::Other(name) => name,
642            known => Cow::Borrowed(known.as_static_str()),
643        }
644    }
645}
646
647impl From<HeaderName<'_>> for String {
648    fn from(name: HeaderName<'_>) -> Self {
649        name.as_str().to_string()
650    }
651}
652
653#[cfg(feature = "serde")]
654impl serde::Serialize for HeaderName<'_> {
655    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
656        serializer.serialize_str(self.as_str())
657    }
658}
659
660#[cfg(feature = "serde")]
661impl<'de> serde::Deserialize<'de> for HeaderName<'static> {
662    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
663        String::deserialize(deserializer).map(HeaderName::from)
664    }
665}
666
667#[cfg(test)]
668mod tests {
669    use super::*;
670
671    const BASELINE_NAMES: &str = include_str!("../resources/header_names.txt");
672
673    #[test]
674    fn baseline_names_round_trip() {
675        for name in BASELINE_NAMES.lines() {
676            let parsed = HeaderName::parse(name).expect("valid name");
677            assert!(!parsed.is_other(), "{name} is not known");
678            assert_eq!(parsed.as_str(), name);
679            assert_eq!(HeaderName::parse(parsed.as_str()), Some(parsed.clone()));
680            assert_eq!(
681                HeaderName::parse(name.to_ascii_uppercase()),
682                Some(parsed.clone())
683            );
684        }
685    }
686
687    #[test]
688    fn vendor_names_follow_the_baseline_names() {
689        assert_eq!(FIRST_VENDOR as usize, BASELINE_NAMES.lines().count());
690        for (id, name) in BASELINE_NAMES.lines().enumerate() {
691            assert_eq!(canonical(id as u16), name);
692        }
693    }
694
695    #[test]
696    fn every_known_name_round_trips() {
697        for (canonical, lowercase) in CANONICAL.iter().zip(LOWERCASE.iter()) {
698            assert_eq!(&canonical.to_ascii_lowercase(), lowercase);
699            let parsed = HeaderName::parse(*canonical).expect("valid");
700            assert_eq!(parsed.as_str(), *canonical);
701            assert_eq!(lookup(lowercase.as_bytes()), Some(parsed.id()));
702        }
703        assert_eq!(KNOWN_NAMES, 252);
704        assert_eq!(MAX_NAME_LEN, 62);
705    }
706
707    #[test]
708    fn content_headers_match_the_content_prefix() {
709        // every known Content-* name, and only those, is a content header
710        for (variant, lowercase) in VARIANTS.iter().zip(LOWERCASE.iter()) {
711            assert_eq!(
712                variant.is_content_header(),
713                lowercase.starts_with("content-"),
714                "{lowercase}"
715            );
716        }
717        assert!(HeaderName::Other("CONTENT-Base".into()).is_content_header());
718        assert!(!HeaderName::Other("Contents".into()).is_content_header());
719        assert!(!HeaderName::MimeVersion.is_content_header());
720    }
721
722    #[test]
723    fn other_names() {
724        let custom = HeaderName::parse("X-Custom-Field").expect("valid");
725        assert_eq!(custom, HeaderName::Other("x-custom-field".into()));
726        assert_eq!(custom.as_str(), "X-Custom-Field");
727        assert!(HeaderName::parse("").is_none());
728        assert!(HeaderName::parse("mal formed").is_none());
729        assert_eq!(HeaderName::parse("X-Mailer"), Some(HeaderName::XMailer));
730        assert_eq!(
731            HeaderName::parse("x-face").map(|n| n.is_other()),
732            Some(true)
733        );
734        assert_eq!(
735            HeaderName::parse("Original-Encoded-Information-Types-Extra").map(|n| n.is_other()),
736            Some(true)
737        );
738        assert_eq!(lookup(b"SUBJECT"), Some(HeaderName::Subject.id()));
739        for name in [
740            &b" subject"[..],
741            b"Sub ject",
742            b"MIME-version ",
743            b"From\r",
744            b"F\x0crom",
745            b":From",
746            b"",
747        ] {
748            assert_eq!(lookup(name), None, "{name:?}");
749        }
750    }
751
752    #[test]
753    fn every_field_name_character_is_accepted() {
754        for name in ["X.Spam", "A@[]{}~", "X!#$%&'*+/=?^`|", "X;Y"] {
755            let parsed = HeaderName::parse(name).expect("valid field name");
756            assert_eq!(parsed, HeaderName::Other(name.into()));
757            assert_eq!(parsed.as_str(), name);
758        }
759        for name in ["", "X:Y", "X Y", "caf\u{e9}", "X\u{7f}", "X\tY"] {
760            assert_eq!(HeaderName::parse(name), None, "{name:?}");
761        }
762        for name in ["X.Spam", "mal formed", ":From", "caf\u{e9}", ""] {
763            assert_eq!(HeaderName::from(name).as_str(), name);
764            assert_eq!(HeaderName::from(name.to_string()).as_str(), name);
765            assert_eq!(HeaderName::from(Cow::Borrowed(name)).as_str(), name);
766        }
767        assert_eq!(HeaderName::from("x-mailer"), HeaderName::XMailer);
768        assert!(HeaderName::from("Sub ject").is_other());
769    }
770
771    #[test]
772    fn other_spelling_a_known_name_is_that_name() {
773        use std::collections::{BTreeSet, HashSet};
774
775        let pairs = [
776            (HeaderName::Other("Subject".into()), HeaderName::Subject),
777            (HeaderName::Other("x-MAILER".into()), HeaderName::XMailer),
778            (
779                HeaderName::Other("content-type".into()),
780                HeaderName::ContentType,
781            ),
782        ];
783        for (other, known) in &pairs {
784            assert_eq!(other, known);
785            assert_eq!(known, other);
786            assert_eq!(other.cmp(known), Ordering::Equal);
787            assert_eq!(other.resolved_id(), known.id());
788            let hashed: HashSet<HeaderName<'_>> = [other.clone()].into_iter().collect();
789            assert!(hashed.contains(known));
790            let ordered: BTreeSet<HeaderName<'_>> = [known.clone()].into_iter().collect();
791            assert!(ordered.contains(other));
792        }
793        let custom = HeaderName::Other("X-Custom".into());
794        assert_ne!(custom, HeaderName::Subject);
795        assert_ne!(HeaderName::Other("Sub ject".into()), HeaderName::Subject);
796        assert_eq!(custom.cmp(&HeaderName::Subject), Ordering::Greater);
797        assert_eq!(custom.resolved_id(), OTHER_ID);
798        let mut names = [
799            HeaderName::Other("x-b".into()),
800            HeaderName::Other("From".into()),
801            HeaderName::Subject,
802            HeaderName::Other("X-A".into()),
803        ];
804        names.sort();
805        assert_eq!(
806            names.iter().map(HeaderName::as_str).collect::<Vec<_>>(),
807            ["Subject", "From", "X-A", "x-b"]
808        );
809    }
810}
811
812/// A header name resolved once by [`HeaderName::key`], for repeated lookups
813/// with [`crate::Headers::all_key`] and [`crate::Headers::get_key`].
814#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
815pub struct HeaderKey<'x> {
816    id: u16,
817    name: &'x str,
818}
819
820impl<'x> HeaderKey<'x> {
821    /// The spelling of the name the key was made from.
822    pub fn as_str(&self) -> &'x str {
823        self.name
824    }
825
826    pub(crate) fn id(&self) -> u16 {
827        self.id
828    }
829}