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