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