diff --git a/assets/images/ic_bad_signature.svg b/assets/images/ic_bad_signature.svg new file mode 100644 index 0000000000..32c50a0a49 --- /dev/null +++ b/assets/images/ic_bad_signature.svg @@ -0,0 +1,5 @@ + + + diff --git a/assets/images/ic_good_signature.svg b/assets/images/ic_good_signature.svg new file mode 100644 index 0000000000..d08c8771f1 --- /dev/null +++ b/assets/images/ic_good_signature.svg @@ -0,0 +1,5 @@ + + + diff --git a/core/lib/presentation/resources/image_paths.dart b/core/lib/presentation/resources/image_paths.dart index 31228104ac..eca728238e 100644 --- a/core/lib/presentation/resources/image_paths.dart +++ b/core/lib/presentation/resources/image_paths.dart @@ -216,6 +216,8 @@ class ImagePaths { String get icRemoveRule => _getImagePath('ic_remove_rule.svg'); String get icCheckboxUnselected => _getImagePath('ic_checkbox_unselected.svg'); String get icCheckboxSelected => _getImagePath('ic_checkbox_selected.svg'); + String get icGoodSignature => _getImagePath('ic_good_signature.svg'); + String get icBadSignature => _getImagePath('ic_bad_signature.svg'); String _getImagePath(String imageName) { return AssetsPaths.images + imageName; 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..d1385cd9b8 --- /dev/null +++ b/lib/features/email/presentation/extensions/presentation_email_extension.dart @@ -0,0 +1,19 @@ + +import 'package:model/email/presentation_email.dart'; +import 'package:model/extensions/list_email_header_extension.dart'; +import 'package:tmail_ui_user/features/email/presentation/model/smime_signature_status.dart'; +import 'package:tmail_ui_user/features/email/presentation/utils/smime_signature_constant.dart'; + +extension PresentationEmailExtension on PresentationEmail { + + SMimeSignatureStatus get sMimeStatus { + final status = emailHeader?.toSet().sMimeStatus; + if (status == SMimeSignatureConstant.GOOD_SIGNATURE) { + return SMimeSignatureStatus.goodSignature; + } else if (status == SMimeSignatureConstant.BAD_SIGNATURE) { + return SMimeSignatureStatus.badSignature; + } else { + return SMimeSignatureStatus.notSigned; + } + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/model/smime_signature_status.dart b/lib/features/email/presentation/model/smime_signature_status.dart new file mode 100644 index 0000000000..141e7329f7 --- /dev/null +++ b/lib/features/email/presentation/model/smime_signature_status.dart @@ -0,0 +1,32 @@ + +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:flutter/material.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +enum SMimeSignatureStatus { + goodSignature, + badSignature, + notSigned; + + String getIcon(ImagePaths imagePaths) { + switch(this) { + case SMimeSignatureStatus.goodSignature: + return imagePaths.icGoodSignature; + case SMimeSignatureStatus.badSignature: + return imagePaths.icBadSignature; + case SMimeSignatureStatus.notSigned: + return ''; + } + } + + String getTooltipMessage(BuildContext context) { + switch(this) { + case SMimeSignatureStatus.goodSignature: + return AppLocalizations.of(context).sMimeGoodSignatureMessage; + case SMimeSignatureStatus.badSignature: + return AppLocalizations.of(context).sMimeBadSignatureMessage; + case SMimeSignatureStatus.notSigned: + return ''; + } + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/utils/smime_signature_constant.dart b/lib/features/email/presentation/utils/smime_signature_constant.dart new file mode 100644 index 0000000000..167718e752 --- /dev/null +++ b/lib/features/email/presentation/utils/smime_signature_constant.dart @@ -0,0 +1,5 @@ + +class SMimeSignatureConstant { + static const String GOOD_SIGNATURE = 'Good signature'; + static const String BAD_SIGNATURE = 'Bad signature'; +} \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/information_sender_and_receiver_builder.dart b/lib/features/email/presentation/widgets/information_sender_and_receiver_builder.dart index d28f0d1db2..592cd1bce8 100644 --- a/lib/features/email/presentation/widgets/information_sender_and_receiver_builder.dart +++ b/lib/features/email/presentation/widgets/information_sender_and_receiver_builder.dart @@ -3,11 +3,14 @@ import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:core/presentation/views/button/tmail_button_widget.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.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:tmail_ui_user/features/base/widget/email_avatar_builder.dart'; +import 'package:tmail_ui_user/features/email/presentation/extensions/presentation_email_extension.dart'; import 'package:tmail_ui_user/features/email/presentation/model/email_unsubscribe.dart'; +import 'package:tmail_ui_user/features/email/presentation/model/smime_signature_status.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/email_view_app_bar_widget.dart'; @@ -29,7 +32,7 @@ class InformationSenderAndReceiverBuilder extends StatelessWidget { required this.emailSelected, required this.responsiveUtils, required this.imagePaths, - required this.emailUnsubscribe, + this.emailUnsubscribe, this.maxBodyHeight, this.openEmailAddressDetailAction, this.onEmailActionClick, @@ -61,6 +64,18 @@ class InformationSenderAndReceiverBuilder extends StatelessWidget { openEmailAddressDetailAction: openEmailAddressDetailAction, ) )), + if (emailSelected.sMimeStatus != SMimeSignatureStatus.notSigned) + Tooltip( + key: const Key('smime_signature_status_icon'), + message: emailSelected.sMimeStatus.getTooltipMessage(context), + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: SvgPicture.asset( + emailSelected.sMimeStatus.getIcon(imagePaths), + fit: BoxFit.fill, + ), + ), + ), if (!emailSelected.isSubscribed && emailUnsubscribe != null && !responsiveUtils.isPortraitMobile(context)) TMailButtonWidget.fromText( text: AppLocalizations.of(context).unsubscribe, diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index 870f8bed6a..ddb80ff94a 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2024-07-17T01:36:03.095458", + "@@last_modified": "2024-08-05T16:49:32.139855", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -3963,5 +3963,17 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "sMimeGoodSignatureMessage": "The authenticity of this message had been verified with SMime signature.", + "@sMimeGoodSignatureMessage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "sMimeBadSignatureMessage": "This message failed SMime signature verification.", + "@sMimeBadSignatureMessage": { + "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 86a25edd85..0538c12535 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -4148,4 +4148,18 @@ class AppLocalizations { name: 'dialogMessageSessionHasExpired', ); } + + String get sMimeGoodSignatureMessage { + return Intl.message( + 'The authenticity of this message had been verified with SMime signature.', + name: 'sMimeGoodSignatureMessage', + ); + } + + String get sMimeBadSignatureMessage { + return Intl.message( + 'This message failed SMime signature verification.', + name: 'sMimeBadSignatureMessage', + ); + } } \ No newline at end of file diff --git a/model/lib/email/email_property.dart b/model/lib/email/email_property.dart index d1981e4043..28d3e42649 100644 --- a/model/lib/email/email_property.dart +++ b/model/lib/email/email_property.dart @@ -22,4 +22,6 @@ class EmailProperty { static const String headerMdnKey = 'Disposition-Notification-To'; static const String messageId = 'messageId'; static const String references = 'references'; + static const String headerUnsubscribeKey = 'List-Unsubscribe'; + static const String headerSMimeStatusKey = 'X-SMIME-Status'; } \ No newline at end of file diff --git a/model/lib/extensions/email_extension.dart b/model/lib/extensions/email_extension.dart index 2a1a9cf519..26df00cd01 100644 --- a/model/lib/extensions/email_extension.dart +++ b/model/lib/extensions/email_extension.dart @@ -1,9 +1,7 @@ import 'dart:convert'; -import 'package:collection/collection.dart'; import 'package:core/domain/extensions/datetime_extension.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'; @@ -14,8 +12,6 @@ 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; @@ -30,11 +26,7 @@ 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 ?? ''; - } + String get listUnsubscribe => headers.listUnsubscribe; bool get hasListUnsubscribe => listUnsubscribe.isNotEmpty; diff --git a/model/lib/extensions/list_email_header_extension.dart b/model/lib/extensions/list_email_header_extension.dart index 039b3c9964..ff08c91025 100644 --- a/model/lib/extensions/list_email_header_extension.dart +++ b/model/lib/extensions/list_email_header_extension.dart @@ -1,4 +1,5 @@ +import 'package:collection/collection.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_header.dart'; import 'package:model/email/email_property.dart'; @@ -11,4 +12,14 @@ extension ListEmailHeaderExtension on Set? { return false; } } + + String get listUnsubscribe { + final listUnsubscribe = this?.firstWhereOrNull((header) => header.name == EmailProperty.headerUnsubscribeKey); + return listUnsubscribe?.value ?? ''; + } + + String get sMimeStatus { + final sMimeStatus = this?.firstWhereOrNull((header) => header.name == EmailProperty.headerSMimeStatusKey); + return sMimeStatus?.value ?? ''; + } } \ No newline at end of file diff --git a/test/features/email/presentation/information_sender_and_receiver_builder_widget_test.dart b/test/features/email/presentation/information_sender_and_receiver_builder_widget_test.dart new file mode 100644 index 0000000000..60d358c669 --- /dev/null +++ b/test/features/email/presentation/information_sender_and_receiver_builder_widget_test.dart @@ -0,0 +1,277 @@ +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:get/get.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_header.dart'; +import 'package:model/email/email_property.dart'; +import 'package:model/email/presentation_email.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/information_sender_and_receiver_builder.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations_delegate.dart'; +import 'package:tmail_ui_user/main/localizations/localization_service.dart'; + +void main() { + group('InformationSenderAndReceiverBuilder::widgetTest', () { + final responsiveUtils = ResponsiveUtils(); + final imagePaths = ImagePaths(); + + Widget makeTestableWidget({required Widget child}) { + return GetMaterialApp( + localizationsDelegates: const [ + AppLocalizationsDelegate(), + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: LocalizationService.supportedLocales, + locale: LocalizationService.defaultLocale, + home: Scaffold(body: child), + ); + } + + group('SMimeSignatureStatusIcon::test', () { + testWidgets('should be displayed when email header has X-SMIME-Status', (tester) async { + final presentationEmail = PresentationEmail( + id: EmailId(Id('a123')), + from: { + EmailAddress('example', 'example@linagora.com') + }, + emailHeader: [ + EmailHeader(EmailProperty.headerSMimeStatusKey, 'Good signature') + ] + ); + final widget = makeTestableWidget( + child: InformationSenderAndReceiverBuilder( + emailSelected: presentationEmail, + responsiveUtils: responsiveUtils, + imagePaths: imagePaths, + emailUnsubscribe: null, + ), + ); + + await tester.pumpWidget(widget); + + await tester.pumpAndSettle(); + + expect( + find.byKey(const Key('smime_signature_status_icon')), + findsOneWidget); + }); + + testWidgets( + 'should be displayed and have good message \n' + 'when email header has X-SMIME-Status = "Good signature"', + (tester) async { + final presentationEmail = PresentationEmail( + id: EmailId(Id('a123')), + from: { + EmailAddress('example', 'example@linagora.com') + }, + emailHeader: [ + EmailHeader(EmailProperty.headerSMimeStatusKey, 'Good signature') + ] + ); + final widget = makeTestableWidget( + child: InformationSenderAndReceiverBuilder( + emailSelected: presentationEmail, + responsiveUtils: responsiveUtils, + imagePaths: imagePaths, + emailUnsubscribe: null, + ), + ); + + await tester.pumpWidget(widget); + + await tester.pumpAndSettle(); + + expect( + find.byKey(const Key('smime_signature_status_icon')), + findsOneWidget); + + final sMimeSignatureStatusIconWidgetFinder = find.byKey(const Key('smime_signature_status_icon')); + final sMimeSignatureStatusIconWidget = tester.widget(sMimeSignatureStatusIconWidgetFinder); + expect( + sMimeSignatureStatusIconWidget.message, + 'The authenticity of this message had been verified with SMime signature.'); + }); + + testWidgets( + 'should be displayed and have bad message \n' + 'when email header has X-SMIME-Status = "Bad signature"', + (tester) async { + final presentationEmail = PresentationEmail( + id: EmailId(Id('a123')), + from: { + EmailAddress('example', 'example@linagora.com') + }, + emailHeader: [ + EmailHeader(EmailProperty.headerSMimeStatusKey, 'Bad signature') + ] + ); + final widget = makeTestableWidget( + child: InformationSenderAndReceiverBuilder( + emailSelected: presentationEmail, + responsiveUtils: responsiveUtils, + imagePaths: imagePaths, + emailUnsubscribe: null, + ), + ); + + await tester.pumpWidget(widget); + + await tester.pumpAndSettle(); + + expect( + find.byKey(const Key('smime_signature_status_icon')), + findsOneWidget); + + final sMimeSignatureStatusIconWidgetFinder = find.byKey(const Key('smime_signature_status_icon')); + final sMimeSignatureStatusIconWidget = tester.widget(sMimeSignatureStatusIconWidgetFinder); + expect( + sMimeSignatureStatusIconWidget.message, + 'This message failed SMime signature verification.'); + }); + + testWidgets('should not be displayed when email header do not have X-SMIME-Status', (tester) async { + final presentationEmail = PresentationEmail( + id: EmailId(Id('a123')), + from: { + EmailAddress('example', 'example@linagora.com') + } + ); + final widget = makeTestableWidget( + child: InformationSenderAndReceiverBuilder( + emailSelected: presentationEmail, + responsiveUtils: responsiveUtils, + imagePaths: imagePaths, + emailUnsubscribe: null, + ), + ); + + await tester.pumpWidget(widget); + + await tester.pumpAndSettle(); + + expect( + find.byKey(const Key('smime_signature_status_icon')), + findsNothing); + }); + + testWidgets('should not be displayed when email header have X-SMIME-Status = "Good Signatures"', (tester) async { + final presentationEmail = PresentationEmail( + id: EmailId(Id('a123')), + from: { + EmailAddress('example', 'example@linagora.com') + }, + emailHeader: [ + EmailHeader(EmailProperty.headerSMimeStatusKey, 'Good Signatures') + ] + ); + final widget = makeTestableWidget( + child: InformationSenderAndReceiverBuilder( + emailSelected: presentationEmail, + responsiveUtils: responsiveUtils, + imagePaths: imagePaths, + emailUnsubscribe: null, + ), + ); + + await tester.pumpWidget(widget); + + await tester.pumpAndSettle(); + + expect( + find.byKey(const Key('smime_signature_status_icon')), + findsNothing); + }); + + testWidgets('should not be displayed when email header have X-SMIME-Status = "Good signatures"', (tester) async { + final presentationEmail = PresentationEmail( + id: EmailId(Id('a123')), + from: { + EmailAddress('example', 'example@linagora.com') + }, + emailHeader: [ + EmailHeader(EmailProperty.headerSMimeStatusKey, 'Good signatures') + ] + ); + final widget = makeTestableWidget( + child: InformationSenderAndReceiverBuilder( + emailSelected: presentationEmail, + responsiveUtils: responsiveUtils, + imagePaths: imagePaths, + emailUnsubscribe: null, + ), + ); + + await tester.pumpWidget(widget); + + await tester.pumpAndSettle(); + + expect( + find.byKey(const Key('smime_signature_status_icon')), + findsNothing); + }); + + testWidgets('should not be displayed when email header have X-SMIME-Status = "Bad Signatures"', (tester) async { + final presentationEmail = PresentationEmail( + id: EmailId(Id('a123')), + from: { + EmailAddress('example', 'example@linagora.com') + }, + emailHeader: [ + EmailHeader(EmailProperty.headerSMimeStatusKey, 'Bad Signatures') + ] + ); + final widget = makeTestableWidget( + child: InformationSenderAndReceiverBuilder( + emailSelected: presentationEmail, + responsiveUtils: responsiveUtils, + imagePaths: imagePaths, + emailUnsubscribe: null, + ), + ); + + await tester.pumpWidget(widget); + + await tester.pumpAndSettle(); + + expect( + find.byKey(const Key('smime_signature_status_icon')), + findsNothing); + }); + + testWidgets('should not be displayed when email header have X-SMIME-Status = "Bad signatures"', (tester) async { + final presentationEmail = PresentationEmail( + id: EmailId(Id('a123')), + from: { + EmailAddress('example', 'example@linagora.com') + }, + emailHeader: [ + EmailHeader(EmailProperty.headerSMimeStatusKey, 'Bad signatures') + ] + ); + final widget = makeTestableWidget( + child: InformationSenderAndReceiverBuilder( + emailSelected: presentationEmail, + responsiveUtils: responsiveUtils, + imagePaths: imagePaths, + emailUnsubscribe: null, + ), + ); + + await tester.pumpWidget(widget); + + await tester.pumpAndSettle(); + + expect( + find.byKey(const Key('smime_signature_status_icon')), + findsNothing); + }); + }); + }); +} \ No newline at end of file