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