deltachat/sql/
migrations.rs

1//! Migrations module.
2
3use std::collections::BTreeMap;
4use std::collections::BTreeSet;
5use std::time::Duration;
6
7use anyhow::{Context as _, Result, ensure};
8use deltachat_contact_tools::EmailAddress;
9use deltachat_contact_tools::addr_cmp;
10use pgp::composed::SignedPublicKey;
11use rusqlite::OptionalExtension;
12
13use crate::config::Config;
14use crate::configure::EnteredLoginParam;
15use crate::context::Context;
16use crate::key::DcKey;
17use crate::log::warn;
18use crate::provider::get_provider_info;
19use crate::sql::Sql;
20use crate::tools::{Time, inc_and_check, time_elapsed};
21use crate::transport::ConfiguredLoginParam;
22
23const DBVERSION: i32 = 68;
24const VERSION_CFG: &str = "dbversion";
25const TABLES: &str = include_str!("./tables.sql");
26
27#[cfg(test)]
28tokio::task_local! {
29    static STOP_MIGRATIONS_AT: i32;
30}
31
32fn migrate_key_contacts(
33    context: &Context,
34    transaction: &mut rusqlite::Transaction<'_>,
35) -> std::result::Result<(), anyhow::Error> {
36    info!(context, "Starting key-contact transition.");
37
38    // =============================== Step 1: ===============================
39    //                              Alter tables
40    transaction.execute_batch(
41        "ALTER TABLE contacts ADD COLUMN fingerprint TEXT NOT NULL DEFAULT '';
42
43        -- Verifier is an ID of the verifier contact.
44        -- 0 if the contact is not verified.
45        ALTER TABLE contacts ADD COLUMN verifier INTEGER NOT NULL DEFAULT 0;
46
47        CREATE INDEX contacts_fingerprint_index ON contacts (fingerprint);
48
49        CREATE TABLE public_keys (
50        id INTEGER PRIMARY KEY AUTOINCREMENT,
51        fingerprint TEXT NOT NULL UNIQUE, -- Upper-case fingerprint of the key.
52        public_key BLOB NOT NULL -- Binary key, not ASCII-armored
53        ) STRICT;
54        CREATE INDEX public_key_index ON public_keys (fingerprint);
55
56        INSERT OR IGNORE INTO public_keys (fingerprint, public_key)
57        SELECT public_key_fingerprint, public_key FROM acpeerstates
58         WHERE public_key_fingerprint IS NOT NULL AND public_key IS NOT NULL;
59
60        INSERT OR IGNORE INTO public_keys (fingerprint, public_key)
61        SELECT gossip_key_fingerprint, gossip_key FROM acpeerstates
62         WHERE gossip_key_fingerprint IS NOT NULL AND gossip_key IS NOT NULL;
63
64        INSERT OR IGNORE INTO public_keys (fingerprint, public_key)
65        SELECT verified_key_fingerprint, verified_key FROM acpeerstates
66         WHERE verified_key_fingerprint IS NOT NULL AND verified_key IS NOT NULL;
67
68        INSERT OR IGNORE INTO public_keys (fingerprint, public_key)
69        SELECT secondary_verified_key_fingerprint, secondary_verified_key FROM acpeerstates
70         WHERE secondary_verified_key_fingerprint IS NOT NULL AND secondary_verified_key IS NOT NULL;",
71    )
72    .context("Creating key-contact tables")?;
73
74    let Some(self_addr): Option<String> = transaction
75        .query_row(
76            "SELECT value FROM config WHERE keyname='configured_addr'",
77            (),
78            |row| row.get(0),
79        )
80        .optional()
81        .context("Step 0")?
82    else {
83        info!(
84            context,
85            "Not yet configured, no need to migrate key-contacts"
86        );
87        return Ok(());
88    };
89
90    // =============================== Step 2: ===============================
91    // Create up to 3 new contacts for every contact that has a peerstate:
92    // one from the Autocrypt key fingerprint, one from the verified key fingerprint,
93    // one from the secondary verified key fingerprint.
94    // In the process, build maps from old contact id to new contact id:
95    // one that maps to Autocrypt key-contact, one that maps to verified key-contact.
96    let mut autocrypt_key_contacts: BTreeMap<u32, u32> = BTreeMap::new();
97    let mut autocrypt_key_contacts_with_reset_peerstate: BTreeMap<u32, u32> = BTreeMap::new();
98    let mut verified_key_contacts: BTreeMap<u32, u32> = BTreeMap::new();
99    {
100        // This maps from the verified contact to the original contact id of the verifier.
101        // It can't map to the verified key contact id, because at the time of constructing
102        // this map, not all key-contacts are in the database.
103        let mut verifications: BTreeMap<u32, u32> = BTreeMap::new();
104
105        let mut load_contacts_stmt = transaction
106            .prepare(
107                "SELECT c.id, c.name, c.addr, c.origin, c.blocked, c.last_seen,
108                c.authname, c.param, c.status, c.is_bot, c.selfavatar_sent,
109                IFNULL(p.public_key, p.gossip_key),
110                p.verified_key, IFNULL(p.verifier, ''),
111                p.secondary_verified_key, p.secondary_verifier, p.prefer_encrypted
112                FROM contacts c
113                INNER JOIN acpeerstates p ON c.addr=p.addr
114                WHERE c.id > 9
115                ORDER BY p.last_seen DESC",
116            )
117            .context("Step 2")?;
118
119        let all_address_contacts: rusqlite::Result<Vec<_>> = load_contacts_stmt
120            .query_map((), |row| {
121                let id: i64 = row.get(0)?;
122                let name: String = row.get(1)?;
123                let addr: String = row.get(2)?;
124                let origin: i64 = row.get(3)?;
125                let blocked: Option<bool> = row.get(4)?;
126                let last_seen: i64 = row.get(5)?;
127                let authname: String = row.get(6)?;
128                let param: String = row.get(7)?;
129                let status: Option<String> = row.get(8)?;
130                let is_bot: bool = row.get(9)?;
131                let selfavatar_sent: i64 = row.get(10)?;
132                let autocrypt_key = row
133                    .get(11)
134                    .ok()
135                    .and_then(|blob: Vec<u8>| SignedPublicKey::from_slice(&blob).ok());
136                let verified_key = row
137                    .get(12)
138                    .ok()
139                    .and_then(|blob: Vec<u8>| SignedPublicKey::from_slice(&blob).ok());
140                let verifier: String = row.get(13)?;
141                let secondary_verified_key = row
142                    .get(12)
143                    .ok()
144                    .and_then(|blob: Vec<u8>| SignedPublicKey::from_slice(&blob).ok());
145                let secondary_verifier: String = row.get(15)?;
146                let prefer_encrypt: u8 = row.get(16)?;
147                Ok((
148                    id,
149                    name,
150                    addr,
151                    origin,
152                    blocked,
153                    last_seen,
154                    authname,
155                    param,
156                    status,
157                    is_bot,
158                    selfavatar_sent,
159                    autocrypt_key,
160                    verified_key,
161                    verifier,
162                    secondary_verified_key,
163                    secondary_verifier,
164                    prefer_encrypt,
165                ))
166            })
167            .context("Step 3")?
168            .collect();
169
170        let mut insert_contact_stmt = transaction
171            .prepare(
172                "INSERT INTO contacts (name, addr, origin, blocked, last_seen,
173                authname, param, status, is_bot, selfavatar_sent, fingerprint)
174                VALUES(?,?,?,?,?,?,?,?,?,?,?)",
175            )
176            .context("Step 4")?;
177        let mut fingerprint_to_id_stmt = transaction
178            .prepare("SELECT id FROM contacts WHERE fingerprint=? AND id>9")
179            .context("Step 5")?;
180        let mut original_contact_id_from_addr_stmt = transaction
181            .prepare("SELECT id FROM contacts WHERE addr=? AND fingerprint='' AND id>9")
182            .context("Step 6")?;
183
184        for row in all_address_contacts? {
185            let (
186                original_id,
187                name,
188                addr,
189                origin,
190                blocked,
191                last_seen,
192                authname,
193                param,
194                status,
195                is_bot,
196                selfavatar_sent,
197                autocrypt_key,
198                verified_key,
199                verifier,
200                secondary_verified_key,
201                secondary_verifier,
202                prefer_encrypt,
203            ) = row;
204            let mut insert_contact = |key: SignedPublicKey| -> Result<u32> {
205                let fingerprint = key.dc_fingerprint().hex();
206                let existing_contact_id: Option<u32> = fingerprint_to_id_stmt
207                    .query_row((&fingerprint,), |row| row.get(0))
208                    .optional()
209                    .context("Step 7")?;
210                if let Some(existing_contact_id) = existing_contact_id {
211                    return Ok(existing_contact_id);
212                }
213                insert_contact_stmt
214                    .execute((
215                        &name,
216                        &addr,
217                        origin,
218                        blocked,
219                        last_seen,
220                        &authname,
221                        &param,
222                        &status,
223                        is_bot,
224                        selfavatar_sent,
225                        fingerprint.clone(),
226                    ))
227                    .context("Step 8")?;
228                let id = transaction
229                    .last_insert_rowid()
230                    .try_into()
231                    .context("Step 9")?;
232                info!(
233                    context,
234                    "Inserted new contact id={id} name='{name}' addr='{addr}' fingerprint={fingerprint}"
235                );
236                Ok(id)
237            };
238            let mut original_contact_id_from_addr = |addr: &str, default: u32| -> Result<u32> {
239                if addr_cmp(addr, &self_addr) {
240                    Ok(1) // ContactId::SELF
241                } else if addr.is_empty() {
242                    Ok(default)
243                } else {
244                    Ok(original_contact_id_from_addr_stmt
245                        .query_row((addr,), |row| row.get(0))
246                        .optional()
247                        .with_context(|| format!("Original contact '{addr}' not found"))?
248                        .unwrap_or(default))
249                }
250            };
251
252            let Some(autocrypt_key) = autocrypt_key else {
253                continue;
254            };
255            let new_id = insert_contact(autocrypt_key).context("Step 10")?;
256
257            // prefer_encrypt == 20 would mean EncryptPreference::Reset,
258            // i.e. we shouldn't encrypt if possible.
259            if prefer_encrypt != 20 {
260                autocrypt_key_contacts.insert(original_id.try_into().context("Step 11")?, new_id);
261            } else {
262                autocrypt_key_contacts_with_reset_peerstate
263                    .insert(original_id.try_into().context("Step 12")?, new_id);
264            }
265
266            let Some(verified_key) = verified_key else {
267                continue;
268            };
269            let new_id = insert_contact(verified_key).context("Step 13")?;
270            verified_key_contacts.insert(original_id.try_into().context("Step 14")?, new_id);
271
272            let verifier_id = if addr_cmp(&verifier, &addr) {
273                // Earlier versions of Delta Chat signalled a direct verification
274                // by putting the contact's own address into the verifier column
275                1 // 1=ContactId::SELF
276            } else {
277                // If the original verifier is unknown, we represent this in the database
278                // by putting `new_id` into the place of the verifier,
279                // i.e. we say that this contact verified itself.
280                original_contact_id_from_addr(&verifier, new_id).context("Step 15")?
281            };
282            verifications.insert(new_id, verifier_id);
283
284            let Some(secondary_verified_key) = secondary_verified_key else {
285                continue;
286            };
287            let new_id = insert_contact(secondary_verified_key).context("Step 16")?;
288            let verifier_id: u32 = if addr_cmp(&secondary_verifier, &addr) {
289                1 // 1=ContactId::SELF
290            } else {
291                original_contact_id_from_addr(&secondary_verifier, new_id).context("Step 17")?
292            };
293            // Only use secondary verification if there is no primary verification:
294            verifications.entry(new_id).or_insert(verifier_id);
295        }
296        info!(
297            context,
298            "Created key-contacts identified by autocrypt key: {autocrypt_key_contacts:?}"
299        );
300        info!(
301            context,
302            "Created key-contacts  with 'reset' peerstate identified by autocrypt key: {autocrypt_key_contacts_with_reset_peerstate:?}"
303        );
304        info!(
305            context,
306            "Created key-contacts identified by verified key: {verified_key_contacts:?}"
307        );
308
309        for (&new_contact, &verifier_original_contact) in &verifications {
310            let verifier = if verifier_original_contact == 1 {
311                1 // Verified by ContactId::SELF
312            } else if verifier_original_contact == new_contact {
313                new_contact // unkwnown verifier
314            } else {
315                // `verifications` contains the original contact id.
316                // We need to get the new, verified-pgp-identified contact id.
317                match verified_key_contacts.get(&verifier_original_contact) {
318                    Some(v) => *v,
319                    None => {
320                        warn!(
321                            context,
322                            "Couldn't find key-contact for {verifier_original_contact} who verified {new_contact}"
323                        );
324                        continue;
325                    }
326                }
327            };
328            transaction
329                .execute(
330                    "UPDATE contacts SET verifier=? WHERE id=?",
331                    (verifier, new_contact),
332                )
333                .context("Step 18")?;
334        }
335        info!(context, "Migrated verifications: {verifications:?}");
336    }
337
338    // ======================= Step 3: =======================
339    // For each chat, modify the memberlist to retain the correct contacts
340    // In the process, track the set of contacts which remained no any chat at all
341    // in a `BTreeSet<u32>`, which initially contains all contact ids
342    let mut orphaned_contacts: BTreeSet<u32> = transaction
343        .prepare("SELECT id FROM contacts WHERE id>9")
344        .context("Step 19")?
345        .query_map((), |row| row.get::<usize, u32>(0))
346        .context("Step 20")?
347        .collect::<Result<BTreeSet<u32>, rusqlite::Error>>()
348        .context("Step 21")?;
349
350    {
351        let mut stmt = transaction
352            .prepare(
353                "SELECT c.id, c.type, c.grpid, c.protected
354            FROM chats c
355            WHERE id>9",
356            )
357            .context("Step 22")?;
358        let all_chats = stmt
359            .query_map((), |row| {
360                let id: u32 = row.get(0)?;
361                let typ: u32 = row.get(1)?;
362                let grpid: String = row.get(2)?;
363                let protected: u32 = row.get(3)?;
364                Ok((id, typ, grpid, protected))
365            })
366            .context("Step 23")?;
367        let mut load_chat_contacts_stmt = transaction.prepare(
368            "SELECT contact_id, add_timestamp>=remove_timestamp FROM chats_contacts
369             WHERE chat_id=? AND contact_id>9",
370        )?;
371        let is_chatmail: Option<String> = transaction
372            .query_row(
373                "SELECT value FROM config WHERE keyname='is_chatmail'",
374                (),
375                |row| row.get(0),
376            )
377            .optional()
378            .context("Step 23.1")?;
379        let is_chatmail = is_chatmail
380            .and_then(|s| s.parse::<i32>().ok())
381            .unwrap_or_default()
382            != 0;
383        let map_to_key_contact = |old_member: &u32| {
384            autocrypt_key_contacts
385                .get(old_member)
386                .or_else(|| {
387                    // For chatmail servers,
388                    // we send encrypted even if the peerstate is reset,
389                    // because an unencrypted message likely won't arrive.
390                    // This is the same behavior as before key-contacts migration.
391                    if is_chatmail {
392                        autocrypt_key_contacts_with_reset_peerstate.get(old_member)
393                    } else {
394                        None
395                    }
396                })
397                .copied()
398        };
399
400        let mut update_member_stmt = transaction
401            .prepare("UPDATE chats_contacts SET contact_id=? WHERE contact_id=? AND chat_id=?")?;
402        let mut addr_cmp_stmt = transaction
403            .prepare("SELECT c.addr=d.addr FROM contacts c, contacts d WHERE c.id=? AND d.id=?")?;
404        for chat in all_chats {
405            let (chat_id, typ, grpid, protected) = chat.context("Step 24")?;
406            // In groups, this also contains past members, i.e. `(_, false)` entries.
407            let old_members: Vec<(u32, bool)> = load_chat_contacts_stmt
408                .query_map((chat_id,), |row| {
409                    let id: u32 = row.get(0)?;
410                    let present: bool = row.get(1)?;
411                    Ok((id, present))
412                })
413                .context("Step 25")?
414                .collect::<Result<Vec<_>, _>>()
415                .context("Step 26")?;
416
417            let mut keep_address_contacts = |reason: &str| -> Result<()> {
418                info!(
419                    context,
420                    "Chat {chat_id} will be an unencrypted chat with contacts identified by email address: {reason}."
421                );
422                for (m, _) in &old_members {
423                    orphaned_contacts.remove(m);
424                }
425
426                // Unprotect this chat if it was protected.
427                //
428                // Otherwise we get protected chat with address-contact(s).
429                transaction
430                    .execute("UPDATE chats SET protected=0 WHERE id=?", (chat_id,))
431                    .context("Step 26.0")?;
432
433                Ok(())
434            };
435            let old_and_new_members: Vec<(u32, bool, Option<u32>)> = match typ {
436                // 1:1 chats retain:
437                // - address-contact if peerstate is in the "reset" state,
438                //   or if there is no key-contact that has the right email address.
439                // - key-contact identified by the Autocrypt key if Autocrypt key does not match the verified key.
440                // - key-contact identified by the verified key if peerstate Autocrypt key matches the Verified key.
441                //   Since the autocrypt and verified key-contact are identital in this case, we can add the Autocrypt key-contact,
442                //   and the effect will be the same.
443                100 => {
444                    let Some((old_member, _)) = old_members.first() else {
445                        info!(
446                            context,
447                            "1:1 chat {chat_id} doesn't contain contact, probably it's self or device chat."
448                        );
449                        continue;
450                    };
451
452                    let Some(new_contact) = map_to_key_contact(old_member) else {
453                        keep_address_contacts("No peerstate, or peerstate in 'reset' state")?;
454                        continue;
455                    };
456                    if !addr_cmp_stmt
457                        .query_row((old_member, new_contact), |row| row.get::<_, bool>(0))?
458                    {
459                        keep_address_contacts("key contact has different email")?;
460                        continue;
461                    }
462                    vec![(*old_member, true, Some(new_contact))]
463                }
464
465                // Group
466                120 => {
467                    if grpid.is_empty() {
468                        // Ad-hoc group that has empty Chat-Group-ID
469                        // because it was created in response to receiving a non-chat email.
470                        keep_address_contacts("Empty chat-Group-ID")?;
471                        continue;
472                    } else if protected == 1 {
473                        old_members
474                            .iter()
475                            .map(|&(id, present)| {
476                                (id, present, verified_key_contacts.get(&id).copied())
477                            })
478                            .collect()
479                    } else {
480                        old_members
481                            .iter()
482                            .map(|&(id, present)| (id, present, map_to_key_contact(&id)))
483                            .collect::<Vec<(u32, bool, Option<u32>)>>()
484                    }
485                }
486
487                // Mailinglist
488                140 => {
489                    keep_address_contacts("Mailinglist")?;
490                    continue;
491                }
492
493                // Broadcast channel
494                160 => old_members
495                    .iter()
496                    .map(|(original, _)| {
497                        (
498                            *original,
499                            true,
500                            autocrypt_key_contacts
501                                .get(original)
502                                // There will be no unencrypted broadcast lists anymore,
503                                // so, if a peerstate is reset,
504                                // the best we can do is encrypting to this key regardless.
505                                .or_else(|| {
506                                    autocrypt_key_contacts_with_reset_peerstate.get(original)
507                                })
508                                .copied(),
509                        )
510                    })
511                    .collect::<Vec<(u32, bool, Option<u32>)>>(),
512                _ => {
513                    warn!(context, "Invalid chat type {typ}");
514                    continue;
515                }
516            };
517
518            // If a group contains a contact without a key or with 'reset' peerstate,
519            // downgrade to unencrypted Ad-Hoc group.
520            if typ == 120
521                && old_and_new_members
522                    .iter()
523                    .any(|&(_old, present, new)| present && new.is_none())
524            {
525                transaction
526                    .execute("UPDATE chats SET grpid='' WHERE id=?", (chat_id,))
527                    .context("Step 26.1")?;
528                keep_address_contacts("Group contains contact without peerstate")?;
529                continue;
530            }
531
532            let human_readable_transitions = old_and_new_members
533                .iter()
534                .map(|(old, _, new)| format!("{old}->{}", new.unwrap_or_default()))
535                .collect::<Vec<String>>()
536                .join(" ");
537            info!(
538                context,
539                "Migrating chat {chat_id} to key-contacts: {human_readable_transitions}"
540            );
541
542            for (old_member, _, new_member) in old_and_new_members {
543                if let Some(new_member) = new_member {
544                    orphaned_contacts.remove(&new_member);
545                    let res = update_member_stmt.execute((new_member, old_member, chat_id));
546                    if res.is_err() {
547                        // The same chat partner exists multiple times in the chat,
548                        // with mutliple profiles which have different email addresses
549                        // but the same key.
550                        // We can only keep one of them.
551                        // So, if one of them is not in the chat anymore, delete it,
552                        // otherwise delete the one that was added least recently.
553                        let member_to_delete: u32 = transaction
554                            .query_row(
555                                "SELECT contact_id
556                               FROM chats_contacts
557                              WHERE chat_id=? AND contact_id IN (?,?)
558                           ORDER BY add_timestamp>=remove_timestamp, add_timestamp LIMIT 1",
559                                (chat_id, new_member, old_member),
560                                |row| row.get(0),
561                            )
562                            .context("Step 27")?;
563                        info!(
564                            context,
565                            "Chat partner is in the chat {chat_id} multiple times. \
566                            Deleting {member_to_delete}, then trying to update \
567                            {old_member}->{new_member} again"
568                        );
569                        transaction
570                            .execute(
571                                "DELETE FROM chats_contacts WHERE chat_id=? AND contact_id=?",
572                                (chat_id, member_to_delete),
573                            )
574                            .context("Step 28")?;
575                        // If we removed `old_member`, then this will be a no-op,
576                        // which is exactly what we want in this case:
577                        update_member_stmt.execute((new_member, old_member, chat_id))?;
578                    }
579                } else {
580                    info!(
581                        context,
582                        "Old member {old_member} in chat {chat_id} can't be upgraded to key-contact, removing them"
583                    );
584                    transaction
585                        .execute(
586                            "DELETE FROM chats_contacts WHERE contact_id=? AND chat_id=?",
587                            (old_member, chat_id),
588                        )
589                        .context("Step 29")?;
590                }
591            }
592        }
593    }
594
595    // ======================= Step 4: =======================
596    {
597        info!(
598            context,
599            "Marking contacts which remained in no chat at all as hidden: {orphaned_contacts:?}"
600        );
601        let mut mark_as_hidden_stmt = transaction
602            .prepare("UPDATE contacts SET origin=? WHERE id=?")
603            .context("Step 30")?;
604        for contact in orphaned_contacts {
605            mark_as_hidden_stmt
606                .execute((0x8, contact))
607                .context("Step 31")?;
608        }
609    }
610
611    // ======================= Step 5: =======================
612    // Prepare for rewriting `from_id`, `to_id` in messages
613    {
614        let mut contacts_map = autocrypt_key_contacts_with_reset_peerstate;
615        for (old, new) in autocrypt_key_contacts {
616            contacts_map.insert(old, new);
617        }
618        transaction
619            .execute(
620                "CREATE TABLE key_contacts_map (
621                    old_id INTEGER PRIMARY KEY NOT NULL,
622                    new_id INTEGER NOT NULL
623                ) STRICT",
624                (),
625            )
626            .context("Step 32")?;
627        {
628            let mut stmt = transaction
629                .prepare("INSERT INTO key_contacts_map (old_id, new_id) VALUES (?, ?)")
630                .context("Step 33")?;
631            for ids in contacts_map {
632                stmt.execute(ids).context("Step 34")?;
633            }
634        }
635        transaction
636            .execute(
637                "INSERT INTO config (keyname, value) VALUES (
638                    'first_key_contacts_msg_id',
639                    IFNULL((SELECT MAX(id)+1 FROM msgs), 0)
640                )",
641                (),
642            )
643            .context("Step 35")?;
644    }
645
646    Ok(())
647}
648
649/// Rewrite `from_id`, `to_id` in >= 1000 messages starting from the newest ones, to key-contacts.
650#[expect(clippy::arithmetic_side_effects)]
651pub(crate) async fn msgs_to_key_contacts(context: &Context) -> Result<()> {
652    let sql = &context.sql;
653    if sql
654        .get_raw_config_int64("first_key_contacts_msg_id")
655        .await?
656        <= Some(0)
657    {
658        return Ok(());
659    }
660    let trans_fn = |t: &mut rusqlite::Transaction| {
661        let mut first_key_contacts_msg_id: u64 = t
662            .query_one(
663                "SELECT CAST(value AS INTEGER) FROM config WHERE keyname='first_key_contacts_msg_id'",
664                (),
665                |row| row.get(0),
666            )
667            .context("Get first_key_contacts_msg_id")?;
668        let mut stmt = t
669            .prepare(
670                "UPDATE msgs SET
671                    from_id=IFNULL(
672                        (SELECT new_id FROM key_contacts_map WHERE old_id=msgs.from_id),
673                        from_id
674                    ),
675                    to_id=IFNULL(
676                        (SELECT new_id FROM key_contacts_map WHERE old_id=msgs.to_id),
677                        to_id
678                    )
679                WHERE id>=? AND id<?
680                AND chat_id>9
681                AND (param GLOB '*\nc=1*' OR param GLOB 'c=1*')",
682            )
683            .context("Prepare stmt")?;
684        let msgs_to_migrate = 1000;
685        let mut msgs_migrated: u64 = 0;
686        while first_key_contacts_msg_id > 0 && msgs_migrated < msgs_to_migrate {
687            let start_msg_id = first_key_contacts_msg_id.saturating_sub(msgs_to_migrate);
688            let cnt: u64 = stmt
689                .execute((start_msg_id, first_key_contacts_msg_id))
690                .context("UPDATE msgs")?
691                .try_into()?;
692            msgs_migrated += cnt;
693            first_key_contacts_msg_id = start_msg_id;
694        }
695        t.execute(
696            "UPDATE config SET value=? WHERE keyname='first_key_contacts_msg_id'",
697            (first_key_contacts_msg_id,),
698        )
699        .context("Update first_key_contacts_msg_id")?;
700        Ok((msgs_migrated, first_key_contacts_msg_id))
701    };
702    let start = Time::now();
703    let mut msgs_migrated = 0;
704    loop {
705        let (n, first_key_contacts_msg_id) = sql.transaction(trans_fn).await?;
706        msgs_migrated += n;
707        if first_key_contacts_msg_id == 0 || time_elapsed(&start) >= Duration::from_millis(500) {
708            break;
709        }
710    }
711    sql.uncache_raw_config("first_key_contacts_msg_id").await;
712    info!(
713        context,
714        "Rewriting {msgs_migrated} msgs to key-contacts took {:?}.",
715        time_elapsed(&start),
716    );
717    Ok(())
718}
719
720impl Sql {
721    async fn set_db_version(&self, version: i32) -> Result<()> {
722        self.set_raw_config_int(VERSION_CFG, version).await?;
723        Ok(())
724    }
725
726    // Sets db `version` in the `transaction`.
727    fn set_db_version_trans(transaction: &mut rusqlite::Transaction, version: i32) -> Result<()> {
728        transaction.execute(
729            "UPDATE config SET value=? WHERE keyname=?;",
730            (format!("{version}"), VERSION_CFG),
731        )?;
732        Ok(())
733    }
734
735    async fn execute_migration(&self, query: &str, version: i32) -> Result<()> {
736        self.execute_migration_transaction(
737            |transaction| {
738                transaction.execute_batch(query)?;
739                Ok(())
740            },
741            version,
742        )
743        .await
744    }
745
746    async fn execute_migration_transaction(
747        &self,
748        migration: impl Send + FnOnce(&mut rusqlite::Transaction) -> Result<()>,
749        version: i32,
750    ) -> Result<()> {
751        #[cfg(test)]
752        if STOP_MIGRATIONS_AT.try_with(|stop_migrations_at| version > *stop_migrations_at)
753            == Ok(true)
754        {
755            println!("Not running migration {version}, because STOP_MIGRATIONS_AT is set");
756            return Ok(());
757        }
758
759        self.transaction(move |transaction| {
760            let curr_version: String = transaction.query_row(
761                "SELECT IFNULL(value, ?) FROM config WHERE keyname=?;",
762                ("0", VERSION_CFG),
763                |row| row.get(0),
764            )?;
765            let curr_version: i32 = curr_version.parse()?;
766            ensure!(curr_version < version, "Db version must be increased");
767            Self::set_db_version_trans(transaction, version)?;
768            migration(transaction)?;
769
770            Ok(())
771        })
772        .await
773        .with_context(|| format!("execute_migration failed for version {version}"))?;
774
775        self.config_cache.write().await.clear();
776
777        Ok(())
778    }
779}
780
781#[expect(clippy::arithmetic_side_effects)]
782pub async fn run(context: &Context, sql: &Sql) -> Result<bool> {
783    let mut exists_before_update = false;
784    let mut dbversion_before_update = DBVERSION;
785
786    if !sql
787        .table_exists("config")
788        .await
789        .context("failed to check if config table exists")?
790    {
791        sql.transaction(move |transaction| {
792            transaction.execute_batch(TABLES)?;
793
794            // set raw config inside the transaction
795            transaction.execute(
796                "INSERT INTO config (keyname, value) VALUES (?, ?);",
797                (VERSION_CFG, format!("{dbversion_before_update}")),
798            )?;
799            Ok(())
800        })
801        .await
802        .context("Creating tables failed")?;
803
804        let mut lock = context.sql.config_cache.write().await;
805        lock.insert(
806            VERSION_CFG.to_string(),
807            Some(format!("{dbversion_before_update}")),
808        );
809        drop(lock);
810    } else {
811        exists_before_update = true;
812        dbversion_before_update = sql
813            .get_raw_config_int(VERSION_CFG)
814            .await?
815            .unwrap_or_default();
816    }
817
818    let dbversion = dbversion_before_update;
819    let mut recode_avatar = false;
820
821    if dbversion < 1 {
822        sql.execute_migration(
823            r#"
824CREATE TABLE leftgrps ( id INTEGER PRIMARY KEY, grpid TEXT DEFAULT '');
825CREATE INDEX leftgrps_index1 ON leftgrps (grpid);"#,
826            1,
827        )
828        .await?;
829    }
830    if dbversion < 2 {
831        sql.execute_migration(
832            "ALTER TABLE contacts ADD COLUMN authname TEXT DEFAULT '';",
833            2,
834        )
835        .await?;
836    }
837    if dbversion < 7 {
838        sql.execute_migration(
839            "CREATE TABLE keypairs (\
840                 id INTEGER PRIMARY KEY, \
841                 addr TEXT DEFAULT '' COLLATE NOCASE, \
842                 is_default INTEGER DEFAULT 0, \
843                 private_key, \
844                 public_key, \
845                 created INTEGER DEFAULT 0);",
846            7,
847        )
848        .await?;
849    }
850    if dbversion < 10 {
851        sql.execute_migration(
852            "CREATE TABLE acpeerstates (\
853                 id INTEGER PRIMARY KEY, \
854                 addr TEXT DEFAULT '' COLLATE NOCASE, \
855                 last_seen INTEGER DEFAULT 0, \
856                 last_seen_autocrypt INTEGER DEFAULT 0, \
857                 public_key, \
858                 prefer_encrypted INTEGER DEFAULT 0); \
859              CREATE INDEX acpeerstates_index1 ON acpeerstates (addr);",
860            10,
861        )
862        .await?;
863    }
864    if dbversion < 12 {
865        sql.execute_migration(
866            r#"
867CREATE TABLE msgs_mdns ( msg_id INTEGER,  contact_id INTEGER);
868CREATE INDEX msgs_mdns_index1 ON msgs_mdns (msg_id);"#,
869            12,
870        )
871        .await?;
872    }
873    if dbversion < 17 {
874        sql.execute_migration(
875            r#"
876ALTER TABLE chats ADD COLUMN archived INTEGER DEFAULT 0;
877CREATE INDEX chats_index2 ON chats (archived);
878ALTER TABLE msgs ADD COLUMN starred INTEGER DEFAULT 0;
879CREATE INDEX msgs_index5 ON msgs (starred);"#,
880            17,
881        )
882        .await?;
883    }
884    if dbversion < 18 {
885        sql.execute_migration(
886            r#"
887ALTER TABLE acpeerstates ADD COLUMN gossip_timestamp INTEGER DEFAULT 0;
888ALTER TABLE acpeerstates ADD COLUMN gossip_key;"#,
889            18,
890        )
891        .await?;
892    }
893    if dbversion < 27 {
894        // chat.id=1 and chat.id=2 are the old deaddrops,
895        // the current ones are defined by chats.blocked=2
896        sql.execute_migration(
897            r#"
898DELETE FROM msgs WHERE chat_id=1 OR chat_id=2;
899CREATE INDEX chats_contacts_index2 ON chats_contacts (contact_id);
900ALTER TABLE msgs ADD COLUMN timestamp_sent INTEGER DEFAULT 0;
901ALTER TABLE msgs ADD COLUMN timestamp_rcvd INTEGER DEFAULT 0;"#,
902            27,
903        )
904        .await?;
905    }
906    if dbversion < 34 {
907        sql.execute_migration(
908            r#"
909ALTER TABLE msgs ADD COLUMN hidden INTEGER DEFAULT 0;
910ALTER TABLE msgs_mdns ADD COLUMN timestamp_sent INTEGER DEFAULT 0;
911ALTER TABLE acpeerstates ADD COLUMN public_key_fingerprint TEXT DEFAULT '';
912ALTER TABLE acpeerstates ADD COLUMN gossip_key_fingerprint TEXT DEFAULT '';
913CREATE INDEX acpeerstates_index3 ON acpeerstates (public_key_fingerprint);
914CREATE INDEX acpeerstates_index4 ON acpeerstates (gossip_key_fingerprint);"#,
915            34,
916        )
917        .await?;
918    }
919    if dbversion < 39 {
920        sql.execute_migration(
921            r#"
922CREATE TABLE tokens ( 
923  id INTEGER PRIMARY KEY, 
924  namespc INTEGER DEFAULT 0, 
925  foreign_id INTEGER DEFAULT 0, 
926  token TEXT DEFAULT '', 
927  timestamp INTEGER DEFAULT 0
928);
929ALTER TABLE acpeerstates ADD COLUMN verified_key;
930ALTER TABLE acpeerstates ADD COLUMN verified_key_fingerprint TEXT DEFAULT '';
931CREATE INDEX acpeerstates_index5 ON acpeerstates (verified_key_fingerprint);"#,
932            39,
933        )
934        .await?;
935    }
936    if dbversion < 40 {
937        sql.execute_migration("ALTER TABLE jobs ADD COLUMN thread INTEGER DEFAULT 0;", 40)
938            .await?;
939    }
940    if dbversion < 44 {
941        sql.execute_migration("ALTER TABLE msgs ADD COLUMN mime_headers TEXT;", 44)
942            .await?;
943    }
944    if dbversion < 46 {
945        sql.execute_migration(
946            r#"
947ALTER TABLE msgs ADD COLUMN mime_in_reply_to TEXT;
948ALTER TABLE msgs ADD COLUMN mime_references TEXT;"#,
949            46,
950        )
951        .await?;
952    }
953    if dbversion < 47 {
954        sql.execute_migration("ALTER TABLE jobs ADD COLUMN tries INTEGER DEFAULT 0;", 47)
955            .await?;
956    }
957    if dbversion < 48 {
958        // NOTE: move_state is not used anymore
959        sql.execute_migration(
960            "ALTER TABLE msgs ADD COLUMN move_state INTEGER DEFAULT 1;",
961            48,
962        )
963        .await?;
964    }
965    if dbversion < 49 {
966        sql.execute_migration(
967            "ALTER TABLE chats ADD COLUMN gossiped_timestamp INTEGER DEFAULT 0;",
968            49,
969        )
970        .await?;
971    }
972    if dbversion < 50 {
973        // installations <= 0.100.1 used DC_SHOW_EMAILS_ALL implicitly;
974        // keep this default and use DC_SHOW_EMAILS_NO
975        // only for new installations
976        if exists_before_update {
977            sql.set_raw_config_int("show_emails", 2).await?;
978        }
979        sql.set_db_version(50).await?;
980    }
981    if dbversion < 53 {
982        // the messages containing _only_ locations
983        // are also added to the database as _hidden_.
984        sql.execute_migration(
985            r#"
986CREATE TABLE locations ( 
987  id INTEGER PRIMARY KEY AUTOINCREMENT, 
988  latitude REAL DEFAULT 0.0, 
989  longitude REAL DEFAULT 0.0, 
990  accuracy REAL DEFAULT 0.0, 
991  timestamp INTEGER DEFAULT 0, 
992  chat_id INTEGER DEFAULT 0, 
993  from_id INTEGER DEFAULT 0
994);"
995CREATE INDEX locations_index1 ON locations (from_id);
996CREATE INDEX locations_index2 ON locations (timestamp);
997ALTER TABLE chats ADD COLUMN locations_send_begin INTEGER DEFAULT 0;
998ALTER TABLE chats ADD COLUMN locations_send_until INTEGER DEFAULT 0;
999ALTER TABLE chats ADD COLUMN locations_last_sent INTEGER DEFAULT 0;
1000CREATE INDEX chats_index3 ON chats (locations_send_until);"#,
1001            53,
1002        )
1003        .await?;
1004    }
1005    if dbversion < 54 {
1006        sql.execute_migration(
1007            r#"
1008ALTER TABLE msgs ADD COLUMN location_id INTEGER DEFAULT 0;
1009CREATE INDEX msgs_index6 ON msgs (location_id);"#,
1010            54,
1011        )
1012        .await?;
1013    }
1014    if dbversion < 55 {
1015        sql.execute_migration(
1016            "ALTER TABLE locations ADD COLUMN independent INTEGER DEFAULT 0;",
1017            55,
1018        )
1019        .await?;
1020    }
1021    if dbversion < 59 {
1022        // records in the devmsglabels are kept when the message is deleted.
1023        // so, msg_id may or may not exist.
1024        sql.execute_migration(
1025            r#"
1026CREATE TABLE devmsglabels (id INTEGER PRIMARY KEY AUTOINCREMENT, label TEXT, msg_id INTEGER DEFAULT 0);
1027CREATE INDEX devmsglabels_index1 ON devmsglabels (label);"#, 59)
1028            .await?;
1029        if exists_before_update && sql.get_raw_config_int("bcc_self").await?.is_none() {
1030            sql.set_raw_config_int("bcc_self", 1).await?;
1031        }
1032    }
1033
1034    if dbversion < 60 {
1035        sql.execute_migration(
1036            "ALTER TABLE chats ADD COLUMN created_timestamp INTEGER DEFAULT 0;",
1037            60,
1038        )
1039        .await?;
1040    }
1041    if dbversion < 61 {
1042        sql.execute_migration(
1043            "ALTER TABLE contacts ADD COLUMN selfavatar_sent INTEGER DEFAULT 0;",
1044            61,
1045        )
1046        .await?;
1047    }
1048    if dbversion < 62 {
1049        sql.execute_migration(
1050            "ALTER TABLE chats ADD COLUMN muted_until INTEGER DEFAULT 0;",
1051            62,
1052        )
1053        .await?;
1054    }
1055    if dbversion < 63 {
1056        sql.execute_migration("UPDATE chats SET grpid='' WHERE type=100", 63)
1057            .await?;
1058    }
1059    if dbversion < 64 {
1060        sql.execute_migration("ALTER TABLE msgs ADD COLUMN error TEXT DEFAULT '';", 64)
1061            .await?;
1062    }
1063    if dbversion < 65 {
1064        sql.execute_migration(
1065            r#"
1066ALTER TABLE chats ADD COLUMN ephemeral_timer INTEGER;
1067ALTER TABLE msgs ADD COLUMN ephemeral_timer INTEGER DEFAULT 0;
1068ALTER TABLE msgs ADD COLUMN ephemeral_timestamp INTEGER DEFAULT 0;"#,
1069            65,
1070        )
1071        .await?;
1072    }
1073    if dbversion < 66 {
1074        sql.set_db_version(66).await?;
1075    }
1076    if dbversion < 67 {
1077        for prefix in &["", "configured_"] {
1078            if let Some(server_flags) = sql
1079                .get_raw_config_int(&format!("{prefix}server_flags"))
1080                .await?
1081            {
1082                let imap_socket_flags = server_flags & 0x700;
1083                let key = &format!("{prefix}mail_security");
1084                match imap_socket_flags {
1085                    0x100 => sql.set_raw_config_int(key, 2).await?, // STARTTLS
1086                    0x200 => sql.set_raw_config_int(key, 1).await?, // SSL/TLS
1087                    0x400 => sql.set_raw_config_int(key, 3).await?, // Plain
1088                    _ => sql.set_raw_config_int(key, 0).await?,
1089                }
1090                let smtp_socket_flags = server_flags & 0x70000;
1091                let key = &format!("{prefix}send_security");
1092                match smtp_socket_flags {
1093                    0x10000 => sql.set_raw_config_int(key, 2).await?, // STARTTLS
1094                    0x20000 => sql.set_raw_config_int(key, 1).await?, // SSL/TLS
1095                    0x40000 => sql.set_raw_config_int(key, 3).await?, // Plain
1096                    _ => sql.set_raw_config_int(key, 0).await?,
1097                }
1098            }
1099        }
1100        sql.set_db_version(67).await?;
1101    }
1102    if dbversion < 68 {
1103        // the index is used to speed up get_fresh_msg_cnt() (see comment there for more details) and marknoticed_chat()
1104        sql.execute_migration(
1105            "CREATE INDEX IF NOT EXISTS msgs_index7 ON msgs (state, hidden, chat_id);",
1106            68,
1107        )
1108        .await?;
1109    }
1110    if dbversion < 69 {
1111        sql.execute_migration(
1112            r#"
1113ALTER TABLE chats ADD COLUMN protected INTEGER DEFAULT 0;
1114-- 120=group, 130=old verified group
1115UPDATE chats SET protected=1, type=120 WHERE type=130;"#,
1116            69,
1117        )
1118        .await?;
1119    }
1120
1121    if dbversion < 71 {
1122        if let Ok(addr) = context.get_primary_self_addr().await {
1123            if let Ok(domain) = EmailAddress::new(&addr).map(|email| email.domain) {
1124                context
1125                    .set_config_internal(
1126                        Config::ConfiguredProvider,
1127                        get_provider_info(&domain).map(|provider| provider.id),
1128                    )
1129                    .await?;
1130            } else {
1131                warn!(context, "Can't parse configured address: {:?}", addr);
1132            }
1133        }
1134
1135        sql.set_db_version(71).await?;
1136    }
1137    if dbversion < 72 && !sql.col_exists("msgs", "mime_modified").await? {
1138        sql.execute_migration(
1139            r#"
1140    ALTER TABLE msgs ADD COLUMN mime_modified INTEGER DEFAULT 0;"#,
1141            72,
1142        )
1143        .await?;
1144    }
1145    if dbversion < 73 {
1146        sql.execute(
1147            r#"
1148CREATE TABLE imap_sync (folder TEXT PRIMARY KEY, uidvalidity INTEGER DEFAULT 0, uid_next INTEGER DEFAULT 0);"#,
1149()
1150        )
1151            .await?;
1152        for c in [
1153            "configured_inbox_folder",
1154            "configured_sentbox_folder",
1155            "configured_mvbox_folder",
1156        ] {
1157            if let Some(folder) = context.sql.get_raw_config(c).await? {
1158                let key = format!("imap.mailbox.{folder}");
1159
1160                let (uid_validity, last_seen_uid) =
1161                    if let Some(entry) = context.sql.get_raw_config(&key).await? {
1162                        // the entry has the format `imap.mailbox.<folder>=<uidvalidity>:<lastseenuid>`
1163                        let mut parts = entry.split(':');
1164                        (
1165                            parts.next().unwrap_or_default().parse().unwrap_or(0),
1166                            parts.next().unwrap_or_default().parse().unwrap_or(0),
1167                        )
1168                    } else {
1169                        (0, 0)
1170                    };
1171                if last_seen_uid > 0 {
1172                    context
1173                        .sql
1174                        .execute(
1175                            "INSERT INTO imap_sync (folder, uid_next) VALUES (?,?)
1176                             ON CONFLICT(folder) DO UPDATE SET uid_next=excluded.uid_next",
1177                            (&folder, last_seen_uid + 1),
1178                        )
1179                        .await?;
1180                    context
1181                        .sql
1182                        .execute(
1183                            "INSERT INTO imap_sync (folder, uidvalidity) VALUES (?,?)
1184                             ON CONFLICT(folder) DO UPDATE SET uidvalidity=excluded.uidvalidity",
1185                            (&folder, uid_validity),
1186                        )
1187                        .await?;
1188                }
1189            }
1190        }
1191        sql.set_db_version(73).await?;
1192    }
1193    if dbversion < 74 {
1194        sql.execute_migration("UPDATE contacts SET name='' WHERE name=authname", 74)
1195            .await?;
1196    }
1197    if dbversion < 75 {
1198        sql.execute_migration(
1199            "ALTER TABLE contacts ADD COLUMN status TEXT DEFAULT '';",
1200            75,
1201        )
1202        .await?;
1203    }
1204    if dbversion < 76 {
1205        sql.execute_migration("ALTER TABLE msgs ADD COLUMN subject TEXT DEFAULT '';", 76)
1206            .await?;
1207    }
1208    if dbversion < 77 {
1209        recode_avatar = true;
1210        sql.set_db_version(77).await?;
1211    }
1212    if dbversion < 78 {
1213        // move requests to "Archived Chats",
1214        // this way, the app looks familiar after the contact request upgrade.
1215        sql.execute_migration("UPDATE chats SET archived=1 WHERE blocked=2;", 78)
1216            .await?;
1217    }
1218    if dbversion < 79 {
1219        sql.execute_migration(
1220            r#"
1221        ALTER TABLE msgs ADD COLUMN download_state INTEGER DEFAULT 0;
1222        "#,
1223            79,
1224        )
1225        .await?;
1226    }
1227    if dbversion < 80 {
1228        sql.execute_migration(
1229            r#"CREATE TABLE multi_device_sync (
1230id INTEGER PRIMARY KEY AUTOINCREMENT,
1231item TEXT DEFAULT '');"#,
1232            80,
1233        )
1234        .await?;
1235    }
1236    if dbversion < 81 {
1237        sql.execute_migration("ALTER TABLE msgs ADD COLUMN hop_info TEXT;", 81)
1238            .await?;
1239    }
1240    if dbversion < 82 {
1241        sql.execute_migration(
1242            r#"CREATE TABLE imap (
1243id INTEGER PRIMARY KEY AUTOINCREMENT,
1244rfc724_mid TEXT DEFAULT '', -- Message-ID header
1245folder TEXT DEFAULT '', -- IMAP folder
1246target TEXT DEFAULT '', -- Destination folder, empty to delete.
1247uid INTEGER DEFAULT 0, -- UID
1248uidvalidity INTEGER DEFAULT 0,
1249UNIQUE (folder, uid, uidvalidity)
1250);
1251CREATE INDEX imap_folder ON imap(folder);
1252CREATE INDEX imap_messageid ON imap(rfc724_mid);
1253
1254INSERT INTO imap
1255(rfc724_mid, folder, target, uid, uidvalidity)
1256SELECT
1257rfc724_mid,
1258server_folder AS folder,
1259server_folder AS target,
1260server_uid AS uid,
1261(SELECT uidvalidity FROM imap_sync WHERE folder=server_folder) AS uidvalidity
1262FROM msgs
1263WHERE server_uid>0
1264ON CONFLICT (folder, uid, uidvalidity)
1265DO UPDATE SET rfc724_mid=excluded.rfc724_mid,
1266              target=excluded.target;
1267"#,
1268            82,
1269        )
1270        .await?;
1271    }
1272    if dbversion < 83 {
1273        sql.execute_migration(
1274            "ALTER TABLE imap_sync
1275             ADD COLUMN modseq -- Highest modification sequence
1276             INTEGER DEFAULT 0",
1277            83,
1278        )
1279        .await?;
1280    }
1281    if dbversion < 84 {
1282        sql.execute_migration(
1283            r#"CREATE TABLE msgs_status_updates (
1284id INTEGER PRIMARY KEY AUTOINCREMENT,
1285msg_id INTEGER,
1286update_item TEXT DEFAULT '',
1287update_item_read INTEGER DEFAULT 0 -- XXX unused
1288);
1289CREATE INDEX msgs_status_updates_index1 ON msgs_status_updates (msg_id);"#,
1290            84,
1291        )
1292        .await?;
1293    }
1294    if dbversion < 85 {
1295        sql.execute_migration(
1296            r#"CREATE TABLE smtp (
1297id INTEGER PRIMARY KEY,
1298rfc724_mid TEXT NOT NULL,          -- Message-ID
1299mime TEXT NOT NULL,                -- SMTP payload
1300msg_id INTEGER NOT NULL,           -- ID of the message in `msgs` table
1301recipients TEXT NOT NULL,          -- List of recipients separated by space
1302retries INTEGER NOT NULL DEFAULT 0 -- Number of failed attempts to send the message
1303);
1304CREATE INDEX smtp_messageid ON imap(rfc724_mid);
1305"#,
1306            85,
1307        )
1308        .await?;
1309    }
1310    if dbversion < 86 {
1311        sql.execute_migration(
1312            r#"CREATE TABLE bobstate (
1313                   id INTEGER PRIMARY KEY AUTOINCREMENT,
1314                   invite TEXT NOT NULL,
1315                   next_step INTEGER NOT NULL,
1316                   chat_id INTEGER NOT NULL
1317            );"#,
1318            86,
1319        )
1320        .await?;
1321    }
1322    if dbversion < 87 {
1323        // the index is used to speed up delete_expired_messages()
1324        sql.execute_migration(
1325            "CREATE INDEX IF NOT EXISTS msgs_index8 ON msgs (ephemeral_timestamp);",
1326            87,
1327        )
1328        .await?;
1329    }
1330    if dbversion < 88 {
1331        sql.execute_migration("DROP TABLE IF EXISTS backup_blobs;", 88)
1332            .await?;
1333    }
1334    if dbversion < 89 {
1335        sql.execute_migration(
1336            r#"CREATE TABLE imap_markseen (
1337              id INTEGER,
1338              FOREIGN KEY(id) REFERENCES imap(id) ON DELETE CASCADE
1339            );"#,
1340            89,
1341        )
1342        .await?;
1343    }
1344    if dbversion < 90 {
1345        sql.execute_migration(
1346            r#"CREATE TABLE smtp_mdns (
1347              msg_id INTEGER NOT NULL, -- id of the message in msgs table which requested MDN (DEPRECATED 2024-06-21)
1348              from_id INTEGER NOT NULL, -- id of the contact that sent the message, MDN destination
1349              rfc724_mid TEXT NOT NULL, -- Message-ID header
1350              retries INTEGER NOT NULL DEFAULT 0 -- Number of failed attempts to send MDN
1351            );"#,
1352            90,
1353        )
1354        .await?;
1355    }
1356    if dbversion < 91 {
1357        sql.execute_migration(
1358            r#"CREATE TABLE smtp_status_updates (
1359              msg_id INTEGER NOT NULL UNIQUE, -- msg_id of the webxdc instance with pending updates
1360              first_serial INTEGER NOT NULL, -- id in msgs_status_updates
1361              last_serial INTEGER NOT NULL, -- id in msgs_status_updates
1362              descr TEXT NOT NULL -- text to send along with the updates
1363            );"#,
1364            91,
1365        )
1366        .await?;
1367    }
1368    if dbversion < 92 {
1369        sql.execute_migration(
1370            r#"CREATE TABLE reactions (
1371              msg_id INTEGER NOT NULL, -- id of the message reacted to
1372              contact_id INTEGER NOT NULL, -- id of the contact reacting to the message
1373              reaction TEXT DEFAULT '' NOT NULL, -- a sequence of emojis separated by spaces
1374              PRIMARY KEY(msg_id, contact_id),
1375              FOREIGN KEY(msg_id) REFERENCES msgs(id) ON DELETE CASCADE -- delete reactions when message is deleted
1376              FOREIGN KEY(contact_id) REFERENCES contacts(id) ON DELETE CASCADE -- delete reactions when contact is deleted
1377            )"#,
1378            92
1379        ).await?;
1380    }
1381    if dbversion < 93 {
1382        // `sending_domains` is now unused, but was not removed for backwards compatibility.
1383        sql.execute_migration(
1384            "CREATE TABLE sending_domains(domain TEXT PRIMARY KEY, dkim_works INTEGER DEFAULT 0);",
1385            93,
1386        )
1387        .await?;
1388    }
1389    if dbversion < 94 {
1390        sql.execute_migration(
1391            // Create new `acpeerstates` table, same as before but with unique constraint.
1392            //
1393            // This allows to use `UPSERT` to update existing or insert a new peerstate
1394            // depending on whether one exists already.
1395            "CREATE TABLE new_acpeerstates (
1396             id INTEGER PRIMARY KEY,
1397             addr TEXT DEFAULT '' COLLATE NOCASE,
1398             last_seen INTEGER DEFAULT 0,
1399             last_seen_autocrypt INTEGER DEFAULT 0,
1400             public_key,
1401             prefer_encrypted INTEGER DEFAULT 0,
1402             gossip_timestamp INTEGER DEFAULT 0,
1403             gossip_key,
1404             public_key_fingerprint TEXT DEFAULT '',
1405             gossip_key_fingerprint TEXT DEFAULT '',
1406             verified_key,
1407             verified_key_fingerprint TEXT DEFAULT '',
1408             UNIQUE (addr) -- Only one peerstate per address
1409             );
1410            INSERT OR IGNORE INTO new_acpeerstates SELECT
1411                id, addr, last_seen, last_seen_autocrypt, public_key, prefer_encrypted,
1412                gossip_timestamp, gossip_key, public_key_fingerprint,
1413                gossip_key_fingerprint, verified_key, verified_key_fingerprint
1414            FROM acpeerstates;
1415            DROP TABLE acpeerstates;
1416            ALTER TABLE new_acpeerstates RENAME TO acpeerstates;
1417            CREATE INDEX acpeerstates_index1 ON acpeerstates (addr);
1418            CREATE INDEX acpeerstates_index3 ON acpeerstates (public_key_fingerprint);
1419            CREATE INDEX acpeerstates_index4 ON acpeerstates (gossip_key_fingerprint);
1420            CREATE INDEX acpeerstates_index5 ON acpeerstates (verified_key_fingerprint);
1421            ",
1422            94,
1423        )
1424        .await?;
1425    }
1426    if dbversion < 95 {
1427        sql.execute_migration(
1428            "CREATE TABLE new_chats_contacts (chat_id INTEGER, contact_id INTEGER, UNIQUE(chat_id, contact_id));\
1429            INSERT OR IGNORE INTO new_chats_contacts SELECT chat_id, contact_id FROM chats_contacts;\
1430            DROP TABLE chats_contacts;\
1431            ALTER TABLE new_chats_contacts RENAME TO chats_contacts;\
1432            CREATE INDEX chats_contacts_index1 ON chats_contacts (chat_id);\
1433            CREATE INDEX chats_contacts_index2 ON chats_contacts (contact_id);",
1434            95
1435        ).await?;
1436    }
1437    if dbversion < 96 {
1438        sql.execute_migration(
1439            "ALTER TABLE acpeerstates ADD COLUMN verifier TEXT DEFAULT '';",
1440            96,
1441        )
1442        .await?;
1443    }
1444    if dbversion < 97 {
1445        sql.execute_migration(
1446            "CREATE TABLE dns_cache (
1447               hostname TEXT NOT NULL,
1448               address TEXT NOT NULL, -- IPv4 or IPv6 address
1449               timestamp INTEGER NOT NULL,
1450               UNIQUE (hostname, address)
1451             )",
1452            97,
1453        )
1454        .await?;
1455    }
1456    if dbversion < 98 {
1457        if exists_before_update && sql.get_raw_config_int("show_emails").await?.is_none() {
1458            sql.set_raw_config_int("show_emails", 0).await?;
1459        }
1460        sql.set_db_version(98).await?;
1461    }
1462    if dbversion < 99 {
1463        // sql.execute_migration(
1464        //     "ALTER TABLE msgs DROP COLUMN server_folder;
1465        //      ALTER TABLE msgs DROP COLUMN server_uid;
1466        //      ALTER TABLE msgs DROP COLUMN move_state;
1467        //      ALTER TABLE chats DROP COLUMN draft_timestamp;
1468        //      ALTER TABLE chats DROP COLUMN draft_txt",
1469        //     99,
1470        // )
1471        // .await?;
1472
1473        // Reverted above, as it requires to load the whole DB in memory.
1474        sql.set_db_version(99).await?;
1475    }
1476    if dbversion < 100 {
1477        sql.execute_migration(
1478            "ALTER TABLE msgs ADD COLUMN mime_compressed INTEGER NOT NULL DEFAULT 0",
1479            100,
1480        )
1481        .await?;
1482    }
1483    if dbversion < 101 {
1484        // Recreate `smtp` table with autoincrement.
1485        // rfc724_mid index is not recreated, because it is not used.
1486        sql.execute_migration(
1487            "DROP TABLE smtp;
1488             CREATE TABLE smtp (
1489             id INTEGER PRIMARY KEY AUTOINCREMENT,
1490             rfc724_mid TEXT NOT NULL,          -- Message-ID
1491             mime TEXT NOT NULL,                -- SMTP payload
1492             msg_id INTEGER NOT NULL,           -- ID of the message in `msgs` table
1493             recipients TEXT NOT NULL,          -- List of recipients separated by space
1494             retries INTEGER NOT NULL DEFAULT 0 -- Number of failed attempts to send the message
1495            );
1496            ",
1497            101,
1498        )
1499        .await?;
1500    }
1501
1502    if dbversion < 102 {
1503        sql.execute_migration(
1504            "CREATE TABLE download (
1505            msg_id INTEGER NOT NULL -- id of the message stub in msgs table
1506            )",
1507            102,
1508        )
1509        .await?;
1510    }
1511
1512    // Add is_bot column to contacts table with default false.
1513    if dbversion < 103 {
1514        sql.execute_migration(
1515            "ALTER TABLE contacts ADD COLUMN is_bot INTEGER NOT NULL DEFAULT 0",
1516            103,
1517        )
1518        .await?;
1519    }
1520
1521    if dbversion < 104 {
1522        sql.execute_migration(
1523            "ALTER TABLE acpeerstates
1524             ADD COLUMN secondary_verified_key;
1525             ALTER TABLE acpeerstates
1526             ADD COLUMN secondary_verified_key_fingerprint TEXT DEFAULT '';
1527             ALTER TABLE acpeerstates
1528             ADD COLUMN secondary_verifier TEXT DEFAULT ''",
1529            104,
1530        )
1531        .await?;
1532    }
1533
1534    if dbversion < 105 {
1535        // Create UNIQUE uid column and drop unused update_item_read column.
1536        sql.execute_migration(
1537            r#"CREATE TABLE new_msgs_status_updates (
1538id INTEGER PRIMARY KEY AUTOINCREMENT,
1539msg_id INTEGER,
1540update_item TEXT DEFAULT '',
1541uid TEXT UNIQUE
1542);
1543INSERT OR IGNORE INTO new_msgs_status_updates SELECT
1544  id, msg_id, update_item, NULL
1545FROM msgs_status_updates;
1546DROP TABLE msgs_status_updates;
1547ALTER TABLE new_msgs_status_updates RENAME TO msgs_status_updates;
1548CREATE INDEX msgs_status_updates_index1 ON msgs_status_updates (msg_id);
1549CREATE INDEX msgs_status_updates_index2 ON msgs_status_updates (uid);
1550"#,
1551            105,
1552        )
1553        .await?;
1554    }
1555
1556    if dbversion < 106 {
1557        // Recreate `config` table with UNIQUE constraint on `keyname`.
1558        sql.execute_migration(
1559            "CREATE TABLE new_config (
1560               id INTEGER PRIMARY KEY,
1561               keyname TEXT UNIQUE,
1562               value TEXT NOT NULL
1563             );
1564             INSERT OR IGNORE INTO new_config SELECT
1565               id, keyname, value
1566             FROM config;
1567             DROP TABLE config;
1568             ALTER TABLE new_config RENAME TO config;
1569             CREATE INDEX config_index1 ON config (keyname);",
1570            106,
1571        )
1572        .await?;
1573    }
1574
1575    if dbversion < 107 {
1576        sql.execute_migration(
1577            "CREATE TABLE new_keypairs (
1578               id INTEGER PRIMARY KEY AUTOINCREMENT,
1579               private_key UNIQUE NOT NULL,
1580               public_key UNIQUE NOT NULL
1581             );
1582             INSERT OR IGNORE INTO new_keypairs SELECT id, private_key, public_key FROM keypairs;
1583
1584             INSERT OR IGNORE
1585             INTO config (keyname, value)
1586             VALUES
1587             ('key_id', (SELECT id FROM new_keypairs
1588                         WHERE private_key=
1589                           (SELECT private_key FROM keypairs
1590                            WHERE addr=(SELECT value FROM config WHERE keyname='configured_addr')
1591                            AND is_default=1)));
1592
1593             -- We do not drop the old `keypairs` table for now,
1594             -- but move it to `old_keypairs`. We can remove it later
1595             -- in next migrations. This may be needed for recovery
1596             -- in case something is wrong with the migration.
1597             ALTER TABLE keypairs RENAME TO old_keypairs;
1598             ALTER TABLE new_keypairs RENAME TO keypairs;
1599             ",
1600            107,
1601        )
1602        .await?;
1603    }
1604
1605    // Migration 108 is removed, it was using high level code
1606    // to split SMTP queue messages into chunks with smaller number of recipients
1607    // and started to fail later as high level code started
1608    // expecting `transports` table that is only added in future migrations.
1609    // Migration 108 was not changing the database schema.
1610
1611    if dbversion < 109 {
1612        sql.execute_migration(
1613            r#"ALTER TABLE acpeerstates
1614               ADD COLUMN backward_verified_key_id -- What we think the contact has as our verified key
1615               INTEGER;
1616               UPDATE acpeerstates
1617               SET backward_verified_key_id=(SELECT value FROM config WHERE keyname='key_id')
1618               WHERE verified_key IS NOT NULL
1619               "#,
1620            109,
1621        )
1622        .await?;
1623    }
1624
1625    if dbversion < 110 {
1626        sql.execute_migration(
1627            "ALTER TABLE keypairs ADD COLUMN addr TEXT DEFAULT '' COLLATE NOCASE;
1628            ALTER TABLE keypairs ADD COLUMN is_default INTEGER DEFAULT 0;
1629            ALTER TABLE keypairs ADD COLUMN created INTEGER DEFAULT 0;
1630            UPDATE keypairs SET addr=(SELECT value FROM config WHERE keyname='configured_addr'), is_default=1;",
1631            110,
1632        )
1633        .await?;
1634    }
1635
1636    if dbversion < 111 {
1637        sql.execute_migration(
1638            "CREATE TABLE iroh_gossip_peers (msg_id TEXT not NULL, topic TEXT NOT NULL, public_key TEXT NOT NULL)",
1639            111,
1640        )
1641        .await?;
1642    }
1643
1644    if dbversion < 112 {
1645        sql.execute_migration(
1646            "DROP TABLE iroh_gossip_peers; CREATE TABLE iroh_gossip_peers (msg_id INTEGER not NULL, topic BLOB NOT NULL, public_key BLOB NOT NULL, relay_server TEXT, UNIQUE (public_key, topic)) STRICT",
1647            112,
1648        )
1649        .await?;
1650    }
1651
1652    if dbversion < 113 {
1653        sql.execute_migration(
1654            "DROP TABLE iroh_gossip_peers; CREATE TABLE iroh_gossip_peers (msg_id INTEGER not NULL, topic BLOB NOT NULL, public_key BLOB NOT NULL, relay_server TEXT, UNIQUE (topic, public_key), PRIMARY KEY(topic, public_key)) STRICT",
1655            113,
1656        )
1657        .await?;
1658    }
1659
1660    if dbversion < 114 {
1661        sql.execute_migration("CREATE INDEX reactions_index1 ON reactions (msg_id)", 114)
1662            .await?;
1663    }
1664
1665    if dbversion < 115 {
1666        sql.execute_migration("ALTER TABLE msgs ADD COLUMN txt_normalized TEXT", 115)
1667            .await?;
1668    }
1669    let mut migration_version: i32 = 115;
1670
1671    inc_and_check(&mut migration_version, 116)?;
1672    if dbversion < migration_version {
1673        // Whether the message part doesn't need to be stored on the server. If all parts are marked
1674        // deleted, a server-side deletion is issued.
1675        sql.execute_migration(
1676            "ALTER TABLE msgs ADD COLUMN deleted INTEGER NOT NULL DEFAULT 0",
1677            migration_version,
1678        )
1679        .await?;
1680    }
1681
1682    inc_and_check(&mut migration_version, 117)?;
1683    if dbversion < migration_version {
1684        sql.execute_migration(
1685            "CREATE TABLE connection_history (
1686                host TEXT NOT NULL, -- server hostname
1687                port INTEGER NOT NULL, -- server port
1688                alpn TEXT NOT NULL, -- ALPN such as smtp or imap
1689                addr TEXT NOT NULL, -- IP address
1690                timestamp INTEGER NOT NULL, -- timestamp of the most recent successful connection
1691                UNIQUE (host, port, alpn, addr)
1692            ) STRICT",
1693            migration_version,
1694        )
1695        .await?;
1696    }
1697
1698    inc_and_check(&mut migration_version, 118)?;
1699    if dbversion < migration_version {
1700        sql.execute_migration(
1701            "CREATE TABLE tokens_new (
1702                id INTEGER PRIMARY KEY,
1703                namespc INTEGER DEFAULT 0,
1704                foreign_key TEXT DEFAULT '',
1705                token TEXT DEFAULT '',
1706                timestamp INTEGER DEFAULT 0
1707            ) STRICT;
1708            INSERT INTO tokens_new
1709                SELECT t.id, t.namespc, IFNULL(c.grpid, ''), t.token, t.timestamp
1710                FROM tokens t LEFT JOIN chats c ON t.foreign_id=c.id;
1711            DROP TABLE tokens;
1712            ALTER TABLE tokens_new RENAME TO tokens;",
1713            migration_version,
1714        )
1715        .await?;
1716    }
1717
1718    inc_and_check(&mut migration_version, 119)?;
1719    if dbversion < migration_version {
1720        // This table is deprecated sinc 2025-12-25.
1721        // Sync messages are again sent over SMTP.
1722        sql.execute_migration(
1723            "CREATE TABLE imap_send (
1724                id INTEGER PRIMARY KEY AUTOINCREMENT,
1725                mime TEXT NOT NULL, -- Message content
1726                msg_id INTEGER NOT NULL, -- ID of the message in the `msgs` table
1727                attempts INTEGER NOT NULL DEFAULT 0 -- Number of failed attempts to send the message
1728            )",
1729            migration_version,
1730        )
1731        .await?;
1732    }
1733
1734    inc_and_check(&mut migration_version, 120)?;
1735    if dbversion < migration_version {
1736        // Core 1.143.0 changed the default for `delete_server_after`
1737        // to delete immediately (`1`) for chatmail accounts that don't have multidevice
1738        // and updating to `0` when backup is exported.
1739        //
1740        // Since we don't know if existing configurations
1741        // are multidevice, we set `delete_server_after` for them
1742        // to the old default of `0`, so only new configurations are
1743        // affected by the default change.
1744        //
1745        // `INSERT OR IGNORE` works
1746        // because `keyname` was made UNIQUE in migration 106.
1747        sql.execute_migration(
1748            "INSERT OR IGNORE INTO config (keyname, value)
1749             SELECT 'delete_server_after', '0'
1750             FROM config WHERE keyname='configured'
1751            ",
1752            migration_version,
1753        )
1754        .await?;
1755    }
1756
1757    inc_and_check(&mut migration_version, 121)?;
1758    if dbversion < migration_version {
1759        sql.execute_migration(
1760            "CREATE INDEX chats_index4 ON chats (name)",
1761            migration_version,
1762        )
1763        .await?;
1764    }
1765
1766    inc_and_check(&mut migration_version, 122)?;
1767    if dbversion < migration_version {
1768        sql.execute_migration(
1769            "ALTER TABLE tokens ADD COLUMN foreign_id INTEGER NOT NULL DEFAULT 0",
1770            migration_version,
1771        )
1772        .await?;
1773    }
1774
1775    inc_and_check(&mut migration_version, 123)?;
1776    if dbversion < migration_version {
1777        // Add FOREIGN KEY(msg_id).
1778        sql.execute_migration(
1779            "CREATE TABLE new_msgs_status_updates (
1780                id INTEGER PRIMARY KEY AUTOINCREMENT,
1781                msg_id INTEGER,
1782                update_item TEXT DEFAULT '',
1783                uid TEXT UNIQUE,
1784                FOREIGN KEY(msg_id) REFERENCES msgs(id) ON DELETE CASCADE
1785            );
1786            INSERT OR IGNORE INTO new_msgs_status_updates SELECT
1787                id, msg_id, update_item, uid
1788            FROM msgs_status_updates;
1789            DROP TABLE msgs_status_updates;
1790            ALTER TABLE new_msgs_status_updates RENAME TO msgs_status_updates;
1791            CREATE INDEX msgs_status_updates_index1 ON msgs_status_updates (msg_id);
1792            CREATE INDEX msgs_status_updates_index2 ON msgs_status_updates (uid);
1793            ",
1794            migration_version,
1795        )
1796        .await?;
1797    }
1798
1799    inc_and_check(&mut migration_version, 124)?;
1800    if dbversion < migration_version {
1801        // Mark Saved Messages chat as protected if it already exists.
1802        sql.execute_migration(
1803            "UPDATE chats
1804             SET protected=1 -- ProtectionStatus::Protected
1805             WHERE type==100 -- Chattype::Single
1806             AND EXISTS (
1807                 SELECT 1 FROM chats_contacts cc
1808                 WHERE cc.chat_id==chats.id
1809                 AND cc.contact_id=1
1810             )
1811             ",
1812            migration_version,
1813        )
1814        .await?;
1815    }
1816
1817    inc_and_check(&mut migration_version, 125)?;
1818    if dbversion < migration_version {
1819        sql.execute_migration(
1820            "CREATE TABLE http_cache (
1821                url TEXT PRIMARY KEY,
1822                expires INTEGER NOT NULL, -- When the cache entry is considered expired, timestamp in seconds.
1823                blobname TEXT NOT NULL,
1824                mimetype TEXT NOT NULL DEFAULT '', -- MIME type extracted from Content-Type header.
1825                encoding TEXT NOT NULL DEFAULT '' -- Encoding from Content-Type header.
1826            ) STRICT",
1827            migration_version,
1828        )
1829        .await?;
1830    }
1831
1832    inc_and_check(&mut migration_version, 126)?;
1833    if dbversion < migration_version {
1834        // Recreate http_cache table with new `stale` column.
1835        sql.execute_migration(
1836            "DROP TABLE http_cache;
1837             CREATE TABLE http_cache (
1838                url TEXT PRIMARY KEY,
1839                expires INTEGER NOT NULL, -- When the cache entry is considered expired, timestamp in seconds.
1840                stale INTEGER NOT NULL, -- When the cache entry is considered stale, timestamp in seconds.
1841                blobname TEXT NOT NULL,
1842                mimetype TEXT NOT NULL DEFAULT '', -- MIME type extracted from Content-Type header.
1843                encoding TEXT NOT NULL DEFAULT '' -- Encoding from Content-Type header.
1844            ) STRICT",
1845            migration_version,
1846        )
1847        .await?;
1848    }
1849
1850    inc_and_check(&mut migration_version, 127)?;
1851    if dbversion < migration_version {
1852        // This is buggy: `delete_server_after` > 1 isn't handled. Migration #129 fixes this.
1853        sql.execute_migration(
1854            "INSERT OR IGNORE INTO config (keyname, value)
1855             SELECT 'bcc_self', '1'
1856             FROM config WHERE keyname='delete_server_after' AND value='0'
1857            ",
1858            migration_version,
1859        )
1860        .await?;
1861    }
1862
1863    inc_and_check(&mut migration_version, 128)?;
1864    if dbversion < migration_version {
1865        // Add the timestamps of addition and removal.
1866        //
1867        // If `add_timestamp >= remove_timestamp`,
1868        // then the member is currently a member of the chat.
1869        // Otherwise the member is a past member.
1870        sql.execute_migration(
1871            "ALTER TABLE chats_contacts
1872             ADD COLUMN add_timestamp NOT NULL DEFAULT 0;
1873             ALTER TABLE chats_contacts
1874             ADD COLUMN remove_timestamp NOT NULL DEFAULT 0;
1875            ",
1876            migration_version,
1877        )
1878        .await?;
1879    }
1880
1881    inc_and_check(&mut migration_version, 129)?;
1882    if dbversion < migration_version {
1883        // Existing chatmail configurations having `delete_server_after` != "delete at once" should
1884        // get `bcc_self` enabled, they may be multidevice configurations:
1885        // - Before migration #127, `delete_server_after` was set to 0 upon a backup export, but
1886        //   then `bcc_self` is enabled instead (whose default is changed to 0 for chatmail).
1887        // - The user might set `delete_server_after` to a value other than 0 or 1 when that was
1888        //   possible in UIs.
1889        // We don't check `is_chatmail` for simplicity.
1890        sql.execute_migration(
1891            "INSERT OR IGNORE INTO config (keyname, value)
1892             SELECT 'bcc_self', '1'
1893             FROM config WHERE keyname='delete_server_after' AND value!='1'
1894            ",
1895            migration_version,
1896        )
1897        .await?;
1898    }
1899
1900    inc_and_check(&mut migration_version, 130)?;
1901    if dbversion < migration_version {
1902        sql.execute_migration(
1903            "
1904CREATE TABLE gossip_timestamp (
1905  chat_id INTEGER NOT NULL, 
1906  fingerprint TEXT NOT NULL, -- Upper-case fingerprint of the key.
1907  timestamp INTEGER NOT NULL,
1908  UNIQUE (chat_id, fingerprint)
1909) STRICT;
1910CREATE INDEX gossip_timestamp_index ON gossip_timestamp (chat_id, fingerprint);
1911",
1912            migration_version,
1913        )
1914        .await?;
1915    }
1916
1917    inc_and_check(&mut migration_version, 131)?;
1918    if dbversion < migration_version {
1919        let entered_param = EnteredLoginParam::load_legacy(context).await?;
1920        let configured_param = ConfiguredLoginParam::load_legacy(context).await?;
1921
1922        sql.execute_migration_transaction(
1923            |transaction| {
1924                transaction.execute(
1925                    "CREATE TABLE transports (
1926                        id INTEGER PRIMARY KEY AUTOINCREMENT,
1927                        addr TEXT NOT NULL,
1928                        entered_param TEXT NOT NULL,
1929                        configured_param TEXT NOT NULL,
1930                        UNIQUE(addr)
1931                        )",
1932                    (),
1933                )?;
1934                if let Some(configured_param) = configured_param {
1935                    transaction.execute(
1936                        "INSERT INTO transports (addr, entered_param, configured_param)
1937                         VALUES (?, ?, ?)",
1938                        (
1939                            configured_param.addr.clone(),
1940                            serde_json::to_string(&entered_param)?,
1941                            configured_param.into_json()?,
1942                        ),
1943                    )?;
1944                }
1945
1946                Ok(())
1947            },
1948            migration_version,
1949        )
1950        .await?;
1951    }
1952
1953    inc_and_check(&mut migration_version, 132)?;
1954    if dbversion < migration_version {
1955        let start = Time::now();
1956        sql.execute_migration_transaction(|t| migrate_key_contacts(context, t), migration_version)
1957            .await?;
1958        info!(
1959            context,
1960            "key-contacts migration took {:?} in total.",
1961            time_elapsed(&start),
1962        );
1963        // Schedule `msgs_to_key_contacts()`.
1964        context
1965            .set_config_internal(Config::LastHousekeeping, None)
1966            .await?;
1967    }
1968
1969    inc_and_check(&mut migration_version, 133)?;
1970    if dbversion < migration_version {
1971        // Make `ProtectionBroken` chats `Unprotected`. Chats can't break anymore.
1972        sql.execute_migration(
1973            "UPDATE chats SET protected=0 WHERE protected!=1",
1974            migration_version,
1975        )
1976        .await?;
1977    }
1978
1979    inc_and_check(&mut migration_version, 134)?; // Migration 134 was removed
1980
1981    inc_and_check(&mut migration_version, 135)?;
1982    if dbversion < migration_version {
1983        sql.execute_migration(
1984            "CREATE TABLE stats_securejoin_sources(
1985                source INTEGER PRIMARY KEY,
1986                count INTEGER NOT NULL DEFAULT 0
1987            ) STRICT;
1988            CREATE TABLE stats_securejoin_uipaths(
1989                uipath INTEGER PRIMARY KEY,
1990                count INTEGER NOT NULL DEFAULT 0
1991            ) STRICT;
1992            CREATE TABLE stats_securejoin_invites(
1993                already_existed INTEGER NOT NULL,
1994                already_verified INTEGER NOT NULL,
1995                type TEXT NOT NULL
1996            ) STRICT;
1997            CREATE TABLE stats_msgs(
1998                chattype INTEGER PRIMARY KEY,
1999                verified INTEGER NOT NULL DEFAULT 0,
2000                unverified_encrypted INTEGER NOT NULL DEFAULT 0,
2001                unencrypted INTEGER NOT NULL DEFAULT 0,
2002                only_to_self INTEGER NOT NULL DEFAULT 0,
2003                last_counted_msg_id INTEGER NOT NULL DEFAULT 0
2004            ) STRICT;",
2005            migration_version,
2006        )
2007        .await?;
2008    }
2009
2010    inc_and_check(&mut migration_version, 136)?;
2011    if dbversion < migration_version {
2012        sql.execute_migration(
2013            "CREATE TABLE stats_sending_enabled_events(timestamp INTEGER NOT NULL) STRICT;
2014            CREATE TABLE stats_sending_disabled_events(timestamp INTEGER NOT NULL) STRICT;",
2015            migration_version,
2016        )
2017        .await?;
2018    }
2019
2020    inc_and_check(&mut migration_version, 137)?;
2021    if dbversion < migration_version {
2022        sql.execute_migration(
2023            "DELETE FROM config WHERE keyname IN (
2024                'configured',
2025                'configured_imap_certificate_checks',
2026                'configured_imap_servers',
2027                'configured_mail_port',
2028                'configured_mail_pw',
2029                'configured_mail_security',
2030                'configured_mail_server',
2031                'configured_mail_user',
2032                'configured_send_port',
2033                'configured_send_pw',
2034                'configured_send_security',
2035                'configured_send_server',
2036                'configured_send_user',
2037                'configured_server_flags',
2038                'configured_smtp_certificate_checks',
2039                'configured_smtp_servers'
2040            )",
2041            migration_version,
2042        )
2043        .await?;
2044    }
2045
2046    inc_and_check(&mut migration_version, 138)?;
2047    if dbversion < migration_version {
2048        sql.execute_migration(
2049            "CREATE TABLE broadcast_secrets(
2050                chat_id INTEGER PRIMARY KEY NOT NULL,
2051                secret TEXT NOT NULL
2052            ) STRICT",
2053            migration_version,
2054        )
2055        .await?;
2056    }
2057
2058    inc_and_check(&mut migration_version, 139)?;
2059    if dbversion < migration_version {
2060        sql.execute_migration_transaction(
2061            |transaction| {
2062                if exists_before_update {
2063                    let is_chatmail = transaction
2064                        .query_row(
2065                            "SELECT value FROM config WHERE keyname='is_chatmail'",
2066                            (),
2067                            |row| {
2068                                let value: String = row.get(0)?;
2069                                Ok(value)
2070                            },
2071                        )
2072                        .optional()?
2073                        .as_deref()
2074                        == Some("1");
2075
2076                    // For non-chatmail accounts
2077                    // default "bcc_self" was "1".
2078                    // If it is not in the database,
2079                    // save the old default explicity
2080                    // as the new default is "0"
2081                    // for all accounts.
2082                    if !is_chatmail {
2083                        transaction.execute(
2084                            "INSERT OR IGNORE
2085                             INTO config (keyname, value)
2086                             VALUES (?, ?)",
2087                            ("bcc_self", "1"),
2088                        )?;
2089                    }
2090                }
2091                Ok(())
2092            },
2093            migration_version,
2094        )
2095        .await?;
2096    }
2097
2098    inc_and_check(&mut migration_version, 140)?;
2099    if dbversion < migration_version {
2100        sql.execute_migration(
2101            "
2102CREATE TABLE new_imap (
2103id INTEGER PRIMARY KEY AUTOINCREMENT,
2104transport_id INTEGER NOT NULL, -- ID of the transport in the `transports` table.
2105rfc724_mid TEXT NOT NULL, -- Message-ID header
2106folder TEXT NOT NULL, -- IMAP folder
2107target TEXT NOT NULL, -- Destination folder. Empty string means that the message shall be deleted.
2108uid INTEGER NOT NULL, -- UID
2109uidvalidity INTEGER NOT NULL,
2110UNIQUE (transport_id, folder, uid, uidvalidity)
2111) STRICT;
2112
2113INSERT OR IGNORE INTO new_imap SELECT
2114  id, 1, rfc724_mid, folder, target, uid, uidvalidity
2115FROM imap;
2116DROP TABLE imap;
2117ALTER TABLE new_imap RENAME TO imap;
2118CREATE INDEX imap_folder ON imap(transport_id, folder);
2119CREATE INDEX imap_rfc724_mid ON imap(transport_id, rfc724_mid);
2120
2121CREATE TABLE new_imap_sync (
2122    transport_id INTEGER NOT NULL, -- ID of the transport in the `transports` table.
2123    folder TEXT NOT NULL,
2124    uidvalidity INTEGER NOT NULL DEFAULT 0,
2125    uid_next INTEGER NOT NULL DEFAULT 0,
2126    modseq INTEGER NOT NULL DEFAULT 0,
2127    UNIQUE (transport_id, folder)
2128) STRICT;
2129INSERT OR IGNORE INTO new_imap_sync SELECT
2130    1, folder, uidvalidity, uid_next, modseq
2131FROM imap_sync;
2132DROP TABLE imap_sync;
2133ALTER TABLE new_imap_sync RENAME TO imap_sync;
2134CREATE INDEX imap_sync_index ON imap_sync(transport_id, folder);
2135",
2136            migration_version,
2137        )
2138        .await?;
2139    }
2140
2141    inc_and_check(&mut migration_version, 141)?;
2142    if dbversion < migration_version {
2143        sql.execute_migration(
2144            "CREATE INDEX imap_only_rfc724_mid ON imap(rfc724_mid)",
2145            migration_version,
2146        )
2147        .await?;
2148    }
2149
2150    inc_and_check(&mut migration_version, 142)?;
2151    if dbversion < migration_version {
2152        sql.execute_migration(
2153            "ALTER TABLE transports
2154             ADD COLUMN add_timestamp INTEGER NOT NULL DEFAULT 0;
2155             CREATE TABLE removed_transports (
2156                 addr TEXT NOT NULL,
2157                 remove_timestamp INTEGER NOT NULL,
2158                 UNIQUE(addr)
2159             ) STRICT;",
2160            migration_version,
2161        )
2162        .await?;
2163    }
2164
2165    inc_and_check(&mut migration_version, 143)?;
2166    if dbversion < migration_version {
2167        sql.execute_migration(
2168            "
2169ALTER TABLE chats ADD COLUMN name_normalized TEXT;
2170ALTER TABLE contacts ADD COLUMN name_normalized TEXT;
2171                ",
2172            migration_version,
2173        )
2174        .await?;
2175    }
2176
2177    inc_and_check(&mut migration_version, 144)?;
2178    if dbversion < migration_version {
2179        sql.execute_migration_transaction(
2180            |transaction| {
2181                let is_chatmail = transaction
2182                    .query_row(
2183                        "SELECT value FROM config WHERE keyname='is_chatmail'",
2184                        (),
2185                        |row| {
2186                            let value: String = row.get(0)?;
2187                            Ok(value)
2188                        },
2189                    )
2190                    .optional()?
2191                    .as_deref()
2192                    == Some("1");
2193
2194                if is_chatmail {
2195                    transaction.execute_batch(
2196                        "DELETE FROM config WHERE keyname='only_fetch_mvbox';
2197                         DELETE FROM config WHERE keyname='show_emails';
2198                         UPDATE config SET value='0' WHERE keyname='mvbox_move'",
2199                    )?;
2200                }
2201                Ok(())
2202            },
2203            migration_version,
2204        )
2205        .await?;
2206    }
2207
2208    inc_and_check(&mut migration_version, 145)?;
2209    if dbversion < migration_version {
2210        // `msg_id` in `download` table is not needed anymore,
2211        // but we still keep it so that it's possible to import a backup into an older DC version,
2212        // because we don't always release at the same time on all platforms.
2213        sql.execute_migration(
2214            "CREATE TABLE download_new (
2215                rfc724_mid TEXT PRIMARY KEY,
2216                msg_id INTEGER NOT NULL DEFAULT 0
2217            ) STRICT;
2218            INSERT OR IGNORE INTO download_new (rfc724_mid, msg_id)
2219                SELECT m.rfc724_mid, d.msg_id FROM download d
2220                LEFT JOIN msgs m ON d.msg_id = m.id
2221                WHERE m.rfc724_mid IS NOT NULL AND m.rfc724_mid != '';
2222            DROP TABLE download;
2223            ALTER TABLE download_new RENAME TO download;
2224            CREATE TABLE available_post_msgs (
2225                rfc724_mid TEXT PRIMARY KEY
2226            ) STRICT;
2227            ALTER TABLE msgs ADD COLUMN pre_rfc724_mid TEXT DEFAULT '';
2228            CREATE INDEX msgs_index9 ON msgs (pre_rfc724_mid);",
2229            migration_version,
2230        )
2231        .await?;
2232    }
2233
2234    // Copy of migration 144, but inserts a value for `mvbox_move`
2235    // if it does not exist, because no value is treated as `1`.
2236    inc_and_check(&mut migration_version, 146)?;
2237    if dbversion < migration_version {
2238        sql.execute_migration_transaction(
2239            |transaction| {
2240                let is_chatmail = transaction
2241                    .query_row(
2242                        "SELECT value FROM config WHERE keyname='is_chatmail'",
2243                        (),
2244                        |row| {
2245                            let value: String = row.get(0)?;
2246                            Ok(value)
2247                        },
2248                    )
2249                    .optional()?
2250                    .as_deref()
2251                    == Some("1");
2252
2253                if is_chatmail {
2254                    transaction.execute_batch(
2255                        "DELETE FROM config WHERE keyname='only_fetch_mvbox';
2256                         DELETE FROM config WHERE keyname='show_emails';
2257                         INSERT OR REPLACE INTO config (keyname, value) VALUES ('mvbox_move', '0')",
2258                    )?;
2259                }
2260                Ok(())
2261            },
2262            migration_version,
2263        )
2264        .await?;
2265    }
2266
2267    inc_and_check(&mut migration_version, 147)?;
2268    if dbversion < migration_version {
2269        sql.execute_migration(
2270            "CREATE TABLE chats_descriptions (chat_id INTEGER PRIMARY KEY AUTOINCREMENT, description TEXT NOT NULL DEFAULT '') STRICT;",
2271            migration_version,
2272        )
2273        .await?;
2274    }
2275
2276    // Add UNIQUE bound to token, in order to avoid saving the same token multiple times
2277    inc_and_check(&mut migration_version, 148)?;
2278    if dbversion < migration_version {
2279        sql.execute_migration(
2280            "CREATE TABLE tokens_new (
2281                id INTEGER PRIMARY KEY,
2282                namespc INTEGER NOT NULL,
2283                foreign_key TEXT DEFAULT '' NOT NULL,
2284                token TEXT NOT NULL UNIQUE,
2285                timestamp INTEGER DEFAULT 0 NOT NULL
2286            ) STRICT;
2287            INSERT OR IGNORE INTO tokens_new
2288                SELECT id, namespc, foreign_key, token, timestamp FROM tokens;
2289            DROP TABLE tokens;
2290            ALTER TABLE tokens_new RENAME TO tokens;",
2291            migration_version,
2292        )
2293        .await?;
2294    }
2295
2296    // Add an `is_published` flag to transports.
2297    // Unpublished transports are not advertised to contacts,
2298    // and self-sent messages are not sent there,
2299    // so that we don't cause extra messages to the corresponding inbox,
2300    // but can still receive messages from contacts who don't know our new transport addresses yet.
2301    // The default is true, but when when the user updates the app,
2302    // existing secondary transports are set to unpublished,
2303    // so that an existing transport address doesn't suddenly get spammed with a lot of messages.
2304    inc_and_check(&mut migration_version, 149)?;
2305    if dbversion < migration_version {
2306        sql.execute_migration(
2307            "ALTER TABLE transports ADD COLUMN is_published INTEGER DEFAULT 1 NOT NULL;
2308            UPDATE transports SET is_published=0 WHERE addr!=(
2309                SELECT value FROM config WHERE keyname='configured_addr'
2310            )",
2311            migration_version,
2312        )
2313        .await?;
2314    }
2315
2316    inc_and_check(&mut migration_version, 150)?;
2317    if dbversion < migration_version {
2318        sql.execute_migration_transaction(
2319            |transaction| {
2320                let only_fetch_mvbox = transaction
2321                    .query_row(
2322                        "SELECT value FROM config WHERE keyname='only_fetch_mvbox'",
2323                        (),
2324                        |row| {
2325                            let value: String = row.get(0)?;
2326                            Ok(value)
2327                        },
2328                    )
2329                    .optional()?
2330                    .as_deref()
2331                    == Some("1");
2332
2333                if only_fetch_mvbox {
2334                    let mvbox_folder = transaction
2335                        .query_row(
2336                            "SELECT value FROM config WHERE keyname='configured_mvbox_folder'",
2337                            (),
2338                            |row| {
2339                                let value: String = row.get(0)?;
2340                                Ok(value)
2341                            },
2342                        )
2343                        .optional()?
2344                        .unwrap_or_else(|| "DeltaChat".to_string());
2345
2346                    transaction.execute(
2347                        "UPDATE transports
2348                         SET entered_param=json_set(entered_param, '$.imap.folder', ?1),
2349                             configured_param=json_set(configured_param', '$.imap_folder', ?1)",
2350                        (mvbox_folder,),
2351                    )?;
2352                }
2353                Ok(())
2354            },
2355            migration_version,
2356        )
2357        .await?;
2358    }
2359
2360    inc_and_check(&mut migration_version, 151)?;
2361    if dbversion < migration_version {
2362        sql.execute_migration(
2363            "CREATE TABLE tls_spki (
2364               host TEXT NOT NULL UNIQUE,
2365               spki_hash TEXT NOT NULL, -- base64 of SPKI SHA-256 hash
2366               timestamp INTEGER NOT NULL -- timestamp of the last time we have seen this key
2367             ) STRICT;
2368             -- Index on host column is created implicitly because of UNIQUE constraint.
2369             CREATE INDEX tls_spki_index_timestamp ON tls_spki (timestamp);
2370            ",
2371            migration_version,
2372        )
2373        .await?;
2374    }
2375
2376    let new_version = sql
2377        .get_raw_config_int(VERSION_CFG)
2378        .await?
2379        .unwrap_or_default();
2380    if new_version != dbversion || !exists_before_update {
2381        let created_db = if exists_before_update {
2382            ""
2383        } else {
2384            "Created new database. "
2385        };
2386        info!(context, "{}Migration done from v{}.", created_db, dbversion);
2387    }
2388    info!(context, "Database version: v{new_version}.");
2389
2390    Ok(recode_avatar)
2391}
2392
2393#[cfg(test)]
2394mod migrations_tests;