1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
//! # Key-value configuration management.

use std::env;
use std::path::Path;
use std::str::FromStr;

use anyhow::{ensure, Context as _, Result};
use base64::Engine as _;
use deltachat_contact_tools::{addr_cmp, sanitize_single_line};
use serde::{Deserialize, Serialize};
use strum::{EnumProperty, IntoEnumIterator};
use strum_macros::{AsRefStr, Display, EnumIter, EnumString};
use tokio::fs;

use crate::blob::BlobObject;
use crate::constants;
use crate::context::Context;
use crate::events::EventType;
use crate::log::LogExt;
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
use crate::provider::{get_provider_by_id, Provider};
use crate::sync::{self, Sync::*, SyncData};
use crate::tools::get_abs_path;

/// The available configuration keys.
#[derive(
    Debug,
    Clone,
    Copy,
    PartialEq,
    Eq,
    Display,
    EnumString,
    AsRefStr,
    EnumIter,
    EnumProperty,
    PartialOrd,
    Ord,
    Serialize,
    Deserialize,
)]
#[strum(serialize_all = "snake_case")]
pub enum Config {
    /// Email address, used in the `From:` field.
    Addr,

    /// IMAP server hostname.
    MailServer,

    /// IMAP server username.
    MailUser,

    /// IMAP server password.
    MailPw,

    /// IMAP server port.
    MailPort,

    /// IMAP server security (e.g. TLS, STARTTLS).
    MailSecurity,

    /// How to check TLS certificates.
    ///
    /// "IMAP" in the name is for compatibility,
    /// this actually applies to both IMAP and SMTP connections.
    ImapCertificateChecks,

    /// SMTP server hostname.
    SendServer,

    /// SMTP server username.
    SendUser,

    /// SMTP server password.
    SendPw,

    /// SMTP server port.
    SendPort,

    /// SMTP server security (e.g. TLS, STARTTLS).
    SendSecurity,

    /// Deprecated option for backwards compatibilty.
    ///
    /// Certificate checks for SMTP are actually controlled by `imap_certificate_checks` config.
    SmtpCertificateChecks,

    /// Whether to use OAuth 2.
    ///
    /// Historically contained other bitflags, which are now deprecated.
    /// Should not be extended in the future, create new config keys instead.
    ServerFlags,

    /// True if proxy is enabled.
    ///
    /// Can be used to disable proxy without erasing known URLs.
    ProxyEnabled,

    /// Proxy URL.
    ///
    /// Supported URLs schemes are `http://` (HTTP), `https://` (HTTPS),
    /// `socks5://` (SOCKS5) and `ss://` (Shadowsocks).
    ///
    /// May contain multiple URLs separated by newline, in which case the first one is used.
    ProxyUrl,

    /// True if SOCKS5 is enabled.
    ///
    /// Can be used to disable SOCKS5 without erasing SOCKS5 configuration.
    ///
    /// Deprecated in favor of `ProxyEnabled`.
    Socks5Enabled,

    /// SOCKS5 proxy server hostname or address.
    ///
    /// Deprecated in favor of `ProxyUrl`.
    Socks5Host,

    /// SOCKS5 proxy server port.
    ///
    /// Deprecated in favor of `ProxyUrl`.
    Socks5Port,

    /// SOCKS5 proxy server username.
    ///
    /// Deprecated in favor of `ProxyUrl`.
    Socks5User,

    /// SOCKS5 proxy server password.
    ///
    /// Deprecated in favor of `ProxyUrl`.
    Socks5Password,

    /// Own name to use in the `From:` field when sending messages.
    Displayname,

    /// Own status to display, sent in message footer.
    Selfstatus,

    /// Own avatar filename.
    Selfavatar,

    /// Send BCC copy to self.
    ///
    /// Should be enabled for multidevice setups.
    #[strum(props(default = "1"))]
    BccSelf,

    /// True if encryption is preferred according to Autocrypt standard.
    #[strum(props(default = "1"))]
    E2eeEnabled,

    /// True if Message Delivery Notifications (read receipts) should
    /// be sent and requested.
    #[strum(props(default = "1"))]
    MdnsEnabled,

    /// True if "Sent" folder should be watched for changes.
    #[strum(props(default = "0"))]
    SentboxWatch,

    /// True if chat messages should be moved to a separate folder. Auto-sent messages like sync
    /// ones are moved there anyway.
    #[strum(props(default = "1"))]
    MvboxMove,

    /// Watch for new messages in the "Mvbox" (aka DeltaChat folder) only.
    ///
    /// This will not entirely disable other folders, e.g. the spam folder will also still
    /// be watched for new messages.
    #[strum(props(default = "0"))]
    OnlyFetchMvbox,

    /// Whether to show classic emails or only chat messages.
    #[strum(props(default = "2"))] // also change ShowEmails.default() on changes
    ShowEmails,

    /// Quality of the media files to send.
    #[strum(props(default = "0"))] // also change MediaQuality.default() on changes
    MediaQuality,

    /// If set to "1", on the first time `start_io()` is called after configuring,
    /// the newest existing messages are fetched.
    /// Existing recipients are added to the contact database regardless of this setting.
    #[strum(props(default = "0"))]
    FetchExistingMsgs,

    /// If set to "1", then existing messages are considered to be already fetched.
    /// This flag is reset after successful configuration.
    #[strum(props(default = "1"))]
    FetchedExistingMsgs,

    /// Type of the OpenPGP key to generate.
    #[strum(props(default = "0"))]
    KeyGenType,

