deltachat/
qr.rs

1//! # QR code module.
2
3mod dclogin_scheme;
4use std::collections::BTreeMap;
5use std::sync::LazyLock;
6
7use anyhow::{Context as _, Result, anyhow, bail, ensure};
8pub use dclogin_scheme::LoginOptions;
9pub(crate) use dclogin_scheme::login_param_from_login_qr;
10use deltachat_contact_tools::{ContactAddress, addr_normalize, may_be_valid_addr};
11use percent_encoding::{NON_ALPHANUMERIC, percent_decode_str, percent_encode};
12use rand::TryRngCore as _;
13use rand::distr::{Alphanumeric, SampleString};
14use serde::Deserialize;
15
16use crate::config::Config;
17use crate::contact::{Contact, ContactId, Origin};
18use crate::context::Context;
19use crate::events::EventType;
20use crate::key::Fingerprint;
21use crate::login_param::{EnteredCertificateChecks, EnteredLoginParam, EnteredServerLoginParam};
22use crate::net::http::post_empty;
23use crate::net::proxy::{DEFAULT_SOCKS_PORT, ProxyConfig};
24use crate::token;
25use crate::tools::{time, validate_id};
26
27const OPENPGP4FPR_SCHEME: &str = "OPENPGP4FPR:"; // yes: uppercase
28const IDELTACHAT_SCHEME: &str = "https://i.delta.chat/#";
29const IDELTACHAT_NOSLASH_SCHEME: &str = "https://i.delta.chat#";
30const DCACCOUNT_SCHEME: &str = "DCACCOUNT:";
31pub(super) const DCLOGIN_SCHEME: &str = "DCLOGIN:";
32const TG_SOCKS_SCHEME: &str = "https://t.me/socks";
33const MAILTO_SCHEME: &str = "mailto:";
34const MATMSG_SCHEME: &str = "MATMSG:";
35const VCARD_SCHEME: &str = "BEGIN:VCARD";
36const SMTP_SCHEME: &str = "SMTP:";
37const HTTPS_SCHEME: &str = "https://";
38const SHADOWSOCKS_SCHEME: &str = "ss://";
39
40/// Backup transfer based on iroh-net.
41pub(crate) const DCBACKUP_SCHEME_PREFIX: &str = "DCBACKUP";
42
43/// Version written to Backups and Backup-QR-Codes.
44/// Imports will fail when they have a larger version.
45pub(crate) const DCBACKUP_VERSION: i32 = 3;
46
47/// Scanned QR code.
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub enum Qr {
50    /// Ask the user whether to verify the contact.
51    ///
52    /// If the user agrees, pass this QR code to [`crate::securejoin::join_securejoin`].
53    AskVerifyContact {
54        /// ID of the contact.
55        contact_id: ContactId,
56
57        /// Fingerprint of the contact key as scanned from the QR code.
58        fingerprint: Fingerprint,
59
60        /// Invite number.
61        invitenumber: String,
62
63        /// Authentication code.
64        authcode: String,
65    },
66
67    /// Ask the user whether to join the group.
68    AskVerifyGroup {
69        /// Group name.
70        grpname: String,
71
72        /// Group ID.
73        grpid: String,
74
75        /// ID of the contact.
76        contact_id: ContactId,
77
78        /// Fingerprint of the contact key as scanned from the QR code.
79        fingerprint: Fingerprint,
80
81        /// Invite number.
82        invitenumber: String,
83
84        /// Authentication code.
85        authcode: String,
86    },
87
88    /// Ask whether to join the broadcast channel.
89    AskJoinBroadcast {
90        /// The user-visible name of this broadcast channel
91        name: String,
92
93        /// A string of random characters,
94        /// uniquely identifying this broadcast channel across all databases/clients.
95        /// Called `grpid` for historic reasons:
96        /// The id of multi-user chats is always called `grpid` in the database
97        /// because groups were once the only multi-user chats.
98        grpid: String,
99
100        /// ID of the contact who owns the channel and created the QR code.
101        contact_id: ContactId,
102
103        /// Fingerprint of the contact's key as scanned from the QR code.
104        fingerprint: Fingerprint,
105
106        /// Invite number.
107        invitenumber: String,
108        /// Authentication code.
109        authcode: String,
110    },
111
112    /// Contact fingerprint is verified.
113    ///
114    /// Ask the user if they want to start chatting.
115    FprOk {
116        /// Contact ID.
117        contact_id: ContactId,
118    },
119
120    /// Scanned fingerprint does not match the last seen fingerprint.
121    FprMismatch {
122        /// Contact ID.
123        contact_id: Option<ContactId>,
124    },
125
126    /// The scanned QR code contains a fingerprint but no e-mail address.
127    FprWithoutAddr {
128        /// Key fingerprint.
129        fingerprint: String,
130    },
131
132    /// Ask the user if they want to create an account on the given domain.
133    Account {
134        /// Server domain name.
135        domain: String,
136    },
137
138    /// Provides a backup that can be retrieved using iroh-net based backup transfer protocol.
139    Backup2 {
140        /// Iroh node address.
141        node_addr: iroh::NodeAddr,
142
143        /// Authentication token.
144        auth_token: String,
145    },
146
147    /// The QR code is a backup, but it is too new. The user has to update its Delta Chat.
148    BackupTooNew {},
149
150    /// Ask the user if they want to use the given proxy.
151    ///
152    /// Note that HTTP(S) URLs without a path
153    /// and query parameters are treated as HTTP(S) proxy URL.
154    /// UI may want to still offer to open the URL
155    /// in the browser if QR code contents
156    /// starts with `http://` or `https://`
157    /// and the QR code was not scanned from
158    /// the proxy configuration screen.
159    Proxy {
160        /// Proxy URL.
161        ///
162        /// This is the URL that is going to be added.
163        url: String,
164
165        /// Host extracted from the URL to display in the UI.
166        host: String,
167
168        /// Port extracted from the URL to display in the UI.
169        port: u16,
170    },
171
172    /// Contact address is scanned.
173    ///
174    /// Optionally, a draft message could be provided.
175    /// Ask the user if they want to start chatting.
176    Addr {
177        /// Contact ID.
178        contact_id: ContactId,
179
180        /// Draft message.
181        draft: Option<String>,
182    },
183
184    /// URL scanned.
185    ///
186    /// Ask the user if they want to open a browser or copy the URL to clipboard.
187    Url {
188        /// URL.
189        url: String,
190    },
191
192    /// Text scanned.
193    ///
194    /// Ask the user if they want to copy the text to clipboard.
195    Text {
196        /// Scanned text.
197        text: String,
198    },
199
200    /// Ask the user if they want to withdraw their own QR code.
201    WithdrawVerifyContact {
202        /// Contact ID.
203        contact_id: ContactId,
204
205        /// Fingerprint of the contact key as scanned from the QR code.
206        fingerprint: Fingerprint,
207
208        /// Invite number.
209        invitenumber: String,
210
211        /// Authentication code.
212        authcode: String,
213    },
214
215    /// Ask the user if they want to withdraw their own group invite QR code.
216    WithdrawVerifyGroup {
217        /// Group name.
218        grpname: String,
219
220        /// Group ID.
221        grpid: String,
222
223        /// Contact ID.
224        contact_id: ContactId,
225
226        /// Fingerprint of the contact key as scanned from the QR code.
227        fingerprint: Fingerprint,
228
229        /// Invite number.
230        invitenumber: String,
231
232        /// Authentication code.
233        authcode: String,
234    },
235
236    /// Ask the user if they want to revive their own QR code.
237    ReviveVerifyContact {
238        /// Contact ID.
239        contact_id: ContactId,
240
241        /// Fingerprint of the contact key as scanned from the QR code.
242        fingerprint: Fingerprint,
243
244        /// Invite number.
245        invitenumber: String,
246
247        /// Authentication code.
248        authcode: String,
249    },
250
251    /// Ask the user if they want to revive their own group invite QR code.
252    ReviveVerifyGroup {
253        /// Group name.
254        grpname: String,
255
256        /// Group ID.
257        grpid: String,
258
259        /// Contact ID.
260        contact_id: ContactId,
261
262        /// Fingerprint of the contact key as scanned from the QR code.
263        fingerprint: Fingerprint,
264
265        /// Invite number.
266        invitenumber: String,
267
268        /// Authentication code.
269        authcode: String,
270    },
271
272    /// `dclogin:` scheme parameters.
273    ///
274    /// Ask the user if they want to login with the email address.
275    Login {
276        /// Email address.
277        address: String,
278
279        /// Login parameters.
280        options: LoginOptions,
281    },
282}
283
284// hack around the changed JSON accidentally used by an iroh upgrade, see #6518 for more details and for code snippet.
285// this hack is mainly needed to give ppl time to upgrade and can be removed after some months (added 2025-02)
286fn fix_add_second_device_qr(qr: &str) -> String {
287    qr.replacen(r#","info":{"relay_url":"#, r#","relay_url":"#, 1)
288        .replacen(r#""]}}"#, r#""]}"#, 1)
289}
290
291fn starts_with_ignore_case(string: &str, pattern: &str) -> bool {
292    string.to_lowercase().starts_with(&pattern.to_lowercase())
293}
294
295/// Checks a scanned QR code.
296///
297/// The function should be called after a QR code is scanned.
298/// The function takes the raw text scanned and checks what can be done with it.
299pub async fn check_qr(context: &Context, qr: &str) -> Result<Qr> {
300    let qr = qr.trim();
301    let qrcode = if starts_with_ignore_case(qr, OPENPGP4FPR_SCHEME) {
302        decode_openpgp(context, qr)
303            .await
304            .context("failed to decode OPENPGP4FPR QR code")?
305    } else if qr.starts_with(IDELTACHAT_SCHEME) {
306        decode_ideltachat(context, IDELTACHAT_SCHEME, qr).await?
307    } else if qr.starts_with(IDELTACHAT_NOSLASH_SCHEME) {
308        decode_ideltachat(context, IDELTACHAT_NOSLASH_SCHEME, qr).await?
309    } else if starts_with_ignore_case(qr, DCACCOUNT_SCHEME) {
310        decode_account(qr)?
311    } else if starts_with_ignore_case(qr, DCLOGIN_SCHEME) {
312        dclogin_scheme::decode_login(qr)?
313    } else if starts_with_ignore_case(qr, TG_SOCKS_SCHEME) {
314        decode_tg_socks_proxy(context, qr)?
315    } else if qr.starts_with(SHADOWSOCKS_SCHEME) {
316        decode_shadowsocks_proxy(qr)?
317    } else if starts_with_ignore_case(qr, DCBACKUP_SCHEME_PREFIX) {
318        let qr_fixed = fix_add_second_device_qr(qr);
319        decode_backup2(&qr_fixed)?
320    } else if qr.starts_with(MAILTO_SCHEME) {
321        decode_mailto(context, qr).await?
322    } else if qr.starts_with(SMTP_SCHEME) {
323        decode_smtp(context, qr).await?
324    } else if qr.starts_with(MATMSG_SCHEME) {
325        decode_matmsg(context, qr).await?
326    } else if qr.starts_with(VCARD_SCHEME) {
327        decode_vcard(context, qr).await?
328    } else if let Ok(url) = url::Url::parse(qr) {
329        match url.scheme() {
330            "socks5" => Qr::Proxy {
331                url: qr.to_string(),
332                host: url.host_str().context("URL has no host")?.to_string(),
333                port: url.port().unwrap_or(DEFAULT_SOCKS_PORT),
334            },
335            "http" | "https" => {
336                // Parsing with a non-standard scheme
337                // is a hack to work around the `url` crate bug
338                // <https://github.com/servo/rust-url/issues/957>.
339                let url = if let Some(rest) = qr.strip_prefix("http://") {
340                    url::Url::parse(&format!("foobarbaz://{rest}"))?
341                } else if let Some(rest) = qr.strip_prefix("https://") {
342                    url::Url::parse(&format!("foobarbaz://{rest}"))?
343                } else {
344                    // Should not happen.
345                    url
346                };
347
348                if url.port().is_none() | (url.path() != "") | url.query().is_some() {
349                    // URL without a port, with a path or query cannot be a proxy URL.
350                    Qr::Url {
351                        url: qr.to_string(),
352                    }
353                } else {
354                    Qr::Proxy {
355                        url: qr.to_string(),
356                        host: url.host_str().context("URL has no host")?.to_string(),
357                        port: url
358                            .port_or_known_default()
359                            .context("HTTP(S) URLs are guaranteed to return Some port")?,
360                    }
361                }
362            }
363            _ => Qr::Url {
364                url: qr.to_string(),
365            },
366        }
367    } else {
368        Qr::Text {
369            text: qr.to_string(),
370        }
371    };
372    Ok(qrcode)
373}
374
375/// Formats the text of the [`Qr::Backup2`] variant.
376///
377/// This is the inverse of [`check_qr`] for that variant only.
378///
379/// TODO: Refactor this so all variants have a correct [`Display`] and transform `check_qr`
380/// into `FromStr`.
381pub fn format_backup(qr: &Qr) -> Result<String> {
382    match qr {
383        Qr::Backup2 {
384            node_addr,
385            auth_token,
386        } => {
387            let node_addr = serde_json::to_string(node_addr)?;
388            Ok(format!(
389                "{DCBACKUP_SCHEME_PREFIX}{DCBACKUP_VERSION}:{auth_token}&{node_addr}"
390            ))
391        }
392        _ => Err(anyhow!("Not a backup QR code")),
393    }
394}
395
396/// scheme: `OPENPGP4FPR:FINGERPRINT#a=ADDR&n=NAME&i=INVITENUMBER&s=AUTH`
397///     or: `OPENPGP4FPR:FINGERPRINT#a=ADDR&g=GROUPNAME&x=GROUPID&i=INVITENUMBER&s=AUTH`
398///     or: `OPENPGP4FPR:FINGERPRINT#a=ADDR&b=BROADCAST_NAME&x=BROADCAST_ID&j=INVITENUMBER&s=AUTH`
399///     or: `OPENPGP4FPR:FINGERPRINT#a=ADDR`
400async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
401    let payload = qr
402        .get(OPENPGP4FPR_SCHEME.len()..)
403        .context("Invalid OPENPGP4FPR scheme")?;
404
405    // macOS and iOS sometimes replace the # with %23 (uri encode it), we should be able to parse this wrong format too.
406    // see issue https://github.com/deltachat/deltachat-core-rust/issues/1969 for more info
407    let (fingerprint, fragment) = match payload
408        .split_once('#')
409        .or_else(|| payload.split_once("%23"))
410    {
411        Some(pair) => pair,
412        None => (payload, ""),
413    };
414    let fingerprint: Fingerprint = fingerprint
415        .parse()
416        .context("Failed to parse fingerprint in the QR code")?;
417
418    let param: BTreeMap<&str, &str> = fragment
419        .split('&')
420        .filter_map(|s| {
421            if let [key, value] = s.splitn(2, '=').collect::<Vec<_>>()[..] {
422                Some((key, value))
423            } else {
424                None
425            }
426        })
427        .collect();
428
429    let addr = if let Some(addr) = param.get("a") {
430        Some(normalize_address(addr)?)
431    } else {
432        None
433    };
434
435    let name = decode_name(&param, "n")?.unwrap_or_default();
436
437    let invitenumber = param
438        .get("i")
439        // For historic reansons, broadcasts currently use j instead of i for the invitenumber:
440        .or_else(|| param.get("j"))
441        .filter(|&s| validate_id(s))
442        .map(|s| s.to_string());
443    let authcode = param
444        .get("s")
445        .filter(|&s| validate_id(s))
446        .map(|s| s.to_string());
447    let grpid = param
448        .get("x")
449        .filter(|&s| validate_id(s))
450        .map(|s| s.to_string());
451
452    let grpname = decode_name(&param, "g")?;
453    let broadcast_name = decode_name(&param, "b")?;
454
455    if let (Some(addr), Some(invitenumber), Some(authcode)) = (&addr, invitenumber, authcode) {
456        let addr = ContactAddress::new(addr)?;
457        let (contact_id, _) = Contact::add_or_lookup_ex(
458            context,
459            &name,
460            &addr,
461            &fingerprint.hex(),
462            Origin::UnhandledSecurejoinQrScan,
463        )
464        .await
465        .with_context(|| format!("failed to add or lookup contact for address {addr:?}"))?;
466
467        if let (Some(grpid), Some(grpname)) = (grpid.clone(), grpname) {
468            if context
469                .is_self_addr(&addr)
470                .await
471                .with_context(|| format!("can't check if address {addr:?} is our address"))?
472            {
473                if token::exists(context, token::Namespace::InviteNumber, &invitenumber).await? {
474                    Ok(Qr::WithdrawVerifyGroup {
475                        grpname,
476                        grpid,
477                        contact_id,
478                        fingerprint,
479                        invitenumber,
480                        authcode,
481                    })
482                } else {
483                    Ok(Qr::ReviveVerifyGroup {
484                        grpname,
485                        grpid,
486                        contact_id,
487                        fingerprint,
488                        invitenumber,
489                        authcode,
490                    })
491                }
492            } else {
493                Ok(Qr::AskVerifyGroup {
494                    grpname,
495                    grpid,
496                    contact_id,
497                    fingerprint,
498                    invitenumber,
499                    authcode,
500                })
501            }
502        } else if let (Some(grpid), Some(name)) = (grpid, broadcast_name) {
503            Ok(Qr::AskJoinBroadcast {
504                name,
505                grpid,
506                contact_id,
507                fingerprint,
508                invitenumber,
509                authcode,
510            })
511        } else if context.is_self_addr(&addr).await? {
512            if token::exists(context, token::Namespace::InviteNumber, &invitenumber).await? {
513                Ok(Qr::WithdrawVerifyContact {
514                    contact_id,
515                    fingerprint,
516                    invitenumber,
517                    authcode,
518                })
519            } else {
520                Ok(Qr::ReviveVerifyContact {
521                    contact_id,
522                    fingerprint,
523                    invitenumber,
524                    authcode,
525                })
526            }
527        } else {
528            Ok(Qr::AskVerifyContact {
529                contact_id,
530                fingerprint,
531                invitenumber,
532                authcode,
533            })
534        }
535    } else if let Some(addr) = addr {
536        let fingerprint = fingerprint.hex();
537        let (contact_id, _) =
538            Contact::add_or_lookup_ex(context, "", &addr, &fingerprint, Origin::UnhandledQrScan)
539                .await?;
540        let contact = Contact::get_by_id(context, contact_id).await?;
541
542        if contact.public_key(context).await?.is_some() {
543            Ok(Qr::FprOk { contact_id })
544        } else {
545            Ok(Qr::FprMismatch {
546                contact_id: Some(contact_id),
547            })
548        }
549    } else {
550        Ok(Qr::FprWithoutAddr {
551            fingerprint: fingerprint.to_string(),
552        })
553    }
554}
555
556fn decode_name(param: &BTreeMap<&str, &str>, key: &str) -> Result<Option<String>> {
557    if let Some(encoded_name) = param.get(key) {
558        let encoded_name = encoded_name.replace('+', "%20"); // sometimes spaces are encoded as `+`
559        let mut name = match percent_decode_str(&encoded_name).decode_utf8() {
560            Ok(name) => name.to_string(),
561            Err(err) => bail!("Invalid QR param {key}: {err}"),
562        };
563        if let Some(n) = name.strip_suffix('_') {
564            name = format!("{n}…");
565        }
566        Ok(Some(name))
567    } else {
568        Ok(None)
569    }
570}
571
572/// scheme: `https://i.delta.chat[/]#FINGERPRINT&a=ADDR[&OPTIONAL_PARAMS]`
573async fn decode_ideltachat(context: &Context, prefix: &str, qr: &str) -> Result<Qr> {
574    let qr = qr.replacen(prefix, OPENPGP4FPR_SCHEME, 1);
575    let qr = qr.replacen('&', "#", 1);
576    decode_openpgp(context, &qr)
577        .await
578        .with_context(|| format!("failed to decode {prefix} QR code"))
579}
580
581/// scheme: `DCACCOUNT:example.org`
582/// or `DCACCOUNT:https://example.org/new`
583/// or `DCACCOUNT:https://example.org/new_email?t=1w_7wDjgjelxeX884x96v3`
584fn decode_account(qr: &str) -> Result<Qr> {
585    let payload = qr
586        .get(DCACCOUNT_SCHEME.len()..)
587        .context("Invalid DCACCOUNT payload")?;
588    if payload.starts_with("https://") {
589        let url = url::Url::parse(payload).context("Invalid account URL")?;
590        if url.scheme() == "https" {
591            Ok(Qr::Account {
592                domain: url
593                    .host_str()
594                    .context("can't extract account setup domain")?
595                    .to_string(),
596            })
597        } else {
598            bail!("Bad scheme for account URL: {:?}.", url.scheme());
599        }
600    } else {
601        Ok(Qr::Account {
602            domain: payload.to_string(),
603        })
604    }
605}
606
607/// scheme: `https://t.me/socks?server=foo&port=123` or `https://t.me/socks?server=1.2.3.4&port=123`
608fn decode_tg_socks_proxy(_context: &Context, qr: &str) -> Result<Qr> {
609    let url = url::Url::parse(qr).context("Invalid t.me/socks url")?;
610
611    let mut host: Option<String> = None;
612    let mut port: u16 = DEFAULT_SOCKS_PORT;
613    let mut user: Option<String> = None;
614    let mut pass: Option<String> = None;
615    for (key, value) in url.query_pairs() {
616        if key == "server" {
617            host = Some(value.to_string());
618        } else if key == "port" {
619            port = value.parse().unwrap_or(DEFAULT_SOCKS_PORT);
620        } else if key == "user" {
621            user = Some(value.to_string());
622        } else if key == "pass" {
623            pass = Some(value.to_string());
624        }
625    }
626
627    let Some(host) = host else {
628        bail!("Bad t.me/socks url: {url:?}");
629    };
630
631    let mut url = "socks5://".to_string();
632    if let Some(pass) = pass {
633        url += &percent_encode(user.unwrap_or_default().as_bytes(), NON_ALPHANUMERIC).to_string();
634        url += ":";
635        url += &percent_encode(pass.as_bytes(), NON_ALPHANUMERIC).to_string();
636        url += "@";
637    };
638    url += &host;
639    url += ":";
640    url += &port.to_string();
641
642    Ok(Qr::Proxy { url, host, port })
643}
644
645/// Decodes `ss://` URLs for Shadowsocks proxies.
646fn decode_shadowsocks_proxy(qr: &str) -> Result<Qr> {
647    let server_config = shadowsocks::config::ServerConfig::from_url(qr)?;
648    let addr = server_config.addr();
649    let host = addr.host().to_string();
650    let port = addr.port();
651    Ok(Qr::Proxy {
652        url: qr.to_string(),
653        host,
654        port,
655    })
656}
657
658/// Decodes a `DCBACKUP` QR code.
659fn decode_backup2(qr: &str) -> Result<Qr> {
660    let version_and_payload = qr
661        .strip_prefix(DCBACKUP_SCHEME_PREFIX)
662        .ok_or_else(|| anyhow!("Invalid DCBACKUP scheme"))?;
663    let (version, payload) = version_and_payload
664        .split_once(':')
665        .context("DCBACKUP scheme separator missing")?;
666    let version: i32 = version.parse().context("Not a valid number")?;
667    if version > DCBACKUP_VERSION {
668        return Ok(Qr::BackupTooNew {});
669    }
670
671    let (auth_token, node_addr) = payload
672        .split_once('&')
673        .context("Backup QR code has no separator")?;
674    let auth_token = auth_token.to_string();
675    let node_addr = serde_json::from_str::<iroh::NodeAddr>(node_addr)
676        .context("Invalid node addr in backup QR code")?;
677
678    Ok(Qr::Backup2 {
679        node_addr,
680        auth_token,
681    })
682}
683
684#[derive(Debug, Deserialize)]
685struct CreateAccountSuccessResponse {
686    /// Email address.
687    email: String,
688
689    /// Password.
690    password: String,
691}
692#[derive(Debug, Deserialize)]
693struct CreateAccountErrorResponse {
694    /// Reason for the failure to create account returned by the server.
695    reason: String,
696}
697
698/// Takes a QR with `DCACCOUNT:` scheme, parses its parameters,
699/// downloads additional information from the contained URL
700/// and returns the login parameters.
701pub(crate) async fn login_param_from_account_qr(
702    context: &Context,
703    qr: &str,
704) -> Result<EnteredLoginParam> {
705    let payload = qr
706        .get(DCACCOUNT_SCHEME.len()..)
707        .context("Invalid DCACCOUNT scheme")?;
708
709    if !payload.starts_with(HTTPS_SCHEME) {
710        let rng = &mut rand::rngs::OsRng.unwrap_err();
711        let username = Alphanumeric.sample_string(rng, 9);
712        let addr = username + "@" + payload;
713        let password = Alphanumeric.sample_string(rng, 50);
714
715        let param = EnteredLoginParam {
716            addr,
717            imap: EnteredServerLoginParam {
718                password,
719                ..Default::default()
720            },
721            smtp: Default::default(),
722            certificate_checks: EnteredCertificateChecks::Strict,
723            oauth2: false,
724        };
725        return Ok(param);
726    }
727
728    let (response_text, response_success) = post_empty(context, payload).await?;
729    if response_success {
730        let CreateAccountSuccessResponse { password, email } = serde_json::from_str(&response_text)
731            .with_context(|| {
732                format!("Cannot create account, response is malformed:\n{response_text:?}")
733            })?;
734
735        let param = EnteredLoginParam {
736            addr: email,
737            imap: EnteredServerLoginParam {
738                password,
739                ..Default::default()
740            },
741            smtp: Default::default(),
742            certificate_checks: EnteredCertificateChecks::Strict,
743            oauth2: false,
744        };
745
746        Ok(param)
747    } else {
748        match serde_json::from_str::<CreateAccountErrorResponse>(&response_text) {
749            Ok(error) => Err(anyhow!(error.reason)),
750            Err(parse_error) => {
751                context.emit_event(EventType::Error(format!(
752                    "Cannot create account, server response could not be parsed:\n{parse_error:#}\nraw response:\n{response_text}"
753                )));
754                bail!("Cannot create account, unexpected server response:\n{response_text:?}")
755            }
756        }
757    }
758}
759
760/// Sets configuration values from a QR code.
761pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
762    match check_qr(context, qr).await? {
763        Qr::Account { .. } => {
764            let mut param = login_param_from_account_qr(context, qr).await?;
765            context.add_transport_inner(&mut param).await?
766        }
767        Qr::Proxy { url, .. } => {
768            let old_proxy_url_value = context
769                .get_config(Config::ProxyUrl)
770                .await?
771                .unwrap_or_default();
772
773            // Normalize the URL.
774            let url = ProxyConfig::from_url(&url)?.to_url();
775
776            let proxy_urls: Vec<&str> = std::iter::once(url.as_str())
777                .chain(
778                    old_proxy_url_value
779                        .split('\n')
780                        .filter(|s| !s.is_empty() && *s != url),
781                )
782                .collect();
783            context
784                .set_config(Config::ProxyUrl, Some(&proxy_urls.join("\n")))
785                .await?;
786            context.set_config_bool(Config::ProxyEnabled, true).await?;
787        }
788        Qr::WithdrawVerifyContact {
789            invitenumber,
790            authcode,
791            ..
792        } => {
793            token::delete(context, "").await?;
794            context
795                .sync_qr_code_token_deletion(invitenumber, authcode)
796                .await?;
797        }
798        Qr::WithdrawVerifyGroup {
799            grpid,
800            invitenumber,
801            authcode,
802            ..
803        } => {
804            token::delete(context, &grpid).await?;
805            context
806                .sync_qr_code_token_deletion(invitenumber, authcode)
807                .await?;
808        }
809        Qr::ReviveVerifyContact {
810            invitenumber,
811            authcode,
812            ..
813        } => {
814            let timestamp = time();
815            token::save(
816                context,
817                token::Namespace::InviteNumber,
818                None,
819                &invitenumber,
820                timestamp,
821            )
822            .await?;
823            token::save(context, token::Namespace::Auth, None, &authcode, timestamp).await?;
824            context.sync_qr_code_tokens(None).await?;
825            context.scheduler.interrupt_inbox().await;
826        }
827        Qr::ReviveVerifyGroup {
828            invitenumber,
829            authcode,
830            grpid,
831            ..
832        } => {
833            let timestamp = time();
834            token::save(
835                context,
836                token::Namespace::InviteNumber,
837                Some(&grpid),
838                &invitenumber,
839                timestamp,
840            )
841            .await?;
842            token::save(
843                context,
844                token::Namespace::Auth,
845                Some(&grpid),
846                &authcode,
847                timestamp,
848            )
849            .await?;
850            context.sync_qr_code_tokens(Some(&grpid)).await?;
851            context.scheduler.interrupt_inbox().await;
852        }
853        Qr::Login { address, options } => {
854            let mut param = login_param_from_login_qr(&address, options)?;
855            context.add_transport_inner(&mut param).await?
856        }
857        _ => bail!("QR code does not contain config"),
858    }
859
860    Ok(())
861}
862
863/// Extract address for the mailto scheme.
864///
865/// Scheme: `mailto:addr...?subject=...&body=..`
866async fn decode_mailto(context: &Context, qr: &str) -> Result<Qr> {
867    let payload = qr
868        .get(MAILTO_SCHEME.len()..)
869        .context("Invalid mailto: scheme")?;
870
871    let (addr, query) = payload.split_once('?').unwrap_or((payload, ""));
872
873    let param: BTreeMap<&str, &str> = query
874        .split('&')
875        .filter_map(|s| {
876            if let [key, value] = s.splitn(2, '=').collect::<Vec<_>>()[..] {
877                Some((key, value))
878            } else {
879                None
880            }
881        })
882        .collect();
883
884    let subject = if let Some(subject) = param.get("subject") {
885        subject.to_string()
886    } else {
887        "".to_string()
888    };
889    let draft = if let Some(body) = param.get("body") {
890        if subject.is_empty() {
891            body.to_string()
892        } else {
893            subject + "\n" + body
894        }
895    } else {
896        subject
897    };
898    let draft = draft.replace('+', "%20"); // sometimes spaces are encoded as `+`
899    let draft = match percent_decode_str(&draft).decode_utf8() {
900        Ok(decoded_draft) => decoded_draft.to_string(),
901        Err(_err) => draft,
902    };
903
904    let addr = normalize_address(addr)?;
905    let name = "";
906    Qr::from_address(
907        context,
908        name,
909        &addr,
910        if draft.is_empty() { None } else { Some(draft) },
911    )
912    .await
913}
914
915/// Extract address for the smtp scheme.
916///
917/// Scheme: `SMTP:addr...:subject...:body...`
918async fn decode_smtp(context: &Context, qr: &str) -> Result<Qr> {
919    let payload = qr.get(SMTP_SCHEME.len()..).context("Invalid SMTP scheme")?;
920
921    let (addr, _rest) = payload
922        .split_once(':')
923        .context("Invalid SMTP scheme payload")?;
924    let addr = normalize_address(addr)?;
925    let name = "";
926    Qr::from_address(context, name, &addr, None).await
927}
928
929/// Extract address for the matmsg scheme.
930///
931/// Scheme: `MATMSG:TO:addr...;SUB:subject...;BODY:body...;`
932///
933/// There may or may not be linebreaks after the fields.
934async fn decode_matmsg(context: &Context, qr: &str) -> Result<Qr> {
935    // Does not work when the text `TO:` is used in subject/body _and_ TO: is not the first field.
936    // we ignore this case.
937    let addr = if let Some(to_index) = qr.find("TO:") {
938        let addr = qr.get(to_index + 3..).unwrap_or_default().trim();
939        if let Some(semi_index) = addr.find(';') {
940            addr.get(..semi_index).unwrap_or_default().trim()
941        } else {
942            addr
943        }
944    } else {
945        bail!("Invalid MATMSG found");
946    };
947
948    let addr = normalize_address(addr)?;
949    let name = "";
950    Qr::from_address(context, name, &addr, None).await
951}
952
953static VCARD_NAME_RE: LazyLock<regex::Regex> =
954    LazyLock::new(|| regex::Regex::new(r"(?m)^N:([^;]*);([^;\n]*)").unwrap());
955static VCARD_EMAIL_RE: LazyLock<regex::Regex> =
956    LazyLock::new(|| regex::Regex::new(r"(?m)^EMAIL([^:\n]*):([^;\n]*)").unwrap());
957
958/// Extract address for the vcard scheme.
959///
960/// Scheme: `VCARD:BEGIN\nN:last name;first name;...;\nEMAIL;<type>:addr...;`
961async fn decode_vcard(context: &Context, qr: &str) -> Result<Qr> {
962    let name = VCARD_NAME_RE
963        .captures(qr)
964        .and_then(|caps| {
965            let last_name = caps.get(1)?.as_str().trim();
966            let first_name = caps.get(2)?.as_str().trim();
967
968            Some(format!("{first_name} {last_name}"))
969        })
970        .unwrap_or_default();
971
972    let addr = if let Some(cap) = VCARD_EMAIL_RE.captures(qr).and_then(|caps| caps.get(2)) {
973        normalize_address(cap.as_str().trim())?
974    } else {
975        bail!("Bad e-mail address");
976    };
977
978    Qr::from_address(context, &name, &addr, None).await
979}
980
981impl Qr {
982    /// Creates a new scanned QR code of a contact address.
983    ///
984    /// May contain a message draft.
985    pub async fn from_address(
986        context: &Context,
987        name: &str,
988        addr: &str,
989        draft: Option<String>,
990    ) -> Result<Self> {
991        let addr = ContactAddress::new(addr)?;
992        let (contact_id, _) =
993            Contact::add_or_lookup(context, name, &addr, Origin::UnhandledQrScan).await?;
994        Ok(Qr::Addr { contact_id, draft })
995    }
996}
997
998/// URL decodes a given address, does basic email validation on the result.
999fn normalize_address(addr: &str) -> Result<String> {
1000    // urldecoding is needed at least for OPENPGP4FPR but should not hurt in the other cases
1001    let new_addr = percent_decode_str(addr).decode_utf8()?;
1002    let new_addr = addr_normalize(&new_addr);
1003
1004    ensure!(may_be_valid_addr(&new_addr), "Bad e-mail address");
1005
1006    Ok(new_addr.to_string())
1007}
1008
1009#[cfg(test)]
1010mod qr_tests;