Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ extension EmailActionTypeExtension on EmailActionType {
case EmailActionType.moveToMailbox:
return AppLocalizations.of(context).movedToFolder(destinationPath ?? '');
case EmailActionType.moveToTrash:
return AppLocalizations.of(context).moved_to_trash;
return destinationPath?.trim().isNotEmpty == true
? AppLocalizations.of(context).movedToFolder(destinationPath!)
: AppLocalizations.of(context).moved_to_trash;
case EmailActionType.moveToSpam:
return AppLocalizations.of(context).marked_as_spam;
case EmailActionType.unSpam:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ import 'package:tmail_ui_user/features/mailbox/presentation/action/mailbox_ui_ac
import 'package:tmail_ui_user/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/action/download_ui_action.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart';
import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.dart';
import 'package:tmail_ui_user/features/mailbox/domain/exceptions/mailbox_exception.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/get_mailbox_contain_extension.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/get_trash_mailbox_id_and_path_extension.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/handle_download_attachment_extension.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/handle_preview_attachment_extension.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/open_and_close_composer_extension.dart';
Expand Down Expand Up @@ -743,24 +747,51 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
}

void moveToTrash(PresentationEmail email) {
if (session != null && accountId != null) {
final moveActionRequest = emailActionReactor.moveToTrash(
email,
mapMailbox: mailboxDashBoardController.mapMailboxById,
selectedMailbox: mailboxDashBoardController.selectedMailbox.value,
isSearchEmailRunning: mailboxDashBoardController.searchController.isSearchEmailRunning,
mapDefaultMailboxIdByRole: mailboxDashBoardController.mapDefaultMailboxIdByRole,
if (session == null) {
mailboxDashBoardController.emitMoveToTrashFailure(
NotFoundSessionException(),
);
if (moveActionRequest == null) return;
mailboxDashBoardController.moveToMailbox(
session!,
accountId!,
moveActionRequest.moveRequest,
moveActionRequest.emailIdsWithReadStatus,
return;
}

if (accountId == null) {
mailboxDashBoardController.emitMoveToTrashFailure(
NotFoundAccountIdException(),
);
if (_threadDetailController?.emailIdsPresentation.length == 1) {
_threadDetailController?.closeThreadDetailAction();
}
return;
}

final currentMailbox = mailboxDashBoardController.getMailboxContain(email);
if (currentMailbox == null) {
Comment on lines +764 to +765
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Virtual folders still mask the real mailbox here.

mailboxDashBoardController.getMailboxContain(email) returns selectedMailbox whenever search isn't running (lib/features/mailbox_dashboard/presentation/extensions/get_mailbox_contain_extension.dart:6-15). If the message is opened from Favorites or another virtual folder, currentMailbox becomes that presentation-only mailbox instead of the email’s actual team mailbox, so the trash lookup below falls back to personal Trash. Mirror the bulk-action virtual-folder guard here and resolve from email.findMailboxContain(...) in that case.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/features/email/presentation/controller/single_email_controller.dart`
around lines 764 - 765, The code uses
mailboxDashBoardController.getMailboxContain(email) which can return a
presentation-only virtual mailbox (e.g., selectedMailbox) and thus masks the
email's real team mailbox; update the logic in the single_email_controller to
detect virtual/presentation mailboxes (same guard used for bulk actions) and
when encountered call email.findMailboxContain(...) to resolve the actual
containing mailbox before doing the trash lookup — replace or fallback the
currentMailbox value from mailboxDashBoardController.getMailboxContain with the
result of email.findMailboxContain(...) when the mailbox is a
virtual/presentation mailbox.

mailboxDashBoardController.emitMoveToTrashFailure(
NotFoundMailboxOfEmailException(),
);
return;
}

final (:trashId, :trashPath) =
mailboxDashBoardController.getTrashMailboxIdAndPath(currentMailbox);
if (trashId == null) {
mailboxDashBoardController.emitMoveToTrashFailure(
NotFoundTrashMailboxException(),
);
return;
}

final moveActionRequest = emailActionReactor.buildMoveToTrashRequest(
email,
trashMailboxId: trashId,
currentMailbox: currentMailbox,
trashMailboxPath: trashPath,
);
mailboxDashBoardController.moveToMailbox(
session!,
accountId!,
moveActionRequest.moveRequest,
moveActionRequest.emailIdsWithReadStatus,
);
if (_threadDetailController?.emailIdsPresentation.length == 1) {
_threadDetailController?.closeThreadDetailAction();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,30 +160,20 @@ class EmailActionReactor {
({
MoveToMailboxRequest moveRequest,
Map<EmailId, bool> emailIdsWithReadStatus
})? moveToTrash(
}) buildMoveToTrashRequest(
PresentationEmail presentationEmail, {
required Map<MailboxId, PresentationMailbox> mapMailbox,
required PresentationMailbox? selectedMailbox,
required bool isSearchEmailRunning,
required Map<Role, MailboxId> mapDefaultMailboxIdByRole,
required MailboxId trashMailboxId,
required PresentationMailbox currentMailbox,
String? trashMailboxPath,
}) {
final trashMailboxId = mapDefaultMailboxIdByRole[
PresentationMailbox.roleTrash
];
final currentMailbox = _getMailboxContain(
presentationEmail,
mapMailbox: mapMailbox,
isSearchEmailRunning: isSearchEmailRunning,
selectedMailbox: selectedMailbox,
);
if (trashMailboxId == null || currentMailbox == null) return null;

return (
moveRequest: MoveToMailboxRequest(
{currentMailbox.id: [presentationEmail.id!]},
trashMailboxId,
MoveAction.moving,
EmailActionType.moveToTrash),
EmailActionType.moveToTrash,
destinationPath: trashMailboxPath,
),
emailIdsWithReadStatus: {presentationEmail.id!: presentationEmail.hasRead},
);
}
Expand Down
14 changes: 14 additions & 0 deletions lib/features/mailbox/domain/exceptions/mailbox_exception.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,17 @@ class CannotMoveAllEmailException extends AppBaseException {
@override
String get exceptionName => 'CannotMoveAllEmailException';
}

class NotFoundMailboxOfEmailException extends AppBaseException {
NotFoundMailboxOfEmailException([super.message]);

@override
String get exceptionName => 'NotFoundMailboxOfEmailException';
}

class NotFoundTrashMailboxException extends AppBaseException {
NotFoundTrashMailboxException([super.message]);

@override
String get exceptionName => 'NotFoundTrashMailboxException';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart';
import 'package:jmap_dart_client/jmap/mail/mailbox/namespace.dart';
import 'package:model/mailbox/presentation_mailbox.dart';
import 'package:tmail_ui_user/features/mailbox/presentation/mailbox_controller.dart';
import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_node.dart';
import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_tree.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart';

mixin HandleTeamMailboxMixin {
MailboxTree? get _teamMailboxesTree =>
getBinding<MailboxController>()?.teamMailboxesTree.value;

MailboxNode? _findTeamMailboxNodeByNamespaceOnFirstLevel(
Namespace namespace,
) {
return _teamMailboxesTree?.findNodeOnFirstLevel(
(node) => node.item.namespace == namespace,
);
}

PresentationMailbox? _findDefaultMailboxInTeamMailbox({
required Namespace namespace,
required String mailboxName,
}) {
final teamMailboxNode =
_findTeamMailboxNodeByNamespaceOnFirstLevel(namespace);
if (teamMailboxNode == null) return null;

final mailboxNode = teamMailboxNode.findNodeOnFirstLevel(
(node) =>
node.mailboxNameAsString.toLowerCase() == mailboxName.toLowerCase(),
);
if (mailboxNode == null) return null;

return mailboxNode.item;
}

MailboxId? findDefaultMailboxIdInTeamMailbox({
required Namespace namespace,
required String mailboxName,
}) {
final teamMailbox = _findDefaultMailboxInTeamMailbox(
namespace: namespace,
mailboxName: mailboxName,
);
return teamMailbox?.id;
}

String? getTeamMailboxNodePathWithSeparator({
required MailboxId mailboxId,
String pathSeparator = '/',
}) {
return _teamMailboxesTree?.getNodePath(mailboxId, pathSeparator);
}
}
5 changes: 5 additions & 0 deletions lib/features/mailbox/presentation/model/mailbox_node.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:collection/collection.dart';
import 'package:equatable/equatable.dart';
import 'package:jmap_dart_client/jmap/core/id.dart';
import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart';
Expand All @@ -6,6 +7,7 @@ import 'package:model/mailbox/expand_mode.dart';
import 'package:model/mailbox/mailbox_state.dart';
import 'package:model/mailbox/presentation_mailbox.dart';
import 'package:model/mailbox/select_mode.dart';
import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_tree.dart';

class MailboxNode with EquatableMixin {
static final PresentationMailbox _root = PresentationMailbox(MailboxId(Id('root')));
Expand Down Expand Up @@ -172,4 +174,7 @@ extension MailboxNodeExtension on MailboxNode {

return item.sortOrder!.value.value.compareTo(other.item.sortOrder!.value.value);
}

MailboxNode? findNodeOnFirstLevel(NodeQuery nodeQuery) =>
childrenItems?.firstWhereOrNull(nodeQuery);
}
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/handle_reactive_obx_variable_extension.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/handle_save_email_as_draft_extension.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/handle_store_email_sort_order_extension.dart';
import 'package:tmail_ui_user/features/mailbox/presentation/mixin/handle_team_mailbox_mixin.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/initialize_app_language.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/labels/handle_logic_label_extension.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/notify_thread_detail_setting_updated.dart';
Expand Down Expand Up @@ -232,6 +233,7 @@ class MailboxDashBoardController extends ReloadableController
OwnEmailAddressMixin,
SaaSPremiumMixin,
AiScribeMixin,
HandleTeamMailboxMixin,
SearchLabelFilterModalMixin {

final RemoveEmailDraftsInteractor _removeEmailDraftsInteractor = Get.find<RemoveEmailDraftsInteractor>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart';
import 'package:model/email/email_action_type.dart';
import 'package:model/extensions/presentation_mailbox_extension.dart';
import 'package:model/mailbox/presentation_mailbox.dart';
import 'package:tmail_ui_user/features/email/domain/state/move_to_mailbox_state.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart';

extension GetTrashMailboxIdAndPathExtension on MailboxDashBoardController {
({MailboxId? trashId, String? trashPath}) getTrashMailboxIdAndPath(
PresentationMailbox emailMailbox,
) {
final defaultResult = (
trashId: mapDefaultMailboxIdByRole[PresentationMailbox.roleTrash],
trashPath: null as String?,
);

final mailbox = selectedMailbox.value ?? emailMailbox;

if (mailbox.isPersonal) return defaultResult;

final namespace = mailbox.namespace;
if (namespace == null) return defaultResult;

final trashId = findDefaultMailboxIdInTeamMailbox(
namespace: namespace,
mailboxName: PresentationMailbox.trashRole,
);
if (trashId == null) return defaultResult;

final trashPath = getTeamMailboxNodePathWithSeparator(
mailboxId: trashId,
);
return (trashId: trashId, trashPath: trashPath);
}

void emitMoveToTrashFailure(Exception exception) {
emitFailure(
controller: this,
failure: MoveToMailboxFailure(
EmailActionType.moveToTrash,
exception: exception,
),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import 'package:tmail_ui_user/features/email/domain/model/move_to_mailbox_reques
import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.dart';
import 'package:tmail_ui_user/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/get_trash_mailbox_id_and_path_extension.dart';
import 'package:tmail_ui_user/features/thread/domain/state/move_multiple_email_to_mailbox_state.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart';

Expand All @@ -29,7 +30,15 @@ extension HandleActionTypeForEmailSelection on MailboxDashBoardController {
} else if (actionType == EmailActionType.moveToSpam) {
destinationMailboxId = spamMailboxId;
} else if (actionType == EmailActionType.moveToTrash) {
destinationMailboxId = getMailboxIdByRole(PresentationMailbox.roleTrash);
if (selectedMailbox.value?.isChildOfTeamMailboxes == true) {
final (:trashId, :trashPath) =
getTrashMailboxIdAndPath(selectedMailbox.value!);
destinationMailboxId = trashId;
destinationFolderPath = trashPath;
} else {
destinationMailboxId =
getMailboxIdByRole(PresentationMailbox.roleTrash);
Comment on lines 32 to +40
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't resolve one trash mailbox from selectedMailbox.

This branch ignores selectedMailboxId and picks a single destination from the current selection, but the loop below already handles search/virtual-folder selections per email. In global search or virtual folders, team-mailbox messages will still be sent to the personal Trash, and a Trash folder explicitly chosen in the destination picker can be silently replaced. Resolve the trash mailbox per source mailbox, or at least honor selectedMailboxId when it is provided, before building the request.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@lib/features/mailbox_dashboard/presentation/extensions/handle_action_type_for_email_selection.dart`
around lines 32 - 40, The moveToTrash branch in
handle_action_type_for_email_selection.dart currently resolves a single trash
destination from selectedMailbox regardless of selectedMailboxId or per-source
mailboxes; update the logic in the EmailActionType.moveToTrash branch (where
getTrashMailboxIdAndPath, getMailboxIdByRole, destinationMailboxId and
destinationFolderPath are used) to either (a) when selectedMailboxId is
provided, use that mailbox's trash (honor selectedMailboxId) or (b) if operating
on multiple source mailboxes (e.g. search/virtual-folder selections), resolve
the trash mailbox per source mailbox by calling getTrashMailboxIdAndPath for
each source mailboxId before building the request so team mailboxes map to their
own Trash and a chosen Trash in the destination picker is not overwritten.

}
} else if (actionType == EmailActionType.archiveMessage) {
destinationMailboxId = getMailboxIdByRole(PresentationMailbox.roleArchive);
}
Expand Down
58 changes: 46 additions & 12 deletions lib/features/thread/presentation/mixin/email_action_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@ import 'package:tmail_ui_user/features/email/domain/model/mark_read_action.dart'
import 'package:tmail_ui_user/features/email/domain/model/move_action.dart';
import 'package:tmail_ui_user/features/email/domain/model/move_to_mailbox_request.dart';
import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart';
import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.dart';
import 'package:tmail_ui_user/features/mailbox/domain/exceptions/mailbox_exception.dart';
import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_actions.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/get_trash_mailbox_id_and_path_extension.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/handle_action_type_for_email_selection.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/open_and_close_composer_extension.dart';
import 'package:tmail_ui_user/features/thread/presentation/model/delete_action_type.dart';
Expand Down Expand Up @@ -59,23 +62,54 @@ mixin EmailActionController {
mailboxDashBoardController.openEmailDetailedView(presentationEmail);
}

void moveToTrash(PresentationEmail email, {PresentationMailbox? mailboxContain}) async {
void moveToTrash(
PresentationEmail email, {
PresentationMailbox? mailboxContain,
}) {
if (mailboxContain == null) {
mailboxDashBoardController.emitMoveToTrashFailure(
NotFoundMailboxOfEmailException(),
);
return;
}

final session = mailboxDashBoardController.sessionCurrent;
if (session == null) {
mailboxDashBoardController.emitMoveToTrashFailure(
NotFoundSessionException(),
);
return;
}

final accountId = mailboxDashBoardController.accountId.value;
final trashMailboxId = mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleTrash];
if (accountId == null) {
mailboxDashBoardController.emitMoveToTrashFailure(
NotFoundAccountIdException(),
);
return;
}

if (session != null && mailboxContain != null && accountId != null && trashMailboxId != null) {
_moveToTrashAction(
session,
accountId,
MoveToMailboxRequest(
{mailboxContain.id: email.id != null ? [email.id!] : []},
trashMailboxId,
MoveAction.moving,
EmailActionType.moveToTrash),
email.id != null ? {email.id! : email.hasRead} : {},
final (:trashId, :trashPath) =
mailboxDashBoardController.getTrashMailboxIdAndPath(mailboxContain);
Comment on lines +92 to +93
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Make the trash lookup honor mailboxContain.

getTrashMailboxIdAndPath() currently prefers selectedMailbox.value over the explicit argument (lib/features/mailbox_dashboard/presentation/extensions/get_trash_mailbox_id_and_path_extension.dart:17-22). In favorites or other virtual-selection contexts, this delete path can ignore the email’s real team mailbox and resolve the wrong trash namespace. Please make the helper prefer the method argument, or resolve the namespace locally here before building the request.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/features/thread/presentation/mixin/email_action_controller.dart` around
lines 92 - 93, The delete/trash lookup currently ignores the explicit
mailboxContain and uses mailboxDashBoardController.selectedMailbox.value; update
the logic so getTrashMailboxIdAndPath or this call honors the passed-in
mailboxContain: either modify getTrashMailboxIdAndPath to prefer its mailbox
argument over selectedMailbox.value, or resolve the correct trash namespace
locally in email_action_controller.dart by deriving the trashId/trashPath from
the mailboxContain before building the request (use the same resolution logic as
in getTrashMailboxIdAndPath but seeded with mailboxContain). Ensure you
reference getTrashMailboxIdAndPath, mailboxDashBoardController, and
mailboxContain when making the change so the explicit mailbox argument is always
preferred.

if (trashId == null) {
mailboxDashBoardController.emitMoveToTrashFailure(
NotFoundTrashMailboxException(),
);
return;
}

_moveToTrashAction(
session,
accountId,
MoveToMailboxRequest(
{mailboxContain.id: email.id != null ? [email.id!] : []},
trashId,
MoveAction.moving,
EmailActionType.moveToTrash,
destinationPath: trashPath,
),
email.id != null ? {email.id!: email.hasRead} : {},
);
}

void _moveToTrashAction(
Expand Down
Loading
Loading