    /// Timer in seconds after which the message is deleted from the
    /// server.
    ///
    /// 0 means messages are never deleted by Delta Chat.
    ///
    /// Value 1 is treated as "delete at once": messages are deleted
    /// immediately, without moving to DeltaChat folder.
    ///
    /// Default is 1 for chatmail accounts before a backup export, 0 otherwise.
    DeleteServerAfter,

    /// Timer in seconds after which the message is deleted from the
    /// device.
    ///
    /// Equals to 0 by default, which means the message is never
    /// deleted.
    #[strum(props(default = "0"))]
    DeleteDeviceAfter,

    /// Move messages to the Trash folder instead of marking them "\Deleted". Overrides
    /// `ProviderOptions::delete_to_trash`.
    DeleteToTrash,

    /// Save raw MIME messages with headers in the database if true.
    SaveMimeHeaders,

    /// The primary email address. Also see `SecondaryAddrs`.
    ConfiguredAddr,

    /// List of configured IMAP servers as a JSON array.
    ConfiguredImapServers,

    /// Configured IMAP server hostname.
    ///
    /// This is replaced by `configured_imap_servers` for new configurations.
    ConfiguredMailServer,

    /// Configured IMAP server port.
    ///
    /// This is replaced by `configured_imap_servers` for new configurations.
    ConfiguredMailPort,

    /// Configured IMAP server security (e.g. TLS, STARTTLS).
    ///
    /// This is replaced by `configured_imap_servers` for new configurations.
    ConfiguredMailSecurity,

    /// Configured IMAP server username.
    ///
    /// This is set if user has configured username manually.
    ConfiguredMailUser,

    /// Configured IMAP server password.
    ConfiguredMailPw,

    /// Configured TLS certificate checks.
    /// This option is saved on successful configuration
    /// and should not be modified manually.
    ///
    /// This actually applies to both IMAP and SMTP connections,
    /// but has "IMAP" in the name for backwards compatibility.
    ConfiguredImapCertificateChecks,

    /// List of configured SMTP servers as a JSON array.
    ConfiguredSmtpServers,

    /// Configured SMTP server hostname.
    ///
    /// This is replaced by `configured_smtp_servers` for new configurations.
    ConfiguredSendServer,

    /// Configured SMTP server port.
    ///
    /// This is replaced by `configured_smtp_servers` for new configurations.
    ConfiguredSendPort,

    /// Configured SMTP server security (e.g. TLS, STARTTLS).
    ///
    /// This is replaced by `configured_smtp_servers` for new configurations.
    ConfiguredSendSecurity,

    /// Configured SMTP server username.
    ///
    /// This is set if user has configured username manually.
    ConfiguredSendUser,

    /// Configured SMTP server password.
    ConfiguredSendPw,

    /// Deprecated, stored for backwards compatibility.
    ///
    /// ConfiguredImapCertificateChecks is actually used.
    ConfiguredSmtpCertificateChecks,

    /// Whether OAuth 2 is used with configured provider.
    ConfiguredServerFlags,

    /// Configured folder for incoming messages.
    ConfiguredInboxFolder,

    /// Configured folder for chat messages.
    ConfiguredMvboxFolder,

    /// Configured "Sent" folder.
    ConfiguredSentboxFolder,

    /// Configured "Trash" folder.
    ConfiguredTrashFolder,

    /// Unix timestamp of the last successful configuration.
    ConfiguredTimestamp,

    /// ID of the configured provider from the provider database.
    ConfiguredProvider,

    /// True if account is configured.
    Configured,

    /// True if account is a chatmail account.
    IsChatmail,

    /// True if `IsChatmail` mustn't be autoconfigured. For tests.
    FixIsChatmail,

    /// True if account is muted.
    IsMuted,

    /// Optional tag as "Work", "Family".
    /// Meant to help profile owner to differ between profiles with similar names.
    PrivateTag,

    /// All secondary self addresses separated by spaces
    /// (`addr1@example.org addr2@example.org addr3@example.org`)
    SecondaryAddrs,

    /// Read-only core version string.
    #[strum(serialize = "sys.version")]
    SysVersion,

    /// Maximal recommended attachment size in bytes.
    #[strum(serialize = "sys.msgsize_max_recommended")]
    SysMsgsizeMaxRecommended,

    /// Space separated list of all config keys available.
    #[strum(serialize = "sys.config_keys")]
    SysConfigKeys,

    /// True if it is a bot account.
    Bot,

    /// True when to skip initial start messages in groups.
    #[strum(props(default = "0"))]
    SkipStartMessages,

    /// Whether we send a warning if the password is wrong (set to false when we send a warning
    /// because we do not want to send a second warning)
    #[strum(props(default = "0"))]
    NotifyAboutWrongPw,

    /// If a warning about exceeding quota was shown recently,
    /// this is the percentage of quota at the time the warning was given.
    /// Unset, when quota falls below minimal warning threshold again.
    QuotaExceeding,

    /// address to webrtc instance to use for videochats
    WebrtcInstance,

    /// Timestamp of the last time housekeeping was run
    LastHousekeeping,

    /// Timestamp of the last `CantDecryptOutgoingMsgs` notification.
    LastCantDecryptOutgoingMsgs,

    /// To how many seconds to debounce scan_all_folders. Used mainly in tests, to disable debouncing completely.
    #[strum(props(default = "60"))]
    ScanAllFoldersDebounceSecs,

    /// Whether to avoid using IMAP IDLE even if the server supports it.
    ///
    /// This is a developer option for testing "fake idle".
    #[strum(props(default = "0"))]
    DisableIdle,

