TF-3413 Support Reply to list for email
This commit is contained in:
@@ -40,7 +40,9 @@ class EmailRequest with EquatableMixin {
|
|||||||
];
|
];
|
||||||
|
|
||||||
bool get isEmailAnswered => emailIdAnsweredOrForwarded != null &&
|
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;
|
bool get isEmailForwarded => emailIdAnsweredOrForwarded != null && emailActionType == EmailActionType.forward;
|
||||||
}
|
}
|
||||||
@@ -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/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/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/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/model/composer_arguments.dart';
|
||||||
import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.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';
|
import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.dart';
|
||||||
@@ -640,11 +641,15 @@ class ComposerController extends BaseController
|
|||||||
_updateStatusEmailSendButton();
|
_updateStatusEmailSendButton();
|
||||||
break;
|
break;
|
||||||
case EmailActionType.reply:
|
case EmailActionType.reply:
|
||||||
|
case EmailActionType.replyToList:
|
||||||
case EmailActionType.replyAll:
|
case EmailActionType.replyAll:
|
||||||
|
log('ComposerController::_initEmail:listPost = ${arguments.listPost}');
|
||||||
_initEmailAddress(
|
_initEmailAddress(
|
||||||
presentationEmail: arguments.presentationEmail!,
|
presentationEmail: arguments.presentationEmail!,
|
||||||
actionType: arguments.emailActionType,
|
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(
|
_initSubjectEmail(
|
||||||
presentationEmail: arguments.presentationEmail!,
|
presentationEmail: arguments.presentationEmail!,
|
||||||
@@ -798,12 +803,15 @@ class ComposerController extends BaseController
|
|||||||
required PresentationEmail presentationEmail,
|
required PresentationEmail presentationEmail,
|
||||||
required EmailActionType actionType,
|
required EmailActionType actionType,
|
||||||
Role? mailboxRole,
|
Role? mailboxRole,
|
||||||
|
String? listPost,
|
||||||
}) {
|
}) {
|
||||||
|
log('ComposerController::_initEmailAddress:listPost = $listPost');
|
||||||
final recipients = presentationEmail.generateRecipientsEmailAddressForComposer(
|
final recipients = presentationEmail.generateRecipientsEmailAddressForComposer(
|
||||||
emailActionType: actionType,
|
emailActionType: actionType,
|
||||||
mailboxRole: mailboxRole
|
mailboxRole: mailboxRole,
|
||||||
|
listPost: listPost,
|
||||||
);
|
);
|
||||||
final userName = mailboxDashBoardController.sessionCurrent?.username;
|
final userName = mailboxDashBoardController.sessionCurrent?.username;
|
||||||
if (userName != null) {
|
if (userName != null) {
|
||||||
final isSender = presentationEmail.from.asList().every((element) => element.email == userName.value);
|
final isSender = presentationEmail.from.asList().every((element) => element.email == userName.value);
|
||||||
if (isSender) {
|
if (isSender) {
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ extension CreateEmailRequestExtension on CreateEmailRequest {
|
|||||||
|
|
||||||
MessageIdsHeaderValue? createInReplyTo() {
|
MessageIdsHeaderValue? createInReplyTo() {
|
||||||
if (emailActionType == EmailActionType.reply ||
|
if (emailActionType == EmailActionType.reply ||
|
||||||
|
emailActionType == EmailActionType.replyToList ||
|
||||||
emailActionType == EmailActionType.replyAll
|
emailActionType == EmailActionType.replyAll
|
||||||
) {
|
) {
|
||||||
return messageId;
|
return messageId;
|
||||||
@@ -83,6 +84,7 @@ extension CreateEmailRequestExtension on CreateEmailRequest {
|
|||||||
|
|
||||||
MessageIdsHeaderValue? createReferences() {
|
MessageIdsHeaderValue? createReferences() {
|
||||||
if (emailActionType == EmailActionType.reply ||
|
if (emailActionType == EmailActionType.reply ||
|
||||||
|
emailActionType == EmailActionType.replyToList ||
|
||||||
emailActionType == EmailActionType.replyAll ||
|
emailActionType == EmailActionType.replyAll ||
|
||||||
emailActionType == EmailActionType.forward
|
emailActionType == EmailActionType.forward
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ extension EmailActionTypeExtension on EmailActionType {
|
|||||||
String getSubjectComposer(BuildContext? context, String subject) {
|
String getSubjectComposer(BuildContext? context, String subject) {
|
||||||
switch(this) {
|
switch(this) {
|
||||||
case EmailActionType.reply:
|
case EmailActionType.reply:
|
||||||
|
case EmailActionType.replyToList:
|
||||||
case EmailActionType.replyAll:
|
case EmailActionType.replyAll:
|
||||||
if (subject.toLowerCase().startsWith('re:')) {
|
if (subject.toLowerCase().startsWith('re:')) {
|
||||||
return subject;
|
return subject;
|
||||||
@@ -60,6 +61,7 @@ extension EmailActionTypeExtension on EmailActionType {
|
|||||||
final languageTag = locale.toLanguageTag();
|
final languageTag = locale.toLanguageTag();
|
||||||
switch(this) {
|
switch(this) {
|
||||||
case EmailActionType.reply:
|
case EmailActionType.reply:
|
||||||
|
case EmailActionType.replyToList:
|
||||||
case EmailActionType.replyAll:
|
case EmailActionType.replyAll:
|
||||||
final receivedAt = presentationEmail.receivedAt;
|
final receivedAt = presentationEmail.receivedAt;
|
||||||
final emailAddress = presentationEmail.from.toEscapeHtmlStringUseCommaSeparator();
|
final emailAddress = presentationEmail.from.toEscapeHtmlStringUseCommaSeparator();
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ class MobileEditorView extends StatelessWidget with EditorViewMixin {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
case EmailActionType.reply:
|
case EmailActionType.reply:
|
||||||
|
case EmailActionType.replyToList:
|
||||||
case EmailActionType.replyAll:
|
case EmailActionType.replyAll:
|
||||||
case EmailActionType.forward:
|
case EmailActionType.forward:
|
||||||
if (contentViewState == null) {
|
if (contentViewState == null) {
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ class WebEditorView extends StatelessWidget with EditorViewMixin {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
case EmailActionType.reply:
|
case EmailActionType.reply:
|
||||||
|
case EmailActionType.replyToList:
|
||||||
case EmailActionType.replyAll:
|
case EmailActionType.replyAll:
|
||||||
case EmailActionType.forward:
|
case EmailActionType.forward:
|
||||||
if (contentViewState == null) {
|
if (contentViewState == null) {
|
||||||
|
|||||||
@@ -1425,6 +1425,20 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
break;
|
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:
|
case EmailActionType.replyAll:
|
||||||
mailboxDashBoardController.goToComposer(
|
mailboxDashBoardController.goToComposer(
|
||||||
ComposerArguments.replyAllEmail(
|
ComposerArguments.replyAllEmail(
|
||||||
|
|||||||
@@ -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<EmailAddress>, List<EmailAddress>, List<EmailAddress>> 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,6 +32,7 @@ class ComposerArguments extends RouterArguments {
|
|||||||
final ScreenDisplayMode displayMode;
|
final ScreenDisplayMode displayMode;
|
||||||
final List<EmailAddress>? cc;
|
final List<EmailAddress>? cc;
|
||||||
final List<EmailAddress>? bcc;
|
final List<EmailAddress>? bcc;
|
||||||
|
final String? listPost;
|
||||||
|
|
||||||
ComposerArguments({
|
ComposerArguments({
|
||||||
this.emailActionType = EmailActionType.compose,
|
this.emailActionType = EmailActionType.compose,
|
||||||
@@ -54,6 +55,7 @@ class ComposerArguments extends RouterArguments {
|
|||||||
this.displayMode = ScreenDisplayMode.normal,
|
this.displayMode = ScreenDisplayMode.normal,
|
||||||
this.cc,
|
this.cc,
|
||||||
this.bcc,
|
this.bcc,
|
||||||
|
this.listPost,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory ComposerArguments.fromSendingEmail(SendingEmail sendingEmail) =>
|
factory ComposerArguments.fromSendingEmail(SendingEmail sendingEmail) =>
|
||||||
@@ -130,6 +132,25 @@ class ComposerArguments extends RouterArguments {
|
|||||||
references: references,
|
references: references,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
factory ComposerArguments.replyToListEmail({
|
||||||
|
required PresentationEmail presentationEmail,
|
||||||
|
required String content,
|
||||||
|
required List<Attachment> 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({
|
factory ComposerArguments.replyAllEmail({
|
||||||
required PresentationEmail presentationEmail,
|
required PresentationEmail presentationEmail,
|
||||||
required String content,
|
required String content,
|
||||||
@@ -206,6 +227,7 @@ class ComposerArguments extends RouterArguments {
|
|||||||
displayMode,
|
displayMode,
|
||||||
cc,
|
cc,
|
||||||
bcc,
|
bcc,
|
||||||
|
listPost,
|
||||||
];
|
];
|
||||||
|
|
||||||
ComposerArguments copyWith({
|
ComposerArguments copyWith({
|
||||||
@@ -229,6 +251,7 @@ class ComposerArguments extends RouterArguments {
|
|||||||
ScreenDisplayMode? displayMode,
|
ScreenDisplayMode? displayMode,
|
||||||
List<EmailAddress>? cc,
|
List<EmailAddress>? cc,
|
||||||
List<EmailAddress>? bcc,
|
List<EmailAddress>? bcc,
|
||||||
|
String? listPost,
|
||||||
}) {
|
}) {
|
||||||
return ComposerArguments(
|
return ComposerArguments(
|
||||||
emailActionType: emailActionType ?? this.emailActionType,
|
emailActionType: emailActionType ?? this.emailActionType,
|
||||||
@@ -251,6 +274,7 @@ class ComposerArguments extends RouterArguments {
|
|||||||
displayMode: displayMode ?? this.displayMode,
|
displayMode: displayMode ?? this.displayMode,
|
||||||
cc: cc ?? this.cc,
|
cc: cc ?? this.cc,
|
||||||
bcc: bcc ?? this.bcc,
|
bcc: bcc ?? this.bcc,
|
||||||
|
listPost: listPost ?? this.listPost,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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/capability/capability_identifier.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/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/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/email/presentation/model/email_unsubscribe.dart';
|
||||||
import 'package:tmail_ui_user/features/thread/domain/constants/thread_constants.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/error/capability_validator.dart';
|
||||||
|
import 'package:tmail_ui_user/main/routes/route_utils.dart';
|
||||||
|
|
||||||
class EmailUtils {
|
class EmailUtils {
|
||||||
|
|
||||||
@@ -83,4 +85,38 @@ class EmailUtils {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static List<EmailAddress>? 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:model/email/email_action_type.dart';
|
import 'package:model/email/email_action_type.dart';
|
||||||
import 'package:model/email/presentation_email.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: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/controller/single_email_controller.dart';
|
||||||
import 'package:tmail_ui_user/features/email/presentation/styles/email_view_bottom_bar_widget_styles.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();
|
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(() {
|
Obx(() {
|
||||||
if (_singleEmailController.currentEmailLoaded.value != null) {
|
if (_singleEmailController.currentEmailLoaded.value != null) {
|
||||||
return Expanded(
|
return Expanded(
|
||||||
|
|||||||
@@ -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": "Initializing data...",
|
||||||
"@initializing_data": {
|
"@initializing_data": {
|
||||||
"type": "text",
|
"type": "text",
|
||||||
@@ -4083,5 +4083,11 @@
|
|||||||
"type": "text",
|
"type": "text",
|
||||||
"placeholders_order": [],
|
"placeholders_order": [],
|
||||||
"placeholders": {}
|
"placeholders": {}
|
||||||
|
},
|
||||||
|
"replyToList": "Reply to list",
|
||||||
|
"@replyToList": {
|
||||||
|
"type": "text",
|
||||||
|
"placeholders_order": [],
|
||||||
|
"placeholders": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4286,4 +4286,12 @@ class AppLocalizations {
|
|||||||
name: 'getHelpOrReportABug',
|
name: 'getHelpOrReportABug',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String get replyToList {
|
||||||
|
return Intl.message(
|
||||||
|
'Reply to list',
|
||||||
|
name: 'replyToList',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
|
||||||
enum EmailActionType {
|
enum EmailActionType {
|
||||||
reply,
|
reply,
|
||||||
|
replyToList,
|
||||||
forward,
|
forward,
|
||||||
replyAll,
|
replyAll,
|
||||||
compose,
|
compose,
|
||||||
|
|||||||
@@ -24,4 +24,5 @@ class EmailProperty {
|
|||||||
static const String references = 'references';
|
static const String references = 'references';
|
||||||
static const String headerUnsubscribeKey = 'List-Unsubscribe';
|
static const String headerUnsubscribeKey = 'List-Unsubscribe';
|
||||||
static const String headerSMimeStatusKey = 'X-SMIME-Status';
|
static const String headerSMimeStatusKey = 'X-SMIME-Status';
|
||||||
|
static const String headerListPostKey = 'List-Post';
|
||||||
}
|
}
|
||||||
@@ -36,6 +36,10 @@ extension EmailExtension on Email {
|
|||||||
|
|
||||||
String get sMimeStatusHeaderParsed => sMimeStatusHeader?[IndividualHeaderIdentifier.sMimeStatusHeader]?.trim() ?? '';
|
String get sMimeStatusHeaderParsed => sMimeStatusHeader?[IndividualHeaderIdentifier.sMimeStatusHeader]?.trim() ?? '';
|
||||||
|
|
||||||
|
String get listPost => headers.listPost;
|
||||||
|
|
||||||
|
bool get hasListPost => listPost.isNotEmpty;
|
||||||
|
|
||||||
IdentityId? get identityIdFromHeader {
|
IdentityId? get identityIdFromHeader {
|
||||||
final rawIdentityId = identityHeader?[IndividualHeaderIdentifier.identityHeader];
|
final rawIdentityId = identityHeader?[IndividualHeaderIdentifier.identityHeader];
|
||||||
if (rawIdentityId == null) return null;
|
if (rawIdentityId == null) return null;
|
||||||
|
|||||||
@@ -24,4 +24,9 @@ extension ListEmailHeaderExtension on Set<EmailHeader>? {
|
|||||||
logger.log('ListEmailHeaderExtension::sMimeStatus: $sMimeStatus');
|
logger.log('ListEmailHeaderExtension::sMimeStatus: $sMimeStatus');
|
||||||
return sMimeStatus?.value.trim() ?? '';
|
return sMimeStatus?.value.trim() ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String get listPost {
|
||||||
|
final listPost = this?.firstWhereOrNull((header) => header.name == EmailProperty.headerListPostKey);
|
||||||
|
return listPost?.value ?? '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -6,11 +6,9 @@ import 'package:core/presentation/extensions/color_extension.dart';
|
|||||||
import 'package:core/utils/app_logger.dart';
|
import 'package:core/utils/app_logger.dart';
|
||||||
import 'package:http_parser/http_parser.dart';
|
import 'package:http_parser/http_parser.dart';
|
||||||
import 'package:jmap_dart_client/jmap/mail/email/email.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/email_address.dart';
|
||||||
import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart';
|
import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart';
|
||||||
import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.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/eml_attachment.dart';
|
||||||
import 'package:model/email/presentation_email.dart';
|
import 'package:model/email/presentation_email.dart';
|
||||||
import 'package:model/extensions/email_address_extension.dart';
|
import 'package:model/extensions/email_address_extension.dart';
|
||||||
@@ -133,30 +131,6 @@ extension PresentationEmailExtension on PresentationEmail {
|
|||||||
return allEmailAddress.isNotEmpty ? allEmailAddress.join(', ') : '';
|
return allEmailAddress.isNotEmpty ? allEmailAddress.join(', ') : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
Tuple3<List<EmailAddress>, List<EmailAddress>, List<EmailAddress>> 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<MailboxId, PresentationMailbox> mapMailboxes) {
|
PresentationEmail toSearchPresentationEmail(Map<MailboxId, PresentationMailbox> mapMailboxes) {
|
||||||
mailboxIds?.removeWhere((key, value) => !value);
|
mailboxIds?.removeWhere((key, value) => !value);
|
||||||
|
|
||||||
|
|||||||
@@ -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('<mailto:user@example.com>');
|
||||||
|
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(
|
||||||
|
'<mailto:support@example.com>, <mailto:support@example2.com>, <mailto:support@example3.com>');
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -3,8 +3,9 @@ import 'package:flutter_test/flutter_test.dart';
|
|||||||
import 'package:jmap_dart_client/jmap/mail/email/email_address.dart';
|
import 'package:jmap_dart_client/jmap/mail/email/email_address.dart';
|
||||||
import 'package:model/email/email_action_type.dart';
|
import 'package:model/email/email_action_type.dart';
|
||||||
import 'package:model/email/presentation_email.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:model/mailbox/presentation_mailbox.dart';
|
||||||
|
import 'package:tmail_ui_user/features/email/presentation/extensions/presentation_email_extension.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('presentation email extension test', () {
|
group('presentation email extension test', () {
|
||||||
@@ -14,6 +15,7 @@ void main() {
|
|||||||
final userDEmailAddress = EmailAddress('User D', 'userD@domain.com');
|
final userDEmailAddress = EmailAddress('User D', 'userD@domain.com');
|
||||||
final userEEmailAddress = EmailAddress('User E', 'userE@domain.com');
|
final userEEmailAddress = EmailAddress('User E', 'userE@domain.com');
|
||||||
final replyToEmailAddress = EmailAddress('Reply To', 'replyToThis@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', () {
|
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', () {
|
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.value2, containsAll(expectedResult.value2));
|
||||||
expect(result.value3, containsAll(expectedResult.value3));
|
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: '<mailto:${replyToListEmailAddress.emailAddress}>',
|
||||||
|
);
|
||||||
|
|
||||||
|
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', () {
|
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', () {
|
||||||
|
|||||||
Reference in New Issue
Block a user