1use std::ffi::OsStr;
4use std::path::{Path, PathBuf};
5use std::pin::Pin;
6
7use anyhow::{Context as _, Result, bail, ensure, format_err};
8use futures::TryStreamExt;
9use futures_lite::FutureExt;
10use pin_project::pin_project;
11
12use tokio::fs::{self, File};
13use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
14use tokio_tar::Archive;
15
16use crate::blob::BlobDirContents;
17use crate::chat::delete_and_reset_all_device_msgs;
18use crate::config::Config;
19use crate::context::Context;
20use crate::e2ee;
21use crate::events::EventType;
22use crate::key::{self, DcKey, SignedSecretKey};
23use crate::log::{LogExt, warn};
24use crate::qr::DCBACKUP_VERSION;
25use crate::sql;
26use crate::tools::{
27 TempPathGuard, create_folder, delete_file, get_filesuffix_lc, read_file, time, usize_to_u64,
28 write_file,
29};
30
31mod transfer;
32
33use ::pgp::types::KeyDetails;
34pub use transfer::{BackupProvider, get_backup};
35
36const DBFILE_BACKUP_NAME: &str = "dc_database_backup.sqlite";
38pub(crate) const BLOBS_BACKUP_NAME: &str = "blobs_backup";
39
40#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive)]
42#[repr(u32)]
43pub enum ImexMode {
44 ExportSelfKeys = 1,
49
50 ImportSelfKeys = 2,
55
56 ExportBackup = 11,
61
62 ImportBackup = 12,
66}
67
68pub async fn imex(
83 context: &Context,
84 what: ImexMode,
85 path: &Path,
86 passphrase: Option<String>,
87) -> Result<()> {
88 let cancel = context.alloc_ongoing().await?;
89
90 let res = {
91 let _guard = context.scheduler.pause(context).await?;
92 imex_inner(context, what, path, passphrase)
93 .race(async {
94 cancel.recv().await.ok();
95 Err(format_err!("canceled"))
96 })
97 .await
98 };
99 context.free_ongoing().await;
100
101 if let Err(err) = res.as_ref() {
102 error!(context, "{:#}", err);
104 warn!(context, "IMEX failed to complete: {:#}", err);
105 context.emit_event(EventType::ImexProgress(0));
106 } else {
107 info!(context, "IMEX successfully completed");
108 context.emit_event(EventType::ImexProgress(1000));
109 }
110
111 res
112}
113
114pub async fn has_backup(_context: &Context, dir_name: &Path) -> Result<String> {
116 let mut dir_iter = tokio::fs::read_dir(dir_name).await?;
117 let mut newest_backup_name = "".to_string();
118 let mut newest_backup_path: Option<PathBuf> = None;
119
120 while let Ok(Some(dirent)) = dir_iter.next_entry().await {
121 let path = dirent.path();
122 let name = dirent.file_name();
123 let name: String = name.to_string_lossy().into();
124 if name.starts_with("delta-chat")
125 && name.ends_with(".tar")
126 && (newest_backup_name.is_empty() || name > newest_backup_name)
127 {
128 newest_backup_path = Some(path);
131 newest_backup_name = name;
132 }
133 }
134
135 match newest_backup_path {
136 Some(path) => Ok(path.to_string_lossy().into_owned()),
137 None => bail!("no backup found in {}", dir_name.display()),
138 }
139}
140
141async fn set_self_key(context: &Context, armored: &str) -> Result<()> {
142 let secret_key = SignedSecretKey::from_asc(armored)?;
143 key::store_self_keypair(context, &secret_key).await?;
144
145 info!(
146 context,
147 "Stored self key: {:?}.",
148 secret_key.public_key().fingerprint()
149 );
150 Ok(())
151}
152
153async fn imex_inner(
154 context: &Context,
155 what: ImexMode,
156 path: &Path,
157 passphrase: Option<String>,
158) -> Result<()> {
159 info!(
160 context,
161 "{} path: {}",
162 match what {
163 ImexMode::ExportSelfKeys | ImexMode::ExportBackup => "Export",
164 ImexMode::ImportSelfKeys | ImexMode::ImportBackup => "Import",
165 },
166 path.display()
167 );
168 ensure!(context.sql.is_open().await, "Database not opened.");
169 context.emit_event(EventType::ImexProgress(1));
170
171 if what == ImexMode::ExportBackup || what == ImexMode::ExportSelfKeys {
172 e2ee::ensure_secret_key_exists(context)
174 .await
175 .context("Cannot create private key or private key not available")?;
176
177 create_folder(context, path).await?;
178 }
179
180 match what {
181 ImexMode::ExportSelfKeys => export_self_keys(context, path).await,
182 ImexMode::ImportSelfKeys => import_self_keys(context, path).await,
183
184 ImexMode::ExportBackup => {
185 export_backup(context, path, passphrase.unwrap_or_default()).await
186 }
187 ImexMode::ImportBackup => {
188 import_backup(context, path, passphrase.unwrap_or_default()).await
189 }
190 }
191}
192
193async fn import_backup(
200 context: &Context,
201 backup_to_import: &Path,
202 passphrase: String,
203) -> Result<()> {
204 let backup_file = File::open(backup_to_import).await?;
205 let file_size = backup_file.metadata().await?.len();
206 info!(
207 context,
208 "Import \"{}\" ({} bytes) to \"{}\".",
209 backup_to_import.display(),
210 file_size,
211 context.get_dbfile().display()
212 );
213
214 import_backup_stream(context, backup_file, file_size, passphrase).await?;
215 Ok(())
216}
217
218pub(crate) async fn import_backup_stream<R: tokio::io::AsyncRead + Unpin>(
232 context: &Context,
233 backup_file: R,
234 file_size: u64,
235 passphrase: String,
236) -> Result<()> {
237 ensure!(
238 !context.is_configured().await?,
239 "Cannot import backups to accounts in use"
240 );
241 ensure!(
242 !context.scheduler.is_running().await,
243 "Cannot import backup, IO is running"
244 );
245
246 import_backup_stream_inner(context, backup_file, file_size, passphrase)
247 .await
248 .0
249}
250
251#[pin_project]
253struct ProgressReader<R> {
254 #[pin]
256 inner: R,
257
258 read: u64,
260
261 file_size: u64,
264
265 last_progress: u16,
267
268 context: Context,
270}
271
272impl<R> ProgressReader<R> {
273 fn new(r: R, context: Context, file_size: u64) -> Self {
274 Self {
275 inner: r,
276 read: 0,
277 file_size,
278 last_progress: 1,
279 context,
280 }
281 }
282}
283
284impl<R> AsyncRead for ProgressReader<R>
285where
286 R: AsyncRead,
287{
288 #[expect(clippy::arithmetic_side_effects)]
289 fn poll_read(
290 self: Pin<&mut Self>,
291 cx: &mut std::task::Context<'_>,
292 buf: &mut ReadBuf<'_>,
293 ) -> std::task::Poll<std::io::Result<()>> {
294 let this = self.project();
295 let before = buf.filled().len();
296 let res = this.inner.poll_read(cx, buf);
297 if let std::task::Poll::Ready(Ok(())) = res {
298 *this.read = this
299 .read
300 .saturating_add(usize_to_u64(buf.filled().len() - before));
301
302 let progress = std::cmp::min(1000 * *this.read / *this.file_size, 999) as u16;
303 if progress > *this.last_progress {
304 this.context.emit_event(EventType::ImexProgress(progress));
305 *this.last_progress = progress;
306 }
307 }
308 res
309 }
310}
311
312async fn import_backup_stream_inner<R: tokio::io::AsyncRead + Unpin>(
316 context: &Context,
317 backup_file: R,
318 file_size: u64,
319 passphrase: String,
320) -> (Result<()>,) {
321 let backup_file = ProgressReader::new(backup_file, context.clone(), file_size);
322 let mut archive = Archive::new(backup_file);
323
324 let mut entries = match archive.entries() {
325 Ok(entries) => entries,
326 Err(e) => return (Err(e).context("Failed to get archive entries"),),
327 };
328 let mut blobs = Vec::new();
329 let mut res: Result<()> = loop {
330 let mut f = match entries.try_next().await {
331 Ok(Some(f)) => f,
332 Ok(None) => break Ok(()),
333 Err(e) => break Err(e).context("Failed to get next entry"),
334 };
335
336 let path = match f.path() {
337 Ok(path) => path.to_path_buf(),
338 Err(e) => break Err(e).context("Failed to get entry path"),
339 };
340 if let Err(e) = f.unpack_in(context.get_blobdir()).await {
341 break Err(e).context("Failed to unpack file");
342 }
343 if path.file_name() == Some(OsStr::new(DBFILE_BACKUP_NAME)) {
344 continue;
345 }
346 let from_path = context.get_blobdir().join(&path);
348 if from_path.is_file() {
349 if let Some(name) = from_path.file_name() {
350 let to_path = context.get_blobdir().join(name);
351 if let Err(e) = fs::rename(&from_path, &to_path).await {
352 blobs.push(from_path);
353 break Err(e).context("Failed to move file to blobdir");
354 }
355 blobs.push(to_path);
356 } else {
357 warn!(context, "No file name");
358 }
359 }
360 };
361
362 let unpacked_database = context.get_blobdir().join(DBFILE_BACKUP_NAME);
363 if res.is_ok() {
364 res = context
365 .sql
366 .import(&unpacked_database, passphrase.clone())
367 .await
368 .context("cannot import unpacked database");
369 }
370 if res.is_ok() {
371 res = check_backup_version(context).await;
372 }
373 fs::remove_file(unpacked_database)
374 .await
375 .context("cannot remove unpacked database")
376 .log_err(context)
377 .ok();
378 if res.is_ok() {
379 context.emit_event(EventType::ImexProgress(999));
380 res = context.sql.run_migrations(context).await;
381 context.emit_event(EventType::AccountsItemChanged);
382 }
383 if res.is_err() {
384 context.sql.close().await;
385 fs::remove_file(context.sql.dbfile.as_path())
386 .await
387 .log_err(context)
388 .ok();
389 for blob in blobs {
390 fs::remove_file(&blob).await.log_err(context).ok();
391 }
392 context
393 .sql
394 .open(context, "".to_string())
395 .await
396 .log_err(context)
397 .ok();
398 }
399 if res.is_ok() {
400 delete_and_reset_all_device_msgs(context)
401 .await
402 .log_err(context)
403 .ok();
404 }
405 (res,)
406}
407
408fn get_next_backup_path(
416 folder: &Path,
417 addr: &str,
418 backup_time: i64,
419) -> Result<(PathBuf, PathBuf, PathBuf)> {
420 let folder = PathBuf::from(folder);
421 let stem = chrono::DateTime::<chrono::Utc>::from_timestamp(backup_time, 0)
422 .context("can't get next backup path")?
423 .format("delta-chat-backup-%Y-%m-%d")
425 .to_string();
426
427 for i in 0..64 {
429 let mut tempdbfile = folder.clone();
430 tempdbfile.push(format!("{stem}-{i:02}-{addr}.db"));
431
432 let mut tempfile = folder.clone();
433 tempfile.push(format!("{stem}-{i:02}-{addr}.tar.part"));
434
435 let mut destfile = folder.clone();
436 destfile.push(format!("{stem}-{i:02}-{addr}.tar"));
437
438 if !tempdbfile.exists() && !tempfile.exists() && !destfile.exists() {
439 return Ok((tempdbfile, tempfile, destfile));
440 }
441 }
442 bail!("could not create backup file, disk full?");
443}
444
445#[expect(clippy::arithmetic_side_effects)]
449async fn export_backup(context: &Context, dir: &Path, passphrase: String) -> Result<()> {
450 let now = time();
452 let self_addr = context.get_primary_self_addr().await?;
453 let (temp_db_path, temp_path, dest_path) = get_next_backup_path(dir, &self_addr, now)?;
454 let temp_db_path = TempPathGuard::new(temp_db_path);
455 let temp_path = TempPathGuard::new(temp_path);
456
457 export_database(context, &temp_db_path, passphrase, now)
458 .await
459 .context("could not export database")?;
460
461 info!(
462 context,
463 "Backup '{}' to '{}'.",
464 context.get_dbfile().display(),
465 dest_path.display(),
466 );
467
468 let file = File::create(&temp_path).await?;
469 let blobdir = BlobDirContents::new(context).await?;
470
471 let mut file_size = 0;
472 file_size += temp_db_path.metadata()?.len();
473 for blob in blobdir.iter() {
474 file_size += blob.to_abs_path().metadata()?.len()
475 }
476
477 export_backup_stream(context, &temp_db_path, blobdir, file, file_size)
478 .await
479 .context("Exporting backup to file failed")?;
480 fs::rename(temp_path, &dest_path).await?;
481 context.emit_event(EventType::ImexFileWritten(dest_path));
482 Ok(())
483}
484
485#[pin_project]
487struct ProgressWriter<W> {
488 #[pin]
490 inner: W,
491
492 written: u64,
494
495 file_size: u64,
498
499 last_progress: u16,
501
502 context: Context,
504}
505
506impl<W> ProgressWriter<W> {
507 fn new(w: W, context: Context, file_size: u64) -> Self {
508 Self {
509 inner: w,
510 written: 0,
511 file_size,
512 last_progress: 1,
513 context,
514 }
515 }
516}
517
518impl<W> AsyncWrite for ProgressWriter<W>
519where
520 W: AsyncWrite,
521{
522 #[expect(clippy::arithmetic_side_effects)]
523 fn poll_write(
524 self: Pin<&mut Self>,
525 cx: &mut std::task::Context<'_>,
526 buf: &[u8],
527 ) -> std::task::Poll<Result<usize, std::io::Error>> {
528 let this = self.project();
529 let res = this.inner.poll_write(cx, buf);
530 if let std::task::Poll::Ready(Ok(written)) = res {
531 *this.written = this.written.saturating_add(usize_to_u64(written));
532
533 let progress = std::cmp::min(1000 * *this.written / *this.file_size, 999) as u16;
534 if progress > *this.last_progress {
535 this.context.emit_event(EventType::ImexProgress(progress));
536 *this.last_progress = progress;
537 }
538 }
539 res
540 }
541
542 fn poll_flush(
543 self: Pin<&mut Self>,
544 cx: &mut std::task::Context<'_>,
545 ) -> std::task::Poll<Result<(), std::io::Error>> {
546 self.project().inner.poll_flush(cx)
547 }
548
549 fn poll_shutdown(
550 self: Pin<&mut Self>,
551 cx: &mut std::task::Context<'_>,
552 ) -> std::task::Poll<Result<(), std::io::Error>> {
553 self.project().inner.poll_shutdown(cx)
554 }
555}
556
557pub(crate) async fn export_backup_stream<'a, W>(
559 context: &'a Context,
560 temp_db_path: &Path,
561 blobdir: BlobDirContents<'a>,
562 writer: W,
563 file_size: u64,
564) -> Result<()>
565where
566 W: tokio::io::AsyncWrite + tokio::io::AsyncWriteExt + Unpin + Send + 'static,
567{
568 let writer = ProgressWriter::new(writer, context.clone(), file_size);
569 let mut builder = tokio_tar::Builder::new(writer);
570
571 builder
572 .append_path_with_name(temp_db_path, DBFILE_BACKUP_NAME)
573 .await?;
574
575 for blob in blobdir.iter() {
576 let mut file = File::open(blob.to_abs_path()).await?;
577 let path_in_archive = PathBuf::from(BLOBS_BACKUP_NAME).join(blob.as_name());
578 builder.append_file(path_in_archive, &mut file).await?;
579 }
580
581 builder.finish().await?;
582 Ok(())
583}
584
585async fn import_secret_key(context: &Context, path: &Path) -> Result<()> {
587 let buf = read_file(context, path).await?;
588 let armored = std::string::String::from_utf8_lossy(&buf);
589 set_self_key(context, &armored).await?;
590 Ok(())
591}
592
593#[expect(clippy::arithmetic_side_effects)]
603async fn import_self_keys(context: &Context, path: &Path) -> Result<()> {
604 let attr = tokio::fs::metadata(path).await?;
605
606 if attr.is_file() {
607 info!(
608 context,
609 "Importing secret key from {} as the default key.",
610 path.display()
611 );
612 import_secret_key(context, path).await?;
613 return Ok(());
614 }
615
616 let mut imported_cnt = 0;
617
618 let mut dir_handle = tokio::fs::read_dir(&path).await?;
619 while let Ok(Some(entry)) = dir_handle.next_entry().await {
620 let entry_fn = entry.file_name();
621 let name_f = entry_fn.to_string_lossy();
622 let path_plus_name = path.join(&entry_fn);
623 if let Some(suffix) = get_filesuffix_lc(&name_f) {
624 if suffix != "asc" {
625 continue;
626 }
627 } else {
628 continue;
629 };
630 info!(
631 context,
632 "Considering key file: {}.",
633 path_plus_name.display()
634 );
635
636 if let Err(err) = import_secret_key(context, &path_plus_name).await {
637 warn!(
638 context,
639 "Failed to import secret key from {}: {:#}.",
640 path_plus_name.display(),
641 err
642 );
643 continue;
644 }
645
646 imported_cnt += 1;
647 }
648 ensure!(
649 imported_cnt > 0,
650 "No private keys found in {}.",
651 path.display()
652 );
653 Ok(())
654}
655
656#[expect(clippy::arithmetic_side_effects)]
657async fn export_self_keys(context: &Context, dir: &Path) -> Result<()> {
658 let mut export_errors = 0;
659
660 let keys = context
661 .sql
662 .query_map_vec(
663 "SELECT id, private_key, id=(SELECT value FROM config WHERE keyname='key_id') FROM keypairs;",
664 (),
665 |row| {
666 let id = row.get(0)?;
667 let private_key_blob: Vec<u8> = row.get(1)?;
668 let private_key = SignedSecretKey::from_slice(&private_key_blob);
669 let is_default: i32 = row.get(2)?;
670
671 Ok((id, private_key, is_default))
672 },
673 )
674 .await?;
675 let self_addr = context.get_primary_self_addr().await?;
676 for (id, private_key, is_default) in keys {
677 let id = Some(id).filter(|_| is_default == 0);
678
679 let Ok(private_key) = private_key else {
680 export_errors += 1;
681 continue;
682 };
683
684 if let Err(err) = export_key_to_asc_file(context, dir, &self_addr, id, &private_key).await {
685 error!(context, "Failed to export private key: {:#}.", err);
686 export_errors += 1;
687 }
688
689 let public_key = private_key.to_public_key();
690
691 if let Err(err) = export_key_to_asc_file(context, dir, &self_addr, id, &public_key).await {
692 error!(context, "Failed to export public key: {:#}.", err);
693 export_errors += 1;
694 }
695 }
696
697 ensure!(export_errors == 0, "errors while exporting keys");
698 Ok(())
699}
700
701async fn export_key_to_asc_file<T>(
703 context: &Context,
704 dir: &Path,
705 addr: &str,
706 id: Option<i64>,
707 key: &T,
708) -> Result<String>
709where
710 T: DcKey,
711{
712 let file_name = {
713 let kind = match T::is_private() {
714 false => "public",
715 true => "private",
716 };
717 let id = id.map_or("default".into(), |i| i.to_string());
718 let fp = key.dc_fingerprint().hex();
719 format!("{kind}-key-{addr}-{id}-{fp}.asc")
720 };
721 let path = dir.join(&file_name);
722 info!(context, "Exporting key to {}.", path.display());
723
724 delete_file(context, &path).await.ok();
726
727 let content = key.to_asc(None).into_bytes();
728 write_file(context, &path, &content)
729 .await
730 .with_context(|| format!("cannot write key to {}", path.display()))?;
731 context.emit_event(EventType::ImexFileWritten(path));
732 Ok(file_name)
733}
734
735async fn export_database(
742 context: &Context,
743 dest: &Path,
744 passphrase: String,
745 timestamp: i64,
746) -> Result<()> {
747 ensure!(
748 !context.scheduler.is_running().await,
749 "cannot export backup, IO is running"
750 );
751 let timestamp = timestamp.try_into().context("32-bit UNIX time overflow")?;
752
753 let dest = dest
755 .to_str()
756 .with_context(|| format!("path {} is not valid unicode", dest.display()))?;
757
758 context.set_config(Config::BccSelf, Some("1")).await?;
759 context
760 .sql
761 .set_raw_config_int("backup_time", timestamp)
762 .await?;
763 context
764 .sql
765 .set_raw_config_int("backup_version", DCBACKUP_VERSION)
766 .await?;
767 sql::housekeeping(context).await.log_err(context).ok();
768 context
769 .sql
770 .call_write(|conn| {
771 conn.execute("VACUUM;", ())
772 .map_err(|err| warn!(context, "Vacuum failed, exporting anyway {err}"))
773 .ok();
774 conn.execute("ATTACH DATABASE ? AS backup KEY ?", (dest, passphrase))
775 .context("failed to attach backup database")?;
776 let res = conn
777 .query_row("SELECT sqlcipher_export('backup')", [], |_row| Ok(()))
778 .context("failed to export to attached backup database");
779 conn.execute(
780 "UPDATE backup.config SET value='0' WHERE keyname='verified_one_on_one_chats';",
781 [],
782 )
783 .ok(); conn.execute("DETACH DATABASE backup", [])
785 .context("failed to detach backup database")?;
786 res?;
787 Ok(())
788 })
789 .await
790}
791
792async fn check_backup_version(context: &Context) -> Result<()> {
793 let version = (context.sql.get_raw_config_int("backup_version").await?).unwrap_or(2);
794 ensure!(
795 version <= DCBACKUP_VERSION,
796 "This profile is from a newer version of Delta Chat. Please update Delta Chat and try again (profile version is v{version}, the latest supported is v{DCBACKUP_VERSION})"
797 );
798 Ok(())
799}
800
801#[cfg(test)]
802mod tests {
803 use std::time::Duration;
804
805 use tokio::task;
806
807 use super::*;
808 use crate::config::Config;
809 use crate::test_utils::{TestContext, TestContextManager, alice_keypair};
810
811 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
812 async fn test_export_public_key_to_asc_file() {
813 let context = TestContext::new().await;
814 let key = alice_keypair().to_public_key();
815 let blobdir = Path::new("$BLOBDIR");
816 let filename = export_key_to_asc_file(&context.ctx, blobdir, "a@b", None, &key)
817 .await
818 .unwrap();
819 assert!(filename.starts_with("public-key-a@b-default-"));
820 assert!(filename.ends_with(".asc"));
821 let blobdir = context.ctx.get_blobdir().to_str().unwrap();
822 let filename = format!("{blobdir}/{filename}");
823 let bytes = tokio::fs::read(&filename).await.unwrap();
824
825 assert_eq!(bytes, key.to_asc(None).into_bytes());
826 }
827
828 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
829 async fn test_import_private_key_exported_to_asc_file() {
830 let context = TestContext::new().await;
831 let key = alice_keypair();
832 let blobdir = Path::new("$BLOBDIR");
833 let filename = export_key_to_asc_file(&context.ctx, blobdir, "a@b", None, &key)
834 .await
835 .unwrap();
836 let fingerprint = filename
837 .strip_prefix("private-key-a@b-default-")
838 .unwrap()
839 .strip_suffix(".asc")
840 .unwrap();
841 assert_eq!(fingerprint, key.dc_fingerprint().hex());
842 let blobdir = context.ctx.get_blobdir().to_str().unwrap();
843 let filename = format!("{blobdir}/{filename}");
844 let bytes = tokio::fs::read(&filename).await.unwrap();
845
846 assert_eq!(bytes, key.to_asc(None).into_bytes());
847
848 let alice = &TestContext::new().await;
849 if let Err(err) = imex(alice, ImexMode::ImportSelfKeys, Path::new(&filename), None).await {
850 panic!("got error on import: {err:#}");
851 }
852 }
853
854 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
855 async fn test_export_and_import_key_from_dir() {
856 let export_dir = tempfile::tempdir().unwrap();
857
858 let context = TestContext::new_alice().await;
859 if let Err(err) = imex(
860 &context.ctx,
861 ImexMode::ExportSelfKeys,
862 export_dir.path(),
863 None,
864 )
865 .await
866 {
867 panic!("got error on export: {err:#}");
868 }
869
870 let context2 = TestContext::new().await;
871 if let Err(err) = imex(
872 &context2.ctx,
873 ImexMode::ImportSelfKeys,
874 export_dir.path(),
875 None,
876 )
877 .await
878 {
879 panic!("got error on import: {err:#}");
880 }
881 }
882
883 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
884 async fn test_import_second_key() -> Result<()> {
885 let alice = &TestContext::new_alice().await;
886 let chat = alice.create_chat(alice).await;
887 let sent = alice.send_text(chat.id, "Encrypted with old key").await;
888 let export_dir = tempfile::tempdir().unwrap();
889
890 let alice = &TestContext::new().await;
891 alice.configure_addr("alice@example.org").await;
892 imex(alice, ImexMode::ExportSelfKeys, export_dir.path(), None).await?;
893
894 let alice = &TestContext::new_alice().await;
895 let old_key = key::load_self_secret_key(alice).await?;
896
897 assert!(
898 imex(alice, ImexMode::ImportSelfKeys, export_dir.path(), None)
899 .await
900 .is_err()
901 );
902
903 assert_eq!(key::load_self_secret_key(alice).await?, old_key);
906
907 assert_eq!(key::load_self_secret_keyring(alice).await?, vec![old_key]);
908
909 let msg = alice.recv_msg(&sent).await;
910 assert!(msg.get_showpadlock());
911 assert_eq!(msg.chat_id, alice.get_self_chat().await.id);
912 assert_eq!(msg.get_text(), "Encrypted with old key");
913
914 Ok(())
915 }
916
917 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
918 async fn test_export_and_import_backup() -> Result<()> {
919 let backup_dir = tempfile::tempdir().unwrap();
920
921 let context1 = TestContext::new_alice().await;
922 assert!(context1.is_configured().await?);
923
924 let context2 = TestContext::new().await;
925 assert!(!context2.is_configured().await?);
926 assert!(has_backup(&context2, backup_dir.path()).await.is_err());
927
928 assert!(
930 imex(&context1, ImexMode::ExportBackup, backup_dir.path(), None)
931 .await
932 .is_ok()
933 );
934 let _event = context1
935 .evtracker
936 .get_matching(|evt| matches!(evt, EventType::ImexProgress(1000)))
937 .await;
938
939 let backup = has_backup(&context2, backup_dir.path()).await?;
941
942 assert!(
944 imex(
945 &context2,
946 ImexMode::ImportBackup,
947 backup.as_ref(),
948 Some("foobar".to_string())
949 )
950 .await
951 .is_err()
952 );
953
954 assert!(
955 imex(&context2, ImexMode::ImportBackup, backup.as_ref(), None)
956 .await
957 .is_ok()
958 );
959 let _event = context2
960 .evtracker
961 .get_matching(|evt| matches!(evt, EventType::ImexProgress(1000)))
962 .await;
963
964 assert!(context2.is_configured().await?);
965 assert_eq!(
966 context2.get_config(Config::Addr).await?,
967 Some("alice@example.org".to_string())
968 );
969 Ok(())
970 }
971
972 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
973 async fn test_export_import_chatmail_backup() -> Result<()> {
974 let backup_dir = tempfile::tempdir().unwrap();
975
976 let context1 = &TestContext::new_alice().await;
977
978 context1.set_config(Config::BccSelf, None).await?;
980
981 assert_eq!(
983 context1.get_config(Config::DeleteServerAfter).await?,
984 Some("0".to_string())
985 );
986 context1.set_config_bool(Config::IsChatmail, true).await?;
987 assert_eq!(
988 context1.get_config(Config::BccSelf).await?,
989 Some("0".to_string())
990 );
991 assert_eq!(
992 context1.get_config(Config::DeleteServerAfter).await?,
993 Some("1".to_string())
994 );
995
996 assert_eq!(context1.get_config_delete_server_after().await?, Some(0));
997 imex(context1, ImexMode::ExportBackup, backup_dir.path(), None).await?;
998 let _event = context1
999 .evtracker
1000 .get_matching(|evt| matches!(evt, EventType::ImexProgress(1000)))
1001 .await;
1002
1003 let context2 = &TestContext::new().await;
1004 let backup = has_backup(context2, backup_dir.path()).await?;
1005 imex(context2, ImexMode::ImportBackup, backup.as_ref(), None).await?;
1006 let _event = context2
1007 .evtracker
1008 .get_matching(|evt| matches!(evt, EventType::ImexProgress(1000)))
1009 .await;
1010 assert!(context2.is_configured().await?);
1011 assert!(context2.is_chatmail().await?);
1012 for ctx in [context1, context2] {
1013 assert_eq!(
1014 ctx.get_config(Config::BccSelf).await?,
1015 Some("1".to_string())
1016 );
1017 assert_eq!(
1018 ctx.get_config(Config::DeleteServerAfter).await?,
1019 Some("0".to_string())
1020 );
1021 assert_eq!(ctx.get_config_delete_server_after().await?, None);
1022 }
1023 Ok(())
1024 }
1025
1026 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1028 async fn test_import_backup_fails_because_of_dcbackup_version() -> Result<()> {
1029 let mut tcm = TestContextManager::new();
1030 let context1 = tcm.alice().await;
1031 let context2 = tcm.unconfigured().await;
1032
1033 assert!(context1.is_configured().await?);
1034 assert!(!context2.is_configured().await?);
1035
1036 let backup_dir = tempfile::tempdir().unwrap();
1037
1038 tcm.section("export from context1");
1039 assert!(
1040 imex(&context1, ImexMode::ExportBackup, backup_dir.path(), None)
1041 .await
1042 .is_ok()
1043 );
1044 let _event = context1
1045 .evtracker
1046 .get_matching(|evt| matches!(evt, EventType::ImexProgress(1000)))
1047 .await;
1048 let backup = has_backup(&context2, backup_dir.path()).await?;
1049 let modified_backup = backup_dir.path().join("modified_backup.tar");
1050
1051 tcm.section("Change backup_version to be higher than DCBACKUP_VERSION");
1052 {
1053 let unpack_dir = tempfile::tempdir().unwrap();
1054 let mut ar = Archive::new(File::open(&backup).await?);
1055 ar.unpack(&unpack_dir).await?;
1056
1057 let sql = sql::Sql::new(unpack_dir.path().join(DBFILE_BACKUP_NAME));
1058 sql.open(&context2, "".to_string()).await?;
1059 assert_eq!(
1060 sql.get_raw_config_int("backup_version").await?.unwrap(),
1061 DCBACKUP_VERSION
1062 );
1063 sql.set_raw_config_int("backup_version", DCBACKUP_VERSION + 1)
1064 .await?;
1065 sql.close().await;
1066
1067 let modified_backup_file = File::create(&modified_backup).await?;
1068 let mut builder = tokio_tar::Builder::new(modified_backup_file);
1069 builder.append_dir_all("", unpack_dir.path()).await?;
1070 builder.finish().await?;
1071 }
1072
1073 tcm.section("import to context2");
1074 let err = imex(&context2, ImexMode::ImportBackup, &modified_backup, None)
1075 .await
1076 .unwrap_err();
1077 assert!(err.to_string().starts_with("This profile is from a newer version of Delta Chat. Please update Delta Chat and try again"));
1078
1079 let err_event = context2
1082 .evtracker
1083 .get_matching(|evt| matches!(evt, EventType::Error(_)))
1084 .await;
1085 let EventType::Error(err_msg) = err_event else {
1086 unreachable!()
1087 };
1088 assert!(err_msg.starts_with("This profile is from a newer version of Delta Chat. Please update Delta Chat and try again"));
1089
1090 context2
1091 .evtracker
1092 .get_matching(|evt| matches!(evt, EventType::ImexProgress(0)))
1093 .await;
1094
1095 assert!(!context2.is_configured().await?);
1096 assert_eq!(context2.get_config(Config::ConfiguredAddr).await?, None);
1097
1098 Ok(())
1099 }
1100
1101 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1105 async fn test_import_backup_reset_config_cache() -> Result<()> {
1106 let backup_dir = tempfile::tempdir()?;
1107 let context1 = TestContext::new_alice().await;
1108 let context2 = TestContext::new().await;
1109 assert!(!context2.is_configured().await?);
1110
1111 imex(&context1, ImexMode::ExportBackup, backup_dir.path(), None).await?;
1113
1114 let backup = has_backup(&context2, backup_dir.path()).await?;
1116 let context2_cloned = context2.clone();
1117 let handle = task::spawn(async move {
1118 imex(
1119 &context2_cloned,
1120 ImexMode::ImportBackup,
1121 backup.as_ref(),
1122 None,
1123 )
1124 .await
1125 .unwrap();
1126 });
1127
1128 while !handle.is_finished() {
1129 context2.is_configured().await.ok();
1132 tokio::time::sleep(Duration::from_micros(1)).await;
1133 }
1134
1135 assert!(context2.is_configured().await?);
1137
1138 Ok(())
1139 }
1140
1141 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1143 async fn test_import_ancient_backup() -> Result<()> {
1144 let mut tcm = TestContextManager::new();
1145 let context = &tcm.unconfigured().await;
1146
1147 let backup_path = Path::new("test-data/core-1.86.0-backup.tar");
1148 imex(context, ImexMode::ImportBackup, backup_path, None).await?;
1149
1150 Ok(())
1151 }
1152}