    /// Defines the max. size (in bytes) of messages downloaded automatically.
    /// 0 = no limit.
    #[strum(props(default = "0"))]
    DownloadLimit,

    /// Enable sending and executing (applying) sync messages. Sending requires `BccSelf` to be set
    /// and `Bot` unset.
    #[strum(props(default = "1"))]
    SyncMsgs,

    /// Space-separated list of all the authserv-ids which we believe
    /// may be the one of our email server.
    ///
    /// See `crate::authres::update_authservid_candidates`.
    AuthservIdCandidates,

    /// Make all outgoing messages with Autocrypt header "multipart/signed".
    SignUnencrypted,

    /// Enable header protection for `Autocrypt` header.
    ///
    /// This is an experimental setting not compatible to other MUAs
    /// and older Delta Chat versions (core version <= v1.149.0).
    ProtectAutocrypt,

    /// Let the core save all events to the database.
    /// This value is used internally to remember the MsgId of the logging xdc
    #[strum(props(default = "0"))]
    DebugLogging,

    /// Last message processed by the bot.
    LastMsgId,

    /// How often to gossip Autocrypt keys in chats with multiple recipients, in seconds. 2 days by
    /// default.
    ///
    /// This is not supposed to be changed by UIs and only used for testing.
    #[strum(props(default = "172800"))]
    GossipPeriod,

    /// Feature flag for verified 1:1 chats; the UI should set it
    /// to 1 if it supports verified 1:1 chats.
    /// Regardless of this setting, `chat.is_protected()` returns true while the key is verified,
    /// and when the key changes, an info message is posted into the chat.
    /// 0=Nothing else happens when the key changes.
    /// 1=After the key changed, `can_send()` returns false and `is_protection_broken()` returns true
    /// until `chat_id.accept()` is called.
    #[strum(props(default = "0"))]
    VerifiedOneOnOneChats,

    /// Row ID of the key in the `keypairs` table
    /// used for signatures, encryption to self and included in `Autocrypt` header.
    KeyId,

    /// This key is sent to the self_reporting bot so that the bot can recognize the user
    /// without storing the email address
    SelfReportingId,

    /// MsgId of webxdc map integration.
    WebxdcIntegration,

    /// Enable webxdc realtime features.
    #[strum(props(default = "1"))]
    WebxdcRealtimeEnabled,
}

impl Config {
    /// Whether the config option is synced across devices.
    ///
    /// This must be checked on both sides so that if there are different client versions, the
    /// synchronisation of a particular option is either done or not done in both directions.
    /// Moreover, receivers of a config value need to check if a key can be synced because if it is
    /// a file path, it could otherwise lead to exfiltration of files from a receiver's
    /// device if we assume an attacker to have control of a device in a multi-device setting or if
    /// multiple users are sharing an account. Another example is `Self::SyncMsgs` itself which
    /// mustn't be controlled by other devices.
    pub(crate) fn is_synced(&self) -> bool {
        matches!(
            self,
            Self::Displayname
                | Self::MdnsEnabled
                | Self::MvboxMove
                | Self::ShowEmails
                | Self::Selfavatar
                | Self::Selfstatus,
        )
    }

    /// Whether the config option needs an IO scheduler restart to take effect.
    pub(crate) fn needs_io_restart(&self) -> bool {
        matches!(self, Config::OnlyFetchMvbox | Config::SentboxWatch)
    }
}

impl Context {
    /// Returns true if configuration value is set in the db for the given key.
    ///
    /// NB: Don't use this to check if the key is configured because this doesn't look into
    /// environment. The proper use of this function is e.g. checking a key before setting it.
    pub(crate) async fn config_exists(&self, key: Config) -> Result<bool> {
        Ok(self.sql.get_raw_config(key.as_ref()).await?.is_some())
    }

    /// Get a config key value. Returns `None` if no value is set.
    pub(crate) async fn get_config_opt(&self, key: Config) -> Result<Option<String>> {
        let env_key = format!("DELTACHAT_{}", key.as_ref().to_uppercase());
        if let Ok(value) = env::var(env_key) {
            return Ok(Some(value));
        }

        let value = match key {
            Config::Selfavatar => {
                let rel_path = self.sql.get_raw_config(key.as_ref()).await?;
                rel_path.map(|p| {
                    get_abs_path(self, Path::new(&p))
                        .to_string_lossy()
                        .into_owned()
                })
            }
            Config::SysVersion => Some((*constants::DC_VERSION_STR).clone()),
            Config::SysMsgsizeMaxRecommended => Some(format!("{RECOMMENDED_FILE_SIZE}")),
            Config::SysConfigKeys => Some(get_config_keys_string()),
            _ => self.sql.get_raw_config(key.as_ref()).await?,
        };
        Ok(value)
    }

    /// Get a config key value if set, or a default value. Returns `None` if no value exists.
    pub async fn get_config(&self, key: Config) -> Result<Option<String>> {
        let value = self.get_config_opt(key).await?;
        if value.is_some() {
            return Ok(value);
        }

        // Default values
        let val = match key {
            Config::ConfiguredInboxFolder => Some("INBOX"),
            Config::DeleteServerAfter => match Box::pin(self.is_chatmail()).await? {
                false => Some("0"),
                true => Some("1"),
            },
            _ => key.get_str("default"),
        };
        Ok(val.map(|s| s.to_string()))
    }

