deltachat/qr/
dclogin_scheme.rs

1use std::collections::HashMap;
2
3use anyhow::{Context as _, Result, bail};
4
5use deltachat_contact_tools::may_be_valid_addr;
6use num_traits::cast::ToPrimitive;
7
8use super::{DCLOGIN_SCHEME, Qr};
9use crate::config::Config;
10use crate::context::Context;
11use crate::login_param::EnteredCertificateChecks;
12use crate::provider::Socket;
13
14/// Options for `dclogin:` scheme.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum LoginOptions {
17    /// Unsupported version.
18    UnsuportedVersion(u32),
19
20    /// Version 1.
21    V1 {
22        /// IMAP server password.
23        ///
24        /// Used for SMTP if separate SMTP password is not provided.
25        mail_pw: String,
26
27        /// IMAP host.
28        imap_host: Option<String>,
29
30        /// IMAP port.
31        imap_port: Option<u16>,
32
33        /// IMAP username.
34        imap_username: Option<String>,
35
36        /// IMAP password.
37        imap_password: Option<String>,
38
39        /// IMAP socket security.
40        imap_security: Option<Socket>,
41
42        /// SMTP host.
43        smtp_host: Option<String>,
44
45        /// SMTP port.
46        smtp_port: Option<u16>,
47
48        /// SMTP username.
49        smtp_username: Option<String>,
50
51        /// SMTP password.
52        smtp_password: Option<String>,
53
54        /// SMTP socket security.
55        smtp_security: Option<Socket>,
56
57        /// Certificate checks.
58        certificate_checks: Option<EnteredCertificateChecks>,
59    },
60}
61
62/// scheme: `dclogin://user@host/?p=password&v=1[&options]`
63/// read more about the scheme at <https://github.com/deltachat/interface/blob/master/uri-schemes.md#DCLOGIN>
64pub(super) fn decode_login(qr: &str) -> Result<Qr> {
65    let url = url::Url::parse(qr).with_context(|| format!("Malformed url: {qr:?}"))?;
66
67    let url_without_scheme = qr
68        .get(DCLOGIN_SCHEME.len()..)
69        .context("invalid DCLOGIN payload E1")?;
70    let payload = url_without_scheme
71        .strip_prefix("//")
72        .unwrap_or(url_without_scheme);
73
74    let addr = payload
75        .split(['?', '/'])
76        .next()
77        .context("invalid DCLOGIN payload E3")?;
78
79    if url.scheme().eq_ignore_ascii_case("dclogin") {
80        let options = url.query_pairs();
81        if options.count() == 0 {
82            bail!("invalid DCLOGIN payload E4")
83        }
84        // load options into hashmap
85        let parameter_map: HashMap<String, String> = options
86            .map(|(key, value)| (key.into_owned(), value.into_owned()))
87            .collect();
88
89        // check if username is there
90        if !may_be_valid_addr(addr) {
91            bail!("invalid DCLOGIN payload: invalid username E5");
92        }
93
94        // apply to result struct
95        let options: LoginOptions = match parameter_map.get("v").map(|i| i.parse::<u32>()) {
96            Some(Ok(1)) => LoginOptions::V1 {
97                mail_pw: parameter_map
98                    .get("p")
99                    .map(|s| s.to_owned())
100                    .context("password missing")?,
101                imap_host: parameter_map.get("ih").map(|s| s.to_owned()),
102                imap_port: parse_port(parameter_map.get("ip"))
103                    .context("could not parse imap port")?,
104                imap_username: parameter_map.get("iu").map(|s| s.to_owned()),
105                imap_password: parameter_map.get("ipw").map(|s| s.to_owned()),
106                imap_security: parse_socket_security(parameter_map.get("is"))?,
107                smtp_host: parameter_map.get("sh").map(|s| s.to_owned()),
108                smtp_port: parse_port(parameter_map.get("sp"))
109                    .context("could not parse smtp port")?,
110                smtp_username: parameter_map.get("su").map(|s| s.to_owned()),
111                smtp_password: parameter_map.get("spw").map(|s| s.to_owned()),
112                smtp_security: parse_socket_security(parameter_map.get("ss"))?,
113                certificate_checks: parse_certificate_checks(parameter_map.get("ic"))?,
114            },
115            Some(Ok(v)) => LoginOptions::UnsuportedVersion(v),
116            Some(Err(_)) => bail!("version could not be parsed as number E6"),
117            None => bail!("invalid DCLOGIN payload: version missing E7"),
118        };
119
120        Ok(Qr::Login {
121            address: addr.to_owned(),
122            options,
123        })
124    } else {
125        bail!("Bad scheme for account URL: {:?}.", payload);
126    }
127}
128
129fn parse_port(port: Option<&String>) -> core::result::Result<Option<u16>, std::num::ParseIntError> {
130    match port {
131        Some(p) => Ok(Some(p.parse::<u16>()?)),
132        None => Ok(None),
133    }
134}
135
136fn parse_socket_security(security: Option<&String>) -> Result<Option<Socket>> {
137    Ok(match security.map(|s| s.as_str()) {
138        Some("ssl") => Some(Socket::Ssl),
139        Some("starttls") => Some(Socket::Starttls),
140        Some("default") => Some(Socket::Automatic),
141        Some("plain") => Some(Socket::Plain),
142        Some(other) => bail!("Unknown security level: {}", other),
143        None => None,
144    })
145}
146
147fn parse_certificate_checks(
148    certificate_checks: Option<&String>,
149) -> Result<Option<EnteredCertificateChecks>> {
150    Ok(match certificate_checks.map(|s| s.as_str()) {
151        Some("0") => Some(EnteredCertificateChecks::Automatic),
152        Some("1") => Some(EnteredCertificateChecks::Strict),
153        Some("2") => Some(EnteredCertificateChecks::AcceptInvalidCertificates),
154        Some("3") => Some(EnteredCertificateChecks::AcceptInvalidCertificates2),
155        Some(other) => bail!("Unknown certificatecheck level: {}", other),
156        None => None,
157    })
158}
159
160pub(crate) async fn configure_from_login_qr(
161    context: &Context,
162    address: &str,
163    options: LoginOptions,
164) -> Result<()> {
165    context
166        .set_config_internal(Config::Addr, Some(address))
167        .await?;
168
169    match options {
170        LoginOptions::V1 {
171            mail_pw,
172            imap_host,
173            imap_port,
174            imap_username,
175            imap_password,
176            imap_security,
177            smtp_host,
178            smtp_port,
179            smtp_username,
180            smtp_password,
181            smtp_security,
182            certificate_checks,
183        } => {
184            context
185                .set_config_internal(Config::MailPw, Some(&mail_pw))
186                .await?;
187            if let Some(value) = imap_host {
188                context
189                    .set_config_internal(Config::MailServer, Some(&value))
190                    .await?;
191            }
192            if let Some(value) = imap_port {
193                context
194                    .set_config_internal(Config::MailPort, Some(&value.to_string()))
195                    .await?;
196            }
197            if let Some(value) = imap_username {
198                context
199                    .set_config_internal(Config::MailUser, Some(&value))
200                    .await?;
201            }
202            if let Some(value) = imap_password {
203                context
204                    .set_config_internal(Config::MailPw, Some(&value))
205                    .await?;
206            }
207            if let Some(value) = imap_security {
208                let code = value
209                    .to_u8()
210                    .context("could not convert imap security value to number")?;
211                context
212                    .set_config_internal(Config::MailSecurity, Some(&code.to_string()))
213                    .await?;
214            }
215            if let Some(value) = smtp_host {
216                context
217                    .set_config_internal(Config::SendServer, Some(&value))
218                    .await?;
219            }
220            if let Some(value) = smtp_port {
221                context
222                    .set_config_internal(Config::SendPort, Some(&value.to_string()))
223                    .await?;
224            }
225            if let Some(value) = smtp_username {
226                context
227                    .set_config_internal(Config::SendUser, Some(&value))
228                    .await?;
229            }
230            if let Some(value) = smtp_password {
231                context
232                    .set_config_internal(Config::SendPw, Some(&value))
233                    .await?;
234            }
235            if let Some(value) = smtp_security {
236                let code = value
237                    .to_u8()
238                    .context("could not convert smtp security value to number")?;
239                context
240                    .set_config_internal(Config::SendSecurity, Some(&code.to_string()))
241                    .await?;
242            }
243            if let Some(value) = certificate_checks {
244                let code = value
245                    .to_u32()
246                    .context("could not convert certificate checks value to number")?;
247                context
248                    .set_config_internal(Config::ImapCertificateChecks, Some(&code.to_string()))
249                    .await?;
250                context
251                    .set_config_internal(Config::SmtpCertificateChecks, Some(&code.to_string()))
252                    .await?;
253            }
254            Ok(())
255        }
256        _ => bail!(
257            "DeltaChat does not understand this QR Code yet, please update the app and try again."
258        ),
259    }
260}
261
262#[cfg(test)]
263mod test {
264    use anyhow::bail;
265
266    use super::{LoginOptions, decode_login};
267    use crate::{login_param::EnteredCertificateChecks, provider::Socket, qr::Qr};
268
269    macro_rules! login_options_just_pw {
270        ($pw: expr) => {
271            LoginOptions::V1 {
272                mail_pw: $pw,
273                imap_host: None,
274                imap_port: None,
275                imap_username: None,
276                imap_password: None,
277                imap_security: None,
278                smtp_host: None,
279                smtp_port: None,
280                smtp_username: None,
281                smtp_password: None,
282                smtp_security: None,
283                certificate_checks: None,
284            }
285        };
286    }
287
288    #[test]
289    fn minimal_no_options() -> anyhow::Result<()> {
290        let result = decode_login("dclogin://email@host.tld?p=123&v=1")?;
291        if let Qr::Login { address, options } = result {
292            assert_eq!(address, "email@host.tld".to_owned());
293            assert_eq!(options, login_options_just_pw!("123".to_owned()));
294        } else {
295            bail!("wrong type")
296        }
297        let result = decode_login("dclogin://email@host.tld/?p=123456&v=1")?;
298        if let Qr::Login { address, options } = result {
299            assert_eq!(address, "email@host.tld".to_owned());
300            assert_eq!(options, login_options_just_pw!("123456".to_owned()));
301        } else {
302            bail!("wrong type")
303        }
304        let result = decode_login("dclogin://email@host.tld/ignored/path?p=123456&v=1")?;
305        if let Qr::Login { address, options } = result {
306            assert_eq!(address, "email@host.tld".to_owned());
307            assert_eq!(options, login_options_just_pw!("123456".to_owned()));
308        } else {
309            bail!("wrong type")
310        }
311        Ok(())
312    }
313    #[test]
314    fn minimal_no_options_no_double_slash() -> anyhow::Result<()> {
315        let result = decode_login("dclogin:email@host.tld?p=123&v=1")?;
316        if let Qr::Login { address, options } = result {
317            assert_eq!(address, "email@host.tld".to_owned());
318            assert_eq!(options, login_options_just_pw!("123".to_owned()));
319        } else {
320            bail!("wrong type")
321        }
322        let result = decode_login("dclogin:email@host.tld/?p=123456&v=1")?;
323        if let Qr::Login { address, options } = result {
324            assert_eq!(address, "email@host.tld".to_owned());
325            assert_eq!(options, login_options_just_pw!("123456".to_owned()));
326        } else {
327            bail!("wrong type")
328        }
329        let result = decode_login("dclogin:email@host.tld/ignored/path?p=123456&v=1")?;
330        if let Qr::Login { address, options } = result {
331            assert_eq!(address, "email@host.tld".to_owned());
332            assert_eq!(options, login_options_just_pw!("123456".to_owned()));
333        } else {
334            bail!("wrong type")
335        }
336        Ok(())
337    }
338
339    #[test]
340    fn no_version_set() {
341        assert!(decode_login("dclogin:email@host.tld?p=123").is_err());
342    }
343
344    #[test]
345    fn invalid_version_set() {
346        assert!(decode_login("dclogin:email@host.tld?p=123&v=").is_err());
347        assert!(decode_login("dclogin:email@host.tld?p=123&v=%40").is_err());
348        assert!(decode_login("dclogin:email@host.tld?p=123&v=-20").is_err());
349        assert!(decode_login("dclogin:email@host.tld?p=123&v=hi").is_err());
350    }
351
352    #[test]
353    fn version_too_new() -> anyhow::Result<()> {
354        let result = decode_login("dclogin:email@host.tld/?p=123456&v=2")?;
355        if let Qr::Login { options, .. } = result {
356            assert_eq!(options, LoginOptions::UnsuportedVersion(2));
357        } else {
358            bail!("wrong type");
359        }
360        let result = decode_login("dclogin:email@host.tld/?p=123456&v=5")?;
361        if let Qr::Login { options, .. } = result {
362            assert_eq!(options, LoginOptions::UnsuportedVersion(5));
363        } else {
364            bail!("wrong type");
365        }
366        Ok(())
367    }
368
369    #[test]
370    fn all_advanced_options() -> anyhow::Result<()> {
371        let result = decode_login(
372            "dclogin:email@host.tld?p=secret&v=1&ih=imap.host.tld&ip=4000&iu=max&ipw=87654&is=ssl&ic=1&sh=mail.host.tld&sp=3000&su=max@host.tld&spw=3242HS&ss=plain&sc=3",
373        )?;
374        if let Qr::Login { address, options } = result {
375            assert_eq!(address, "email@host.tld".to_owned());
376            assert_eq!(
377                options,
378                LoginOptions::V1 {
379                    mail_pw: "secret".to_owned(),
380                    imap_host: Some("imap.host.tld".to_owned()),
381                    imap_port: Some(4000),
382                    imap_username: Some("max".to_owned()),
383                    imap_password: Some("87654".to_owned()),
384                    imap_security: Some(Socket::Ssl),
385                    smtp_host: Some("mail.host.tld".to_owned()),
386                    smtp_port: Some(3000),
387                    smtp_username: Some("max@host.tld".to_owned()),
388                    smtp_password: Some("3242HS".to_owned()),
389                    smtp_security: Some(Socket::Plain),
390                    certificate_checks: Some(EnteredCertificateChecks::Strict),
391                }
392            );
393        } else {
394            bail!("wrong type")
395        }
396        Ok(())
397    }
398
399    #[test]
400    fn uri_encoded_password() -> anyhow::Result<()> {
401        let result = decode_login(
402            "dclogin:email@host.tld?p=%7BDaehFl%3B%22as%40%21fhdodn5%24234%22%7B%7Dfg&v=1",
403        )?;
404        if let Qr::Login { address, options } = result {
405            assert_eq!(address, "email@host.tld".to_owned());
406            assert_eq!(
407                options,
408                login_options_just_pw!("{DaehFl;\"as@!fhdodn5$234\"{}fg".to_owned())
409            );
410        } else {
411            bail!("wrong type")
412        }
413        Ok(())
414    }
415
416    #[test]
417    fn email_with_plus_extension() -> anyhow::Result<()> {
418        let result = decode_login("dclogin:usename+extension@host?p=1234&v=1")?;
419        if let Qr::Login { address, options } = result {
420            assert_eq!(address, "usename+extension@host".to_owned());
421            assert_eq!(options, login_options_just_pw!("1234".to_owned()));
422        } else {
423            bail!("wrong type")
424        }
425        Ok(())
426    }
427}