1use std::collections::HashMap;
2
3use anyhow::{Context as _, Result, bail};
4
5use deltachat_contact_tools::may_be_valid_addr;
6
7use super::{DCLOGIN_SCHEME, Qr};
8use crate::login_param::{EnteredCertificateChecks, EnteredLoginParam, EnteredServerLoginParam};
9use crate::provider::Socket;
10
11#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum LoginOptions {
14 UnsuportedVersion(u32),
16
17 V1 {
19 mail_pw: String,
23
24 imap_host: Option<String>,
26
27 imap_port: Option<u16>,
29
30 imap_username: Option<String>,
32
33 imap_password: Option<String>,
35
36 imap_security: Option<Socket>,
38
39 smtp_host: Option<String>,
41
42 smtp_port: Option<u16>,
44
45 smtp_username: Option<String>,
47
48 smtp_password: Option<String>,
50
51 smtp_security: Option<Socket>,
53
54 certificate_checks: Option<EnteredCertificateChecks>,
56 },
57}
58
59pub(super) fn decode_login(qr: &str) -> Result<Qr> {
62 let url = url::Url::parse(qr).with_context(|| format!("Malformed url: {qr:?}"))?;
63
64 let url_without_scheme = qr
65 .get(DCLOGIN_SCHEME.len()..)
66 .context("invalid DCLOGIN payload E1")?;
67 let payload = url_without_scheme
68 .strip_prefix("//")
69 .unwrap_or(url_without_scheme);
70
71 let addr = payload
72 .split(['?', '/'])
73 .next()
74 .context("invalid DCLOGIN payload E3")?;
75
76 if url.scheme().eq_ignore_ascii_case("dclogin") {
77 let options = url.query_pairs();
78 if options.count() == 0 {
79 bail!("invalid DCLOGIN payload E4")
80 }
81 let parameter_map: HashMap<String, String> = options
83 .map(|(key, value)| (key.into_owned(), value.into_owned()))
84 .collect();
85
86 if !may_be_valid_addr(addr) {
88 bail!("invalid DCLOGIN payload: invalid username E5");
89 }
90
91 let options: LoginOptions = match parameter_map.get("v").map(|i| i.parse::<u32>()) {
93 Some(Ok(1)) => LoginOptions::V1 {
94 mail_pw: parameter_map
95 .get("p")
96 .map(|s| s.to_owned())
97 .context("password missing")?,
98 imap_host: parameter_map.get("ih").map(|s| s.to_owned()),
99 imap_port: parse_port(parameter_map.get("ip"))
100 .context("could not parse imap port")?,
101 imap_username: parameter_map.get("iu").map(|s| s.to_owned()),
102 imap_password: parameter_map.get("ipw").map(|s| s.to_owned()),
103 imap_security: parse_socket_security(parameter_map.get("is"))?,
104 smtp_host: parameter_map.get("sh").map(|s| s.to_owned()),
105 smtp_port: parse_port(parameter_map.get("sp"))
106 .context("could not parse smtp port")?,
107 smtp_username: parameter_map.get("su").map(|s| s.to_owned()),
108 smtp_password: parameter_map.get("spw").map(|s| s.to_owned()),
109 smtp_security: parse_socket_security(parameter_map.get("ss"))?,
110 certificate_checks: parse_certificate_checks(parameter_map.get("ic"))?,
111 },
112 Some(Ok(v)) => LoginOptions::UnsuportedVersion(v),
113 Some(Err(_)) => bail!("version could not be parsed as number E6"),
114 None => bail!("invalid DCLOGIN payload: version missing E7"),
115 };
116
117 Ok(Qr::Login {
118 address: addr.to_owned(),
119 options,
120 })
121 } else {
122 bail!("Bad scheme for account URL: {payload:?}.");
123 }
124}
125
126fn parse_port(port: Option<&String>) -> core::result::Result<Option<u16>, std::num::ParseIntError> {
127 match port {
128 Some(p) => Ok(Some(p.parse::<u16>()?)),
129 None => Ok(None),
130 }
131}
132
133fn parse_socket_security(security: Option<&String>) -> Result<Option<Socket>> {
134 Ok(match security.map(|s| s.as_str()) {
135 Some("ssl") => Some(Socket::Ssl),
136 Some("starttls") => Some(Socket::Starttls),
137 Some("default") => Some(Socket::Automatic),
138 Some("plain") => Some(Socket::Plain),
139 Some(other) => bail!("Unknown security level: {other}"),
140 None => None,
141 })
142}
143
144fn parse_certificate_checks(
145 certificate_checks: Option<&String>,
146) -> Result<Option<EnteredCertificateChecks>> {
147 Ok(match certificate_checks.map(|s| s.as_str()) {
148 Some("0") => Some(EnteredCertificateChecks::Automatic),
149 Some("1") => Some(EnteredCertificateChecks::Strict),
150 Some("2") => Some(EnteredCertificateChecks::AcceptInvalidCertificates),
151 Some("3") => Some(EnteredCertificateChecks::AcceptInvalidCertificates2),
152 Some(other) => bail!("Unknown certificatecheck level: {other}"),
153 None => None,
154 })
155}
156
157pub(crate) fn login_param_from_login_qr(
158 addr: &str,
159 options: LoginOptions,
160) -> Result<EnteredLoginParam> {
161 match options {
162 LoginOptions::V1 {
163 mail_pw,
164 imap_host,
165 imap_port,
166 imap_username,
167 imap_password,
168 imap_security,
169 smtp_host,
170 smtp_port,
171 smtp_username,
172 smtp_password,
173 smtp_security,
174 certificate_checks,
175 } => {
176 let param = EnteredLoginParam {
177 addr: addr.to_string(),
178 imap: EnteredServerLoginParam {
179 server: imap_host.unwrap_or_default(),
180 port: imap_port.unwrap_or_default(),
181 security: imap_security.unwrap_or_default(),
182 user: imap_username.unwrap_or_default(),
183 password: imap_password.unwrap_or(mail_pw),
184 },
185 smtp: EnteredServerLoginParam {
186 server: smtp_host.unwrap_or_default(),
187 port: smtp_port.unwrap_or_default(),
188 security: smtp_security.unwrap_or_default(),
189 user: smtp_username.unwrap_or_default(),
190 password: smtp_password.unwrap_or_default(),
191 },
192 certificate_checks: certificate_checks.unwrap_or_default(),
193 oauth2: false,
194 };
195 Ok(param)
196 }
197 _ => bail!(
198 "DeltaChat does not understand this QR Code yet, please update the app and try again."
199 ),
200 }
201}
202
203#[cfg(test)]
204mod test {
205 use anyhow::bail;
206
207 use super::{LoginOptions, decode_login};
208 use crate::{login_param::EnteredCertificateChecks, provider::Socket, qr::Qr};
209
210 macro_rules! login_options_just_pw {
211 ($pw: expr) => {
212 LoginOptions::V1 {
213 mail_pw: $pw,
214 imap_host: None,
215 imap_port: None,
216 imap_username: None,
217 imap_password: None,
218 imap_security: None,
219 smtp_host: None,
220 smtp_port: None,
221 smtp_username: None,
222 smtp_password: None,
223 smtp_security: None,
224 certificate_checks: None,
225 }
226 };
227 }
228
229 #[test]
230 fn minimal_no_options() -> anyhow::Result<()> {
231 let result = decode_login("dclogin://email@host.tld?p=123&v=1")?;
232 if let Qr::Login { address, options } = result {
233 assert_eq!(address, "email@host.tld".to_owned());
234 assert_eq!(options, login_options_just_pw!("123".to_owned()));
235 } else {
236 bail!("wrong type")
237 }
238 let result = decode_login("dclogin://email@host.tld/?p=123456&v=1")?;
239 if let Qr::Login { address, options } = result {
240 assert_eq!(address, "email@host.tld".to_owned());
241 assert_eq!(options, login_options_just_pw!("123456".to_owned()));
242 } else {
243 bail!("wrong type")
244 }
245 let result = decode_login("dclogin://email@host.tld/ignored/path?p=123456&v=1")?;
246 if let Qr::Login { address, options } = result {
247 assert_eq!(address, "email@host.tld".to_owned());
248 assert_eq!(options, login_options_just_pw!("123456".to_owned()));
249 } else {
250 bail!("wrong type")
251 }
252 Ok(())
253 }
254 #[test]
255 fn minimal_no_options_no_double_slash() -> anyhow::Result<()> {
256 let result = decode_login("dclogin:email@host.tld?p=123&v=1")?;
257 if let Qr::Login { address, options } = result {
258 assert_eq!(address, "email@host.tld".to_owned());
259 assert_eq!(options, login_options_just_pw!("123".to_owned()));
260 } else {
261 bail!("wrong type")
262 }
263 let result = decode_login("dclogin:email@host.tld/?p=123456&v=1")?;
264 if let Qr::Login { address, options } = result {
265 assert_eq!(address, "email@host.tld".to_owned());
266 assert_eq!(options, login_options_just_pw!("123456".to_owned()));
267 } else {
268 bail!("wrong type")
269 }
270 let result = decode_login("dclogin:email@host.tld/ignored/path?p=123456&v=1")?;
271 if let Qr::Login { address, options } = result {
272 assert_eq!(address, "email@host.tld".to_owned());
273 assert_eq!(options, login_options_just_pw!("123456".to_owned()));
274 } else {
275 bail!("wrong type")
276 }
277 Ok(())
278 }
279
280 #[test]
281 fn no_version_set() {
282 assert!(decode_login("dclogin:email@host.tld?p=123").is_err());
283 }
284
285 #[test]
286 fn invalid_version_set() {
287 assert!(decode_login("dclogin:email@host.tld?p=123&v=").is_err());
288 assert!(decode_login("dclogin:email@host.tld?p=123&v=%40").is_err());
289 assert!(decode_login("dclogin:email@host.tld?p=123&v=-20").is_err());
290 assert!(decode_login("dclogin:email@host.tld?p=123&v=hi").is_err());
291 }
292
293 #[test]
294 fn version_too_new() -> anyhow::Result<()> {
295 let result = decode_login("dclogin:email@host.tld/?p=123456&v=2")?;
296 if let Qr::Login { options, .. } = result {
297 assert_eq!(options, LoginOptions::UnsuportedVersion(2));
298 } else {
299 bail!("wrong type");
300 }
301 let result = decode_login("dclogin:email@host.tld/?p=123456&v=5")?;
302 if let Qr::Login { options, .. } = result {
303 assert_eq!(options, LoginOptions::UnsuportedVersion(5));
304 } else {
305 bail!("wrong type");
306 }
307 Ok(())
308 }
309
310 #[test]
311 fn all_advanced_options() -> anyhow::Result<()> {
312 let result = decode_login(
313 "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",
314 )?;
315 if let Qr::Login { address, options } = result {
316 assert_eq!(address, "email@host.tld".to_owned());
317 assert_eq!(
318 options,
319 LoginOptions::V1 {
320 mail_pw: "secret".to_owned(),
321 imap_host: Some("imap.host.tld".to_owned()),
322 imap_port: Some(4000),
323 imap_username: Some("max".to_owned()),
324 imap_password: Some("87654".to_owned()),
325 imap_security: Some(Socket::Ssl),
326 smtp_host: Some("mail.host.tld".to_owned()),
327 smtp_port: Some(3000),
328 smtp_username: Some("max@host.tld".to_owned()),
329 smtp_password: Some("3242HS".to_owned()),
330 smtp_security: Some(Socket::Plain),
331 certificate_checks: Some(EnteredCertificateChecks::Strict),
332 }
333 );
334 } else {
335 bail!("wrong type")
336 }
337 Ok(())
338 }
339
340 #[test]
341 fn uri_encoded_password() -> anyhow::Result<()> {
342 let result = decode_login(
343 "dclogin:email@host.tld?p=%7BDaehFl%3B%22as%40%21fhdodn5%24234%22%7B%7Dfg&v=1",
344 )?;
345 if let Qr::Login { address, options } = result {
346 assert_eq!(address, "email@host.tld".to_owned());
347 assert_eq!(
348 options,
349 login_options_just_pw!("{DaehFl;\"as@!fhdodn5$234\"{}fg".to_owned())
350 );
351 } else {
352 bail!("wrong type")
353 }
354 Ok(())
355 }
356
357 #[test]
358 fn email_with_plus_extension() -> anyhow::Result<()> {
359 let result = decode_login("dclogin:usename+extension@host?p=1234&v=1")?;
360 if let Qr::Login { address, options } = result {
361 assert_eq!(address, "usename+extension@host".to_owned());
362 assert_eq!(options, login_options_just_pw!("1234".to_owned()));
363 } else {
364 bail!("wrong type")
365 }
366 Ok(())
367 }
368}