    /// Returns Some(T) if a value for the given key is set and was successfully parsed.
    /// Returns None if could not parse.
    pub(crate) async fn get_config_opt_parsed<T: FromStr>(&self, key: Config) -> Result<Option<T>> {
        self.get_config_opt(key)
            .await
            .map(|s: Option<String>| s.and_then(|s| s.parse().ok()))
    }

    /// Returns Some(T) if a value for the given key exists (incl. default value) and was
    /// successfully parsed.
    /// Returns None if could not parse.
    pub async fn get_config_parsed<T: FromStr>(&self, key: Config) -> Result<Option<T>> {
        self.get_config(key)
            .await
            .map(|s: Option<String>| s.and_then(|s| s.parse().ok()))
    }

    /// Returns 32-bit signed integer configuration value for the given key.
    pub async fn get_config_int(&self, key: Config) -> Result<i32> {
        Ok(self.get_config_parsed(key).await?.unwrap_or_default())
    }

    /// Returns 32-bit unsigned integer configuration value for the given key.
    pub async fn get_config_u32(&self, key: Config) -> Result<u32> {
        Ok(self.get_config_parsed(key).await?.unwrap_or_default())
    }

    /// Returns 64-bit signed integer configuration value for the given key.
    pub async fn get_config_i64(&self, key: Config) -> Result<i64> {
        Ok(self.get_config_parsed(key).await?.unwrap_or_default())
    }

    /// Returns 64-bit unsigned integer configuration value for the given key.
    pub async fn get_config_u64(&self, key: Config) -> Result<u64> {
        Ok(self.get_config_parsed(key).await?.unwrap_or_default())
    }

    /// Returns boolean configuration value (if set) for the given key.
    pub(crate) async fn get_config_bool_opt(&self, key: Config) -> Result<Option<bool>> {
        Ok(self
            .get_config_opt_parsed::<i32>(key)
            .await?
            .map(|x| x != 0))
    }

    /// Returns boolean configuration value for the given key.
    pub async fn get_config_bool(&self, key: Config) -> Result<bool> {
        Ok(self
            .get_config_parsed::<i32>(key)
            .await?
            .map(|x| x != 0)
            .unwrap_or_default())
    }

    /// Returns true if movebox ("DeltaChat" folder) should be watched.
    pub(crate) async fn should_watch_mvbox(&self) -> Result<bool> {
        Ok(self.get_config_bool(Config::MvboxMove).await?
            || self.get_config_bool(Config::OnlyFetchMvbox).await?
            || !self.get_config_bool(Config::IsChatmail).await?)
    }

    /// Returns true if sentbox ("Sent" folder) should be watched.
    pub(crate) async fn should_watch_sentbox(&self) -> Result<bool> {
        Ok(self.get_config_bool(Config::SentboxWatch).await?
            && self
                .get_config(Config::ConfiguredSentboxFolder)
                .await?
                .is_some())
    }

    /// Returns true if sync messages should be sent.
    pub(crate) async fn should_send_sync_msgs(&self) -> Result<bool> {
        Ok(self.get_config_bool(Config::SyncMsgs).await?
            && self.get_config_bool(Config::BccSelf).await?
            && !self.get_config_bool(Config::Bot).await?)
    }

    /// Returns whether sync messages should be uploaded to the mvbox.
    pub(crate) async fn should_move_sync_msgs(&self) -> Result<bool> {
        Ok(self.get_config_bool(Config::MvboxMove).await?
            || !self.get_config_bool(Config::IsChatmail).await?)
    }

    /// Returns whether MDNs should be requested.
    pub(crate) async fn should_request_mdns(&self) -> Result<bool> {
        match self.get_config_bool_opt(Config::MdnsEnabled).await? {
            Some(val) => Ok(val),
            None => Ok(!self.get_config_bool(Config::Bot).await?),
        }
    }

    /// Returns whether MDNs should be sent.
    pub(crate) async fn should_send_mdns(&self) -> Result<bool> {
        self.get_config_bool(Config::MdnsEnabled).await
    }

    /// Gets configured "delete_server_after" value.
    ///
    /// `None` means never delete the message, `Some(0)` means delete
    /// at once, `Some(x)` means delete after `x` seconds.
    pub async fn get_config_delete_server_after(&self) -> Result<Option<i64>> {
        let val = match self
            .get_config_parsed::<i64>(Config::DeleteServerAfter)
            .await?
            .unwrap_or(0)
        {
            0 => None,
            1 => Some(0),
            x => Some(x),
        };
        Ok(val)
    }

