Skip to main content

new_tokio_smtp/
common.rs

1use std::collections::HashMap;
2use std::fmt::Debug;
3use std::io as std_io;
4use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
5
6use hostname::get_hostname;
7use native_tls::{self, TlsConnector as NativeTlsConnector, TlsConnectorBuilder};
8
9use crate::{
10    ascii::IgnoreAsciiCaseStr,
11    data_types::{AddressLiteral, Capability, Domain, EhloParam},
12};
13
14/// Represents the identity of an client
15///
16/// If you connect to an MSA this can be as simple as
17/// localhost, through for smtp communication between
18/// servers or for connecting with an MX server this
19/// should be a public facing domain or ip address
20///
21/// ---
22///
23/// MSA: Mail Submission Agent
24///
25/// MX: Mail Exchanger
26///
27#[derive(Debug, Clone)]
28pub enum ClientId {
29    /// a registered domain
30    Domain(Domain),
31    /// a ipv4/ipv6 address, through theoretically others protocols are
32    /// possible too
33    AddressLiteral(AddressLiteral),
34}
35
36impl ClientId {
37    /// creates a client identity for "localhost" (here fixed to 127.0.0.1)
38    ///
39    /// This can be used as client identity when connecting a mail client to
40    /// a Mail Submission Agent (MSA), but should not be used when connecting
41    /// to an Mail Exchanger (MX).
42    pub fn localhost() -> Self {
43        //TODO use "domain" localhost??
44        Self::from(Ipv4Addr::new(127, 0, 0, 1))
45    }
46
47    /// creates a client identity using hostname (fallback localhost)
48    ///
49    /// This uses the `hostname` crate to create a client identity.
50    /// If this fails `ClientId::localhost()` is used.
51    ///
52    pub fn hostname() -> Self {
53        Self::try_hostname().unwrap_or_else(Self::localhost)
54    }
55
56    /// creates a client identity if a hostname can be found
57    ///
58    /// # Implementation Note
59    ///
60    /// As the `hostname` crate currently only returns an `Option`
61    /// we also do so.
62    pub fn try_hostname() -> Option<Self> {
63        get_hostname().map(|name| {
64            //SEMANTIC_SAFE: the systems hostname should be a valid domain (syntactically)
65            let domain = Domain::new_unchecked(name);
66            ClientId::Domain(domain)
67        })
68    }
69}
70
71impl From<Domain> for ClientId {
72    fn from(dm: Domain) -> Self {
73        ClientId::Domain(dm)
74    }
75}
76
77impl From<AddressLiteral> for ClientId {
78    fn from(adl: AddressLiteral) -> Self {
79        ClientId::AddressLiteral(adl)
80    }
81}
82
83impl From<IpAddr> for ClientId {
84    fn from(saddr: IpAddr) -> Self {
85        let adl = AddressLiteral::from(saddr);
86        ClientId::from(adl)
87    }
88}
89
90impl From<Ipv4Addr> for ClientId {
91    fn from(saddr: Ipv4Addr) -> Self {
92        let adl = AddressLiteral::from(saddr);
93        ClientId::from(adl)
94    }
95}
96
97impl From<Ipv6Addr> for ClientId {
98    fn from(saddr: Ipv6Addr) -> Self {
99        let adl = AddressLiteral::from(saddr);
100        ClientId::from(adl)
101    }
102}
103
104/// A Tls configuration
105///
106/// This consists of a domain, which is the domain of the
107/// server we connect to and a `SetupTls` instance,
108/// which can be used to modify the tls setup e.g. to
109/// use a client certificate for authentication.
110///
111/// The `SetupTls` default to `DefaultTlsSetup` which
112/// is enough for most use cases.
113#[derive(Debug, Clone, PartialEq)]
114pub struct TlsConfig<S = DefaultTlsSetup>
115where
116    S: SetupTls,
117{
118    /// domain of the server we connect to
119    pub domain: Domain,
120    /// setup allowing modifying TLS setup process
121    pub setup: S,
122}
123
124impl From<Domain> for TlsConfig {
125    fn from(domain: Domain) -> Self {
126        TlsConfig {
127            domain,
128            setup: DefaultTlsSetup,
129        }
130    }
131}
132
133/// Trait used when setting up tls to modify the setup process
134pub trait SetupTls: Debug + Send + 'static {
135    /// Accepts a connection builder and returns a connector if possible
136    fn setup(self, builder: TlsConnectorBuilder) -> Result<NativeTlsConnector, native_tls::Error>;
137}
138
139/// The default tls setup, which just calls `builder.build()`
140#[derive(Debug, Clone, PartialEq)]
141pub struct DefaultTlsSetup;
142
143impl SetupTls for DefaultTlsSetup {
144    fn setup(self, builder: TlsConnectorBuilder) -> Result<NativeTlsConnector, native_tls::Error> {
145        builder.build()
146    }
147}
148
149impl<F: 'static> SetupTls for F
150where
151    F: Send + Debug + FnOnce(TlsConnectorBuilder) -> Result<NativeTlsConnector, native_tls::Error>,
152{
153    fn setup(self, builder: TlsConnectorBuilder) -> Result<NativeTlsConnector, native_tls::Error> {
154        (self)(builder)
155    }
156}
157
158//FIXME[rust/catch]: use catch once in stable
159macro_rules! alttry {
160    ($block:block => $emap:expr) => {{
161        let func = move || -> Result<_, _> { $block };
162        match func() {
163            Ok(ok) => ok,
164            Err(err) => {
165                #[allow(clippy::redundant_closure_call)]
166                return ($emap)(err);
167            }
168        }
169    }};
170}
171
172pub(crate) fn map_tls_err(err: native_tls::Error) -> std_io::Error {
173    std_io::Error::new(std_io::ErrorKind::Other, err)
174}
175
176/// A type representing the ehlo response of the last ehlo call
177///
178/// This is mainly used to check if a certain capability/command
179/// is supported. E.g. if SMTPUTF8 is supported.
180#[derive(Debug, Clone)]
181pub struct EhloData {
182    domain: Domain,
183    data: HashMap<Capability, Vec<EhloParam>>,
184}
185
186impl EhloData {
187    /// create a new Ehlo data from the domain with which the server responded and the
188    /// ehlo parameters of the response
189    pub fn new(domain: Domain, data: HashMap<Capability, Vec<EhloParam>>) -> Self {
190        EhloData { domain, data }
191    }
192
193    /// check if a ehlo contained a specific capability e.g. `SMTPUTF8`
194    pub fn has_capability<A>(&self, cap: A) -> bool
195    where
196        A: AsRef<str>,
197    {
198        self.data
199            .contains_key(<&IgnoreAsciiCaseStr>::from(cap.as_ref()))
200    }
201
202    /// get the parameters for a specific capability e.g. the size of `SIZE`
203    pub fn get_capability_params<A>(&self, cap: A) -> Option<&[EhloParam]>
204    where
205        A: AsRef<str>,
206    {
207        self.data
208            .get(<&IgnoreAsciiCaseStr>::from(cap.as_ref()))
209            .map(|vec| &**vec)
210    }
211
212    /// return a reference to the inner hash map
213    pub fn capability_map(&self) -> &HashMap<Capability, Vec<EhloParam>> {
214        &self.data
215    }
216
217    /// the domain for which the server acts
218    pub fn domain(&self) -> &Domain {
219        &self.domain
220    }
221}
222
223impl From<(Domain, HashMap<Capability, Vec<EhloParam>>)> for EhloData {
224    fn from((domain, map): (Domain, HashMap<Capability, Vec<EhloParam>>)) -> Self {
225        EhloData::new(domain, map)
226    }
227}
228
229impl Into<(Domain, HashMap<Capability, Vec<EhloParam>>)> for EhloData {
230    fn into(self) -> (Domain, HashMap<Capability, Vec<EhloParam>>) {
231        let EhloData { domain, data } = self;
232        (domain, data)
233    }
234}