deltachat/
qr.rs

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