diff --git a/cli/bin/main.cpp b/cli/bin/main.cpp index 4bd6124..9f68e9f 100644 --- a/cli/bin/main.cpp +++ b/cli/bin/main.cpp @@ -519,6 +519,11 @@ int performBackup(etcpp::Session& session, cxxopts::ParseResult const& argParseR return EXIT_FAILURE; } + std::string addressFilter; + if (argParseResult.count("address")) { + addressFilter = argParseResult["address"].as(); + } + // Telemetry - we'd like to know whether the user overwrote the default export path session.setUsingDefaultExportPath(!pathCameFromArgs && usingDefaultBackupPath); @@ -533,7 +538,7 @@ int performBackup(etcpp::Session& session, cxxopts::ParseResult const& argParseR std::unique_ptr backupTask; try { - backupTask = std::make_unique(session, backupPath); + backupTask = std::make_unique(session, backupPath, addressFilter); } catch (const etcpp::SessionException& e) { etLogError("Failed to create export task: {}", e.what()); std::cerr << "Failed to create export task: " << e.what() << std::endl; @@ -662,6 +667,7 @@ int main(int argc, const char** argv) { cxxopts::value())("t,totp", "User's TOTP 2FA code (can also be set with env var ET_TOTP_CODE)", cxxopts::value())( "u,user", "User's account/email (can also be set with env var ET_USER_EMAIL", cxxopts::value())( + "a,address", "Email address to backup (defaults to all addresses)", cxxopts::value())( "k, telemetry", "Disable anonymous telemetry statistics (can also be set with env var ET_TELEMETRY_OFF)", cxxopts::value())( "h,help", "Show help"); @@ -745,4 +751,4 @@ int main(int argc, const char** argv) { return EXIT_FAILURE; } return EXIT_SUCCESS; -} \ No newline at end of file +} diff --git a/cli/bin/tasks/backup_task.cpp b/cli/bin/tasks/backup_task.cpp index 12e5dbd..63a3dd5 100644 --- a/cli/bin/tasks/backup_task.cpp +++ b/cli/bin/tasks/backup_task.cpp @@ -19,8 +19,9 @@ #include #include -BackupTask::BackupTask(etcpp::Session& session, const std::filesystem::path& backupPath) : - mBackup(session.newBackup(backupPath.u8string().c_str())) {} +BackupTask::BackupTask(etcpp::Session& session, const std::filesystem::path& backupPath, const std::string& addressFilter) : + mBackup(session.newBackup(backupPath.u8string().c_str(), + addressFilter.empty() ? nullptr : addressFilter.c_str())) {} void BackupTask::onProgress(float progress) { updateProgress(progress); diff --git a/cli/bin/tasks/backup_task.hpp b/cli/bin/tasks/backup_task.hpp index 59b22d8..fa6184a 100644 --- a/cli/bin/tasks/backup_task.hpp +++ b/cli/bin/tasks/backup_task.hpp @@ -29,7 +29,7 @@ class BackupTask final : public TaskWithProgress, etcpp::BackupCallback { CLIProgressBar mProgressBar; public: - BackupTask(etcpp::Session& session, const std::filesystem::path& backupPath); + BackupTask(etcpp::Session& session, const std::filesystem::path& backupPath, const std::string& addressFilter = ""); ~BackupTask() override = default; BackupTask(const BackupTask&) = delete; BackupTask(BackupTask&&) = delete; diff --git a/go-lib/cmd/lib/export_backup.go b/go-lib/cmd/lib/export_backup.go index caeedf2..76ffaa1 100644 --- a/go-lib/cmd/lib/export_backup.go +++ b/go-lib/cmd/lib/export_backup.go @@ -40,7 +40,7 @@ import ( ) //export etSessionNewBackup -func etSessionNewBackup(sessionPtr *C.etSession, cExportPath *C.cchar_t, outBackup **C.etBackup) C.etSessionStatus { +func etSessionNewBackup(sessionPtr *C.etSession, cExportPath *C.cchar_t, cAddressFilter *C.cchar_t, outBackup **C.etBackup) C.etSessionStatus { cSession, ok := resolveSession(sessionPtr) if !ok { return C.ET_SESSION_STATUS_INVALID @@ -56,7 +56,12 @@ func etSessionNewBackup(sessionPtr *C.etSession, cExportPath *C.cchar_t, outBack exportPath := C.GoString(cExportPath) exportPath = filepath.Join(exportPath, cSession.s.GetUser().Email) - mailExport := mail.NewExportTask(cSession.ctx, exportPath, cSession.s) + addressFilter := "" + if cAddressFilter != nil { + addressFilter = C.GoString(cAddressFilter) + } + + mailExport := mail.NewExportTask(cSession.ctx, exportPath, cSession.s, addressFilter) h := internal.NewHandle(&cBackup{ csession: cSession, diff --git a/go-lib/internal/mail/export.go b/go-lib/internal/mail/export.go index 4aef1c3..8aec514 100644 --- a/go-lib/internal/mail/export.go +++ b/go-lib/internal/mail/export.go @@ -63,12 +63,14 @@ type ExportTask struct { session *session.Session log *logrus.Entry cancelledByUser bool + addressFilter string } func NewExportTask( ctx context.Context, exportPath string, session *session.Session, + addressFilter string, ) *ExportTask { exportPath = filepath.Join(exportPath, generateUniqueExportDir()) @@ -78,13 +80,14 @@ func NewExportTask( ctx, cancel := context.WithCancel(ctx) return &ExportTask{ - ctx: ctx, - ctxCancel: cancel, - group: async.NewGroup(ctx, session.GetPanicHandler()), - tmpDir: tmpDir, - exportDir: exportPath, - session: session, - log: logrus.WithField("export", "mail").WithField("userID", session.GetUser().ID), + ctx: ctx, + ctxCancel: cancel, + group: async.NewGroup(ctx, session.GetPanicHandler()), + tmpDir: tmpDir, + exportDir: exportPath, + session: session, + log: logrus.WithField("export", "mail").WithField("userID", session.GetUser().ID), + addressFilter: addressFilter, } } @@ -209,8 +212,30 @@ func (e *ExportTask) Run(ctx context.Context, reporter Reporter) error { downloadMemMb = MinDownloadMemMB } + // Get the address ID if a filter is specified + var addressID string + if e.addressFilter != "" { + addresses, err := client.GetAddresses(ctx) + if err != nil { + return fmt.Errorf("failed to get addresses: %w", err) + } + + for _, addr := range addresses { + if addr.Email == e.addressFilter { + addressID = addr.ID + break + } + } + + if addressID == "" { + return fmt.Errorf("specified email address %s not found in account", e.addressFilter) + } + + e.log.WithField("address", e.addressFilter).Info("Filtering backup to single address") + } + // Build stages - metaStage := NewMetadataStage(client, e.log, MetadataPageSize, NumParallelDownloads) + metaStage := NewMetadataStage(client, e.log, MetadataPageSize, NumParallelDownloads, addressID) downloadStage := NewDownloadStage(client, NumParallelDownloads, e.log, downloadMemMb, e.session.GetPanicHandler()) buildStage := NewBuildStage(NumParallelBuilders, e.log, buildMemMB, e.session.GetPanicHandler(), e.session.GetReporter(), user.ID) writeStage := NewWriteStage(e.tmpDir, e.exportDir, NumParallelWriters, e.log, reporter, e.session.GetPanicHandler()) diff --git a/go-lib/internal/mail/export_stage_metadata.go b/go-lib/internal/mail/export_stage_metadata.go index 9aefff3..b666ecf 100644 --- a/go-lib/internal/mail/export_stage_metadata.go +++ b/go-lib/internal/mail/export_stage_metadata.go @@ -36,6 +36,7 @@ type MetadataStage struct { outputCh chan []proton.MessageMetadata pageSize int splitSize int + addressID string } func NewMetadataStage( @@ -43,6 +44,7 @@ func NewMetadataStage( entry *logrus.Entry, pageSize int, splitSize int, + addressID string, ) *MetadataStage { return &MetadataStage{ client: client, @@ -50,6 +52,7 @@ func NewMetadataStage( outputCh: make(chan []proton.MessageMetadata), pageSize: pageSize, splitSize: splitSize, + addressID: addressID, } } @@ -76,8 +79,9 @@ func (m *MetadataStage) Run( if lastMessageID != "" { meta, err := client.GetMessageMetadataPage(ctx, 0, m.pageSize, proton.MessageFilter{ - EndID: lastMessageID, - Desc: true, + EndID: lastMessageID, + AddressID: m.addressID, + Desc: true, }) if err != nil { @@ -93,7 +97,8 @@ func (m *MetadataStage) Run( metadata = meta } else { meta, err := client.GetMessageMetadataPage(ctx, 0, m.pageSize, proton.MessageFilter{ - Desc: true, + AddressID: m.addressID, + Desc: true, }) if err != nil { errReporter.ReportStageError(err) diff --git a/lib/include/etsession.hpp b/lib/include/etsession.hpp index e265826..9abda85 100644 --- a/lib/include/etsession.hpp +++ b/lib/include/etsession.hpp @@ -72,7 +72,7 @@ class Session final { [[nodiscard]] std::string getHVSolveURL() const; [[nodiscard]] LoginState markHVSolved(); - [[nodiscard]] Backup newBackup(const char* exportPath) const; + [[nodiscard]] Backup newBackup(const char* exportPath, const char* addressFilter = nullptr) const; [[nodiscard]] Restore newRestore(const char* backupPath) const; void setUsingDefaultExportPath(const bool usingDefaultExportPath); diff --git a/lib/lib/etsession.cpp b/lib/lib/etsession.cpp index 0bd2b55..f52973f 100644 --- a/lib/lib/etsession.cpp +++ b/lib/lib/etsession.cpp @@ -151,9 +151,9 @@ Session::LoginState Session::getLoginState() const { return ls; } -Backup Session::newBackup(const char* exportPath) const { +Backup Session::newBackup(const char* exportPath, const char* addressFilter) const { etBackup* exportPtr = nullptr; - wrapCCall([&](etSession* ptr) -> etSessionStatus { return etSessionNewBackup(ptr, exportPath, &exportPtr); }); + wrapCCall([&](etSession* ptr) -> etSessionStatus { return etSessionNewBackup(ptr, exportPath, addressFilter, &exportPtr); }); return Backup(*this, exportPtr); }