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