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
//!
//! A simple implementation of Mozilla Thunderbird's autoconfig (https://wiki.mozilla.org/Thunderbird:Autoconfiguration) in Rust.
//!
//! Useful if a user needs to fill in their mail server configuration, but are not tech savy enough to do so or just for general convenience of not having to manually fill anything in.
//!
//! # Usage
//!
//! You can request a config by simply calling the `from_addr` function:
//!
//! ```rust,ignore
//!
//! extern crate autoconfig;
//!
//! #[tokio::main]
//! async fn main() {
//!     let config = autoconfig::from_addr("test@gmail.com").await.unwrap();
//!
//!     println!("{}", config.email_provider().id())
//!     
//!     // Outputs:
//!     // "googlemail.com"
//! }
//!
//! ```
//!
//! You can also achieve the same thing but from just a domain name:
//!
//! ```rust,ignore
//!
//! extern crate autoconfig;
//!
//! #[tokio::main]
//! async fn main() {
//!     let config = autoconfig::from_domain("gmail.com").await.unwrap();
//!
//!     println!("{}", config.email_provider().id())
//!     
//!     // Outputs:
//!     // "googlemail.com"
//! }
//!
//! ```
//!

use client::Client;
use futures::{future::select_ok, FutureExt};
use utils::validate_email;

mod client;
pub mod config;
mod dns;
pub mod error;
mod http;
mod parse;
mod utils;

const AT_SYMBOL: char = '@';

use config::Config;
use error::{Error, ErrorKind, Result};

/// Given an email providers domain, try to connect to autoconfig servers for that provider and return the config.
pub async fn from_domain<D: AsRef<str>>(domain: D) -> Result<Config> {
    let mut errors: Vec<_> = Vec::new();

    let client = Client::new().await?;

    let mut futures = Vec::new();

    let mut urls = vec![
        // Try connect to connect with the users mail server directly
        format!("http://autoconfig.{}/mail/config-v1.1.xml", domain.as_ref()),
        // The fallback url
        format!(
            "http://{}/.well-known/autoconfig/mail/config-v1.1.xml",
            domain.as_ref()
        ),
        // If the previous two methods did not work then the email server provider has not setup Thunderbird autoconfig, so we ask Mozilla for their config.
        format!(
            "https://autoconfig.thunderbird.net/v1.1/{}",
            domain.as_ref()
        ),
    ];

    match client.get_url_from_txt(domain.as_ref()).await {
        Ok(txt_urls) => {
            for url in txt_urls {
                urls.push(url)
            }
        }
        Err(error) => errors.push(error),
    };

    urls.sort();
    urls.dedup();

    for url in urls {
        let future = client.get_config(url);

        futures.push(future.boxed());
    }

    let result = select_ok(futures).await;

    match result {
        Ok((config, _remaining)) => return Ok(config),
        Err(error) => errors.push(error),
    }

    Err(Error::new(
        ErrorKind::NotFound(errors),
        "Could not find a valid config",
    ))
}

/// Given an email address, try to connect to the email providers autoconfig servers and return the config that was found, if one was found.
pub async fn from_addr(email_address: &str) -> Result<Config> {
    if !validate_email(email_address) {
        return Err(Error::new(
            ErrorKind::BadInput,
            "Given email address is invalid",
        ));
    };

    let mut split = email_address.split(AT_SYMBOL);

    // Skip the prefix
    split.next();

    let domain = match split.next() {
        Some(domain) => domain,
        None => {
            return Err(Error::new(
                ErrorKind::BadInput,
                "An email address must specify a domain after the '@' symbol",
            ))
        }
    };

    from_domain(domain).await
}

#[cfg(test)]
mod test;