    /// Gets the configured provider, as saved in the `configured_provider` value.
    ///
    /// The provider is determined by `get_provider_info()` during configuration and then saved
    /// to the db in `param.save_to_database()`, together with all the other `configured_*` values.
    pub async fn get_configured_provider(&self) -> Result<Option<&'static Provider>> {
        if let Some(cfg) = self.get_config(Config::ConfiguredProvider).await? {
            return Ok(get_provider_by_id(&cfg));
        }
        Ok(None)
    }

    /// Gets configured "delete_device_after" value.
    ///
    /// `None` means never delete the message, `Some(x)` means delete
    /// after `x` seconds.
    pub async fn get_config_delete_device_after(&self) -> Result<Option<i64>> {
        match self.get_config_int(Config::DeleteDeviceAfter).await? {
            0 => Ok(None),
            x => Ok(Some(i64::from(x))),
        }
    }

    /// Executes [`SyncData::Config`] item sent by other device.
    pub(crate) async fn sync_config(&self, key: &Config, value: &str) -> Result<()> {
        let config_value;
        let value = match key {
            Config::Selfavatar if value.is_empty() => None,
            Config::Selfavatar => {
                config_value = BlobObject::store_from_base64(self, value, "avatar").await?;
                Some(config_value.as_str())
            }
            _ => Some(value),
        };
        match key.is_synced() {
            true => self.set_config_ex(Nosync, *key, value).await,
            false => Ok(()),
        }
    }

    fn check_config(key: Config, value: Option<&str>) -> Result<()> {
        match key {
            Config::Socks5Enabled
            | Config::ProxyEnabled
            | Config::BccSelf
            | Config::E2eeEnabled
            | Config::MdnsEnabled
            | Config::SentboxWatch
            | Config::MvboxMove
            | Config::OnlyFetchMvbox
            | Config::FetchExistingMsgs
            | Config::DeleteToTrash
            | Config::SaveMimeHeaders
            | Config::Configured
            | Config::Bot
            | Config::NotifyAboutWrongPw
            | Config::SyncMsgs
            | Config::SignUnencrypted
            | Config::DisableIdle => {
                ensure!(
                    matches!(value, None | Some("0") | Some("1")),
                    "Boolean value must be either 0 or 1"
                );
            }
            _ => (),
        }
        Ok(())
    }

    /// Set the given config key and make it effective.
    /// This may restart the IO scheduler. If `None` is passed as a value the value is cleared and
    /// set to the default if there is one.
    pub async fn set_config(&self, key: Config, value: Option<&str>) -> Result<()> {
        Self::check_config(key, value)?;

        let _pause = match key.needs_io_restart() {
            true => self.scheduler.pause(self.clone()).await?,
            _ => Default::default(),
        };
        self.set_config_internal(key, value).await?;
        if key == Config::SentboxWatch {
            self.last_full_folder_scan.lock().await.take();
        }
        Ok(())
    }

    pub(crate) async fn set_config_internal(&self, key: Config, value: Option<&str>) -> Result<()> {
        self.set_config_ex(Sync, key, value).await
    }

    pub(crate) async fn set_config_ex(
        &self,
        sync: sync::Sync,
        key: Config,
        mut value: Option<&str>,
    ) -> Result<()> {
        Self::check_config(key, value)?;
        let sync = sync == Sync && key.is_synced() && self.is_configured().await?;
        let better_value;

        match key {
            Config::Selfavatar => {
                self.sql
                    .execute("UPDATE contacts SET selfavatar_sent=0;", ())
                    .await?;
                match value {
                    Some(path) => {
                        let mut blob = BlobObject::new_from_path(self, path.as_ref()).await?;
                        blob.recode_to_avatar_size(self).await?;
                        self.sql
                            .set_raw_config(key.as_ref(), Some(blob.as_name()))
                            .await?;
                        if sync {
                            let buf = fs::read(blob.to_abs_path()).await?;
                            better_value = base64::engine::general_purpose::STANDARD.encode(buf);
                            value = Some(&better_value);
                        }
                    }
                    None => {
                        self.sql.set_raw_config(key.as_ref(), None).await?;
                        if sync {
                            better_value = String::new();
                            value = Some(&better_value);
                        }
                    }
                }
                self.emit_event(EventType::SelfavatarChanged);
            }
            Config::DeleteDeviceAfter => {
                let ret = self.sql.set_raw_config(key.as_ref(), value).await;
                // Interrupt ephemeral loop to delete old messages immediately.
                self.scheduler.interrupt_ephemeral_task().await;
                ret?
            }
            Config::Displayname => {
                if let Some(v) = value {
                    better_value = sanitize_single_line(v);
                    value = Some(&better_value);
                }
                self.sql.set_raw_config(key.as_ref(), value).await?;
            }
            Config::Addr => {
                self.sql
                    .set_raw_config(key.as_ref(), value.map(|s| s.to_lowercase()).as_deref())
                    .await?;
            }
            Config::MvboxMove => {
                self.sql.set_raw_config(key.as_ref(), value).await?;
                self.sql
                    .set_raw_config(constants::DC_FOLDERS_CONFIGURED_KEY, None)
                    .await?;
            }
            _ => {
                self.sql.set_raw_config(key.as_ref(), value).await?;
            }
        }
        if key.is_synced() {
            self.emit_event(EventType::ConfigSynced { key });
        }
        if !sync {
            return Ok(());
        }
        let Some(val) = value else {
            return Ok(());
        };
        let val = val.to_string();
        if self
            .add_sync_item(SyncData::Config { key, val })
            .await
            .log_err(self)
            .is_err()
        {
            return Ok(());
        }
        self.scheduler.interrupt_inbox().await;
        Ok(())
    }

    /// Set the given config to an unsigned 32-bit integer value.
    pub async fn set_config_u32(&self, key: Config, value: u32) -> Result<()> {
        self.set_config(key, Some(&value.to_string())).await?;
        Ok(())
    }

    /// Set the given config to a boolean value.
    pub async fn set_config_bool(&self, key: Config, value: bool) -> Result<()> {
        self.set_config(key, from_bool(value)).await?;
        Ok(())
    }

    /// Sets an ui-specific key-value pair.
    /// Keys must be prefixed by `ui.`
    /// and should be followed by the name of the system and maybe subsystem,
    /// eg. `ui.desktop.linux.foo`, `ui.desktop.macos.bar`, `ui.ios.foobar`.
    pub async fn set_ui_config(&self, key: &str, value: Option<&str>) -> Result<()> {
        ensure!(key.starts_with("ui."), "set_ui_config(): prefix missing.");
        self.sql.set_raw_config(key, value).await
    }

    /// Gets an ui-specific value set by set_ui_config().
    pub async fn get_ui_config(&self, key: &str) -> Result<Option<String>> {
        ensure!(key.starts_with("ui."), "get_ui_config(): prefix missing.");
        self.sql.get_raw_config(key).await
    }
}

