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
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
6 changes: 3 additions & 3 deletions lib/features/email/data/network/email_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -351,12 +351,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
4 changes: 3 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,6 @@ class NotFoundEmailContentException implements Exception {}

class EmptyEmailContentException implements Exception {}

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

class NotFoundEmailBlobIdException implements Exception {}
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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<Object?> get props => [taskId, ...super.props];
List<Object?> get props => [attachment, taskId, ...super.props];
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class ViewAttachmentForWebSuccess extends DownloadAttachmentForWebSuccess {

class ViewAttachmentForWebFailure extends DownloadAttachmentForWebFailure {
ViewAttachmentForWebFailure({
required super.attachmentBlobId,
required super.attachment,
super.taskId,
super.exception,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ class DownloadAttachmentForWebInteractor {
} catch (exception) {
yield Left<Failure, Success>(
DownloadAttachmentForWebFailure(
attachmentBlobId: attachment.blobId,
attachment: attachment,
taskId: taskId,
exception: exception
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -28,6 +29,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';
Expand Down Expand Up @@ -810,7 +812,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
} else {
consumeState(Stream.value(
Left(DownloadAttachmentForWebFailure(
attachmentBlobId: attachment.blobId,
attachment: attachment,
exception: NotFoundSessionException()))
));
}
Expand All @@ -831,7 +833,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
} else {
consumeState(Stream.value(
Left(ViewAttachmentForWebFailure(
attachmentBlobId: attachment.blobId,
attachment: attachment,
exception: NotFoundSessionException()))
));
}
Expand Down Expand Up @@ -876,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);
}
}

Expand Down Expand Up @@ -1141,6 +1147,9 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
case EmailActionType.printAll:
_printEmail(context, presentationEmail);
break;
case EmailActionType.downloadMessageAsEML:
_downloadMessageAsEML(presentationEmail);
break;
default:
break;
}
Expand Down Expand Up @@ -1772,4 +1781,14 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
AppLocalizations.of(currentContext!).eventReplyWasSentUnsuccessfully);
}
}

void _downloadMessageAsEML(PresentationEmail presentationEmail) {
final emlAttachment = presentationEmail.createEMLAttachment();
if (emlAttachment.blobId == null) {
consumeState(Stream.value(Left(DownloadAttachmentForWebFailure(exception: NotFoundEmailBlobIdException()))));
return;
}

downloadAttachmentForWeb(emlAttachment);
}
}
2 changes: 2 additions & 0 deletions lib/features/email/presentation/email_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,8 @@ class EmailView extends GetWidget<SingleEmailController> {
EmailActionType.unsubscribe,
if (mailboxContain?.isArchive == false)
EmailActionType.archiveMessage,
if (PlatformInfo.isWeb && PlatformInfo.isCanvasKit)
EmailActionType.downloadMessageAsEML
];

if (position == null) {
Expand Down
1 change: 0 additions & 1 deletion lib/features/email/presentation/utils/email_utils.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
}

Expand Down
1 change: 1 addition & 0 deletions lib/features/thread/data/extensions/email_extension.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ extension EmailExtension on Email {
replyTo: replyTo?.map((emailAddress) => emailAddress.toEmailAddressHiveCache()).toList(),
mailboxIds: mailboxIds?.toMapString(),
headerCalendarEvent: headerCalendarEvent?.toMapString(),
blobId: blobId?.value,
);
}

Expand Down
5 changes: 5 additions & 0 deletions lib/features/thread/data/model/email_cache.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ class EmailCache extends HiveObject with EquatableMixin {
@HiveField(14)
Map<String, String?>? headerCalendarEvent;

@HiveField(15)
final String? blobId;

EmailCache(
this.id,
{
Expand All @@ -71,6 +74,7 @@ class EmailCache extends HiveObject with EquatableMixin {
this.replyTo,
this.mailboxIds,
this.headerCalendarEvent,
this.blobId,
}
);

Expand All @@ -91,5 +95,6 @@ class EmailCache extends HiveObject with EquatableMixin {
hasAttachment,
mailboxIds,
headerCalendarEvent,
blobId,
];
}
4 changes: 4 additions & 0 deletions lib/features/thread/domain/constants/thread_constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -29,6 +30,7 @@ class ThreadConstants {

static final propertiesQuickSearch = Properties({
EmailProperty.id,
EmailProperty.blobId,
EmailProperty.subject,
EmailProperty.from,
EmailProperty.to,
Expand All @@ -53,6 +55,7 @@ class ThreadConstants {

static final propertiesGetDetailedEmail = Properties({
EmailProperty.id,
EmailProperty.blobId,
EmailProperty.subject,
EmailProperty.from,
EmailProperty.to,
Expand All @@ -74,6 +77,7 @@ class ThreadConstants {

static final propertiesCalendarEvent = Properties({
EmailProperty.id,
EmailProperty.blobId,
EmailProperty.subject,
EmailProperty.from,
EmailProperty.to,
Expand Down
14 changes: 13 additions & 1 deletion lib/l10n/intl_messages.arb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"@@last_modified": "2024-04-19T16:31:35.757887",
"@@last_modified": "2024-05-16T11:36:53.008027",
"initializing_data": "Initializing data...",
"@initializing_data": {
"type": "text",
Expand Down Expand Up @@ -3719,5 +3719,17 @@
"type": "text",
"placeholders_order": [],
"placeholders": {}
},
"downloadMessageAsEML": "Download message as EML",
"@downloadMessageAsEML": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
},
"downloadMessageAsEMLFailed": "Download message as EML failed",
"@downloadMessageAsEMLFailed": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
}
}
14 changes: 14 additions & 0 deletions lib/main/localizations/app_localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3879,4 +3879,18 @@ class AppLocalizations {
name: 'youMayAttendThisMeeting',
);
}

String get downloadMessageAsEML {
return Intl.message(
'Download message as EML',
name: 'downloadMessageAsEML',
);
}

String get downloadMessageAsEMLFailed {
return Intl.message(
'Download message as EML failed',
name: 'downloadMessageAsEMLFailed',
);
}
}
4 changes: 2 additions & 2 deletions model/lib/email/attachment.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
3 changes: 2 additions & 1 deletion model/lib/email/email_action_type.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,6 @@ enum EmailActionType {
unsubscribe,
composeFromUnsubscribeMailtoLink,
archiveMessage,
printAll
printAll,
downloadMessageAsEML
}
1 change: 1 addition & 0 deletions model/lib/email/email_property.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
Loading
Loading