From 4a8c6befc09fc8ab19f998ebb123c035b4dd7338 Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 7 May 2024 16:38:15 +0700 Subject: [PATCH 1/8] TF-825 Create download message as EML from jmap api --- core/lib/data/constants/constant.dart | 1 + .../email/data/network/email_api.dart | 74 ++++++++++++++++++- .../domain/exceptions/email_exceptions.dart | 6 +- pubspec.lock | 2 +- pubspec.yaml | 2 + 5 files changed, 80 insertions(+), 5 deletions(-) diff --git a/core/lib/data/constants/constant.dart b/core/lib/data/constants/constant.dart index 8ec6a6caa4..e6a50f300a 100644 --- a/core/lib/data/constants/constant.dart +++ b/core/lib/data/constants/constant.dart @@ -3,4 +3,5 @@ class Constant { static const contentTypeHeaderDefault = 'application/json'; static const pdfMimeType = 'application/pdf'; static const textHtmlMimeType = 'text/html'; + static const octetStreamMimeType = 'application/octet-stream'; } \ 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 1612575dd5..68953b94ce 100644 --- a/lib/features/email/data/network/email_api.dart +++ b/lib/features/email/data/network/email_api.dart @@ -47,6 +47,7 @@ import 'package:model/email/email_action_type.dart'; import 'package:model/email/email_property.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'; @@ -68,10 +69,16 @@ import 'package:tmail_ui_user/features/login/domain/exceptions/authentication_ex 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 { + static const String accountIdProperty = 'accountId'; + static const String blobIdProperty = 'blobId'; + static const String nameProperty = 'name'; + static const String typeProperty = 'type'; + final HttpClient _httpClient; final DownloadManager _downloadManager; final DioClient _dioClient; @@ -351,12 +358,12 @@ class EmailAPI with HandleSetErrorMixin { headers: headerParam, responseType: ResponseType.bytes), onReceiveProgress: (downloaded, total) { - log('DownloadClient::downloadFileForWeb(): downloaded = $downloaded | total: $total'); + log('EmailAPI::downloadFileForWeb(): downloaded = $downloaded | total: $total'); double progress = 0; - if (downloaded > 0 && total > downloaded) { + if (downloaded > 0 && total >= downloaded) { progress = (downloaded / total) * 100; } - log('DownloadClient::downloadFileForWeb(): progress = ${progress.round()}%'); + log('EmailAPI::downloadFileForWeb(): progress = ${progress.round()}%'); onReceiveController.add(Right(DownloadingAttachmentForWeb( taskId, attachment, @@ -734,4 +741,65 @@ class EmailAPI with HandleSetErrorMixin { throw NotFoundEmailRecoveryActionException(); } } + + Future downloadMessageAsEML( + AccountId accountId, + String baseDownloadUrl, + AccountRequest accountRequest, + Id blobId, + String subjectEmail, + ) async { + final authentication = accountRequest.authenticationType == AuthenticationType.oidc + ? accountRequest.bearerToken + : accountRequest.basicAuth; + + final fileName = subjectEmail.isEmpty + ? '${_uuid.v1()}.eml' + : '$subjectEmail.eml'; + + final downloadUrl = _getDownloadUrl( + baseDownloadUrl: baseDownloadUrl, + accountId: accountId, + blobId: blobId, + fileName: fileName, + mimeType: Constant.octetStreamMimeType + ); + + final headerParam = _dioClient.getHeaders(); + headerParam[HttpHeaders.authorizationHeader] = authentication; + headerParam[HttpHeaders.acceptHeader] = DioClient.jmapHeader; + + final result = await _dioClient.get( + downloadUrl, + options: Options( + headers: headerParam, + responseType: ResponseType.bytes + ) + ); + + if (result is Uint8List) { + _downloadManager.createAnchorElementDownloadFileWeb(result, fileName); + } else { + throw NotFoundByteFileDownloadedException(); + } + } + + String _getDownloadUrl({ + required String baseDownloadUrl, + required AccountId accountId, + required Id blobId, + String? fileName, + String? mimeType, + }) { + final downloadUriTemplate = UriTemplate(baseDownloadUrl); + final downloadUri = downloadUriTemplate.expand({ + accountIdProperty : accountId.asString, + blobIdProperty : blobId.value, + nameProperty : '$fileName', + typeProperty : '$mimeType', + }); + final downloadUriDecoded = Uri.decodeFull(downloadUri); + log('EmailAPI::getDownloadUrl:downloadUriDecoded = $downloadUriDecoded'); + return downloadUriDecoded; + } } \ No newline at end of file diff --git a/lib/features/email/domain/exceptions/email_exceptions.dart b/lib/features/email/domain/exceptions/email_exceptions.dart index a5aa2441ec..37ab4d3f89 100644 --- a/lib/features/email/domain/exceptions/email_exceptions.dart +++ b/lib/features/email/domain/exceptions/email_exceptions.dart @@ -4,4 +4,8 @@ class NotFoundEmailContentException implements Exception {} class EmptyEmailContentException implements Exception {} -class NotFoundEmailRecoveryActionException implements Exception {} \ No newline at end of file +class NotFoundEmailRecoveryActionException implements Exception {} + +class NotFoundByteFileDownloadedException implements Exception {} + +class NotFoundEmailBlobIdException implements Exception {} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 95382eb089..c9a9af4f9c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1758,7 +1758,7 @@ packages: source: hosted version: "2.2.2" uri: - dependency: transitive + dependency: "direct main" description: name: uri sha256: "889eea21e953187c6099802b7b4cf5219ba8f3518f604a1033064d45b1b8268a" diff --git a/pubspec.yaml b/pubspec.yaml index 713b1693ab..41ede2c62a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -232,6 +232,8 @@ dependencies: mime: 1.0.4 + uri: 1.0.0 + dev_dependencies: flutter_test: sdk: flutter From bd0ac87eecd4d2e334c91533c9088050e02ff89d Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 7 May 2024 16:39:15 +0700 Subject: [PATCH 2/8] TF-825 Add download message as EML method in datasource --- .../data/datasource/email_datasource.dart | 9 +++++++++ .../email_datasource_impl.dart | 19 +++++++++++++++++++ .../email_hive_cache_datasource_impl.dart | 12 ++++++++++++ 3 files changed, 40 insertions(+) diff --git a/lib/features/email/data/datasource/email_datasource.dart b/lib/features/email/data/datasource/email_datasource.dart index db3d4c2832..28597f22f9 100644 --- a/lib/features/email/data/datasource/email_datasource.dart +++ b/lib/features/email/data/datasource/email_datasource.dart @@ -9,6 +9,7 @@ import 'package:dio/dio.dart'; import 'package:email_recovery/email_recovery/email_recovery_action.dart'; import 'package:email_recovery/email_recovery/email_recovery_action_id.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; @@ -112,4 +113,12 @@ abstract class EmailDataSource { Future restoreDeletedMessage(RestoredDeletedMessageRequest restoredDeletedMessageRequest); Future getRestoredDeletedMessage(EmailRecoveryActionId emailRecoveryActionId); + + Future downloadMessageAsEML( + AccountId accountId, + String baseDownloadUrl, + AccountRequest accountRequest, + Id blobId, + String subjectEmail + ); } \ 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 76a5e7ee83..f93817c1e0 100644 --- a/lib/features/email/data/datasource_impl/email_datasource_impl.dart +++ b/lib/features/email/data/datasource_impl/email_datasource_impl.dart @@ -10,6 +10,7 @@ import 'package:email_recovery/email_recovery/email_recovery_action.dart'; import 'package:email_recovery/email_recovery/email_recovery_action_id.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; @@ -252,4 +253,22 @@ class EmailDataSourceImpl extends EmailDataSource { return await emailAPI.getRestoredDeletedMessage(emailRecoveryActionId); }).catchError(_exceptionThrower.throwException); } + + @override + Future downloadMessageAsEML( + AccountId accountId, + String baseDownloadUrl, + AccountRequest accountRequest, + Id blobId, + String subjectEmail + ) { + return Future.sync(() async { + return await emailAPI.downloadMessageAsEML( + accountId, + baseDownloadUrl, + accountRequest, + blobId, + subjectEmail); + }).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 c188147467..b5c03f6550 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 @@ -12,6 +12,7 @@ import 'package:dio/dio.dart'; import 'package:email_recovery/email_recovery/email_recovery_action.dart'; import 'package:email_recovery/email_recovery/email_recovery_action_id.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; @@ -303,4 +304,15 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource { Future getRestoredDeletedMessage(EmailRecoveryActionId emailRecoveryActionId) { throw UnimplementedError(); } + + @override + Future downloadMessageAsEML( + AccountId accountId, + String baseDownloadUrl, + AccountRequest accountRequest, + Id blobId, + String subjectEmail + ) { + throw UnimplementedError(); + } } \ No newline at end of file From 50f2c2f93d14a27fe2b63ca033e5e0986ee747f2 Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 7 May 2024 16:39:39 +0700 Subject: [PATCH 3/8] TF-825 Add download message as EML method in repository --- .../data/repository/email_repository_impl.dart | 17 +++++++++++++++++ .../domain/repository/email_repository.dart | 9 +++++++++ 2 files changed, 26 insertions(+) diff --git a/lib/features/email/data/repository/email_repository_impl.dart b/lib/features/email/data/repository/email_repository_impl.dart index 014bca06ba..276752e73b 100644 --- a/lib/features/email/data/repository/email_repository_impl.dart +++ b/lib/features/email/data/repository/email_repository_impl.dart @@ -11,6 +11,7 @@ import 'package:dio/dio.dart'; import 'package:email_recovery/email_recovery/email_recovery_action.dart'; import 'package:email_recovery/email_recovery/email_recovery_action_id.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; @@ -237,4 +238,20 @@ class EmailRepositoryImpl extends EmailRepository { Future printEmail(EmailPrint emailPrint) { return _printFileDataSource.printEmail(emailPrint); } + + @override + Future downloadMessageAsEML( + AccountId accountId, + String baseDownloadUrl, + AccountRequest accountRequest, + Id blobId, + String subjectEmail + ) { + return emailDataSource[DataSourceType.network]!.downloadMessageAsEML( + accountId, + baseDownloadUrl, + accountRequest, + blobId, + subjectEmail); + } } \ 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 53bb1daf5f..298ebc5bc6 100644 --- a/lib/features/email/domain/repository/email_repository.dart +++ b/lib/features/email/domain/repository/email_repository.dart @@ -10,6 +10,7 @@ import 'package:dio/dio.dart'; import 'package:email_recovery/email_recovery/email_recovery_action.dart'; import 'package:email_recovery/email_recovery/email_recovery_action_id.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; @@ -115,4 +116,12 @@ abstract class EmailRepository { Future getRestoredDeletedMessage(EmailRecoveryActionId emailRecoveryActionId); Future printEmail(EmailPrint emailPrint); + + Future downloadMessageAsEML( + AccountId accountId, + String baseDownloadUrl, + AccountRequest accountRequest, + Id blobId, + String subjectEmail + ); } \ No newline at end of file From e5624f947ed93ccf9b6d8910e3081f885e4310df Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 7 May 2024 16:40:35 +0700 Subject: [PATCH 4/8] TF-825 Add download message as EML method in interactor --- .../state/download_message_as_eml_state.dart | 11 ++++ .../download_message_as_eml_interactor.dart | 66 +++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 lib/features/email/domain/state/download_message_as_eml_state.dart create mode 100644 lib/features/email/domain/usecases/download_message_as_eml_interactor.dart diff --git a/lib/features/email/domain/state/download_message_as_eml_state.dart b/lib/features/email/domain/state/download_message_as_eml_state.dart new file mode 100644 index 0000000000..a6dcf74306 --- /dev/null +++ b/lib/features/email/domain/state/download_message_as_eml_state.dart @@ -0,0 +1,11 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; + +class StartDownloadMessageAsEML extends LoadingState {} + +class DownloadMessageAsEMLSuccess extends UIState {} + +class DownloadMessageAsEMLFailure extends FeatureFailure { + + DownloadMessageAsEMLFailure(dynamic exception) : super(exception: exception); +} \ No newline at end of file diff --git a/lib/features/email/domain/usecases/download_message_as_eml_interactor.dart b/lib/features/email/domain/usecases/download_message_as_eml_interactor.dart new file mode 100644 index 0000000000..d22cf7b76d --- /dev/null +++ b/lib/features/email/domain/usecases/download_message_as_eml_interactor.dart @@ -0,0 +1,66 @@ +import 'dart:async'; + +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:model/account/account_request.dart'; +import 'package:model/account/authentication_type.dart'; +import 'package:model/account/password.dart'; +import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; +import 'package:tmail_ui_user/features/email/domain/state/download_message_as_eml_state.dart'; +import 'package:tmail_ui_user/features/login/domain/repository/account_repository.dart'; +import 'package:tmail_ui_user/features/login/domain/repository/authentication_oidc_repository.dart'; +import 'package:tmail_ui_user/features/login/domain/repository/credential_repository.dart'; + +class DownloadMessageAsEMLInteractor { + final EmailRepository _emailRepository; + final CredentialRepository _credentialRepository; + final AccountRepository _accountRepository; + final AuthenticationOIDCRepository _authenticationOIDCRepository; + + DownloadMessageAsEMLInteractor( + this._emailRepository, + this._credentialRepository, + this._accountRepository, + this._authenticationOIDCRepository + ); + + Stream> execute( + AccountId accountId, + String baseDownloadUrl, + Id blobId, + String subjectEmail + ) async* { + try { + yield Right(StartDownloadMessageAsEML()); + + final currentAccount = await _accountRepository.getCurrentAccount(); + AccountRequest? accountRequest; + + if (currentAccount.authenticationType == AuthenticationType.oidc) { + final tokenOidc = await _authenticationOIDCRepository.getStoredTokenOIDC(currentAccount.id); + accountRequest = AccountRequest.withOidc(token: tokenOidc); + } else { + final authenticationInfoCache = await _credentialRepository.getAuthenticationInfoStored(); + accountRequest = AccountRequest.withBasic( + userName: UserName(authenticationInfoCache.username), + password: Password(authenticationInfoCache.password), + ); + } + + await _emailRepository.downloadMessageAsEML( + accountId, + baseDownloadUrl, + accountRequest, + blobId, + subjectEmail); + + yield Right(DownloadMessageAsEMLSuccess()); + } catch (exception) { + yield Left(DownloadMessageAsEMLFailure(exception)); + } + } +} \ No newline at end of file From 6b3777f14e91d26f7ee68a35b6d46fafe3935a4a Mon Sep 17 00:00:00 2001 From: dab246 Date: Wed, 15 May 2024 11:29:48 +0700 Subject: [PATCH 5/8] TF-825 Implement download message as eml in presentation layer --- .../caching/config/hive_cache_config.dart | 4 +- .../email_action_type_extension.dart | 4 ++ .../presentation/bindings/email_bindings.dart | 9 +++- .../controller/single_email_controller.dart | 45 +++++++++++++++++++ .../email/presentation/email_view.dart | 2 + .../extensions/email_cache_extension.dart | 3 +- .../data/extensions/email_extension.dart | 1 + .../thread/data/model/email_cache.dart | 5 +++ .../domain/constants/thread_constants.dart | 4 ++ lib/l10n/intl_messages.arb | 14 +++++- lib/main/localizations/app_localizations.dart | 14 ++++++ model/lib/email/email_action_type.dart | 3 +- model/lib/email/email_property.dart | 1 + model/lib/email/presentation_email.dart | 4 ++ model/lib/extensions/email_extension.dart | 4 ++ .../presentation_email_extension.dart | 7 +++ .../single_email_controller_test.dart | 4 ++ 17 files changed, 123 insertions(+), 5 deletions(-) diff --git a/lib/features/caching/config/hive_cache_config.dart b/lib/features/caching/config/hive_cache_config.dart index c2ad73acb3..2cf1ce1f42 100644 --- a/lib/features/caching/config/hive_cache_config.dart +++ b/lib/features/caching/config/hive_cache_config.dart @@ -66,7 +66,9 @@ class HiveCacheConfig { await UpgradeHiveDatabaseStepsV7(cachingManager).onUpgrade(oldVersion, newVersion); await UpgradeHiveDatabaseStepsV10(cachingManager).onUpgrade(oldVersion, newVersion); - await cachingManager.storeCacheVersion(newVersion); + if (oldVersion != newVersion) { + await cachingManager.storeCacheVersion(newVersion); + } } Future initializeEncryptionKey() async { diff --git a/lib/features/composer/presentation/extensions/email_action_type_extension.dart b/lib/features/composer/presentation/extensions/email_action_type_extension.dart index 17a0e36752..5c0a523d15 100644 --- a/lib/features/composer/presentation/extensions/email_action_type_extension.dart +++ b/lib/features/composer/presentation/extensions/email_action_type_extension.dart @@ -133,6 +133,8 @@ extension EmailActionTypeExtension on EmailActionType { return imagePaths.icUnsubscribe; case EmailActionType.archiveMessage: return imagePaths.icMailboxArchived; + case EmailActionType.downloadMessageAsEML: + return imagePaths.icDownloadAttachment; default: return ''; } @@ -152,6 +154,8 @@ extension EmailActionTypeExtension on EmailActionType { return AppLocalizations.of(context).unsubscribe; case EmailActionType.archiveMessage: return AppLocalizations.of(context).archiveMessage; + case EmailActionType.downloadMessageAsEML: + return AppLocalizations.of(context).downloadMessageAsEML; default: return ''; } diff --git a/lib/features/email/presentation/bindings/email_bindings.dart b/lib/features/email/presentation/bindings/email_bindings.dart index 19f0b8aeba..dcdcabb6df 100644 --- a/lib/features/email/presentation/bindings/email_bindings.dart +++ b/lib/features/email/presentation/bindings/email_bindings.dart @@ -14,6 +14,7 @@ import 'package:tmail_ui_user/features/email/data/repository/email_repository_im import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/download_attachment_for_web_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/download_attachments_interactor.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/download_message_as_eml_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/export_attachment_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/get_email_content_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/get_stored_email_state_interactor.dart'; @@ -69,7 +70,8 @@ class EmailBindings extends BaseBindings { Get.find(), Get.find(), Get.find(), - Get.find() + Get.find(), + Get.find(), )); } @@ -152,6 +154,11 @@ class EmailBindings extends BaseBindings { Get.find())); Get.lazyPut(() => StoreOpenedEmailInteractor(Get.find())); Get.lazyPut(() => PrintEmailInteractor(Get.find())); + Get.lazyPut(() => DownloadMessageAsEMLInteractor( + Get.find(), + Get.find(), + Get.find(), + Get.find())); IdentityInteractorsBindings().dependencies(); } diff --git a/lib/features/email/presentation/controller/single_email_controller.dart b/lib/features/email/presentation/controller/single_email_controller.dart index 3dea9c1c8b..1d97a9afb2 100644 --- a/lib/features/email/presentation/controller/single_email_controller.dart +++ b/lib/features/email/presentation/controller/single_email_controller.dart @@ -28,6 +28,7 @@ import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/email_action_type_extension.dart'; import 'package:tmail_ui_user/features/destination_picker/presentation/model/destination_picker_arguments.dart'; +import 'package:tmail_ui_user/features/email/domain/exceptions/email_exceptions.dart'; import 'package:tmail_ui_user/features/email/domain/extensions/list_attachments_extension.dart'; import 'package:tmail_ui_user/features/email/domain/model/detailed_email.dart'; import 'package:tmail_ui_user/features/email/domain/model/email_print.dart'; @@ -42,6 +43,7 @@ import 'package:tmail_ui_user/features/email/domain/state/calendar_event_reject_ import 'package:tmail_ui_user/features/email/domain/state/calendar_event_reply_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/download_attachment_for_web_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/download_attachments_state.dart'; +import 'package:tmail_ui_user/features/email/domain/state/download_message_as_eml_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/export_attachment_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/get_email_content_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_read_state.dart'; @@ -57,6 +59,7 @@ import 'package:tmail_ui_user/features/email/domain/usecases/maybe_calendar_even import 'package:tmail_ui_user/features/email/domain/usecases/calendar_event_reject_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/download_attachment_for_web_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/download_attachments_interactor.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/download_message_as_eml_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/export_attachment_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/get_email_content_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/mark_as_email_read_interactor.dart'; @@ -123,6 +126,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { final StoreOpenedEmailInteractor _storeOpenedEmailInteractor; final ViewAttachmentForWebInteractor _viewAttachmentForWebInteractor; final PrintEmailInteractor _printEmailInteractor; + final DownloadMessageAsEMLInteractor _downloadMessageAsEMLInteractor; CreateNewEmailRuleFilterInteractor? _createNewEmailRuleFilterInteractor; SendReceiptToSenderInteractor? _sendReceiptToSenderInteractor; @@ -171,6 +175,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { this._storeOpenedEmailInteractor, this._viewAttachmentForWebInteractor, this._printEmailInteractor, + this._downloadMessageAsEMLInteractor, ); @override @@ -231,6 +236,8 @@ class SingleEmailController extends BaseController with AppLoaderMixin { _handlePrintEmailSuccess(success); } else if (success is CalendarEventReplySuccess) { _calendarEventSuccess(success); + } else if (success is StartDownloadMessageAsEML) { + _showMessageWhenStartingDownloadMessageAsEML(); } } @@ -1141,6 +1148,9 @@ class SingleEmailController extends BaseController with AppLoaderMixin { case EmailActionType.printAll: _printEmail(context, presentationEmail); break; + case EmailActionType.downloadMessageAsEML: + _downloadMessageAsEML(presentationEmail); + break; default: break; } @@ -1772,4 +1782,39 @@ class SingleEmailController extends BaseController with AppLoaderMixin { AppLocalizations.of(currentContext!).eventReplyWasSentUnsuccessfully); } } + + void _downloadMessageAsEML(PresentationEmail presentationEmail) { + final accountId = mailboxDashBoardController.accountId.value; + final session = mailboxDashBoardController.sessionCurrent; + + if (accountId == null || session == null) { + consumeState(Stream.value(Left(DownloadMessageAsEMLFailure(NotFoundSessionException())))); + return; + } + + final blobId = presentationEmail.blobId; + if (blobId == null) { + consumeState(Stream.value(Left(DownloadMessageAsEMLFailure(NotFoundEmailBlobIdException())))); + return; + } + + final baseDownloadUrl = session.getDownloadUrl(jmapUrl: dynamicUrlInterceptors.jmapUrl); + + consumeState(_downloadMessageAsEMLInteractor.execute( + accountId, + baseDownloadUrl, + blobId, + presentationEmail.getEmailTitle() + )); + } + + void _showMessageWhenStartingDownloadMessageAsEML() { + if (currentOverlayContext != null && currentContext != null) { + appToast.showToastMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).downloadMessageAsEMLInProgress, + leadingSVGIconColor: AppColor.primaryColor, + leadingSVGIcon: imagePaths.icDownloadAttachment); + } + } } \ No newline at end of file diff --git a/lib/features/email/presentation/email_view.dart b/lib/features/email/presentation/email_view.dart index 990fd895a9..9555e6f489 100644 --- a/lib/features/email/presentation/email_view.dart +++ b/lib/features/email/presentation/email_view.dart @@ -473,6 +473,8 @@ class EmailView extends GetWidget { EmailActionType.unsubscribe, if (mailboxContain?.isArchive == false) EmailActionType.archiveMessage, + if (PlatformInfo.isWeb && PlatformInfo.isCanvasKit) + EmailActionType.downloadMessageAsEML ]; if (position == null) { diff --git a/lib/features/thread/data/extensions/email_cache_extension.dart b/lib/features/thread/data/extensions/email_cache_extension.dart index 5d101f04ab..4edb88c50d 100644 --- a/lib/features/thread/data/extensions/email_cache_extension.dart +++ b/lib/features/thread/data/extensions/email_cache_extension.dart @@ -34,7 +34,8 @@ extension EmailCacheExtension on EmailCache { : null, headerCalendarEvent: headerCalendarEvent != null ? Map.fromIterables(headerCalendarEvent!.keys.map((value) => IndividualHeaderIdentifier(value)), headerCalendarEvent!.values) - : null + : null, + blobId: blobId != null ? Id(blobId!) : null, ); } diff --git a/lib/features/thread/data/extensions/email_extension.dart b/lib/features/thread/data/extensions/email_extension.dart index d6a02dd60d..d5c6166bc1 100644 --- a/lib/features/thread/data/extensions/email_extension.dart +++ b/lib/features/thread/data/extensions/email_extension.dart @@ -25,6 +25,7 @@ extension EmailExtension on Email { replyTo: replyTo?.map((emailAddress) => emailAddress.toEmailAddressHiveCache()).toList(), mailboxIds: mailboxIds?.toMapString(), headerCalendarEvent: headerCalendarEvent?.toMapString(), + blobId: blobId?.value, ); } diff --git a/lib/features/thread/data/model/email_cache.dart b/lib/features/thread/data/model/email_cache.dart index 6db80e1d50..6218d7505d 100644 --- a/lib/features/thread/data/model/email_cache.dart +++ b/lib/features/thread/data/model/email_cache.dart @@ -54,6 +54,9 @@ class EmailCache extends HiveObject with EquatableMixin { @HiveField(14) Map? headerCalendarEvent; + @HiveField(15) + final String? blobId; + EmailCache( this.id, { @@ -71,6 +74,7 @@ class EmailCache extends HiveObject with EquatableMixin { this.replyTo, this.mailboxIds, this.headerCalendarEvent, + this.blobId, } ); @@ -91,5 +95,6 @@ class EmailCache extends HiveObject with EquatableMixin { hasAttachment, mailboxIds, headerCalendarEvent, + blobId, ]; } \ No newline at end of file diff --git a/lib/features/thread/domain/constants/thread_constants.dart b/lib/features/thread/domain/constants/thread_constants.dart index b72edf5be0..a5da5b4027 100644 --- a/lib/features/thread/domain/constants/thread_constants.dart +++ b/lib/features/thread/domain/constants/thread_constants.dart @@ -8,6 +8,7 @@ class ThreadConstants { static final defaultLimit = UnsignedInt(maxCountEmails); static final propertiesDefault = Properties({ EmailProperty.id, + EmailProperty.blobId, EmailProperty.subject, EmailProperty.from, EmailProperty.to, @@ -29,6 +30,7 @@ class ThreadConstants { static final propertiesQuickSearch = Properties({ EmailProperty.id, + EmailProperty.blobId, EmailProperty.subject, EmailProperty.from, EmailProperty.to, @@ -53,6 +55,7 @@ class ThreadConstants { static final propertiesGetDetailedEmail = Properties({ EmailProperty.id, + EmailProperty.blobId, EmailProperty.subject, EmailProperty.from, EmailProperty.to, @@ -74,6 +77,7 @@ class ThreadConstants { static final propertiesCalendarEvent = Properties({ EmailProperty.id, + EmailProperty.blobId, EmailProperty.subject, EmailProperty.from, EmailProperty.to, diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index 8b095991f0..29fadaca47 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2024-04-19T16:31:35.757887", + "@@last_modified": "2024-05-15T11:16:30.181676", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -3719,5 +3719,17 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "downloadMessageAsEML": "Download message as EML", + "@downloadMessageAsEML": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "downloadMessageAsEMLInProgress": "Download message as EML in progress", + "@downloadMessageAsEMLInProgress": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index 176ff8a042..d16eb61415 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -3879,4 +3879,18 @@ class AppLocalizations { name: 'youMayAttendThisMeeting', ); } + + String get downloadMessageAsEML { + return Intl.message( + 'Download message as EML', + name: 'downloadMessageAsEML', + ); + } + + String get downloadMessageAsEMLInProgress { + return Intl.message( + 'Download message as EML in progress', + name: 'downloadMessageAsEMLInProgress' + ); + } } \ No newline at end of file diff --git a/model/lib/email/email_action_type.dart b/model/lib/email/email_action_type.dart index e9a7b4460f..a125b4c3ae 100644 --- a/model/lib/email/email_action_type.dart +++ b/model/lib/email/email_action_type.dart @@ -27,5 +27,6 @@ enum EmailActionType { unsubscribe, composeFromUnsubscribeMailtoLink, archiveMessage, - printAll + printAll, + downloadMessageAsEML } \ No newline at end of file diff --git a/model/lib/email/email_property.dart b/model/lib/email/email_property.dart index f705555db8..d1981e4043 100644 --- a/model/lib/email/email_property.dart +++ b/model/lib/email/email_property.dart @@ -1,6 +1,7 @@ class EmailProperty { static const String id = 'id'; + static const String blobId = 'blobId'; static const String keywords = 'keywords'; static const String size = 'size'; static const String receivedAt = 'receivedAt'; diff --git a/model/lib/email/presentation_email.dart b/model/lib/email/presentation_email.dart index 599aced126..999605dd79 100644 --- a/model/lib/email/presentation_email.dart +++ b/model/lib/email/presentation_email.dart @@ -1,6 +1,7 @@ import 'package:core/presentation/extensions/string_extension.dart'; import 'package:equatable/equatable.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; import 'package:jmap_dart_client/jmap/core/utc_date.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; @@ -21,6 +22,7 @@ import 'package:model/mailbox/select_mode.dart'; class PresentationEmail with EquatableMixin { final EmailId? id; + final Id? blobId; final Map? keywords; final UnsignedInt? size; final UTCDate? receivedAt; @@ -44,6 +46,7 @@ class PresentationEmail with EquatableMixin { PresentationEmail({ this.id, + this.blobId, this.keywords, this.size, this.receivedAt, @@ -142,6 +145,7 @@ class PresentationEmail with EquatableMixin { @override List get props => [ id, + blobId, keywords, size, receivedAt, diff --git a/model/lib/extensions/email_extension.dart b/model/lib/extensions/email_extension.dart index a7738ebc37..7b7707abde 100644 --- a/model/lib/extensions/email_extension.dart +++ b/model/lib/extensions/email_extension.dart @@ -67,6 +67,7 @@ extension EmailExtension on Email { Email updatedEmail({Map? newKeywords, Map? newMailboxIds}) { return Email( id: id, + blobId: blobId, keywords: newKeywords ?? keywords, size: size, receivedAt: receivedAt, @@ -91,6 +92,7 @@ extension EmailExtension on Email { PresentationEmail toPresentationEmail({SelectMode selectMode = SelectMode.INACTIVE}) { return PresentationEmail( id: id, + blobId: blobId, keywords: keywords, size: size, receivedAt: receivedAt, @@ -113,6 +115,7 @@ extension EmailExtension on Email { Email combineEmail(Email newEmail, Properties updatedProperties) { return Email( id: newEmail.id, + blobId: updatedProperties.contain(EmailProperty.blobId) ? newEmail.blobId : blobId, keywords: updatedProperties.contain(EmailProperty.keywords) ? newEmail.keywords : keywords, size: updatedProperties.contain(EmailProperty.size) ? newEmail.size : size, receivedAt: updatedProperties.contain(EmailProperty.receivedAt) ? newEmail.receivedAt : receivedAt, @@ -171,6 +174,7 @@ extension EmailExtension on Email { ) { return PresentationEmail( id: emailId ?? id, + blobId: blobId, keywords: keywords, size: size, receivedAt: receivedAt, diff --git a/model/lib/extensions/presentation_email_extension.dart b/model/lib/extensions/presentation_email_extension.dart index efab88131c..e16c7e05a7 100644 --- a/model/lib/extensions/presentation_email_extension.dart +++ b/model/lib/extensions/presentation_email_extension.dart @@ -44,6 +44,7 @@ extension PresentationEmailExtension on PresentationEmail { PresentationEmail toggleSelect() { return PresentationEmail( id: this.id, + blobId: blobId, keywords: keywords, size: size, receivedAt: receivedAt, @@ -67,6 +68,7 @@ extension PresentationEmailExtension on PresentationEmail { PresentationEmail toSelectedEmail({required SelectMode selectMode}) { return PresentationEmail( id: this.id, + blobId: blobId, keywords: keywords, size: size, receivedAt: receivedAt, @@ -90,6 +92,7 @@ extension PresentationEmailExtension on PresentationEmail { Email toEmail() { return Email( id: this.id, + blobId: blobId, keywords: keywords, size: size, receivedAt: receivedAt, @@ -146,6 +149,7 @@ extension PresentationEmailExtension on PresentationEmail { return PresentationEmail( id: this.id, + blobId: blobId, keywords: keywords, size: size, receivedAt: receivedAt, @@ -182,6 +186,7 @@ extension PresentationEmailExtension on PresentationEmail { PresentationEmail withRouteWeb(Uri routeWeb) { return PresentationEmail( id: this.id, + blobId: blobId, keywords: keywords, size: size, receivedAt: receivedAt, @@ -205,6 +210,7 @@ extension PresentationEmailExtension on PresentationEmail { PresentationEmail updateKeywords(Map? newKeywords) { return PresentationEmail( id: this.id, + blobId: blobId, keywords: newKeywords, size: size, receivedAt: receivedAt, @@ -228,6 +234,7 @@ extension PresentationEmailExtension on PresentationEmail { PresentationEmail syncPresentationEmail({PresentationMailbox? mailboxContain, Uri? routeWeb}) { return PresentationEmail( id: this.id, + blobId: blobId, keywords: keywords, size: size, receivedAt: receivedAt, diff --git a/test/features/email/presentation/controller/single_email_controller_test.dart b/test/features/email/presentation/controller/single_email_controller_test.dart index 9b0bfc9808..04eb78cba9 100644 --- a/test/features/email/presentation/controller/single_email_controller_test.dart +++ b/test/features/email/presentation/controller/single_email_controller_test.dart @@ -22,6 +22,7 @@ import 'package:tmail_ui_user/features/email/domain/usecases/maybe_calendar_even import 'package:tmail_ui_user/features/email/domain/usecases/calendar_event_reject_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/download_attachment_for_web_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/download_attachments_interactor.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/download_message_as_eml_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/export_attachment_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/get_email_content_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/mark_as_email_read_interactor.dart'; @@ -81,6 +82,7 @@ const fallbackGenerators = { MockSpec(), MockSpec(), MockSpec(), + MockSpec(), ]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -112,6 +114,7 @@ void main() { final responsiveUtils = MockResponsiveUtils(); final uuid = MockUuid(); final printEmailInteractor = MockPrintEmailInteractor(); + final downloadMessageAsEMLInteractor = MockDownloadMessageAsEMLInteractor(); late SingleEmailController singleEmailController; @@ -161,6 +164,7 @@ void main() { storeOpenedEmailInteractor, viewAttachmentForWebInteractor, printEmailInteractor, + downloadMessageAsEMLInteractor, ); }); From 865c090236770c27d378ba311fe5bd8b4f369011 Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 7 May 2024 16:42:44 +0700 Subject: [PATCH 6/8] TF-825 Upgrade hive database version to 11 --- .../upgrade_hive_database_steps_v11.dart | 17 +++++++++++++++++ lib/features/caching/config/cache_version.dart | 2 +- .../caching/config/hive_cache_config.dart | 2 ++ 3 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 lib/features/base/upgradeable/upgrade_hive_database_steps_v11.dart diff --git a/lib/features/base/upgradeable/upgrade_hive_database_steps_v11.dart b/lib/features/base/upgradeable/upgrade_hive_database_steps_v11.dart new file mode 100644 index 0000000000..0f7de94c98 --- /dev/null +++ b/lib/features/base/upgradeable/upgrade_hive_database_steps_v11.dart @@ -0,0 +1,17 @@ + +import 'package:tmail_ui_user/features/base/upgradeable/upgrade_database_steps.dart'; +import 'package:tmail_ui_user/features/caching/caching_manager.dart'; + +class UpgradeHiveDatabaseStepsV11 extends UpgradeDatabaseSteps { + + final CachingManager _cachingManager; + + UpgradeHiveDatabaseStepsV11(this._cachingManager); + + @override + Future onUpgrade(int oldVersion, int newVersion) async { + if (oldVersion > 0 && oldVersion < newVersion && newVersion == 11) { + await _cachingManager.clearEmailCacheAndAllStateCache(); + } + } +} \ No newline at end of file diff --git a/lib/features/caching/config/cache_version.dart b/lib/features/caching/config/cache_version.dart index 6fafaf284c..304bfea834 100644 --- a/lib/features/caching/config/cache_version.dart +++ b/lib/features/caching/config/cache_version.dart @@ -1,4 +1,4 @@ class CacheVersion { - static const int hiveDBVersion = 10; + static const int hiveDBVersion = 11; } \ No newline at end of file diff --git a/lib/features/caching/config/hive_cache_config.dart b/lib/features/caching/config/hive_cache_config.dart index 2cf1ce1f42..40f2333b0f 100644 --- a/lib/features/caching/config/hive_cache_config.dart +++ b/lib/features/caching/config/hive_cache_config.dart @@ -7,6 +7,7 @@ import 'package:core/utils/platform_info.dart'; import 'package:hive/hive.dart'; import 'package:path_provider/path_provider.dart' as path_provider; import 'package:tmail_ui_user/features/base/upgradeable/upgrade_hive_database_steps_v10.dart'; +import 'package:tmail_ui_user/features/base/upgradeable/upgrade_hive_database_steps_v11.dart'; import 'package:tmail_ui_user/features/base/upgradeable/upgrade_hive_database_steps_v7.dart'; import 'package:tmail_ui_user/features/caching/caching_manager.dart'; import 'package:tmail_ui_user/features/caching/config/cache_version.dart'; @@ -65,6 +66,7 @@ class HiveCacheConfig { await UpgradeHiveDatabaseStepsV7(cachingManager).onUpgrade(oldVersion, newVersion); await UpgradeHiveDatabaseStepsV10(cachingManager).onUpgrade(oldVersion, newVersion); + await UpgradeHiveDatabaseStepsV11(cachingManager).onUpgrade(oldVersion, newVersion); if (oldVersion != newVersion) { await cachingManager.storeCacheVersion(newVersion); From 05c67fd605ee7e1df071b8a5b9bf99ff8c68e6ab Mon Sep 17 00:00:00 2001 From: dab246 Date: Wed, 15 May 2024 14:01:33 +0700 Subject: [PATCH 7/8] TF-825 Write unit test for download message as EML --- .../email/data/network/email_api.dart | 45 +--- .../email/data/utils/download_utils.dart | 60 +++++ .../email/presentation/utils/email_utils.dart | 1 - lib/main/bindings/core/core_bindings.dart | 2 + .../bindings/network/network_bindings.dart | 2 + .../network/network_isolate_binding.dart | 4 +- model/lib/email/attachment.dart | 4 +- .../email/data/network/email_api_test.dart | 206 ++++++++++++++++++ .../email/data/utils/download_utils_test.dart | 81 +++++++ 9 files changed, 368 insertions(+), 37 deletions(-) create mode 100644 lib/features/email/data/utils/download_utils.dart create mode 100644 test/features/email/data/network/email_api_test.dart create mode 100644 test/features/email/data/utils/download_utils_test.dart diff --git a/lib/features/email/data/network/email_api.dart b/lib/features/email/data/network/email_api.dart index 68953b94ce..deffe3f05d 100644 --- a/lib/features/email/data/network/email_api.dart +++ b/lib/features/email/data/network/email_api.dart @@ -47,7 +47,6 @@ import 'package:model/email/email_action_type.dart'; import 'package:model/email/email_property.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'; @@ -60,6 +59,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:tmail_ui_user/features/base/mixin/handle_error_mixin.dart'; import 'package:tmail_ui_user/features/composer/domain/exceptions/set_method_exception.dart'; import 'package:tmail_ui_user/features/composer/domain/model/email_request.dart'; +import 'package:tmail_ui_user/features/email/data/utils/download_utils.dart'; import 'package:tmail_ui_user/features/email/domain/exceptions/email_exceptions.dart'; import 'package:tmail_ui_user/features/email/domain/model/move_action.dart'; import 'package:tmail_ui_user/features/email/domain/model/move_to_mailbox_request.dart'; @@ -69,22 +69,23 @@ import 'package:tmail_ui_user/features/login/domain/exceptions/authentication_ex 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 { - static const String accountIdProperty = 'accountId'; - static const String blobIdProperty = 'blobId'; - static const String nameProperty = 'name'; - static const String typeProperty = 'type'; - final HttpClient _httpClient; final DownloadManager _downloadManager; final DioClient _dioClient; final Uuid _uuid; + final DownloadUtils _downloadUtils; - EmailAPI(this._httpClient, this._downloadManager, this._dioClient, this._uuid); + EmailAPI( + this._httpClient, + this._downloadManager, + this._dioClient, + this._uuid, + this._downloadUtils, + ); Future getEmailContent(Session session, AccountId accountId, EmailId emailId) async { final processingInvocation = ProcessingInvocation(); @@ -753,16 +754,13 @@ class EmailAPI with HandleSetErrorMixin { ? accountRequest.bearerToken : accountRequest.basicAuth; - final fileName = subjectEmail.isEmpty - ? '${_uuid.v1()}.eml' - : '$subjectEmail.eml'; + final fileName = _downloadUtils.createEMLFileName(subjectEmail); - final downloadUrl = _getDownloadUrl( + final downloadUrl = _downloadUtils.getEMLDownloadUrl( baseDownloadUrl: baseDownloadUrl, accountId: accountId, blobId: blobId, - fileName: fileName, - mimeType: Constant.octetStreamMimeType + subject: subjectEmail ); final headerParam = _dioClient.getHeaders(); @@ -783,23 +781,4 @@ class EmailAPI with HandleSetErrorMixin { throw NotFoundByteFileDownloadedException(); } } - - String _getDownloadUrl({ - required String baseDownloadUrl, - required AccountId accountId, - required Id blobId, - String? fileName, - String? mimeType, - }) { - final downloadUriTemplate = UriTemplate(baseDownloadUrl); - final downloadUri = downloadUriTemplate.expand({ - accountIdProperty : accountId.asString, - blobIdProperty : blobId.value, - nameProperty : '$fileName', - typeProperty : '$mimeType', - }); - final downloadUriDecoded = Uri.decodeFull(downloadUri); - log('EmailAPI::getDownloadUrl:downloadUriDecoded = $downloadUriDecoded'); - return downloadUriDecoded; - } } \ No newline at end of file diff --git a/lib/features/email/data/utils/download_utils.dart b/lib/features/email/data/utils/download_utils.dart new file mode 100644 index 0000000000..970035180c --- /dev/null +++ b/lib/features/email/data/utils/download_utils.dart @@ -0,0 +1,60 @@ +import 'package:core/data/constants/constant.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:model/extensions/account_id_extensions.dart'; +import 'package:uri/uri.dart'; +import 'package:uuid/uuid.dart'; + +class DownloadUtils { + static const String accountIdProperty = 'accountId'; + static const String blobIdProperty = 'blobId'; + static const String nameProperty = 'name'; + static const String typeProperty = 'type'; + + final Uuid _uuid; + + DownloadUtils(this._uuid); + + String getDownloadUrl({ + required String baseDownloadUrl, + required AccountId accountId, + required Id blobId, + String? fileName, + String? mimeType, + }) { + final downloadUriTemplate = UriTemplate(baseDownloadUrl); + final downloadUri = downloadUriTemplate.expand({ + accountIdProperty : accountId.asString, + blobIdProperty : blobId.value, + nameProperty : fileName ?? '', + typeProperty : mimeType ?? '', + }); + final downloadUriDecoded = Uri.decodeFull(downloadUri); + log('DownloadUtils::getDownloadUrl: $downloadUriDecoded'); + return downloadUriDecoded; + } + + String getEMLDownloadUrl({ + required String baseDownloadUrl, + required AccountId accountId, + required Id blobId, + required String subject, + }) { + final fileName = createEMLFileName(subject); + + final downloadUrl = getDownloadUrl( + baseDownloadUrl: baseDownloadUrl, + accountId: accountId, + blobId: blobId, + fileName: fileName, + mimeType: Constant.octetStreamMimeType + ); + log('DownloadUtils::getEMLDownloadUrl: $downloadUrl'); + return downloadUrl; + } + + String createEMLFileName(String subject) { + return subject.isEmpty ? '${_uuid.v1()}.eml' : '$subject.eml'; + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/utils/email_utils.dart b/lib/features/email/presentation/utils/email_utils.dart index 730866beff..79b3a66bb3 100644 --- a/lib/features/email/presentation/utils/email_utils.dart +++ b/lib/features/email/presentation/utils/email_utils.dart @@ -1,6 +1,5 @@ import 'package:collection/collection.dart'; -import 'package:core/utils/app_logger.dart'; import 'package:get/get_utils/src/get_utils/get_utils.dart'; import 'package:core/core.dart'; import 'package:dartz/dartz.dart'; diff --git a/lib/main/bindings/core/core_bindings.dart b/lib/main/bindings/core/core_bindings.dart index 389a91fcb9..65f585edc3 100644 --- a/lib/main/bindings/core/core_bindings.dart +++ b/lib/main/bindings/core/core_bindings.dart @@ -11,6 +11,7 @@ import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:get/get.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:tmail_ui_user/features/email/data/utils/download_utils.dart'; import 'package:tmail_ui_user/features/sending_queue/presentation/utils/sending_queue_isolate_manager.dart'; import 'package:tmail_ui_user/main/utils/app_config.dart'; import 'package:tmail_ui_user/main/utils/email_receive_manager.dart'; @@ -62,6 +63,7 @@ class CoreBindings extends Bindings { Get.put(AppConfigLoader()); Get.put(FileUtils()); Get.put(PrintUtils()); + Get.put(DownloadUtils(Get.find())); } void _bindingIsolate() { diff --git a/lib/main/bindings/network/network_bindings.dart b/lib/main/bindings/network/network_bindings.dart index 2a015626b8..203244aab9 100644 --- a/lib/main/bindings/network/network_bindings.dart +++ b/lib/main/bindings/network/network_bindings.dart @@ -13,6 +13,7 @@ import 'package:jmap_dart_client/http/http_client.dart'; import 'package:tmail_ui_user/features/email/data/local/html_analyzer.dart'; import 'package:tmail_ui_user/features/email/data/network/email_api.dart'; import 'package:tmail_ui_user/features/email/data/network/mdn_api.dart'; +import 'package:tmail_ui_user/features/email/data/utils/download_utils.dart'; import 'package:tmail_ui_user/features/home/data/network/session_api.dart'; import 'package:tmail_ui_user/features/login/data/local/account_cache_manager.dart'; import 'package:tmail_ui_user/features/login/data/local/authentication_info_cache_manager.dart'; @@ -111,6 +112,7 @@ class NetworkBindings extends Bindings { Get.find(), Get.find(), Get.find(), + Get.find(), )); Get.put(RuleFilterAPI(Get.find())); Get.put(VacationAPI(Get.find())); diff --git a/lib/main/bindings/network/network_isolate_binding.dart b/lib/main/bindings/network/network_isolate_binding.dart index 91f8862b5e..66ebe59386 100644 --- a/lib/main/bindings/network/network_isolate_binding.dart +++ b/lib/main/bindings/network/network_isolate_binding.dart @@ -6,6 +6,7 @@ import 'package:flutter_appauth/flutter_appauth.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/http/http_client.dart'; import 'package:tmail_ui_user/features/email/data/network/email_api.dart'; +import 'package:tmail_ui_user/features/email/data/utils/download_utils.dart'; import 'package:tmail_ui_user/features/login/data/local/account_cache_manager.dart'; import 'package:tmail_ui_user/features/login/data/local/token_oidc_cache_manager.dart'; import 'package:tmail_ui_user/features/login/data/network/authentication_client/authentication_client_base.dart'; @@ -63,7 +64,8 @@ class NetworkIsolateBindings extends Bindings { httpClient, Get.find(tag: BindingTag.isolateTag), Get.find(tag: BindingTag.isolateTag), - Get.find() + Get.find(), + Get.find(), ), tag: BindingTag.isolateTag); } diff --git a/model/lib/email/attachment.dart b/model/lib/email/attachment.dart index dd73c7bad7..9819bdf81b 100644 --- a/model/lib/email/attachment.dart +++ b/model/lib/email/attachment.dart @@ -44,8 +44,8 @@ class Attachment with EquatableMixin { final downloadUri = downloadUriTemplate.expand({ 'accountId' : accountId.id.value, 'blobId' : '${blobId?.value}', - 'name' : '$name', - 'type' : '${type?.mimeType}', + 'name' : name ?? '', + 'type' : type?.mimeType ?? '', }); return Uri.decodeFull(downloadUri); } diff --git a/test/features/email/data/network/email_api_test.dart b/test/features/email/data/network/email_api_test.dart new file mode 100644 index 0000000000..57ea1d491f --- /dev/null +++ b/test/features/email/data/network/email_api_test.dart @@ -0,0 +1,206 @@ +import 'dart:io'; + +import 'package:core/data/constants/constant.dart'; +import 'package:core/data/network/dio_client.dart'; +import 'package:core/data/network/download/download_manager.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/http/http_client.dart' as jmap; +import 'package:mockito/mockito.dart'; +import 'package:mockito/annotations.dart'; +import 'package:dio/dio.dart'; +import 'package:model/model.dart'; +import 'package:tmail_ui_user/features/email/data/network/email_api.dart'; +import 'package:tmail_ui_user/features/email/data/utils/download_utils.dart'; +import 'package:tmail_ui_user/features/email/domain/exceptions/email_exceptions.dart'; +import 'package:uuid/uuid.dart'; +import 'dart:typed_data'; + +import 'email_api_test.mocks.dart'; + +@GenerateNiceMocks([ + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), +]) +void main() { + late MockDioClient dioClient; + late MockDownloadManager downloadManager; + late MockUuid uuid; + late MockHttpClient httpClient; + late MockDownloadUtils downloadUtils; + late EmailAPI emailAPI; + + final accountId = AccountId(Id('abc123')); + const baseDownloadUrl = 'https://example.com/download/{accountId}/{blobId}?type={type}&name={name}'; + final blobId = Id('xyz123'); + + setUp(() { + dioClient = MockDioClient(); + downloadManager = MockDownloadManager(); + uuid = MockUuid(); + httpClient = MockHttpClient(); + downloadUtils = MockDownloadUtils(); + emailAPI = EmailAPI( + httpClient, + downloadManager, + dioClient, + uuid, + downloadUtils + ); + }); + + group('downloadMessageAsEML method test', () { + test('should call createAnchorElementDownloadFileWeb when download email as EML file success and return Uint8List', () async { + final accountRequestFixture = AccountRequest( + authenticationType: AuthenticationType.oidc, + token: TokenOIDC( + 'accessToken', + TokenId('token-id'), + 'refreshToken' + ), + ); + const subjectEmailFixture = 'hello'; + const fileNameFixture = '$subjectEmailFixture.eml'; + final downloadUrlFixture = 'https://example.com/download/${accountId.asString}/${blobId.value}?type=${Constant.octetStreamMimeType}&name=$fileNameFixture'; + final responseFixture = Uint8List.fromList([1, 2, 3, 4]); + + when( + downloadUtils.createEMLFileName(subjectEmailFixture) + ).thenAnswer((_) => fileNameFixture); + + when( + downloadUtils.getEMLDownloadUrl( + baseDownloadUrl: baseDownloadUrl, + accountId: accountId, + blobId: blobId, + subject: subjectEmailFixture + ) + ).thenAnswer((_) => downloadUrlFixture); + + when(dioClient.getHeaders()).thenAnswer((_) => {}); + + when( + dioClient.get( + any, + options: anyNamed('options'), + ) + ).thenAnswer((_) async => responseFixture); + + await emailAPI.downloadMessageAsEML( + accountId, + baseDownloadUrl, + accountRequestFixture, + blobId, + subjectEmailFixture); + + verify(downloadUtils.createEMLFileName(subjectEmailFixture)).called(1); + + verify( + downloadUtils.getEMLDownloadUrl( + baseDownloadUrl: baseDownloadUrl, + accountId: accountId, + blobId: blobId, + subject: subjectEmailFixture, + ) + ).called(1); + + verify(dioClient.getHeaders()).called(1); + + verify( + dioClient.get( + downloadUrlFixture, + options: argThat( + isA() + .having((o) => o.headers?[HttpHeaders.authorizationHeader], 'authorization header', accountRequestFixture.bearerToken) + .having((o) => o.headers?[HttpHeaders.acceptHeader], 'accept header', DioClient.jmapHeader) + .having((o) => o.responseType, 'responseType', ResponseType.bytes), + named: 'options', + ), + )).called(1); + + verify( + downloadManager.createAnchorElementDownloadFileWeb( + responseFixture, + fileNameFixture + ) + ).called(1); + }); + + test('should throw an NotFoundByteFileDownloadedException exception if response is not Uint8List', () async { + final accountRequestFixture = AccountRequest( + authenticationType: AuthenticationType.oidc, + token: TokenOIDC( + 'accessToken', + TokenId('token-id'), + 'refreshToken' + ), + ); + const subjectEmailFixture = 'hello'; + const fileNameFixture = '$subjectEmailFixture.eml'; + final downloadUrlFixture = 'https://example.com/download/${accountId.asString}/${blobId.value}?type=${Constant.octetStreamMimeType}&name=$fileNameFixture'; + + when( + downloadUtils.createEMLFileName(subjectEmailFixture) + ).thenAnswer((_) => fileNameFixture); + + when( + downloadUtils.getEMLDownloadUrl( + baseDownloadUrl: baseDownloadUrl, + accountId: accountId, + blobId: blobId, + subject: subjectEmailFixture + ) + ).thenAnswer((_) => downloadUrlFixture); + + when(dioClient.getHeaders()).thenAnswer((_) => {}); + + when( + dioClient.get( + any, + options: anyNamed('options'), + ) + ).thenAnswer((_) async => 'Unexpected response'); + + expect( + () async => await emailAPI.downloadMessageAsEML( + accountId, + baseDownloadUrl, + accountRequestFixture, + blobId, + subjectEmailFixture + ), + throwsA(isA()), + ); + + verify(downloadUtils.createEMLFileName(subjectEmailFixture)).called(1); + + verify( + downloadUtils.getEMLDownloadUrl( + baseDownloadUrl: baseDownloadUrl, + accountId: accountId, + blobId: blobId, + subject: subjectEmailFixture, + ) + ).called(1); + + verify(dioClient.getHeaders()).called(1); + + verify( + dioClient.get( + downloadUrlFixture, + options: argThat( + isA() + .having((o) => o.headers?[HttpHeaders.authorizationHeader], 'authorization header', accountRequestFixture.bearerToken) + .having((o) => o.headers?[HttpHeaders.acceptHeader], 'accept header', DioClient.jmapHeader) + .having((o) => o.responseType, 'responseType', ResponseType.bytes), + named: 'options', + ), + ) + ).called(1); + }); + }); +} diff --git a/test/features/email/data/utils/download_utils_test.dart b/test/features/email/data/utils/download_utils_test.dart new file mode 100644 index 0000000000..d0ef4517fd --- /dev/null +++ b/test/features/email/data/utils/download_utils_test.dart @@ -0,0 +1,81 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:tmail_ui_user/features/email/data/utils/download_utils.dart'; +import 'package:uuid/uuid.dart'; + +import 'download_utils_test.mocks.dart'; + +@GenerateNiceMocks([ + MockSpec(), +]) +void main() { + late MockUuid uuid; + late DownloadUtils downloadUtils; + + final accountId = AccountId(Id('abc123')); + const baseDownloadUrl = 'https://example.com/download/{accountId}/{blobId}?type={type}&name={name}'; + final blobId = Id('xyz123'); + + setUp(() { + uuid = MockUuid(); + downloadUtils = DownloadUtils(uuid); + }); + + group('getDownloadUrl method test', () { + test('should be url decoded correctly in case of fileName & mimeType are not null', () async { + const expectedDownloadUrl = 'https://example.com/download/abc123/xyz123?type=&name='; + + final downloadUrl = downloadUtils.getDownloadUrl( + baseDownloadUrl: baseDownloadUrl, + accountId: accountId, + blobId: blobId); + + expect(downloadUrl, expectedDownloadUrl); + }); + + test('should be url decoded correctly in case of fileName & mimeType are null', () async { + const expectedDownloadUrl = 'https://example.com/download/abc123/xyz123?type=application/octet-stream&name=hello.eml'; + + final downloadUrl = downloadUtils.getDownloadUrl( + baseDownloadUrl: baseDownloadUrl, + accountId: accountId, + blobId: blobId, + fileName: 'hello.eml', + mimeType: 'application/octet-stream'); + + expect(downloadUrl, expectedDownloadUrl); + }); + }); + + group('getEMLDownloadUrl method test', () { + test('should be url decoded correctly in case of subject is not empty', () async { + const expectedDownloadUrl = 'https://example.com/download/abc123/xyz123?type=application/octet-stream&name=hello.eml'; + + final downloadUrl = downloadUtils.getEMLDownloadUrl( + baseDownloadUrl: baseDownloadUrl, + accountId: accountId, + blobId: blobId, + subject: 'hello'); + + expect(downloadUrl, expectedDownloadUrl); + }); + + test('should be url decoded correctly in case of subject is empty', () async { + const nameFixture = '12345678'; + const expectedDownloadUrl = 'https://example.com/download/abc123/xyz123?type=application/octet-stream&name=$nameFixture.eml'; + + when(uuid.v1()).thenAnswer((_) => nameFixture); + + final downloadUrl = downloadUtils.getEMLDownloadUrl( + baseDownloadUrl: baseDownloadUrl, + accountId: accountId, + blobId: blobId, + subject: ''); + + expect(downloadUrl, expectedDownloadUrl); + }); + }); +} From f1fda99ff58e3d08f8f348ea3ce6627a030bbc13 Mon Sep 17 00:00:00 2001 From: dab246 Date: Thu, 16 May 2024 11:39:04 +0700 Subject: [PATCH 8/8] TF-825 Synchronize download messages as EML like download attachments --- .../data/datasource/email_datasource.dart | 9 - .../email_datasource_impl.dart | 19 -- .../email_hive_cache_datasource_impl.dart | 12 - .../email/data/network/email_api.dart | 49 +---- .../repository/email_repository_impl.dart | 17 -- .../email/data/utils/download_utils.dart | 60 ----- .../domain/exceptions/email_exceptions.dart | 2 - .../domain/repository/email_repository.dart | 9 - .../download_attachment_for_web_state.dart | 7 +- .../state/download_message_as_eml_state.dart | 11 - .../state/view_attachment_for_web_state.dart | 2 +- ...ownload_attachment_for_web_interactor.dart | 2 +- .../download_message_as_eml_interactor.dart | 66 ------ .../view_attachment_for_web_interactor.dart | 2 +- .../presentation/bindings/email_bindings.dart | 9 +- .../controller/single_email_controller.dart | 52 ++--- lib/l10n/intl_messages.arb | 6 +- lib/main/bindings/core/core_bindings.dart | 2 - .../bindings/network/network_bindings.dart | 2 - .../network/network_isolate_binding.dart | 4 +- lib/main/localizations/app_localizations.dart | 6 +- model/lib/email/eml_attachment.dart | 15 ++ .../presentation_email_extension.dart | 11 + pubspec.lock | 2 +- pubspec.yaml | 2 - .../email/data/network/email_api_test.dart | 206 ------------------ .../email/data/utils/download_utils_test.dart | 81 ------- ...ew_attachment_for_web_interactor_test.dart | 4 +- .../single_email_controller_test.dart | 4 - .../authorization_interceptor_test.dart | 2 +- 30 files changed, 58 insertions(+), 617 deletions(-) delete mode 100644 lib/features/email/data/utils/download_utils.dart delete mode 100644 lib/features/email/domain/state/download_message_as_eml_state.dart delete mode 100644 lib/features/email/domain/usecases/download_message_as_eml_interactor.dart create mode 100644 model/lib/email/eml_attachment.dart delete mode 100644 test/features/email/data/network/email_api_test.dart delete mode 100644 test/features/email/data/utils/download_utils_test.dart diff --git a/lib/features/email/data/datasource/email_datasource.dart b/lib/features/email/data/datasource/email_datasource.dart index 28597f22f9..db3d4c2832 100644 --- a/lib/features/email/data/datasource/email_datasource.dart +++ b/lib/features/email/data/datasource/email_datasource.dart @@ -9,7 +9,6 @@ import 'package:dio/dio.dart'; import 'package:email_recovery/email_recovery/email_recovery_action.dart'; import 'package:email_recovery/email_recovery/email_recovery_action_id.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; -import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; @@ -113,12 +112,4 @@ abstract class EmailDataSource { Future restoreDeletedMessage(RestoredDeletedMessageRequest restoredDeletedMessageRequest); Future getRestoredDeletedMessage(EmailRecoveryActionId emailRecoveryActionId); - - Future downloadMessageAsEML( - AccountId accountId, - String baseDownloadUrl, - AccountRequest accountRequest, - Id blobId, - String subjectEmail - ); } \ 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 f93817c1e0..76a5e7ee83 100644 --- a/lib/features/email/data/datasource_impl/email_datasource_impl.dart +++ b/lib/features/email/data/datasource_impl/email_datasource_impl.dart @@ -10,7 +10,6 @@ import 'package:email_recovery/email_recovery/email_recovery_action.dart'; import 'package:email_recovery/email_recovery/email_recovery_action_id.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; -import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; @@ -253,22 +252,4 @@ class EmailDataSourceImpl extends EmailDataSource { return await emailAPI.getRestoredDeletedMessage(emailRecoveryActionId); }).catchError(_exceptionThrower.throwException); } - - @override - Future downloadMessageAsEML( - AccountId accountId, - String baseDownloadUrl, - AccountRequest accountRequest, - Id blobId, - String subjectEmail - ) { - return Future.sync(() async { - return await emailAPI.downloadMessageAsEML( - accountId, - baseDownloadUrl, - accountRequest, - blobId, - subjectEmail); - }).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 b5c03f6550..c188147467 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 @@ -12,7 +12,6 @@ import 'package:dio/dio.dart'; import 'package:email_recovery/email_recovery/email_recovery_action.dart'; import 'package:email_recovery/email_recovery/email_recovery_action_id.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; -import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; @@ -304,15 +303,4 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource { Future getRestoredDeletedMessage(EmailRecoveryActionId emailRecoveryActionId) { throw UnimplementedError(); } - - @override - Future downloadMessageAsEML( - AccountId accountId, - String baseDownloadUrl, - AccountRequest accountRequest, - Id blobId, - String subjectEmail - ) { - 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 deffe3f05d..2540ba130b 100644 --- a/lib/features/email/data/network/email_api.dart +++ b/lib/features/email/data/network/email_api.dart @@ -59,7 +59,6 @@ import 'package:path_provider/path_provider.dart'; import 'package:tmail_ui_user/features/base/mixin/handle_error_mixin.dart'; import 'package:tmail_ui_user/features/composer/domain/exceptions/set_method_exception.dart'; import 'package:tmail_ui_user/features/composer/domain/model/email_request.dart'; -import 'package:tmail_ui_user/features/email/data/utils/download_utils.dart'; import 'package:tmail_ui_user/features/email/domain/exceptions/email_exceptions.dart'; import 'package:tmail_ui_user/features/email/domain/model/move_action.dart'; import 'package:tmail_ui_user/features/email/domain/model/move_to_mailbox_request.dart'; @@ -77,15 +76,8 @@ class EmailAPI with HandleSetErrorMixin { final DownloadManager _downloadManager; final DioClient _dioClient; final Uuid _uuid; - final DownloadUtils _downloadUtils; - EmailAPI( - this._httpClient, - this._downloadManager, - this._dioClient, - this._uuid, - this._downloadUtils, - ); + EmailAPI(this._httpClient, this._downloadManager, this._dioClient, this._uuid); Future getEmailContent(Session session, AccountId accountId, EmailId emailId) async { final processingInvocation = ProcessingInvocation(); @@ -742,43 +734,4 @@ class EmailAPI with HandleSetErrorMixin { throw NotFoundEmailRecoveryActionException(); } } - - Future downloadMessageAsEML( - AccountId accountId, - String baseDownloadUrl, - AccountRequest accountRequest, - Id blobId, - String subjectEmail, - ) async { - final authentication = accountRequest.authenticationType == AuthenticationType.oidc - ? accountRequest.bearerToken - : accountRequest.basicAuth; - - final fileName = _downloadUtils.createEMLFileName(subjectEmail); - - final downloadUrl = _downloadUtils.getEMLDownloadUrl( - baseDownloadUrl: baseDownloadUrl, - accountId: accountId, - blobId: blobId, - subject: subjectEmail - ); - - final headerParam = _dioClient.getHeaders(); - headerParam[HttpHeaders.authorizationHeader] = authentication; - headerParam[HttpHeaders.acceptHeader] = DioClient.jmapHeader; - - final result = await _dioClient.get( - downloadUrl, - options: Options( - headers: headerParam, - responseType: ResponseType.bytes - ) - ); - - if (result is Uint8List) { - _downloadManager.createAnchorElementDownloadFileWeb(result, fileName); - } else { - throw NotFoundByteFileDownloadedException(); - } - } } \ No newline at end of file diff --git a/lib/features/email/data/repository/email_repository_impl.dart b/lib/features/email/data/repository/email_repository_impl.dart index 276752e73b..014bca06ba 100644 --- a/lib/features/email/data/repository/email_repository_impl.dart +++ b/lib/features/email/data/repository/email_repository_impl.dart @@ -11,7 +11,6 @@ import 'package:dio/dio.dart'; import 'package:email_recovery/email_recovery/email_recovery_action.dart'; import 'package:email_recovery/email_recovery/email_recovery_action_id.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; -import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; @@ -238,20 +237,4 @@ class EmailRepositoryImpl extends EmailRepository { Future printEmail(EmailPrint emailPrint) { return _printFileDataSource.printEmail(emailPrint); } - - @override - Future downloadMessageAsEML( - AccountId accountId, - String baseDownloadUrl, - AccountRequest accountRequest, - Id blobId, - String subjectEmail - ) { - return emailDataSource[DataSourceType.network]!.downloadMessageAsEML( - accountId, - baseDownloadUrl, - accountRequest, - blobId, - subjectEmail); - } } \ No newline at end of file diff --git a/lib/features/email/data/utils/download_utils.dart b/lib/features/email/data/utils/download_utils.dart deleted file mode 100644 index 970035180c..0000000000 --- a/lib/features/email/data/utils/download_utils.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:core/data/constants/constant.dart'; -import 'package:core/utils/app_logger.dart'; -import 'package:jmap_dart_client/jmap/account_id.dart'; -import 'package:jmap_dart_client/jmap/core/id.dart'; -import 'package:model/extensions/account_id_extensions.dart'; -import 'package:uri/uri.dart'; -import 'package:uuid/uuid.dart'; - -class DownloadUtils { - static const String accountIdProperty = 'accountId'; - static const String blobIdProperty = 'blobId'; - static const String nameProperty = 'name'; - static const String typeProperty = 'type'; - - final Uuid _uuid; - - DownloadUtils(this._uuid); - - String getDownloadUrl({ - required String baseDownloadUrl, - required AccountId accountId, - required Id blobId, - String? fileName, - String? mimeType, - }) { - final downloadUriTemplate = UriTemplate(baseDownloadUrl); - final downloadUri = downloadUriTemplate.expand({ - accountIdProperty : accountId.asString, - blobIdProperty : blobId.value, - nameProperty : fileName ?? '', - typeProperty : mimeType ?? '', - }); - final downloadUriDecoded = Uri.decodeFull(downloadUri); - log('DownloadUtils::getDownloadUrl: $downloadUriDecoded'); - return downloadUriDecoded; - } - - String getEMLDownloadUrl({ - required String baseDownloadUrl, - required AccountId accountId, - required Id blobId, - required String subject, - }) { - final fileName = createEMLFileName(subject); - - final downloadUrl = getDownloadUrl( - baseDownloadUrl: baseDownloadUrl, - accountId: accountId, - blobId: blobId, - fileName: fileName, - mimeType: Constant.octetStreamMimeType - ); - log('DownloadUtils::getEMLDownloadUrl: $downloadUrl'); - return downloadUrl; - } - - String createEMLFileName(String subject) { - return subject.isEmpty ? '${_uuid.v1()}.eml' : '$subject.eml'; - } -} \ No newline at end of file diff --git a/lib/features/email/domain/exceptions/email_exceptions.dart b/lib/features/email/domain/exceptions/email_exceptions.dart index 37ab4d3f89..8057441639 100644 --- a/lib/features/email/domain/exceptions/email_exceptions.dart +++ b/lib/features/email/domain/exceptions/email_exceptions.dart @@ -6,6 +6,4 @@ class EmptyEmailContentException implements Exception {} class NotFoundEmailRecoveryActionException implements Exception {} -class NotFoundByteFileDownloadedException implements Exception {} - class NotFoundEmailBlobIdException implements Exception {} \ 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 298ebc5bc6..53bb1daf5f 100644 --- a/lib/features/email/domain/repository/email_repository.dart +++ b/lib/features/email/domain/repository/email_repository.dart @@ -10,7 +10,6 @@ import 'package:dio/dio.dart'; import 'package:email_recovery/email_recovery/email_recovery_action.dart'; import 'package:email_recovery/email_recovery/email_recovery_action_id.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; -import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; @@ -116,12 +115,4 @@ abstract class EmailRepository { Future getRestoredDeletedMessage(EmailRecoveryActionId emailRecoveryActionId); Future printEmail(EmailPrint emailPrint); - - Future downloadMessageAsEML( - AccountId accountId, - String baseDownloadUrl, - AccountRequest accountRequest, - Id blobId, - String subjectEmail - ); } \ No newline at end of file diff --git a/lib/features/email/domain/state/download_attachment_for_web_state.dart b/lib/features/email/domain/state/download_attachment_for_web_state.dart index 72082f0571..77fcc7c9b6 100644 --- a/lib/features/email/domain/state/download_attachment_for_web_state.dart +++ b/lib/features/email/domain/state/download_attachment_for_web_state.dart @@ -1,7 +1,6 @@ import 'dart:typed_data'; import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; -import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:model/download/download_task_id.dart'; import 'package:model/email/attachment.dart'; @@ -59,14 +58,14 @@ class DownloadAttachmentForWebSuccess extends UIState { class DownloadAttachmentForWebFailure extends FeatureFailure { final DownloadTaskId? taskId; - final Id? attachmentBlobId; + final Attachment? attachment; DownloadAttachmentForWebFailure({ - required this.attachmentBlobId, + this.attachment, this.taskId, dynamic exception }) : super(exception: exception); @override - List get props => [taskId, ...super.props]; + List get props => [attachment, taskId, ...super.props]; } \ No newline at end of file diff --git a/lib/features/email/domain/state/download_message_as_eml_state.dart b/lib/features/email/domain/state/download_message_as_eml_state.dart deleted file mode 100644 index a6dcf74306..0000000000 --- a/lib/features/email/domain/state/download_message_as_eml_state.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:core/presentation/state/failure.dart'; -import 'package:core/presentation/state/success.dart'; - -class StartDownloadMessageAsEML extends LoadingState {} - -class DownloadMessageAsEMLSuccess extends UIState {} - -class DownloadMessageAsEMLFailure extends FeatureFailure { - - DownloadMessageAsEMLFailure(dynamic exception) : super(exception: exception); -} \ No newline at end of file diff --git a/lib/features/email/domain/state/view_attachment_for_web_state.dart b/lib/features/email/domain/state/view_attachment_for_web_state.dart index d1e812465a..96dcaab065 100644 --- a/lib/features/email/domain/state/view_attachment_for_web_state.dart +++ b/lib/features/email/domain/state/view_attachment_for_web_state.dart @@ -22,7 +22,7 @@ class ViewAttachmentForWebSuccess extends DownloadAttachmentForWebSuccess { class ViewAttachmentForWebFailure extends DownloadAttachmentForWebFailure { ViewAttachmentForWebFailure({ - required super.attachmentBlobId, + required super.attachment, super.taskId, super.exception, }); diff --git a/lib/features/email/domain/usecases/download_attachment_for_web_interactor.dart b/lib/features/email/domain/usecases/download_attachment_for_web_interactor.dart index 9e7b820887..3715d76478 100644 --- a/lib/features/email/domain/usecases/download_attachment_for_web_interactor.dart +++ b/lib/features/email/domain/usecases/download_attachment_for_web_interactor.dart @@ -72,7 +72,7 @@ class DownloadAttachmentForWebInteractor { } catch (exception) { yield Left( DownloadAttachmentForWebFailure( - attachmentBlobId: attachment.blobId, + attachment: attachment, taskId: taskId, exception: exception ) diff --git a/lib/features/email/domain/usecases/download_message_as_eml_interactor.dart b/lib/features/email/domain/usecases/download_message_as_eml_interactor.dart deleted file mode 100644 index d22cf7b76d..0000000000 --- a/lib/features/email/domain/usecases/download_message_as_eml_interactor.dart +++ /dev/null @@ -1,66 +0,0 @@ -import 'dart:async'; - -import 'package:core/presentation/state/failure.dart'; -import 'package:core/presentation/state/success.dart'; -import 'package:dartz/dartz.dart'; -import 'package:jmap_dart_client/jmap/account_id.dart'; -import 'package:jmap_dart_client/jmap/core/id.dart'; -import 'package:jmap_dart_client/jmap/core/user_name.dart'; -import 'package:model/account/account_request.dart'; -import 'package:model/account/authentication_type.dart'; -import 'package:model/account/password.dart'; -import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; -import 'package:tmail_ui_user/features/email/domain/state/download_message_as_eml_state.dart'; -import 'package:tmail_ui_user/features/login/domain/repository/account_repository.dart'; -import 'package:tmail_ui_user/features/login/domain/repository/authentication_oidc_repository.dart'; -import 'package:tmail_ui_user/features/login/domain/repository/credential_repository.dart'; - -class DownloadMessageAsEMLInteractor { - final EmailRepository _emailRepository; - final CredentialRepository _credentialRepository; - final AccountRepository _accountRepository; - final AuthenticationOIDCRepository _authenticationOIDCRepository; - - DownloadMessageAsEMLInteractor( - this._emailRepository, - this._credentialRepository, - this._accountRepository, - this._authenticationOIDCRepository - ); - - Stream> execute( - AccountId accountId, - String baseDownloadUrl, - Id blobId, - String subjectEmail - ) async* { - try { - yield Right(StartDownloadMessageAsEML()); - - final currentAccount = await _accountRepository.getCurrentAccount(); - AccountRequest? accountRequest; - - if (currentAccount.authenticationType == AuthenticationType.oidc) { - final tokenOidc = await _authenticationOIDCRepository.getStoredTokenOIDC(currentAccount.id); - accountRequest = AccountRequest.withOidc(token: tokenOidc); - } else { - final authenticationInfoCache = await _credentialRepository.getAuthenticationInfoStored(); - accountRequest = AccountRequest.withBasic( - userName: UserName(authenticationInfoCache.username), - password: Password(authenticationInfoCache.password), - ); - } - - await _emailRepository.downloadMessageAsEML( - accountId, - baseDownloadUrl, - accountRequest, - blobId, - subjectEmail); - - yield Right(DownloadMessageAsEMLSuccess()); - } catch (exception) { - yield Left(DownloadMessageAsEMLFailure(exception)); - } - } -} \ No newline at end of file diff --git a/lib/features/email/domain/usecases/view_attachment_for_web_interactor.dart b/lib/features/email/domain/usecases/view_attachment_for_web_interactor.dart index f69ff81d82..fc318bf9e3 100644 --- a/lib/features/email/domain/usecases/view_attachment_for_web_interactor.dart +++ b/lib/features/email/domain/usecases/view_attachment_for_web_interactor.dart @@ -34,7 +34,7 @@ class ViewAttachmentForWebInteractor { (failure) { if (failure is DownloadAttachmentForWebFailure) { return Left(ViewAttachmentForWebFailure( - attachmentBlobId: attachment.blobId, + attachment: attachment, taskId: failure.taskId, exception: failure.exception, )); diff --git a/lib/features/email/presentation/bindings/email_bindings.dart b/lib/features/email/presentation/bindings/email_bindings.dart index dcdcabb6df..19f0b8aeba 100644 --- a/lib/features/email/presentation/bindings/email_bindings.dart +++ b/lib/features/email/presentation/bindings/email_bindings.dart @@ -14,7 +14,6 @@ import 'package:tmail_ui_user/features/email/data/repository/email_repository_im import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/download_attachment_for_web_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/download_attachments_interactor.dart'; -import 'package:tmail_ui_user/features/email/domain/usecases/download_message_as_eml_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/export_attachment_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/get_email_content_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/get_stored_email_state_interactor.dart'; @@ -70,8 +69,7 @@ class EmailBindings extends BaseBindings { Get.find(), Get.find(), Get.find(), - Get.find(), - Get.find(), + Get.find() )); } @@ -154,11 +152,6 @@ class EmailBindings extends BaseBindings { Get.find())); Get.lazyPut(() => StoreOpenedEmailInteractor(Get.find())); Get.lazyPut(() => PrintEmailInteractor(Get.find())); - Get.lazyPut(() => DownloadMessageAsEMLInteractor( - Get.find(), - Get.find(), - Get.find(), - Get.find())); IdentityInteractorsBindings().dependencies(); } diff --git a/lib/features/email/presentation/controller/single_email_controller.dart b/lib/features/email/presentation/controller/single_email_controller.dart index 1d97a9afb2..4c81c87ee6 100644 --- a/lib/features/email/presentation/controller/single_email_controller.dart +++ b/lib/features/email/presentation/controller/single_email_controller.dart @@ -20,6 +20,7 @@ import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:jmap_dart_client/jmap/mdn/disposition.dart'; import 'package:jmap_dart_client/jmap/mdn/mdn.dart'; import 'package:mime/mime.dart'; +import 'package:model/email/eml_attachment.dart'; import 'package:model/model.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; @@ -43,7 +44,6 @@ import 'package:tmail_ui_user/features/email/domain/state/calendar_event_reject_ import 'package:tmail_ui_user/features/email/domain/state/calendar_event_reply_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/download_attachment_for_web_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/download_attachments_state.dart'; -import 'package:tmail_ui_user/features/email/domain/state/download_message_as_eml_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/export_attachment_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/get_email_content_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_read_state.dart'; @@ -59,7 +59,6 @@ import 'package:tmail_ui_user/features/email/domain/usecases/maybe_calendar_even import 'package:tmail_ui_user/features/email/domain/usecases/calendar_event_reject_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/download_attachment_for_web_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/download_attachments_interactor.dart'; -import 'package:tmail_ui_user/features/email/domain/usecases/download_message_as_eml_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/export_attachment_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/get_email_content_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/mark_as_email_read_interactor.dart'; @@ -126,7 +125,6 @@ class SingleEmailController extends BaseController with AppLoaderMixin { final StoreOpenedEmailInteractor _storeOpenedEmailInteractor; final ViewAttachmentForWebInteractor _viewAttachmentForWebInteractor; final PrintEmailInteractor _printEmailInteractor; - final DownloadMessageAsEMLInteractor _downloadMessageAsEMLInteractor; CreateNewEmailRuleFilterInteractor? _createNewEmailRuleFilterInteractor; SendReceiptToSenderInteractor? _sendReceiptToSenderInteractor; @@ -175,7 +173,6 @@ class SingleEmailController extends BaseController with AppLoaderMixin { this._storeOpenedEmailInteractor, this._viewAttachmentForWebInteractor, this._printEmailInteractor, - this._downloadMessageAsEMLInteractor, ); @override @@ -236,8 +233,6 @@ class SingleEmailController extends BaseController with AppLoaderMixin { _handlePrintEmailSuccess(success); } else if (success is CalendarEventReplySuccess) { _calendarEventSuccess(success); - } else if (success is StartDownloadMessageAsEML) { - _showMessageWhenStartingDownloadMessageAsEML(); } } @@ -817,7 +812,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } else { consumeState(Stream.value( Left(DownloadAttachmentForWebFailure( - attachmentBlobId: attachment.blobId, + attachment: attachment, exception: NotFoundSessionException())) )); } @@ -838,7 +833,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } else { consumeState(Stream.value( Left(ViewAttachmentForWebFailure( - attachmentBlobId: attachment.blobId, + attachment: attachment, exception: NotFoundSessionException())) )); } @@ -883,12 +878,16 @@ class SingleEmailController extends BaseController with AppLoaderMixin { mailboxDashBoardController.deleteDownloadTask(failure.taskId!); } - _updateAttachmentsViewState(failure.attachmentBlobId, Left(failure)); + if (failure.attachment != null) { + _updateAttachmentsViewState(failure.attachment?.blobId, Left(failure)); + } if (currentOverlayContext != null && currentContext != null) { appToast.showToastErrorMessage( currentOverlayContext!, - AppLocalizations.of(currentContext!).attachment_download_failed); + failure.attachment is EMLAttachment + ? AppLocalizations.of(currentContext!).downloadMessageAsEMLFailed + : AppLocalizations.of(currentContext!).attachment_download_failed); } } @@ -1784,37 +1783,12 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } void _downloadMessageAsEML(PresentationEmail presentationEmail) { - final accountId = mailboxDashBoardController.accountId.value; - final session = mailboxDashBoardController.sessionCurrent; - - if (accountId == null || session == null) { - consumeState(Stream.value(Left(DownloadMessageAsEMLFailure(NotFoundSessionException())))); - return; - } - - final blobId = presentationEmail.blobId; - if (blobId == null) { - consumeState(Stream.value(Left(DownloadMessageAsEMLFailure(NotFoundEmailBlobIdException())))); + final emlAttachment = presentationEmail.createEMLAttachment(); + if (emlAttachment.blobId == null) { + consumeState(Stream.value(Left(DownloadAttachmentForWebFailure(exception: NotFoundEmailBlobIdException())))); return; } - final baseDownloadUrl = session.getDownloadUrl(jmapUrl: dynamicUrlInterceptors.jmapUrl); - - consumeState(_downloadMessageAsEMLInteractor.execute( - accountId, - baseDownloadUrl, - blobId, - presentationEmail.getEmailTitle() - )); - } - - void _showMessageWhenStartingDownloadMessageAsEML() { - if (currentOverlayContext != null && currentContext != null) { - appToast.showToastMessage( - currentOverlayContext!, - AppLocalizations.of(currentContext!).downloadMessageAsEMLInProgress, - leadingSVGIconColor: AppColor.primaryColor, - leadingSVGIcon: imagePaths.icDownloadAttachment); - } + downloadAttachmentForWeb(emlAttachment); } } \ No newline at end of file diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index 29fadaca47..81ce7423e4 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2024-05-15T11:16:30.181676", + "@@last_modified": "2024-05-16T11:36:53.008027", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -3726,8 +3726,8 @@ "placeholders_order": [], "placeholders": {} }, - "downloadMessageAsEMLInProgress": "Download message as EML in progress", - "@downloadMessageAsEMLInProgress": { + "downloadMessageAsEMLFailed": "Download message as EML failed", + "@downloadMessageAsEMLFailed": { "type": "text", "placeholders_order": [], "placeholders": {} diff --git a/lib/main/bindings/core/core_bindings.dart b/lib/main/bindings/core/core_bindings.dart index 65f585edc3..389a91fcb9 100644 --- a/lib/main/bindings/core/core_bindings.dart +++ b/lib/main/bindings/core/core_bindings.dart @@ -11,7 +11,6 @@ import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:get/get.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:tmail_ui_user/features/email/data/utils/download_utils.dart'; import 'package:tmail_ui_user/features/sending_queue/presentation/utils/sending_queue_isolate_manager.dart'; import 'package:tmail_ui_user/main/utils/app_config.dart'; import 'package:tmail_ui_user/main/utils/email_receive_manager.dart'; @@ -63,7 +62,6 @@ class CoreBindings extends Bindings { Get.put(AppConfigLoader()); Get.put(FileUtils()); Get.put(PrintUtils()); - Get.put(DownloadUtils(Get.find())); } void _bindingIsolate() { diff --git a/lib/main/bindings/network/network_bindings.dart b/lib/main/bindings/network/network_bindings.dart index 203244aab9..2a015626b8 100644 --- a/lib/main/bindings/network/network_bindings.dart +++ b/lib/main/bindings/network/network_bindings.dart @@ -13,7 +13,6 @@ import 'package:jmap_dart_client/http/http_client.dart'; import 'package:tmail_ui_user/features/email/data/local/html_analyzer.dart'; import 'package:tmail_ui_user/features/email/data/network/email_api.dart'; import 'package:tmail_ui_user/features/email/data/network/mdn_api.dart'; -import 'package:tmail_ui_user/features/email/data/utils/download_utils.dart'; import 'package:tmail_ui_user/features/home/data/network/session_api.dart'; import 'package:tmail_ui_user/features/login/data/local/account_cache_manager.dart'; import 'package:tmail_ui_user/features/login/data/local/authentication_info_cache_manager.dart'; @@ -112,7 +111,6 @@ class NetworkBindings extends Bindings { Get.find(), Get.find(), Get.find(), - Get.find(), )); Get.put(RuleFilterAPI(Get.find())); Get.put(VacationAPI(Get.find())); diff --git a/lib/main/bindings/network/network_isolate_binding.dart b/lib/main/bindings/network/network_isolate_binding.dart index 66ebe59386..91f8862b5e 100644 --- a/lib/main/bindings/network/network_isolate_binding.dart +++ b/lib/main/bindings/network/network_isolate_binding.dart @@ -6,7 +6,6 @@ import 'package:flutter_appauth/flutter_appauth.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/http/http_client.dart'; import 'package:tmail_ui_user/features/email/data/network/email_api.dart'; -import 'package:tmail_ui_user/features/email/data/utils/download_utils.dart'; import 'package:tmail_ui_user/features/login/data/local/account_cache_manager.dart'; import 'package:tmail_ui_user/features/login/data/local/token_oidc_cache_manager.dart'; import 'package:tmail_ui_user/features/login/data/network/authentication_client/authentication_client_base.dart'; @@ -64,8 +63,7 @@ class NetworkIsolateBindings extends Bindings { httpClient, Get.find(tag: BindingTag.isolateTag), Get.find(tag: BindingTag.isolateTag), - Get.find(), - Get.find(), + Get.find() ), tag: BindingTag.isolateTag); } diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index d16eb61415..2f8f746615 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -3887,10 +3887,10 @@ class AppLocalizations { ); } - String get downloadMessageAsEMLInProgress { + String get downloadMessageAsEMLFailed { return Intl.message( - 'Download message as EML in progress', - name: 'downloadMessageAsEMLInProgress' + 'Download message as EML failed', + name: 'downloadMessageAsEMLFailed', ); } } \ No newline at end of file diff --git a/model/lib/email/eml_attachment.dart b/model/lib/email/eml_attachment.dart new file mode 100644 index 0000000000..08189e14be --- /dev/null +++ b/model/lib/email/eml_attachment.dart @@ -0,0 +1,15 @@ + +import 'package:model/email/attachment.dart'; + +class EMLAttachment extends Attachment { + + EMLAttachment({ + super.partId, + super.blobId, + super.size, + super.name, + super.type, + super.cid, + super.disposition, + }); +} \ No newline at end of file diff --git a/model/lib/extensions/presentation_email_extension.dart b/model/lib/extensions/presentation_email_extension.dart index e16c7e05a7..1ca8061f4b 100644 --- a/model/lib/extensions/presentation_email_extension.dart +++ b/model/lib/extensions/presentation_email_extension.dart @@ -1,14 +1,17 @@ import 'dart:ui'; +import 'package:core/data/constants/constant.dart'; import 'package:core/domain/extensions/datetime_extension.dart'; import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/utils/app_logger.dart'; +import 'package:http_parser/http_parser.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/email/email_action_type.dart'; +import 'package:model/email/eml_attachment.dart'; import 'package:model/email/presentation_email.dart'; import 'package:model/extensions/email_address_extension.dart'; import 'package:model/extensions/list_email_address_extension.dart'; @@ -269,4 +272,12 @@ extension PresentationEmailExtension on PresentationEmail { return false; } + + EMLAttachment createEMLAttachment() { + return EMLAttachment( + blobId: blobId, + name: getEmailTitle().isEmpty ? '${blobId?.value}.eml' : '${getEmailTitle()}.eml', + type: MediaType.parse(Constant.octetStreamMimeType) + ); + } } \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index c9a9af4f9c..95382eb089 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1758,7 +1758,7 @@ packages: source: hosted version: "2.2.2" uri: - dependency: "direct main" + dependency: transitive description: name: uri sha256: "889eea21e953187c6099802b7b4cf5219ba8f3518f604a1033064d45b1b8268a" diff --git a/pubspec.yaml b/pubspec.yaml index 41ede2c62a..713b1693ab 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -232,8 +232,6 @@ dependencies: mime: 1.0.4 - uri: 1.0.0 - dev_dependencies: flutter_test: sdk: flutter diff --git a/test/features/email/data/network/email_api_test.dart b/test/features/email/data/network/email_api_test.dart deleted file mode 100644 index 57ea1d491f..0000000000 --- a/test/features/email/data/network/email_api_test.dart +++ /dev/null @@ -1,206 +0,0 @@ -import 'dart:io'; - -import 'package:core/data/constants/constant.dart'; -import 'package:core/data/network/dio_client.dart'; -import 'package:core/data/network/download/download_manager.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:jmap_dart_client/jmap/account_id.dart'; -import 'package:jmap_dart_client/jmap/core/id.dart'; -import 'package:jmap_dart_client/http/http_client.dart' as jmap; -import 'package:mockito/mockito.dart'; -import 'package:mockito/annotations.dart'; -import 'package:dio/dio.dart'; -import 'package:model/model.dart'; -import 'package:tmail_ui_user/features/email/data/network/email_api.dart'; -import 'package:tmail_ui_user/features/email/data/utils/download_utils.dart'; -import 'package:tmail_ui_user/features/email/domain/exceptions/email_exceptions.dart'; -import 'package:uuid/uuid.dart'; -import 'dart:typed_data'; - -import 'email_api_test.mocks.dart'; - -@GenerateNiceMocks([ - MockSpec(), - MockSpec(), - MockSpec(), - MockSpec(), - MockSpec(), -]) -void main() { - late MockDioClient dioClient; - late MockDownloadManager downloadManager; - late MockUuid uuid; - late MockHttpClient httpClient; - late MockDownloadUtils downloadUtils; - late EmailAPI emailAPI; - - final accountId = AccountId(Id('abc123')); - const baseDownloadUrl = 'https://example.com/download/{accountId}/{blobId}?type={type}&name={name}'; - final blobId = Id('xyz123'); - - setUp(() { - dioClient = MockDioClient(); - downloadManager = MockDownloadManager(); - uuid = MockUuid(); - httpClient = MockHttpClient(); - downloadUtils = MockDownloadUtils(); - emailAPI = EmailAPI( - httpClient, - downloadManager, - dioClient, - uuid, - downloadUtils - ); - }); - - group('downloadMessageAsEML method test', () { - test('should call createAnchorElementDownloadFileWeb when download email as EML file success and return Uint8List', () async { - final accountRequestFixture = AccountRequest( - authenticationType: AuthenticationType.oidc, - token: TokenOIDC( - 'accessToken', - TokenId('token-id'), - 'refreshToken' - ), - ); - const subjectEmailFixture = 'hello'; - const fileNameFixture = '$subjectEmailFixture.eml'; - final downloadUrlFixture = 'https://example.com/download/${accountId.asString}/${blobId.value}?type=${Constant.octetStreamMimeType}&name=$fileNameFixture'; - final responseFixture = Uint8List.fromList([1, 2, 3, 4]); - - when( - downloadUtils.createEMLFileName(subjectEmailFixture) - ).thenAnswer((_) => fileNameFixture); - - when( - downloadUtils.getEMLDownloadUrl( - baseDownloadUrl: baseDownloadUrl, - accountId: accountId, - blobId: blobId, - subject: subjectEmailFixture - ) - ).thenAnswer((_) => downloadUrlFixture); - - when(dioClient.getHeaders()).thenAnswer((_) => {}); - - when( - dioClient.get( - any, - options: anyNamed('options'), - ) - ).thenAnswer((_) async => responseFixture); - - await emailAPI.downloadMessageAsEML( - accountId, - baseDownloadUrl, - accountRequestFixture, - blobId, - subjectEmailFixture); - - verify(downloadUtils.createEMLFileName(subjectEmailFixture)).called(1); - - verify( - downloadUtils.getEMLDownloadUrl( - baseDownloadUrl: baseDownloadUrl, - accountId: accountId, - blobId: blobId, - subject: subjectEmailFixture, - ) - ).called(1); - - verify(dioClient.getHeaders()).called(1); - - verify( - dioClient.get( - downloadUrlFixture, - options: argThat( - isA() - .having((o) => o.headers?[HttpHeaders.authorizationHeader], 'authorization header', accountRequestFixture.bearerToken) - .having((o) => o.headers?[HttpHeaders.acceptHeader], 'accept header', DioClient.jmapHeader) - .having((o) => o.responseType, 'responseType', ResponseType.bytes), - named: 'options', - ), - )).called(1); - - verify( - downloadManager.createAnchorElementDownloadFileWeb( - responseFixture, - fileNameFixture - ) - ).called(1); - }); - - test('should throw an NotFoundByteFileDownloadedException exception if response is not Uint8List', () async { - final accountRequestFixture = AccountRequest( - authenticationType: AuthenticationType.oidc, - token: TokenOIDC( - 'accessToken', - TokenId('token-id'), - 'refreshToken' - ), - ); - const subjectEmailFixture = 'hello'; - const fileNameFixture = '$subjectEmailFixture.eml'; - final downloadUrlFixture = 'https://example.com/download/${accountId.asString}/${blobId.value}?type=${Constant.octetStreamMimeType}&name=$fileNameFixture'; - - when( - downloadUtils.createEMLFileName(subjectEmailFixture) - ).thenAnswer((_) => fileNameFixture); - - when( - downloadUtils.getEMLDownloadUrl( - baseDownloadUrl: baseDownloadUrl, - accountId: accountId, - blobId: blobId, - subject: subjectEmailFixture - ) - ).thenAnswer((_) => downloadUrlFixture); - - when(dioClient.getHeaders()).thenAnswer((_) => {}); - - when( - dioClient.get( - any, - options: anyNamed('options'), - ) - ).thenAnswer((_) async => 'Unexpected response'); - - expect( - () async => await emailAPI.downloadMessageAsEML( - accountId, - baseDownloadUrl, - accountRequestFixture, - blobId, - subjectEmailFixture - ), - throwsA(isA()), - ); - - verify(downloadUtils.createEMLFileName(subjectEmailFixture)).called(1); - - verify( - downloadUtils.getEMLDownloadUrl( - baseDownloadUrl: baseDownloadUrl, - accountId: accountId, - blobId: blobId, - subject: subjectEmailFixture, - ) - ).called(1); - - verify(dioClient.getHeaders()).called(1); - - verify( - dioClient.get( - downloadUrlFixture, - options: argThat( - isA() - .having((o) => o.headers?[HttpHeaders.authorizationHeader], 'authorization header', accountRequestFixture.bearerToken) - .having((o) => o.headers?[HttpHeaders.acceptHeader], 'accept header', DioClient.jmapHeader) - .having((o) => o.responseType, 'responseType', ResponseType.bytes), - named: 'options', - ), - ) - ).called(1); - }); - }); -} diff --git a/test/features/email/data/utils/download_utils_test.dart b/test/features/email/data/utils/download_utils_test.dart deleted file mode 100644 index d0ef4517fd..0000000000 --- a/test/features/email/data/utils/download_utils_test.dart +++ /dev/null @@ -1,81 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:jmap_dart_client/jmap/account_id.dart'; -import 'package:jmap_dart_client/jmap/core/id.dart'; -import 'package:mockito/annotations.dart'; -import 'package:mockito/mockito.dart'; -import 'package:tmail_ui_user/features/email/data/utils/download_utils.dart'; -import 'package:uuid/uuid.dart'; - -import 'download_utils_test.mocks.dart'; - -@GenerateNiceMocks([ - MockSpec(), -]) -void main() { - late MockUuid uuid; - late DownloadUtils downloadUtils; - - final accountId = AccountId(Id('abc123')); - const baseDownloadUrl = 'https://example.com/download/{accountId}/{blobId}?type={type}&name={name}'; - final blobId = Id('xyz123'); - - setUp(() { - uuid = MockUuid(); - downloadUtils = DownloadUtils(uuid); - }); - - group('getDownloadUrl method test', () { - test('should be url decoded correctly in case of fileName & mimeType are not null', () async { - const expectedDownloadUrl = 'https://example.com/download/abc123/xyz123?type=&name='; - - final downloadUrl = downloadUtils.getDownloadUrl( - baseDownloadUrl: baseDownloadUrl, - accountId: accountId, - blobId: blobId); - - expect(downloadUrl, expectedDownloadUrl); - }); - - test('should be url decoded correctly in case of fileName & mimeType are null', () async { - const expectedDownloadUrl = 'https://example.com/download/abc123/xyz123?type=application/octet-stream&name=hello.eml'; - - final downloadUrl = downloadUtils.getDownloadUrl( - baseDownloadUrl: baseDownloadUrl, - accountId: accountId, - blobId: blobId, - fileName: 'hello.eml', - mimeType: 'application/octet-stream'); - - expect(downloadUrl, expectedDownloadUrl); - }); - }); - - group('getEMLDownloadUrl method test', () { - test('should be url decoded correctly in case of subject is not empty', () async { - const expectedDownloadUrl = 'https://example.com/download/abc123/xyz123?type=application/octet-stream&name=hello.eml'; - - final downloadUrl = downloadUtils.getEMLDownloadUrl( - baseDownloadUrl: baseDownloadUrl, - accountId: accountId, - blobId: blobId, - subject: 'hello'); - - expect(downloadUrl, expectedDownloadUrl); - }); - - test('should be url decoded correctly in case of subject is empty', () async { - const nameFixture = '12345678'; - const expectedDownloadUrl = 'https://example.com/download/abc123/xyz123?type=application/octet-stream&name=$nameFixture.eml'; - - when(uuid.v1()).thenAnswer((_) => nameFixture); - - final downloadUrl = downloadUtils.getEMLDownloadUrl( - baseDownloadUrl: baseDownloadUrl, - accountId: accountId, - blobId: blobId, - subject: ''); - - expect(downloadUrl, expectedDownloadUrl); - }); - }); -} diff --git a/test/features/email/domain/usecases/view_attachment_for_web_interactor_test.dart b/test/features/email/domain/usecases/view_attachment_for_web_interactor_test.dart index 6b947149c1..ba4fe053c7 100644 --- a/test/features/email/domain/usecases/view_attachment_for_web_interactor_test.dart +++ b/test/features/email/domain/usecases/view_attachment_for_web_interactor_test.dart @@ -55,7 +55,7 @@ void main() { (_) => Stream.value( Left( DownloadAttachmentForWebFailure( - attachmentBlobId: testAttachment.blobId, + attachment: testAttachment, taskId: testDownloadTaskId, exception: testException, ), @@ -75,7 +75,7 @@ void main() { emitsInOrder([ Left( ViewAttachmentForWebFailure( - attachmentBlobId: testAttachment.blobId, + attachment: testAttachment, taskId: testDownloadTaskId, exception: testException, ), diff --git a/test/features/email/presentation/controller/single_email_controller_test.dart b/test/features/email/presentation/controller/single_email_controller_test.dart index 04eb78cba9..9b0bfc9808 100644 --- a/test/features/email/presentation/controller/single_email_controller_test.dart +++ b/test/features/email/presentation/controller/single_email_controller_test.dart @@ -22,7 +22,6 @@ import 'package:tmail_ui_user/features/email/domain/usecases/maybe_calendar_even import 'package:tmail_ui_user/features/email/domain/usecases/calendar_event_reject_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/download_attachment_for_web_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/download_attachments_interactor.dart'; -import 'package:tmail_ui_user/features/email/domain/usecases/download_message_as_eml_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/export_attachment_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/get_email_content_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/mark_as_email_read_interactor.dart'; @@ -82,7 +81,6 @@ const fallbackGenerators = { MockSpec(), MockSpec(), MockSpec(), - MockSpec(), ]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -114,7 +112,6 @@ void main() { final responsiveUtils = MockResponsiveUtils(); final uuid = MockUuid(); final printEmailInteractor = MockPrintEmailInteractor(); - final downloadMessageAsEMLInteractor = MockDownloadMessageAsEMLInteractor(); late SingleEmailController singleEmailController; @@ -164,7 +161,6 @@ void main() { storeOpenedEmailInteractor, viewAttachmentForWebInteractor, printEmailInteractor, - downloadMessageAsEMLInteractor, ); }); diff --git a/test/features/interceptor/authorization_interceptor_test.dart b/test/features/interceptor/authorization_interceptor_test.dart index b614309c53..fcc70b1cc5 100644 --- a/test/features/interceptor/authorization_interceptor_test.dart +++ b/test/features/interceptor/authorization_interceptor_test.dart @@ -59,7 +59,7 @@ void main() { }; final baseOption = BaseOptions(headers: headers); dio = Dio(baseOption) - ..options.baseUrl = baseUrl;; + ..options.baseUrl = baseUrl; authenticationClient = MockAuthenticationClientBase(); tokenOidcCacheManager = MockTokenOidcCacheManager();