TF-3416 Implement preview EML attachment

This commit is contained in:
dab246
2025-01-13 10:02:50 +07:00
committed by Dat H. Pham
parent 7cbdfddb6d
commit d304083b2e
54 changed files with 1489 additions and 148 deletions
+2 -1
View File
@@ -14,9 +14,9 @@ export 'presentation/extensions/compare_string_extension.dart';
export 'presentation/extensions/compare_list_extensions.dart';
export 'presentation/extensions/string_extension.dart';
export 'presentation/extensions/tap_down_details_extension.dart';
export 'domain/extensions/media_type_extension.dart';
export 'presentation/extensions/map_extensions.dart';
export 'presentation/extensions/either_view_state_extension.dart';
export 'presentation/extensions/media_type_extension.dart';
// Exceptions
export 'domain/exceptions/download_file_exception.dart';
@@ -51,6 +51,7 @@ export 'utils/broadcast_channel/broadcast_channel.dart';
export 'utils/list_utils.dart';
export 'utils/mail/domain.dart';
export 'utils/mail/mail_address.dart';
export 'utils/preview_eml_file_utils.dart';
// Views
export 'presentation/views/text/slogan_builder.dart';
+1
View File
@@ -9,4 +9,5 @@ class Constant {
static const imageType = 'image';
static const textVCardMimeType = 'text/x-vcard';
static const textPlainMimeType = 'text/plain';
static const emlMimeType = 'message/rfc822';
}
@@ -1,23 +0,0 @@
import 'package:core/domain/preview/document_uti.dart';
import 'package:core/domain/preview/supported_preview_file_types.dart';
import 'package:http_parser/http_parser.dart';
extension MediaTypeExtension on MediaType {
bool isAndroidSupportedPreview() => SupportedPreviewFileTypes.androidSupportedTypes.contains(mimeType);
bool isIOSSupportedPreview() => SupportedPreviewFileTypes.iOSSupportedTypes.containsKey(mimeType);
bool isImageFile() => SupportedPreviewFileTypes.imageMimeTypes.contains(mimeType);
bool isDocFile() => SupportedPreviewFileTypes.docMimeTypes.contains(mimeType);
bool isPowerPointFile() => SupportedPreviewFileTypes.pptMimeTypes.contains(mimeType);
bool isExcelFile() => SupportedPreviewFileTypes.xlsMimeTypes.contains(mimeType);
bool isZipFile() => SupportedPreviewFileTypes.zipMimeTypes.contains(mimeType);
bool isPdfFile() => SupportedPreviewFileTypes.pdfMimeTypes.contains(mimeType);
DocumentUti getDocumentUti() => DocumentUti(SupportedPreviewFileTypes.iOSSupportedTypes[mimeType]);
}
@@ -14,9 +14,7 @@ class SupportedPreviewFileTypes {
'application/msword',
'application/vnd.ms-works'];
static const pdfMimeTypes = [
'application/pdf',
'application/rtf'];
static const rtfMimeTypes = ['application/rtf'];
static const xlsMimeTypes = [
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
@@ -0,0 +1,50 @@
import 'package:core/data/constants/constant.dart';
import 'package:core/domain/preview/document_uti.dart';
import 'package:core/domain/preview/supported_preview_file_types.dart';
import 'package:core/presentation/resources/image_paths.dart';
import 'package:http_parser/http_parser.dart';
extension MediaTypeExtension on MediaType {
bool isAndroidSupportedPreview() => SupportedPreviewFileTypes.androidSupportedTypes.contains(mimeType);
bool isIOSSupportedPreview() => SupportedPreviewFileTypes.iOSSupportedTypes.containsKey(mimeType);
bool isImageFile() => SupportedPreviewFileTypes.imageMimeTypes.contains(mimeType);
bool isDocFile() => SupportedPreviewFileTypes.docMimeTypes.contains(mimeType);
bool isPowerPointFile() => SupportedPreviewFileTypes.pptMimeTypes.contains(mimeType);
bool isExcelFile() => SupportedPreviewFileTypes.xlsMimeTypes.contains(mimeType);
bool isZipFile() => SupportedPreviewFileTypes.zipMimeTypes.contains(mimeType);
bool isRtfFile() => SupportedPreviewFileTypes.rtfMimeTypes.contains(mimeType);
DocumentUti getDocumentUti() => DocumentUti(SupportedPreviewFileTypes.iOSSupportedTypes[mimeType]);
String getIcon(ImagePaths imagePaths, {String? fileName}) {
if (isPDFFile(fileName: fileName) == true || isRtfFile()) {
return imagePaths.icFilePdf;
} else if (isDocFile()) {
return imagePaths.icFileDocx;
} else if (isExcelFile()) {
return imagePaths.icFileXlsx;
} else if (isPowerPointFile()) {
return imagePaths.icFilePptx;
} else if (isZipFile()) {
return imagePaths.icFileZip;
} else if (isImageFile()) {
return imagePaths.icFilePng;
} else {
return imagePaths.icFileEPup;
}
}
bool isPDFFile({required String? fileName}) =>
mimeType == Constant.pdfMimeType ||
(mimeType == Constant.octetStreamMimeType &&
fileName?.endsWith(Constant.pdfExtension) == true);
bool get isEMLFile => mimeType == Constant.emlMimeType;
}
+151
View File
@@ -1,5 +1,6 @@
class HtmlTemplate {
static const String fontLink = '<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">';
static const String nameClassToolTip = 'tmail-tooltip';
static const String tooltipLinkCss = '''
.$nameClassToolTip .tooltiptext {
@@ -64,4 +65,154 @@ class HtmlTemplate {
}
</style>
''';
static const String previewEMLFileCssStyle = '''
<style>
body {
font-family: Arial, 'Inter', sans-serif;
color: #333;
margin: 0;
padding: 0;
}
.email-container {
display: flex;
flex-direction: column;
gap: 10px;
padding: 16px;
}
.email-subject {
font-weight: bold;
font-size: 16px;
color: #333;
margin-bottom: 10px;
}
.email-header {
display: flex;
align-items: flex-start;
gap: 10px;
}
.placeholder-icon {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
background-color: #f2f2f2;
border-radius: 50%;
color: #d9534f;
font-size: 18px;
font-weight: bold;
flex-shrink: 0;
}
.email-info {
display: flex;
flex-direction: column;
flex-grow: 1;
}
.sender {
font-weight: bold;
color: #333;
}
.sender-email {
font-size: 12px;
color: #666;
}
.recipients {
font-size: 12px;
color: #666;
margin-top: 5px;
word-wrap: break-word;
line-height: 1.5;
}
.email-meta {
display: flex;
align-items: center;
gap: 5px;
}
.attachment-icon {
width: 16px;
height: 16px;
}
.email-date {
font-size: 12px;
color: #666;
white-space: nowrap;
}
.email-body {
font-size: 14px;
color: #333;
margin-top: 10px;
margin-left: 40px;
}
.attachments {
margin-top: 15px;
padding-top: 10px;
border-top: 1px solid #ddd;
margin-left: 40px;
}
.attachment-title {
font-weight: bold;
color: #333;
margin-bottom: 10px;
font-size: 14px;
}
.attachment-item {
display: flex;
align-items: flex-start;
gap: 10px;
font-size: 12px;
color: #666;
margin-bottom: 10px;
cursor: pointer;
padding: 5px;
transition: background-color 0.3s, box-shadow 0.3s;
}
.attachment-item:hover {
background-color: #f5f5f5;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.attachment-item .icon {
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
background-color: #f2f2f2;
border-radius: 50%;
color: #007bff;
}
.attachment-item .file-details {
display: flex;
flex-direction: column;
align-items: flex-start;
}
.attachment-item .file-name {
font-weight: bold;
}
.attachment-item .file-size {
font-size: 12px;
color: #666;
}
</style>
''';
}
+24
View File
@@ -421,4 +421,28 @@ class HtmlUtils {
const fileSizeSpan = document.getElementById('file-size');
fileSizeSpan.textContent = formatFileSize(bytesJs.length);''';
}
static void openNewWindowByUrl(
String url,
{
int width = 800,
int height = 600,
}
) async {
try {
final screenWidth = html.window.screen?.width ?? width;
final screenHeight = html.window.screen?.height ?? height;
final left = (screenWidth - width) ~/ 2;
final top = (screenHeight - height) ~/ 2;
final options = 'width=$width,height=$height,top=$top,left=$left';
html.window.open(url, '_blank', options);
html.Url.revokeObjectUrl(url);
} catch (e) {
logError('AppUtils::openNewWindowByUrl:Exception = $e');
}
}
}
+179
View File
@@ -0,0 +1,179 @@
import 'package:core/data/model/print_attachment.dart';
import 'package:core/utils/app_logger.dart';
import 'package:core/utils/html/html_template.dart';
import 'package:core/utils/html/html_utils.dart';
import 'package:html/dom.dart';
import 'package:html/parser.dart';
class PreviewEmlFileUtils {
Element? _createEmailElement({
required String subjectPrefix,
required String fromPrefix,
required String toPrefix,
required String ccPrefix,
required String bccPrefix,
required String replyToPrefix,
required String titleAttachment,
String? subject,
String? senderName,
String? senderEmailAddress,
String? toAddress,
String? ccAddress,
String? bccAddress,
String? replyToAddress,
String? dateTime,
String? attachmentIcon,
String? emailContent,
List<PrintAttachment>? listAttachment,
}) {
try {
return Element.html('''
<div class="email-container">
<!-- Email Subject -->
<div class="email-subject">$subjectPrefix: $subject</div>
<!-- Email Header -->
<div class="email-header">
<!-- Placeholder icon -->
<div class="placeholder-icon">?</div>
<!-- Email information -->
<div class="email-info">
<div class="sender">
${senderName ?? ''} <span class="sender-email">&lt;${senderEmailAddress ?? ''}&gt;</span>
</div>
${replyToAddress?.isNotEmpty == true ? _createRecipientHtmlTag(replyToPrefix, replyToAddress!) : ''}
${toAddress?.isNotEmpty == true ? _createRecipientHtmlTag(toPrefix, toAddress!) : ''}
${ccAddress?.isNotEmpty == true ? _createRecipientHtmlTag(ccPrefix, ccAddress!) : ''}
${bccAddress?.isNotEmpty == true ? _createRecipientHtmlTag(bccPrefix, bccAddress!) : ''}
</div>
<!-- Email metadata -->
<div class="email-meta">
${attachmentIcon?.isNotEmpty == true ? '<img width="16" height="16" src="${HtmlUtils.generateSVGImageData(attachmentIcon!)}" alt="Attachment Icon" class="attachment-icon">' : ''}
${dateTime?.isNotEmpty == true ? '<div class="email-date">$dateTime</div>' : ''}
</div>
</div>
<!-- Email Body -->
<div class="email-body">
<p>$emailContent</p>
</div>
${listAttachment?.isNotEmpty == true ? _createAttachmentsElement(listAttachment: listAttachment ?? [], titleAttachment: titleAttachment) : ''}
''');
} catch (e) {
logError('PreviewEmlFileUtils::_createEmailElement: Exception = $e');
return null;
}
}
String _createRecipientHtmlTag(String prefix, String emailAddress) {
try {
return '''
<div class="recipients">
$prefix: $emailAddress
</div>
''';
} catch (e) {
logError('PreviewEmlFileUtils::_createRecipientHtmlTag: Exception = $e');
return '';
}
}
String _createAttachmentHtmlTag(PrintAttachment printAttachment) {
return '''
<div class="attachment-item">
<div class="icon">
<img width="16" height="16" src="${HtmlUtils.generateSVGImageData(printAttachment.iconBase64Data)}" alt="attachment-icon"/>
</div>
<div class="file-details">
<div class="file-name">${printAttachment.name}</div>
<div class="file-size">${printAttachment.size}</div>
</div>
</div>
''';
}
String _createAttachmentsElement({
required String titleAttachment,
required List<PrintAttachment> listAttachment
}) {
try {
return '''
<div class="attachments">
<!-- Attachment Title -->
<div class="attachment-title">${listAttachment.length} $titleAttachment</div>
<!-- Attachment Items -->
${listAttachment.map((attachment) => _createAttachmentHtmlTag(attachment)).toList().join('\n')}
</div>
''';
} catch (e) {
logError('PreviewEmlFileUtils::_createAttachmentsElement: Exception = $e');
return '';
}
}
String generatePreviewEml({
required String appName,
required String userName,
required String subjectPrefix,
required String subject,
required String emailContent,
required String senderName,
required String senderEmailAddress,
required String dateTime,
required String fromPrefix,
required String toPrefix,
required String ccPrefix,
required String bccPrefix,
required String replyToPrefix,
required String titleAttachment,
required String attachmentIcon,
String? toAddress,
String? ccAddress,
String? bccAddress,
String? replyToAddress,
List<PrintAttachment>? listAttachment,
}) {
Document document = parse(HtmlUtils.createTemplateHtmlDocument(title: '$subject - $userName'));
Element? emailElement = _createEmailElement(
subjectPrefix: subjectPrefix,
fromPrefix: fromPrefix,
toPrefix: toPrefix,
ccPrefix: ccPrefix,
bccPrefix: bccPrefix,
replyToPrefix: replyToPrefix,
subject: subject,
toAddress: toAddress,
ccAddress: ccAddress,
bccAddress: bccAddress,
replyToAddress: replyToAddress,
senderName: senderName,
senderEmailAddress: senderEmailAddress,
dateTime: dateTime,
emailContent: emailContent,
attachmentIcon: attachmentIcon,
titleAttachment: titleAttachment,
listAttachment: listAttachment,
);
Element linkFontElement = Element.html(HtmlTemplate.fontLink);
document.head?.append(linkFontElement);
Element styleElement = Element.html(HtmlTemplate.previewEMLFileCssStyle);
document.head?.append(styleElement);
if (emailElement != null) {
document.body?.append(emailElement);
}
final htmlDocument = document.outerHtml;
return htmlDocument;
}
}
@@ -0,0 +1 @@
class NotFoundDataWithThisKeyException implements Exception {}
@@ -0,0 +1,27 @@
import 'package:collection/collection.dart';
import 'package:tmail_ui_user/features/caching/exceptions/local_storage_exception.dart';
import 'package:universal_html/html.dart' as html;
class LocalStorageManager {
void save(String key, String value) {
html.window.localStorage.addAll({key: value});
}
String get(String key) {
final entry = html.window.localStorage
.entries
.firstWhereOrNull((entry) => entry.key == key);
if (entry != null) {
return entry.value;
} else {
throw NotFoundDataWithThisKeyException();
}
}
void remove(String key) {
html.window.localStorage.remove(key);
}
}
@@ -2,6 +2,7 @@ import 'package:core/core.dart';
import 'package:core/utils/application_manager.dart';
import 'package:get/get.dart';
import 'package:tmail_ui_user/features/base/base_bindings.dart';
import 'package:tmail_ui_user/features/caching/utils/local_storage_manager.dart';
import 'package:tmail_ui_user/features/composer/data/datasource/composer_datasource.dart';
import 'package:tmail_ui_user/features/composer/data/datasource/contact_datasource.dart';
import 'package:tmail_ui_user/features/composer/data/datasource_impl/composer_datasource_impl.dart';
@@ -24,6 +25,7 @@ import 'package:tmail_ui_user/features/email/data/datasource/html_datasource.dar
import 'package:tmail_ui_user/features/email/data/datasource/print_file_datasource.dart';
import 'package:tmail_ui_user/features/email/data/datasource_impl/email_datasource_impl.dart';
import 'package:tmail_ui_user/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart';
import 'package:tmail_ui_user/features/email/data/datasource_impl/email_local_storage_datasource_impl.dart';
import 'package:tmail_ui_user/features/email/data/datasource_impl/html_datasource_impl.dart';
import 'package:tmail_ui_user/features/email/data/datasource_impl/print_file_datasource_impl.dart';
import 'package:tmail_ui_user/features/email/data/local/html_analyzer.dart';
@@ -131,6 +133,9 @@ class ComposerBindings extends BaseBindings {
Get.find<SendingEmailCacheManager>(),
Get.find<FileUtils>(),
Get.find<CacheExceptionThrower>()));
Get.lazyPut(() => EmailLocalStorageDataSourceImpl(
Get.find<LocalStorageManager>(),
Get.find<CacheExceptionThrower>()));
Get.lazyPut(() => RemoteServerSettingsDataSourceImpl(
Get.find<ServerSettingsAPI>(),
Get.find<RemoteExceptionThrower>()));
@@ -170,7 +175,8 @@ class ComposerBindings extends BaseBindings {
Get.lazyPut(() => EmailRepositoryImpl(
{
DataSourceType.network: Get.find<EmailDataSource>(),
DataSourceType.hiveCache: Get.find<EmailHiveCacheDataSourceImpl>()
DataSourceType.hiveCache: Get.find<EmailHiveCacheDataSourceImpl>(),
DataSourceType.local: Get.find<EmailLocalStorageDataSourceImpl>(),
},
Get.find<HtmlDataSource>(),
Get.find<StateDataSource>(),
@@ -24,6 +24,7 @@ import 'package:tmail_ui_user/features/composer/domain/model/email_request.dart'
import 'package:tmail_ui_user/features/email/domain/model/detailed_email.dart';
import 'package:tmail_ui_user/features/email/domain/model/event_action.dart';
import 'package:tmail_ui_user/features/email/domain/model/move_to_mailbox_request.dart';
import 'package:tmail_ui_user/features/email/domain/model/preview_email_eml_request.dart';
import 'package:tmail_ui_user/features/email/domain/model/restore_deleted_message_request.dart';
import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart';
import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email.dart';
@@ -187,4 +188,10 @@ abstract class EmailDataSource {
List<EmailId> emailIds);
Future<List<Email>> parseEmailByBlobIds(AccountId accountId, Set<Id> blobIds);
Future<String> generatePreviewEmailEMLContent(PreviewEmailEMLRequest previewEmailEMLRequest);
Future<void> sharePreviewEmailEMLContent(String keyStored, String previewEMLContent);
Future<String> getPreviewEmailEMLContentShared(String keyStored);
}
@@ -1,13 +1,23 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:core/data/model/print_attachment.dart';
import 'package:core/data/network/download/downloaded_response.dart';
import 'package:core/domain/extensions/datetime_extension.dart';
import 'package:core/presentation/extensions/html_extension.dart';
import 'package:core/presentation/resources/image_paths.dart';
import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
import 'package:core/presentation/utils/html_transformer/transform_configuration.dart';
import 'package:core/utils/app_logger.dart';
import 'package:core/utils/file_utils.dart';
import 'package:core/utils/platform_info.dart';
import 'package:core/utils/preview_eml_file_utils.dart';
import 'package:dartz/dartz.dart';
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:filesize/filesize.dart';
import 'package:get/get.dart';
import 'package:jmap_dart_client/jmap/account_id.dart';
import 'package:jmap_dart_client/jmap/core/error/set_error.dart';
@@ -19,11 +29,14 @@ import 'package:jmap_dart_client/jmap/mail/email/email.dart';
import 'package:model/model.dart';
import 'package:tmail_ui_user/features/composer/domain/model/email_request.dart';
import 'package:tmail_ui_user/features/email/data/datasource/email_datasource.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/domain/model/detailed_email.dart';
import 'package:tmail_ui_user/features/email/domain/model/event_action.dart';
import 'package:tmail_ui_user/features/email/domain/model/move_to_mailbox_request.dart';
import 'package:tmail_ui_user/features/email/domain/model/preview_email_eml_request.dart';
import 'package:tmail_ui_user/features/email/domain/model/restore_deleted_message_request.dart';
import 'package:tmail_ui_user/features/email/presentation/extensions/attachment_extension.dart';
import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart';
import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email.dart';
import 'package:tmail_ui_user/main/exceptions/exception_thrower.dart';
@@ -34,6 +47,10 @@ class EmailDataSourceImpl extends EmailDataSource {
final EmailAPI emailAPI;
final ExceptionThrower _exceptionThrower;
final ExceptionThrower _sendEmailExceptionThrower = Get.find<SendEmailExceptionThrower>();
final PreviewEmlFileUtils _previewEmailUtils = Get.find<PreviewEmlFileUtils>();
final HtmlAnalyzer _htmlAnalyzer = Get.find<HtmlAnalyzer>();
final FileUtils _fileUtils = Get.find<FileUtils>();
final ImagePaths _imagePaths = Get.find<ImagePaths>();
EmailDataSourceImpl(this.emailAPI, this._exceptionThrower);
@@ -385,4 +402,125 @@ class EmailDataSourceImpl extends EmailDataSource {
return await emailAPI.parseEmailByBlobIds(accountId, blobIds);
}).catchError(_exceptionThrower.throwException);
}
@override
Future<String> generatePreviewEmailEMLContent(PreviewEmailEMLRequest previewEmailEMLRequest) {
return Future.sync(() async {
final email = previewEmailEMLRequest.email;
final appLocalizations = previewEmailEMLRequest.appLocalizations;
final locale = previewEmailEMLRequest.locale.toLanguageTag();
final listAttachments = email
.allAttachments
.getListAttachmentsDisplayedOutside(email.htmlBodyAttachments);
final listInlineImages = email
.allAttachments
.listAttachmentsDisplayedInContent;
final mapCidImageDownloadUrl = listInlineImages.toMapCidImageDownloadUrl(
accountId: previewEmailEMLRequest.accountId,
downloadUrl: previewEmailEMLRequest.baseDownloadUrl,
);
final emailContentEscaped = await _transformEmailContent(
email.emailContentList,
mapCidImageDownloadUrl,
);
final sender = email.from?.isNotEmpty == true
? email.from!.first
: null;
final receiveTime = email.getReceivedAt(
newLocale: locale,
pattern: email.receivedAt?.value.toLocal().toPatternForPrinting(locale)
);
final sentTime = email.getSentAt(
newLocale: locale,
pattern: email.sentAt?.value.toLocal().toPatternForPrinting(locale)
);
final List<PrintAttachment> listPrintAttachment = [];
if (listAttachments.isNotEmpty) {
await Future.forEach<Attachment>(listAttachments, (attachment) async {
final iconBase64Data = await _fileUtils.convertImageAssetToBase64(
attachment.getIcon(_imagePaths));
final printAttachment = PrintAttachment(
iconBase64Data: iconBase64Data,
name: attachment.name.escapeLtGtHtmlString(),
size: filesize(attachment.size?.value),
);
listPrintAttachment.add(printAttachment);
});
}
final attachmentIconBase64Data = email.hasAttachment == true
? await _fileUtils.convertImageAssetToBase64(_imagePaths.icAttachment)
: '';
final previewEmlHtmlDocument = _previewEmailUtils.generatePreviewEml(
appName: appLocalizations.app_name,
userName: previewEmailEMLRequest.userName.value,
subjectPrefix: appLocalizations.subject,
subject: previewEmailEMLRequest.email.subject?.escapeLtGtHtmlString() ?? '',
emailContent: emailContentEscaped,
senderName: sender?.name.escapeLtGtHtmlString() ?? '',
senderEmailAddress: sender?.email ?? '',
dateTime: receiveTime.isNotEmpty ? receiveTime : sentTime,
fromPrefix: appLocalizations.from_email_address_prefix,
toPrefix: appLocalizations.to_email_address_prefix,
ccPrefix: appLocalizations.cc_email_address_prefix,
bccPrefix: appLocalizations.bcc_email_address_prefix,
replyToPrefix: appLocalizations.replyToEmailAddressPrefix,
titleAttachment: appLocalizations.attachments.toLowerCase(),
attachmentIcon: attachmentIconBase64Data,
toAddress: email.to?.listEmailAddressToString(isFullEmailAddress: true),
ccAddress: email.cc?.listEmailAddressToString(isFullEmailAddress: true),
bccAddress: email.bcc?.listEmailAddressToString(isFullEmailAddress: true),
replyToAddress: email.replyTo?.listEmailAddressToString(isFullEmailAddress: true),
listAttachment: listPrintAttachment,
);
return previewEmlHtmlDocument;
}).catchError(_exceptionThrower.throwException);
}
Future<String> _transformEmailContent(
List<EmailContent> emailContents,
Map<String, String> mapCidImageDownloadUrl,
) async {
try {
final transformConfiguration = PlatformInfo.isWeb
? TransformConfiguration.forPreviewEmailOnWeb()
: TransformConfiguration.forPreviewEmail();
final listEmailContent = await Future.wait(emailContents
.map((emailContent) async => await _htmlAnalyzer.transformEmailContent(
emailContent,
mapCidImageDownloadUrl,
transformConfiguration,
))
.toList());
return listEmailContent.asHtmlString;
} catch (e) {
logError('EmailDataSourceImpl::_transformEmailContent:Exception = $e');
return '';
}
}
@override
Future<void> sharePreviewEmailEMLContent(String keyStored, String previewEMLContent) {
throw UnimplementedError();
}
@override
Future<String> getPreviewEmailEMLContentShared(String keyStored) {
throw UnimplementedError();
}
}
@@ -34,6 +34,7 @@ import 'package:tmail_ui_user/features/email/domain/extensions/detailed_email_hi
import 'package:tmail_ui_user/features/email/domain/model/detailed_email.dart';
import 'package:tmail_ui_user/features/email/domain/model/event_action.dart';
import 'package:tmail_ui_user/features/email/domain/model/move_to_mailbox_request.dart';
import 'package:tmail_ui_user/features/email/domain/model/preview_email_eml_request.dart';
import 'package:tmail_ui_user/features/email/domain/model/restore_deleted_message_request.dart';
import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart';
import 'package:tmail_ui_user/features/offline_mode/extensions/list_sending_email_hive_cache_extension.dart';
@@ -531,4 +532,19 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource {
Future<List<Email>> parseEmailByBlobIds(AccountId accountId, Set<Id> blobIds) {
throw UnimplementedError();
}
@override
Future<String> generatePreviewEmailEMLContent(PreviewEmailEMLRequest previewEmailEMLRequest) {
throw UnimplementedError();
}
@override
Future<void> sharePreviewEmailEMLContent(String keyStored, String previewEMLContent) {
throw UnimplementedError();
}
@override
Future<String> getPreviewEmailEMLContentShared(String keyStored) {
throw UnimplementedError();
}
}
@@ -0,0 +1,224 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:core/data/network/download/downloaded_response.dart';
import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
import 'package:dartz/dartz.dart';
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/error/set_error.dart';
import 'package:jmap_dart_client/jmap/core/id.dart';
import 'package:jmap_dart_client/jmap/core/properties/properties.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';
import 'package:model/account/account_request.dart';
import 'package:model/download/download_task_id.dart';
import 'package:model/email/attachment.dart';
import 'package:model/email/mark_star_action.dart';
import 'package:model/email/read_actions.dart';
import 'package:tmail_ui_user/features/caching/utils/local_storage_manager.dart';
import 'package:tmail_ui_user/features/composer/domain/model/email_request.dart';
import 'package:tmail_ui_user/features/email/data/datasource/email_datasource.dart';
import 'package:tmail_ui_user/features/email/domain/model/detailed_email.dart';
import 'package:tmail_ui_user/features/email/domain/model/event_action.dart';
import 'package:tmail_ui_user/features/email/domain/model/move_to_mailbox_request.dart';
import 'package:tmail_ui_user/features/email/domain/model/preview_email_eml_request.dart';
import 'package:tmail_ui_user/features/email/domain/model/restore_deleted_message_request.dart';
import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart';
import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email.dart';
import 'package:tmail_ui_user/main/exceptions/exception_thrower.dart';
class EmailLocalStorageDataSourceImpl extends EmailDataSource {
final LocalStorageManager _localStorageManager;
final ExceptionThrower _exceptionThrower;
EmailLocalStorageDataSourceImpl(
this._localStorageManager,
this._exceptionThrower,
);
@override
Future<bool> deleteEmailPermanently(Session session, AccountId accountId, EmailId emailId, {CancelToken? cancelToken}) {
throw UnimplementedError();
}
@override
Future<({List<EmailId> emailIdsSuccess, Map<Id, SetError> mapErrors})> deleteMultipleEmailsPermanently(Session session, AccountId accountId, List<EmailId> emailIds) {
throw UnimplementedError();
}
@override
Future<void> deleteMultipleSendingEmail(AccountId accountId, UserName userName, List<String> sendingIds) {
throw UnimplementedError();
}
@override
Future<void> deleteSendingEmail(AccountId accountId, UserName userName, String sendingId) {
throw UnimplementedError();
}
@override
Future<Uint8List> downloadAttachmentForWeb(DownloadTaskId taskId, Attachment attachment, AccountId accountId, String baseDownloadUrl, AccountRequest accountRequest, StreamController<Either<Failure, Success>> onReceiveController, {CancelToken? cancelToken}) {
throw UnimplementedError();
}
@override
Future<List<DownloadTaskId>> downloadAttachments(List<Attachment> attachments, AccountId accountId, String baseDownloadUrl, AccountRequest accountRequest) {
throw UnimplementedError();
}
@override
Future<DownloadedResponse> exportAttachment(Attachment attachment, AccountId accountId, String baseDownloadUrl, AccountRequest accountRequest, CancelToken cancelToken) {
throw UnimplementedError();
}
@override
Future<String> generatePreviewEmailEMLContent(PreviewEmailEMLRequest previewEmailEMLRequest) {
throw UnimplementedError();
}
@override
Future<List<SendingEmail>> getAllSendingEmails(AccountId accountId, UserName userName) {
throw UnimplementedError();
}
@override
Future<Email> getDetailedEmailById(Session session, AccountId accountId, EmailId emailId) {
throw UnimplementedError();
}
@override
Future<Email> getEmailContent(Session session, AccountId accountId, EmailId emailId, {Properties? additionalProperties}) {
throw UnimplementedError();
}
@override
Future<EmailRecoveryAction> getRestoredDeletedMessage(EmailRecoveryActionId emailRecoveryActionId) {
throw UnimplementedError();
}
@override
Future<Email> getStoredEmail(Session session, AccountId accountId, EmailId emailId) {
throw UnimplementedError();
}
@override
Future<DetailedEmail> getStoredNewEmail(Session session, AccountId accountId, EmailId emailId) {
throw UnimplementedError();
}
@override
Future<DetailedEmail> getStoredOpenedEmail(Session session, AccountId accountId, EmailId emailId) {
throw UnimplementedError();
}
@override
Future<SendingEmail> getStoredSendingEmail(AccountId accountId, UserName userName, String sendingId) {
throw UnimplementedError();
}
@override
Future<({List<EmailId> emailIdsSuccess, Map<Id, SetError> mapErrors})> markAsRead(Session session, AccountId accountId, List<EmailId> emailIds, ReadActions readActions) {
throw UnimplementedError();
}
@override
Future<({List<EmailId> emailIdsSuccess, Map<Id, SetError> mapErrors})> markAsStar(Session session, AccountId accountId, List<EmailId> emailIds, MarkStarAction markStarAction) {
throw UnimplementedError();
}
@override
Future<({List<EmailId> emailIdsSuccess, Map<Id, SetError> mapErrors})> moveToMailbox(Session session, AccountId accountId, MoveToMailboxRequest moveRequest) {
throw UnimplementedError();
}
@override
Future<List<Email>> parseEmailByBlobIds(AccountId accountId, Set<Id> blobIds) {
throw UnimplementedError();
}
@override
Future<bool> removeEmailDrafts(Session session, AccountId accountId, EmailId emailId, {CancelToken? cancelToken}) {
throw UnimplementedError();
}
@override
Future<EmailRecoveryAction> restoreDeletedMessage(RestoredDeletedMessageRequest restoredDeletedMessageRequest) {
throw UnimplementedError();
}
@override
Future<Email> saveEmailAsDrafts(Session session, AccountId accountId, Email email, {CancelToken? cancelToken}) {
throw UnimplementedError();
}
@override
Future<void> sendEmail(Session session, AccountId accountId, EmailRequest emailRequest, {CreateNewMailboxRequest? mailboxRequest, CancelToken? cancelToken}) {
throw UnimplementedError();
}
@override
Future<void> sharePreviewEmailEMLContent(String keyStored, String previewEMLContent) {
return Future.sync(() async {
return _localStorageManager.save(keyStored, previewEMLContent);
}).catchError(_exceptionThrower.throwException);
}
@override
Future<void> storeDetailedNewEmail(Session session, AccountId accountId, DetailedEmail detailedEmail) {
throw UnimplementedError();
}
@override
Future<void> storeEmail(Session session, AccountId accountId, Email email) {
throw UnimplementedError();
}
@override
Future<void> storeEventAttendanceStatus(Session session, AccountId accountId, EmailId emailId, EventActionType eventActionType) {
throw UnimplementedError();
}
@override
Future<void> storeOpenedEmail(Session session, AccountId accountId, DetailedEmail detailedEmail) {
throw UnimplementedError();
}
@override
Future<SendingEmail> storeSendingEmail(AccountId accountId, UserName userName, SendingEmail sendingEmail) {
throw UnimplementedError();
}
@override
Future<void> unsubscribeMail(Session session, AccountId accountId, EmailId emailId) {
throw UnimplementedError();
}
@override
Future<Email> updateEmailDrafts(Session session, AccountId accountId, Email newEmail, EmailId oldEmailId, {CancelToken? cancelToken}) {
throw UnimplementedError();
}
@override
Future<List<SendingEmail>> updateMultipleSendingEmail(AccountId accountId, UserName userName, List<SendingEmail> newSendingEmails) {
throw UnimplementedError();
}
@override
Future<SendingEmail> updateSendingEmail(AccountId accountId, UserName userName, SendingEmail newSendingEmail) {
throw UnimplementedError();
}
@override
Future<String> getPreviewEmailEMLContentShared(String keyStored) {
return Future.sync(() async {
return _localStorageManager.get(keyStored);
}).catchError(_exceptionThrower.throwException);
}
}
@@ -902,7 +902,9 @@ class EmailAPI with HandleSetErrorMixin {
Future<List<Email>> parseEmailByBlobIds(AccountId accountId, Set<Id> blobIds) async {
final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation());
final parseEmailMethod = ParseEmailMethod(accountId, blobIds);
final parseEmailMethod = ParseEmailMethod(accountId, blobIds)
..addProperties(ThreadConstants.propertiesParseEmailByBlobId)
..addFetchHTMLBodyValues(true);
final parseEmailInvocation = requestBuilder.invocation(parseEmailMethod);
final response = await (requestBuilder
@@ -32,6 +32,7 @@ import 'package:tmail_ui_user/features/email/domain/model/detailed_email.dart';
import 'package:tmail_ui_user/features/email/domain/model/email_print.dart';
import 'package:tmail_ui_user/features/email/domain/model/event_action.dart';
import 'package:tmail_ui_user/features/email/domain/model/move_to_mailbox_request.dart';
import 'package:tmail_ui_user/features/email/domain/model/preview_email_eml_request.dart';
import 'package:tmail_ui_user/features/email/domain/model/restore_deleted_message_request.dart';
import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart';
import 'package:tmail_ui_user/features/mailbox/data/datasource/state_datasource.dart';
@@ -486,6 +487,21 @@ class EmailRepositoryImpl extends EmailRepository {
@override
Future<List<Email>> parseEmailByBlobIds(AccountId accountId, Set<Id> blobIds) {
throw UnimplementedError();
return emailDataSource[DataSourceType.network]!.parseEmailByBlobIds(accountId, blobIds);
}
@override
Future<String> generatePreviewEmailEMLContent(PreviewEmailEMLRequest previewEmailEMLRequest) {
return emailDataSource[DataSourceType.network]!.generatePreviewEmailEMLContent(previewEmailEMLRequest);
}
@override
Future<void> sharePreviewEmailEMLContent(String keyStored, String previewEMLContent) {
return emailDataSource[DataSourceType.local]!.sharePreviewEmailEMLContent(keyStored, previewEMLContent);
}
@override
Future<String> getPreviewEmailEMLContentShared(String keyStored) {
return emailDataSource[DataSourceType.local]!.getPreviewEmailEMLContentShared(keyStored);
}
}
@@ -0,0 +1,40 @@
import 'package:equatable/equatable.dart';
import 'package:flutter/material.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:jmap_dart_client/jmap/mail/email/email.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
class PreviewEmailEMLRequest with EquatableMixin {
final AccountId accountId;
final UserName userName;
final Id blobId;
final Email email;
final Locale locale;
final AppLocalizations appLocalizations;
final String baseDownloadUrl;
PreviewEmailEMLRequest({
required this.accountId,
required this.userName,
required this.blobId,
required this.email,
required this.locale,
required this.appLocalizations,
required this.baseDownloadUrl,
});
String get keyStored => blobId.value;
@override
List<Object?> get props => [
accountId,
userName,
blobId,
email,
locale,
appLocalizations,
baseDownloadUrl,
];
}
@@ -27,6 +27,7 @@ import 'package:tmail_ui_user/features/email/domain/model/detailed_email.dart';
import 'package:tmail_ui_user/features/email/domain/model/email_print.dart';
import 'package:tmail_ui_user/features/email/domain/model/event_action.dart';
import 'package:tmail_ui_user/features/email/domain/model/move_to_mailbox_request.dart';
import 'package:tmail_ui_user/features/email/domain/model/preview_email_eml_request.dart';
import 'package:tmail_ui_user/features/email/domain/model/restore_deleted_message_request.dart';
import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart';
@@ -181,4 +182,10 @@ abstract class EmailRepository {
EventActionType eventActionType);
Future<List<Email>> parseEmailByBlobIds(AccountId accountId, Set<Id> blobIds);
Future<String> generatePreviewEmailEMLContent(PreviewEmailEMLRequest previewEmailEMLRequest);
Future<void> sharePreviewEmailEMLContent(String keyStored, String previewEMLContent);
Future<String> getPreviewEmailEMLContentShared(String keyStored);
}
@@ -0,0 +1,19 @@
import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
class GettingPreviewEmailEMLContentShared extends LoadingState {}
class GetPreviewEmailEMLContentSharedSuccess extends UIState {
final String previewEMLContent;
GetPreviewEmailEMLContentSharedSuccess(this.previewEMLContent);
@override
List<Object?> get props => [previewEMLContent];
}
class GetPreviewEmailEMLContentSharedFailure extends FeatureFailure {
GetPreviewEmailEMLContentSharedFailure(dynamic exception) : super(exception: exception);
}
@@ -1,5 +1,6 @@
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:jmap_dart_client/jmap/mail/email/email.dart';
class ParsingEmailByBlobId extends LoadingState {}
@@ -7,11 +8,12 @@ class ParsingEmailByBlobId extends LoadingState {}
class ParseEmailByBlobIdSuccess extends UIState {
final Email email;
final Id blobId;
ParseEmailByBlobIdSuccess(this.email);
ParseEmailByBlobIdSuccess(this.email, this.blobId);
@override
List<Object> get props => [email];
List<Object> get props => [email, blobId];
}
class ParseEmailByBlobIdFailure extends FeatureFailure {
@@ -0,0 +1,19 @@
import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
class PreviewingEmailFromEmlFile extends LoadingState {}
class PreviewEmailFromEmlFileSuccess extends UIState {
final String storeKey;
PreviewEmailFromEmlFileSuccess(this.storeKey);
@override
List<Object?> get props => [storeKey];
}
class PreviewEmailFromEmlFileFailure extends FeatureFailure {
PreviewEmailFromEmlFileFailure(dynamic exception) : super(exception: exception);
}
@@ -0,0 +1,24 @@
import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
import 'package:dartz/dartz.dart';
import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart';
import 'package:tmail_ui_user/features/email/domain/state/get_preview_email_content_shared_state.dart';
class GetPreviewEmailEMLContentSharedInteractor {
final EmailRepository _emailRepository;
const GetPreviewEmailEMLContentSharedInteractor(this._emailRepository);
Stream<Either<Failure, Success>> execute(String keyStored) async* {
try {
yield Right<Failure, Success>(GettingPreviewEmailEMLContentShared());
final previewEMLContent = await _emailRepository
.getPreviewEmailEMLContentShared(keyStored);
yield Right<Failure, Success>(GetPreviewEmailEMLContentSharedSuccess(previewEMLContent));
} catch (e) {
yield Left<Failure, Success>(GetPreviewEmailEMLContentSharedFailure(e));
}
}
}
@@ -20,7 +20,10 @@ class ParseEmailByBlobIdInteractor {
{blobId},
);
yield Right<Failure, Success>(ParseEmailByBlobIdSuccess(emailParsed.first));
yield Right<Failure, Success>(ParseEmailByBlobIdSuccess(
emailParsed.first,
blobId,
));
} catch (e) {
yield Left<Failure, Success>(ParseEmailByBlobIdFailure(e));
}
@@ -0,0 +1,31 @@
import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
import 'package:dartz/dartz.dart';
import 'package:tmail_ui_user/features/email/domain/model/preview_email_eml_request.dart';
import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart';
import 'package:tmail_ui_user/features/email/domain/state/preview_email_from_eml_file_state.dart';
class PreviewEmailFromEmlFileInteractor {
final EmailRepository _emailRepository;
const PreviewEmailFromEmlFileInteractor(this._emailRepository);
Stream<Either<Failure, Success>> execute(
PreviewEmailEMLRequest previewEmailEMLRequest,
) async* {
try {
yield Right<Failure, Success>(PreviewingEmailFromEmlFile());
final previewEMLContent = await _emailRepository
.generatePreviewEmailEMLContent(previewEmailEMLRequest);
final keyStored = previewEmailEMLRequest.keyStored;
await _emailRepository.sharePreviewEmailEMLContent(keyStored, previewEMLContent);
yield Right<Failure, Success>(PreviewEmailFromEmlFileSuccess(keyStored));
} catch (e) {
yield Left<Failure, Success>(PreviewEmailFromEmlFileFailure(e));
}
}
}
@@ -1,11 +1,13 @@
import 'package:core/core.dart';
import 'package:get/get.dart';
import 'package:tmail_ui_user/features/base/base_bindings.dart';
import 'package:tmail_ui_user/features/caching/utils/local_storage_manager.dart';
import 'package:tmail_ui_user/features/email/data/datasource/email_datasource.dart';
import 'package:tmail_ui_user/features/email/data/datasource/html_datasource.dart';
import 'package:tmail_ui_user/features/email/data/datasource/print_file_datasource.dart';
import 'package:tmail_ui_user/features/email/data/datasource_impl/email_datasource_impl.dart';
import 'package:tmail_ui_user/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart';
import 'package:tmail_ui_user/features/email/data/datasource_impl/email_local_storage_datasource_impl.dart';
import 'package:tmail_ui_user/features/email/data/datasource_impl/html_datasource_impl.dart';
import 'package:tmail_ui_user/features/email/data/datasource_impl/print_file_datasource_impl.dart';
import 'package:tmail_ui_user/features/email/data/local/html_analyzer.dart';
@@ -20,6 +22,8 @@ import 'package:tmail_ui_user/features/email/domain/usecases/get_stored_email_st
import 'package:tmail_ui_user/features/email/domain/usecases/mark_as_email_read_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/mark_as_star_email_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/move_to_mailbox_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/parse_email_by_blob_id_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/preview_email_from_eml_file_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/print_email_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/store_event_attendance_status_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/store_opened_email_interactor.dart';
@@ -70,6 +74,8 @@ class EmailBindings extends BaseBindings {
Get.find<StoreOpenedEmailInteractor>(),
Get.find<PrintEmailInteractor>(),
Get.find<StoreEventAttendanceStatusInteractor>(),
Get.find<ParseEmailByBlobIdInteractor>(),
Get.find<PreviewEmailFromEmlFileInteractor>(),
));
}
@@ -118,6 +124,9 @@ class EmailBindings extends BaseBindings {
Get.find<SendingEmailCacheManager>(),
Get.find<FileUtils>(),
Get.find<CacheExceptionThrower>()));
Get.lazyPut(() => EmailLocalStorageDataSourceImpl(
Get.find<LocalStorageManager>(),
Get.find<CacheExceptionThrower>()));
}
@override
@@ -148,6 +157,8 @@ class EmailBindings extends BaseBindings {
Get.lazyPut(() => StoreOpenedEmailInteractor(Get.find<EmailRepository>()));
Get.lazyPut(() => PrintEmailInteractor(Get.find<EmailRepository>()));
Get.lazyPut(() => StoreEventAttendanceStatusInteractor(Get.find<EmailRepository>()));
Get.lazyPut(() => ParseEmailByBlobIdInteractor(Get.find<EmailRepository>()));
Get.lazyPut(() => PreviewEmailFromEmlFileInteractor(Get.find<EmailRepository>()));
IdentityInteractorsBindings().dependencies();
}
@@ -169,7 +180,8 @@ class EmailBindings extends BaseBindings {
Get.lazyPut(() => EmailRepositoryImpl(
{
DataSourceType.network: Get.find<EmailDataSource>(),
DataSourceType.hiveCache: Get.find<EmailHiveCacheDataSourceImpl>()
DataSourceType.hiveCache: Get.find<EmailHiveCacheDataSourceImpl>(),
DataSourceType.local: Get.find<EmailLocalStorageDataSourceImpl>(),
},
Get.find<HtmlDataSource>(),
Get.find<StateDataSource>(),
@@ -7,10 +7,12 @@ import 'package:core/core.dart';
import 'package:core/presentation/utils/html_transformer/text/sanitize_autolink_unescape_html_transformer.dart';
import 'package:core/presentation/utils/html_transformer/text/new_line_transformer.dart';
import 'package:core/presentation/utils/html_transformer/text/standardize_html_sanitizing_transformers.dart';
import 'package:core/utils/html/html_utils.dart';
import 'package:dartz/dartz.dart';
import 'package:dio/dio.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:jmap_dart_client/jmap/account_id.dart';
@@ -46,6 +48,7 @@ import 'package:tmail_ui_user/features/email/domain/model/event_action.dart';
import 'package:tmail_ui_user/features/email/domain/model/mark_read_action.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';
import 'package:tmail_ui_user/features/email/domain/model/preview_email_eml_request.dart';
import 'package:tmail_ui_user/features/email/domain/model/send_receipt_to_sender_request.dart';
import 'package:tmail_ui_user/features/email/domain/state/calendar_event_accept_state.dart';
import 'package:tmail_ui_user/features/email/domain/state/calendar_event_maybe_state.dart';
@@ -59,6 +62,8 @@ import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_read_sta
import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_star_state.dart';
import 'package:tmail_ui_user/features/email/domain/state/move_to_mailbox_state.dart';
import 'package:tmail_ui_user/features/email/domain/state/parse_calendar_event_state.dart';
import 'package:tmail_ui_user/features/email/domain/state/parse_email_by_blob_id_state.dart';
import 'package:tmail_ui_user/features/email/domain/state/preview_email_from_eml_file_state.dart';
import 'package:tmail_ui_user/features/email/domain/state/print_email_state.dart';
import 'package:tmail_ui_user/features/email/domain/state/send_receipt_to_sender_state.dart';
import 'package:tmail_ui_user/features/email/domain/state/store_event_attendance_status_state.dart';
@@ -74,6 +79,8 @@ import 'package:tmail_ui_user/features/email/domain/usecases/get_email_content_i
import 'package:tmail_ui_user/features/email/domain/usecases/mark_as_email_read_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/move_to_mailbox_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/parse_calendar_event_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/parse_email_by_blob_id_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/preview_email_from_eml_file_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/print_email_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/send_receipt_to_sender_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/store_event_attendance_status_interactor.dart';
@@ -138,6 +145,8 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
final StoreOpenedEmailInteractor _storeOpenedEmailInteractor;
final PrintEmailInteractor _printEmailInteractor;
final StoreEventAttendanceStatusInteractor _storeEventAttendanceStatusInteractor;
final ParseEmailByBlobIdInteractor _parseEmailByBlobIdInteractor;
final PreviewEmailFromEmlFileInteractor _previewEmailFromEmlFileInteractor;
CreateNewEmailRuleFilterInteractor? _createNewEmailRuleFilterInteractor;
SendReceiptToSenderInteractor? _sendReceiptToSenderInteractor;
@@ -191,6 +200,8 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
this._storeOpenedEmailInteractor,
this._printEmailInteractor,
this._storeEventAttendanceStatusInteractor,
this._parseEmailByBlobIdInteractor,
this._previewEmailFromEmlFileInteractor,
);
@override
@@ -246,6 +257,10 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
calendarEventSuccess(success);
} else if (success is StoreEventAttendanceStatusSuccess) {
_showToastMessageEventAttendanceSuccess(success);
} else if (success is ParseEmailByBlobIdSuccess) {
_handleParseEmailByBlobIdSuccess(success);
} else if (success is PreviewEmailFromEmlFileSuccess) {
_handlePreviewEmailFromEMLFileSuccess(success);
}
}
@@ -269,6 +284,10 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
} else if (failure is CalendarEventReplyFailure
|| failure is StoreEventAttendanceStatusFailure) {
_calendarEventFailure(failure);
} else if (failure is ParseEmailByBlobIdFailure) {
_handleParseEmailByBlobIdFailure(failure);
} else if (failure is PreviewEmailFromEmlFileFailure) {
_handlePreviewEmailFromEMLFileFailure(failure);
}
}
@@ -1896,16 +1915,12 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
}
void handleViewAttachmentAction(BuildContext context, Attachment attachment) {
if (PlatformInfo.isWeb) {
if (PlatformInfo.isCanvasKit && attachment.validatePDFIcon()) {
previewPDFFileAction(context, attachment);
} else {
downloadAttachmentForWeb(attachment);
}
} else if (PlatformInfo.isMobile) {
exportAttachment(context, attachment);
if (PlatformInfo.isWeb && PlatformInfo.isCanvasKit && attachment.isPDFFile) {
previewPDFFileAction(context, attachment);
} else if (attachment.isEMLFile) {
previewEMLFileAction(attachment, AppLocalizations.of(context));
} else {
log('EmailView::_handleViewAttachmentAction: THE PLATFORM IS SUPPORTED');
handleDownloadAttachmentAction(context, attachment);
}
}
@@ -1936,6 +1951,80 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
);
}
void previewEMLFileAction(Attachment attachment, AppLocalizations appLocalizations) {
SmartDialog.showLoading(
msg: appLocalizations.loadingPleaseWait,
maskColor: Colors.black38,
);
if (accountId == null) {
consumeState(Stream.value(Left(ParseEmailByBlobIdFailure(NotFoundAccountIdException()))));
return;
}
if (attachment.blobId == null) {
consumeState(Stream.value(Left(ParseEmailByBlobIdFailure(NotFoundBlobIdException([])))));
return;
}
consumeState(_parseEmailByBlobIdInteractor.execute(
accountId!,
attachment.blobId!,
));
}
void _handleParseEmailByBlobIdSuccess(ParseEmailByBlobIdSuccess success) {
if (session == null) {
consumeState(Stream.value(Left(PreviewEmailFromEmlFileFailure(NotFoundSessionException()))));
return;
}
if (accountId == null) {
consumeState(Stream.value(Left(PreviewEmailFromEmlFileFailure(NotFoundAccountIdException()))));
return;
}
if (currentContext == null) {
consumeState(Stream.value(Left(PreviewEmailFromEmlFileFailure(NotFoundContextException()))));
return;
}
consumeState(_previewEmailFromEmlFileInteractor.execute(
PreviewEmailEMLRequest(
accountId: accountId!,
userName: userName!,
blobId: success.blobId,
email: success.email,
locale: Localizations.localeOf(currentContext!),
appLocalizations: AppLocalizations.of(currentContext!),
baseDownloadUrl: session!.getDownloadUrl(jmapUrl: dynamicUrlInterceptors.jmapUrl),
),
));
}
void _handleParseEmailByBlobIdFailure(ParseEmailByBlobIdFailure failure) {
SmartDialog.dismiss();
toastManager.showMessageFailure(failure);
}
void _handlePreviewEmailFromEMLFileFailure(PreviewEmailFromEmlFileFailure failure) {
SmartDialog.dismiss();
toastManager.showMessageFailure(failure);
}
void _handlePreviewEmailFromEMLFileSuccess(PreviewEmailFromEmlFileSuccess success) {
SmartDialog.dismiss();
if (PlatformInfo.isWeb) {
HtmlUtils.openNewWindowByUrl(
RouteUtils.createUrlWebLocationBar(
AppRoutes.emailEMLPreviewer,
previewId: success.storeKey
).toString(),
);
}
}
void handleMailToAttendees(CalendarOrganizer? organizer, List<CalendarAttendee>? attendees) {
final listEmailAddressAttendees = attendees
?.map((attendee) => EmailAddress(attendee.name?.name, attendee.mailto?.mailAddress.value))
@@ -1,9 +1,11 @@
import 'package:core/presentation/extensions/media_type_extension.dart';
import 'package:core/presentation/resources/image_paths.dart';
import 'package:model/email/attachment.dart';
import 'package:tmail_ui_user/features/upload/domain/extensions/media_type_extension.dart';
extension AttachmentExtension on Attachment {
String getIcon(ImagePaths imagePaths) => type?.getIcon(imagePaths, fileName: name) ?? imagePaths.icFileEPup;
bool validatePDFIcon() => type?.validatePDFIcon(fileName: name) ?? false;
bool get isPDFFile => type?.isPDFFile(fileName: name) ?? false;
bool get isEMLFile => type?.isEMLFile ?? false;
}
@@ -0,0 +1,107 @@
import 'package:core/data/model/source_type/data_source_type.dart';
import 'package:core/presentation/resources/image_paths.dart';
import 'package:core/utils/file_utils.dart';
import 'package:core/utils/print_utils.dart';
import 'package:get/get.dart';
import 'package:tmail_ui_user/features/base/base_bindings.dart';
import 'package:tmail_ui_user/features/caching/utils/local_storage_manager.dart';
import 'package:tmail_ui_user/features/email/data/datasource/email_datasource.dart';
import 'package:tmail_ui_user/features/email/data/datasource/html_datasource.dart';
import 'package:tmail_ui_user/features/email/data/datasource/print_file_datasource.dart';
import 'package:tmail_ui_user/features/email/data/datasource_impl/email_datasource_impl.dart';
import 'package:tmail_ui_user/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart';
import 'package:tmail_ui_user/features/email/data/datasource_impl/email_local_storage_datasource_impl.dart';
import 'package:tmail_ui_user/features/email/data/datasource_impl/html_datasource_impl.dart';
import 'package:tmail_ui_user/features/email/data/datasource_impl/print_file_datasource_impl.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/repository/email_repository_impl.dart';
import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/get_preview_email_eml_content_shared_interactor.dart';
import 'package:tmail_ui_user/features/email_previewer/email_previewer_controller.dart';
import 'package:tmail_ui_user/features/mailbox/data/datasource/state_datasource.dart';
import 'package:tmail_ui_user/features/mailbox/data/datasource_impl/state_datasource_impl.dart';
import 'package:tmail_ui_user/features/mailbox/data/local/state_cache_manager.dart';
import 'package:tmail_ui_user/features/offline_mode/manager/new_email_cache_manager.dart';
import 'package:tmail_ui_user/features/offline_mode/manager/new_email_cache_worker_queue.dart';
import 'package:tmail_ui_user/features/offline_mode/manager/opened_email_cache_manager.dart';
import 'package:tmail_ui_user/features/offline_mode/manager/opened_email_cache_worker_queue.dart';
import 'package:tmail_ui_user/features/offline_mode/manager/sending_email_cache_manager.dart';
import 'package:tmail_ui_user/features/thread/data/local/email_cache_manager.dart';
import 'package:tmail_ui_user/main/exceptions/cache_exception_thrower.dart';
import 'package:tmail_ui_user/main/exceptions/remote_exception_thrower.dart';
import 'package:tmail_ui_user/main/utils/ios_sharing_manager.dart';
class EmailPreviewerBindings extends BaseBindings {
@override
void bindingsController() {
Get.lazyPut(() => EmailPreviewerController(
Get.find<GetPreviewEmailEMLContentSharedInteractor>()));
}
@override
void bindingsDataSource() {
Get.lazyPut<EmailDataSource>(() => Get.find<EmailDataSourceImpl>());
Get.lazyPut<HtmlDataSource>(() => Get.find<HtmlDataSourceImpl>());
Get.lazyPut<StateDataSource>(() => Get.find<StateDataSourceImpl>());
Get.lazyPut<PrintFileDataSource>(() => Get.find<PrintFileDataSourceImpl>());
}
@override
void bindingsDataSourceImpl() {
Get.lazyPut(() => EmailDataSourceImpl(
Get.find<EmailAPI>(),
Get.find<RemoteExceptionThrower>()));
Get.lazyPut(() => HtmlDataSourceImpl(
Get.find<HtmlAnalyzer>(),
Get.find<CacheExceptionThrower>()));
Get.lazyPut(() => StateDataSourceImpl(
Get.find<StateCacheManager>(),
Get.find<IOSSharingManager>(),
Get.find<CacheExceptionThrower>()));
Get.lazyPut(() => PrintFileDataSourceImpl(
Get.find<PrintUtils>(),
Get.find<ImagePaths>(),
Get.find<FileUtils>(),
Get.find<HtmlAnalyzer>(),
Get.find<CacheExceptionThrower>()));
Get.lazyPut(() => EmailHiveCacheDataSourceImpl(
Get.find<NewEmailCacheManager>(),
Get.find<OpenedEmailCacheManager>(),
Get.find<NewEmailCacheWorkerQueue>(),
Get.find<OpenedEmailCacheWorkerQueue>(),
Get.find<EmailCacheManager>(),
Get.find<SendingEmailCacheManager>(),
Get.find<FileUtils>(),
Get.find<CacheExceptionThrower>()));
Get.lazyPut(() => EmailLocalStorageDataSourceImpl(
Get.find<LocalStorageManager>(),
Get.find<CacheExceptionThrower>()));
}
@override
void bindingsInteractor() {
Get.lazyPut(() => GetPreviewEmailEMLContentSharedInteractor(
Get.find<EmailRepository>()));
}
@override
void bindingsRepository() {
Get.lazyPut<EmailRepository>(() => Get.find<EmailRepositoryImpl>());
}
@override
void bindingsRepositoryImpl() {
Get.lazyPut(() => EmailRepositoryImpl(
{
DataSourceType.network: Get.find<EmailDataSource>(),
DataSourceType.hiveCache: Get.find<EmailHiveCacheDataSourceImpl>(),
DataSourceType.local: Get.find<EmailLocalStorageDataSourceImpl>(),
},
Get.find<HtmlDataSource>(),
Get.find<StateDataSource>(),
Get.find<PrintFileDataSource>()));
}
}
@@ -0,0 +1,41 @@
import 'package:dartz/dartz.dart';
import 'package:get/get.dart';
import 'package:tmail_ui_user/features/base/base_controller.dart';
import 'package:tmail_ui_user/features/email/domain/exceptions/email_exceptions.dart';
import 'package:tmail_ui_user/features/email/domain/state/get_preview_email_content_shared_state.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/get_preview_email_eml_content_shared_interactor.dart';
import 'package:tmail_ui_user/main/routes/route_utils.dart';
class EmailPreviewerController extends BaseController {
final GetPreviewEmailEMLContentSharedInteractor _getPreviewEmailEMLContentSharedInteractor;
EmailPreviewerController(
this._getPreviewEmailEMLContentSharedInteractor,
);
@override
void onReady() {
super.onReady();
_getPreviewEmailEMLContentShared();
}
void _getPreviewEmailEMLContentShared() {
final parameters = Get.parameters;
if (parameters.isEmpty) {
consumeState(Stream.value(Left(GetPreviewEmailEMLContentSharedFailure(
NotFoundBlobIdException([])))));
return;
}
final keyStored = parameters[RouteUtils.paramID];
if (keyStored?.trim().isNotEmpty == true) {
consumeState(_getPreviewEmailEMLContentSharedInteractor.execute(keyStored!));
} else {
consumeState(Stream.value(Left(GetPreviewEmailEMLContentSharedFailure(
NotFoundBlobIdException([])))));
}
}
}
@@ -0,0 +1,48 @@
import 'package:core/presentation/views/html_viewer/html_content_viewer_on_web_widget.dart';
import 'package:core/presentation/views/loading/cupertino_loading_widget.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:tmail_ui_user/features/email/domain/state/get_preview_email_content_shared_state.dart';
import 'package:tmail_ui_user/features/email_previewer/email_previewer_controller.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
class EmailPreviewerView extends GetWidget<EmailPreviewerController> {
const EmailPreviewerView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: Obx(() {
final viewState = controller.viewState.value;
return viewState.fold(
(failure) {
if (viewState is GetPreviewEmailEMLContentSharedFailure) {
return Center(
child: Text(AppLocalizations.of(context).previewEmailFromEMLFileFailed),
);
} else {
return const Center(
child: CupertinoLoadingWidget(),
);
}
},
(success) {
if (success is GetPreviewEmailEMLContentSharedSuccess) {
return HtmlContentViewerOnWeb(
widthContent: context.width,
heightContent: context.height,
contentHtml: success.previewEMLContent,
);
} else {
return const Center(
child: CupertinoLoadingWidget(),
);
}
});
}),
);
}
}
@@ -1,2 +1,6 @@
class NotFoundSessionException implements Exception {}
class NotFoundSessionException implements Exception {}
class NotFoundAccountIdException implements Exception {}
class NotFoundContextException implements Exception {}
@@ -7,6 +7,7 @@ import 'package:core/utils/print_utils.dart';
import 'package:get/get.dart';
import 'package:tmail_ui_user/features/base/base_bindings.dart';
import 'package:tmail_ui_user/features/caching/clients/recent_search_cache_client.dart';
import 'package:tmail_ui_user/features/caching/utils/local_storage_manager.dart';
import 'package:tmail_ui_user/features/composer/data/repository/contact_repository_impl.dart';
import 'package:tmail_ui_user/features/composer/domain/repository/contact_repository.dart';
import 'package:tmail_ui_user/features/composer/domain/usecases/send_email_interactor.dart';
@@ -15,6 +16,7 @@ import 'package:tmail_ui_user/features/email/data/datasource/html_datasource.dar
import 'package:tmail_ui_user/features/email/data/datasource/print_file_datasource.dart';
import 'package:tmail_ui_user/features/email/data/datasource_impl/email_datasource_impl.dart';
import 'package:tmail_ui_user/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart';
import 'package:tmail_ui_user/features/email/data/datasource_impl/email_local_storage_datasource_impl.dart';
import 'package:tmail_ui_user/features/email/data/datasource_impl/html_datasource_impl.dart';
import 'package:tmail_ui_user/features/email/data/datasource_impl/print_file_datasource_impl.dart';
import 'package:tmail_ui_user/features/email/data/local/html_analyzer.dart';
@@ -254,6 +256,9 @@ class MailboxDashBoardBindings extends BaseBindings {
Get.find<SendingEmailCacheManager>(),
Get.find<FileUtils>(),
Get.find<CacheExceptionThrower>()));
Get.lazyPut(() => EmailLocalStorageDataSourceImpl(
Get.find<LocalStorageManager>(),
Get.find<CacheExceptionThrower>()));
Get.lazyPut(() => RemoteServerSettingsDataSourceImpl(
Get.find<ServerSettingsAPI>(),
Get.find<RemoteExceptionThrower>()
@@ -340,7 +345,8 @@ class MailboxDashBoardBindings extends BaseBindings {
Get.lazyPut(() => EmailRepositoryImpl(
{
DataSourceType.network: Get.find<EmailDataSource>(),
DataSourceType.hiveCache: Get.find<EmailHiveCacheDataSourceImpl>()
DataSourceType.hiveCache: Get.find<EmailHiveCacheDataSourceImpl>(),
DataSourceType.local: Get.find<EmailLocalStorageDataSourceImpl>(),
},
Get.find<HtmlDataSource>(),
Get.find<StateDataSource>(),
@@ -4,12 +4,14 @@ import 'package:core/utils/file_utils.dart';
import 'package:core/utils/print_utils.dart';
import 'package:get/get.dart';
import 'package:tmail_ui_user/features/base/interactors_bindings.dart';
import 'package:tmail_ui_user/features/caching/utils/local_storage_manager.dart';
import 'package:tmail_ui_user/features/composer/domain/usecases/send_email_interactor.dart';
import 'package:tmail_ui_user/features/email/data/datasource/email_datasource.dart';
import 'package:tmail_ui_user/features/email/data/datasource/html_datasource.dart';
import 'package:tmail_ui_user/features/email/data/datasource/print_file_datasource.dart';
import 'package:tmail_ui_user/features/email/data/datasource_impl/email_datasource_impl.dart';
import 'package:tmail_ui_user/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart';
import 'package:tmail_ui_user/features/email/data/datasource_impl/email_local_storage_datasource_impl.dart';
import 'package:tmail_ui_user/features/email/data/datasource_impl/html_datasource_impl.dart';
import 'package:tmail_ui_user/features/email/data/datasource_impl/print_file_datasource_impl.dart';
import 'package:tmail_ui_user/features/email/data/local/html_analyzer.dart';
@@ -89,6 +91,9 @@ class SendEmailInteractorBindings extends InteractorsBindings {
Get.find<SendingEmailCacheManager>(),
Get.find<FileUtils>(),
Get.find<CacheExceptionThrower>()));
Get.lazyPut(() => EmailLocalStorageDataSourceImpl(
Get.find<LocalStorageManager>(),
Get.find<CacheExceptionThrower>()));
Get.lazyPut(() => RemoteServerSettingsDataSourceImpl(
Get.find<ServerSettingsAPI>(),
Get.find<RemoteExceptionThrower>()));
@@ -111,7 +116,8 @@ class SendEmailInteractorBindings extends InteractorsBindings {
Get.lazyPut(() => EmailRepositoryImpl(
{
DataSourceType.network: Get.find<EmailDataSource>(),
DataSourceType.hiveCache: Get.find<EmailHiveCacheDataSourceImpl>()
DataSourceType.hiveCache: Get.find<EmailHiveCacheDataSourceImpl>(),
DataSourceType.local: Get.find<EmailLocalStorageDataSourceImpl>(),
},
Get.find<HtmlDataSource>(),
Get.find<StateDataSource>(),
@@ -4,11 +4,13 @@ import 'package:core/utils/file_utils.dart';
import 'package:core/utils/print_utils.dart';
import 'package:get/get.dart';
import 'package:tmail_ui_user/features/base/interactors_bindings.dart';
import 'package:tmail_ui_user/features/caching/utils/local_storage_manager.dart';
import 'package:tmail_ui_user/features/email/data/datasource/email_datasource.dart';
import 'package:tmail_ui_user/features/email/data/datasource/html_datasource.dart';
import 'package:tmail_ui_user/features/email/data/datasource/print_file_datasource.dart';
import 'package:tmail_ui_user/features/email/data/datasource_impl/email_datasource_impl.dart';
import 'package:tmail_ui_user/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart';
import 'package:tmail_ui_user/features/email/data/datasource_impl/email_local_storage_datasource_impl.dart';
import 'package:tmail_ui_user/features/email/data/datasource_impl/html_datasource_impl.dart';
import 'package:tmail_ui_user/features/email/data/datasource_impl/print_file_datasource_impl.dart';
import 'package:tmail_ui_user/features/email/data/local/html_analyzer.dart';
@@ -127,6 +129,9 @@ class FcmInteractorBindings extends InteractorsBindings {
Get.find<SendingEmailCacheManager>(),
Get.find<FileUtils>(),
Get.find<CacheExceptionThrower>()));
Get.lazyPut(() => EmailLocalStorageDataSourceImpl(
Get.find<LocalStorageManager>(),
Get.find<CacheExceptionThrower>()));
}
@override
@@ -180,7 +185,8 @@ class FcmInteractorBindings extends InteractorsBindings {
Get.lazyPut(() => EmailRepositoryImpl(
{
DataSourceType.network: Get.find<EmailDataSource>(),
DataSourceType.hiveCache: Get.find<EmailHiveCacheDataSourceImpl>()
DataSourceType.hiveCache: Get.find<EmailHiveCacheDataSourceImpl>(),
DataSourceType.local: Get.find<EmailLocalStorageDataSourceImpl>(),
},
Get.find<HtmlDataSource>(),
Get.find<StateDataSource>(),
@@ -2,10 +2,10 @@ import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
import 'package:dartz/dartz.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:jmap_dart_client/jmap/core/session/session.dart';
import 'package:model/oidc/oidc_configuration.dart';
import 'package:model/oidc/token_oidc.dart';
import 'package:tip_dialog/tip_dialog.dart';
import 'package:tmail_ui_user/features/base/reloadable/reloadable_controller.dart';
import 'package:tmail_ui_user/features/home/domain/state/get_session_state.dart';
import 'package:tmail_ui_user/features/login/data/network/config/oidc_constant.dart';
@@ -44,7 +44,7 @@ class TwakeWelcomeController extends ReloadableController {
}
void onClickSignIn(BuildContext context) {
TipDialogHelper.loading(AppLocalizations.of(context).loadingPleaseWait);
SmartDialog.showLoading(msg: AppLocalizations.of(context).loadingPleaseWait);
final baseUri = Uri.tryParse(AppConfig.saasJmapServerUrl);
@@ -64,7 +64,7 @@ class TwakeWelcomeController extends ReloadableController {
}
void onSignUpTwakeWorkplace(BuildContext context) {
TipDialogHelper.loading(AppLocalizations.of(context).loadingPleaseWait);
SmartDialog.showLoading(msg: AppLocalizations.of(context).loadingPleaseWait);
final baseUri = Uri.tryParse(AppConfig.saasJmapServerUrl);
@@ -115,7 +115,7 @@ class TwakeWelcomeController extends ReloadableController {
@override
void handleReloaded(Session session) {
TipDialogHelper.dismiss();
SmartDialog.dismiss();
popAndPush(
RouteUtils.generateNavigationRoute(AppRoutes.dashboard),
@@ -124,7 +124,7 @@ class TwakeWelcomeController extends ReloadableController {
@override
void handleGetSessionFailure(GetSessionFailure failure) {
TipDialogHelper.dismiss();
SmartDialog.dismiss();
toastManager.showMessageFailure(failure);
}
@@ -147,13 +147,13 @@ class TwakeWelcomeController extends ReloadableController {
}
void _handleSignInTwakeWorkplaceFailure(SignInTwakeWorkplaceFailure failure) {
TipDialogHelper.dismiss();
SmartDialog.dismiss();
toastManager.showMessageFailure(failure);
}
void _handleSignUpTwakeWorkplaceFailure(SignUpTwakeWorkplaceFailure failure) {
TipDialogHelper.dismiss();
SmartDialog.dismiss();
toastManager.showMessageFailure(failure);
}
@@ -3,7 +3,6 @@ import 'package:flutter/services.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:linagora_design_flutter/linagora_design_flutter.dart';
import 'package:tip_dialog/tip_dialog.dart';
import 'package:tmail_ui_user/features/starting_page/presentation/twake_welcome/twake_welcome_controller.dart';
import 'package:tmail_ui_user/features/starting_page/presentation/twake_welcome/twake_welcome_view_style.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
@@ -19,42 +18,29 @@ class TwakeWelcomeView extends GetWidget<TwakeWelcomeController> {
DeviceOrientation.portraitDown
]);
return Stack(
children: [
TwakeWelcomeScreen(
logo: Padding(
padding: const EdgeInsetsDirectional.only(bottom: 16),
child: SvgPicture.asset(
controller.imagePaths.icLogoTwakeWelcome,
fit: BoxFit.fill,
),
),
focusColor: Colors.transparent,
hoverColor: Colors.transparent,
highlightColor: Colors.transparent,
overlayColor: WidgetStateProperty.all(Colors.transparent),
signInTitle: AppLocalizations.of(context).signIn.capitalizeFirst ?? '',
onSignInOnTap: () => controller.onClickSignIn(context),
onCreateTwakeIdOnTap: () => controller.onSignUpTwakeWorkplace(context),
createTwakeIdTitle: AppLocalizations.of(context).createTwakeId,
useCompanyServerTitle: AppLocalizations.of(context).useCompanyServer,
description: AppLocalizations.of(context).descriptionWelcomeTo,
descriptionTextStyle: TwakeWelcomeViewStyle.descriptionTextStyle,
privacyPolicy: AppLocalizations.of(context).privacyPolicy,
descriptionPrivacyPolicy: AppLocalizations.of(context).byContinuingYouAreAgreeingToOur,
onPrivacyPolicyOnTap: controller.onClickPrivacyPolicy,
onUseCompanyServerOnTap: controller.handleUseCompanyServer,
return TwakeWelcomeScreen(
logo: Padding(
padding: const EdgeInsetsDirectional.only(bottom: 16),
child: SvgPicture.asset(
controller.imagePaths.icLogoTwakeWelcome,
fit: BoxFit.fill,
),
TipDialogContainer(
duration: const Duration(seconds: 2),
outsideTouchable: true,
onOutsideTouch: (tipDialog) {
if (tipDialog is TipDialog && tipDialog.type == TipDialogType.LOADING) {
TipDialogHelper.dismiss();
}
}
)
],
),
focusColor: Colors.transparent,
hoverColor: Colors.transparent,
highlightColor: Colors.transparent,
overlayColor: WidgetStateProperty.all(Colors.transparent),
signInTitle: AppLocalizations.of(context).signIn.capitalizeFirst ?? '',
onSignInOnTap: () => controller.onClickSignIn(context),
onCreateTwakeIdOnTap: () => controller.onSignUpTwakeWorkplace(context),
createTwakeIdTitle: AppLocalizations.of(context).createTwakeId,
useCompanyServerTitle: AppLocalizations.of(context).useCompanyServer,
description: AppLocalizations.of(context).descriptionWelcomeTo,
descriptionTextStyle: TwakeWelcomeViewStyle.descriptionTextStyle,
privacyPolicy: AppLocalizations.of(context).privacyPolicy,
descriptionPrivacyPolicy: AppLocalizations.of(context).byContinuingYouAreAgreeingToOur,
onPrivacyPolicyOnTap: controller.onClickPrivacyPolicy,
onUseCompanyServerOnTap: controller.handleUseCompanyServer,
);
}
}
@@ -84,4 +84,28 @@ class ThreadConstants {
static const int defaultMaxHeightEmailItemOnBrowser = 40;
static const int defaultMaxHeightBrowser = 1200;
static final propertiesParseEmailByBlobId = Properties({
EmailProperty.id,
EmailProperty.blobId,
EmailProperty.subject,
EmailProperty.from,
EmailProperty.to,
EmailProperty.cc,
EmailProperty.bcc,
EmailProperty.keywords,
EmailProperty.size,
EmailProperty.receivedAt,
EmailProperty.sentAt,
EmailProperty.preview,
EmailProperty.hasAttachment,
EmailProperty.replyTo,
EmailProperty.mailboxIds,
EmailProperty.bodyValues,
EmailProperty.htmlBody,
EmailProperty.attachments,
EmailProperty.headers,
EmailProperty.messageId,
EmailProperty.references,
});
}
@@ -1,28 +0,0 @@
import 'package:core/domain/extensions/media_type_extension.dart';
import 'package:core/presentation/resources/image_paths.dart';
import 'package:http_parser/http_parser.dart';
extension MediaTypeExtension on MediaType {
String getIcon(ImagePaths imagePaths, {String? fileName}) {
if (validatePDFIcon(fileName: fileName) == true) {
return imagePaths.icFilePdf;
} else if (isDocFile()) {
return imagePaths.icFileDocx;
} else if (isExcelFile()) {
return imagePaths.icFileXlsx;
} else if (isPowerPointFile()) {
return imagePaths.icFilePptx;
} else if (isPdfFile()) {
return imagePaths.icFilePdf;
} else if (isZipFile()) {
return imagePaths.icFileZip;
} else if (isImageFile()) {
return imagePaths.icFilePng;
} else {
return imagePaths.icFileEPup;
}
}
bool validatePDFIcon({required String? fileName}) => mimeType == 'application/pdf' ||
(mimeType == 'application/octet-stream' && fileName?.endsWith('.pdf') == true);
}
@@ -1,4 +1,5 @@
import 'package:core/presentation/extensions/media_type_extension.dart';
import 'package:core/presentation/resources/image_paths.dart';
import 'package:core/utils/app_logger.dart';
import 'package:dio/dio.dart';
@@ -6,7 +7,6 @@ import 'package:equatable/equatable.dart';
import 'package:http_parser/http_parser.dart';
import 'package:model/email/attachment.dart';
import 'package:model/upload/file_info.dart';
import 'package:tmail_ui_user/features/upload/domain/extensions/media_type_extension.dart';
import 'package:tmail_ui_user/features/upload/domain/model/upload_task_id.dart';
import 'package:tmail_ui_user/features/upload/presentation/model/upload_file_status.dart';
+13 -1
View File
@@ -1,5 +1,5 @@
{
"@@last_modified": "2025-01-09T04:35:09.125475",
"@@last_modified": "2025-01-10T23:14:09.007935",
"initializing_data": "Initializing data...",
"@initializing_data": {
"type": "text",
@@ -4089,5 +4089,17 @@
"type": "text",
"placeholders_order": [],
"placeholders": {}
},
"parseEmailByBlobIdFailed": "Cannot parse email by blob id",
"@parseEmailByBlobIdFailed": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
},
"previewEmailFromEMLFileFailed": "Cannot preview this eml file",
"@previewEmailFromEMLFileFailed": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
}
}
+4 -1
View File
@@ -3,6 +3,7 @@ import 'package:core/utils/app_logger.dart';
import 'package:core/utils/build_utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:tmail_ui_user/features/caching/config/hive_cache_config.dart';
import 'package:tmail_ui_user/main/bindings/main_bindings.dart';
@@ -74,6 +75,8 @@ class TMailApp extends StatelessWidget {
unknownRoute: AppPages.unknownRoutePage,
defaultTransition: Transition.noTransition,
initialRoute: AppRoutes.home,
getPages: AppPages.pages);
getPages: AppPages.pages,
builder: FlutterSmartDialog.init(),
);
}
}
@@ -7,12 +7,14 @@ import 'package:core/utils/application_manager.dart';
import 'package:core/utils/config/app_config_loader.dart';
import 'package:core/utils/file_utils.dart';
import 'package:core/utils/platform_info.dart';
import 'package:core/utils/preview_eml_file_utils.dart';
import 'package:core/utils/print_utils.dart';
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/base/before_reconnect_manager.dart';
import 'package:tmail_ui_user/features/caching/utils/local_storage_manager.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';
@@ -72,6 +74,7 @@ class CoreBindings extends Bindings {
if (PlatformInfo.isIOS) {
Get.put(IOSNotificationManager());
}
Get.put(PreviewEmlFileUtils());
}
void _bindingIsolate() {
@@ -88,5 +91,6 @@ class CoreBindings extends Bindings {
accessibility: KeychainAccessibility.first_unlock_this_device
)
));
Get.put(LocalStorageManager());
}
}
@@ -4294,4 +4294,17 @@ class AppLocalizations {
);
}
String get parseEmailByBlobIdFailed {
return Intl.message(
'Cannot parse email by blob id',
name: 'parseEmailByBlobIdFailed',
);
}
String get previewEmailFromEMLFileFailed {
return Intl.message(
'Cannot preview this eml file',
name: 'previewEmailFromEMLFileFailed',
);
}
}
+10
View File
@@ -8,6 +8,7 @@ import 'package:tmail_ui_user/features/composer/presentation/composer_view.dart'
import 'package:tmail_ui_user/features/contact/presentation/contact_bindings.dart';
import 'package:tmail_ui_user/features/destination_picker/presentation/destination_picker_bindings.dart';
import 'package:tmail_ui_user/features/destination_picker/presentation/destination_picker_view.dart' deferred as destination_picker;
import 'package:tmail_ui_user/features/email_previewer/email_previewer_bindings.dart';
import 'package:tmail_ui_user/features/email_recovery/presentation/email_recovery_bindings.dart';
import 'package:tmail_ui_user/features/home/presentation/home_bindings.dart';
import 'package:tmail_ui_user/features/home/presentation/home_view.dart';
@@ -36,6 +37,7 @@ import 'package:tmail_ui_user/main/routes/app_routes.dart';
import 'package:tmail_ui_user/features/search/mailbox/presentation/search_mailbox_view.dart' deferred as search_mailbox_view;
import 'package:tmail_ui_user/features/mailto/presentation/mailto_url_view.dart' deferred as mailto_url_view;
import 'package:tmail_ui_user/features/email_recovery/presentation/email_recovery_view.dart' deferred as email_recovery;
import 'package:tmail_ui_user/features/email_previewer/email_previewer_view.dart' deferred as email_previewer_view;
class AppPages {
static final pages = [
@@ -82,6 +84,14 @@ class AppPages {
contact_view.loadLibrary,
() => contact_view.ContactView()),
binding: ContactBindings()),
GetPage(
name: AppRoutes.emailEMLPreviewer,
page: () => DeferredWidget(
email_previewer_view.loadLibrary,
() => email_previewer_view.EmailPreviewerView(),
),
binding: EmailPreviewerBindings(),
),
unknownRoutePage,
if (PlatformInfo.isMobile)
...[
+1
View File
@@ -16,4 +16,5 @@ abstract class AppRoutes {
static const searchMailbox = '/search_mailbox';
static const mailtoURL = '/mailto';
static const emailRecovery = '/email_recovery';
static const emailEMLPreviewer = '/mail/eml/preview';
}
+17 -1
View File
@@ -83,12 +83,22 @@ abstract class RouteUtils {
return servicePath;
}
static ServicePath _createEmailEMLPreviewerServicePath(String route, {String? previewId}) {
ServicePath servicePath = ServicePath(route);
if (previewId?.trim().isNotEmpty == true) {
servicePath = servicePath.withQueryParameters([
StringQueryParameter(paramID, previewId!)
]);
}
return servicePath;
}
static ServicePath _createServicePath(String route) {
ServicePath servicePath = ServicePath(route);
return servicePath;
}
static Uri createUrlWebLocationBar(String route, {NavigationRouter? router}) {
static Uri createUrlWebLocationBar(String route, {NavigationRouter? router, String? previewId}) {
if (route == AppRoutes.dashboard) {
final servicePath = _createDashboardServicePath(
'$baseOriginUrl$route',
@@ -98,6 +108,12 @@ abstract class RouteUtils {
} else if (route == AppRoutes.settings) {
final servicePath = _createSettingServicePath('$baseOriginUrl$route', router: router);
return Uri.parse(servicePath.path);
} else if (route == AppRoutes.emailEMLPreviewer) {
final servicePath = _createEmailEMLPreviewerServicePath(
'$baseOriginUrl$route',
previewId: previewId,
);
return Uri.parse(servicePath.path);
} else {
final servicePath = _createServicePath('$baseOriginUrl$route');
return Uri.parse(servicePath.path);
+6
View File
@@ -5,6 +5,8 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:model/email/email_action_type.dart';
import 'package:tmail_ui_user/features/email/domain/model/move_action.dart';
import 'package:tmail_ui_user/features/email/domain/state/parse_email_by_blob_id_state.dart';
import 'package:tmail_ui_user/features/email/domain/state/preview_email_from_eml_file_state.dart';
import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.dart';
import 'package:tmail_ui_user/features/home/domain/state/get_session_state.dart';
import 'package:tmail_ui_user/features/login/data/network/oidc_error.dart';
@@ -84,6 +86,10 @@ class ToastManager {
} else {
message = AppLocalizations.of(currentContext!).createTwakeIdFailed;
}
} else if (failure is ParseEmailByBlobIdFailure) {
message = AppLocalizations.of(currentContext!).parseEmailByBlobIdFailed;
} else if (failure is PreviewEmailFromEmlFileFailure) {
message = AppLocalizations.of(currentContext!).previewEmailFromEMLFileFailed;
}
if (message?.isNotEmpty == true) {
+10
View File
@@ -64,6 +64,16 @@ extension EmailExtension on Email {
return '';
}
String getSentAt({required String newLocale, String? pattern}) {
if (sentAt != null) {
return sentAt!.formatDateToLocal(
pattern: pattern ?? sentAt!.value.toLocal().toPattern(),
locale: newLocale
);
}
return '';
}
Set<String> getRecipientEmailAddressList() {
final listEmailAddress = <String>{};
final listToAddress = to.getListAddress() ?? [];
+8 -16
View File
@@ -1021,6 +1021,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.0"
flutter_smart_dialog:
dependency: "direct main"
description:
name: flutter_smart_dialog
sha256: d7b915461fdc9bb8111d23a709b4ce910dbc4b9bef0fbd941655f74bf7de09a6
url: "https://pub.dev"
source: hosted
version: "4.9.8+5"
flutter_staggered_grid_view:
dependency: "direct main"
description:
@@ -1337,14 +1345,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.1"
loading_view:
dependency: transitive
description:
name: loading_view
sha256: "3d31c2c5293c2e3518b7330ffdc2fab5c83daaa9218cc45d4406c875f08f0795"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
logging:
dependency: transitive
description:
@@ -2023,14 +2023,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.1"
tip_dialog:
dependency: "direct main"
description:
name: tip_dialog
sha256: edc1ebbb4b9f9575220c85206cf6986921c2b63d173744be0babac00c2e48bd0
url: "https://pub.dev"
source: hosted
version: "4.0.0"
typed_data:
dependency: transitive
description:
+1 -1
View File
@@ -247,7 +247,7 @@ dependencies:
web_socket_channel: 2.4.3
tip_dialog: 4.0.0
flutter_smart_dialog: 4.9.8+5
flutter_web_auth_2: 3.1.1
@@ -47,7 +47,7 @@ void main() {
result,
emitsInOrder([
Right<Failure, Success>(ParsingEmailByBlobId()),
Right<Failure, Success>(ParseEmailByBlobIdSuccess(parsedEmail)),
Right<Failure, Success>(ParseEmailByBlobIdSuccess(parsedEmail, blobId)),
]),
);
@@ -39,6 +39,8 @@ import 'package:tmail_ui_user/features/email/domain/usecases/mark_as_email_read_
import 'package:tmail_ui_user/features/email/domain/usecases/mark_as_star_email_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/move_to_mailbox_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/parse_calendar_event_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/parse_email_by_blob_id_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/preview_email_from_eml_file_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/print_email_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/store_event_attendance_status_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/store_opened_email_interactor.dart';
@@ -98,6 +100,8 @@ const fallbackGenerators = {
MockSpec<MaybeCalendarEventInteractor>(),
MockSpec<RejectCalendarEventInteractor>(),
MockSpec<StoreEventAttendanceStatusInteractor>(),
MockSpec<ParseEmailByBlobIdInteractor>(),
MockSpec<PreviewEmailFromEmlFileInteractor>(),
MockSpec<PrintUtils>(),
MockSpec<ApplicationManager>(),
MockSpec<ToastManager>(),
@@ -134,6 +138,8 @@ void main() {
final uuid = MockUuid();
final printEmailInteractor = MockPrintEmailInteractor();
final storeEventAttendanceStatusInteractor = MockStoreEventAttendanceStatusInteractor();
final parseEmailByBlobIdInteractor = MockParseEmailByBlobIdInteractor();
final previewEmailFromEmlFileInteractor = MockPreviewEmailFromEmlFileInteractor();
final printUtils = MockPrintUtils();
final applicationManager = MockApplicationManager();
final mockToastManager = MockToastManager();
@@ -191,6 +197,8 @@ void main() {
storeOpenedEmailInteractor,
printEmailInteractor,
storeEventAttendanceStatusInteractor,
parseEmailByBlobIdInteractor,
previewEmailFromEmlFileInteractor,
);
});
@@ -1,7 +1,7 @@
import 'package:core/core.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:http_parser/http_parser.dart';
import 'package:model/email/attachment.dart';
import 'package:tmail_ui_user/features/upload/domain/extensions/media_type_extension.dart';
void main() {
final attachmentA = Attachment(
@@ -34,7 +34,7 @@ void main() {
'WHEN perform call `attachmentA.isDisplayedPDFIcon()`\n'
'SHOULD return true',
() {
bool result = attachmentA.type!.validatePDFIcon(fileName: null);
bool result = attachmentA.type!.isPDFFile(fileName: null);
expect(result, isTrue);
});
@@ -45,7 +45,7 @@ void main() {
'WHEN perform call `attachmentB.isDisplayedPDFIcon()`\n'
'SHOULD return true',
() {
bool result = attachmentB.type!.validatePDFIcon(fileName: 'attachmentB.pdf');
bool result = attachmentB.type!.isPDFFile(fileName: 'attachmentB.pdf');
expect(result, isTrue);
});
@@ -56,7 +56,7 @@ void main() {
'WHEN perform call `attachmentC.isDisplayedPDFIcon()`\n'
'SHOULD return false',
() {
bool result = attachmentC.type!.validatePDFIcon(fileName: 'attachmentC.docx');
bool result = attachmentC.type!.isPDFFile(fileName: 'attachmentC.docx');
expect(result, isFalse);
});
@@ -67,7 +67,7 @@ void main() {
'WHEN perform call `attachmentD.isDisplayedPDFIcon()`\n'
'SHOULD return false',
() {
bool result = attachmentD.type!.validatePDFIcon(fileName: 'attachmentD.png');
bool result = attachmentD.type!.isPDFFile(fileName: 'attachmentD.png');
expect(result, isFalse);
});
@@ -78,7 +78,7 @@ void main() {
'WHEN perform call `attachmentE.isDisplayedPDFIcon()`\n'
'SHOULD return false',
() {
bool result = attachmentE.type!.validatePDFIcon(fileName: 'attachmentE.pdf');
bool result = attachmentE.type!.isPDFFile(fileName: 'attachmentE.pdf');
expect(result, isFalse);
});