From 9026cfc9fa73f502bd0aff1e91e3a0b6247bd3ad Mon Sep 17 00:00:00 2001 From: dab246 Date: Mon, 23 Dec 2024 19:15:34 +0700 Subject: [PATCH] TF-3337 Set maximum objects in `Email/Set` method when mark as read/star and move emails --- .../email/data/network/email_api.dart | 275 +++++++++++------- .../domain/model/move_to_mailbox_request.dart | 5 +- lib/main/error/capability_validator.dart | 1 + 3 files changed, 177 insertions(+), 104 deletions(-) diff --git a/lib/features/email/data/network/email_api.dart b/lib/features/email/data/network/email_api.dart index 4f764d31c..44e7b87ba 100644 --- a/lib/features/email/data/network/email_api.dart +++ b/lib/features/email/data/network/email_api.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io'; +import 'dart:math'; import 'dart:typed_data'; import 'package:core/core.dart'; @@ -43,7 +44,7 @@ import 'package:model/account/account_request.dart'; import 'package:model/account/authentication_type.dart'; import 'package:model/download/download_task_id.dart'; import 'package:model/email/attachment.dart'; -import 'package:model/email/email_action_type.dart'; +import 'package:model/email/email_property.dart'; import 'package:model/email/mark_star_action.dart'; import 'package:model/email/read_actions.dart'; import 'package:model/extensions/email_extension.dart'; @@ -60,7 +61,6 @@ import 'package:tmail_ui_user/features/composer/domain/model/email_request.dart' import 'package:tmail_ui_user/features/email/domain/exceptions/email_exceptions.dart'; import 'package:tmail_ui_user/features/email/domain/extensions/email_id_extensions.dart'; import 'package:tmail_ui_user/features/email/domain/model/event_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/domain/model/restore_deleted_message_request.dart'; import 'package:tmail_ui_user/features/email/domain/state/download_attachment_for_web_state.dart'; @@ -243,37 +243,52 @@ class EmailAPI with HandleSetErrorMixin { List emailIds, ReadActions readActions, ) async { - final setEmailMethod = SetEmailMethod(accountId) - ..addUpdates(emailIds.generateMapUpdateObjectMarkAsRead(readActions)); + final maxBatches = _getMaxObjectsInSetMethod(session, accountId); + final totalEmails = emails.length; - final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); + final List updatedEmails = List.empty(growable: true); - final setEmailInvocation = requestBuilder.invocation(setEmailMethod); + for (int start = 0; start < totalEmails; start += maxBatches) { + int end = (start + maxBatches < totalEmails) + ? start + maxBatches + : totalEmails; + log('EmailAPI::markAsRead:emails from ${start + 1} to $end'); - final capabilities = setEmailMethod.requiredCapabilities - .toCapabilitiesSupportTeamMailboxes(session, accountId); + final currentListEmails = emails.sublist(start, end); - final response = await (requestBuilder - ..usings(capabilities)) - .build() - .execute(); + final setEmailMethod = SetEmailMethod(accountId) + ..addUpdates(currentListEmails.listEmailIds.generateMapUpdateObjectMarkAsRead(readActions)); - final setEmailResponse = response.parse( - setEmailInvocation.methodCallId, - SetEmailResponse.deserialize, - ); + final getEmailMethod = GetEmailMethod(accountId) + ..addIds(emails.listEmailIds.toIds().toSet()) + ..addProperties(Properties({EmailProperty.keywords})); - final emailIdUpdated = setEmailResponse?.updated - ?.keys - .map((id) => EmailId(id)) - .toList() ?? []; - final mapErrors = handleSetResponse([setEmailResponse]); + final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); - if (emailIdUpdated.isNotEmpty) { - return emailIdUpdated; - } else { - throw SetMethodException(mapErrors); + requestBuilder.invocation(setEmailMethod); + + final getEmailInvocation = requestBuilder.invocation(getEmailMethod); + + final capabilities = setEmailMethod.requiredCapabilities + .toCapabilitiesSupportTeamMailboxes(session, accountId); + + final response = await (requestBuilder + ..usings(capabilities)) + .build() + .execute(); + + final getEmailResponse = response.parse( + getEmailInvocation.methodCallId, + GetEmailResponse.deserialize, + ); + + final listEmails = getEmailResponse?.list ?? []; + if (listEmails.isNotEmpty) { + updatedEmails.addAll(listEmails); + } } + + return updatedEmails; } Future> downloadAttachments( @@ -387,65 +402,104 @@ class EmailAPI with HandleSetErrorMixin { AccountId accountId, MoveToMailboxRequest moveRequest ) async { - final coreCapability = session.getCapabilityProperties( - accountId, - CapabilityIdentifier.jmapCore - ); - int maxMethodCount = coreCapability?.maxCallsInRequest?.value.toInt() ?? CapabilityIdentifierExtension.defaultMaxCallsInRequest; - log('EmailAPI::moveToMailbox:maxMethodCount: $maxMethodCount'); - int start = 0; - int end = 0; + final maxBatches = _getMaxObjectsInSetMethod(session, accountId); final List listEmailIdResult = List.empty(growable: true); - final listCurrentMailboxesEntries = moveRequest.currentMailboxes.entries.toList(); - while (end < moveRequest.currentMailboxes.length) { - start = end; - if (moveRequest.currentMailboxes.length - start >= maxMethodCount) { - end = maxMethodCount; - } else { - end = moveRequest.currentMailboxes.length; - } - log('EmailAPI::moveToMailbox(): move from $start to $end / ${listCurrentMailboxesEntries.length}'); - final currentExecuteList = listCurrentMailboxesEntries.sublist(start, end); - - final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); - final currentSetEmailInvocations = currentExecuteList.map((currentItem) { - - final moveProperties = (moveRequest.moveAction == MoveAction.moving && moveRequest.emailActionType == EmailActionType.moveToSpam) - ? currentItem.value.generateMapUpdateObjectMoveToSpam(currentItem.key, moveRequest.destinationMailboxId) - : currentItem.value.generateMapUpdateObjectMoveToMailbox(currentItem.key, moveRequest.destinationMailboxId); - - return SetEmailMethod(accountId) - ..addUpdates(moveProperties); - }).map(requestBuilder.invocation).toList(); - - final capabilities = {CapabilityIdentifier.jmapCore, CapabilityIdentifier.jmapMail} - .toCapabilitiesSupportTeamMailboxes(session, accountId); - - final response = await (requestBuilder..usings(capabilities)) - .build() - .execute(); - - Future.sync(() async { - final listSetEmailResponse = currentSetEmailInvocations - .map((currentInvocation) => response.parse(currentInvocation.methodCallId, SetEmailResponse.deserialize)) - .toList(); - - listEmailIdResult.addAll(_getListEmailIdUpdatedFormSetEmailResponse(listSetEmailResponse, moveRequest)); - - }).catchError((error) { - throw error; - }); + final listMailboxIds = moveRequest.currentMailboxes.keys.toList(); + for (int i = 0; i < listMailboxIds.length; i++) { + final currentMailboxId = listMailboxIds[i]; + final listEmailIds = moveRequest.currentMailboxes[currentMailboxId]!; + log('EmailAPI::moveToMailbox:from mailbox ${currentMailboxId.asString} with ${listEmailIds.length} emails to mailbox ${moveRequest.destinationMailboxId.asString}'); + final movedEmailIds = await _moveEmailsBetweenMailboxes( + session: session, + accountId: accountId, + listEmailIds: listEmailIds, + currentMailboxId: currentMailboxId, + maxBatches: maxBatches, + destinationMailboxId: moveRequest.destinationMailboxId, + isMovingToSpam: moveRequest.isMovingToSpam, + ); + listEmailIdResult.addAll(movedEmailIds); } - return listEmailIdResult; } - List _getListEmailIdUpdatedFormSetEmailResponse(List listSetEmailResponse, MoveToMailboxRequest moveRequest) { - final listUpdated = listSetEmailResponse.map((e) => e!.updated!.keys).toList(); - List listEmailIdRequest = moveRequest.currentMailboxes.values.expand((e) => e).toList(); - return listEmailIdRequest.where((emailId) => listUpdated.expand((e) => e).toList().contains(emailId.id)).toList(); + Future> _moveEmailsBetweenMailboxes({ + required Session session, + required AccountId accountId, + required List listEmailIds, + required MailboxId currentMailboxId, + required MailboxId destinationMailboxId, + required int maxBatches, + bool isMovingToSpam = false, + }) async { + final maxBatches = _getMaxObjectsInSetMethod(session, accountId); + final totalEmails = listEmailIds.length; + + final List updatedEmailIds = List.empty(growable: true); + + for (int start = 0; start < totalEmails; start += maxBatches) { + int end = (start + maxBatches < totalEmails) + ? start + maxBatches + : totalEmails; + log('EmailAPI::_moveEmailsBetweenMailboxes:emails from ${start + 1} to $end'); + + final currentEmailIds = listEmailIds.sublist(start, end); + + final moveProperties = isMovingToSpam + ? currentEmailIds.generateMapUpdateObjectMoveToSpam( + currentMailboxId, + destinationMailboxId, + ) + : currentEmailIds.generateMapUpdateObjectMoveToMailbox( + currentMailboxId, + destinationMailboxId, + ); + + final setEmailMethod = SetEmailMethod(accountId) + ..addUpdates(moveProperties); + + final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); + + final setEmailInvocation = requestBuilder.invocation(setEmailMethod); + + final capabilities = setEmailMethod.requiredCapabilities + .toCapabilitiesSupportTeamMailboxes(session, accountId); + + final response = await (requestBuilder + ..usings(capabilities)) + .build() + .execute(); + + final setEmailResponse = response.parse( + setEmailInvocation.methodCallId, + SetEmailResponse.deserialize, + ); + + final listIdsUpdated = setEmailResponse?.updated?.keys ?? []; + if (listIdsUpdated.isNotEmpty == true) { + final listEmailIdsUpdated = listIdsUpdated.map((e) => EmailId(e)).toList(); + updatedEmailIds.addAll(listEmailIdsUpdated); + } + } + return updatedEmailIds; + } + + int _getMaxObjectsInSetMethod(Session session, AccountId accountId) { + final coreCapability = session.getCapabilityProperties( + accountId, + CapabilityIdentifier.jmapCore, + ); + final maxObjectsInSetMethod = coreCapability?.maxObjectsInSet?.value.toInt() + ?? CapabilityIdentifierExtension.defaultMaxObjectsInSet; + + final minOfMaxObjectsInSetMethod = min( + maxObjectsInSetMethod, + CapabilityIdentifierExtension.defaultMaxObjectsInSet, + ); + log('EmailAPI::_getMaxObjectsInSetMethod:minOfMaxObjectsInSetMethod = $minOfMaxObjectsInSetMethod'); + return minOfMaxObjectsInSetMethod; } Future> markAsStar( @@ -454,37 +508,52 @@ class EmailAPI with HandleSetErrorMixin { List emailIds, MarkStarAction markStarAction ) async { - final setEmailMethod = SetEmailMethod(accountId) - ..addUpdates(emailIds.generateMapUpdateObjectMarkAsStar(markStarAction)); + final maxBatches = _getMaxObjectsInSetMethod(session, accountId); + final totalEmails = emails.length; - final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); + final List updatedEmails = List.empty(growable: true); - final setEmailInvocation = requestBuilder.invocation(setEmailMethod); + for (int start = 0; start < totalEmails; start += maxBatches) { + int end = (start + maxBatches < totalEmails) + ? start + maxBatches + : totalEmails; + log('EmailAPI::markAsStar:emails from ${start + 1} to $end'); - final capabilities = setEmailMethod.requiredCapabilities - .toCapabilitiesSupportTeamMailboxes(session, accountId); + final currentListEmails = emails.sublist(start, end); - final response = await (requestBuilder - ..usings(capabilities)) - .build() - .execute(); + final setEmailMethod = SetEmailMethod(accountId) + ..addUpdates(currentListEmails.listEmailIds.generateMapUpdateObjectMarkAsStar(markStarAction)); - final setEmailResponse = response.parse( - setEmailInvocation.methodCallId, - SetEmailResponse.deserialize, - ); + final getEmailMethod = GetEmailMethod(accountId) + ..addIds(emails.listEmailIds.toIds().toSet()) + ..addProperties(Properties({EmailProperty.keywords})); - final emailIdUpdated = setEmailResponse?.updated - ?.keys - .map((id) => EmailId(id)) - .toList() ?? []; - final mapErrors = handleSetResponse([setEmailResponse]); + final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); - if (emailIdUpdated.isNotEmpty) { - return emailIdUpdated; - } else { - throw SetMethodException(mapErrors); + requestBuilder.invocation(setEmailMethod); + + final getEmailInvocation = requestBuilder.invocation(getEmailMethod); + + final capabilities = setEmailMethod.requiredCapabilities + .toCapabilitiesSupportTeamMailboxes(session, accountId); + + final response = await (requestBuilder + ..usings(capabilities)) + .build() + .execute(); + + final getEmailResponse = response.parse( + getEmailInvocation.methodCallId, + GetEmailResponse.deserialize, + ); + + final listEmails = getEmailResponse?.list ?? []; + if (listEmails.isNotEmpty) { + updatedEmails.addAll(listEmails); + } } + + return updatedEmails; } Future saveEmailAsDrafts( @@ -723,7 +792,7 @@ class EmailAPI with HandleSetErrorMixin { ..usings(emailRecoveryActionSetMethod.requiredCapabilities)) .build() .execute(); - + final emailRecoveryActionSetResponse = response.parse( emailRecoveryActionSetInvocation.methodCallId, SetEmailRecoveryActionResponse.deserialize @@ -739,12 +808,12 @@ class EmailAPI with HandleSetErrorMixin { final getEmailRecoveryActionMethod = GetEmailRecoveryActionMethod() ..addIds({emailRecoveryActionId.id}); final getEmailRecoveryActionInvocation = requestBuilder.invocation(getEmailRecoveryActionMethod); - + final response = await (requestBuilder ..usings(getEmailRecoveryActionMethod.requiredCapabilities)) .build() .execute(); - + final getEmailRecoveryActionResponse = response.parse( getEmailRecoveryActionInvocation.methodCallId, GetEmailRecoveryActionResponse.deserialize diff --git a/lib/features/email/domain/model/move_to_mailbox_request.dart b/lib/features/email/domain/model/move_to_mailbox_request.dart index aa958d274..9f047b71c 100644 --- a/lib/features/email/domain/model/move_to_mailbox_request.dart +++ b/lib/features/email/domain/model/move_to_mailbox_request.dart @@ -7,7 +7,7 @@ import 'package:tmail_ui_user/features/email/domain/model/move_action.dart'; class MoveToMailboxRequest with EquatableMixin { - final Map> currentMailboxes; + final Map> currentMailboxes; final MailboxId destinationMailboxId; final MoveAction moveAction; final EmailActionType emailActionType; @@ -25,6 +25,9 @@ class MoveToMailboxRequest with EquatableMixin { .values .fold(0, (sum, element) => sum + element.length); + bool get isMovingToSpam => moveAction == MoveAction.moving && + emailActionType == EmailActionType.moveToSpam; + @override List get props => [ currentMailboxes, diff --git a/lib/main/error/capability_validator.dart b/lib/main/error/capability_validator.dart index 5741f74be..90cd99aa0 100644 --- a/lib/main/error/capability_validator.dart +++ b/lib/main/error/capability_validator.dart @@ -42,6 +42,7 @@ extension ListCapabilityIdentifierExtension on List { extension CapabilityIdentifierExtension on CapabilityIdentifier { static const int defaultMaxCallsInRequest = 1; + static const int defaultMaxObjectsInSet = 50; bool isSupported(Session session, AccountId accountId) { try {