Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TF-825 Download email as EML file #2854

Merged
merged 8 commits into from
May 16, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions core/lib/data/constants/constant.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}
17 changes: 17 additions & 0 deletions lib/features/base/upgradeable/upgrade_hive_database_steps_v11.dart
Original file line number Diff line number Diff line change
@@ -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<void> onUpgrade(int oldVersion, int newVersion) async {
if (oldVersion > 0 && oldVersion < newVersion && newVersion == 11) {
await _cachingManager.clearEmailCacheAndAllStateCache();
}
}
}
2 changes: 1 addition & 1 deletion lib/features/caching/config/cache_version.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@

class CacheVersion {
static const int hiveDBVersion = 10;
static const int hiveDBVersion = 11;
}
6 changes: 5 additions & 1 deletion lib/features/caching/config/hive_cache_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -65,8 +66,11 @@ class HiveCacheConfig {

await UpgradeHiveDatabaseStepsV7(cachingManager).onUpgrade(oldVersion, newVersion);
await UpgradeHiveDatabaseStepsV10(cachingManager).onUpgrade(oldVersion, newVersion);
await UpgradeHiveDatabaseStepsV11(cachingManager).onUpgrade(oldVersion, newVersion);

await cachingManager.storeCacheVersion(newVersion);
if (oldVersion != newVersion) {
await cachingManager.storeCacheVersion(newVersion);
}
}

Future<void> initializeEncryptionKey() async {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 '';
}
Expand All @@ -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 '';
}
Expand Down
9 changes: 9 additions & 0 deletions lib/features/email/data/datasource/email_datasource.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -112,4 +113,12 @@ abstract class EmailDataSource {
Future<EmailRecoveryAction> restoreDeletedMessage(RestoredDeletedMessageRequest restoredDeletedMessageRequest);

Future<EmailRecoveryAction> getRestoredDeletedMessage(EmailRecoveryActionId emailRecoveryActionId);

Future<void> downloadMessageAsEML(
AccountId accountId,
String baseDownloadUrl,
AccountRequest accountRequest,
Id blobId,
String subjectEmail
);
}
19 changes: 19 additions & 0 deletions lib/features/email/data/datasource_impl/email_datasource_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -252,4 +253,22 @@ class EmailDataSourceImpl extends EmailDataSource {
return await emailAPI.getRestoredDeletedMessage(emailRecoveryActionId);
}).catchError(_exceptionThrower.throwException);
}

@override
Future<void> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -303,4 +304,15 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource {
Future<EmailRecoveryAction> getRestoredDeletedMessage(EmailRecoveryActionId emailRecoveryActionId) {
throw UnimplementedError();
}

@override
Future<void> downloadMessageAsEML(
AccountId accountId,
String baseDownloadUrl,
AccountRequest accountRequest,
Id blobId,
String subjectEmail
) {
throw UnimplementedError();
}
}
55 changes: 51 additions & 4 deletions lib/features/email/data/network/email_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -59,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';
Expand All @@ -76,8 +77,15 @@ 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);
EmailAPI(
this._httpClient,
this._downloadManager,
this._dioClient,
this._uuid,
this._downloadUtils,
);

Future<Email> getEmailContent(Session session, AccountId accountId, EmailId emailId) async {
final processingInvocation = ProcessingInvocation();
Expand Down Expand Up @@ -351,12 +359,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,
Expand Down Expand Up @@ -734,4 +742,43 @@ class EmailAPI with HandleSetErrorMixin {
throw NotFoundEmailRecoveryActionException();
}
}

Future<void> downloadMessageAsEML(
hoangdat marked this conversation as resolved.
Show resolved Hide resolved
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();
}
}
}
17 changes: 17 additions & 0 deletions lib/features/email/data/repository/email_repository_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -237,4 +238,20 @@ class EmailRepositoryImpl extends EmailRepository {
Future<void> printEmail(EmailPrint emailPrint) {
return _printFileDataSource.printEmail(emailPrint);
}

@override
Future<void> downloadMessageAsEML(
AccountId accountId,
String baseDownloadUrl,
AccountRequest accountRequest,
Id blobId,
String subjectEmail
) {
return emailDataSource[DataSourceType.network]!.downloadMessageAsEML(
accountId,
baseDownloadUrl,
accountRequest,
blobId,
subjectEmail);
}
}
60 changes: 60 additions & 0 deletions lib/features/email/data/utils/download_utils.dart
Original file line number Diff line number Diff line change
@@ -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';
}
}
6 changes: 5 additions & 1 deletion lib/features/email/domain/exceptions/email_exceptions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,8 @@ class NotFoundEmailContentException implements Exception {}

class EmptyEmailContentException implements Exception {}

class NotFoundEmailRecoveryActionException implements Exception {}
class NotFoundEmailRecoveryActionException implements Exception {}

class NotFoundByteFileDownloadedException implements Exception {}

class NotFoundEmailBlobIdException implements Exception {}
9 changes: 9 additions & 0 deletions lib/features/email/domain/repository/email_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -115,4 +116,12 @@ abstract class EmailRepository {
Future<EmailRecoveryAction> getRestoredDeletedMessage(EmailRecoveryActionId emailRecoveryActionId);

Future<void> printEmail(EmailPrint emailPrint);

Future<void> downloadMessageAsEML(
AccountId accountId,
String baseDownloadUrl,
AccountRequest accountRequest,
Id blobId,
String subjectEmail
);
}
11 changes: 11 additions & 0 deletions lib/features/email/domain/state/download_message_as_eml_state.dart
Original file line number Diff line number Diff line change
@@ -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);
}
Loading
Loading