From 6f86506b28c79610b58e67c78c53548afecf979e Mon Sep 17 00:00:00 2001 From: smizrahi Date: Wed, 20 Nov 2024 16:50:15 +0100 Subject: [PATCH] feat(mail): Deletion of mail older than x. Closes #6023. --- Documentation/SOGoInstallationGuide.asciidoc | 3 + SoObjects/Mailer/SOGoMailAccount.h | 1 + SoObjects/Mailer/SOGoMailAccount.m | 36 +++-- SoObjects/SOGo/SOGoSystemDefaults.h | 2 + SoObjects/SOGo/SOGoSystemDefaults.m | 5 + UI/MailerUI/English.lproj/Localizable.strings | 15 +++ UI/MailerUI/French.lproj/Localizable.strings | 15 +++ UI/MailerUI/UIxMailFolderActions.m | 124 +++++++++++++++++ UI/MailerUI/UIxMailMainFrame.m | 11 ++ UI/MailerUI/product.plist | 10 ++ .../MailerUI/UIxMailFolderTemplate.wox | 83 ++++++++++++ UI/Templates/MailerUI/UIxMailMainFrame.wox | 14 ++ .../js/Mailer/Mailbox.service.js | 17 +++ .../js/Mailer/MailboxesController.js | 126 ++++++++++++++++++ .../js/Mailer/sgMailboxListItem.directive.js | 8 ++ .../scss/views/MailerUI.scss | 6 + 16 files changed, 467 insertions(+), 9 deletions(-) diff --git a/Documentation/SOGoInstallationGuide.asciidoc b/Documentation/SOGoInstallationGuide.asciidoc index 0e373f4f3..29f4dbfd6 100644 --- a/Documentation/SOGoInstallationGuide.asciidoc +++ b/Documentation/SOGoInstallationGuide.asciidoc @@ -1678,6 +1678,9 @@ URL could be set to something like: See the "EMail reminders" section in this document for more information. +|S |SOGoDisableMailCleaning +|Parameter used to disable the feature 'remove mails older than X days' introduced in 5.12. Default value is `NO`. + |S |SOGoDisableOrganizerEventCheck |Parameter used to disable organizer's calendar event check diff --git a/SoObjects/Mailer/SOGoMailAccount.h b/SoObjects/Mailer/SOGoMailAccount.h index 4c8e86d6e..46c1bb0c6 100644 --- a/SoObjects/Mailer/SOGoMailAccount.h +++ b/SoObjects/Mailer/SOGoMailAccount.h @@ -104,6 +104,7 @@ typedef enum { - (NSArray *) toManyRelationshipKeysWithNamespaces: (BOOL) withNSs; - (NSArray *) allFolderPaths: (SOGoMailListingMode) theListingMode; +- (NSArray *) allFolderPaths: (SOGoMailListingMode) theListingMode onlyRoot: (BOOL) onlyRoot; - (NSArray *) allFoldersMetadata: (SOGoMailListingMode) theListingMode; - (NSDictionary *) imapFolderGUIDs; diff --git a/SoObjects/Mailer/SOGoMailAccount.m b/SoObjects/Mailer/SOGoMailAccount.m index aa2a30b0c..2b4e2a34a 100644 --- a/SoObjects/Mailer/SOGoMailAccount.m +++ b/SoObjects/Mailer/SOGoMailAccount.m @@ -378,14 +378,21 @@ static NSString *inboxFolderName = @"INBOX"; return folders; } -// -// -// -- (NSArray *) allFolderPaths: (SOGoMailListingMode) theListingMode +- (NSArray *) allFolderPaths: (SOGoMailListingMode) theListingMode { - NSMutableArray *folderPaths, *namespaces; + return [self allFolderPaths: theListingMode onlyRoot: NO]; +} + +// +// +// +- (NSArray *) allFolderPaths: (SOGoMailListingMode) theListingMode onlyRoot: (BOOL) onlyRoot +{ + NSMutableArray *folderPaths, *namespaces, *filteredFolders; NSArray *folders, *mainFolders; NSString *namespace; + NSString *folder; + NSUInteger slashCount; BOOL subscribedOnly; int count, max; @@ -401,10 +408,10 @@ static NSString *inboxFolderName = @"INBOX"; onlySubscribedFolders: YES]; max = [folders count]; for (count = 0; count < max; count++) - { - [subscribedFolders setObject: [NSNull null] - forKey: [folders objectAtIndex: count]]; - } + { + [subscribedFolders setObject: [NSNull null] + forKey: [folders objectAtIndex: count]]; + } [[self imap4Connection] flushFolderHierarchyCache]; } @@ -418,6 +425,17 @@ static NSString *inboxFolderName = @"INBOX"; nil] stringsWithFormat: @"/%@"]; folders = [[self imap4Connection] allFoldersForURL: [self imap4URL] onlySubscribedFolders: subscribedOnly]; + if (onlyRoot) { + filteredFolders = [[NSMutableArray alloc] init]; + for (folder in folders) { + slashCount = [[folder componentsSeparatedByString:@"/"] count] - 1; + if (slashCount <= 1) { + [filteredFolders addObject: folder]; + } + } + folders = [NSArray arrayWithArray: filteredFolders]; + [filteredFolders release]; + } folderPaths = [folders mutableCopy]; [folderPaths autorelease]; diff --git a/SoObjects/SOGo/SOGoSystemDefaults.h b/SoObjects/SOGo/SOGoSystemDefaults.h index e3e2c8d2c..72e063a64 100644 --- a/SoObjects/SOGo/SOGoSystemDefaults.h +++ b/SoObjects/SOGo/SOGoSystemDefaults.h @@ -142,6 +142,8 @@ NSComparisonResult languageSort(id el1, id el2, void *context); - (NSString *)urlCreateAccount; +- (BOOL)disableMailCleaning; + @end #endif /* SOGOSYSTEMDEFAULTS_H */ diff --git a/SoObjects/SOGo/SOGoSystemDefaults.m b/SoObjects/SOGo/SOGoSystemDefaults.m index 61c299245..87d63f85e 100644 --- a/SoObjects/SOGo/SOGoSystemDefaults.m +++ b/SoObjects/SOGo/SOGoSystemDefaults.m @@ -933,4 +933,9 @@ NSComparisonResult languageSort(id el1, id el2, void *context) return [self stringForKey: @"SOGoURLCreateAccount"]; } +- (BOOL) disableMailCleaning +{ + return [self boolForKey: @"SOGoDisableMailCleaning"]; +} + @end diff --git a/UI/MailerUI/English.lproj/Localizable.strings b/UI/MailerUI/English.lproj/Localizable.strings index 7095b54e4..bafd2d681 100644 --- a/UI/MailerUI/English.lproj/Localizable.strings +++ b/UI/MailerUI/English.lproj/Localizable.strings @@ -514,3 +514,18 @@ /* Hotkey to forward to a message */ "hotkey_forward" = "f"; + +/* Email deletion */ +"Clean folder" = "Clean folder"; +"Clean mailbox" = "Clean mailbox"; +"Delete permanently (do not use trash)" = "Delete permanently (do not use trash)"; +"Apply to subfolders" = "Apply to subfolders"; +"Delete e-mails older than" = "Delete e-mails older than"; +"3 months" = "3 months"; +"6 months" = "6 months"; +"9 months" = "9 months"; +"1 year" = "1 year"; +"Custom" = "Custom"; +"You are about to permanently delete some messages. Are you sure you want to proceed with this action?" = "You are about to permanently delete some messages. Are you sure you want to proceed with this action?"; +"Apply" = "Apply"; +"%{0} message(s) deleted" = "%{0} message(s) deleted"; \ No newline at end of file diff --git a/UI/MailerUI/French.lproj/Localizable.strings b/UI/MailerUI/French.lproj/Localizable.strings index fa8b8cec9..e70f62520 100644 --- a/UI/MailerUI/French.lproj/Localizable.strings +++ b/UI/MailerUI/French.lproj/Localizable.strings @@ -514,3 +514,18 @@ /* Hotkey to forward to a message */ "hotkey_forward" = "f"; + +/* Email deletion */ +"Clean folder" = "Nettoyer le dossier"; +"Clean mailbox" = "Nettoyer la boite mail"; +"Delete permanently (do not use trash)" = "Supprimer définitivement (ne pas utiliser la corbeille)"; +"Apply to subfolders" = "Appliquer aux sous-dossiers"; +"Delete e-mails older than" = "Supprimer les e-mails plus vieux de"; +"3 months" = "3 mois"; +"6 months" = "6 mois"; +"9 months" = "9 mois"; +"1 year" = "1 an"; +"Custom" = "Personnalisé"; +"You are about to permanently delete some messages. Are you sure you want to proceed with this action?" = "Vous êtes sur le point de supprimer définitivement certains messages. Êtes-vous certain de vouloir effectuer cette action ?"; +"Apply" = "Appliquer"; +"%{0} message(s) deleted" = "%{0} message(s) supprimés"; \ No newline at end of file diff --git a/UI/MailerUI/UIxMailFolderActions.m b/UI/MailerUI/UIxMailFolderActions.m index 660b81a4b..60bb13e24 100644 --- a/UI/MailerUI/UIxMailFolderActions.m +++ b/UI/MailerUI/UIxMailFolderActions.m @@ -21,6 +21,7 @@ #import #import #import +#import #import #import @@ -46,6 +47,8 @@ #import #import +#import + #import "UIxMailFolderActions.h" @implementation UIxMailFolderActions @@ -1298,4 +1301,125 @@ return [self _markMessagesAsJunkOrNotJunk: NO]; } +- (BOOL)isDateStringValid: (NSString *)dateString { + NSString *dateRegex = @"^\\d{4}-\\d{2}-\\d{2}$"; + NSPredicate *dateTest = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", dateRegex]; + return [dateTest evaluateWithObject:dateString]; +} + +- (void) cleanFolderWithFolder: (SOGoMailFolder *)folder + withQualifier: (EOQualifier *)qualifier + recursive: (BOOL)isRecursive + withTrashFolder: (BOOL)useTrashFolder + exception: (NSException **)exception + counter: (NSUInteger *)counter { + NSArray *results, *subfolders; + NSUInteger i; + SOGoMailFolder *newFolder; + NSString *folderName; + NSException *e; + + results = [folder fetchUIDsMatchingQualifier: qualifier + sortOrdering: @"REVERSE DATE" + threaded: NO]; + + if (results && [results count] > 0) { + e = [folder deleteUIDs: results + useTrashFolder: &useTrashFolder + inContext: [self context]]; + [folder expunge]; + [self logWithFormat: @"Removed %d messages in %@ (%@)", [results count], [folder nameInContainer], results]; + *counter += [results count]; + if (e) { + [self errorWithFormat: @"Error while cleaning mailbox : %@", e]; + *exception = e; + } + } + + if (isRecursive) { + subfolders = [folder subfolders]; + for (i = 0 ; i < [subfolders count] ; i++) { + folderName = [NSString stringWithFormat:@"%@/%@", [folder nameInContainer], [subfolders objectAtIndex: i]]; + newFolder = [[SOGoMailFolder alloc] initWithName: folderName inContainer: [folder container]]; + [self cleanFolderWithFolder: newFolder + withQualifier: qualifier + recursive: isRecursive + withTrashFolder: useTrashFolder + exception: exception + counter: counter]; + [newFolder release]; + } + } +} + +- (WOResponse *) cleanMailboxAction +{ + NSDictionary *jsonRequest, *jsonResponse; + WORequest *request; + NSString *searchString, *folderName; + EOQualifier *searchQualifier; + BOOL isRecursive, useTrashFolder; + NSException *exception; + SOGoMailFolder *folder; + SOGoMailAccount *account; + NSUInteger i, counter; + NSArray *folderNames; + + request = [[self context] request]; + jsonRequest = [[request contentAsString] objectFromJSONString]; + + if ([[SOGoSystemDefaults sharedSystemDefaults] disableMailCleaning]) + return [self responseWithStatus: 401]; + + if (![self isDateStringValid: [jsonRequest objectForKey: @"date"]]) { + [self errorWithFormat: @"Error while cleaning mailbox, invalid date : %@", [jsonRequest objectForKey: @"date"]]; + return [self responseWithStatus: 502]; + } + + searchString = [NSString stringWithFormat: @"(NOT (flags = 'deleted') AND (DATE <= (NSCalendarDate)\"%@\"))", [jsonRequest objectForKey: @"date"]]; + searchQualifier = [EOQualifier qualifierWithQualifierFormat: searchString]; + isRecursive = [[jsonRequest objectForKey: @"applyToSubfolders"] boolValue]; + useTrashFolder = ![[jsonRequest objectForKey: @"permanentlyDelete"] boolValue]; + counter = 0; + exception = nil; + + if ([[self clientObject] isKindOfClass: [SOGoMailFolder class]]) { + folder = [self clientObject]; + [self cleanFolderWithFolder: folder + withQualifier: searchQualifier + recursive: isRecursive + withTrashFolder: useTrashFolder + exception: &exception + counter: &counter]; + } else if ([[self clientObject] isKindOfClass: [SOGoMailAccount class]]) { + account = [self clientObject]; + folderNames = [account allFolderPaths: SOGoMailStandardListing onlyRoot: YES]; + for (i = 0 ; i < [folderNames count] ; i ++) { + folderName = [folderNames objectAtIndex: i]; + if ([folderName hasPrefix:@"/"]) { + folderName = [folderName substringFromIndex:1]; + } + folder = [account folderWithTraversal: folderName andClassName: nil]; + // Disable clean for trash folder + if (![folderName isEqualToString: [account trashFolderNameInContext: [self context]]]) + [self cleanFolderWithFolder: folder + withQualifier: searchQualifier + recursive: isRecursive + withTrashFolder: useTrashFolder + exception: &exception + counter: &counter]; + } + + [[account imap4Connection] flushMailCaches]; + } + + if (exception) { + return [self responseWithStatus: 500]; + } + + jsonResponse = [NSDictionary dictionaryWithObject: [NSNumber numberWithInt: counter] + forKey: @"nbMessageDeleted"]; + return [self responseWithStatus: 200 andJSONRepresentation: jsonResponse]; +} + @end diff --git a/UI/MailerUI/UIxMailMainFrame.m b/UI/MailerUI/UIxMailMainFrame.m index 82dea706c..cb0d357f9 100644 --- a/UI/MailerUI/UIxMailMainFrame.m +++ b/UI/MailerUI/UIxMailMainFrame.m @@ -611,6 +611,17 @@ return result; } +- (BOOL) isCleanMailboxEnabled { + BOOL result; + SOGoSystemDefaults *sd; + + sd = [SOGoSystemDefaults sharedSystemDefaults]; + + result = ![sd disableMailCleaning]; + + return result; +} + @end /* UIxMailMainFrame */ @interface UIxMailFolderTemplate : UIxComponent diff --git a/UI/MailerUI/product.plist b/UI/MailerUI/product.plist index fd1d96fe6..e81dec6f5 100644 --- a/UI/MailerUI/product.plist +++ b/UI/MailerUI/product.plist @@ -49,6 +49,11 @@ actionClass = "UIxMailFolderActions"; actionName = "expunge"; }; + cleanMailbox = { + protectedBy = "View"; + actionClass = "UIxMailFolderActions"; + actionName = "cleanMailbox"; + }; createFolder = { protectedBy = "View"; actionClass = "UIxMailFolderActions"; @@ -417,6 +422,11 @@ protectedBy = "View"; pageName = "UIxMailUserDelegationEditor"; }; + cleanMailbox = { + protectedBy = "View"; + actionClass = "UIxMailFolderActions"; + actionName = "cleanMailbox"; + }; addDelegate = { protectedBy = "View"; actionClass = "UIxMailAccountActions"; diff --git a/UI/Templates/MailerUI/UIxMailFolderTemplate.wox b/UI/Templates/MailerUI/UIxMailFolderTemplate.wox index c66cd9e6b..e0fa2ea0d 100644 --- a/UI/Templates/MailerUI/UIxMailFolderTemplate.wox +++ b/UI/Templates/MailerUI/UIxMailFolderTemplate.wox @@ -632,4 +632,87 @@ + + diff --git a/UI/Templates/MailerUI/UIxMailMainFrame.wox b/UI/Templates/MailerUI/UIxMailMainFrame.wox index 2e9b0fade..2a20e5eae 100644 --- a/UI/Templates/MailerUI/UIxMailMainFrame.wox +++ b/UI/Templates/MailerUI/UIxMailMainFrame.wox @@ -69,6 +69,13 @@ + + + + ... + + + + + + + ... + + + diff --git a/UI/WebServerResources/js/Mailer/Mailbox.service.js b/UI/WebServerResources/js/Mailer/Mailbox.service.js index 7dca6d67a..6408a5cc5 100644 --- a/UI/WebServerResources/js/Mailer/Mailbox.service.js +++ b/UI/WebServerResources/js/Mailer/Mailbox.service.js @@ -1312,4 +1312,21 @@ Mailbox.prototype.getHighlightWords = function () { return this.$highlightWords; }; + + /** + * @function cleanMailbox + * @memberof Mailbox.prototype + * @desc Cleans up the mailbox by applying the specified parameters. + * This can include operations such as moving emails to trash, + * filtering emails based on a duration, and applying actions to subfolders. + * @param {Object} parameters - An object containing the parameters for cleaning the mailbox. + * @param {boolean} [parameters.applyToSubfolders=false] - Whether to apply the cleaning operation to subfolders. + * @param {boolean} [parameters.moveToTrash=false] - Whether to move the emails to the trash folder. + * @param {string|null} [parameters.filterDuration=null] - A duration filter (e.g., "3m", "6m") to select emails older than the specified duration. + * @returns {Promise} A promise that resolves when the cleaning operation is completed, or rejects with an error if the operation fails. + */ + Mailbox.prototype.cleanMailbox = function (parameters) { + return parameters.folders.length > 0 ? Mailbox.$$resource.post(this.id.split("/")[0], 'cleanMailbox', parameters) : Mailbox.$$resource.post(this.id, 'cleanMailbox', parameters); + }; + })(); diff --git a/UI/WebServerResources/js/Mailer/MailboxesController.js b/UI/WebServerResources/js/Mailer/MailboxesController.js index 83f01735c..8bb12425e 100644 --- a/UI/WebServerResources/js/Mailer/MailboxesController.js +++ b/UI/WebServerResources/js/Mailer/MailboxesController.js @@ -53,6 +53,10 @@ $rootScope.$on('resetMailAdvancedSearchPanel', function () { vm.reset(); }); + + $rootScope.$on('showCleanMailboxPanel', function (e, d) { + vm.showCleanMailboxPanel(d.folder, d.account); + }); }; @@ -447,6 +451,128 @@ }); }; + this.showCleanMailboxPanel = function (folder, account) { + // Close sidenav on small devices + if (!$mdMedia(sgConstant['gt-md'])) + $mdSidenav('left').close(); + + $mdDialog.show({ + template: document.getElementById('cleanMailbox').innerHTML, + parent: angular.element(document.body), + controller: function () { + var dialogCtrl = this; + + this.$onInit = function () { + this.mainController = vm; + this.folder = folder; + this.isMailbox = (folder ? false : true); + this.name = folder ? folder.$displayName : account.name; + this.loading = false; + this.date = null; + this.form = { + filterDuration: "3m", + permanentlyDelete: false, + confirmDelete: false, + filterDurationDate: null + }; + + var today = new Date(); + var maxDate = new Date(today); + maxDate.setMonth(today.getMonth() - 3); + this.maxDate = maxDate; + }; + + dialogCtrl.closeDialog = function () { + $mdDialog.hide(); + }; + + dialogCtrl.isLoading = function () { + return this.loading; + } + + dialogCtrl.isWarningDisplayed = function () { + return (this.form && this.form.permanentlyDelete); + } + + dialogCtrl.isApplyDisabled = function () { + return !(!this.loading + && (!this.form.permanentlyDelete || (this.form.permanentlyDelete && this.form.confirmDelete)) + && (this.form.filterDuration != 'custom' || (this.form.filterDuration == 'custom' && this.form.filterDurationDate)) + ); + } + + dialogCtrl.apply = function () { + var folders = []; + var i; + if (account) { + for (i = 0; i < account.$mailboxes.length ; i++) { + folders.push(account.$mailboxes[i].id); + } + this.folder = account.$mailboxes[0]; + } + var date = ''; + var durationMonth = 12; + var date = new Date(); + switch (this.form.filterDuration) { + case '3m': + durationMonth = 3; + date.setMonth(date.getMonth() - durationMonth); + break; + case '6m': + durationMonth = 6; + date.setMonth(date.getMonth() - durationMonth); + break; + case '9m': + durationMonth = 9; + date.setMonth(date.getMonth() - durationMonth); + break; + case '1y': + durationMonth = 12; + date.setMonth(date.getMonth() - durationMonth); + break; + case 'custom': + date = this.form.filterDurationDate; + break; + } + var year = date.getFullYear(); + var month = String(date.getMonth() + 1).padStart(2, '0'); + var day = String(date.getDate()).padStart(2, '0'); + this.date = `${year}-${month}-${day}`; + this.folder.cleanMailbox({ + 'applyToSubfolders': (this.form && this.form.applyToSubfolders) ? this.form.applyToSubfolders : false, + 'permanentlyDelete': (this.form && this.form.permanentlyDelete) ? this.form.permanentlyDelete : false, + 'date': this.date, + 'folders': folders + }).then(function (data) { + dialogCtrl.loading = true; + Mailbox.selectedFolder.$filter({ + "sort": "date", + "asc": false, + "match": "OR" + }).then(function () { + $state.go('mail.account.mailbox', { accountId: vm.accounts[0].id, mailboxId: encodeUriFilter(Mailbox.selectedFolder.path) }); + dialogCtrl.loading = false; + $mdDialog.hide(); + + $mdToast.show( + $mdToast.simple() + .textContent(l('%{0} message(s) deleted', data.nbMessageDeleted)) + .position(sgConstant.toastPosition) + .hideDelay(2000)); + }); + }).catch(function () { + dialogCtrl.loading = false; + $mdDialog.hide(); + }); + }; + }, + controllerAs: 'dialogCtrl', + clickOutsideToClose: false, + escapeToClose: false, + }); + }; + + this.delegate = function(account) { $mdDialog.show({ templateUrl: account.id + '/delegation', // UI/Templates/MailerUI/UIxMailUserDelegation.wox diff --git a/UI/WebServerResources/js/Mailer/sgMailboxListItem.directive.js b/UI/WebServerResources/js/Mailer/sgMailboxListItem.directive.js index 08a4b5be3..3502af092 100644 --- a/UI/WebServerResources/js/Mailer/sgMailboxListItem.directive.js +++ b/UI/WebServerResources/js/Mailer/sgMailboxListItem.directive.js @@ -271,6 +271,14 @@ }); }; + this.cleanMailbox = function () { + // Close sidenav on small devices + if (!$mdMedia(sgConstant['gt-md'])) + $mdSidenav('left').close(); + + $rootScope.$broadcast('showCleanMailboxPanel', {folder: this.folder, account: null}); // Show remove old emails panel (broadcast event to MailboxesController) + }; + this.emptyJunkFolder = function() { return this.emptyFolder(l('Junk folder emptied')); }; diff --git a/UI/WebServerResources/scss/views/MailerUI.scss b/UI/WebServerResources/scss/views/MailerUI.scss index ba5719534..f014b55bd 100644 --- a/UI/WebServerResources/scss/views/MailerUI.scss +++ b/UI/WebServerResources/scss/views/MailerUI.scss @@ -531,4 +531,10 @@ mark { cursor: pointer; padding-left: 10px; padding-right: 10px; +} + +/* Clean mailbox */ + +.clean-mailbox-warn { + color: #f44336; } \ No newline at end of file