TF-1718 Check List-Unsubscribe in header of email & parsing to EmailUnsubscribe
(cherry picked from commit 9dc046a951e5198461178860c8ebe4bedff9062f)
This commit is contained in:
@@ -58,6 +58,8 @@ import 'package:tmail_ui_user/features/email/domain/usecases/mark_as_star_email_
|
||||
import 'package:tmail_ui_user/features/email/domain/usecases/move_to_mailbox_interactor.dart';
|
||||
import 'package:tmail_ui_user/features/email/domain/usecases/send_receipt_to_sender_interactor.dart';
|
||||
import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart';
|
||||
import 'package:tmail_ui_user/features/email/presentation/model/email_unsubscribe.dart';
|
||||
import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart';
|
||||
import 'package:tmail_ui_user/features/email/presentation/widgets/attachment_list/attachment_list_bottom_sheet_builder.dart';
|
||||
import 'package:tmail_ui_user/features/email/presentation/widgets/attachment_list/attachment_list_dialog_builder.dart';
|
||||
import 'package:tmail_ui_user/features/email/presentation/widgets/email_address_bottom_sheet_builder.dart';
|
||||
@@ -113,6 +115,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
|
||||
final calendarEvent = Rxn<CalendarEvent>();
|
||||
final eventActions = <EventAction>[].obs;
|
||||
final emailLoadedViewState = Rx<Either<Failure, Success>>(Right(UIState.idle));
|
||||
final emailUnsubscribe = Rxn<EmailUnsubscribe>();
|
||||
|
||||
EmailId? _currentEmailId;
|
||||
Identity? _identitySelected;
|
||||
@@ -472,8 +475,8 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
|
||||
emailId: currentEmail!.id!,
|
||||
createdTime: currentEmail?.receivedAt?.value ?? DateTime.now(),
|
||||
attachments: success.attachments,
|
||||
headers: currentEmail?.emailHeader?.toSet(),
|
||||
keywords: currentEmail?.keywords,
|
||||
headers: success.emailCurrent?.headers,
|
||||
keywords: success.emailCurrent?.keywords,
|
||||
htmlEmailContent: success.htmlEmailContent,
|
||||
messageId: success.emailCurrent?.messageId,
|
||||
references: success.emailCurrent?.references,
|
||||
@@ -491,9 +494,19 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
|
||||
if (isShowMessageReadReceipt) {
|
||||
_handleReadReceipt();
|
||||
}
|
||||
|
||||
if (success.emailCurrent?.hasListUnsubscribe == true) {
|
||||
_handleUnsubscribe(success.emailCurrent!.listUnsubscribe);
|
||||
} else {
|
||||
emailUnsubscribe.value = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _handleUnsubscribe(String listUnsubscribe) {
|
||||
emailUnsubscribe.value = EmailUtils.parsingUnsubscribe(listUnsubscribe);
|
||||
}
|
||||
|
||||
void _handleReadReceipt() {
|
||||
if (currentContext != null) {
|
||||
showConfirmDialogAction(currentContext!,
|
||||
@@ -514,6 +527,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
|
||||
attachments.clear();
|
||||
calendarEvent.value = null;
|
||||
eventActions.clear();
|
||||
emailUnsubscribe.value = null;
|
||||
}
|
||||
|
||||
PresentationMailbox? getMailboxContain(PresentationEmail email) {
|
||||
|
||||
@@ -298,12 +298,13 @@ class EmailView extends GetWidget<SingleEmailController> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
EmailSubjectWidget(presentationEmail: presentationEmail),
|
||||
InformationSenderAndReceiverBuilder(
|
||||
controller: controller,
|
||||
Obx(() => InformationSenderAndReceiverBuilder(
|
||||
emailSelected: presentationEmail,
|
||||
imagePaths: controller.imagePaths,
|
||||
responsiveUtils: controller.responsiveUtils,
|
||||
),
|
||||
emailUnsubscribe: controller.emailUnsubscribe.value,
|
||||
openEmailAddressDetailAction: controller.openEmailAddressDialog,
|
||||
)),
|
||||
Obx(() {
|
||||
final attachments = controller.attachments.listAttachmentsDisplayedOutSide;
|
||||
if (attachments.isNotEmpty) {
|
||||
@@ -429,7 +430,8 @@ class EmailView extends GetWidget<SingleEmailController> {
|
||||
EmailActionType.moveToSpam,
|
||||
if (presentationEmail.from?.isNotEmpty == true)
|
||||
EmailActionType.createRule,
|
||||
EmailActionType.unsubscribe
|
||||
if (controller.emailUnsubscribe.value != null)
|
||||
EmailActionType.unsubscribe
|
||||
];
|
||||
|
||||
if (position == null) {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class EmailUnsubscribe with EquatableMixin {
|
||||
|
||||
final List<String> httpLinks;
|
||||
final List<String> mailtoLinks;
|
||||
|
||||
EmailUnsubscribe({
|
||||
required this.httpLinks,
|
||||
required this.mailtoLinks
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
httpLinks,
|
||||
mailtoLinks
|
||||
];
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:core/utils/app_logger.dart';
|
||||
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: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';
|
||||
|
||||
@@ -15,4 +18,35 @@ class EmailUtils {
|
||||
return ThreadConstants.propertiesDefault;
|
||||
}
|
||||
}
|
||||
|
||||
static EmailUnsubscribe? parsingUnsubscribe(String listUnsubscribe) {
|
||||
if (listUnsubscribe.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final regExpMailtoLinks = RegExp(r'mailto:([^>,]*)');
|
||||
final allMatchesMailtoLinks = regExpMailtoLinks.allMatches(listUnsubscribe);
|
||||
final listMailtoLinks = allMatchesMailtoLinks
|
||||
.map((match) => match.group(0))
|
||||
.whereNotNull()
|
||||
.toList();
|
||||
log('EmailUtils::parsingUnsubscribe:listMailtoLinks: $listMailtoLinks');
|
||||
|
||||
final regExpHttpLinks = RegExp(r'http([^>,]*)');
|
||||
final allMatchesHttpLinks = regExpHttpLinks.allMatches(listUnsubscribe);
|
||||
final listHttpLinks = allMatchesHttpLinks
|
||||
.map((match) => match.group(0))
|
||||
.whereNotNull()
|
||||
.toList();
|
||||
log('EmailUtils::parsingUnsubscribe:listHttpLinks: $listHttpLinks');
|
||||
|
||||
if (listMailtoLinks.isNotEmpty || listHttpLinks.isNotEmpty) {
|
||||
return EmailUnsubscribe(
|
||||
httpLinks: listHttpLinks,
|
||||
mailtoLinks: listMailtoLinks
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,22 +15,21 @@ import 'package:model/extensions/list_email_address_extension.dart';
|
||||
import 'package:model/extensions/presentation_email_extension.dart';
|
||||
import 'package:tmail_ui_user/features/base/widget/material_text_button.dart';
|
||||
import 'package:tmail_ui_user/features/composer/presentation/extensions/prefix_email_address_extension.dart';
|
||||
import 'package:tmail_ui_user/features/email/presentation/widgets/email_sender_builder.dart';
|
||||
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
|
||||
import 'package:tmail_ui_user/main/utils/app_utils.dart';
|
||||
|
||||
typedef OnPreviewEmailAddressActionCallback = Function(BuildContext context, EmailAddress emailAddress);
|
||||
|
||||
class EmailReceiverWidget extends StatefulWidget {
|
||||
|
||||
final PresentationEmail emailSelected;
|
||||
final double maxWidth;
|
||||
final OnPreviewEmailAddressActionCallback? onPreviewEmailAddressActionCallback;
|
||||
final OnOpenEmailAddressDetailAction? openEmailAddressDetailAction;
|
||||
|
||||
const EmailReceiverWidget({
|
||||
Key? key,
|
||||
required this.emailSelected,
|
||||
this.maxWidth = 200,
|
||||
this.onPreviewEmailAddressActionCallback,
|
||||
this.openEmailAddressDetailAction,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -218,7 +217,7 @@ class _EmailReceiverWidgetState extends State<EmailReceiverWidget> {
|
||||
label: lastEmailAddress == emailAddress
|
||||
? emailAddress.asString()
|
||||
: '${emailAddress.asString()},',
|
||||
onTap: () => widget.onPreviewEmailAddressActionCallback?.call(context, emailAddress),
|
||||
onTap: () => widget.openEmailAddressDetailAction?.call(context, emailAddress),
|
||||
onLongPress: () {
|
||||
AppUtils.copyEmailAddressToClipboard(context, emailAddress.emailAddress);
|
||||
},
|
||||
|
||||
+9
-7
@@ -6,7 +6,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:model/email/presentation_email.dart';
|
||||
import 'package:model/extensions/presentation_email_extension.dart';
|
||||
import 'package:tmail_ui_user/features/base/widget/email_avatar_builder.dart';
|
||||
import 'package:tmail_ui_user/features/email/presentation/controller/single_email_controller.dart';
|
||||
import 'package:tmail_ui_user/features/email/presentation/model/email_unsubscribe.dart';
|
||||
import 'package:tmail_ui_user/features/email/presentation/widgets/email_receiver_widget.dart';
|
||||
import 'package:tmail_ui_user/features/email/presentation/widgets/email_sender_builder.dart';
|
||||
import 'package:tmail_ui_user/features/email/presentation/widgets/received_time_builder.dart';
|
||||
@@ -14,17 +14,19 @@ import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
|
||||
|
||||
class InformationSenderAndReceiverBuilder extends StatelessWidget {
|
||||
|
||||
final SingleEmailController controller;
|
||||
final PresentationEmail emailSelected;
|
||||
final ResponsiveUtils responsiveUtils;
|
||||
final ImagePaths imagePaths;
|
||||
final EmailUnsubscribe? emailUnsubscribe;
|
||||
final OnOpenEmailAddressDetailAction? openEmailAddressDetailAction;
|
||||
|
||||
const InformationSenderAndReceiverBuilder({
|
||||
Key? key,
|
||||
required this.controller,
|
||||
required this.emailSelected,
|
||||
required this.responsiveUtils,
|
||||
required this.imagePaths
|
||||
required this.imagePaths,
|
||||
required this.emailUnsubscribe,
|
||||
this.openEmailAddressDetailAction,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -54,10 +56,10 @@ class InformationSenderAndReceiverBuilder extends StatelessWidget {
|
||||
transform: Matrix4.translationValues(-5.0, 0.0, 0.0),
|
||||
child: EmailSenderBuilder(
|
||||
emailAddress: emailSelected.from!.first,
|
||||
openEmailAddressDetailAction: controller.openEmailAddressDialog,
|
||||
openEmailAddressDetailAction: openEmailAddressDetailAction,
|
||||
)
|
||||
)),
|
||||
if (!responsiveUtils.isPortraitMobile(context))
|
||||
if (emailUnsubscribe != null && !responsiveUtils.isPortraitMobile(context))
|
||||
TMailButtonWidget.fromText(
|
||||
text: AppLocalizations.of(context).unsubscribe,
|
||||
textStyle: const TextStyle(
|
||||
@@ -81,7 +83,7 @@ class InformationSenderAndReceiverBuilder extends StatelessWidget {
|
||||
EmailReceiverWidget(
|
||||
emailSelected: emailSelected,
|
||||
maxWidth: constraints.maxWidth,
|
||||
onPreviewEmailAddressActionCallback: controller.openEmailAddressDialog,
|
||||
openEmailAddressDetailAction: openEmailAddressDetailAction,
|
||||
)
|
||||
]
|
||||
),
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:core/utils/app_logger.dart';
|
||||
import 'package:jmap_dart_client/jmap/core/properties/properties.dart';
|
||||
import 'package:jmap_dart_client/jmap/mail/email/email.dart';
|
||||
import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart';
|
||||
@@ -11,6 +13,8 @@ import 'package:model/model.dart';
|
||||
|
||||
extension EmailExtension on Email {
|
||||
|
||||
static const String unsubscribeHeaderName = 'List-Unsubscribe';
|
||||
|
||||
String asString() => jsonEncode(toJson());
|
||||
|
||||
bool get hasRead => keywords?.containsKey(KeyWordIdentifier.emailSeen) == true;
|
||||
@@ -23,6 +27,14 @@ extension EmailExtension on Email {
|
||||
|
||||
bool get withAttachments => hasAttachment == true;
|
||||
|
||||
String get listUnsubscribe {
|
||||
final listUnsubscribe = headers?.firstWhereOrNull((header) => header.name == unsubscribeHeaderName);
|
||||
log('EmailExtension::listUnsubscribe: $listUnsubscribe');
|
||||
return listUnsubscribe?.value ?? '';
|
||||
}
|
||||
|
||||
bool get hasListUnsubscribe => listUnsubscribe.isNotEmpty;
|
||||
|
||||
bool hasReadReceipt(Map<MailboxId, PresentationMailbox> mapMailbox) {
|
||||
final mailboxCurrent = findMailboxContain(mapMailbox);
|
||||
return !hasMdnSent &&
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart';
|
||||
|
||||
void main() {
|
||||
group('parsing email unsubscribe test', () {
|
||||
test('parsingUnsubscribe returns null for empty input', () {
|
||||
expect(EmailUtils.parsingUnsubscribe(''), isNull);
|
||||
});
|
||||
|
||||
test('parsingUnsubscribe returns null for input without links', () {
|
||||
expect(EmailUtils.parsingUnsubscribe('Some text without links'), isNull);
|
||||
});
|
||||
|
||||
test('parsingUnsubscribe parses mailto links', () {
|
||||
final emailUnsubscribe = EmailUtils.parsingUnsubscribe('<mailto:user@example.com>');
|
||||
expect(emailUnsubscribe, isNotNull);
|
||||
expect(emailUnsubscribe!.mailtoLinks, contains('mailto:user@example.com'));
|
||||
expect(emailUnsubscribe.httpLinks, isEmpty);
|
||||
});
|
||||
|
||||
test('parsingUnsubscribe parses http links', () {
|
||||
final emailUnsubscribe = EmailUtils.parsingUnsubscribe('<http://example.com>');
|
||||
expect(emailUnsubscribe, isNotNull);
|
||||
expect(emailUnsubscribe!.httpLinks, contains('http://example.com'));
|
||||
expect(emailUnsubscribe.mailtoLinks, isEmpty);
|
||||
});
|
||||
|
||||
test('parsingUnsubscribe parses https links', () {
|
||||
final emailUnsubscribe = EmailUtils.parsingUnsubscribe('<https://example.com>');
|
||||
expect(emailUnsubscribe, isNotNull);
|
||||
expect(emailUnsubscribe!.httpLinks, contains('https://example.com'));
|
||||
expect(emailUnsubscribe.mailtoLinks, isEmpty);
|
||||
});
|
||||
|
||||
test('parsingUnsubscribe parses both http and https links', () {
|
||||
final emailUnsubscribe = EmailUtils.parsingUnsubscribe('<http://example.com>, <https://example.com>');
|
||||
expect(emailUnsubscribe, isNotNull);
|
||||
expect(emailUnsubscribe!.httpLinks, containsAll(['http://example.com', 'https://example.com']));
|
||||
expect(emailUnsubscribe.mailtoLinks, isEmpty);
|
||||
});
|
||||
|
||||
test('parsingUnsubscribe parses both mailto and http links', () {
|
||||
final emailUnsubscribe = EmailUtils.parsingUnsubscribe('<mailto:support@example.com>, <http://example.com>');
|
||||
expect(emailUnsubscribe, isNotNull);
|
||||
expect(emailUnsubscribe!.mailtoLinks, contains('mailto:support@example.com'));
|
||||
expect(emailUnsubscribe.httpLinks, contains('http://example.com'));
|
||||
});
|
||||
|
||||
test('parsingUnsubscribe parses both mailto and http links without <>', () {
|
||||
final emailUnsubscribe = EmailUtils.parsingUnsubscribe('mailto:support@example.com, http://example.com');
|
||||
expect(emailUnsubscribe, isNotNull);
|
||||
expect(emailUnsubscribe!.mailtoLinks, contains('mailto:support@example.com'));
|
||||
expect(emailUnsubscribe.httpLinks, contains('http://example.com'));
|
||||
});
|
||||
|
||||
test('parsingUnsubscribe parses more mailto and http links', () {
|
||||
final emailUnsubscribe = EmailUtils.parsingUnsubscribe('<mailto:support@example.com>, <http://example.com>, <http://example2.com>, <http://example3.com>, <mailto:support@example2.com>, <mailto:support@example3.com>');
|
||||
expect(emailUnsubscribe, isNotNull);
|
||||
expect(emailUnsubscribe!.mailtoLinks, containsAll(['mailto:support@example.com', 'mailto:support@example2.com', 'mailto:support@example3.com']));
|
||||
expect(emailUnsubscribe.httpLinks, containsAll(['http://example.com', 'http://example2.com', 'http://example3.com']));
|
||||
});
|
||||
|
||||
test('parsingUnsubscribe returns null for input with invalid links', () {
|
||||
expect(EmailUtils.parsingUnsubscribe('Invalid link: invalid'), isNull);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user