TF-3514 Detect base64 image to transfer it to cid attachment

Signed-off-by: dab246 <tdvu@linagora.com>
This commit is contained in:
dab246
2025-03-05 14:26:32 +07:00
committed by Dat H. Pham
parent 5c6a995107
commit 4765921a35
25 changed files with 367 additions and 231 deletions
+26
View File
@@ -3,6 +3,7 @@ import 'dart:typed_data';
import 'package:core/utils/app_logger.dart'; import 'package:core/utils/app_logger.dart';
import 'package:core/domain/exceptions/string_exception.dart'; import 'package:core/domain/exceptions/string_exception.dart';
import 'package:http_parser/http_parser.dart';
class StringConvert { class StringConvert {
static const String separatorPattern = r'[ ,;]+'; static const String separatorPattern = r'[ ,;]+';
@@ -73,4 +74,29 @@ class StringConvert {
static String toUrlScheme(String hostScheme) { static String toUrlScheme(String hostScheme) {
return '$hostScheme://'; return '$hostScheme://';
} }
static Uint8List convertBase64ImageTagToBytes(String base64ImageTag) {
if (!base64ImageTag.contains('base64,')) {
throw ArgumentError('The string is not valid Base64 data from an <img> tag.');
}
final base64Data = base64ImageTag.split(',').last;
return base64Decode(base64Data);
}
static MediaType? getMediaTypeFromBase64ImageTag(String base64ImageTag) {
try {
if (!base64ImageTag.startsWith("data:") || !base64ImageTag.contains(";base64,")) {
return null;
}
final mimeType = base64ImageTag.split(";")[0].split(":")[1];
log('StringConvert::getMediaTypeFromBase64ImageTag:mimeType = $mimeType');
return MediaType.parse(mimeType);
} catch (e) {
logError('StringConvert::getMimeTypeFromBase64ImageTag:Exception = $e');
return null;
}
}
} }
@@ -54,15 +54,14 @@ class ComposerRepositoryImpl extends ComposerRepository {
String emailContent = createEmailRequest.emailContent; String emailContent = createEmailRequest.emailContent;
Set<EmailBodyPart> emailAttachments = Set.from(createEmailRequest.createAttachments()); Set<EmailBodyPart> emailAttachments = Set.from(createEmailRequest.createAttachments());
if (createEmailRequest.inlineAttachments?.isNotEmpty == true) { final tupleContentInlineAttachments = await _replaceImageBase64ToImageCID(
final tupleContentInlineAttachments = await _replaceImageBase64ToImageCID( emailContent: emailContent,
emailContent: emailContent, inlineAttachments: createEmailRequest.inlineAttachments ?? {},
inlineAttachments: createEmailRequest.inlineAttachments! uploadUri: createEmailRequest.uploadUri,
); );
emailContent = tupleContentInlineAttachments.value1; emailContent = tupleContentInlineAttachments.value1;
emailAttachments.addAll(tupleContentInlineAttachments.value2); emailAttachments.addAll(tupleContentInlineAttachments.value2);
}
emailContent = await _removeCollapsedExpandedSignatureEffect(emailContent: emailContent); emailContent = await _removeCollapsedExpandedSignatureEffect(emailContent: emailContent);
@@ -83,18 +82,23 @@ class ComposerRepositoryImpl extends ComposerRepository {
Future<Tuple2<String, Set<EmailBodyPart>>> _replaceImageBase64ToImageCID({ Future<Tuple2<String, Set<EmailBodyPart>>> _replaceImageBase64ToImageCID({
required String emailContent, required String emailContent,
required Map<String, Attachment> inlineAttachments required Map<String, Attachment> inlineAttachments,
required Uri? uploadUri,
}) { }) {
try { try {
return _htmlDataSource.replaceImageBase64ToImageCID( return _htmlDataSource.replaceImageBase64ToImageCID(
emailContent: emailContent, emailContent: emailContent,
inlineAttachments: inlineAttachments); inlineAttachments: inlineAttachments,
uploadUri: uploadUri,
);
} catch (e) { } catch (e) {
logError('ComposerRepositoryImpl::_replaceImageBase64ToImageCID: Exception: $e'); logError('ComposerRepositoryImpl::_replaceImageBase64ToImageCID: Exception: $e');
return Future.value( return Future.value(
Tuple2( Tuple2(
emailContent, emailContent,
inlineAttachments.values.toList().toEmailBodyPart(charset: Constant.base64Charset) inlineAttachments.isNotEmpty
? inlineAttachments.values.toList().toEmailBodyPart(charset: Constant.base64Charset)
: {},
) )
); );
} }
@@ -65,29 +65,13 @@ import 'package:tmail_ui_user/features/upload/data/network/file_uploader.dart';
import 'package:tmail_ui_user/features/upload/domain/usecases/local_file_picker_interactor.dart'; import 'package:tmail_ui_user/features/upload/domain/usecases/local_file_picker_interactor.dart';
import 'package:tmail_ui_user/features/upload/domain/usecases/local_image_picker_interactor.dart'; import 'package:tmail_ui_user/features/upload/domain/usecases/local_image_picker_interactor.dart';
import 'package:tmail_ui_user/features/upload/presentation/controller/upload_controller.dart'; import 'package:tmail_ui_user/features/upload/presentation/controller/upload_controller.dart';
import 'package:tmail_ui_user/main/bindings/network/binding_tag.dart';
import 'package:tmail_ui_user/main/exceptions/cache_exception_thrower.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/exceptions/remote_exception_thrower.dart';
import 'package:tmail_ui_user/main/utils/ios_sharing_manager.dart'; import 'package:tmail_ui_user/main/utils/ios_sharing_manager.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import 'package:worker_manager/worker_manager.dart';
class ComposerBindings extends BaseBindings { class ComposerBindings extends BaseBindings {
@override
void dependencies() {
_bindingsUtils();
super.dependencies();
}
void _bindingsUtils() {
Get.lazyPut(() => FileUploader(
Get.find<DioClient>(tag: BindingTag.isolateTag),
Get.find<Executor>(),
Get.find<FileUtils>(),
));
}
@override @override
void bindingsDataSourceImpl() { void bindingsDataSourceImpl() {
Get.lazyPut(() => AttachmentUploadDataSourceImpl( Get.lazyPut(() => AttachmentUploadDataSourceImpl(
@@ -20,6 +20,7 @@ import 'package:html/parser.dart';
import 'package:html_editor_enhanced/html_editor.dart'; import 'package:html_editor_enhanced/html_editor.dart';
import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/account_id.dart';
import 'package:jmap_dart_client/jmap/core/properties/properties.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/identities/identity.dart'; import 'package:jmap_dart_client/jmap/identities/identity.dart';
import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart';
import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart';
@@ -474,24 +475,35 @@ class ComposerController extends BaseController
mailboxDashBoardController.sessionCurrent!.username); mailboxDashBoardController.sessionCurrent!.username);
} }
Uri? _getUploadUriFromSession(Session session, AccountId accountId) {
try {
return session.getUploadUri(accountId, jmapUrl: dynamicUrlInterceptors.jmapUrl);
} catch (e) {
logError('ComposerController::_getUploadUriFromSession:Exception = $e');
return null;
}
}
Future<CreateEmailRequest?> _generateCreateEmailRequest() async { Future<CreateEmailRequest?> _generateCreateEmailRequest() async {
if (composerArguments.value == null || final arguments = composerArguments.value;
mailboxDashBoardController.sessionCurrent == null || final session = mailboxDashBoardController.sessionCurrent;
mailboxDashBoardController.accountId.value == null final accountId = mailboxDashBoardController.accountId.value;
) {
if (arguments == null || session == null || accountId == null) {
log('ComposerController::_generateCreateEmailRequest: SESSION or ACCOUNT_ID or ARGUMENTS is NULL'); log('ComposerController::_generateCreateEmailRequest: SESSION or ACCOUNT_ID or ARGUMENTS is NULL');
return null; return null;
} }
final emailContent = await getContentInEditor(); final emailContent = await getContentInEditor();
final uploadUri = _getUploadUriFromSession(session, accountId);
return CreateEmailRequest( return CreateEmailRequest(
session: mailboxDashBoardController.sessionCurrent!, session: session,
accountId: mailboxDashBoardController.accountId.value!, accountId: accountId,
emailActionType: composerArguments.value!.emailActionType, emailActionType: arguments.emailActionType,
subject: subjectEmail.value ?? '', subject: subjectEmail.value ?? '',
emailContent: emailContent, emailContent: emailContent,
fromSender: composerArguments.value!.presentationEmail?.from ?? {}, fromSender: arguments.presentationEmail?.from ?? {},
toRecipients: listToEmailAddress.toSet(), toRecipients: listToEmailAddress.toSet(),
ccRecipients: listCcEmailAddress.toSet(), ccRecipients: listCcEmailAddress.toSet(),
bccRecipients: listBccEmailAddress.toSet(), bccRecipients: listBccEmailAddress.toSet(),
@@ -505,12 +517,13 @@ class ComposerController extends BaseController
sentMailboxId: getSentMailboxIdForComposer(), sentMailboxId: getSentMailboxIdForComposer(),
draftsMailboxId: getDraftMailboxIdForComposer(), draftsMailboxId: getDraftMailboxIdForComposer(),
draftsEmailId: getDraftEmailId(), draftsEmailId: getDraftEmailId(),
answerForwardEmailId: composerArguments.value!.presentationEmail?.id, answerForwardEmailId: arguments.presentationEmail?.id,
unsubscribeEmailId: composerArguments.value!.previousEmailId, unsubscribeEmailId: arguments.previousEmailId,
messageId: composerArguments.value!.messageId, messageId: arguments.messageId,
references: composerArguments.value!.references, references: arguments.references,
emailSendingQueue: composerArguments.value!.sendingEmail, emailSendingQueue: arguments.sendingEmail,
displayMode: screenDisplayMode.value displayMode: screenDisplayMode.value,
uploadUri: uploadUri,
); );
} }
@@ -1043,10 +1056,11 @@ class ComposerController extends BaseController
} }
void _handleSendMessages(BuildContext context) async { void _handleSendMessages(BuildContext context) async {
if (composerArguments.value == null || final arguments = composerArguments.value;
mailboxDashBoardController.sessionCurrent == null || final session = mailboxDashBoardController.sessionCurrent;
mailboxDashBoardController.accountId.value == null final accountId = mailboxDashBoardController.accountId.value;
) {
if (arguments == null || session == null || accountId == null) {
log('ComposerController::_handleSendMessages: SESSION or ACCOUNT_ID or ARGUMENTS is NULL'); log('ComposerController::_handleSendMessages: SESSION or ACCOUNT_ID or ARGUMENTS is NULL');
_sendButtonState = ButtonState.enabled; _sendButtonState = ButtonState.enabled;
_closeComposerAction(closeOverlays: true); _closeComposerAction(closeOverlays: true);
@@ -1058,9 +1072,14 @@ class ComposerController extends BaseController
} }
final emailContent = await getContentInEditor(); final emailContent = await getContentInEditor();
final uploadUri = _getUploadUriFromSession(session, accountId);
final cancelToken = CancelToken(); final cancelToken = CancelToken();
final resultState = await _showSendingMessageDialog( final resultState = await _showSendingMessageDialog(
session: session,
accountId: accountId,
arguments: arguments,
emailContent: emailContent, emailContent: emailContent,
uploadUri: uploadUri,
cancelToken: cancelToken cancelToken: cancelToken
); );
log('ComposerController::_handleSendMessages: resultState = $resultState'); log('ComposerController::_handleSendMessages: resultState = $resultState');
@@ -1080,18 +1099,22 @@ class ComposerController extends BaseController
} }
Future<dynamic> _showSendingMessageDialog({ Future<dynamic> _showSendingMessageDialog({
required Session session,
required AccountId accountId,
required ComposerArguments arguments,
required String emailContent, required String emailContent,
CancelToken? cancelToken required Uri? uploadUri,
CancelToken? cancelToken,
}) { }) {
final childWidget = PointerInterceptor( final childWidget = PointerInterceptor(
child: SendingMessageDialogView( child: SendingMessageDialogView(
createEmailRequest: CreateEmailRequest( createEmailRequest: CreateEmailRequest(
session: mailboxDashBoardController.sessionCurrent!, session: session,
accountId: mailboxDashBoardController.accountId.value!, accountId: accountId,
emailActionType: composerArguments.value!.emailActionType, emailActionType: arguments.emailActionType,
subject: subjectEmail.value ?? '', subject: subjectEmail.value ?? '',
emailContent: emailContent, emailContent: emailContent,
fromSender: composerArguments.value!.presentationEmail?.from ?? {}, fromSender: arguments.presentationEmail?.from ?? {},
toRecipients: listToEmailAddress.toSet(), toRecipients: listToEmailAddress.toSet(),
ccRecipients: listCcEmailAddress.toSet(), ccRecipients: listCcEmailAddress.toSet(),
bccRecipients: listBccEmailAddress.toSet(), bccRecipients: listBccEmailAddress.toSet(),
@@ -1104,12 +1127,13 @@ class ComposerController extends BaseController
outboxMailboxId: getOutboxMailboxIdForComposer(), outboxMailboxId: getOutboxMailboxIdForComposer(),
sentMailboxId: getSentMailboxIdForComposer(), sentMailboxId: getSentMailboxIdForComposer(),
draftsEmailId: getDraftEmailId(), draftsEmailId: getDraftEmailId(),
answerForwardEmailId: composerArguments.value!.presentationEmail?.id, answerForwardEmailId: arguments.presentationEmail?.id,
unsubscribeEmailId: composerArguments.value!.previousEmailId, unsubscribeEmailId: arguments.previousEmailId,
messageId: composerArguments.value!.messageId, messageId: arguments.messageId,
references: composerArguments.value!.references, references: arguments.references,
emailSendingQueue: composerArguments.value!.sendingEmail, emailSendingQueue: arguments.sendingEmail,
displayMode: screenDisplayMode.value displayMode: screenDisplayMode.value,
uploadUri: uploadUri,
), ),
createNewAndSendEmailInteractor: _createNewAndSendEmailInteractor, createNewAndSendEmailInteractor: _createNewAndSendEmailInteractor,
onCancelSendingEmailAction: _handleCancelSendingMessage, onCancelSendingEmailAction: _handleCancelSendingMessage,
@@ -1350,9 +1374,13 @@ class ComposerController extends BaseController
_saveToDraftButtonState = ButtonState.disabled; _saveToDraftButtonState = ButtonState.disabled;
if (composerArguments.value == null || final arguments = composerArguments.value;
mailboxDashBoardController.sessionCurrent == null || final session = mailboxDashBoardController.sessionCurrent;
mailboxDashBoardController.accountId.value == null || final accountId = mailboxDashBoardController.accountId.value;
if (arguments == null ||
session == null ||
accountId == null ||
getDraftMailboxIdForComposer() == null getDraftMailboxIdForComposer() == null
) { ) {
log('ComposerController::handleClickSaveAsDraftsButton: SESSION or ACCOUNT_ID or ARGUMENTS is NULL'); log('ComposerController::handleClickSaveAsDraftsButton: SESSION or ACCOUNT_ID or ARGUMENTS is NULL');
@@ -1361,9 +1389,14 @@ class ComposerController extends BaseController
} }
final emailContent = await getContentInEditor(); final emailContent = await getContentInEditor();
final uploadUri = _getUploadUriFromSession(session, accountId);
final cancelToken = CancelToken(); final cancelToken = CancelToken();
final resultState = await _showSavingMessageToDraftsDialog( final resultState = await _showSavingMessageToDraftsDialog(
session: session,
accountId: accountId,
arguments: arguments,
emailContent: emailContent, emailContent: emailContent,
uploadUri: uploadUri,
draftEmailId: _emailIdEditing, draftEmailId: _emailIdEditing,
cancelToken: cancelToken cancelToken: cancelToken
); );
@@ -2291,9 +2324,13 @@ class ComposerController extends BaseController
} }
void _handleSaveMessageToDraft(BuildContext context) async { void _handleSaveMessageToDraft(BuildContext context) async {
if (composerArguments.value == null || final arguments = composerArguments.value;
mailboxDashBoardController.sessionCurrent == null || final session = mailboxDashBoardController.sessionCurrent;
mailboxDashBoardController.accountId.value == null || final accountId = mailboxDashBoardController.accountId.value;
if (arguments == null ||
session == null ||
accountId == null ||
getDraftMailboxIdForComposer() == null getDraftMailboxIdForComposer() == null
) { ) {
log('ComposerController::_handleSaveMessageToDraft: SESSION or ACCOUNT_ID or ARGUMENTS is NULL'); log('ComposerController::_handleSaveMessageToDraft: SESSION or ACCOUNT_ID or ARGUMENTS is NULL');
@@ -2305,11 +2342,16 @@ class ComposerController extends BaseController
popBack(); popBack();
final emailContent = await getContentInEditor(); final emailContent = await getContentInEditor();
final uploadUri = _getUploadUriFromSession(session, accountId);
final draftEmailId = getDraftEmailId(); final draftEmailId = getDraftEmailId();
log('ComposerController::_handleSaveMessageToDraft: draftEmailId = $draftEmailId'); log('ComposerController::_handleSaveMessageToDraft: draftEmailId = $draftEmailId');
final cancelToken = CancelToken(); final cancelToken = CancelToken();
final resultState = await _showSavingMessageToDraftsDialog( final resultState = await _showSavingMessageToDraftsDialog(
session: session,
accountId: accountId,
arguments: arguments,
emailContent: emailContent, emailContent: emailContent,
uploadUri: uploadUri,
draftEmailId: draftEmailId, draftEmailId: draftEmailId,
cancelToken: cancelToken cancelToken: cancelToken
); );
@@ -2346,19 +2388,23 @@ class ComposerController extends BaseController
} }
Future<dynamic> _showSavingMessageToDraftsDialog({ Future<dynamic> _showSavingMessageToDraftsDialog({
required Session session,
required AccountId accountId,
required ComposerArguments arguments,
required String emailContent, required String emailContent,
required Uri? uploadUri,
EmailId? draftEmailId, EmailId? draftEmailId,
CancelToken? cancelToken, CancelToken? cancelToken,
}) { }) {
final childWidget = PointerInterceptor( final childWidget = PointerInterceptor(
child: SavingMessageDialogView( child: SavingMessageDialogView(
createEmailRequest: CreateEmailRequest( createEmailRequest: CreateEmailRequest(
session: mailboxDashBoardController.sessionCurrent!, session: session,
accountId: mailboxDashBoardController.accountId.value!, accountId: accountId,
emailActionType: composerArguments.value!.emailActionType, emailActionType: arguments.emailActionType,
subject: subjectEmail.value ?? '', subject: subjectEmail.value ?? '',
emailContent: emailContent, emailContent: emailContent,
fromSender: composerArguments.value!.presentationEmail?.from ?? {}, fromSender: arguments.presentationEmail?.from ?? {},
toRecipients: listToEmailAddress.toSet(), toRecipients: listToEmailAddress.toSet(),
ccRecipients: listCcEmailAddress.toSet(), ccRecipients: listCcEmailAddress.toSet(),
bccRecipients: listBccEmailAddress.toSet(), bccRecipients: listBccEmailAddress.toSet(),
@@ -2371,12 +2417,13 @@ class ComposerController extends BaseController
sentMailboxId: getSentMailboxIdForComposer(), sentMailboxId: getSentMailboxIdForComposer(),
draftsMailboxId: getDraftMailboxIdForComposer(), draftsMailboxId: getDraftMailboxIdForComposer(),
draftsEmailId: draftEmailId, draftsEmailId: draftEmailId,
answerForwardEmailId: composerArguments.value!.presentationEmail?.id, answerForwardEmailId: arguments.presentationEmail?.id,
unsubscribeEmailId: composerArguments.value!.previousEmailId, unsubscribeEmailId: arguments.previousEmailId,
messageId: composerArguments.value!.messageId, messageId: arguments.messageId,
references: composerArguments.value!.references, references: arguments.references,
emailSendingQueue: composerArguments.value!.sendingEmail, emailSendingQueue: arguments.sendingEmail,
displayMode: screenDisplayMode.value displayMode: screenDisplayMode.value,
uploadUri: uploadUri,
), ),
createNewAndSaveEmailToDraftsInteractor: _createNewAndSaveEmailToDraftsInteractor, createNewAndSaveEmailToDraftsInteractor: _createNewAndSaveEmailToDraftsInteractor,
onCancelSavingEmailToDraftsAction: _handleCancelSavingMessageToDrafts, onCancelSavingEmailToDraftsAction: _handleCancelSavingMessageToDrafts,
@@ -38,6 +38,7 @@ class CreateEmailRequest with EquatableMixin {
final MessageIdsHeaderValue? references; final MessageIdsHeaderValue? references;
final SendingEmail? emailSendingQueue; final SendingEmail? emailSendingQueue;
final ScreenDisplayMode displayMode; final ScreenDisplayMode displayMode;
final Uri? uploadUri;
CreateEmailRequest({ CreateEmailRequest({
required this.session, required this.session,
@@ -64,7 +65,8 @@ class CreateEmailRequest with EquatableMixin {
this.messageId, this.messageId,
this.references, this.references,
this.emailSendingQueue, this.emailSendingQueue,
this.displayMode = ScreenDisplayMode.normal this.displayMode = ScreenDisplayMode.normal,
this.uploadUri,
}); });
@override @override
@@ -94,5 +96,6 @@ class CreateEmailRequest with EquatableMixin {
references, references,
emailSendingQueue, emailSendingQueue,
displayMode, displayMode,
uploadUri,
]; ];
} }
@@ -18,7 +18,8 @@ abstract class HtmlDataSource {
Future<Tuple2<String, Set<EmailBodyPart>>> replaceImageBase64ToImageCID({ Future<Tuple2<String, Set<EmailBodyPart>>> replaceImageBase64ToImageCID({
required String emailContent, required String emailContent,
required Map<String, Attachment> inlineAttachments required Map<String, Attachment> inlineAttachments,
required Uri? uploadUri,
}); });
Future<String> removeCollapsedExpandedSignatureEffect({required String emailContent}); Future<String> removeCollapsedExpandedSignatureEffect({required String emailContent});
@@ -42,12 +42,14 @@ class HtmlDataSourceImpl extends HtmlDataSource {
@override @override
Future<Tuple2<String, Set<EmailBodyPart>>> replaceImageBase64ToImageCID({ Future<Tuple2<String, Set<EmailBodyPart>>> replaceImageBase64ToImageCID({
required String emailContent, required String emailContent,
required Map<String, Attachment> inlineAttachments required Map<String, Attachment> inlineAttachments,
required Uri? uploadUri,
}) { }) {
return Future.sync(() async { return Future.sync(() async {
return await _htmlAnalyzer.replaceImageBase64ToImageCID( return await _htmlAnalyzer.replaceImageBase64ToImageCID(
emailContent: emailContent, emailContent: emailContent,
inlineAttachments: inlineAttachments inlineAttachments: inlineAttachments,
uploadUri: uploadUri,
); );
}).catchError(_exceptionThrower.throwException); }).catchError(_exceptionThrower.throwException);
} }
@@ -1,3 +1,5 @@
import 'dart:collection';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:core/data/constants/constant.dart'; import 'package:core/data/constants/constant.dart';
import 'package:core/presentation/utils/html_transformer/html_transform.dart'; import 'package:core/presentation/utils/html_transformer/html_transform.dart';
@@ -5,6 +7,7 @@ import 'package:core/presentation/utils/html_transformer/text/persist_preformatt
import 'package:core/presentation/utils/html_transformer/text/sanitize_autolink_html_transformers.dart'; import 'package:core/presentation/utils/html_transformer/text/sanitize_autolink_html_transformers.dart';
import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; import 'package:core/presentation/utils/html_transformer/transform_configuration.dart';
import 'package:core/utils/app_logger.dart'; import 'package:core/utils/app_logger.dart';
import 'package:core/utils/string_convert.dart';
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import 'package:html/parser.dart'; import 'package:html/parser.dart';
import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart';
@@ -12,13 +15,21 @@ import 'package:model/email/attachment.dart';
import 'package:model/email/email_content.dart'; import 'package:model/email/email_content.dart';
import 'package:model/email/email_content_type.dart'; import 'package:model/email/email_content_type.dart';
import 'package:model/extensions/attachment_extension.dart'; import 'package:model/extensions/attachment_extension.dart';
import 'package:model/upload/file_info.dart';
import 'package:tmail_ui_user/features/email/domain/extensions/list_attachments_extension.dart';
import 'package:tmail_ui_user/features/email/domain/model/event_action.dart'; import 'package:tmail_ui_user/features/email/domain/model/event_action.dart';
import 'package:tmail_ui_user/features/upload/data/network/file_uploader.dart';
import 'package:tmail_ui_user/features/upload/domain/model/upload_task_id.dart';
import 'package:uuid/uuid.dart';
class HtmlAnalyzer { class HtmlAnalyzer {
static const String cidPrefixKey = 'cid:';
final HtmlTransform _htmlTransform; final HtmlTransform _htmlTransform;
final FileUploader _fileUploader;
final Uuid _uuid;
HtmlAnalyzer(this._htmlTransform); HtmlAnalyzer(this._htmlTransform, this._fileUploader, this._uuid);
Future<EmailContent> transformEmailContent( Future<EmailContent> transformEmailContent(
EmailContent emailContent, EmailContent emailContent,
@@ -112,35 +123,71 @@ class HtmlAnalyzer {
Future<Tuple2<String, Set<EmailBodyPart>>> replaceImageBase64ToImageCID({ Future<Tuple2<String, Set<EmailBodyPart>>> replaceImageBase64ToImageCID({
required String emailContent, required String emailContent,
required Map<String, Attachment> inlineAttachments required Map<String, Attachment> inlineAttachments,
required Uri? uploadUri,
}) async { }) async {
final document = parse(emailContent); final document = parse(emailContent);
final listImgTag = document.querySelectorAll('img[src^="data:image/"][id^="cid:"]'); final listImgTag = document.querySelectorAll('img[src^="data:image/"]');
log('HtmlAnalyzer::replaceImageBase64ToImageCID:listImgTagLength = ${listImgTag.length} | inlineAttachments = ${inlineAttachments.length}');
final listInlineAttachment = await Future.wait(listImgTag.map((imgTag) async { if (listImgTag.isEmpty) {
final idImg = imgTag.attributes['id']; return Tuple2(
final cid = idImg!.replaceFirst('cid:', '').trim(); emailContent,
imgTag.attributes['src'] = 'cid:$cid'; inlineAttachments.isNotEmpty
imgTag.attributes.remove('id'); ? inlineAttachments.values.toList().toEmailBodyPart(charset: Constant.base64Charset)
return cid; : {},
})).then((listCid) { );
final listInlineAttachment = listCid }
.map((cid) {
if (inlineAttachments.containsKey(cid)) {
return inlineAttachments[cid]!.toEmailBodyPart(charset: Constant.base64Charset);
} else {
return null;
}
})
.whereNotNull()
.toSet();
return listInlineAttachment; final Set<EmailBodyPart> inlineAttachmentsSet = {};
}); final List<Future<void>> asyncTasks = [];
for (final imgTag in listImgTag) {
late final LinkedHashMap<Object, String> attributes = imgTag.attributes;
final idImg = attributes['id'];
final imageSrc = attributes['src'];
if (imageSrc?.isEmpty ?? true) continue;
if (idImg?.startsWith(cidPrefixKey) == true) {
final cid = idImg!.substring(cidPrefixKey.length).trim();
attributes['src'] = '$cidPrefixKey$cid';
attributes.remove('id');
final attachment = inlineAttachments[cid];
log('HtmlAnalyzer::replaceImageBase64ToImageCID:attachment = $attachment');
if (attachment != null) {
inlineAttachmentsSet.add(attachment.toEmailBodyPart(charset: Constant.base64Charset));
}
continue;
}
if (uploadUri == null) continue;
asyncTasks.add(_retrieveAttachmentFromUpload(
uploadUri: uploadUri,
base64ImageTag: imageSrc!,
).then((newAttachment) {
if (newAttachment == null) return;
final newInlineAttachment = newAttachment.toAttachmentWithDisposition(
disposition: ContentDisposition.inline,
cid: _uuid.v1(),
);
final newCid = newInlineAttachment.cid;
inlineAttachments[newCid!] = newInlineAttachment;
attributes['src'] = '$cidPrefixKey$newCid';
inlineAttachmentsSet.add(newInlineAttachment.toEmailBodyPart(charset: Constant.base64Charset));
}));
}
if (asyncTasks.isNotEmpty) {
await Future.wait(asyncTasks);
}
final newContent = document.body?.innerHtml ?? emailContent; final newContent = document.body?.innerHtml ?? emailContent;
return Tuple2(newContent, inlineAttachmentsSet);
return Tuple2(newContent, listInlineAttachment);
} }
Future<String> removeCollapsedExpandedSignatureEffect({required String emailContent}) async { Future<String> removeCollapsedExpandedSignatureEffect({required String emailContent}) async {
@@ -162,4 +209,32 @@ class HtmlAnalyzer {
log('HtmlAnalyzer::removeCollapsedExpandedSignatureEffect: AFTER = $newContent'); log('HtmlAnalyzer::removeCollapsedExpandedSignatureEffect: AFTER = $newContent');
return newContent; return newContent;
} }
Future<Attachment?> _retrieveAttachmentFromUpload({
required Uri uploadUri,
required String base64ImageTag,
}) async {
try {
final imageBytes = StringConvert.convertBase64ImageTagToBytes(base64ImageTag);
final mediaType = StringConvert.getMediaTypeFromBase64ImageTag(base64ImageTag);
log('HtmlAnalyzer::_retrieveAttachmentFromUpload: mimeType = ${mediaType?.mimeType} | imageBytesLength = ${imageBytes.length}');
final generateId = _uuid.v1();
final fileInfo = FileInfo.fromBytes(
bytes: imageBytes,
name: '$generateId.${mediaType?.subtype ?? 'png'}',
type: mediaType?.mimeType,
);
final attachment = await _fileUploader.uploadAttachment(
UploadTaskId(generateId),
fileInfo,
uploadUri,
);
log('HtmlAnalyzer::_retrieveAttachmentFromUpload:Attachment = $attachment');
return attachment;
} catch (e) {
logError('HtmlAnalyzer::_retrieveAttachmentFromUpload:Exception = $e');
return null;
}
}
} }
@@ -13,6 +13,7 @@ extension DetailedEmailHiveCacheExtension on DetailedEmailHiveCache {
emailId: EmailId(Id(emailId)), emailId: EmailId(Id(emailId)),
createdTime: timeSaved, createdTime: timeSaved,
attachments: attachments?.toAttachment(), attachments: attachments?.toAttachment(),
emailContentPath: emailContentPath,
headers: headers?.toSetEmailHeader(), headers: headers?.toSetEmailHeader(),
keywords: keywords != null keywords: keywords != null
? Map.fromIterables(keywords!.keys.map((value) => KeyWordIdentifier(value)), keywords!.values) ? Map.fromIterables(keywords!.keys.map((value) => KeyWordIdentifier(value)), keywords!.values)
@@ -68,7 +68,7 @@ class GetEmailContentInteractor {
additionalProperties: additionalProperties); additionalProperties: additionalProperties);
final listAttachments = email.allAttachments.getListAttachmentsDisplayedOutside(email.htmlBodyAttachments); final listAttachments = email.allAttachments.getListAttachmentsDisplayedOutside(email.htmlBodyAttachments);
final listInlineImages = email.allAttachments.listAttachmentsDisplayedInContent; final listInlineImages = email.allAttachments.listAttachmentsDisplayedInContent;
log('GetEmailContentInteractor::_getContentEmailFromServer: listAttachments = ${listAttachments.length} | listInlineImages = ${listInlineImages.length}');
if (email.emailContentList.isNotEmpty) { if (email.emailContentList.isNotEmpty) {
final mapCidImageDownloadUrl = listInlineImages.toMapCidImageDownloadUrl( final mapCidImageDownloadUrl = listInlineImages.toMapCidImageDownloadUrl(
accountId: accountId, accountId: accountId,
@@ -109,8 +109,8 @@ class GetEmailContentInteractor {
{Properties? additionalProperties} {Properties? additionalProperties}
) async* { ) async* {
try { try {
log('GetEmailContentInteractor::_getStoredOpenedEmail(): CALLED');
final detailedEmail = await emailRepository.getStoredOpenedEmail(session, accountId, emailId); final detailedEmail = await emailRepository.getStoredOpenedEmail(session, accountId, emailId);
log('GetEmailContentInteractor::_getStoredOpenedEmail: attachments = ${detailedEmail.attachments?.length} | inlineImages = ${detailedEmail.inlineImages?.length}');
yield Right<Failure, Success>(GetEmailContentFromCacheSuccess( yield Right<Failure, Success>(GetEmailContentFromCacheSuccess(
htmlEmailContent: detailedEmail.htmlEmailContent ?? '', htmlEmailContent: detailedEmail.htmlEmailContent ?? '',
attachments: detailedEmail.attachments ?? [], attachments: detailedEmail.attachments ?? [],
@@ -144,8 +144,8 @@ class GetEmailContentInteractor {
{Properties? additionalProperties} {Properties? additionalProperties}
) async* { ) async* {
try { try {
log('GetEmailContentInteractor::_getStoredNewEmail():CALLED');
final detailedEmail = await emailRepository.getStoredNewEmail(session, accountId, emailId); final detailedEmail = await emailRepository.getStoredNewEmail(session, accountId, emailId);
log('GetEmailContentInteractor::_getStoredNewEmail: attachments = ${detailedEmail.attachments?.length} | inlineImages = ${detailedEmail.inlineImages?.length}');
yield Right<Failure, Success>(GetEmailContentFromCacheSuccess( yield Right<Failure, Success>(GetEmailContentFromCacheSuccess(
htmlEmailContent: detailedEmail.htmlEmailContent ?? '', htmlEmailContent: detailedEmail.htmlEmailContent ?? '',
attachments: detailedEmail.attachments ?? [], attachments: detailedEmail.attachments ?? [],
@@ -686,6 +686,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
emailId: currentEmail!.id!, emailId: currentEmail!.id!,
createdTime: currentEmail?.receivedAt?.value ?? DateTime.now(), createdTime: currentEmail?.receivedAt?.value ?? DateTime.now(),
attachments: success.attachments, attachments: success.attachments,
inlineImages: success.inlineImages,
headers: success.emailCurrent?.headers, headers: success.emailCurrent?.headers,
keywords: success.emailCurrent?.keywords, keywords: success.emailCurrent?.keywords,
htmlEmailContent: success.htmlEmailContent, htmlEmailContent: success.htmlEmailContent,
@@ -1,7 +1,5 @@
import 'package:core/data/network/dio_client.dart';
import 'package:core/data/network/download/download_client.dart'; import 'package:core/data/network/download/download_client.dart';
import 'package:core/utils/application_manager.dart'; import 'package:core/utils/application_manager.dart';
import 'package:core/utils/file_utils.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:jmap_dart_client/http/http_client.dart'; import 'package:jmap_dart_client/http/http_client.dart';
import 'package:tmail_ui_user/features/base/base_bindings.dart'; import 'package:tmail_ui_user/features/base/base_bindings.dart';
@@ -29,7 +27,6 @@ import 'package:tmail_ui_user/main/bindings/network/binding_tag.dart';
import 'package:tmail_ui_user/main/exceptions/cache_exception_thrower.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/exceptions/remote_exception_thrower.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import 'package:worker_manager/worker_manager.dart';
class PublicAssetBindings extends BaseBindings { class PublicAssetBindings extends BaseBindings {
PublicAssetBindings(this._publicAssetArguments); PublicAssetBindings(this._publicAssetArguments);
@@ -38,12 +35,6 @@ class PublicAssetBindings extends BaseBindings {
@override @override
void dependencies() { void dependencies() {
Get.lazyPut(
() => FileUploader(
Get.find<DioClient>(tag: BindingTag.isolateTag),
Get.find<Executor>(),
Get.find<FileUtils>()),
tag: BindingTag.publicAssetBindingsTag);
Get.lazyPut( Get.lazyPut(
() => PublicAssetApi(Get.find<HttpClient>(), Get.find<Uuid>()), () => PublicAssetApi(Get.find<HttpClient>(), Get.find<Uuid>()),
tag: BindingTag.publicAssetBindingsTag); tag: BindingTag.publicAssetBindingsTag);
@@ -82,7 +73,7 @@ class PublicAssetBindings extends BaseBindings {
void bindingsDataSourceImpl() { void bindingsDataSourceImpl() {
Get.lazyPut( Get.lazyPut(
() => AttachmentUploadDataSourceImpl( () => AttachmentUploadDataSourceImpl(
Get.find<FileUploader>(tag: BindingTag.publicAssetBindingsTag), Get.find<FileUploader>(),
Get.find<Uuid>(), Get.find<Uuid>(),
Get.find<RemoteExceptionThrower>()), Get.find<RemoteExceptionThrower>()),
tag: BindingTag.publicAssetBindingsTag); tag: BindingTag.publicAssetBindingsTag);
@@ -2,8 +2,8 @@
import 'package:core/data/network/dio_client.dart'; import 'package:core/data/network/dio_client.dart';
import 'package:core/utils/file_utils.dart'; import 'package:core/utils/file_utils.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:model/upload/file_info.dart';
import 'package:tmail_ui_user/features/base/isolate/background_isolate_binary_messenger/background_isolate_binary_messenger.dart'; import 'package:tmail_ui_user/features/base/isolate/background_isolate_binary_messenger/background_isolate_binary_messenger.dart';
import 'package:tmail_ui_user/features/upload/domain/model/mobile_file_upload.dart';
import 'package:tmail_ui_user/features/upload/domain/model/upload_task_id.dart'; import 'package:tmail_ui_user/features/upload/domain/model/upload_task_id.dart';
class UploadFileArguments with EquatableMixin { class UploadFileArguments with EquatableMixin {
@@ -11,7 +11,7 @@ class UploadFileArguments with EquatableMixin {
final DioClient dioClient; final DioClient dioClient;
final FileUtils fileUtils; final FileUtils fileUtils;
final UploadTaskId uploadId; final UploadTaskId uploadId;
final MobileFileUpload mobileFileUpload; final FileInfo fileInfo;
final Uri uploadUri; final Uri uploadUri;
final RootIsolateToken isolateToken; final RootIsolateToken isolateToken;
@@ -19,7 +19,7 @@ class UploadFileArguments with EquatableMixin {
this.dioClient, this.dioClient,
this.fileUtils, this.fileUtils,
this.uploadId, this.uploadId,
this.mobileFileUpload, this.fileInfo,
this.uploadUri, this.uploadUri,
this.isolateToken, this.isolateToken,
); );
@@ -29,7 +29,7 @@ class UploadFileArguments with EquatableMixin {
dioClient, dioClient,
fileUtils, fileUtils,
uploadId, uploadId,
mobileFileUpload, fileInfo,
uploadUri, uploadUri,
isolateToken, isolateToken,
]; ];
@@ -19,7 +19,6 @@ import 'package:tmail_ui_user/features/base/isolate/background_isolate_binary_me
import 'package:tmail_ui_user/features/caching/config/hive_cache_config.dart'; import 'package:tmail_ui_user/features/caching/config/hive_cache_config.dart';
import 'package:tmail_ui_user/features/upload/data/model/upload_file_arguments.dart'; import 'package:tmail_ui_user/features/upload/data/model/upload_file_arguments.dart';
import 'package:tmail_ui_user/features/upload/domain/exceptions/upload_exception.dart'; import 'package:tmail_ui_user/features/upload/domain/exceptions/upload_exception.dart';
import 'package:tmail_ui_user/features/upload/domain/extensions/file_info_extension.dart';
import 'package:tmail_ui_user/features/upload/domain/model/upload_task_id.dart'; import 'package:tmail_ui_user/features/upload/domain/model/upload_task_id.dart';
import 'package:tmail_ui_user/features/upload/domain/state/attachment_upload_state.dart'; import 'package:tmail_ui_user/features/upload/domain/state/attachment_upload_state.dart';
import 'package:tmail_ui_user/main/exceptions/isolate_exception.dart'; import 'package:tmail_ui_user/main/exceptions/isolate_exception.dart';
@@ -43,31 +42,33 @@ class FileUploader {
Future<Attachment> uploadAttachment( Future<Attachment> uploadAttachment(
UploadTaskId uploadId, UploadTaskId uploadId,
StreamController<Either<Failure, Success>> onSendController,
FileInfo fileInfo, FileInfo fileInfo,
Uri uploadUri, Uri uploadUri,
{CancelToken? cancelToken} {
CancelToken? cancelToken,
StreamController<Either<Failure, Success>>? onSendController,
}
) async { ) async {
if (PlatformInfo.isWeb) { if (PlatformInfo.isWeb) {
return _handleUploadAttachmentActionOnWeb( return _handleUploadAttachmentActionOnWeb(
uploadId, uploadId,
onSendController,
fileInfo, fileInfo,
uploadUri, uploadUri,
cancelToken: cancelToken); cancelToken: cancelToken,
onSendController: onSendController,
);
} else { } else {
final rootIsolateToken = RootIsolateToken.instance; final rootIsolateToken = RootIsolateToken.instance;
if (rootIsolateToken == null) { if (rootIsolateToken == null) {
throw CanNotGetRootIsolateToken(); throw CanNotGetRootIsolateToken();
} }
final mobileFileUpload = fileInfo.toMobileFileUpload();
return await _isolateExecutor.execute( return await _isolateExecutor.execute(
arg1: UploadFileArguments( arg1: UploadFileArguments(
_dioClient, _dioClient,
_fileUtils, _fileUtils,
uploadId, uploadId,
mobileFileUpload, fileInfo,
uploadUri, uploadUri,
rootIsolateToken, rootIsolateToken,
), ),
@@ -75,7 +76,7 @@ class FileUploader {
notification: (value) { notification: (value) {
if (value is Success) { if (value is Success) {
log('FileUploader::uploadAttachment(): onUpdateProgress: $value'); log('FileUploader::uploadAttachment(): onUpdateProgress: $value');
onSendController.add(Right(value)); onSendController?.add(Right(value));
} }
} }
) )
@@ -94,12 +95,15 @@ class FileUploader {
await HiveCacheConfig.instance.setUp(); await HiveCacheConfig.instance.setUp();
final headerParam = argsUpload.dioClient.getHeaders(); final headerParam = argsUpload.dioClient.getHeaders();
headerParam[HttpHeaders.contentTypeHeader] = argsUpload.mobileFileUpload.mimeType; headerParam[HttpHeaders.contentTypeHeader] = argsUpload.fileInfo.mimeType;
headerParam[HttpHeaders.contentLengthHeader] = argsUpload.mobileFileUpload.fileSize; headerParam[HttpHeaders.contentLengthHeader] = argsUpload.fileInfo.fileSize;
final mapExtra = <String, dynamic>{ final mapExtra = <String, dynamic>{
uploadAttachmentExtraKey: { uploadAttachmentExtraKey: {
filePathExtraKey: argsUpload.mobileFileUpload.filePath, if (argsUpload.fileInfo.filePath?.isNotEmpty == true)
filePathExtraKey: argsUpload.fileInfo.filePath,
if (argsUpload.fileInfo.bytes?.isNotEmpty == true)
streamDataExtraKey: BodyBytesStream.fromBytes(argsUpload.fileInfo.bytes!),
} }
}; };
@@ -109,31 +113,39 @@ class FileUploader {
headers: headerParam, headers: headerParam,
extra: mapExtra extra: mapExtra
), ),
data: File(argsUpload.mobileFileUpload.filePath).openRead(), data: argsUpload.fileInfo.filePath?.isNotEmpty == true
? File(argsUpload.fileInfo.filePath!).openRead()
: argsUpload.fileInfo.bytes != null
? BodyBytesStream.fromBytes(argsUpload.fileInfo.bytes!)
: null,
onSendProgress: (count, total) { onSendProgress: (count, total) {
log('FileUploader::_handleUploadAttachmentAction():onSendProgress: FILE[${argsUpload.uploadId.id}] : { PROGRESS = $count | TOTAL = $total}'); log('FileUploader::_handleUploadAttachmentAction():onSendProgress: FILE[${argsUpload.uploadId.id}] : { PROGRESS = $count | TOTAL = $total}');
sendPort.send( sendPort.send(
UploadingAttachmentUploadState( UploadingAttachmentUploadState(
argsUpload.uploadId, argsUpload.uploadId,
count, count,
argsUpload.mobileFileUpload.fileSize argsUpload.fileInfo.fileSize
) )
); );
} }
); );
log('FileUploader::_handleUploadAttachmentAction(): RESULT_JSON = $resultJson'); log('FileUploader::_handleUploadAttachmentAction(): RESULT_JSON = $resultJson');
if (argsUpload.mobileFileUpload.mimeType == FileUtils.TEXT_PLAIN_MIME_TYPE) { if (argsUpload.fileInfo.mimeType == FileUtils.TEXT_PLAIN_MIME_TYPE) {
final fileCharset = await argsUpload.fileUtils.getCharsetFromBytes( final fileBytes = argsUpload.fileInfo.filePath?.isNotEmpty == true
File(argsUpload.mobileFileUpload.filePath).readAsBytesSync() ? File(argsUpload.fileInfo.filePath!).readAsBytesSync()
); : argsUpload.fileInfo.bytes;
final fileCharset = fileBytes != null
? await argsUpload.fileUtils.getCharsetFromBytes(fileBytes)
: null;
return _parsingResponse( return _parsingResponse(
resultJson: resultJson, resultJson: resultJson,
fileName: argsUpload.mobileFileUpload.fileName, fileName: argsUpload.fileInfo.fileName,
fileCharset: fileCharset.toLowerCase()); fileCharset: fileCharset?.toLowerCase());
} else { } else {
return _parsingResponse( return _parsingResponse(
resultJson: resultJson, resultJson: resultJson,
fileName: argsUpload.mobileFileUpload.fileName); fileName: argsUpload.fileInfo.fileName);
} }
} on DioError catch (exception) { } on DioError catch (exception) {
logError('FileUploader::_handleUploadAttachmentAction():DioError: $exception'); logError('FileUploader::_handleUploadAttachmentAction():DioError: $exception');
@@ -149,10 +161,12 @@ class FileUploader {
Future<Attachment> _handleUploadAttachmentActionOnWeb( Future<Attachment> _handleUploadAttachmentActionOnWeb(
UploadTaskId uploadId, UploadTaskId uploadId,
StreamController<Either<Failure, Success>> onSendController,
FileInfo fileInfo, FileInfo fileInfo,
Uri uploadUri, Uri uploadUri,
{CancelToken? cancelToken} {
CancelToken? cancelToken,
StreamController<Either<Failure, Success>>? onSendController,
}
) async { ) async {
final headerParam = _dioClient.getHeaders(); final headerParam = _dioClient.getHeaders();
headerParam[HttpHeaders.contentTypeHeader] = fileInfo.mimeType; headerParam[HttpHeaders.contentTypeHeader] = fileInfo.mimeType;
@@ -160,7 +174,8 @@ class FileUploader {
final mapExtra = <String, dynamic>{ final mapExtra = <String, dynamic>{
uploadAttachmentExtraKey: { uploadAttachmentExtraKey: {
streamDataExtraKey: BodyBytesStream.fromBytes(fileInfo.bytes!), if (fileInfo.bytes?.isNotEmpty == true)
streamDataExtraKey: BodyBytesStream.fromBytes(fileInfo.bytes!),
} }
}; };
@@ -174,7 +189,7 @@ class FileUploader {
cancelToken: cancelToken, cancelToken: cancelToken,
onSendProgress: (count, total) { onSendProgress: (count, total) {
log('FileUploader::_handleUploadAttachmentActionOnWeb():onSendProgress: FILE[${uploadId.id}] : { PROGRESS = $count | TOTAL = $total}'); log('FileUploader::_handleUploadAttachmentActionOnWeb():onSendProgress: FILE[${uploadId.id}] : { PROGRESS = $count | TOTAL = $total}');
onSendController.add( onSendController?.add(
Right(UploadingAttachmentUploadState( Right(UploadingAttachmentUploadState(
uploadId, uploadId,
count, count,
@@ -1,17 +1,13 @@
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:model/upload/file_info.dart'; import 'package:model/upload/file_info.dart';
import 'package:tmail_ui_user/features/upload/domain/model/mobile_file_upload.dart';
extension FileInfoExtension on FileInfo { extension FileInfoExtension on FileInfo {
MobileFileUpload toMobileFileUpload() => MobileFileUpload(fileName, filePath!, fileSize, mimeType);
FileInfo withInline() => FileInfo( FileInfo withInline() => FileInfo(
fileName: fileName, fileName: fileName,
fileSize: fileSize, fileSize: fileSize,
filePath: filePath, filePath: filePath,
bytes: bytes, bytes: bytes,
readStream: readStream,
type: type, type: type,
isInline: true, isInline: true,
isShared: isShared); isShared: isShared);
@@ -21,6 +17,5 @@ extension FileInfoExtension on FileInfo {
path: filePath, path: filePath,
size: fileSize, size: fileSize,
bytes: bytes, bytes: bytes,
readStream: readStream,
); );
} }
@@ -8,6 +8,5 @@ extension PlatformFileExtension on PlatformFile {
fileSize: size, fileSize: size,
filePath: PlatformInfo.isWeb ? '' : path ?? '', filePath: PlatformInfo.isWeb ? '' : path ?? '',
bytes: bytes, bytes: bytes,
readStream: readStream
); );
} }
@@ -1,24 +0,0 @@
import 'package:equatable/equatable.dart';
class MobileFileUpload with EquatableMixin {
final String fileName;
final String filePath;
final int fileSize;
final String mimeType;
MobileFileUpload(
this.fileName,
this.filePath,
this.fileSize,
this.mimeType
);
@override
List<Object?> get props => [
fileName,
filePath,
fileSize,
mimeType,
];
}
@@ -42,10 +42,10 @@ class UploadAttachment with EquatableMixin {
final attachment = await fileUploader.uploadAttachment( final attachment = await fileUploader.uploadAttachment(
uploadTaskId, uploadTaskId,
_progressStateController,
fileInfo, fileInfo,
uploadUri, uploadUri,
cancelToken: cancelToken cancelToken: cancelToken,
onSendController: _progressStateController,
); );
log('UploadAttachment::upload: ATTACHMENT_UPLOADED = $attachment'); log('UploadAttachment::upload: ATTACHMENT_UPLOADED = $attachment');
@@ -193,24 +193,24 @@ class UploadController extends BaseController {
} }
void initializeUploadAttachments(List<Attachment> attachments) { void initializeUploadAttachments(List<Attachment> attachments) {
final listUploadFilesState = attachments _uploadingStateFiles.addAll(
.map((attachment) => UploadFileState( attachments.map((attachment) => UploadFileState(
UploadTaskId(attachment.blobId!.value), UploadTaskId(attachment.blobId!.value),
uploadStatus: UploadFileStatus.succeed, uploadStatus: UploadFileStatus.succeed,
attachment: attachment)) attachment: attachment
.toList(); ))
_uploadingStateFiles.addAll(listUploadFilesState); );
_refreshListUploadAttachmentState(); _refreshListUploadAttachmentState();
} }
void initializeUploadInlineAttachments(List<Attachment> inlineAttachments) { void initializeUploadInlineAttachments(List<Attachment> inlineAttachments) {
final listUploadInlineImagesState = inlineAttachments _uploadingStateInlineFiles.addAll(
.map((inlineAttachment) => UploadFileState( inlineAttachments.map((inlineAttachment) => UploadFileState(
UploadTaskId(inlineAttachment.blobId!.value), UploadTaskId(inlineAttachment.blobId!.value),
uploadStatus: UploadFileStatus.succeed, uploadStatus: UploadFileStatus.succeed,
attachment: inlineAttachment)) attachment: inlineAttachment,
.toList(); ))
_uploadingStateInlineFiles.addAll(listUploadInlineImagesState); );
_refreshListUploadAttachmentState(); _refreshListUploadAttachmentState();
} }
@@ -428,45 +428,44 @@ class UploadController extends BaseController {
} }
List<Attachment> get inlineAttachmentsUploaded { List<Attachment> get inlineAttachmentsUploaded {
if (_uploadingStateInlineFiles.uploadingStateFiles.isEmpty) { return _uploadingStateInlineFiles.uploadingStateFiles.fold<List<Attachment>>(
return List.empty(); [],
} (list, fileState) {
return _uploadingStateInlineFiles.uploadingStateFiles final attachment = fileState?.attachment;
.whereNotNull() if (attachment?.cid != null) {
.map((fileState) => fileState.attachment) list.add(attachment!);
.whereNotNull() }
.where((attachment) => attachment.cid != null) return list;
.toList(); },
);
} }
List<FileInfo> get inlineAttachmentsPicked { List<FileInfo> get inlineAttachmentsPicked {
if (_uploadingStateInlineFiles.uploadingStateFiles.isEmpty) { return _uploadingStateInlineFiles.uploadingStateFiles.fold<List<FileInfo>>(
return List.empty(); [],
} (list, fileState) {
return _uploadingStateInlineFiles.uploadingStateFiles final file = fileState?.file;
.whereNotNull() if (file != null) {
.map((fileState) => fileState.file) list.add(file);
.whereNotNull() }
.toList(); return list;
},
);
} }
Map<String, Attachment> get mapInlineAttachments { Map<String, Attachment> get mapInlineAttachments {
final inlineAttachments = _uploadingStateInlineFiles.uploadingStateFiles final mapInlineAttachments = _uploadingStateInlineFiles.uploadingStateFiles.fold<Map<String, Attachment>>(
.whereNotNull() {},
.map((fileState) => fileState.attachment) (map, fileState) {
.whereNotNull() final attachment = fileState?.attachment;
.where((attachment) => attachment.cid != null) if (attachment?.cid != null) {
.toList(); map[attachment!.cid!] = attachment;
}
return map;
},
);
log('UploadController::mapInlineAttachments(): $inlineAttachments'); log('UploadController::mapInlineAttachments(): Found ${mapInlineAttachments.length} inline attachments.');
if (inlineAttachments.isEmpty) {
return {};
}
final mapInlineAttachments = {
for (var item in inlineAttachments) item.cid!: item
};
return mapInlineAttachments; return mapInlineAttachments;
} }
@@ -18,7 +18,7 @@ class UploadFileStateList {
return this; return this;
} }
UploadFileStateList addAll(List<UploadFileState> elements) { UploadFileStateList addAll(Iterable<UploadFileState> elements) {
_uploadingStateFiles.addAll(elements); _uploadingStateFiles.addAll(elements);
return this; return this;
} }
@@ -37,10 +37,12 @@ import 'package:tmail_ui_user/features/push_notification/data/network/web_socket
import 'package:tmail_ui_user/features/quotas/data/network/quotas_api.dart'; import 'package:tmail_ui_user/features/quotas/data/network/quotas_api.dart';
import 'package:tmail_ui_user/features/server_settings/data/network/server_settings_api.dart'; import 'package:tmail_ui_user/features/server_settings/data/network/server_settings_api.dart';
import 'package:tmail_ui_user/features/thread/data/network/thread_api.dart'; import 'package:tmail_ui_user/features/thread/data/network/thread_api.dart';
import 'package:tmail_ui_user/features/upload/data/network/file_uploader.dart';
import 'package:tmail_ui_user/main/exceptions/remote_exception_thrower.dart'; import 'package:tmail_ui_user/main/exceptions/remote_exception_thrower.dart';
import 'package:tmail_ui_user/main/exceptions/send_email_exception_thrower.dart'; import 'package:tmail_ui_user/main/exceptions/send_email_exception_thrower.dart';
import 'package:tmail_ui_user/main/utils/ios_sharing_manager.dart'; import 'package:tmail_ui_user/main/utils/ios_sharing_manager.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import 'package:worker_manager/worker_manager.dart';
class NetworkBindings extends Bindings { class NetworkBindings extends Bindings {
@@ -84,6 +86,7 @@ class NetworkBindings extends Bindings {
Get.find<OIDCHttpClient>(), Get.find<OIDCHttpClient>(),
Get.find<MailboxCacheManager>(), Get.find<MailboxCacheManager>(),
)); ));
Get.put(Executor());
} }
void _bindingInterceptors() { void _bindingInterceptors() {
@@ -126,6 +129,11 @@ class NetworkBindings extends Bindings {
Get.put(ServerSettingsAPI(Get.find<HttpClient>())); Get.put(ServerSettingsAPI(Get.find<HttpClient>()));
Get.put(WebSocketApi(Get.find<DioClient>())); Get.put(WebSocketApi(Get.find<DioClient>()));
Get.put(LinagoraEcosystemApi(Get.find<DioClient>())); Get.put(LinagoraEcosystemApi(Get.find<DioClient>()));
Get.put(FileUploader(
Get.find<DioClient>(),
Get.find<Executor>(),
Get.find<FileUtils>(),
));
} }
void _bindingConnection() { void _bindingConnection() {
@@ -140,7 +148,11 @@ class NetworkBindings extends Bindings {
void _bindingTransformer() { void _bindingTransformer() {
Get.put(const HtmlEscape()); Get.put(const HtmlEscape());
Get.put(HtmlTransform(Get.find<DioClient>(), Get.find<HtmlEscape>())); Get.put(HtmlTransform(Get.find<DioClient>(), Get.find<HtmlEscape>()));
Get.put(HtmlAnalyzer(Get.find<HtmlTransform>())); Get.put(HtmlAnalyzer(
Get.find<HtmlTransform>(),
Get.find<FileUploader>(),
Get.find<Uuid>(),
));
} }
void _bindingServices() { void _bindingServices() {
@@ -87,7 +87,6 @@ class NetworkIsolateBindings extends Bindings {
} }
void _bindingIsolateWorker() { void _bindingIsolateWorker() {
Get.put(Executor());
Get.put(ThreadIsolateWorker( Get.put(ThreadIsolateWorker(
Get.find<ThreadAPI>(tag: BindingTag.isolateTag), Get.find<ThreadAPI>(tag: BindingTag.isolateTag),
Get.find<EmailAPI>(tag: BindingTag.isolateTag), Get.find<EmailAPI>(tag: BindingTag.isolateTag),
+1 -4
View File
@@ -7,7 +7,6 @@ class FileInfo with EquatableMixin {
final int fileSize; final int fileSize;
final String? filePath; final String? filePath;
final Uint8List? bytes; final Uint8List? bytes;
final Stream<List<int>>? readStream;
final String? type; final String? type;
final bool? isInline; final bool? isInline;
final bool? isShared; final bool? isShared;
@@ -17,7 +16,6 @@ class FileInfo with EquatableMixin {
required this.fileSize, required this.fileSize,
this.filePath, this.filePath,
this.bytes, this.bytes,
this.readStream,
this.type, this.type,
this.isInline, this.isInline,
this.isShared, this.isShared,
@@ -32,7 +30,7 @@ class FileInfo with EquatableMixin {
}) { }) {
return FileInfo( return FileInfo(
fileName: name ?? '', fileName: name ?? '',
fileSize: size ?? 0, fileSize: size ?? bytes.length,
bytes: bytes, bytes: bytes,
type: type, type: type,
isInline: isInline, isInline: isInline,
@@ -59,7 +57,6 @@ class FileInfo with EquatableMixin {
filePath, filePath,
fileSize, fileSize,
bytes, bytes,
readStream,
type, type,
isInline, isInline,
isShared, isShared,
@@ -57,6 +57,7 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller
import 'package:tmail_ui_user/features/manage_account/data/local/language_cache_manager.dart'; import 'package:tmail_ui_user/features/manage_account/data/local/language_cache_manager.dart';
import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_all_identities_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_all_identities_interactor.dart';
import 'package:tmail_ui_user/features/manage_account/domain/usecases/log_out_oidc_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/log_out_oidc_interactor.dart';
import 'package:tmail_ui_user/features/upload/data/network/file_uploader.dart';
import 'package:tmail_ui_user/main/bindings/network/binding_tag.dart'; import 'package:tmail_ui_user/main/bindings/network/binding_tag.dart';
import 'package:tmail_ui_user/main/exceptions/cache_exception_thrower.dart'; import 'package:tmail_ui_user/main/exceptions/cache_exception_thrower.dart';
import 'package:tmail_ui_user/main/utils/toast_manager.dart'; import 'package:tmail_ui_user/main/utils/toast_manager.dart';
@@ -114,6 +115,7 @@ const fallbackGenerators = {
MockSpec<TwakeAppManager>(), MockSpec<TwakeAppManager>(),
MockSpec<DownloadAllAttachmentsForWebInteractor>(), MockSpec<DownloadAllAttachmentsForWebInteractor>(),
MockSpec<ExportAllAttachmentsInteractor>(), MockSpec<ExportAllAttachmentsInteractor>(),
MockSpec<FileUploader>(),
]) ])
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
@@ -405,10 +407,14 @@ void main() {
final calendarEventRepository = CalendarEventRepositoryImpl( final calendarEventRepository = CalendarEventRepositoryImpl(
{DataSourceType.network :calendarEventDataSource}, {DataSourceType.network :calendarEventDataSource},
HtmlDataSourceImpl( HtmlDataSourceImpl(
HtmlAnalyzer(HtmlTransform( HtmlAnalyzer(
MockDioClient(), HtmlTransform(
const HtmlEscape(), MockDioClient(),
)), const HtmlEscape(),
),
MockFileUploader(),
MockUuid(),
),
CacheExceptionThrower(), CacheExceptionThrower(),
), ),
); );
@@ -45,6 +45,7 @@ import 'package:tmail_ui_user/features/manage_account/presentation/profiles/iden
import 'package:tmail_ui_user/features/public_asset/domain/model/public_assets_in_identity_arguments.dart'; import 'package:tmail_ui_user/features/public_asset/domain/model/public_assets_in_identity_arguments.dart';
import 'package:tmail_ui_user/features/public_asset/domain/usecase/create_public_asset_interactor.dart'; import 'package:tmail_ui_user/features/public_asset/domain/usecase/create_public_asset_interactor.dart';
import 'package:tmail_ui_user/features/public_asset/domain/usecase/delete_public_assets_interactor.dart'; import 'package:tmail_ui_user/features/public_asset/domain/usecase/delete_public_assets_interactor.dart';
import 'package:tmail_ui_user/features/upload/data/network/file_uploader.dart';
import 'package:tmail_ui_user/main/bindings/network/binding_tag.dart'; import 'package:tmail_ui_user/main/bindings/network/binding_tag.dart';
import 'package:tmail_ui_user/main/exceptions/remote_exception_thrower.dart'; import 'package:tmail_ui_user/main/exceptions/remote_exception_thrower.dart';
import 'package:tmail_ui_user/main/universal_import/html_stub.dart'; import 'package:tmail_ui_user/main/universal_import/html_stub.dart';
@@ -78,6 +79,7 @@ import 'identity_creator_controller_test.mocks.dart';
MockSpec<DioClient>(), MockSpec<DioClient>(),
MockSpec<Executor>(), MockSpec<Executor>(),
MockSpec<FileUtils>(), MockSpec<FileUtils>(),
MockSpec<FileUploader>(),
MockSpec<RemoteExceptionThrower>(), MockSpec<RemoteExceptionThrower>(),
MockSpec<DownloadClient>(), MockSpec<DownloadClient>(),
MockSpec<HtmlAnalyzer>(), MockSpec<HtmlAnalyzer>(),
@@ -163,6 +165,7 @@ void main() {
Get.put<DioClient>(MockDioClient(), tag: BindingTag.isolateTag); Get.put<DioClient>(MockDioClient(), tag: BindingTag.isolateTag);
Get.put<Executor>(MockExecutor()); Get.put<Executor>(MockExecutor());
Get.put<FileUtils>(MockFileUtils()); Get.put<FileUtils>(MockFileUtils());
Get.put<FileUploader>(MockFileUploader());
Get.put<RemoteExceptionThrower>(MockRemoteExceptionThrower()); Get.put<RemoteExceptionThrower>(MockRemoteExceptionThrower());
Get.put<DownloadClient>(MockDownloadClient()); Get.put<DownloadClient>(MockDownloadClient());
Get.put<HtmlAnalyzer>(MockHtmlAnalyzer()); Get.put<HtmlAnalyzer>(MockHtmlAnalyzer());