From a6ac3d6b30e5eb05c4981e845a5a20c1c0890300 Mon Sep 17 00:00:00 2001 From: dab246 Date: Thu, 9 Jan 2025 05:04:55 +0700 Subject: [PATCH 1/2] TF-3413 Support `Reply to list` for email --- .../composer/domain/model/email_request.dart | 4 +- .../presentation/composer_controller.dart | 14 ++++-- .../create_email_request_extension.dart | 2 + .../email_action_type_extension.dart | 2 + .../view/mobile/mobile_editor_view.dart | 1 + .../view/web/web_editor_view.dart | 1 + .../controller/single_email_controller.dart | 14 ++++++ .../presentation_email_extension.dart | 49 +++++++++++++++++++ .../model/composer_arguments.dart | 24 +++++++++ .../email/presentation/utils/email_utils.dart | 36 ++++++++++++++ .../widgets/email_view_bottom_bar_widget.dart | 32 ++++++++++++ lib/l10n/intl_messages.arb | 8 ++- lib/main/localizations/app_localizations.dart | 8 +++ model/lib/email/email_action_type.dart | 1 + model/lib/email/email_property.dart | 1 + model/lib/extensions/email_extension.dart | 4 ++ .../list_email_header_extension.dart | 5 ++ .../presentation_email_extension.dart | 26 ---------- .../email/parsing_email_list_post_test.dart | 48 ++++++++++++++++++ .../presentation_email_extension_test.dart | 29 ++++++++++- 20 files changed, 277 insertions(+), 32 deletions(-) create mode 100644 lib/features/email/presentation/extensions/presentation_email_extension.dart create mode 100644 test/features/email/parsing_email_list_post_test.dart diff --git a/lib/features/composer/domain/model/email_request.dart b/lib/features/composer/domain/model/email_request.dart index 9528e6a7b8..b5ba40d0e2 100644 --- a/lib/features/composer/domain/model/email_request.dart +++ b/lib/features/composer/domain/model/email_request.dart @@ -40,7 +40,9 @@ class EmailRequest with EquatableMixin { ]; bool get isEmailAnswered => emailIdAnsweredOrForwarded != null && - (emailActionType == EmailActionType.reply || emailActionType == EmailActionType.replyAll); + (emailActionType == EmailActionType.reply + || emailActionType == EmailActionType.replyToList + || emailActionType == EmailActionType.replyAll); bool get isEmailForwarded => emailIdAnsweredOrForwarded != null && emailActionType == EmailActionType.forward; } \ No newline at end of file diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index 9281901953..e5a0df67bd 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -74,6 +74,7 @@ import 'package:tmail_ui_user/features/email/domain/state/get_email_content_stat import 'package:tmail_ui_user/features/email/domain/state/transform_html_email_content_state.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/get_email_content_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/transform_html_email_content_interactor.dart'; +import 'package:tmail_ui_user/features/email/presentation/extensions/presentation_email_extension.dart'; import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.dart'; @@ -640,11 +641,15 @@ class ComposerController extends BaseController _updateStatusEmailSendButton(); break; case EmailActionType.reply: + case EmailActionType.replyToList: case EmailActionType.replyAll: + log('ComposerController::_initEmail:listPost = ${arguments.listPost}'); _initEmailAddress( presentationEmail: arguments.presentationEmail!, actionType: arguments.emailActionType, - mailboxRole: arguments.presentationEmail!.mailboxContain?.role ?? mailboxDashBoardController.selectedMailbox.value?.role + mailboxRole: arguments.presentationEmail!.mailboxContain?.role + ?? mailboxDashBoardController.selectedMailbox.value?.role, + listPost: arguments.listPost, ); _initSubjectEmail( presentationEmail: arguments.presentationEmail!, @@ -798,12 +803,15 @@ class ComposerController extends BaseController required PresentationEmail presentationEmail, required EmailActionType actionType, Role? mailboxRole, + String? listPost, }) { + log('ComposerController::_initEmailAddress:listPost = $listPost'); final recipients = presentationEmail.generateRecipientsEmailAddressForComposer( emailActionType: actionType, - mailboxRole: mailboxRole + mailboxRole: mailboxRole, + listPost: listPost, ); - final userName = mailboxDashBoardController.sessionCurrent?.username; + final userName = mailboxDashBoardController.sessionCurrent?.username; if (userName != null) { final isSender = presentationEmail.from.asList().every((element) => element.email == userName.value); if (isSender) { diff --git a/lib/features/composer/presentation/extensions/create_email_request_extension.dart b/lib/features/composer/presentation/extensions/create_email_request_extension.dart index f6de928bb1..e2dda92579 100644 --- a/lib/features/composer/presentation/extensions/create_email_request_extension.dart +++ b/lib/features/composer/presentation/extensions/create_email_request_extension.dart @@ -74,6 +74,7 @@ extension CreateEmailRequestExtension on CreateEmailRequest { MessageIdsHeaderValue? createInReplyTo() { if (emailActionType == EmailActionType.reply || + emailActionType == EmailActionType.replyToList || emailActionType == EmailActionType.replyAll ) { return messageId; @@ -83,6 +84,7 @@ extension CreateEmailRequestExtension on CreateEmailRequest { MessageIdsHeaderValue? createReferences() { if (emailActionType == EmailActionType.reply || + emailActionType == EmailActionType.replyToList || emailActionType == EmailActionType.replyAll || emailActionType == EmailActionType.forward ) { diff --git a/lib/features/composer/presentation/extensions/email_action_type_extension.dart b/lib/features/composer/presentation/extensions/email_action_type_extension.dart index b664c37175..2a5b5b510f 100644 --- a/lib/features/composer/presentation/extensions/email_action_type_extension.dart +++ b/lib/features/composer/presentation/extensions/email_action_type_extension.dart @@ -12,6 +12,7 @@ extension EmailActionTypeExtension on EmailActionType { String getSubjectComposer(BuildContext? context, String subject) { switch(this) { case EmailActionType.reply: + case EmailActionType.replyToList: case EmailActionType.replyAll: if (subject.toLowerCase().startsWith('re:')) { return subject; @@ -60,6 +61,7 @@ extension EmailActionTypeExtension on EmailActionType { final languageTag = locale.toLanguageTag(); switch(this) { case EmailActionType.reply: + case EmailActionType.replyToList: case EmailActionType.replyAll: final receivedAt = presentationEmail.receivedAt; final emailAddress = presentationEmail.from.toEscapeHtmlStringUseCommaSeparator(); diff --git a/lib/features/composer/presentation/view/mobile/mobile_editor_view.dart b/lib/features/composer/presentation/view/mobile/mobile_editor_view.dart index 1859f9026f..28c6860667 100644 --- a/lib/features/composer/presentation/view/mobile/mobile_editor_view.dart +++ b/lib/features/composer/presentation/view/mobile/mobile_editor_view.dart @@ -80,6 +80,7 @@ class MobileEditorView extends StatelessWidget with EditorViewMixin { } ); case EmailActionType.reply: + case EmailActionType.replyToList: case EmailActionType.replyAll: case EmailActionType.forward: if (contentViewState == null) { diff --git a/lib/features/composer/presentation/view/web/web_editor_view.dart b/lib/features/composer/presentation/view/web/web_editor_view.dart index f7cd0cca81..519a8245df 100644 --- a/lib/features/composer/presentation/view/web/web_editor_view.dart +++ b/lib/features/composer/presentation/view/web/web_editor_view.dart @@ -145,6 +145,7 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { } ); case EmailActionType.reply: + case EmailActionType.replyToList: case EmailActionType.replyAll: case EmailActionType.forward: if (contentViewState == null) { diff --git a/lib/features/email/presentation/controller/single_email_controller.dart b/lib/features/email/presentation/controller/single_email_controller.dart index 556c4a399b..d32c7d6143 100644 --- a/lib/features/email/presentation/controller/single_email_controller.dart +++ b/lib/features/email/presentation/controller/single_email_controller.dart @@ -1425,6 +1425,20 @@ class SingleEmailController extends BaseController with AppLoaderMixin { ) ); break; + case EmailActionType.replyToList: + log('SingleEmailController::pressEmailAction:replyToList'); + mailboxDashBoardController.goToComposer( + ComposerArguments.replyToListEmail( + presentationEmail: presentationEmail, + content: currentEmailLoaded.value?.htmlContent ?? '', + inlineImages: currentEmailLoaded.value?.inlineImages ?? [], + mailboxRole: presentationEmail.mailboxContain?.role, + messageId: currentEmailLoaded.value?.emailCurrent?.messageId, + references: currentEmailLoaded.value?.emailCurrent?.references, + listPost: currentEmailLoaded.value?.emailCurrent?.listPost, + ) + ); + break; case EmailActionType.replyAll: mailboxDashBoardController.goToComposer( ComposerArguments.replyAllEmail( diff --git a/lib/features/email/presentation/extensions/presentation_email_extension.dart b/lib/features/email/presentation/extensions/presentation_email_extension.dart new file mode 100644 index 0000000000..017c7a6323 --- /dev/null +++ b/lib/features/email/presentation/extensions/presentation_email_extension.dart @@ -0,0 +1,49 @@ + +import 'package:core/utils/app_logger.dart'; +import 'package:dartz/dartz.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:model/email/email_action_type.dart'; +import 'package:model/email/presentation_email.dart'; +import 'package:model/extensions/list_email_address_extension.dart'; +import 'package:model/mailbox/presentation_mailbox.dart'; +import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; + +extension PresentationEmailExtension on PresentationEmail { + Tuple3, List, List> generateRecipientsEmailAddressForComposer({ + required EmailActionType emailActionType, + Role? mailboxRole, + String? listPost, + }) { + switch (emailActionType) { + case EmailActionType.reply: + if (mailboxRole == PresentationMailbox.roleSent) { + return Tuple3(to.asList(), [], []); + } else { + final replyToAddress = replyTo.asList().isNotEmpty + ? replyTo.asList() + : from.asList(); + return Tuple3(replyToAddress, [], []); + } + case EmailActionType.replyToList: + final listEmailAddress = EmailUtils.parsingListPost(listPost ?? '') ?? []; + log('PresentationEmailExtension::generateRecipientsEmailAddressForComposer:listEmailAddress = $listEmailAddress'); + return Tuple3(listEmailAddress, [], []); + case EmailActionType.replyAll: + if (mailboxRole == PresentationMailbox.roleSent) { + return Tuple3(to.asList(), cc.asList(), bcc.asList()); + } else { + final senderReplyToAddress = replyTo.asList().isNotEmpty + ? replyTo.asList() + : from.asList(); + return Tuple3( + to.asList() + senderReplyToAddress, + cc.asList(), + bcc.asList(), + ); + } + default: + return Tuple3(to.asList(), cc.asList(), bcc.asList()); + } + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/model/composer_arguments.dart b/lib/features/email/presentation/model/composer_arguments.dart index 479d0e617a..8b57446194 100644 --- a/lib/features/email/presentation/model/composer_arguments.dart +++ b/lib/features/email/presentation/model/composer_arguments.dart @@ -32,6 +32,7 @@ class ComposerArguments extends RouterArguments { final ScreenDisplayMode displayMode; final List? cc; final List? bcc; + final String? listPost; ComposerArguments({ this.emailActionType = EmailActionType.compose, @@ -54,6 +55,7 @@ class ComposerArguments extends RouterArguments { this.displayMode = ScreenDisplayMode.normal, this.cc, this.bcc, + this.listPost, }); factory ComposerArguments.fromSendingEmail(SendingEmail sendingEmail) => @@ -130,6 +132,25 @@ class ComposerArguments extends RouterArguments { references: references, ); + factory ComposerArguments.replyToListEmail({ + required PresentationEmail presentationEmail, + required String content, + required List inlineImages, + Role? mailboxRole, + MessageIdsHeaderValue? messageId, + MessageIdsHeaderValue? references, + String? listPost, + }) => ComposerArguments( + emailActionType: EmailActionType.replyToList, + presentationEmail: presentationEmail, + emailContents: content, + inlineImages: inlineImages, + mailboxRole: mailboxRole, + messageId: messageId, + references: references, + listPost: listPost, + ); + factory ComposerArguments.replyAllEmail({ required PresentationEmail presentationEmail, required String content, @@ -206,6 +227,7 @@ class ComposerArguments extends RouterArguments { displayMode, cc, bcc, + listPost, ]; ComposerArguments copyWith({ @@ -229,6 +251,7 @@ class ComposerArguments extends RouterArguments { ScreenDisplayMode? displayMode, List? cc, List? bcc, + String? listPost, }) { return ComposerArguments( emailActionType: emailActionType ?? this.emailActionType, @@ -251,6 +274,7 @@ class ComposerArguments extends RouterArguments { displayMode: displayMode ?? this.displayMode, cc: cc ?? this.cc, bcc: bcc ?? this.bcc, + listPost: listPost ?? this.listPost, ); } } diff --git a/lib/features/email/presentation/utils/email_utils.dart b/lib/features/email/presentation/utils/email_utils.dart index d4eb188455..964af35c2c 100644 --- a/lib/features/email/presentation/utils/email_utils.dart +++ b/lib/features/email/presentation/utils/email_utils.dart @@ -9,10 +9,12 @@ import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.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/mail/email/email_address.dart'; import 'package:tmail_ui_user/features/email/domain/state/download_attachment_for_web_state.dart'; import 'package:tmail_ui_user/features/email/presentation/model/email_unsubscribe.dart'; import 'package:tmail_ui_user/features/thread/domain/constants/thread_constants.dart'; import 'package:tmail_ui_user/main/error/capability_validator.dart'; +import 'package:tmail_ui_user/main/routes/route_utils.dart'; class EmailUtils { @@ -83,4 +85,38 @@ class EmailUtils { return false; } } + + static List? parsingListPost(String listPost) { + try { + if (listPost.isEmpty) { + return null; + } + + final regExpMailtoLinks = RegExp(r'mailto:([^>,]*)'); + final allMatchesMailtoLinks = regExpMailtoLinks.allMatches(listPost); + final listMailtoLinks = allMatchesMailtoLinks + .map((match) => match.group(0)) + .whereNotNull() + .toList(); + log('EmailUtils::parsingListPost:listMailtoLinks: $listMailtoLinks'); + + if (listMailtoLinks.isNotEmpty) { + return listMailtoLinks + .map((mailto) { + final mapMailto = RouteUtils.parseMapMailtoFromUri(mailto); + final emailAddress = mapMailto[RouteUtils.paramMailtoAddress]; + return emailAddress != null + ? EmailAddress(null, emailAddress) + : null; + }) + .whereNotNull() + .toList(); + } else { + return null; + } + } catch (e) { + logError('EmailUtils::parsingListPost:Exception = $e'); + return null; + } + } } \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/email_view_bottom_bar_widget.dart b/lib/features/email/presentation/widgets/email_view_bottom_bar_widget.dart index 381ab3f984..4343695aab 100644 --- a/lib/features/email/presentation/widgets/email_view_bottom_bar_widget.dart +++ b/lib/features/email/presentation/widgets/email_view_bottom_bar_widget.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:model/email/email_action_type.dart'; import 'package:model/email/presentation_email.dart'; +import 'package:model/extensions/email_extension.dart'; import 'package:model/extensions/presentation_email_extension.dart'; import 'package:tmail_ui_user/features/email/presentation/controller/single_email_controller.dart'; import 'package:tmail_ui_user/features/email/presentation/styles/email_view_bottom_bar_widget_styles.dart'; @@ -73,6 +74,37 @@ class EmailViewBottomBarWidget extends StatelessWidget { return const SizedBox.shrink(); } }), + Obx(() { + final currentEmailLoaded = _singleEmailController.currentEmailLoaded.value; + + if (currentEmailLoaded != null && + currentEmailLoaded.emailCurrent?.hasListPost == true) { + return Expanded( + child: TMailButtonWidget( + key: const Key('reply_to_list_email_button'), + text: AppLocalizations.of(context).replyToList, + icon: _imagePaths.icReply, + borderRadius: EmailViewBottomBarWidgetStyles.buttonRadius, + iconSize: EmailViewBottomBarWidgetStyles.buttonIconSize, + textAlign: TextAlign.center, + flexibleText: true, + padding: EmailViewBottomBarWidgetStyles.buttonPadding, + backgroundColor: EmailViewBottomBarWidgetStyles.buttonBackgroundColor, + textStyle: EmailViewBottomBarWidgetStyles.getButtonTextStyle( + context, + _responsiveUtils, + ), + verticalDirection: _responsiveUtils.isPortraitMobile(context), + onTapActionCallback: () => emailActionCallback.call( + EmailActionType.replyToList, + presentationEmail, + ), + ), + ); + } else { + return const SizedBox.shrink(); + } + }), Obx(() { if (_singleEmailController.currentEmailLoaded.value != null) { return Expanded( diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index c86038fb03..61d952d41a 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2024-12-31T12:11:05.777668", + "@@last_modified": "2025-01-09T04:35:09.125475", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -4083,5 +4083,11 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "replyToList": "Reply to list", + "@replyToList": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index 0af16a8f84..ffaf432c0c 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -4286,4 +4286,12 @@ class AppLocalizations { name: 'getHelpOrReportABug', ); } + + String get replyToList { + return Intl.message( + 'Reply to list', + name: 'replyToList', + ); + } + } diff --git a/model/lib/email/email_action_type.dart b/model/lib/email/email_action_type.dart index a125b4c3ae..c158c2dc93 100644 --- a/model/lib/email/email_action_type.dart +++ b/model/lib/email/email_action_type.dart @@ -1,6 +1,7 @@ enum EmailActionType { reply, + replyToList, forward, replyAll, compose, diff --git a/model/lib/email/email_property.dart b/model/lib/email/email_property.dart index 28d3e42649..c8d0e4e3a5 100644 --- a/model/lib/email/email_property.dart +++ b/model/lib/email/email_property.dart @@ -24,4 +24,5 @@ class EmailProperty { static const String references = 'references'; static const String headerUnsubscribeKey = 'List-Unsubscribe'; static const String headerSMimeStatusKey = 'X-SMIME-Status'; + static const String headerListPostKey = 'List-Post'; } \ No newline at end of file diff --git a/model/lib/extensions/email_extension.dart b/model/lib/extensions/email_extension.dart index dca4929de6..e6002557fb 100644 --- a/model/lib/extensions/email_extension.dart +++ b/model/lib/extensions/email_extension.dart @@ -36,6 +36,10 @@ extension EmailExtension on Email { String get sMimeStatusHeaderParsed => sMimeStatusHeader?[IndividualHeaderIdentifier.sMimeStatusHeader]?.trim() ?? ''; + String get listPost => headers.listPost; + + bool get hasListPost => listPost.isNotEmpty; + IdentityId? get identityIdFromHeader { final rawIdentityId = identityHeader?[IndividualHeaderIdentifier.identityHeader]; if (rawIdentityId == null) return null; diff --git a/model/lib/extensions/list_email_header_extension.dart b/model/lib/extensions/list_email_header_extension.dart index 8121e9b522..c3be896738 100644 --- a/model/lib/extensions/list_email_header_extension.dart +++ b/model/lib/extensions/list_email_header_extension.dart @@ -24,4 +24,9 @@ extension ListEmailHeaderExtension on Set? { logger.log('ListEmailHeaderExtension::sMimeStatus: $sMimeStatus'); return sMimeStatus?.value.trim() ?? ''; } + + String get listPost { + final listPost = this?.firstWhereOrNull((header) => header.name == EmailProperty.headerListPostKey); + return listPost?.value ?? ''; + } } \ No newline at end of file diff --git a/model/lib/extensions/presentation_email_extension.dart b/model/lib/extensions/presentation_email_extension.dart index f329e164ab..09b4827867 100644 --- a/model/lib/extensions/presentation_email_extension.dart +++ b/model/lib/extensions/presentation_email_extension.dart @@ -6,11 +6,9 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/utils/app_logger.dart'; import 'package:http_parser/http_parser.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; -import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; -import 'package:model/email/email_action_type.dart'; import 'package:model/email/eml_attachment.dart'; import 'package:model/email/presentation_email.dart'; import 'package:model/extensions/email_address_extension.dart'; @@ -133,30 +131,6 @@ extension PresentationEmailExtension on PresentationEmail { return allEmailAddress.isNotEmpty ? allEmailAddress.join(', ') : ''; } - Tuple3, List, List> generateRecipientsEmailAddressForComposer({ - required EmailActionType emailActionType, - Role? mailboxRole - }) { - switch(emailActionType) { - case EmailActionType.reply: - if (mailboxRole == PresentationMailbox.roleSent) { - return Tuple3(to.asList(), [], []); - } else { - final replyToAddress = replyTo.asList().isNotEmpty ? replyTo.asList() : from.asList(); - return Tuple3(replyToAddress, [], []); - } - case EmailActionType.replyAll: - if (mailboxRole == PresentationMailbox.roleSent) { - return Tuple3(to.asList(), cc.asList(), bcc.asList()); - } else { - final senderReplyToAddress = replyTo.asList().isNotEmpty ? replyTo.asList() : from.asList(); - return Tuple3(to.asList() + senderReplyToAddress, cc.asList(), bcc.asList()); - } - default: - return Tuple3(to.asList(), cc.asList(), bcc.asList()); - } - } - PresentationEmail toSearchPresentationEmail(Map mapMailboxes) { mailboxIds?.removeWhere((key, value) => !value); diff --git a/test/features/email/parsing_email_list_post_test.dart b/test/features/email/parsing_email_list_post_test.dart new file mode 100644 index 0000000000..a0fec2b20c --- /dev/null +++ b/test/features/email/parsing_email_list_post_test.dart @@ -0,0 +1,48 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:model/extensions/list_email_address_extension.dart'; +import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; + +void main() { + group('parsing email list post test', () { + test('parsingListPost returns null for empty input', () { + expect(EmailUtils.parsingListPost(''), isNull); + }); + + test('parsingListPost returns null for input without links', () { + expect(EmailUtils.parsingListPost('Some text without links'), isNull); + }); + + test('parsingListPost parses mailto links', () { + final listEmailAddress = + EmailUtils.parsingListPost(''); + expect(listEmailAddress, isNotNull); + expect(listEmailAddress![0].email, contains('user@example.com')); + }); + + test('parsingListPost parses both mailto without <>', () { + final listEmailAddress = + EmailUtils.parsingListPost('mailto:support@example.com'); + expect(listEmailAddress, isNotNull); + expect(listEmailAddress![0].email, contains('support@example.com')); + }); + + test('parsingListPost parses more mailto', () { + final listEmailAddress = EmailUtils.parsingListPost( + ', , '); + expect(listEmailAddress, isNotNull); + expect(listEmailAddress!.length, equals(3)); + expect( + listEmailAddress.asSetAddress(), + containsAll({ + 'support@example.com', + 'support@example2.com', + 'support@example3.com' + }), + ); + }); + + test('parsingListPost returns null for input with invalid links', () { + expect(EmailUtils.parsingListPost('Invalid link: invalid'), isNull); + }); + }); +} diff --git a/test/model/lib/extensions/presentation_email_extension_test.dart b/test/model/lib/extensions/presentation_email_extension_test.dart index f8e964f8b5..7c83980fc5 100644 --- a/test/model/lib/extensions/presentation_email_extension_test.dart +++ b/test/model/lib/extensions/presentation_email_extension_test.dart @@ -3,8 +3,9 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:model/email/email_action_type.dart'; import 'package:model/email/presentation_email.dart'; -import 'package:model/extensions/presentation_email_extension.dart'; +import 'package:model/extensions/email_address_extension.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; +import 'package:tmail_ui_user/features/email/presentation/extensions/presentation_email_extension.dart'; void main() { group('presentation email extension test', () { @@ -14,6 +15,7 @@ void main() { final userDEmailAddress = EmailAddress('User D', 'userD@domain.com'); final userEEmailAddress = EmailAddress('User E', 'userE@domain.com'); final replyToEmailAddress = EmailAddress('Reply To', 'replyToThis@domain.com'); + final replyToListEmailAddress = EmailAddress(null, 'replyToList@domain.com'); group('GIVEN user A is the sender AND send an email to user B and user E, cc to user C, bcc to user D', () { test('THEN user A click reply, generateRecipientsEmailAddressForComposer SHOULD return user B email + user E email to reply', () { @@ -141,6 +143,31 @@ void main() { expect(result.value2, containsAll(expectedResult.value2)); expect(result.value3, containsAll(expectedResult.value3)); }); + + test( + 'THEN user A click reply to list, generateRecipientsEmailAddressForComposer\n' + 'SHOULD return email address in mailto of List-Post to reply\n', + () { + final expectedResult = Tuple3([replyToListEmailAddress], [], []); + + final emailToReplyToList = PresentationEmail( + from: {userBEmailAddress}, + replyTo: {replyToEmailAddress}, + to: {userAEmailAddress, userEEmailAddress}, + cc: {userCEmailAddress}, + bcc: {userDEmailAddress}, + ); + + final result = emailToReplyToList.generateRecipientsEmailAddressForComposer( + emailActionType: EmailActionType.replyToList, + mailboxRole: PresentationMailbox.roleInbox, + listPost: '', + ); + + expect(result.value1, containsAll(expectedResult.value1)); + expect(result.value2, containsAll(expectedResult.value2)); + expect(result.value3, containsAll(expectedResult.value3)); + }); }); group('Given user A is the sender AND send an email to user B + user E, cc to user C, bcc to user D THEN user B click forward', () { From 39fd0b79191c49ea75159aab03af2a9bd5ebc46d Mon Sep 17 00:00:00 2001 From: dab246 Date: Thu, 9 Jan 2025 05:48:56 +0700 Subject: [PATCH 2/2] TF-3413 Fix `ReplyAll` & `Reply` is buggy --- .../presentation/composer_controller.dart | 34 ++-- .../controller/single_email_controller.dart | 2 + .../presentation_email_extension.dart | 98 ++++++--- .../model/composer_arguments.dart | 4 + .../email/presentation/utils/email_utils.dart | 123 +++++++++--- .../list_email_address_extension.dart | 19 ++ .../presentation_email_extension.dart | 14 +- .../email/extract_email_list_post_test.dart | 186 ++++++++++++++++++ .../email/parsing_email_list_post_test.dart | 48 ----- .../presentation_email_extension_test.dart | 21 +- 10 files changed, 407 insertions(+), 142 deletions(-) create mode 100644 test/features/email/extract_email_list_post_test.dart delete mode 100644 test/features/email/parsing_email_list_post_test.dart diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index e5a0df67bd..2e4887c67f 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -23,7 +23,6 @@ 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_address.dart'; import 'package:jmap_dart_client/jmap/mail/email/individual_header_identifier.dart'; -import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/model.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; @@ -647,8 +646,6 @@ class ComposerController extends BaseController _initEmailAddress( presentationEmail: arguments.presentationEmail!, actionType: arguments.emailActionType, - mailboxRole: arguments.presentationEmail!.mailboxContain?.role - ?? mailboxDashBoardController.selectedMailbox.value?.role, listPost: arguments.listPost, ); _initSubjectEmail( @@ -802,32 +799,23 @@ class ComposerController extends BaseController void _initEmailAddress({ required PresentationEmail presentationEmail, required EmailActionType actionType, - Role? mailboxRole, String? listPost, }) { - log('ComposerController::_initEmailAddress:listPost = $listPost'); + final userName = mailboxDashBoardController.sessionCurrent?.username.value; + final isSender = presentationEmail.from + .asList() + .any((element) => element.emailAddress.isNotEmpty && element.emailAddress == userName); + final recipients = presentationEmail.generateRecipientsEmailAddressForComposer( emailActionType: actionType, - mailboxRole: mailboxRole, + isSender: isSender, + userName: userName, listPost: listPost, ); - final userName = mailboxDashBoardController.sessionCurrent?.username; - if (userName != null) { - final isSender = presentationEmail.from.asList().every((element) => element.email == userName.value); - if (isSender) { - listToEmailAddress = List.from(recipients.value1.toSet()); - listCcEmailAddress = List.from(recipients.value2.toSet()); - listBccEmailAddress = List.from(recipients.value3.toSet()); - } else { - listToEmailAddress = List.from(recipients.value1.toSet().filterEmailAddress(userName.value)); - listCcEmailAddress = List.from(recipients.value2.toSet().filterEmailAddress(userName.value)); - listBccEmailAddress = List.from(recipients.value3.toSet().filterEmailAddress(userName.value)); - } - } else { - listToEmailAddress = List.from(recipients.value1.toSet()); - listCcEmailAddress = List.from(recipients.value2.toSet()); - listBccEmailAddress = List.from(recipients.value3.toSet()); - } + + listToEmailAddress = List.from(recipients.value1); + listCcEmailAddress = List.from(recipients.value2); + listBccEmailAddress = List.from(recipients.value3); if (listToEmailAddress.isNotEmpty || listCcEmailAddress.isNotEmpty || listBccEmailAddress.isNotEmpty) { isInitialRecipient.value = true; diff --git a/lib/features/email/presentation/controller/single_email_controller.dart b/lib/features/email/presentation/controller/single_email_controller.dart index d32c7d6143..7b3194df81 100644 --- a/lib/features/email/presentation/controller/single_email_controller.dart +++ b/lib/features/email/presentation/controller/single_email_controller.dart @@ -1422,6 +1422,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { mailboxRole: presentationEmail.mailboxContain?.role, messageId: currentEmailLoaded.value?.emailCurrent?.messageId, references: currentEmailLoaded.value?.emailCurrent?.references, + listPost: currentEmailLoaded.value?.emailCurrent?.listPost, ) ); break; @@ -1448,6 +1449,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { mailboxRole: presentationEmail.mailboxContain?.role, messageId: currentEmailLoaded.value?.emailCurrent?.messageId, references: currentEmailLoaded.value?.emailCurrent?.references, + listPost: currentEmailLoaded.value?.emailCurrent?.listPost, ) ); break; diff --git a/lib/features/email/presentation/extensions/presentation_email_extension.dart b/lib/features/email/presentation/extensions/presentation_email_extension.dart index 017c7a6323..a2427a3101 100644 --- a/lib/features/email/presentation/extensions/presentation_email_extension.dart +++ b/lib/features/email/presentation/extensions/presentation_email_extension.dart @@ -1,49 +1,91 @@ -import 'package:core/utils/app_logger.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; -import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/email/email_action_type.dart'; import 'package:model/email/presentation_email.dart'; import 'package:model/extensions/list_email_address_extension.dart'; -import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; extension PresentationEmailExtension on PresentationEmail { Tuple3, List, List> generateRecipientsEmailAddressForComposer({ required EmailActionType emailActionType, - Role? mailboxRole, + bool isSender = false, + String? userName, String? listPost, }) { + final newFromAddress = from.removeDuplicateEmails(); + final newToAddress = to.removeDuplicateEmails(); + final newCcAddress = cc.removeDuplicateEmails(); + final newBccAddress = bcc.removeDuplicateEmails(); + final newReplyToAddress = replyTo.removeDuplicateEmails(); + switch (emailActionType) { case EmailActionType.reply: - if (mailboxRole == PresentationMailbox.roleSent) { - return Tuple3(to.asList(), [], []); - } else { - final replyToAddress = replyTo.asList().isNotEmpty - ? replyTo.asList() - : from.asList(); - return Tuple3(replyToAddress, [], []); - } + final listReplyAddress = isSender ? newToAddress : newFromAddress; + final listReplyAddressWithoutUsername = listReplyAddress.withoutMe(userName); + + return Tuple3(listReplyAddressWithoutUsername, [], []); case EmailActionType.replyToList: - final listEmailAddress = EmailUtils.parsingListPost(listPost ?? '') ?? []; - log('PresentationEmailExtension::generateRecipientsEmailAddressForComposer:listEmailAddress = $listEmailAddress'); - return Tuple3(listEmailAddress, [], []); + final recipientRecord = EmailUtils.extractRecipientsFromListPost(listPost ?? ''); + + final listToAddressWithoutUsername = recipientRecord.toMailAddresses + .toSet() + .removeDuplicateEmails() + .withoutMe(userName); + + final listCcAddressWithoutUsername = recipientRecord.ccMailAddresses + .toSet() + .removeDuplicateEmails() + .withoutMe(userName); + + final listBccAddressWithoutUsername = recipientRecord.bccMailAddresses + .toSet() + .removeDuplicateEmails() + .withoutMe(userName); + + return Tuple3( + listToAddressWithoutUsername, + listCcAddressWithoutUsername, + listBccAddressWithoutUsername, + ); case EmailActionType.replyAll: - if (mailboxRole == PresentationMailbox.roleSent) { - return Tuple3(to.asList(), cc.asList(), bcc.asList()); - } else { - final senderReplyToAddress = replyTo.asList().isNotEmpty - ? replyTo.asList() - : from.asList(); - return Tuple3( - to.asList() + senderReplyToAddress, - cc.asList(), - bcc.asList(), - ); - } + final recipientRecord = EmailUtils.extractRecipientsFromListPost(listPost ?? ''); + + final listToAddress = recipientRecord.toMailAddresses + + newReplyToAddress + + newFromAddress + + newToAddress; + final listCcAddress = recipientRecord.ccMailAddresses + newCcAddress; + final listBccAddress = recipientRecord.bccMailAddresses + newBccAddress; + + final listToAddressWithoutUsername = listToAddress + .toSet() + .removeDuplicateEmails() + .withoutMe(userName); + final listCcAddressWithoutUsername = listCcAddress + .toSet() + .removeDuplicateEmails() + .withoutMe(userName); + final listBccAddressWithoutUsername = listBccAddress + .toSet() + .removeDuplicateEmails() + .withoutMe(userName); + + return Tuple3( + listToAddressWithoutUsername, + listCcAddressWithoutUsername, + listBccAddressWithoutUsername, + ); default: - return Tuple3(to.asList(), cc.asList(), bcc.asList()); + final listToAddressWithoutUsername = newToAddress.withoutMe(userName); + final listCcAddressWithoutUsername = newCcAddress.withoutMe(userName); + final listBccAddressWithoutUsername = newBccAddress.withoutMe(userName); + + return Tuple3( + listToAddressWithoutUsername, + listCcAddressWithoutUsername, + listBccAddressWithoutUsername, + ); } } } \ No newline at end of file diff --git a/lib/features/email/presentation/model/composer_arguments.dart b/lib/features/email/presentation/model/composer_arguments.dart index 8b57446194..25ce2fe911 100644 --- a/lib/features/email/presentation/model/composer_arguments.dart +++ b/lib/features/email/presentation/model/composer_arguments.dart @@ -122,6 +122,7 @@ class ComposerArguments extends RouterArguments { Role? mailboxRole, MessageIdsHeaderValue? messageId, MessageIdsHeaderValue? references, + String? listPost, }) => ComposerArguments( emailActionType: EmailActionType.reply, presentationEmail: presentationEmail, @@ -130,6 +131,7 @@ class ComposerArguments extends RouterArguments { mailboxRole: mailboxRole, messageId: messageId, references: references, + listPost: listPost, ); factory ComposerArguments.replyToListEmail({ @@ -158,6 +160,7 @@ class ComposerArguments extends RouterArguments { Role? mailboxRole, MessageIdsHeaderValue? messageId, MessageIdsHeaderValue? references, + String? listPost, }) => ComposerArguments( emailActionType: EmailActionType.replyAll, presentationEmail: presentationEmail, @@ -166,6 +169,7 @@ class ComposerArguments extends RouterArguments { mailboxRole: mailboxRole, messageId: messageId, references: references, + listPost: listPost, ); factory ComposerArguments.forwardEmail({ diff --git a/lib/features/email/presentation/utils/email_utils.dart b/lib/features/email/presentation/utils/email_utils.dart index 964af35c2c..17b8e648b8 100644 --- a/lib/features/email/presentation/utils/email_utils.dart +++ b/lib/features/email/presentation/utils/email_utils.dart @@ -86,37 +86,108 @@ class EmailUtils { } } - static List? parsingListPost(String listPost) { + static List extractMailtoLinksFromListPost(String listPost) { try { - if (listPost.isEmpty) { - return null; + if (listPost.trim().isEmpty) return []; + + final decodedInput = Uri.decodeComponent(listPost); + + final mailtoRegex = RegExp(r'<(mailto:[^<>]+)>'); + + final matches = mailtoRegex.allMatches(decodedInput); + + if (matches.isEmpty) { + log('EmailUtils::extractMailtoLinksFromListPost: Not found mailto link'); + return []; + } + + return matches.map((match) => match.group(1)!).toList(); + } catch (e) { + logError('EmailUtils::extractMailtoLinksFromListPost:Exception = $e'); + return []; + } + } + + static ({ + List toMailAddresses, + List ccMailAddresses, + List bccMailAddresses, + }) extractRecipientsFromListMailtoLink(List mailtoLinks) { + try { + log('EmailUtils::extractRecipientsFromListMailtoLink: mailtoLinks: $mailtoLinks:'); + if (mailtoLinks.isEmpty) { + return ( + toMailAddresses: [], + ccMailAddresses: [], + bccMailAddresses: [], + ); } - final regExpMailtoLinks = RegExp(r'mailto:([^>,]*)'); - final allMatchesMailtoLinks = regExpMailtoLinks.allMatches(listPost); - final listMailtoLinks = allMatchesMailtoLinks - .map((match) => match.group(0)) - .whereNotNull() - .toList(); - log('EmailUtils::parsingListPost:listMailtoLinks: $listMailtoLinks'); - - if (listMailtoLinks.isNotEmpty) { - return listMailtoLinks - .map((mailto) { - final mapMailto = RouteUtils.parseMapMailtoFromUri(mailto); - final emailAddress = mapMailto[RouteUtils.paramMailtoAddress]; - return emailAddress != null - ? EmailAddress(null, emailAddress) - : null; - }) - .whereNotNull() - .toList(); - } else { - return null; + final toMailAddresses = []; + final ccMailAddresses = []; + final bccMailAddresses = []; + + for (var mailtoLink in mailtoLinks) { + final recipientRecord = extractRecipientsFromMailtoLink(mailtoLink); + toMailAddresses.addAll(recipientRecord.toMailAddresses); + ccMailAddresses.addAll(recipientRecord.ccMailAddresses); + bccMailAddresses.addAll(recipientRecord.bccMailAddresses); } + + return ( + toMailAddresses: toMailAddresses, + ccMailAddresses: ccMailAddresses, + bccMailAddresses: bccMailAddresses + ); } catch (e) { - logError('EmailUtils::parsingListPost:Exception = $e'); - return null; + logError('EmailUtils::extractRecipientsFromListMailtoLink:Exception = $e'); + return ( + toMailAddresses: [], + ccMailAddresses: [], + bccMailAddresses: [], + ); } } + + static ({ + List toMailAddresses, + List ccMailAddresses, + List bccMailAddresses, + }) extractRecipientsFromMailtoLink(String mailtoLink) { + try { + log('EmailUtils::extractRecipientsFromMailtoLink:mailtoLink: $mailtoLink:'); + if (mailtoLink.isEmpty) { + return ( + toMailAddresses: [], + ccMailAddresses: [], + bccMailAddresses: [], + ); + } + + final navigationRouter = + RouteUtils.generateNavigationRouterFromMailtoLink(mailtoLink); + log('EmailUtils::extractRecipientsFromMailtoLink:navigationRouter = $navigationRouter'); + return ( + toMailAddresses: navigationRouter.listEmailAddress ?? [], + ccMailAddresses: navigationRouter.cc ?? [], + bccMailAddresses: navigationRouter.bcc ?? [], + ); + } catch (e) { + logError('EmailUtils::extractRecipientsFromMailtoLink:Exception = $e'); + return ( + toMailAddresses: [], + ccMailAddresses: [], + bccMailAddresses: [], + ); + } + } + + static ({ + List toMailAddresses, + List ccMailAddresses, + List bccMailAddresses, + }) extractRecipientsFromListPost(String listPost) { + final mailtoLinks = extractMailtoLinksFromListPost(listPost); + return extractRecipientsFromListMailtoLink(mailtoLinks); + } } \ No newline at end of file diff --git a/model/lib/extensions/list_email_address_extension.dart b/model/lib/extensions/list_email_address_extension.dart index ebece29a4f..8f6416794a 100644 --- a/model/lib/extensions/list_email_address_extension.dart +++ b/model/lib/extensions/list_email_address_extension.dart @@ -51,8 +51,27 @@ extension SetEmailAddressExtension on Set? { Set withoutMe(String userName) { return filterEmailAddress(userName).toSet(); } + + List removeDuplicateEmails() { + final seenEmails = {}; + return this?.where((emailAddress) { + if (emailAddress.emailAddress.isEmpty || + seenEmails.contains(emailAddress.emailAddress)) { + return false; + } else { + seenEmails.add(emailAddress.emailAddress); + return true; + } + }).toList() ?? []; + } } extension ListEmailAddressExtension on List { Set asSetAddress() => map((emailAddress) => emailAddress.emailAddress).toSet(); + + List withoutMe(String? userName) { + if (userName == null) return this; + + return where((emailAddress) => emailAddress.emailAddress != userName).toList(); + } } \ No newline at end of file diff --git a/model/lib/extensions/presentation_email_extension.dart b/model/lib/extensions/presentation_email_extension.dart index 09b4827867..9d6912aaa4 100644 --- a/model/lib/extensions/presentation_email_extension.dart +++ b/model/lib/extensions/presentation_email_extension.dart @@ -52,7 +52,7 @@ extension PresentationEmailExtension on PresentationEmail { PresentationEmail toggleSelect() { return PresentationEmail( - id: this.id, + id: id, blobId: blobId, keywords: keywords, size: size, @@ -78,7 +78,7 @@ extension PresentationEmailExtension on PresentationEmail { PresentationEmail toSelectedEmail({required SelectMode selectMode}) { return PresentationEmail( - id: this.id, + id: id, blobId: blobId, keywords: keywords, size: size, @@ -104,7 +104,7 @@ extension PresentationEmailExtension on PresentationEmail { Email toEmail() { return Email( - id: this.id, + id: id, blobId: blobId, keywords: keywords, size: size, @@ -137,7 +137,7 @@ extension PresentationEmailExtension on PresentationEmail { final matchedMailbox = findMailboxContain(mapMailboxes); return PresentationEmail( - id: this.id, + id: id, blobId: blobId, keywords: keywords, size: size, @@ -176,7 +176,7 @@ extension PresentationEmailExtension on PresentationEmail { PresentationEmail withRouteWeb(Uri routeWeb) { return PresentationEmail( - id: this.id, + id: id, blobId: blobId, keywords: keywords, size: size, @@ -205,7 +205,7 @@ extension PresentationEmailExtension on PresentationEmail { combinedMap.removeWhere((key, value) => !value); log('PresentationEmailExtension::updateKeywords:combinedMap = $combinedMap'); return PresentationEmail( - id: this.id, + id: id, blobId: blobId, keywords: combinedMap, size: size, @@ -231,7 +231,7 @@ extension PresentationEmailExtension on PresentationEmail { PresentationEmail syncPresentationEmail({PresentationMailbox? mailboxContain, Uri? routeWeb}) { return PresentationEmail( - id: this.id, + id: id, blobId: blobId, keywords: keywords, size: size, diff --git a/test/features/email/extract_email_list_post_test.dart b/test/features/email/extract_email_list_post_test.dart new file mode 100644 index 0000000000..82626f6421 --- /dev/null +++ b/test/features/email/extract_email_list_post_test.dart @@ -0,0 +1,186 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; +import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; + +void main() { + group('EmailUtils::extractMailtoLinksFromListPost::', () { + test('should return a list of mailto links when valid mailto links are present', () { + const listPost = ', '; + final result = EmailUtils.extractMailtoLinksFromListPost(listPost); + + expect(result, ['mailto:example1@test.com', 'mailto:example2@test.com']); + }); + + test('should return an empty list when no mailto links are present', () { + const listPost = ', '; + final result = EmailUtils.extractMailtoLinksFromListPost(listPost); + + expect(result, isEmpty); + }); + + test('should return an empty list for an empty string input', () { + const listPost = ''; + final result = EmailUtils.extractMailtoLinksFromListPost(listPost); + + expect(result, isEmpty); + }); + + test('should handle inputs with whitespace only', () { + const listPost = ' '; + final result = EmailUtils.extractMailtoLinksFromListPost(listPost); + + expect(result, isEmpty); + }); + + test('should handle unexpected exceptions and return an empty list', () { + const listPost = '\uD800'; + final result = EmailUtils.extractMailtoLinksFromListPost(listPost); + + expect(result, isEmpty); + }); + + test('should decode encoded URI input and extract mailto links', () { + const listPost = ', '; + final result = EmailUtils.extractMailtoLinksFromListPost(listPost); + + expect(result, ['mailto:example1@test.com', 'mailto:example2@test.com']); + }); + + test('should handle mailto links with additional parameters like cc and bcc', () { + const listPost = ', '; + final result = EmailUtils.extractMailtoLinksFromListPost(listPost); + + expect(result, ['mailto:johndoe@fakeemail.com', 'mailto:janedoe@fakeemail.com?cc=jackdoe@fakeemail.com&bcc=jennydoe@fakeemail.com']); + }); + + test('should handle mailto links with additional parameters like cc, bcc, subject and body', () { + const listPost = ', '; + final result = EmailUtils.extractMailtoLinksFromListPost(listPost); + + expect(result, [ + 'mailto:johndoe@fakeemail.com', + 'mailto:janedoe@fakeemail.com?cc=jackdoe@fakeemail.com&bcc=jennydoe@fakeemail.com&subject=TestSubject&body=TestBody', + ]); + }); + }); + + group('EmailUtils::extractRecipientsFromMailtoLink::', () { + test('should return empty lists for empty mailtoLink', () { + final result = EmailUtils.extractRecipientsFromMailtoLink(''); + expect(result.toMailAddresses, isEmpty); + expect(result.ccMailAddresses, isEmpty); + expect(result.bccMailAddresses, isEmpty); + }); + + test('should extract recipients from valid mailtoLink', () { + const mailtoLink = 'mailto:test@example.com?cc=cc@example.com&bcc=bcc@example.com'; + + final result = EmailUtils.extractRecipientsFromMailtoLink(mailtoLink); + + expect(result.toMailAddresses, [EmailAddress(null, 'test@example.com')]); + expect(result.ccMailAddresses, [EmailAddress(null, 'cc@example.com')]); + expect(result.bccMailAddresses, [EmailAddress(null, 'bcc@example.com')]); + }); + + test('should handle malformed mailtoLink gracefully', () { + const malformedMailtoLink = 'mailto:test@example.com?invalid=params'; + + final result = EmailUtils.extractRecipientsFromMailtoLink(malformedMailtoLink); + + expect(result.toMailAddresses, [EmailAddress(null, 'test@example.com')]); + expect(result.ccMailAddresses, isEmpty); + expect(result.bccMailAddresses, isEmpty); + }); + + test('should handle exceptions gracefully', () { + const invalidMailtoLink = 'mailto:invalid-link, mailto:%E0%A4%A'; + + final result = EmailUtils.extractRecipientsFromMailtoLink(invalidMailtoLink); + + expect(result.toMailAddresses, isEmpty); + expect(result.ccMailAddresses, isEmpty); + expect(result.bccMailAddresses, isEmpty); + }); + + test('should decode and extract recipients from encoded mailtoLink', () { + const encodedMailtoLink = 'mailto:test%40example.com?cc=cc1%40example.com%2Ccc2%40example.com&bcc=bcc%40example.com'; + + final result = EmailUtils.extractRecipientsFromMailtoLink(encodedMailtoLink); + + expect(result.toMailAddresses, [EmailAddress(null, 'test@example.com')]); + expect(result.ccMailAddresses, [EmailAddress(null, 'cc1@example.com'), EmailAddress(null, 'cc2@example.com')]); + expect(result.bccMailAddresses, [EmailAddress(null, 'bcc@example.com')]); + }); + + test('should extract recipients from mailto links with additional parameters like cc, bcc, subject and body', () { + const mailtoLink = 'mailto:test@example.com?cc=cc@example.com&bcc=bcc@example.com&subject=TestSubject&body=TestBody'; + + final result = EmailUtils.extractRecipientsFromMailtoLink(mailtoLink); + + expect(result.toMailAddresses, [EmailAddress(null, 'test@example.com')]); + expect(result.ccMailAddresses, [EmailAddress(null, 'cc@example.com')]); + expect(result.bccMailAddresses, [EmailAddress(null, 'bcc@example.com')]); + }); + }); + + group('EmailUtils::extractRecipientsFromListPost::', () { + test('should return empty lists for empty listPost', () { + final result = EmailUtils.extractRecipientsFromListPost(''); + + expect(result.toMailAddresses, isEmpty); + expect(result.ccMailAddresses, isEmpty); + expect(result.bccMailAddresses, isEmpty); + }); + + test('should extract recipients from a valid listPost', () { + const listPost = ', '; + + final result = EmailUtils.extractRecipientsFromListPost(listPost); + + expect(result.toMailAddresses, [ + EmailAddress(null, 'test@example.com'), + EmailAddress(null, 'another@example.com'), + ]); + expect(result.ccMailAddresses, [EmailAddress(null, 'cc@example.com')]); + expect(result.bccMailAddresses, [EmailAddress(null, 'bcc@example.com')]); + }); + + test('should handle listPost with no valid mailto links', () { + const listPost = 'content without mailto links'; + + final result = EmailUtils.extractRecipientsFromListPost(listPost); + + expect(result.toMailAddresses, isEmpty); + expect(result.ccMailAddresses, isEmpty); + expect(result.bccMailAddresses, isEmpty); + }); + + test('should decode and extract recipients from listPost with encoded mailto links', () { + const listPost = '%3Cmailto%3Atest%2540example.com%3Fcc%3Dcc1%2540example.com%252Ccc2%2540example.com%26bcc%3Dbcc%2540example.com%3E%2C%20%3Cmailto%3Aanother%2540example.com%3E'; + + final result = EmailUtils.extractRecipientsFromListPost(listPost); + + expect(result.toMailAddresses, [ + EmailAddress(null, 'test@example.com'), + EmailAddress(null, 'another@example.com'), + ]); + expect(result.ccMailAddresses, [ + EmailAddress(null, 'cc1@example.com'), + EmailAddress(null, 'cc2@example.com'), + ]); + expect(result.bccMailAddresses, [ + EmailAddress(null, 'bcc@example.com'), + ]); + }); + + test('should extract recipients from list post with additional parameters like cc, bcc, subject and body', () { + const listPost = ''; + + final result = EmailUtils.extractRecipientsFromListPost(listPost); + + expect(result.toMailAddresses, [EmailAddress(null, 'test@example.com')]); + expect(result.ccMailAddresses, [EmailAddress(null, 'cc@example.com')]); + expect(result.bccMailAddresses, [EmailAddress(null, 'bcc@example.com')]); + }); + }); +} diff --git a/test/features/email/parsing_email_list_post_test.dart b/test/features/email/parsing_email_list_post_test.dart deleted file mode 100644 index a0fec2b20c..0000000000 --- a/test/features/email/parsing_email_list_post_test.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:model/extensions/list_email_address_extension.dart'; -import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; - -void main() { - group('parsing email list post test', () { - test('parsingListPost returns null for empty input', () { - expect(EmailUtils.parsingListPost(''), isNull); - }); - - test('parsingListPost returns null for input without links', () { - expect(EmailUtils.parsingListPost('Some text without links'), isNull); - }); - - test('parsingListPost parses mailto links', () { - final listEmailAddress = - EmailUtils.parsingListPost(''); - expect(listEmailAddress, isNotNull); - expect(listEmailAddress![0].email, contains('user@example.com')); - }); - - test('parsingListPost parses both mailto without <>', () { - final listEmailAddress = - EmailUtils.parsingListPost('mailto:support@example.com'); - expect(listEmailAddress, isNotNull); - expect(listEmailAddress![0].email, contains('support@example.com')); - }); - - test('parsingListPost parses more mailto', () { - final listEmailAddress = EmailUtils.parsingListPost( - ', , '); - expect(listEmailAddress, isNotNull); - expect(listEmailAddress!.length, equals(3)); - expect( - listEmailAddress.asSetAddress(), - containsAll({ - 'support@example.com', - 'support@example2.com', - 'support@example3.com' - }), - ); - }); - - test('parsingListPost returns null for input with invalid links', () { - expect(EmailUtils.parsingListPost('Invalid link: invalid'), isNull); - }); - }); -} diff --git a/test/model/lib/extensions/presentation_email_extension_test.dart b/test/model/lib/extensions/presentation_email_extension_test.dart index 7c83980fc5..f0fde8440b 100644 --- a/test/model/lib/extensions/presentation_email_extension_test.dart +++ b/test/model/lib/extensions/presentation_email_extension_test.dart @@ -4,7 +4,6 @@ import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:model/email/email_action_type.dart'; import 'package:model/email/presentation_email.dart'; import 'package:model/extensions/email_address_extension.dart'; -import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:tmail_ui_user/features/email/presentation/extensions/presentation_email_extension.dart'; void main() { @@ -30,7 +29,8 @@ void main() { final result = emailToReply.generateRecipientsEmailAddressForComposer( emailActionType: EmailActionType.reply, - mailboxRole: PresentationMailbox.roleSent + isSender: true, + userName: userAEmailAddress.emailAddress, ); expect(result.value1, containsAll(expectedResult.value1)); @@ -50,7 +50,8 @@ void main() { final result = emailToReply.generateRecipientsEmailAddressForComposer( emailActionType: EmailActionType.replyAll, - mailboxRole: PresentationMailbox.roleSent + isSender: true, + userName: userAEmailAddress.emailAddress, ); expect(result.value1, containsAll(expectedResult.value1)); @@ -61,7 +62,7 @@ void main() { group('GIVEN user B is the sender, SENDER configured the replyTo email AND send an email to user A and user E, cc to user C, bcc to user D', () { test('THEN user A click reply, generateRecipientsEmailAddressForComposer SHOULD return only replyToEmailAddress email to reply' , () { - final expectedResult = Tuple3([replyToEmailAddress], [], []); + final expectedResult = Tuple3([userBEmailAddress], [], []); final emailToReply = PresentationEmail( from: {userBEmailAddress}, @@ -73,7 +74,7 @@ void main() { final result = emailToReply.generateRecipientsEmailAddressForComposer( emailActionType: EmailActionType.reply, - mailboxRole: PresentationMailbox.roleInbox + isSender: false, ); expect(result.value1, containsAll(expectedResult.value1)); @@ -94,7 +95,7 @@ void main() { final result = emailToReply.generateRecipientsEmailAddressForComposer( emailActionType: EmailActionType.replyAll, - mailboxRole: PresentationMailbox.roleInbox + isSender: false, ); expect(result.value1, containsAll(expectedResult.value1)); @@ -116,7 +117,7 @@ void main() { final result = emailToReply.generateRecipientsEmailAddressForComposer( emailActionType: EmailActionType.reply, - mailboxRole: PresentationMailbox.roleInbox + isSender: false, ); expect(result.value1, containsAll(expectedResult.value1)); @@ -136,7 +137,7 @@ void main() { final result = emailToReply.generateRecipientsEmailAddressForComposer( emailActionType: EmailActionType.replyAll, - mailboxRole: PresentationMailbox.roleInbox + isSender: false, ); expect(result.value1, containsAll(expectedResult.value1)); @@ -160,7 +161,7 @@ void main() { final result = emailToReplyToList.generateRecipientsEmailAddressForComposer( emailActionType: EmailActionType.replyToList, - mailboxRole: PresentationMailbox.roleInbox, + isSender: false, listPost: '', ); @@ -183,7 +184,7 @@ void main() { final result = emailToReply.generateRecipientsEmailAddressForComposer( emailActionType: EmailActionType.forward, - mailboxRole: PresentationMailbox.roleInbox + isSender: false, ); expect(result.value1, containsAll(expectedResult.value1));