diff --git a/assets/images/ic_download_all.svg b/assets/images/ic_download_all.svg
new file mode 100644
index 000000000..c296423cc
--- /dev/null
+++ b/assets/images/ic_download_all.svg
@@ -0,0 +1,3 @@
+
diff --git a/core/lib/domain/preview/supported_preview_file_types.dart b/core/lib/domain/preview/supported_preview_file_types.dart
index 99df0bdbf..ede994866 100644
--- a/core/lib/domain/preview/supported_preview_file_types.dart
+++ b/core/lib/domain/preview/supported_preview_file_types.dart
@@ -31,6 +31,7 @@ class SupportedPreviewFileTypes {
'application/vnd.ms-powerpoint'];
static const zipMimeTypes = [
+ 'application/zip',
'application/x-tar',
'application/x-gtar',
'application/x-gzip',
diff --git a/core/lib/presentation/resources/image_paths.dart b/core/lib/presentation/resources/image_paths.dart
index 51206955e..4bf5cc9ac 100644
--- a/core/lib/presentation/resources/image_paths.dart
+++ b/core/lib/presentation/resources/image_paths.dart
@@ -222,6 +222,7 @@ class ImagePaths {
String get icDeleteSelection => _getImagePath('ic_delete_selection.svg');
String get icLogoTwakeWelcome => _getImagePath('ic_logo_twake_welcome.svg');
String get icHelp => _getImagePath('ic_help.svg');
+ String get icDownloadAll => _getImagePath('ic_download_all.svg');
String _getImagePath(String imageName) {
return AssetsPaths.images + imageName;
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 7e72429bd..8899824d3 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -99,6 +99,8 @@ PODS:
- UniversalDetector2 (= 2.0.1)
- flutter_downloader (0.0.1):
- Flutter
+ - flutter_file_dialog (0.0.1):
+ - Flutter
- flutter_image_compress_common (1.0.0):
- Flutter
- Mantle
@@ -221,6 +223,7 @@ DEPENDENCIES:
- flutter_appauth (from `.symlinks/plugins/flutter_appauth/ios`)
- flutter_charset_detector_darwin (from `.symlinks/plugins/flutter_charset_detector_darwin/darwin`)
- flutter_downloader (from `.symlinks/plugins/flutter_downloader/ios`)
+ - flutter_file_dialog (from `.symlinks/plugins/flutter_file_dialog/ios`)
- flutter_image_compress_common (from `.symlinks/plugins/flutter_image_compress_common/ios`)
- flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
@@ -297,6 +300,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_charset_detector_darwin/darwin"
flutter_downloader:
:path: ".symlinks/plugins/flutter_downloader/ios"
+ flutter_file_dialog:
+ :path: ".symlinks/plugins/flutter_file_dialog/ios"
flutter_image_compress_common:
:path: ".symlinks/plugins/flutter_image_compress_common/ios"
flutter_inappwebview_ios:
@@ -363,6 +368,7 @@ SPEC CHECKSUMS:
flutter_appauth: 1ce438877bc111c5d8f42da47729909290624886
flutter_charset_detector_darwin: fb3692d6d72cb6afcce7b0dd1a9516be6e78556e
flutter_downloader: b7301ae057deadd4b1650dc7c05375f10ff12c39
+ flutter_file_dialog: 4c014a45b105709a27391e266c277d7e588e9299
flutter_image_compress_common: ec1d45c362c9d30a3f6a0426c297f47c52007e3e
flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0
flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069
diff --git a/lib/features/email/data/datasource/email_datasource.dart b/lib/features/email/data/datasource/email_datasource.dart
index 65d099dbe..d2150a269 100644
--- a/lib/features/email/data/datasource/email_datasource.dart
+++ b/lib/features/email/data/datasource/email_datasource.dart
@@ -201,4 +201,24 @@ abstract class EmailDataSource {
Future storePreviewEMLContentToSessionStorage(EMLPreviewer emlPreviewer);
Future getPreviewEMLContentInMemory(String keyStored);
+
+ Future downloadAllAttachmentsForWeb(
+ AccountId accountId,
+ EmailId emailId,
+ String baseDownloadAllUrl,
+ String outputFileName,
+ AccountRequest accountRequest,
+ DownloadTaskId taskId,
+ StreamController> onReceiveController,
+ {CancelToken? cancelToken}
+ );
+
+ Future exportAllAttachments(
+ AccountId accountId,
+ EmailId emailId,
+ String baseDownloadAllUrl,
+ String outputFileName,
+ AccountRequest accountRequest,
+ {CancelToken? cancelToken}
+ );
}
\ No newline at end of file
diff --git a/lib/features/email/data/datasource_impl/email_datasource_impl.dart b/lib/features/email/data/datasource_impl/email_datasource_impl.dart
index 93b389ba5..804cd3d07 100644
--- a/lib/features/email/data/datasource_impl/email_datasource_impl.dart
+++ b/lib/features/email/data/datasource_impl/email_datasource_impl.dart
@@ -540,4 +540,46 @@ class EmailDataSourceImpl extends EmailDataSource {
Future getPreviewEMLContentInMemory(String keyStored) {
throw UnimplementedError();
}
+
+ @override
+ Future downloadAllAttachmentsForWeb(
+ AccountId accountId,
+ EmailId emailId,
+ String baseDownloadAllUrl,
+ String outputFileName,
+ AccountRequest accountRequest,
+ DownloadTaskId taskId,
+ StreamController> onReceiveController,
+ {CancelToken? cancelToken}
+ ) => Future.sync(() async {
+ return await emailAPI.downloadAllAttachmentsForWeb(
+ accountId,
+ emailId,
+ baseDownloadAllUrl,
+ outputFileName,
+ accountRequest,
+ taskId,
+ onReceiveController,
+ cancelToken: cancelToken,
+ );
+ }).catchError(_exceptionThrower.throwException);
+
+ @override
+ Future exportAllAttachments(
+ AccountId accountId,
+ EmailId emailId,
+ String baseDownloadAllUrl,
+ String outputFileName,
+ AccountRequest accountRequest, {
+ CancelToken? cancelToken,
+ }) => Future.sync(() async {
+ return await emailAPI.exportAllAttachments(
+ accountId,
+ emailId,
+ baseDownloadAllUrl,
+ outputFileName,
+ accountRequest,
+ cancelToken: cancelToken,
+ );
+ }).catchError(_exceptionThrower.throwException);
}
\ No newline at end of file
diff --git a/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart b/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart
index f7cb296ea..eff48b11a 100644
--- a/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart
+++ b/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart
@@ -563,4 +563,23 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource {
Future getPreviewEMLContentInMemory(String keyStored) {
throw UnimplementedError();
}
+
+ @override
+ Future downloadAllAttachmentsForWeb(
+ AccountId accountId,
+ EmailId emailId,
+ String baseDownloadAllUrl,
+ String outputFileName,
+ AccountRequest accountRequest,
+ DownloadTaskId taskId,
+ StreamController> onReceiveController,
+ {CancelToken? cancelToken}
+ ) {
+ throw UnimplementedError();
+ }
+
+ @override
+ Future exportAllAttachments(AccountId accountId, EmailId emailId, String baseDownloadAllUrl, String outputFileName, AccountRequest accountRequest, {CancelToken? cancelToken}) {
+ throw UnimplementedError();
+ }
}
\ No newline at end of file
diff --git a/lib/features/email/data/datasource_impl/email_local_storage_datasource_impl.dart b/lib/features/email/data/datasource_impl/email_local_storage_datasource_impl.dart
index 6703d114f..e8d9d8a34 100644
--- a/lib/features/email/data/datasource_impl/email_local_storage_datasource_impl.dart
+++ b/lib/features/email/data/datasource_impl/email_local_storage_datasource_impl.dart
@@ -254,4 +254,23 @@ class EmailLocalStorageDataSourceImpl extends EmailDataSource {
Future markAsForwarded(Session session, AccountId accountId, List emailIds) {
throw UnimplementedError();
}
+
+ @override
+ Future downloadAllAttachmentsForWeb(
+ AccountId accountId,
+ EmailId emailId,
+ String baseDownloadAllUrl,
+ String outputFileName,
+ AccountRequest accountRequest,
+ DownloadTaskId taskId,
+ StreamController> onReceiveController,
+ {CancelToken? cancelToken}
+ ) {
+ throw UnimplementedError();
+ }
+
+ @override
+ Future exportAllAttachments(AccountId accountId, EmailId emailId, String baseDownloadAllUrl, String outputFileName, AccountRequest accountRequest, {CancelToken? cancelToken}) {
+ throw UnimplementedError();
+ }
}
\ No newline at end of file
diff --git a/lib/features/email/data/datasource_impl/email_session_storage_datasource_impl.dart b/lib/features/email/data/datasource_impl/email_session_storage_datasource_impl.dart
index 426968d11..2aebbc7ca 100644
--- a/lib/features/email/data/datasource_impl/email_session_storage_datasource_impl.dart
+++ b/lib/features/email/data/datasource_impl/email_session_storage_datasource_impl.dart
@@ -252,4 +252,23 @@ class EmailSessionStorageDatasourceImpl extends EmailDataSource {
Future markAsForwarded(Session session, AccountId accountId, List emailIds) {
throw UnimplementedError();
}
+
+ @override
+ Future downloadAllAttachmentsForWeb(
+ AccountId accountId,
+ EmailId emailId,
+ String baseDownloadAllUrl,
+ String outputFileName,
+ AccountRequest accountRequest,
+ DownloadTaskId taskId,
+ StreamController> onReceiveController,
+ {CancelToken? cancelToken}
+ ) {
+ throw UnimplementedError();
+ }
+
+ @override
+ Future exportAllAttachments(AccountId accountId, EmailId emailId, String baseDownloadAllUrl, String outputFileName, AccountRequest accountRequest, {CancelToken? cancelToken}) {
+ throw UnimplementedError();
+ }
}
\ No newline at end of file
diff --git a/lib/features/email/data/network/email_api.dart b/lib/features/email/data/network/email_api.dart
index d851fd5b2..ae2c40567 100644
--- a/lib/features/email/data/network/email_api.dart
+++ b/lib/features/email/data/network/email_api.dart
@@ -49,6 +49,7 @@ import 'package:model/download/download_task_id.dart';
import 'package:model/email/attachment.dart';
import 'package:model/email/mark_star_action.dart';
import 'package:model/email/read_actions.dart';
+import 'package:model/extensions/account_id_extensions.dart';
import 'package:model/extensions/email_extension.dart';
import 'package:model/extensions/email_id_extensions.dart';
import 'package:model/extensions/keyword_identifier_extension.dart';
@@ -66,11 +67,13 @@ import 'package:tmail_ui_user/features/email/domain/extensions/email_id_extensio
import 'package:tmail_ui_user/features/email/domain/model/event_action.dart';
import 'package:tmail_ui_user/features/email/domain/model/move_to_mailbox_request.dart';
import 'package:tmail_ui_user/features/email/domain/model/restore_deleted_message_request.dart';
+import 'package:tmail_ui_user/features/email/domain/state/download_all_attachments_for_web_state.dart';
import 'package:tmail_ui_user/features/email/domain/state/download_attachment_for_web_state.dart';
import 'package:tmail_ui_user/features/login/domain/exceptions/authentication_exception.dart';
import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart';
import 'package:tmail_ui_user/features/thread/domain/constants/thread_constants.dart';
import 'package:tmail_ui_user/main/error/capability_validator.dart';
+import 'package:uri/uri.dart';
import 'package:uuid/uuid.dart';
class EmailAPI with HandleSetErrorMixin {
@@ -402,6 +405,88 @@ class EmailAPI with HandleSetErrorMixin {
return bytesDownloaded;
}
+ Future downloadAllAttachmentsForWeb(
+ AccountId accountId,
+ EmailId emailId,
+ String baseDownloadAllUrl,
+ String outputFileName,
+ AccountRequest accountRequest,
+ DownloadTaskId taskId,
+ StreamController> onReceiveController,
+ {CancelToken? cancelToken}
+ ) async {
+ final authentication = accountRequest.authenticationType == AuthenticationType.oidc
+ ? accountRequest.bearerToken
+ : accountRequest.basicAuth;
+ final headerParam = _dioClient.getHeaders();
+ headerParam[HttpHeaders.authorizationHeader] = authentication;
+ headerParam[HttpHeaders.acceptHeader] = DioClient.jmapHeader;
+
+ final downloadAllUriTemplate = UriTemplate(Uri.decodeFull(baseDownloadAllUrl));
+ final downloadAllUrl = downloadAllUriTemplate.expand({
+ 'accountId': accountId.asString,
+ 'emailId': emailId.asString,
+ 'name': outputFileName,
+ });
+ final downloadFileName = '$outputFileName.zip';
+
+ final bytesDownloaded = await _dioClient.get(
+ downloadAllUrl,
+ options: Options(
+ headers: headerParam,
+ responseType: ResponseType.bytes),
+ onReceiveProgress: (downloaded, total) {
+ log('EmailAPI::downloadFileForWeb(): downloaded = $downloaded | total: $total');
+ double progress = 0;
+ if (downloaded > 0 && total >= downloaded) {
+ progress = (downloaded / total) * 100;
+ }
+ log('EmailAPI::downloadFileForWeb(): progress = ${progress.round()}%');
+ onReceiveController.add(Right(DownloadingAllAttachmentsForWeb(
+ taskId,
+ downloadFileName,
+ progress,
+ downloaded,
+ total > 0 ? total : 0,
+ )));
+ },
+ cancelToken: cancelToken,
+ );
+
+ _downloadManager.createAnchorElementDownloadFileWeb(
+ bytesDownloaded,
+ downloadFileName,
+ );
+ }
+
+ Future exportAllAttachments(
+ AccountId accountId,
+ EmailId emailId,
+ String baseDownloadAllUrl,
+ String outputFileName,
+ AccountRequest accountRequest,
+ {CancelToken? cancelToken}
+ ) async {
+ final authentication = accountRequest.authenticationType == AuthenticationType.oidc
+ ? accountRequest.bearerToken
+ : accountRequest.basicAuth;
+
+ final downloadAllUriTemplate = UriTemplate(Uri.decodeFull(baseDownloadAllUrl));
+ final downloadAllUrl = downloadAllUriTemplate.expand({
+ 'accountId': accountId.asString,
+ 'emailId': emailId.asString,
+ 'name': outputFileName,
+ });
+ final downloadFileName = '$outputFileName.zip';
+
+ return _downloadManager.downloadFile(
+ downloadAllUrl,
+ getTemporaryDirectory(),
+ downloadFileName,
+ authentication,
+ cancelToken: cancelToken);
+ }
+
Future<({
List emailIdsSuccess,
Map mapErrors,
diff --git a/lib/features/email/data/repository/email_repository_impl.dart b/lib/features/email/data/repository/email_repository_impl.dart
index 5cf29d8d7..b00fdaea5 100644
--- a/lib/features/email/data/repository/email_repository_impl.dart
+++ b/lib/features/email/data/repository/email_repository_impl.dart
@@ -530,4 +530,41 @@ class EmailRepositoryImpl extends EmailRepository {
htmlContent,
configuration);
}
+
+ @override
+ Future downloadAllAttachmentsForWeb(
+ AccountId accountId,
+ EmailId emailId,
+ String baseDownloadAllUrl,
+ String outputFileName,
+ AccountRequest accountRequest,
+ DownloadTaskId taskId,
+ StreamController> onReceiveController,
+ {CancelToken? cancelToken}
+ ) => emailDataSource[DataSourceType.network]!.downloadAllAttachmentsForWeb(
+ accountId,
+ emailId,
+ baseDownloadAllUrl,
+ outputFileName,
+ accountRequest,
+ taskId,
+ onReceiveController,
+ cancelToken: cancelToken,
+ );
+
+ @override
+ Future exportAllAttachments(
+ AccountId accountId,
+ EmailId emailId,
+ String baseDownloadAllUrl,
+ String outputFileName,
+ AccountRequest accountRequest, {
+ CancelToken? cancelToken,
+ }) => emailDataSource[DataSourceType.network]!.exportAllAttachments(
+ accountId,
+ emailId,
+ baseDownloadAllUrl,
+ outputFileName,
+ accountRequest,
+ );
}
\ No newline at end of file
diff --git a/lib/features/email/domain/repository/email_repository.dart b/lib/features/email/domain/repository/email_repository.dart
index 64824ffc8..2496a5ac6 100644
--- a/lib/features/email/domain/repository/email_repository.dart
+++ b/lib/features/email/domain/repository/email_repository.dart
@@ -199,4 +199,24 @@ abstract class EmailRepository {
Future sanitizeHtmlContent(
String htmlContent,
TransformConfiguration configuration);
+
+ Future downloadAllAttachmentsForWeb(
+ AccountId accountId,
+ EmailId emailId,
+ String baseDownloadAllUrl,
+ String outputFileName,
+ AccountRequest accountRequest,
+ DownloadTaskId taskId,
+ StreamController> onReceiveController,
+ {CancelToken? cancelToken}
+ );
+
+ Future exportAllAttachments(
+ AccountId accountId,
+ EmailId emailId,
+ String baseDownloadAllUrl,
+ String outputFileName,
+ AccountRequest accountRequest,
+ {CancelToken? cancelToken}
+ );
}
\ No newline at end of file
diff --git a/lib/features/email/domain/state/download_all_attachments_for_web_state.dart b/lib/features/email/domain/state/download_all_attachments_for_web_state.dart
new file mode 100644
index 000000000..7f63a1f01
--- /dev/null
+++ b/lib/features/email/domain/state/download_all_attachments_for_web_state.dart
@@ -0,0 +1,51 @@
+import 'package:core/presentation/state/failure.dart';
+import 'package:core/presentation/state/success.dart';
+import 'package:model/download/download_task_id.dart';
+import 'package:model/email/attachment.dart';
+
+class StartDownloadAllAttachmentsForWeb extends UIState {
+ StartDownloadAllAttachmentsForWeb(this.taskId, this.attachment);
+
+ final DownloadTaskId taskId;
+ final Attachment attachment;
+
+ @override
+ List