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}