/// Returns a value for use in `Context::set_config_*()` for the given `bool`.
pub(crate) fn from_bool(val: bool) -> Option<&'static str> {
    Some(if val { "1" } else { "0" })
}

// Separate impl block for self address handling
impl Context {
    /// Determine whether the specified addr maps to the/a self addr.
    /// Returns `false` if no addresses are configured.
    pub(crate) async fn is_self_addr(&self, addr: &str) -> Result<bool> {
        Ok(self
            .get_config(Config::ConfiguredAddr)
            .await?
            .iter()
            .any(|a| addr_cmp(addr, a))
            || self
                .get_secondary_self_addrs()
                .await?
                .iter()
                .any(|a| addr_cmp(addr, a)))
    }

    /// Sets `primary_new` as the new primary self address and saves the old
    /// primary address (if exists) as a secondary address.
    ///
    /// This should only be used by test code and during configure.
    pub(crate) async fn set_primary_self_addr(&self, primary_new: &str) -> Result<()> {
        self.quota.write().await.take();

        // add old primary address (if exists) to secondary addresses
        let mut secondary_addrs = self.get_all_self_addrs().await?;
        // never store a primary address also as a secondary
        secondary_addrs.retain(|a| !addr_cmp(a, primary_new));
        self.set_config_internal(
            Config::SecondaryAddrs,
            Some(secondary_addrs.join(" ").as_str()),
        )
        .await?;

        self.set_config_internal(Config::ConfiguredAddr, Some(primary_new))
            .await?;
        self.emit_event(EventType::ConnectivityChanged);
        Ok(())
    }

    /// Returns all primary and secondary self addresses.
    pub(crate) async fn get_all_self_addrs(&self) -> Result<Vec<String>> {
        let primary_addrs = self.get_config(Config::ConfiguredAddr).await?.into_iter();
        let secondary_addrs = self.get_secondary_self_addrs().await?.into_iter();

        Ok(primary_addrs.chain(secondary_addrs).collect())
    }

    /// Returns all secondary self addresses.
    pub(crate) async fn get_secondary_self_addrs(&self) -> Result<Vec<String>> {
        let secondary_addrs = self
            .get_config(Config::SecondaryAddrs)
            .await?
            .unwrap_or_default();
        Ok(secondary_addrs
            .split_ascii_whitespace()
            .map(|s| s.to_string())
            .collect())
    }

    /// Returns the primary self address.
    /// Returns an error if no self addr is configured.
    pub async fn get_primary_self_addr(&self) -> Result<String> {
        self.get_config(Config::ConfiguredAddr)
            .await?
            .context("No self addr configured")
    }
}

/// Returns all available configuration keys concated together.
fn get_config_keys_string() -> String {
    let keys = Config::iter().fold(String::new(), |mut acc, key| {
        acc += key.as_ref();
        acc += " ";
        acc
    });

    format!(" {keys} ")
}

#[cfg(test)]
mod tests {
    use num_traits::FromPrimitive;

    use super::*;
    use crate::test_utils::{sync, TestContext, TestContextManager};

    #[test]
    fn test_to_string() {
        assert_eq!(Config::MailServer.to_string(), "mail_server");
        assert_eq!(Config::from_str("mail_server"), Ok(Config::MailServer));

        assert_eq!(Config::SysConfigKeys.to_string(), "sys.config_keys");
        assert_eq!(
            Config::from_str("sys.config_keys"),
            Ok(Config::SysConfigKeys)
        );
    }

    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    async fn test_set_config_addr() {
        let t = TestContext::new().await;

        // Test that uppercase address get lowercased.
        assert!(t
            .set_config(Config::Addr, Some("Foobar@eXample.oRg"))
            .await
            .is_ok());
        assert_eq!(
            t.get_config(Config::Addr).await.unwrap().unwrap(),
            "foobar@example.org"
        );
    }

    /// Tests that "bot" config can only be set to "0" or "1".
    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    async fn test_set_config_bot() {
        let t = TestContext::new().await;

        assert!(t.set_config(Config::Bot, None).await.is_ok());
        assert!(t.set_config(Config::Bot, Some("0")).await.is_ok());
        assert!(t.set_config(Config::Bot, Some("1")).await.is_ok());
        assert!(t.set_config(Config::Bot, Some("2")).await.is_err());
        assert!(t.set_config(Config::Bot, Some("Foobar")).await.is_err());
    }

    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    async fn test_media_quality_config_option() {
        let t = TestContext::new().await;
        let media_quality = t.get_config_int(Config::MediaQuality).await.unwrap();
        assert_eq!(media_quality, 0);
        let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default();
        assert_eq!(media_quality, constants::MediaQuality::Balanced);

        t.set_config(Config::MediaQuality, Some("1")).await.unwrap();

        let media_quality = t.get_config_int(Config::MediaQuality).await.unwrap();
        assert_eq!(media_quality, 1);
        assert_eq!(constants::MediaQuality::Worse as i32, 1);
        let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default();
        assert_eq!(media_quality, constants::MediaQuality::Worse);
    }

    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    async fn test_ui_config() -> Result<()> {
        let t = TestContext::new().await;

        assert_eq!(t.get_ui_config("ui.desktop.linux.systray").await?, None);

        t.set_ui_config("ui.android.screen_security", Some("safe"))
            .await?;
        assert_eq!(
            t.get_ui_config("ui.android.screen_security").await?,
            Some("safe".to_string())
        );

        t.set_ui_config("ui.android.screen_security", None).await?;
        assert_eq!(t.get_ui_config("ui.android.screen_security").await?, None);

        assert!(t.set_ui_config("configured", Some("bar")).await.is_err());

        Ok(())
    }

