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#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum LoginOptions {
17 UnsuportedVersion(u32),
19
20 V1 {
22 mail_pw: String,
26
27 imap_host: Option<String>,
29
30 imap_port: Option<u16>,
32
33 imap_username: Option<String>,
35
36 imap_password: Option<String>,
38
39 imap_security: Option<Socket>,
41
42 smtp_host: Option<String>,
44
45 smtp_port: Option<u16>,
47
48 smtp_username: Option<String>,
50
51 smtp_password: Option<String>,
53
54 smtp_security: Option<Socket>,
56
57 certificate_checks: Option<EnteredCertificateChecks>,
59 },
60}
61
62pub(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 let parameter_map: HashMap<String, String> = options
86 .map(|(key, value)| (key.into_owned(), value.into_owned()))
87 .collect();
88
89 if !may_be_valid_addr(addr) {
91 bail!("invalid DCLOGIN payload: invalid username E5");
92 }
93
94 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}