1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
use std::collections::HashMap;
use std::fmt::Debug;
use std::io as std_io;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};

use hostname::get_hostname;
use native_tls::{self, TlsConnector as NativeTlsConnector, TlsConnectorBuilder};

use crate::{
    ascii::IgnoreAsciiCaseStr,
    data_types::{AddressLiteral, Capability, Domain, EhloParam},
};

/// Represents the identity of an client
///
/// If you connect to an MSA this can be as simple as
/// localhost, through for smtp communication between
/// servers or for connecting with an MX server this
/// should be a public facing domain or ip address
///
/// ---
///
/// MSA: Mail Submission Agent
///
/// MX: Mail Exchanger
///
#[derive(Debug, Clone)]
pub enum ClientId {
    /// a registered domain
    Domain(Domain),
    /// a ipv4/ipv6 address, through theoretically others protocols are
    /// possible too
    AddressLiteral(AddressLiteral),
}

impl ClientId {
    /// creates a client identity for "localhost" (here fixed to 127.0.0.1)
    ///
    /// This can be used as client identity when connecting a mail client to
    /// a Mail Submission Agent (MSA), but should not be used when connecting
    /// to an Mail Exchanger (MX).
    pub fn localhost() -> Self {
        //TODO use "domain" localhost??
        Self::from(Ipv4Addr::new(127, 0, 0, 1))
    }

    /// creates a client identity using hostname (fallback localhost)
    ///
    /// This uses the `hostname` crate to create a client identity.
    /// If this fails `ClientId::localhost()` is used.
    ///
    pub fn hostname() -> Self {
        Self::try_hostname().unwrap_or_else(Self::localhost)
    }

    /// creates a client identity if a hostname can be found
    ///
    /// # Implementation Note
    ///
    /// As the `hostname` crate currently only returns an `Option`
    /// we also do so.
    pub fn try_hostname() -> Option<Self> {
        get_hostname().map(|name| {
            //SEMANTIC_SAFE: the systems hostname should be a valid domain (syntactically)
            let domain = Domain::new_unchecked(name);
            ClientId::Domain(domain)
        })
    }
}

impl From<Domain> for ClientId {
    fn from(dm: Domain) -> Self {
        ClientId::Domain(dm)
    }
}

impl From<AddressLiteral> for ClientId {
    fn from(adl: AddressLiteral) -> Self {
        ClientId::AddressLiteral(adl)
    }
}

impl From<IpAddr> for ClientId {
    fn from(saddr: IpAddr) -> Self {
        let adl = AddressLiteral::from(saddr);
        ClientId::from(adl)
    }
}

impl From<Ipv4Addr> for ClientId {
    fn from(saddr: Ipv4Addr) -> Self {
        let adl = AddressLiteral::from(saddr);
        ClientId::from(adl)
    }
}

impl From<Ipv6Addr> for ClientId {
    fn from(saddr: Ipv6Addr) -> Self {
        let adl = AddressLiteral::from(saddr);
        ClientId::from(adl)
    }
}

/// A Tls configuration
///
/// This consists of a domain, which is the domain of the
/// server we connect to and a `SetupTls` instance,
/// which can be used to modify the tls setup e.g. to
/// use a client certificate for authentication.
///
/// The `SetupTls` default to `DefaultTlsSetup` which
/// is enough for most use cases.
#[derive(Debug, Clone, PartialEq)]
pub struct TlsConfig<S = DefaultTlsSetup>
where
    S: SetupTls,
{
    /// domain of the server we connect to
    pub domain: Domain,
    /// setup allowing modifying TLS setup process
    pub setup: S,
}

impl From<Domain> for TlsConfig {
    fn from(domain: Domain) -> Self {
        TlsConfig {
            domain,
            setup: DefaultTlsSetup,
        }
    }
}

/// Trait used when setting up tls to modify the setup process
pub trait SetupTls: Debug + Send + 'static {
    /// Accepts a connection builder and returns a connector if possible
    fn setup(self, builder: TlsConnectorBuilder) -> Result<NativeTlsConnector, native_tls::Error>;
}

/// The default tls setup, which just calls `builder.build()`
#[derive(Debug, Clone, PartialEq)]
pub struct DefaultTlsSetup;

impl SetupTls for DefaultTlsSetup {
    fn setup(self, builder: TlsConnectorBuilder) -> Result<NativeTlsConnector, native_tls::Error> {
        builder.build()
    }
}

impl<F: 'static> SetupTls for F
where
    F: Send + Debug + FnOnce(TlsConnectorBuilder) -> Result<NativeTlsConnector, native_tls::Error>,
{
    fn setup(self, builder: TlsConnectorBuilder) -> Result<NativeTlsConnector, native_tls::Error> {
        (self)(builder)
    }
}

//FIXME[rust/catch]: use catch once in stable
macro_rules! alttry {
    ($block:block => $emap:expr) => {{
        let func = move || -> Result<_, _> { $block };
        match func() {
            Ok(ok) => ok,
            Err(err) => {
                #[allow(clippy::redundant_closure_call)]
                return ($emap)(err);
            }
        }
    }};
}

pub(crate) fn map_tls_err(err: native_tls::Error) -> std_io::Error {
    std_io::Error::new(std_io::ErrorKind::Other, err)
}

/// A type representing the ehlo response of the last ehlo call
///
/// This is mainly used to check if a certain capability/command
/// is supported. E.g. if SMTPUTF8 is supported.
#[derive(Debug, Clone)]
pub struct EhloData {
    domain: Domain,
    data: HashMap<Capability, Vec<EhloParam>>,
}

impl EhloData {
    /// create a new Ehlo data from the domain with which the server responded and the
    /// ehlo parameters of the response
    pub fn new(domain: Domain, data: HashMap<Capability, Vec<EhloParam>>) -> Self {
        EhloData { domain, data }
    }

    /// check if a ehlo contained a specific capability e.g. `SMTPUTF8`
    pub fn has_capability<A>(&self, cap: A) -> bool
    where
        A: AsRef<str>,
    {
        self.data
            .contains_key(<&IgnoreAsciiCaseStr>::from(cap.as_ref()))
    }

    /// get the parameters for a specific capability e.g. the size of `SIZE`
    pub fn get_capability_params<A>(&self, cap: A) -> Option<&[EhloParam]>
    where
        A: AsRef<str>,
    {
        self.data
            .get(<&IgnoreAsciiCaseStr>::from(cap.as_ref()))
            .map(|vec| &**vec)
    }

    /// return a reference to the inner hash map
    pub fn capability_map(&self) -> &HashMap<Capability, Vec<EhloParam>> {
        &self.data
    }

    /// the domain for which the server acts
    pub fn domain(&self) -> &Domain {
        &self.domain
    }
}

impl From<(Domain, HashMap<Capability, Vec<EhloParam>>)> for EhloData {
    fn from((domain, map): (Domain, HashMap<Capability, Vec<EhloParam>>)) -> Self {
        EhloData::new(domain, map)
    }
}

impl Into<(Domain, HashMap<Capability, Vec<EhloParam>>)> for EhloData {
    fn into(self) -> (Domain, HashMap<Capability, Vec<EhloParam>>) {
        let EhloData { domain, data } = self;
        (domain, data)
    }
}