Skip to main content

mail_auth/dkim/
builder.rs

1/*
2 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
3 *
4 * SPDX-License-Identifier: Apache-2.0 OR MIT
5 */
6
7//! Type state builder for [`DkimSigner`]: key, then domain, selector and
8//! signed headers, then optional settings.
9
10use super::{Canonicalization, DkimSigner, Signature};
11use crate::{
12    crypto::{HashAlgorithm, SigningKey},
13    signer::{NeedDomain, NeedHeaders, NeedSelector, Ready},
14};
15use std::time::Duration;
16
17impl<T: SigningKey> DkimSigner<T> {
18    /// Starts a signer for `key`.
19    ///
20    /// The `a=` algorithm is taken from the key. Continue with
21    /// [`domain`](DkimSigner::domain), [`selector`](DkimSigner::selector) and
22    /// [`headers`](DkimSigner::headers) to obtain a signer in the
23    /// [`Ready`] state.
24    pub fn from_key(key: T) -> DkimSigner<T, NeedDomain> {
25        DkimSigner {
26            _state: Default::default(),
27            template: Signature {
28                v: 1,
29                a: key.algorithm(),
30                ..Default::default()
31            },
32            key,
33        }
34    }
35}
36
37impl<T: SigningKey> DkimSigner<T, NeedDomain> {
38    /// Sets the signing domain (`d=` tag).
39    ///
40    /// The public key must be published under this domain, at
41    /// `<selector>._domainkey.<domain>`.
42    pub fn domain(mut self, domain: impl Into<String>) -> DkimSigner<T, NeedSelector> {
43        self.template.d = domain.into();
44        DkimSigner {
45            _state: Default::default(),
46            key: self.key,
47            template: self.template,
48        }
49    }
50}
51
52impl<T: SigningKey> DkimSigner<T, NeedSelector> {
53    /// Sets the selector (`s=` tag) naming the key record under the signing
54    /// domain.
55    pub fn selector(mut self, selector: impl Into<String>) -> DkimSigner<T, NeedHeaders> {
56        self.template.s = selector.into();
57        DkimSigner {
58            _state: Default::default(),
59            key: self.key,
60            template: self.template,
61        }
62    }
63}
64
65impl<T: SigningKey> DkimSigner<T, NeedHeaders> {
66    /// Sets the names of the header fields to sign (`h=` tag) and returns a
67    /// signer ready to sign.
68    ///
69    /// Names are matched case insensitively. Every occurrence of a listed
70    /// header present in the message is signed, and `h=` lists them bottom
71    /// up. Listed names absent from the message are still appended to `h=`,
72    /// so adding such a header later breaks the signature (RFC 6376,
73    /// Section 5.4). Include at least `From` (RFC 6376, Section 5.4.1).
74    pub fn headers(
75        mut self,
76        headers: impl IntoIterator<Item = impl Into<String>>,
77    ) -> DkimSigner<T, Ready> {
78        self.template.h = headers.into_iter().map(|h| h.into()).collect();
79        DkimSigner {
80            _state: Default::default(),
81            key: self.key,
82            template: self.template,
83        }
84    }
85}
86
87impl<T: SigningKey> DkimSigner<T, Ready> {
88    /// Sets the RFC 6541 `atps=` tag: the author domain on whose behalf this
89    /// third-party signature is made.
90    ///
91    /// Without [`atps_hash`](DkimSigner::atps_hash), `atpsh=none` is written.
92    pub fn atps(mut self, atps: impl Into<String>) -> Self {
93        self.template.atps = Some(atps.into());
94        self
95    }
96
97    /// Sets the RFC 6541 `atpsh=` tag: the hash applied to `d=` when the
98    /// verifier builds the ATPS query name. Only written together with
99    /// [`atps`](DkimSigner::atps).
100    pub fn atps_hash(mut self, atps_hash: HashAlgorithm) -> Self {
101        self.template.atpsh = atps_hash.into();
102        self
103    }
104
105    /// Sets the agent or user identifier (`i=` tag).
106    ///
107    /// Its domain must be the signing domain or a subdomain of it, otherwise
108    /// verification fails with [`DkimError::AuidMismatch`](super::DkimError::AuidMismatch).
109    pub fn identity(mut self, identity: impl Into<String>) -> Self {
110        self.template.i = identity.into();
111        self
112    }
113
114    /// Sets the signature validity period, counted from the signing time.
115    ///
116    /// The produced signature carries `x=` equal to `t=` plus `expiration`,
117    /// rounded up to whole seconds and capped at `u64::MAX`, so
118    /// `Duration::MAX` means no practical expiry. A zero duration, the
119    /// default, omits the `x=` tag.
120    pub fn expiration(mut self, expiration: Duration) -> Self {
121        self.template.x = expiration
122            .as_secs()
123            .saturating_add(u64::from(expiration.subsec_nanos() > 0));
124        self
125    }
126
127    /// Includes the body length (`l=` tag) in the signature when `true`.
128    ///
129    /// Off by default. Signing the length lets anyone append content
130    /// after the signed body without breaking the signature (RFC 6376,
131    /// Section 8.2), and strict verifiers ignore such signatures.
132    pub fn body_length(mut self, body_length: bool) -> Self {
133        self.template.l = u64::from(body_length);
134        self
135    }
136
137    /// Requests RFC 6651 failure reports (`r=y` tag) when `true`.
138    ///
139    /// Off by default. Verifiers send reports to the address published in
140    /// the `_report._domainkey.<domain>` record.
141    pub fn reporting(mut self, reporting: bool) -> Self {
142        self.template.r = reporting;
143        self
144    }
145
146    /// Sets the header canonicalization (first half of the `c=` tag).
147    /// Defaults to [`Canonicalization::Relaxed`].
148    pub fn header_canonicalization(mut self, ch: Canonicalization) -> Self {
149        self.template.ch = ch;
150        self
151    }
152
153    /// Sets the body canonicalization (second half of the `c=` tag).
154    /// Defaults to [`Canonicalization::Relaxed`].
155    pub fn body_canonicalization(mut self, cb: Canonicalization) -> Self {
156        self.template.cb = cb;
157        self
158    }
159}