    /// Regression test for https://github.com/deltachat/deltachat-core-rust/issues/3012
    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    async fn test_set_config_bool() -> Result<()> {
        let t = TestContext::new().await;

        // We need some config that defaults to true
        let c = Config::E2eeEnabled;
        assert_eq!(t.get_config_bool(c).await?, true);
        t.set_config_bool(c, false).await?;
        assert_eq!(t.get_config_bool(c).await?, false);
        Ok(())
    }

    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    async fn test_self_addrs() -> Result<()> {
        let alice = TestContext::new_alice().await;

        assert!(alice.is_self_addr("alice@example.org").await?);
        assert_eq!(alice.get_all_self_addrs().await?, vec!["alice@example.org"]);
        assert!(!alice.is_self_addr("alice@alice.com").await?);

        // Test adding the same primary address
        alice.set_primary_self_addr("alice@example.org").await?;
        alice.set_primary_self_addr("Alice@Example.Org").await?;
        assert_eq!(alice.get_all_self_addrs().await?, vec!["Alice@Example.Org"]);

        // Test adding a new (primary) self address
        // The address is trimmed during configure by `LoginParam::from_database()`,
        // so `set_primary_self_addr()` doesn't have to trim it.
        alice.set_primary_self_addr("Alice@alice.com").await?;
        assert!(alice.is_self_addr("aliCe@example.org").await?);
        assert!(alice.is_self_addr("alice@alice.com").await?);
        assert_eq!(
            alice.get_all_self_addrs().await?,
            vec!["Alice@alice.com", "Alice@Example.Org"]
        );

        // Check that the entry is not duplicated
        alice.set_primary_self_addr("alice@alice.com").await?;
        alice.set_primary_self_addr("alice@alice.com").await?;
        assert_eq!(
            alice.get_all_self_addrs().await?,
            vec!["alice@alice.com", "Alice@Example.Org"]
        );

        // Test switching back
        alice.set_primary_self_addr("alice@example.org").await?;
        assert_eq!(
            alice.get_all_self_addrs().await?,
            vec!["alice@example.org", "alice@alice.com"]
        );

        // Test setting a new primary self address, the previous self address
        // should be kept as a secondary self address
        alice.set_primary_self_addr("alice@alice.xyz").await?;
        assert_eq!(
            alice.get_all_self_addrs().await?,
            vec!["alice@alice.xyz", "alice@example.org", "alice@alice.com"]
        );
        assert!(alice.is_self_addr("alice@example.org").await?);
        assert!(alice.is_self_addr("alice@alice.com").await?);
        assert!(alice.is_self_addr("Alice@alice.xyz").await?);

        Ok(())
    }

    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    async fn test_mdns_default_behaviour() -> Result<()> {
        let t = &TestContext::new_alice().await;
        assert!(t.should_request_mdns().await?);
        assert!(t.should_send_mdns().await?);
        assert!(t.get_config_bool_opt(Config::MdnsEnabled).await?.is_none());
        // The setting should be displayed correctly.
        assert!(t.get_config_bool(Config::MdnsEnabled).await?);

        t.set_config_bool(Config::Bot, true).await?;
        assert!(!t.should_request_mdns().await?);
        assert!(t.should_send_mdns().await?);
        assert!(t.get_config_bool_opt(Config::MdnsEnabled).await?.is_none());
        assert!(t.get_config_bool(Config::MdnsEnabled).await?);
        Ok(())
    }

    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    async fn test_sync() -> Result<()> {
        let alice0 = TestContext::new_alice().await;
        let alice1 = TestContext::new_alice().await;
        for a in [&alice0, &alice1] {
            a.set_config_bool(Config::SyncMsgs, true).await?;
        }

        let mdns_enabled = alice0.get_config_bool(Config::MdnsEnabled).await?;
        // Alice1 has a different config value.
        alice1
            .set_config_bool(Config::MdnsEnabled, !mdns_enabled)
            .await?;
        // This changes nothing, but still sends a sync message.
        alice0
            .set_config_bool(Config::MdnsEnabled, mdns_enabled)
            .await?;
        sync(&alice0, &alice1).await;
        assert_eq!(
            alice1.get_config_bool(Config::MdnsEnabled).await?,
            mdns_enabled
        );

        // Reset to default. Test that it's not synced because defaults may differ across client
        // versions.
        alice0.set_config(Config::MdnsEnabled, None).await?;
        alice0.set_config_bool(Config::MdnsEnabled, false).await?;
        sync(&alice0, &alice1).await;
        assert_eq!(alice1.get_config_bool(Config::MdnsEnabled).await?, false);

        for key in [Config::ShowEmails, Config::MvboxMove] {
            let val = alice0.get_config_bool(key).await?;
            alice0.set_config_bool(key, !val).await?;
            sync(&alice0, &alice1).await;
            assert_eq!(alice1.get_config_bool(key).await?, !val);
        }

        // `Config::SyncMsgs` mustn't be synced.
        alice0.set_config_bool(Config::SyncMsgs, false).await?;
        alice0.set_config_bool(Config::SyncMsgs, true).await?;
        alice0.set_config_bool(Config::MdnsEnabled, true).await?;
        sync(&alice0, &alice1).await;
        assert!(alice1.get_config_bool(Config::MdnsEnabled).await?);

        // Usual sync scenario.
        async fn test_config_str(
            alice0: &TestContext,
            alice1: &TestContext,
            key: Config,
            val: &str,
        ) -> Result<()> {
            alice0.set_config(key, Some(val)).await?;
            sync(alice0, alice1).await;
            assert_eq!(alice1.get_config(key).await?, Some(val.to_string()));
            Ok(())
        }
        test_config_str(&alice0, &alice1, Config::Displayname, "Alice Sync").await?;
        test_config_str(&alice0, &alice1, Config::Selfstatus, "My status").await?;

        assert!(alice0.get_config(Config::Selfavatar).await?.is_none());
        let file = alice0.dir.path().join("avatar.png");
        let bytes = include_bytes!("../test-data/image/avatar64x64.png");
        tokio::fs::write(&file, bytes).await?;
        alice0
            .set_config(Config::Selfavatar, Some(file.to_str().unwrap()))
            .await?;
        sync(&alice0, &alice1).await;
        // There was a bug that a sync message creates the self-chat with the user avatar instead of
        // the special icon and that remains so when the self-chat becomes user-visible. Let's check
        // this.
        let self_chat = alice0.get_self_chat().await;
        let self_chat_avatar_path = self_chat.get_profile_image(&alice0).await?.unwrap();
        assert_eq!(
            self_chat_avatar_path,
            alice0.get_blobdir().join("icon-saved-messages.png")
        );
        assert!(alice1
            .get_config(Config::Selfavatar)
            .await?
            .filter(|path| path.ends_with(".png"))
            .is_some());
        alice0.set_config(Config::Selfavatar, None).await?;
        sync(&alice0, &alice1).await;
        assert!(alice1.get_config(Config::Selfavatar).await?.is_none());

        Ok(())
    }

