diff --git a/assets/images/ic_copy.svg b/assets/images/ic_copy.svg
new file mode 100644
index 000000000..558c78aa8
--- /dev/null
+++ b/assets/images/ic_copy.svg
@@ -0,0 +1,3 @@
+
diff --git a/core/lib/presentation/resources/image_paths.dart b/core/lib/presentation/resources/image_paths.dart
index a7c312c35..7f1acc88b 100644
--- a/core/lib/presentation/resources/image_paths.dart
+++ b/core/lib/presentation/resources/image_paths.dart
@@ -223,6 +223,7 @@ class ImagePaths {
String get icLogoTwakeWelcome => _getImagePath('ic_logo_twake_welcome.svg');
String get icHelp => _getImagePath('ic_help.svg');
String get icDownloadAll => _getImagePath('ic_download_all.svg');
+ String get icCopy => _getImagePath('ic_copy.svg');
String get icSubaddressingAllow => _getImagePath('ic_subaddressing_allow.svg');
String get icSubaddressingDisallow => _getImagePath('ic_subaddressing_disallow.svg');
diff --git a/lib/features/base/base_mailbox_controller.dart b/lib/features/base/base_mailbox_controller.dart
index cf1f30969..942ba18fd 100644
--- a/lib/features/base/base_mailbox_controller.dart
+++ b/lib/features/base/base_mailbox_controller.dart
@@ -216,14 +216,18 @@ abstract class BaseMailboxController extends BaseController {
return teamMailboxesTree.value.findNode((node) => node.item.id == mailboxId);
}
- String? findNodePath(MailboxId mailboxId) {
- var mailboxNodePath = defaultMailboxTree.value.getNodePath(mailboxId)
- ?? personalMailboxTree.value.getNodePath(mailboxId)
- ?? teamMailboxesTree.value.getNodePath(mailboxId);
+ String? findNodePathWithSeparator(MailboxId mailboxId, String pathSeparator) {
+ var mailboxNodePath = defaultMailboxTree.value.getNodePath(mailboxId, pathSeparator)
+ ?? personalMailboxTree.value.getNodePath(mailboxId, pathSeparator)
+ ?? teamMailboxesTree.value.getNodePath(mailboxId, pathSeparator);
log('BaseMailboxController::findNodePath():mailboxNodePath: $mailboxNodePath');
return mailboxNodePath;
}
+ String? findNodePath(MailboxId mailboxId) {
+ return findNodePathWithSeparator(mailboxId, '/');
+ }
+
MailboxNode? findMailboxNodeByRole(Role role) {
final mailboxNode = defaultMailboxTree.value.findNode((node) => node.item.role == role);
return mailboxNode;
diff --git a/lib/features/mailbox/domain/exceptions/empty_folder_name_exception.dart b/lib/features/mailbox/domain/exceptions/empty_folder_name_exception.dart
new file mode 100644
index 000000000..5a3ff99dd
--- /dev/null
+++ b/lib/features/mailbox/domain/exceptions/empty_folder_name_exception.dart
@@ -0,0 +1,9 @@
+
+class EmptyFolderNameException implements Exception {
+ final String folderName;
+
+ EmptyFolderNameException(this.folderName);
+
+ @override
+ String toString() => 'EmptyFolderNameException: Folder name should not be empty: $folderName';
+}
diff --git a/lib/features/mailbox/domain/exceptions/invalid_mail_format_exception.dart b/lib/features/mailbox/domain/exceptions/invalid_mail_format_exception.dart
new file mode 100644
index 000000000..bd15b87b2
--- /dev/null
+++ b/lib/features/mailbox/domain/exceptions/invalid_mail_format_exception.dart
@@ -0,0 +1,9 @@
+
+class InvalidMailFormatException implements Exception {
+ final String mail;
+
+ InvalidMailFormatException(this.mail);
+
+ @override
+ String toString() => 'InvalidMailFormatException: $mail';
+}
diff --git a/lib/features/mailbox/presentation/mailbox_controller.dart b/lib/features/mailbox/presentation/mailbox_controller.dart
index 1958f89d8..b1b719aba 100644
--- a/lib/features/mailbox/presentation/mailbox_controller.dart
+++ b/lib/features/mailbox/presentation/mailbox_controller.dart
@@ -5,6 +5,7 @@ import 'package:core/core.dart';
import 'package:dartz/dartz.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:jmap_dart_client/jmap/account_id.dart';
@@ -30,6 +31,8 @@ import 'package:tmail_ui_user/features/email/presentation/model/composer_argumen
import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.dart';
import 'package:tmail_ui_user/features/home/domain/extensions/session_extensions.dart';
import 'package:tmail_ui_user/features/mailbox/domain/constants/mailbox_constants.dart';
+import 'package:tmail_ui_user/features/mailbox/domain/exceptions/empty_folder_name_exception.dart';
+import 'package:tmail_ui_user/features/mailbox/domain/exceptions/invalid_mail_format_exception.dart';
import 'package:tmail_ui_user/features/mailbox/domain/exceptions/set_mailbox_name_exception.dart';
import 'package:tmail_ui_user/features/mailbox/domain/exceptions/null_session_or_accountid_exception.dart';
import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart';
@@ -1248,6 +1251,14 @@ class MailboxController extends BaseMailboxController
case MailboxActions.openInNewTab:
openMailboxInNewTabAction(mailbox);
break;
+ case MailboxActions.copySubaddress:
+ try{
+ final subaddress = getSubaddress(mailboxDashBoardController.userEmail, findNodePathWithSeparator(mailbox.id, '.')!);
+ copySubaddressAction(context, subaddress);
+ } catch (error) {
+ appToast.showToastErrorMessage(context, AppLocalizations.of(context).errorWhileFetchingSubaddress);
+ }
+ break;
case MailboxActions.disableSpamReport:
case MailboxActions.enableSpamReport:
mailboxDashBoardController.storeSpamReportStateAction();
@@ -1632,4 +1643,22 @@ class MailboxController extends BaseMailboxController
return session.getContactSupportCapability(accountId);
}
+
+ void copySubaddressAction(BuildContext context, String subaddress) {
+ Clipboard.setData(ClipboardData(text: subaddress));
+ appToast.showToastSuccessMessage(context, AppLocalizations.of(context).emailSubaddressCopiedToClipboard);
+ }
+
+ String getSubaddress(String userEmail, String folderName) {
+ if (folderName.isEmpty) {
+ throw EmptyFolderNameException(folderName);
+ }
+
+ final atIndex = userEmail.indexOf('@');
+ if (atIndex <= 0 || atIndex == userEmail.length - 1) {
+ throw InvalidMailFormatException(userEmail);
+ }
+
+ return '${userEmail.substring(0, atIndex)}+$folderName@${userEmail.substring(atIndex + 1)}';
+ }
}
\ No newline at end of file
diff --git a/lib/features/mailbox/presentation/mixin/mailbox_widget_mixin.dart b/lib/features/mailbox/presentation/mixin/mailbox_widget_mixin.dart
index d1abfa29b..06f2b5fa7 100644
--- a/lib/features/mailbox/presentation/mixin/mailbox_widget_mixin.dart
+++ b/lib/features/mailbox/presentation/mixin/mailbox_widget_mixin.dart
@@ -66,6 +66,8 @@ mixin MailboxWidgetMixin {
MailboxActions.disallowSubaddressing
else
MailboxActions.allowSubaddressing,
+ if (mailbox.isSubaddressingAllowed)
+ MailboxActions.copySubaddress,
if (mailbox.isSubscribedMailbox)
MailboxActions.disableMailbox
else
diff --git a/lib/features/mailbox/presentation/model/mailbox_actions.dart b/lib/features/mailbox/presentation/model/mailbox_actions.dart
index 2cb018b00..e010aec1b 100644
--- a/lib/features/mailbox/presentation/model/mailbox_actions.dart
+++ b/lib/features/mailbox/presentation/model/mailbox_actions.dart
@@ -16,6 +16,7 @@ enum MailboxActions {
markAsRead,
selectForRuleAction,
openInNewTab,
+ copySubaddress,
disableSpamReport,
enableSpamReport,
disableMailbox,
@@ -50,6 +51,8 @@ extension MailboxActionsExtension on MailboxActions {
switch(this) {
case MailboxActions.openInNewTab:
return AppLocalizations.of(context).openInNewTab;
+ case MailboxActions.copySubaddress:
+ return AppLocalizations.of(context).copySubaddress;
case MailboxActions.newSubfolder:
return AppLocalizations.of(context).newSubfolder;
case MailboxActions.disableSpamReport:
@@ -89,6 +92,8 @@ extension MailboxActionsExtension on MailboxActions {
switch(this) {
case MailboxActions.openInNewTab:
return imagePaths.icOpenInNewTab;
+ case MailboxActions.copySubaddress:
+ return imagePaths.icCopy;
case MailboxActions.newSubfolder:
return imagePaths.icAddNewFolder;
case MailboxActions.disableSpamReport:
@@ -181,6 +186,7 @@ extension MailboxActionsExtension on MailboxActions {
ContextMenuItemState getContextMenuItemState(PresentationMailbox mailbox) {
switch(this) {
case MailboxActions.openInNewTab:
+ case MailboxActions.copySubaddress:
case MailboxActions.newSubfolder:
case MailboxActions.disableSpamReport:
case MailboxActions.enableSpamReport:
diff --git a/lib/features/mailbox/presentation/model/mailbox_tree.dart b/lib/features/mailbox/presentation/model/mailbox_tree.dart
index 2b1b54ecc..d13953a5b 100644
--- a/lib/features/mailbox/presentation/model/mailbox_tree.dart
+++ b/lib/features/mailbox/presentation/model/mailbox_tree.dart
@@ -121,7 +121,7 @@ class MailboxTree with EquatableMixin {
return false;
}
- String? getNodePath(MailboxId mailboxId) {
+ String? getNodePath(MailboxId mailboxId, String pathSeparator) {
final matchedNode = findNode((node) => node.item.id == mailboxId);
if (matchedNode == null) {
return null;
@@ -141,9 +141,9 @@ class MailboxTree with EquatableMixin {
break;
}
if (currentContext != null) {
- path = '${parentNode.item.getDisplayName(currentContext!)}/$path';
+ path = '${parentNode.item.getDisplayName(currentContext!)}$pathSeparator$path';
} else {
- path = '${parentNode.item.name?.name}/$path';
+ path = '${parentNode.item.name?.name}$pathSeparator$path';
}
parentId = parentNode.item.parentId;
}
diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb
index 9b2862ae5..6c4ed7bb7 100644
--- a/lib/l10n/intl_messages.arb
+++ b/lib/l10n/intl_messages.arb
@@ -784,6 +784,12 @@
"placeholders_order": [],
"placeholders": {}
},
+ "emailSubaddressCopiedToClipboard": "Email subaddress copied to clipboard",
+ "@emailSubaddressCopiedToClipboard": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
"minimize": "Minimize",
"@minimize": {
"type": "text",
@@ -2592,6 +2598,12 @@
"placeholders_order": [],
"placeholders": {}
},
+ "copySubaddress": "Copy subaddress",
+ "@copySubaddress": {
+ "type": "text",
+ "placeholders_order": [],
+ "placeholders": {}
+ },
"regards": "Regards",
"@regards": {
"type": "text",
diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart
index 5c36da0f7..214e47a57 100644
--- a/lib/main/localizations/app_localizations.dart
+++ b/lib/main/localizations/app_localizations.dart
@@ -800,6 +800,12 @@ class AppLocalizations {
name: 'email_address_copied_to_clipboard');
}
+ String get emailSubaddressCopiedToClipboard {
+ return Intl.message(
+ 'Email subaddress copied to clipboard',
+ name: 'emailSubaddressCopiedToClipboard');
+ }
+
String get minimize {
return Intl.message(
'Minimize',
@@ -2654,6 +2660,13 @@ class AppLocalizations {
);
}
+ String get copySubaddress {
+ return Intl.message(
+ 'Copy subaddress',
+ name: 'copySubaddress',
+ );
+ }
+
String get regards {
return Intl.message(
'Regards',
@@ -3005,6 +3018,13 @@ class AppLocalizations {
);
}
+ String get errorWhileFetchingSubaddress {
+ return Intl.message(
+ 'Error while fetching the subaddress',
+ name: 'errorWhileFetchingSubaddress',
+ );
+ }
+
String get connectedToTheInternet {
return Intl.message(
'Connected to the internet',
diff --git a/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart b/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart
index 7401027b1..ae1465991 100644
--- a/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart
+++ b/test/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller_test.dart
@@ -36,6 +36,8 @@ import 'package:tmail_ui_user/features/login/domain/usecases/delete_authority_oi
import 'package:tmail_ui_user/features/login/domain/usecases/delete_credential_interactor.dart';
import 'package:tmail_ui_user/features/login/domain/usecases/get_authenticated_account_interactor.dart';
import 'package:tmail_ui_user/features/login/domain/usecases/update_account_cache_interactor.dart';
+import 'package:tmail_ui_user/features/mailbox/domain/exceptions/empty_folder_name_exception.dart';
+import 'package:tmail_ui_user/features/mailbox/domain/exceptions/invalid_mail_format_exception.dart';
import 'package:tmail_ui_user/features/mailbox/domain/usecases/create_new_default_mailbox_interactor.dart';
import 'package:tmail_ui_user/features/mailbox/domain/usecases/create_new_mailbox_interactor.dart';
import 'package:tmail_ui_user/features/mailbox/domain/usecases/delete_multiple_mailbox_interactor.dart';
@@ -580,4 +582,81 @@ void main() {
expect(spamId, isNull);
});
});
+
+ group('getSubaddress:test', () {
+ setUp(() {
+ getEmailsInMailboxInteractor = MockGetEmailsInMailboxInteractor();
+
+ when(emailReceiveManager.pendingSharedFileInfo).thenAnswer((_) => BehaviorSubject.seeded([]));
+
+ Get.put(mailboxDashboardController);
+ mailboxDashboardController.onReady();
+
+ mailboxController = MailboxController(
+ createNewMailboxInteractor,
+ deleteMultipleMailboxInteractor,
+ renameMailboxInteractor,
+ moveMailboxInteractor,
+ subscribeMailboxInteractor,
+ subscribeMultipleMailboxInteractor,
+ subaddressingInteractor,
+ createDefaultMailboxInteractor,
+ treeBuilder,
+ verifyNameInteractor,
+ getAllMailboxInteractor,
+ refreshAllMailboxInteractor);
+ mailboxController.onReady();
+
+ threadController = ThreadController(
+ getEmailsInMailboxInteractor,
+ refreshChangesEmailsInMailboxInteractor,
+ loadMoreEmailsInMailboxInteractor,
+ searchEmailInteractor,
+ searchMoreEmailInteractor,
+ getEmailByIdInteractor);
+ Get.put(threadController);
+
+ advancedFilterController = AdvancedFilterController();
+
+ mailboxDashboardController.sessionCurrent = testSession;
+ mailboxDashboardController.filterMessageOption.value = FilterMessageOption.all;
+ mailboxDashboardController.accountId.value = testAccountId;
+ });
+
+ test('should return subaddress with valid email and folder name', () {
+ const String userEmail = 'user@example.com';
+ const String folderName = 'folder';
+ final result = mailboxController.getSubaddress(userEmail, folderName);
+
+ expect(result, equals('user+folder@example.com'));
+ });
+
+ test('should throw an error if empty local part', () {
+ const userEmail = '@example.com';
+ const folderName = 'folder';
+
+ expect(() => mailboxController.getSubaddress(userEmail, folderName), throwsA(isA()));
+ });
+
+ test('should throw an error if empty folder name', () {
+ const userEmail = 'user@example.com';
+ const folderName = '';
+
+ expect(() => mailboxController.getSubaddress(userEmail, folderName), throwsA(isA()));
+ });
+
+ test('should throw an error if empty domain', () {
+ const userEmail = 'user@';
+ const folderName = 'folder';
+
+ expect(() => mailboxController.getSubaddress(userEmail, folderName), throwsA(isA()));
+ });
+
+ test('should throw an error if absent `@`', () {
+ const userEmail = 'invalid-email-format';
+ const folderName = 'folder';
+
+ expect(() => mailboxController.getSubaddress(userEmail, folderName), throwsA(isA()));
+ });
+ });
}