diff --git a/lib/features/email/data/datasource/email_datasource.dart b/lib/features/email/data/datasource/email_datasource.dart index bdfd90afe..9d9e55b37 100644 --- a/lib/features/email/data/datasource/email_datasource.dart +++ b/lib/features/email/data/datasource/email_datasource.dart @@ -9,6 +9,8 @@ import 'package:dio/dio.dart'; import 'package:email_recovery/email_recovery/email_recovery_action.dart'; import 'package:email_recovery/email_recovery/email_recovery_action_id.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/error/set_error.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/user_name.dart'; @@ -43,7 +45,10 @@ abstract class EmailDataSource { } ); - Future> markAsRead( + Future<({ + List emailIdsSuccess, + Map mapErrors, + })> markAsRead( Session session, AccountId accountId, List emailIds, @@ -75,9 +80,19 @@ abstract class EmailDataSource { {CancelToken? cancelToken} ); - Future> moveToMailbox(Session session, AccountId accountId, MoveToMailboxRequest moveRequest); + Future<({ + List emailIdsSuccess, + Map mapErrors, + })> moveToMailbox( + Session session, + AccountId accountId, + MoveToMailboxRequest moveRequest, + ); - Future> markAsStar( + Future<({ + List emailIdsSuccess, + Map mapErrors, + })> markAsStar( Session session, AccountId accountId, List emailIds, @@ -106,8 +121,14 @@ abstract class EmailDataSource { {CancelToken? cancelToken} ); - Future> deleteMultipleEmailsPermanently(Session session, AccountId accountId, List emailIds); - + Future<({ + List emailIdsSuccess, + Map mapErrors, + })> deleteMultipleEmailsPermanently( + Session session, + AccountId accountId, + List emailIds, + ); Future deleteEmailPermanently( Session session, AccountId accountId, diff --git a/lib/features/email/data/datasource_impl/email_datasource_impl.dart b/lib/features/email/data/datasource_impl/email_datasource_impl.dart index 2354d2c41..dcbf0d081 100644 --- a/lib/features/email/data/datasource_impl/email_datasource_impl.dart +++ b/lib/features/email/data/datasource_impl/email_datasource_impl.dart @@ -10,6 +10,8 @@ import 'package:email_recovery/email_recovery/email_recovery_action.dart'; import 'package:email_recovery/email_recovery/email_recovery_action_id.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/error/set_error.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/user_name.dart'; @@ -75,7 +77,10 @@ class EmailDataSourceImpl extends EmailDataSource { } @override - Future> markAsRead( + Future<({ + List emailIdsSuccess, + Map mapErrors, + })> markAsRead( Session session, AccountId accountId, List emailIds, @@ -112,14 +117,24 @@ class EmailDataSourceImpl extends EmailDataSource { } @override - Future> moveToMailbox(Session session, AccountId accountId, MoveToMailboxRequest moveRequest) { + Future<({ + List emailIdsSuccess, + Map mapErrors, + })> moveToMailbox( + Session session, + AccountId accountId, + MoveToMailboxRequest moveRequest, + ) { return Future.sync(() async { return await emailAPI.moveToMailbox(session, accountId, moveRequest); }).catchError(_exceptionThrower.throwException); } @override - Future> markAsStar( + Future<({ + List emailIdsSuccess, + Map mapErrors, + })> markAsStar( Session session, AccountId accountId, List emailIds, @@ -211,9 +226,20 @@ class EmailDataSourceImpl extends EmailDataSource { } @override - Future> deleteMultipleEmailsPermanently(Session session, AccountId accountId, List emailIds) { + Future<({ + List emailIdsSuccess, + Map mapErrors, + })> deleteMultipleEmailsPermanently( + Session session, + AccountId accountId, + List emailIds, + ) { return Future.sync(() async { - return await emailAPI.deleteMultipleEmailsPermanently(session, accountId, emailIds); + return await emailAPI.deleteMultipleEmailsPermanently( + session, + accountId, + emailIds, + ); }).catchError(_exceptionThrower.throwException); } diff --git a/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart b/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart index 85df8381d..9e96e0293 100644 --- a/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart +++ b/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart @@ -12,6 +12,8 @@ import 'package:dio/dio.dart'; import 'package:email_recovery/email_recovery/email_recovery_action.dart'; import 'package:email_recovery/email_recovery/email_recovery_action_id.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/error/set_error.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/user_name.dart'; @@ -82,7 +84,14 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource { } @override - Future> deleteMultipleEmailsPermanently(Session session, AccountId accountId, List emailIds) { + Future<({ + List emailIdsSuccess, + Map mapErrors, + })> deleteMultipleEmailsPermanently( + Session session, + AccountId accountId, + List emailIds, + ) { throw UnimplementedError(); } @@ -120,7 +129,10 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource { } @override - Future> markAsRead( + Future<({ + List emailIdsSuccess, + Map mapErrors, + })> markAsRead( Session session, AccountId accountId, List emailIds, @@ -130,12 +142,27 @@ class EmailHiveCacheDataSourceImpl extends EmailDataSource { } @override - Future> markAsStar(Session session, AccountId accountId, List emailIds, MarkStarAction markStarAction) { + Future<({ + List emailIdsSuccess, + Map mapErrors, + })> markAsStar( + Session session, + AccountId accountId, + List emailIds, + MarkStarAction markStarAction, + ) { throw UnimplementedError(); } @override - Future> moveToMailbox(Session session, AccountId accountId, MoveToMailboxRequest moveRequest) { + Future<({ + List emailIdsSuccess, + Map mapErrors, + })> moveToMailbox( + Session session, + AccountId accountId, + MoveToMailboxRequest moveRequest, + ) { throw UnimplementedError(); } diff --git a/lib/features/email/data/network/email_api.dart b/lib/features/email/data/network/email_api.dart index 44e7b87ba..0fac20749 100644 --- a/lib/features/email/data/network/email_api.dart +++ b/lib/features/email/data/network/email_api.dart @@ -18,6 +18,7 @@ import 'package:jmap_dart_client/http/http_client.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.dart'; import 'package:jmap_dart_client/jmap/core/capability/core_capability.dart'; +import 'package:jmap_dart_client/jmap/core/error/set_error.dart'; import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/core/patch_object.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; @@ -44,13 +45,13 @@ 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_property.dart'; import 'package:model/email/mark_star_action.dart'; import 'package:model/email/read_actions.dart'; import 'package:model/extensions/email_extension.dart'; import 'package:model/extensions/email_id_extensions.dart'; import 'package:model/extensions/keyword_identifier_extension.dart'; import 'package:model/extensions/list_email_id_extension.dart'; +import 'package:model/extensions/list_id_extension.dart'; import 'package:model/extensions/mailbox_id_extension.dart'; import 'package:model/extensions/session_extension.dart'; import 'package:model/oidc/token_oidc.dart'; @@ -237,16 +238,21 @@ class EmailAPI with HandleSetErrorMixin { } } - Future> markAsRead( + Future<({ + List emailIdsSuccess, + Map mapErrors, + })> markAsRead( Session session, AccountId accountId, List emailIds, ReadActions readActions, ) async { - final maxBatches = _getMaxObjectsInSetMethod(session, accountId); - final totalEmails = emails.length; + final maxObjects = _getMaxObjectsInSetMethod(session, accountId); + final totalEmails = emailIds.length; + final maxBatches = min(totalEmails, maxObjects); - final List updatedEmails = List.empty(growable: true); + final List updatedEmailIds = List.empty(growable: true); + final Map mapErrors = {}; for (int start = 0; start < totalEmails; start += maxBatches) { int end = (start + maxBatches < totalEmails) @@ -254,20 +260,16 @@ class EmailAPI with HandleSetErrorMixin { : totalEmails; log('EmailAPI::markAsRead:emails from ${start + 1} to $end'); - final currentListEmails = emails.sublist(start, end); + final currentListEmailIds = emailIds.sublist(start, end); final setEmailMethod = SetEmailMethod(accountId) - ..addUpdates(currentListEmails.listEmailIds.generateMapUpdateObjectMarkAsRead(readActions)); - - final getEmailMethod = GetEmailMethod(accountId) - ..addIds(emails.listEmailIds.toIds().toSet()) - ..addProperties(Properties({EmailProperty.keywords})); + ..addUpdates( + currentListEmailIds.generateMapUpdateObjectMarkAsRead(readActions) + ); final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); - requestBuilder.invocation(setEmailMethod); - - final getEmailInvocation = requestBuilder.invocation(getEmailMethod); + final setEmailInvocation = requestBuilder.invocation(setEmailMethod); final capabilities = setEmailMethod.requiredCapabilities .toCapabilitiesSupportTeamMailboxes(session, accountId); @@ -277,18 +279,19 @@ class EmailAPI with HandleSetErrorMixin { .build() .execute(); - final getEmailResponse = response.parse( - getEmailInvocation.methodCallId, - GetEmailResponse.deserialize, + final setEmailResponse = response.parse( + setEmailInvocation.methodCallId, + SetEmailResponse.deserialize, ); - final listEmails = getEmailResponse?.list ?? []; - if (listEmails.isNotEmpty) { - updatedEmails.addAll(listEmails); - } + final listEmailIds = setEmailResponse?.updated?.keys.toEmailIds() ?? []; + final mapErrors = handleSetResponse([setEmailResponse]); + + updatedEmailIds.addAll(listEmailIds); + mapErrors.addAll(mapErrors); } - return updatedEmails; + return (emailIdsSuccess: updatedEmailIds, mapErrors: mapErrors); } Future> downloadAttachments( @@ -397,47 +400,55 @@ class EmailAPI with HandleSetErrorMixin { return bytesDownloaded; } - Future> moveToMailbox( + Future<({ + List emailIdsSuccess, + Map mapErrors, + })> moveToMailbox( Session session, AccountId accountId, MoveToMailboxRequest moveRequest ) async { - final maxBatches = _getMaxObjectsInSetMethod(session, accountId); - final List listEmailIdResult = List.empty(growable: true); + final Map mapErrors = {}; 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( + final resultRecords = await _moveEmailsBetweenMailboxes( session: session, accountId: accountId, - listEmailIds: listEmailIds, + emailIds: listEmailIds, currentMailboxId: currentMailboxId, - maxBatches: maxBatches, destinationMailboxId: moveRequest.destinationMailboxId, isMovingToSpam: moveRequest.isMovingToSpam, ); - listEmailIdResult.addAll(movedEmailIds); + + listEmailIdResult.addAll(resultRecords.emailIdsSuccess); + mapErrors.addAll(resultRecords.mapErrors); } - return listEmailIdResult; + + return (emailIdsSuccess: listEmailIdResult, mapErrors: mapErrors); } - Future> _moveEmailsBetweenMailboxes({ + Future<({ + List emailIdsSuccess, + Map mapErrors, + })> _moveEmailsBetweenMailboxes({ required Session session, required AccountId accountId, - required List listEmailIds, + required List emailIds, required MailboxId currentMailboxId, required MailboxId destinationMailboxId, - required int maxBatches, bool isMovingToSpam = false, }) async { - final maxBatches = _getMaxObjectsInSetMethod(session, accountId); - final totalEmails = listEmailIds.length; + final maxObjects = _getMaxObjectsInSetMethod(session, accountId); + final totalEmails = emailIds.length; + final maxBatches = min(totalEmails, maxObjects); final List updatedEmailIds = List.empty(growable: true); + final Map mapErrors = {}; for (int start = 0; start < totalEmails; start += maxBatches) { int end = (start + maxBatches < totalEmails) @@ -445,7 +456,7 @@ class EmailAPI with HandleSetErrorMixin { : totalEmails; log('EmailAPI::_moveEmailsBetweenMailboxes:emails from ${start + 1} to $end'); - final currentEmailIds = listEmailIds.sublist(start, end); + final currentEmailIds = emailIds.sublist(start, end); final moveProperties = isMovingToSpam ? currentEmailIds.generateMapUpdateObjectMoveToSpam( @@ -477,13 +488,14 @@ class EmailAPI with HandleSetErrorMixin { SetEmailResponse.deserialize, ); - final listIdsUpdated = setEmailResponse?.updated?.keys ?? []; - if (listIdsUpdated.isNotEmpty == true) { - final listEmailIdsUpdated = listIdsUpdated.map((e) => EmailId(e)).toList(); - updatedEmailIds.addAll(listEmailIdsUpdated); - } + final listEmailIds = setEmailResponse?.updated?.keys.toEmailIds() ?? []; + final mapErrors = handleSetResponse([setEmailResponse]); + + updatedEmailIds.addAll(listEmailIds); + mapErrors.addAll(mapErrors); } - return updatedEmailIds; + + return (emailIdsSuccess: updatedEmailIds, mapErrors: mapErrors); } int _getMaxObjectsInSetMethod(Session session, AccountId accountId) { @@ -502,16 +514,21 @@ class EmailAPI with HandleSetErrorMixin { return minOfMaxObjectsInSetMethod; } - Future> markAsStar( + Future<({ + List emailIdsSuccess, + Map mapErrors, + })> markAsStar( Session session, AccountId accountId, List emailIds, MarkStarAction markStarAction ) async { - final maxBatches = _getMaxObjectsInSetMethod(session, accountId); - final totalEmails = emails.length; + final maxObjects = _getMaxObjectsInSetMethod(session, accountId); + final totalEmails = emailIds.length; + final maxBatches = min(totalEmails, maxObjects); - final List updatedEmails = List.empty(growable: true); + final List updatedEmailIds = List.empty(growable: true); + final Map mapErrors = {}; for (int start = 0; start < totalEmails; start += maxBatches) { int end = (start + maxBatches < totalEmails) @@ -519,20 +536,16 @@ class EmailAPI with HandleSetErrorMixin { : totalEmails; log('EmailAPI::markAsStar:emails from ${start + 1} to $end'); - final currentListEmails = emails.sublist(start, end); + final currentListEmailIds = emailIds.sublist(start, end); final setEmailMethod = SetEmailMethod(accountId) - ..addUpdates(currentListEmails.listEmailIds.generateMapUpdateObjectMarkAsStar(markStarAction)); - - final getEmailMethod = GetEmailMethod(accountId) - ..addIds(emails.listEmailIds.toIds().toSet()) - ..addProperties(Properties({EmailProperty.keywords})); + ..addUpdates( + currentListEmailIds.generateMapUpdateObjectMarkAsStar(markStarAction), + ); final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); - requestBuilder.invocation(setEmailMethod); - - final getEmailInvocation = requestBuilder.invocation(getEmailMethod); + final setEmailInvocation = requestBuilder.invocation(setEmailMethod); final capabilities = setEmailMethod.requiredCapabilities .toCapabilitiesSupportTeamMailboxes(session, accountId); @@ -542,18 +555,19 @@ class EmailAPI with HandleSetErrorMixin { .build() .execute(); - final getEmailResponse = response.parse( - getEmailInvocation.methodCallId, - GetEmailResponse.deserialize, + final setEmailResponse = response.parse( + setEmailInvocation.methodCallId, + SetEmailResponse.deserialize, ); - final listEmails = getEmailResponse?.list ?? []; - if (listEmails.isNotEmpty) { - updatedEmails.addAll(listEmails); - } + final listEmailIds = setEmailResponse?.updated?.keys.toEmailIds() ?? []; + final mapErrors = handleSetResponse([setEmailResponse]); + + updatedEmailIds.addAll(listEmailIds); + mapErrors.addAll(mapErrors); } - return updatedEmails; + return (emailIdsSuccess: updatedEmailIds, mapErrors: mapErrors); } Future saveEmailAsDrafts( @@ -656,36 +670,56 @@ class EmailAPI with HandleSetErrorMixin { return emailCreated; } - Future> deleteMultipleEmailsPermanently( + Future<({ + List emailIdsSuccess, + Map mapErrors, + })> deleteMultipleEmailsPermanently( Session session, AccountId accountId, List emailIds ) async { - final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); - final setEmailMethod = SetEmailMethod(accountId) - ..addDestroy(emailIds.map((emailId) => emailId.id).toSet()); + final maxObjects = _getMaxObjectsInSetMethod(session, accountId); + final totalEmails = emailIds.length; + final maxBatches = min(totalEmails, maxObjects); - final setEmailInvocation = requestBuilder.invocation(setEmailMethod); + final List destroyedEmailIds = List.empty(growable: true); + final Map mapErrors = {}; - final capabilities = setEmailMethod.requiredCapabilities - .toCapabilitiesSupportTeamMailboxes(session, accountId); + for (int start = 0; start < totalEmails; start += maxBatches) { + int end = (start + maxBatches < totalEmails) + ? start + maxBatches + : totalEmails; + log('EmailAPI::deleteMultipleEmailsPermanently:emails from ${start + 1} to $end'); - final response = await (requestBuilder - ..usings(capabilities)) - .build() - .execute(); + final currentListEmailIds = emailIds.sublist(start, end); - final setEmailResponse = response.parse( + final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); + final setEmailMethod = SetEmailMethod(accountId) + ..addDestroy(currentListEmailIds.toIds().toSet()); + + 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); + SetEmailResponse.deserialize, + ); - final listIdResult = setEmailResponse?.destroyed; + final listEmailIds = setEmailResponse?.destroyed?.toEmailIds() ?? []; + final mapErrors = handleSetResponse([setEmailResponse]); - if (listIdResult != null) { - return listIdResult.map((id) => EmailId(id)).toList(); + destroyedEmailIds.addAll(listEmailIds); + mapErrors.addAll(mapErrors); } - return List.empty(); + return (emailIdsSuccess: destroyedEmailIds, mapErrors: mapErrors); } Future deleteEmailPermanently( @@ -698,19 +732,20 @@ class EmailAPI with HandleSetErrorMixin { final setEmailMethod = SetEmailMethod(accountId) ..addDestroy({emailId.id}); - final setEmailInvocation = requestBuilder.invocation(setEmailMethod); + final setEmailInvocation = requestBuilder.invocation(setEmailMethod); - final capabilities = setEmailMethod.requiredCapabilities - .toCapabilitiesSupportTeamMailboxes(session, accountId); + final capabilities = setEmailMethod.requiredCapabilities + .toCapabilitiesSupportTeamMailboxes(session, accountId); final response = await (requestBuilder ..usings(capabilities)) .build() .execute(cancelToken: cancelToken); - final setEmailResponse = response.parse( + final setEmailResponse = response.parse( setEmailInvocation.methodCallId, - SetEmailResponse.deserialize); + SetEmailResponse.deserialize, + ); return setEmailResponse?.destroyed?.contains(emailId.id) == true; } @@ -757,10 +792,10 @@ class EmailAPI with HandleSetErrorMixin { final capabilities = setEmailMethod.requiredCapabilities.toCapabilitiesSupportTeamMailboxes(session, accountId); - final response = await (requestBuilder - ..usings(capabilities)) - .build() - .execute(); + final response = await (requestBuilder + ..usings(capabilities)) + .build() + .execute(); final setEmailResponse = response.parse( setEmailInvocation.methodCallId, diff --git a/lib/features/email/data/repository/email_repository_impl.dart b/lib/features/email/data/repository/email_repository_impl.dart index c48b4c3df..c849e3b22 100644 --- a/lib/features/email/data/repository/email_repository_impl.dart +++ b/lib/features/email/data/repository/email_repository_impl.dart @@ -11,6 +11,8 @@ import 'package:dio/dio.dart'; import 'package:email_recovery/email_recovery/email_recovery_action.dart'; import 'package:email_recovery/email_recovery/email_recovery_action_id.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/error/set_error.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; @@ -83,7 +85,10 @@ class EmailRepositoryImpl extends EmailRepository { } @override - Future> markAsRead( + Future<({ + List emailIdsSuccess, + Map mapErrors, + })> markAsRead( Session session, AccountId accountId, List emailIds, @@ -124,12 +129,26 @@ class EmailRepositoryImpl extends EmailRepository { } @override - Future> moveToMailbox(Session session, AccountId accountId, MoveToMailboxRequest moveRequest) { - return emailDataSource[DataSourceType.network]!.moveToMailbox(session, accountId, moveRequest); + Future<({ + List emailIdsSuccess, + Map mapErrors, + })> moveToMailbox( + Session session, + AccountId accountId, + MoveToMailboxRequest moveRequest, + ) { + return emailDataSource[DataSourceType.network]!.moveToMailbox( + session, + accountId, + moveRequest, + ); } @override - Future> markAsStar( + Future<({ + List emailIdsSuccess, + Map mapErrors, + })> markAsStar( Session session, AccountId accountId, List emailIds, @@ -228,8 +247,19 @@ class EmailRepositoryImpl extends EmailRepository { } @override - Future> deleteMultipleEmailsPermanently(Session session, AccountId accountId, List emailIds) { - return emailDataSource[DataSourceType.network]!.deleteMultipleEmailsPermanently(session, accountId, emailIds); + Future<({ + List emailIdsSuccess, + Map mapErrors, + })> deleteMultipleEmailsPermanently( + Session session, + AccountId accountId, + List emailIds, + ) { + return emailDataSource[DataSourceType.network]!.deleteMultipleEmailsPermanently( + session, + accountId, + emailIds, + ); } @override diff --git a/lib/features/email/domain/repository/email_repository.dart b/lib/features/email/domain/repository/email_repository.dart index 7aaf27b9d..d1211c6b9 100644 --- a/lib/features/email/domain/repository/email_repository.dart +++ b/lib/features/email/domain/repository/email_repository.dart @@ -10,6 +10,8 @@ import 'package:dio/dio.dart'; import 'package:email_recovery/email_recovery/email_recovery_action.dart'; import 'package:email_recovery/email_recovery/email_recovery_action_id.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/error/set_error.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; @@ -45,7 +47,10 @@ abstract class EmailRepository { } ); - Future> markAsRead( + Future<({ + List emailIdsSuccess, + Map mapErrors, + })> markAsRead( Session session, AccountId accountId, List emailIds, @@ -77,9 +82,19 @@ abstract class EmailRepository { {CancelToken? cancelToken} ); - Future> moveToMailbox(Session session, AccountId accountId, MoveToMailboxRequest moveRequest); + Future<({ + List emailIdsSuccess, + Map mapErrors, + })> moveToMailbox( + Session session, + AccountId accountId, + MoveToMailboxRequest moveRequest, + ); - Future> markAsStar( + Future<({ + List emailIdsSuccess, + Map mapErrors, + })> markAsStar( Session session, AccountId accountId, List emailIds, @@ -114,7 +129,14 @@ abstract class EmailRepository { {CancelToken? cancelToken} ); - Future> deleteMultipleEmailsPermanently(Session session, AccountId accountId, List emailIds); + Future<({ + List emailIdsSuccess, + Map mapErrors, + })> deleteMultipleEmailsPermanently( + Session session, + AccountId accountId, + List emailIds, + ); Future deleteEmailPermanently( Session session, diff --git a/lib/features/email/domain/usecases/delete_multiple_emails_permanently_interactor.dart b/lib/features/email/domain/usecases/delete_multiple_emails_permanently_interactor.dart index 685933fd5..b8195c8e6 100644 --- a/lib/features/email/domain/usecases/delete_multiple_emails_permanently_interactor.dart +++ b/lib/features/email/domain/usecases/delete_multiple_emails_permanently_interactor.dart @@ -16,10 +16,10 @@ class DeleteMultipleEmailsPermanentlyInteractor { try { yield Right(LoadingDeleteMultipleEmailsPermanentlyAll()); final listResult = await _emailRepository.deleteMultipleEmailsPermanently(session, accountId, emailIds); - if (listResult.length == emailIds.length) { - yield Right(DeleteMultipleEmailsPermanentlyAllSuccess(listResult)); - } else if (listResult.isNotEmpty) { - yield Right(DeleteMultipleEmailsPermanentlyHasSomeEmailFailure(listResult)); + if (listResult.emailIdsSuccess.length == emailIds.length) { + yield Right(DeleteMultipleEmailsPermanentlyAllSuccess(listResult.emailIdsSuccess)); + } else if (listResult.emailIdsSuccess.isNotEmpty) { + yield Right(DeleteMultipleEmailsPermanentlyHasSomeEmailFailure(listResult.emailIdsSuccess)); } else { yield Left(DeleteMultipleEmailsPermanentlyAllFailure()); } diff --git a/lib/features/email/domain/usecases/mark_as_email_read_interactor.dart b/lib/features/email/domain/usecases/mark_as_email_read_interactor.dart index 7db473e03..5f5659bc6 100644 --- a/lib/features/email/domain/usecases/mark_as_email_read_interactor.dart +++ b/lib/features/email/domain/usecases/mark_as_email_read_interactor.dart @@ -29,11 +29,15 @@ class MarkAsEmailReadInteractor { readAction, ); - yield Right(MarkAsEmailReadSuccess( - result.first, - readAction, - markReadAction, - )); + if (result.emailIdsSuccess.isEmpty) { + yield Left(MarkAsEmailReadFailure(readAction)); + } else { + yield Right(MarkAsEmailReadSuccess( + result.emailIdsSuccess.first, + readAction, + markReadAction, + )); + } } catch (e) { yield Left(MarkAsEmailReadFailure(readAction, exception: e)); } diff --git a/lib/features/email/domain/usecases/move_to_mailbox_interactor.dart b/lib/features/email/domain/usecases/move_to_mailbox_interactor.dart index eebdebd2e..0303c3648 100644 --- a/lib/features/email/domain/usecases/move_to_mailbox_interactor.dart +++ b/lib/features/email/domain/usecases/move_to_mailbox_interactor.dart @@ -16,9 +16,9 @@ class MoveToMailboxInteractor { try { yield Right(LoadingMoveToMailbox()); final result = await _emailRepository.moveToMailbox(session, accountId, moveRequest); - if (result.isNotEmpty) { + if (result.emailIdsSuccess.isNotEmpty) { yield Right(MoveToMailboxSuccess( - result.first, + result.emailIdsSuccess.first, moveRequest.currentMailboxes.keys.first, moveRequest.destinationMailboxId, moveRequest.moveAction, diff --git a/lib/features/mailbox/data/network/mailbox_isolate_worker.dart b/lib/features/mailbox/data/network/mailbox_isolate_worker.dart index 09468ccd0..376757f7a 100644 --- a/lib/features/mailbox/data/network/mailbox_isolate_worker.dart +++ b/lib/features/mailbox/data/network/mailbox_isolate_worker.dart @@ -138,8 +138,8 @@ class MailboxIsolateWorker { listEmailUnread.listEmailIds, ReadActions.markAsRead); - log('MailboxIsolateWorker::_handleMarkAsMailboxRead(): MARK_READ: ${result.length}'); - emailIdsCompleted.addAll(result); + log('MailboxIsolateWorker::_handleMarkAsMailboxRead(): MARK_READ: ${result.emailIdsSuccess.length}'); + emailIdsCompleted.addAll(result.emailIdsSuccess); sendPort.send(emailIdsCompleted); } } @@ -206,8 +206,8 @@ class MailboxIsolateWorker { listEmailUnread.listEmailIds, ReadActions.markAsRead, ); - log('MailboxIsolateWorker::_handleMarkAsMailboxReadActionOnWeb(): MARK_READ: ${result.length}'); - emailIdsCompleted.addAll(result); + log('MailboxIsolateWorker::_handleMarkAsMailboxReadActionOnWeb(): MARK_READ: ${result.emailIdsSuccess.length}'); + emailIdsCompleted.addAll(result.emailIdsSuccess); onProgressController.add(Right(UpdatingMarkAsMailboxReadState( mailboxId: mailboxId, diff --git a/lib/features/thread/data/network/thread_isolate_worker.dart b/lib/features/thread/data/network/thread_isolate_worker.dart index 03ffe5112..f95dba31e 100644 --- a/lib/features/thread/data/network/thread_isolate_worker.dart +++ b/lib/features/thread/data/network/thread_isolate_worker.dart @@ -100,7 +100,7 @@ class ThreadIsolateWorker { ..setIsAscending(false)), filter: EmailFilterCondition(inMailbox: args.mailboxId, before: lastEmail?.receivedAt), properties: Properties({ - EmailProperty.id, + EmailProperty.id, EmailProperty.receivedAt }), ); @@ -119,7 +119,7 @@ class ThreadIsolateWorker { args.session, args.accountId, newEmailList.listEmailIds); - emailListCompleted.addAll(listEmailIdDeleted); + emailListCompleted.addAll(listEmailIdDeleted.emailIdsSuccess); sendPort.send(emailListCompleted); } else { hasEmails = false; @@ -154,7 +154,7 @@ class ThreadIsolateWorker { ..setIsAscending(false)), filter: EmailFilterCondition(inMailbox: mailboxId, before: lastEmail?.receivedAt), properties: Properties({ - EmailProperty.id, + EmailProperty.id, EmailProperty.receivedAt }), ); @@ -173,7 +173,7 @@ class ThreadIsolateWorker { session, accountId, newEmailList.listEmailIds); - emailListCompleted.addAll(listEmailIdDeleted); + emailListCompleted.addAll(listEmailIdDeleted.emailIdsSuccess); onProgressController.add(Right(EmptyingFolderState( mailboxId, emailListCompleted.length, totalEmails diff --git a/lib/features/thread/domain/usecases/mark_as_multiple_email_read_interactor.dart b/lib/features/thread/domain/usecases/mark_as_multiple_email_read_interactor.dart index 08779f964..67299d960 100644 --- a/lib/features/thread/domain/usecases/mark_as_multiple_email_read_interactor.dart +++ b/lib/features/thread/domain/usecases/mark_as_multiple_email_read_interactor.dart @@ -29,16 +29,16 @@ class MarkAsMultipleEmailReadInteractor { readAction, ); - if (emailIds.length == result.length) { + if (emailIds.length == result.emailIdsSuccess.length) { yield Right(MarkAsMultipleEmailReadAllSuccess( - result.length, + result.emailIdsSuccess.length, readAction, )); - } else if (result.isEmpty) { + } else if (result.emailIdsSuccess.isEmpty) { yield Left(MarkAsMultipleEmailReadAllFailure(readAction)); } else { yield Right(MarkAsMultipleEmailReadHasSomeEmailFailure( - result.length, + result.emailIdsSuccess.length, readAction, )); } diff --git a/lib/features/thread/domain/usecases/mark_as_star_multiple_email_interactor.dart b/lib/features/thread/domain/usecases/mark_as_star_multiple_email_interactor.dart index 6f828426b..8ac2e00b4 100644 --- a/lib/features/thread/domain/usecases/mark_as_star_multiple_email_interactor.dart +++ b/lib/features/thread/domain/usecases/mark_as_star_multiple_email_interactor.dart @@ -24,16 +24,16 @@ class MarkAsStarMultipleEmailInteractor { final result = await _emailRepository.markAsStar(session, accountId, emailIds, markStarAction); - if (emailIds.length == result.length) { + if (emailIds.length == result.emailIdsSuccess.length) { yield Right(MarkAsStarMultipleEmailAllSuccess( emailIds.length, markStarAction, )); - } else if (result.isEmpty) { + } else if (result.emailIdsSuccess.isEmpty) { yield Left(MarkAsStarMultipleEmailAllFailure(markStarAction)); } else { yield Right(MarkAsStarMultipleEmailHasSomeEmailFailure( - result.length, + result.emailIdsSuccess.length, markStarAction, )); } diff --git a/lib/features/thread/domain/usecases/move_multiple_email_to_mailbox_interactor.dart b/lib/features/thread/domain/usecases/move_multiple_email_to_mailbox_interactor.dart index 19bf07647..f7481c5b6 100644 --- a/lib/features/thread/domain/usecases/move_multiple_email_to_mailbox_interactor.dart +++ b/lib/features/thread/domain/usecases/move_multiple_email_to_mailbox_interactor.dart @@ -22,20 +22,20 @@ class MoveMultipleEmailToMailboxInteractor { try { yield Right(LoadingMoveMultipleEmailToMailboxAll()); final result = await _emailRepository.moveToMailbox(session, accountId, moveRequest); - if (moveRequest.totalEmails == result.length) { + if (moveRequest.totalEmails == result.emailIdsSuccess.length) { yield Right(MoveMultipleEmailToMailboxAllSuccess( - result, + result.emailIdsSuccess, moveRequest.currentMailboxes.keys.first, moveRequest.destinationMailboxId, moveRequest.moveAction, moveRequest.emailActionType, destinationPath: moveRequest.destinationPath, )); - } else if (result.isEmpty) { + } else if (result.emailIdsSuccess.isEmpty) { yield Left(MoveMultipleEmailToMailboxAllFailure(moveRequest.moveAction, moveRequest.emailActionType)); } else { yield Right(MoveMultipleEmailToMailboxHasSomeEmailFailure( - result, + result.emailIdsSuccess, moveRequest.currentMailboxes.keys.first, moveRequest.destinationMailboxId, moveRequest.moveAction, diff --git a/model/lib/extensions/list_id_extension.dart b/model/lib/extensions/list_id_extension.dart new file mode 100644 index 000000000..cbdd0fc18 --- /dev/null +++ b/model/lib/extensions/list_id_extension.dart @@ -0,0 +1,6 @@ +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; + +extension ListIdExtension on Iterable { + Iterable toEmailIds() => map((id) => EmailId(id)); +} \ No newline at end of file diff --git a/model/lib/model.dart b/model/lib/model.dart index 29e3c9733..00f905bb3 100644 --- a/model/lib/model.dart +++ b/model/lib/model.dart @@ -60,6 +60,7 @@ export 'extensions/session_extension.dart'; export 'extensions/username_extension.dart'; export 'extensions/utc_date_extension.dart'; export 'extensions/contact_support_capability_extension.dart'; +export 'extensions/list_id_extension.dart'; // Identity export 'identity/identity_request_dto.dart'; export 'mailbox/expand_mode.dart'; diff --git a/test/features/email/data/network/email_api_test.dart b/test/features/email/data/network/email_api_test.dart index d442bf9da..6aee8493f 100644 --- a/test/features/email/data/network/email_api_test.dart +++ b/test/features/email/data/network/email_api_test.dart @@ -1,5 +1,4 @@ -import 'dart:math'; - +import 'package:collection/collection.dart'; import 'package:core/data/network/dio_client.dart'; import 'package:core/data/network/download/download_manager.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -60,7 +59,7 @@ void main() { const defaultMaxObjectsInSet = 50; const maxObjectsInSet = 50; const totalEmails = 100; - final maxBatches = min(maxObjectsInSet, defaultMaxObjectsInSet); + final maxBatches = [maxObjectsInSet, defaultMaxObjectsInSet, totalEmails].min; final countIterations = (totalEmails / maxBatches).ceil(); final aliceSession = SessionFixtures.getAliceSessionWithMaxObjectsInSet(maxObjectsInSet); @@ -102,16 +101,16 @@ void main() { ] }); - final emails = List.generate( + final emailIds = List.generate( totalEmails, - (index) => Email(id: EmailId(Id('email_$index'))), + (index) => EmailId(Id('email_$index')), ); // Act final result = await emailApi.markAsRead( aliceSession, AccountFixtures.aliceAccountId, - emails, + emailIds, ReadActions.markAsRead, ); @@ -121,7 +120,8 @@ void main() { data: anyNamed('data'), cancelToken: anyNamed('cancelToken'), )).called(countIterations); - expect(result.length, totalEmails); + expect(result.emailIdsSuccess.length, totalEmails); + expect(result.mapErrors.isEmpty, isTrue); }); test( @@ -133,7 +133,7 @@ void main() { const defaultMaxObjectsInSet = 50; const maxObjectsInSet = 200; const totalEmails = 100; - final maxBatches = min(maxObjectsInSet, defaultMaxObjectsInSet); + final maxBatches = [maxObjectsInSet, defaultMaxObjectsInSet, totalEmails].min; final countIterations = (totalEmails / maxBatches).ceil(); final aliceSession = SessionFixtures.getAliceSessionWithMaxObjectsInSet(maxObjectsInSet); @@ -175,16 +175,16 @@ void main() { ] }); - final emails = List.generate( + final emailIds = List.generate( totalEmails, - (index) => Email(id: EmailId(Id('email_$index'))), + (index) => EmailId(Id('email_$index')), ); // Act final result = await emailApi.markAsRead( aliceSession, AccountFixtures.aliceAccountId, - emails, + emailIds, ReadActions.markAsRead, ); @@ -194,7 +194,8 @@ void main() { data: anyNamed('data'), cancelToken: anyNamed('cancelToken'), )).called(countIterations); - expect(result.length, totalEmails); + expect(result.emailIdsSuccess.length, totalEmails); + expect(result.mapErrors.isEmpty, isTrue); }); test( @@ -206,7 +207,7 @@ void main() { const defaultMaxObjectsInSet = 50; const maxObjectsInSet = 20; const totalEmails = 100; - final maxBatches = min(maxObjectsInSet, defaultMaxObjectsInSet); + final maxBatches = [maxObjectsInSet, defaultMaxObjectsInSet, totalEmails].min; final countIterations = (totalEmails / maxBatches).ceil(); final aliceSession = SessionFixtures.getAliceSessionWithMaxObjectsInSet(maxObjectsInSet); @@ -248,16 +249,16 @@ void main() { ] }); - final emails = List.generate( + final emailIds = List.generate( totalEmails, - (index) => Email(id: EmailId(Id('email_$index'))), + (index) => EmailId(Id('email_$index')), ); // Act final result = await emailApi.markAsRead( aliceSession, AccountFixtures.aliceAccountId, - emails, + emailIds, ReadActions.markAsRead, ); @@ -267,7 +268,83 @@ void main() { data: anyNamed('data'), cancelToken: anyNamed('cancelToken'), )).called(countIterations); - expect(result.length, totalEmails); + expect(result.emailIdsSuccess.length, totalEmails); + expect(result.mapErrors.isEmpty, isTrue); + }); + + test( + 'SHOULD calls execute the correct number of times\n' + 'WHEN totalEmails is less than defaultMaxObjectsInSet\n' + 'WHEN maxObjectsInSet equal defaultMaxObjectsInSet\n' + 'AND defaultMaxObjectsInSet is 50', + () async { + // Arrange + const defaultMaxObjectsInSet = 50; + const maxObjectsInSet = 50; + const totalEmails = 30; + final maxBatches = [maxObjectsInSet, defaultMaxObjectsInSet, totalEmails].min; + final countIterations = (totalEmails / maxBatches).ceil(); + final aliceSession = SessionFixtures.getAliceSessionWithMaxObjectsInSet(maxObjectsInSet); + + when(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).thenAnswer((_) async => { + "sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943", + "methodResponses": [ + [ + "Email/set", + { + "accountId": AccountFixtures.aliceAccountId.asString, + "oldState": "4cb1e760-c11b-11ef-9cfb-ef2eae0e64b1", + "newState": "4cb1e760-c11b-11ef-9cfb-ef2eae0e64b1", + "updated": { + for (var item in List.generate(maxBatches, (index) => {"email_$index": null})) + item.keys.first: item.values.first + } + }, + "c0" + ], + [ + "Email/get", + { + "accountId": AccountFixtures.aliceAccountId.asString, + "state": "b965db40-c11c-11ef-9cfb-ef2eae0e64b1", + "list": List.generate(maxBatches, (index) => { + "id": "email_$index", + "keywords": { + "\$seen": true + } + }), + "notFound": [] + }, + "c1" + ] + ] + }); + + final emailIds = List.generate( + totalEmails, + (index) => EmailId(Id('email_$index')), + ); + + // Act + final result = await emailApi.markAsRead( + aliceSession, + AccountFixtures.aliceAccountId, + emailIds, + ReadActions.markAsRead, + ); + + // Assert + verify(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).called(countIterations); + expect(result.emailIdsSuccess.length, totalEmails); + expect(result.mapErrors.isEmpty, isTrue); }); }); @@ -281,7 +358,7 @@ void main() { const defaultMaxObjectsInSet = 50; const maxObjectsInSet = 50; const totalEmails = 100; - final maxBatches = min(maxObjectsInSet, defaultMaxObjectsInSet); + final maxBatches = [maxObjectsInSet, defaultMaxObjectsInSet, totalEmails].min; final countIterations = (totalEmails / maxBatches).ceil(); final aliceSession = SessionFixtures.getAliceSessionWithMaxObjectsInSet(maxObjectsInSet); @@ -323,16 +400,16 @@ void main() { ] }); - final emails = List.generate( + final emailIds = List.generate( totalEmails, - (index) => Email(id: EmailId(Id('email_$index'))), + (index) => EmailId(Id('email_$index')), ); // Act final result = await emailApi.markAsStar( aliceSession, AccountFixtures.aliceAccountId, - emails, + emailIds, MarkStarAction.markStar, ); @@ -342,7 +419,8 @@ void main() { data: anyNamed('data'), cancelToken: anyNamed('cancelToken'), )).called(countIterations); - expect(result.length, totalEmails); + expect(result.emailIdsSuccess.length, totalEmails); + expect(result.mapErrors.isEmpty, isTrue); }); test( @@ -354,7 +432,7 @@ void main() { const defaultMaxObjectsInSet = 50; const maxObjectsInSet = 200; const totalEmails = 100; - final maxBatches = min(maxObjectsInSet, defaultMaxObjectsInSet); + final maxBatches = [maxObjectsInSet, defaultMaxObjectsInSet, totalEmails].min; final countIterations = (totalEmails / maxBatches).ceil(); final aliceSession = SessionFixtures.getAliceSessionWithMaxObjectsInSet(maxObjectsInSet); @@ -396,16 +474,16 @@ void main() { ] }); - final emails = List.generate( + final emailIds = List.generate( totalEmails, - (index) => Email(id: EmailId(Id('email_$index'))), + (index) => EmailId(Id('email_$index')), ); // Act final result = await emailApi.markAsStar( aliceSession, AccountFixtures.aliceAccountId, - emails, + emailIds, MarkStarAction.markStar, ); @@ -415,7 +493,8 @@ void main() { data: anyNamed('data'), cancelToken: anyNamed('cancelToken'), )).called(countIterations); - expect(result.length, totalEmails); + expect(result.emailIdsSuccess.length, totalEmails); + expect(result.mapErrors.isEmpty, isTrue); }); test( @@ -427,7 +506,7 @@ void main() { const defaultMaxObjectsInSet = 50; const maxObjectsInSet = 20; const totalEmails = 100; - final maxBatches = min(maxObjectsInSet, defaultMaxObjectsInSet); + final maxBatches = [maxObjectsInSet, defaultMaxObjectsInSet, totalEmails].min; final countIterations = (totalEmails / maxBatches).ceil(); final aliceSession = SessionFixtures.getAliceSessionWithMaxObjectsInSet(maxObjectsInSet); @@ -469,16 +548,16 @@ void main() { ] }); - final emails = List.generate( + final emailIds = List.generate( totalEmails, - (index) => Email(id: EmailId(Id('email_$index'))), + (index) => EmailId(Id('email_$index')), ); // Act final result = await emailApi.markAsStar( aliceSession, AccountFixtures.aliceAccountId, - emails, + emailIds, MarkStarAction.markStar, ); @@ -488,7 +567,306 @@ void main() { data: anyNamed('data'), cancelToken: anyNamed('cancelToken'), )).called(countIterations); - expect(result.length, totalEmails); + expect(result.emailIdsSuccess.length, totalEmails); + expect(result.mapErrors.isEmpty, isTrue); + }); + + test( + 'SHOULD calls execute the correct number of times\n' + 'WHEN totalEmails is less than defaultMaxObjectsInSet\n' + 'WHEN maxObjectsInSet equal defaultMaxObjectsInSet\n' + 'AND defaultMaxObjectsInSet is 50', + () async { + // Arrange + const defaultMaxObjectsInSet = 50; + const maxObjectsInSet = 50; + const totalEmails = 30; + final maxBatches = [maxObjectsInSet, defaultMaxObjectsInSet, totalEmails].min; + final countIterations = (totalEmails / maxBatches).ceil(); + final aliceSession = SessionFixtures.getAliceSessionWithMaxObjectsInSet(maxObjectsInSet); + + when(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).thenAnswer((_) async => { + "sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943", + "methodResponses": [ + [ + "Email/set", + { + "accountId": AccountFixtures.aliceAccountId.asString, + "oldState": "4cb1e760-c11b-11ef-9cfb-ef2eae0e64b1", + "newState": "4cb1e760-c11b-11ef-9cfb-ef2eae0e64b1", + "updated": { + for (var item in List.generate(maxBatches, (index) => {"email_$index": null})) + item.keys.first: item.values.first + } + }, + "c0" + ], + [ + "Email/get", + { + "accountId": AccountFixtures.aliceAccountId.asString, + "state": "b965db40-c11c-11ef-9cfb-ef2eae0e64b1", + "list": List.generate(maxBatches, (index) => { + "id": "email_$index", + "keywords": { + "\$flagged": true + } + }), + "notFound": [] + }, + "c1" + ] + ] + }); + + final emailIds = List.generate( + totalEmails, + (index) => EmailId(Id('email_$index')), + ); + + // Act + final result = await emailApi.markAsStar( + aliceSession, + AccountFixtures.aliceAccountId, + emailIds, + MarkStarAction.markStar, + ); + + // Assert + verify(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).called(countIterations); + expect(result.emailIdsSuccess.length, totalEmails); + expect(result.mapErrors.isEmpty, isTrue); + }); + }); + + group('deleteMultipleEmailsPermanently::test', () { + test( + 'SHOULD calls execute the correct number of times\n' + 'WHEN maxObjectsInSet equal defaultMaxObjectsInSet\n' + 'AND defaultMaxObjectsInSet is 50', + () async { + // Arrange + const defaultMaxObjectsInSet = 50; + const maxObjectsInSet = 50; + const totalEmails = 100; + final maxBatches = [maxObjectsInSet, defaultMaxObjectsInSet, totalEmails].min; + final countIterations = (totalEmails / maxBatches).ceil(); + final aliceSession = SessionFixtures.getAliceSessionWithMaxObjectsInSet(maxObjectsInSet); + + when(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).thenAnswer((_) async => { + "sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943", + "methodResponses": [ + [ + "Email/set", + { + "accountId": AccountFixtures.aliceAccountId.asString, + "oldState": "4cb1e760-c11b-11ef-9cfb-ef2eae0e64b1", + "newState": "4cb1e760-c11b-11ef-9cfb-ef2eae0e64b1", + "destroyed": List.generate(maxBatches, (index) => "email_$index") + }, + "c0" + ] + ] + }); + + final emailIds = List.generate( + totalEmails, + (index) => EmailId(Id('email_$index')), + ); + + // Act + final result = await emailApi.deleteMultipleEmailsPermanently( + aliceSession, + AccountFixtures.aliceAccountId, + emailIds, + ); + + // Assert + verify(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).called(countIterations); + expect(result.emailIdsSuccess.length, totalEmails); + expect(result.mapErrors.isEmpty, isTrue); + }); + + test( + 'SHOULD calls execute the correct number of times\n' + 'WHEN maxObjectsInSet is greater than defaultMaxObjectsInSet\n' + 'AND defaultMaxObjectsInSet is 50', + () async { + // Arrange + const defaultMaxObjectsInSet = 50; + const maxObjectsInSet = 200; + const totalEmails = 100; + final maxBatches = [maxObjectsInSet, defaultMaxObjectsInSet, totalEmails].min; + final countIterations = (totalEmails / maxBatches).ceil(); + final aliceSession = SessionFixtures.getAliceSessionWithMaxObjectsInSet(maxObjectsInSet); + + when(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).thenAnswer((_) async => { + "sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943", + "methodResponses": [ + [ + "Email/set", + { + "accountId": AccountFixtures.aliceAccountId.asString, + "oldState": "4cb1e760-c11b-11ef-9cfb-ef2eae0e64b1", + "newState": "4cb1e760-c11b-11ef-9cfb-ef2eae0e64b1", + "destroyed": List.generate(maxBatches, (index) => "email_$index") + }, + "c0" + ] + ] + }); + + final emailIds = List.generate( + totalEmails, + (index) => EmailId(Id('email_$index')), + ); + + // Act + final result = await emailApi.deleteMultipleEmailsPermanently( + aliceSession, + AccountFixtures.aliceAccountId, + emailIds, + ); + + // Assert + verify(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).called(countIterations); + expect(result.emailIdsSuccess.length, totalEmails); + expect(result.mapErrors.isEmpty, isTrue); + }); + + test( + 'SHOULD calls execute the correct number of times\n' + 'WHEN maxObjectsInSet is less than defaultMaxObjectsInSet\n' + 'AND defaultMaxObjectsInSet is 50', + () async { + // Arrange + const defaultMaxObjectsInSet = 50; + const maxObjectsInSet = 20; + const totalEmails = 100; + final maxBatches = [maxObjectsInSet, defaultMaxObjectsInSet, totalEmails].min; + final countIterations = (totalEmails / maxBatches).ceil(); + final aliceSession = SessionFixtures.getAliceSessionWithMaxObjectsInSet(maxObjectsInSet); + + when(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).thenAnswer((_) async => { + "sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943", + "methodResponses": [ + [ + "Email/set", + { + "accountId": AccountFixtures.aliceAccountId.asString, + "oldState": "4cb1e760-c11b-11ef-9cfb-ef2eae0e64b1", + "newState": "4cb1e760-c11b-11ef-9cfb-ef2eae0e64b1", + "destroyed": List.generate(maxBatches, (index) => "email_$index") + }, + "c0" + ] + ] + }); + + final emailIds = List.generate( + totalEmails, + (index) => EmailId(Id('email_$index')), + ); + + // Act + final result = await emailApi.deleteMultipleEmailsPermanently( + aliceSession, + AccountFixtures.aliceAccountId, + emailIds, + ); + + // Assert + verify(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).called(countIterations); + expect(result.emailIdsSuccess.length, totalEmails); + expect(result.mapErrors.isEmpty, isTrue); + }); + + test( + 'SHOULD calls execute the correct number of times\n' + 'WHEN totalEmails is less than defaultMaxObjectsInSet\n' + 'WHEN maxObjectsInSet equal defaultMaxObjectsInSet\n' + 'AND defaultMaxObjectsInSet is 50', + () async { + // Arrange + const defaultMaxObjectsInSet = 50; + const maxObjectsInSet = 50; + const totalEmails = 30; + final maxBatches = [maxObjectsInSet, defaultMaxObjectsInSet, totalEmails].min; + final countIterations = (totalEmails / maxBatches).ceil(); + final aliceSession = SessionFixtures.getAliceSessionWithMaxObjectsInSet(maxObjectsInSet); + + when(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).thenAnswer((_) async => { + "sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943", + "methodResponses": [ + [ + "Email/set", + { + "accountId": AccountFixtures.aliceAccountId.asString, + "oldState": "4cb1e760-c11b-11ef-9cfb-ef2eae0e64b1", + "newState": "4cb1e760-c11b-11ef-9cfb-ef2eae0e64b1", + "destroyed": List.generate(maxBatches, (index) => "email_$index") + }, + "c0" + ] + ] + }); + + final emailIds = List.generate( + totalEmails, + (index) => EmailId(Id('email_$index')), + ); + + // Act + final result = await emailApi.deleteMultipleEmailsPermanently( + aliceSession, + AccountFixtures.aliceAccountId, + emailIds, + ); + + // Assert + verify(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).called(countIterations); + expect(result.emailIdsSuccess.length, totalEmails); + expect(result.mapErrors.isEmpty, isTrue); }); }); @@ -502,7 +880,7 @@ void main() { const defaultMaxObjectsInSet = 50; const maxObjectsInSet = 50; const totalEmails = 100; - final maxBatches = min(maxObjectsInSet, defaultMaxObjectsInSet); + final maxBatches = [maxObjectsInSet, defaultMaxObjectsInSet, totalEmails].min; final countIterations = (totalEmails / maxBatches).ceil(); final aliceSession = SessionFixtures.getAliceSessionWithMaxObjectsInSet(maxObjectsInSet); @@ -554,7 +932,8 @@ void main() { data: anyNamed('data'), cancelToken: anyNamed('cancelToken'), )).called(countIterations); - expect(result.length, totalEmails); + expect(result.emailIdsSuccess.length, totalEmails); + expect(result.mapErrors.isEmpty, isTrue); }); test( @@ -566,7 +945,7 @@ void main() { const defaultMaxObjectsInSet = 50; const maxObjectsInSet = 200; const totalEmails = 100; - final maxBatches = min(maxObjectsInSet, defaultMaxObjectsInSet); + final maxBatches = [maxObjectsInSet, defaultMaxObjectsInSet, totalEmails].min; final countIterations = (totalEmails / maxBatches).ceil(); final aliceSession = SessionFixtures.getAliceSessionWithMaxObjectsInSet(maxObjectsInSet); @@ -618,7 +997,8 @@ void main() { data: anyNamed('data'), cancelToken: anyNamed('cancelToken'), )).called(countIterations); - expect(result.length, totalEmails); + expect(result.emailIdsSuccess.length, totalEmails); + expect(result.mapErrors.isEmpty, isTrue); }); test( @@ -630,7 +1010,7 @@ void main() { const defaultMaxObjectsInSet = 50; const maxObjectsInSet = 20; const totalEmails = 100; - final maxBatches = min(maxObjectsInSet, defaultMaxObjectsInSet); + final maxBatches = [maxObjectsInSet, defaultMaxObjectsInSet, totalEmails].min; final countIterations = (totalEmails / maxBatches).ceil(); final aliceSession = SessionFixtures.getAliceSessionWithMaxObjectsInSet(maxObjectsInSet); @@ -682,7 +1062,74 @@ void main() { data: anyNamed('data'), cancelToken: anyNamed('cancelToken'), )).called(countIterations); - expect(result.length, totalEmails); + expect(result.emailIdsSuccess.length, totalEmails); + expect(result.mapErrors.isEmpty, isTrue); + }); + + test( + 'SHOULD calls execute the correct number of times\n' + 'WHEN totalEmails is less than defaultMaxObjectsInSet\n' + 'WHEN maxObjectsInSet equal defaultMaxObjectsInSet\n' + 'AND defaultMaxObjectsInSet is 50', + () async { + // Arrange + const defaultMaxObjectsInSet = 50; + const maxObjectsInSet = 50; + const totalEmails = 30; + final maxBatches = [maxObjectsInSet, defaultMaxObjectsInSet, totalEmails].min; + final countIterations = (totalEmails / maxBatches).ceil(); + final aliceSession = SessionFixtures.getAliceSessionWithMaxObjectsInSet(maxObjectsInSet); + + when(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).thenAnswer((_) async => { + "sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943", + "methodResponses": [ + [ + "Email/set", + { + "accountId": AccountFixtures.aliceAccountId.asString, + "oldState": "4cb1e760-c11b-11ef-9cfb-ef2eae0e64b1", + "newState": "4cb1e760-c11b-11ef-9cfb-ef2eae0e64b1", + "updated": { + for (var item in List.generate(maxBatches, (index) => {"email_$index": null})) + item.keys.first: item.values.first + } + }, + "c0" + ] + ] + }); + + final emailIds = List.generate( + totalEmails, + (index) => EmailId(Id('email_$index')), + ); + + // Act + final result = await emailApi.moveToMailbox( + aliceSession, + AccountFixtures.aliceAccountId, + MoveToMailboxRequest( + { + MailboxId(Id('mailboxA')): emailIds, + }, + MailboxId(Id('mailboxB')), + MoveAction.moving, + EmailActionType.moveToMailbox, + ), + ); + + // Assert + verify(httpClient.post( + '', + data: anyNamed('data'), + cancelToken: anyNamed('cancelToken'), + )).called(countIterations); + expect(result.emailIdsSuccess.length, totalEmails); + expect(result.mapErrors.isEmpty, isTrue); }); }); });