    /// Sync message mustn't be sent if self-{status,avatar} is changed by a self-sent message.
    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    async fn test_no_sync_on_self_sent_msg() -> Result<()> {
        let mut tcm = TestContextManager::new();
        let alice0 = &tcm.alice().await;
        let alice1 = &tcm.alice().await;
        for a in [alice0, alice1] {
            a.set_config_bool(Config::SyncMsgs, true).await?;
        }

        let status = "Synced via usual message";
        alice0.set_config(Config::Selfstatus, Some(status)).await?;
        alice0.send_sync_msg().await?;
        alice0.pop_sent_sync_msg().await;
        let status1 = "Synced via sync message";
        alice1.set_config(Config::Selfstatus, Some(status1)).await?;
        tcm.send_recv(alice0, alice1, "hi Alice!").await;
        assert_eq!(
            alice1.get_config(Config::Selfstatus).await?,
            Some(status.to_string())
        );
        sync(alice1, alice0).await;
        assert_eq!(
            alice0.get_config(Config::Selfstatus).await?,
            Some(status1.to_string())
        );

        // Need a chat with another contact to send self-avatar.
        let bob = &tcm.bob().await;
        let a0b_chat_id = tcm.send_recv_accept(bob, alice0, "hi").await.chat_id;
        let file = alice0.dir.path().join("avatar.png");
        let bytes = include_bytes!("../test-data/image/avatar64x64.png");
        tokio::fs::write(&file, bytes).await?;
        alice0
            .set_config(Config::Selfavatar, Some(file.to_str().unwrap()))
            .await?;
        alice0.send_sync_msg().await?;
        alice0.pop_sent_sync_msg().await;
        let file = alice1.dir.path().join("avatar.jpg");
        let bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
        tokio::fs::write(&file, bytes).await?;
        alice1
            .set_config(Config::Selfavatar, Some(file.to_str().unwrap()))
            .await?;
        let sent_msg = alice0.send_text(a0b_chat_id, "hi").await;
        alice1.recv_msg(&sent_msg).await;
        assert!(alice1
            .get_config(Config::Selfavatar)
            .await?
            .filter(|path| path.ends_with(".png"))
            .is_some());
        sync(alice1, alice0).await;
        assert!(alice0
            .get_config(Config::Selfavatar)
            .await?
            .filter(|path| path.ends_with(".jpg"))
            .is_some());

        Ok(())
    }

    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    async fn test_event_config_synced() -> Result<()> {
        let alice0 = TestContext::new_alice().await;
        let alice1 = TestContext::new_alice().await;
        for a in [&alice0, &alice1] {
            a.set_config_bool(Config::SyncMsgs, true).await?;
        }

        alice0
            .set_config(Config::Displayname, Some("Alice Sync"))
            .await?;
        alice0
            .evtracker
            .get_matching(|e| {
                matches!(
                    e,
                    EventType::ConfigSynced {
                        key: Config::Displayname
                    }
                )
            })
            .await;
        sync(&alice0, &alice1).await;
        assert_eq!(
            alice1.get_config(Config::Displayname).await?,
            Some("Alice Sync".to_string())
        );
        alice1
            .evtracker
            .get_matching(|e| {
                matches!(
                    e,
                    EventType::ConfigSynced {
                        key: Config::Displayname
                    }
                )
            })
            .await;

        alice0.set_config(Config::Displayname, None).await?;
        alice0
            .evtracker
            .get_matching(|e| {
                matches!(
                    e,
                    EventType::ConfigSynced {
                        key: Config::Displayname
                    }
                )
            })
            .await;

        Ok(())
    }
}