From 9eb80396988bd7c8ac3a1f44dbc48147ced20750 Mon Sep 17 00:00:00 2001 From: smizrahi Date: Tue, 21 May 2024 09:25:20 +0200 Subject: [PATCH] feat(mail): Improve mail search (advanced search) --- UI/MailerUI/English.lproj/Localizable.strings | 17 + UI/MailerUI/French.lproj/Localizable.strings | 17 + UI/MailerUI/UIxMailListActions.m | 237 +++++++++++-- .../MailerUI/UIxMailFolderTemplate.wox | 328 +++++++++++++++--- UI/Templates/MailerUI/UIxMailMainFrame.wox | 55 +-- UI/Templates/MailerUI/UIxMailViewTemplate.wox | 4 +- UI/WebServerResources/GNUmakefile | 2 +- UI/WebServerResources/Gruntfile.js | 3 +- UI/WebServerResources/css/styles.css | 2 +- UI/WebServerResources/css/styles.css.map | 2 +- UI/WebServerResources/js/Mailer.js | 2 +- UI/WebServerResources/js/Mailer.js.map | 2 +- UI/WebServerResources/js/Mailer.services.js | 2 +- .../js/Mailer.services.js.map | 2 +- .../js/Mailer/Mailbox.service.js | 54 +++ .../js/Mailer/MailboxController.js | 14 +- .../js/Mailer/MailboxesController.js | 293 ++++++++++++++-- UI/WebServerResources/js/Mailer/Mailer.app.js | 6 +- .../js/Mailer/Message.service.js | 70 ++++ .../js/Mailer/sgMailboxListItem.directive.js | 12 +- .../Mailer/sgMessageListItemMain.directive.js | 27 +- UI/WebServerResources/js/vendor/mark.min.js | 7 + UI/WebServerResources/package.json | 3 +- .../scss/components/toolbar/toolbar.scss | 33 +- .../scss/views/MailerUI.scss | 75 ++++ 25 files changed, 1063 insertions(+), 206 deletions(-) create mode 100644 UI/WebServerResources/js/vendor/mark.min.js diff --git a/UI/MailerUI/English.lproj/Localizable.strings b/UI/MailerUI/English.lproj/Localizable.strings index 2887b0df7..919bab383 100644 --- a/UI/MailerUI/English.lproj/Localizable.strings +++ b/UI/MailerUI/English.lproj/Localizable.strings @@ -143,6 +143,23 @@ "This mail is being sent from an unsecure network!" = "This mail is being sent from an unsecure network!"; "Address Book" = "Address Book"; "Search For" = "Search For"; +"Contains" = "Contains"; +"Does not contains" = "Does not contains"; +"Advanced search" = "Advanced search"; +"Anytime" = "Anytime"; +"Last 7 days" = "Last 7 days"; +"Last 30 days" = "Last 30 days"; +"Last 6 month" = "Last 6 month"; +"Before" = "Before"; +"After" = "After"; +"Between" = "Between"; +"and" = "and"; +"With attachments" = "With attachments"; +"In favorites" = "In favorites"; +"Unseen only" = "Unseen only"; +"Show more" = "Show more"; +"Reset" = "Reset"; +"Message size" = "Message size"; /* Popup "show" */ "all" = "all"; diff --git a/UI/MailerUI/French.lproj/Localizable.strings b/UI/MailerUI/French.lproj/Localizable.strings index 51ebaac98..dda729eab 100644 --- a/UI/MailerUI/French.lproj/Localizable.strings +++ b/UI/MailerUI/French.lproj/Localizable.strings @@ -143,6 +143,23 @@ "This mail is being sent from an unsecure network!" = "Ce mail est envoyé depuis un réseau non sécurisé !"; "Address Book" = "Carnet d'adresses"; "Search For" = "Rechercher"; +"Contains" = "Contient"; +"Does not contains" = "Ne contient pas"; +"Advanced search" = "Recherche avancée"; +"Anytime" = "N'importe quand"; +"Last 7 days" = "Les 7 derniers jours"; +"Last 30 days" = "Les 30 derniers jours"; +"Last 6 month" = "Les 6 derniers mois"; +"Before" = "Avant"; +"After" = "Après"; +"Between" = "Entre"; +"and" = "et"; +"With attachments" = "Avec des pièces jointes"; +"In favorites" = "En favori"; +"Unseen only" = "Non lu uniquement"; +"Show more" = "Plus de paramètres"; +"Reset" = "Réinitialiser"; +"Message size" = "Taille du message"; /* Popup "show" */ "all" = "Tous"; diff --git a/UI/MailerUI/UIxMailListActions.m b/UI/MailerUI/UIxMailListActions.m index 3008cf19f..29cac782d 100644 --- a/UI/MailerUI/UIxMailListActions.m +++ b/UI/MailerUI/UIxMailListActions.m @@ -32,6 +32,7 @@ #import #import /* for locale string constants */ #import +#import #import #import @@ -340,7 +341,6 @@ && [[currentPart objectForKey:@"disposition"] objectForKey:@"type"] && [[[[currentPart objectForKey:@"disposition"] objectForKey:@"type"] uppercaseString] isEqualToString:@"INLINE"]; isImage = [SOGoMailBodyPart bodyPartClassForMimeType: [contentType lowercaseString] inContext: [self context]] == [SOGoImageMailBodyPart class]; - id foo = [SOGoMailBodyPart bodyPartClassForMimeType: [contentType lowercaseString] inContext: [self context]]; if (![ud hideInlineAttachments] || ([ud hideInlineAttachments] && !(isInline && isImage))) { @@ -471,17 +471,19 @@ EOQualifier *qualifier, *notDeleted, *searchQualifier; WORequest *request; NSDictionary *sortingAttributes, *content, *filter; - NSArray *filters, *labels; - NSString *searchBy, *searchInput, *searchString, *match, *label; + NSArray *filters, *labels, *searchInputSplit, *flags; + NSString *searchBy, *searchInput, *searchString, *match, *label, *operator, *sizeUnit, *dateFrom, *dateTo; NSMutableArray *qualifiers, *searchArray, *labelQualifiers; + NSNumberFormatter *formatter; + NSNumber *size; BOOL unseenOnly, flaggedOnly; - int max, i; + int max, i, j; request = [context request]; content = [[request contentAsString] objectFromJSONString]; notDeleted = [EOQualifier qualifierWithQualifierFormat: @"(not (flags = %@))", @"deleted"]; qualifiers = [NSMutableArray arrayWithObject: notDeleted]; - searchString = nil; + searchString = @""; match = nil; filters = [content objectForKey: @"filters"]; labels = [content objectForKey: @"labels"]; @@ -494,35 +496,145 @@ if (max > 0) { searchArray = [NSMutableArray arrayWithCapacity: max]; for (i = 0; i < max; i++) - { - filter = [filters objectAtIndex:i]; - searchBy = [filter objectForKey: @"searchBy"]; - searchInput = [filter objectForKey: @"searchInput"]; - if (searchBy && searchInput) - { - if ([[filter objectForKey: @"negative"] boolValue]) - searchString = [NSString stringWithFormat: @"(not (%@ doesContain: '%@'))", searchBy, searchInput]; - else - searchString = [NSString stringWithFormat: @"(%@ doesContain: '%@')", searchBy, searchInput]; + { + filter = [filters objectAtIndex:i]; + searchBy = [filter objectForKey: @"searchBy"]; + searchInput = [filter objectForKey: @"searchInput"]; + + if (searchBy && searchInput) + { + // Size + if ([searchBy isEqualToString: @"size"]) { + operator = [filter objectForKey: @"operator"]; + sizeUnit = [filter objectForKey: @"sizeUnit"]; + formatter = [[NSNumberFormatter alloc] init]; + formatter.numberStyle = NSNumberFormatterDecimalStyle; + size = [formatter numberFromString: searchInput]; + [formatter release]; + if ([[sizeUnit lowercaseString] isEqualToString: @"kb"]) { + size = [NSNumber numberWithLongLong: [size longLongValue] * 1024]; + } else if ([[sizeUnit lowercaseString] isEqualToString: @"mb"]) { + size = [NSNumber numberWithLongLong: [size longLongValue] * 1024 * 1024]; + } else if ([[sizeUnit lowercaseString] isEqualToString: @"gb"]) { + size = [NSNumber numberWithLongLong: [size longLongValue] * 1024 * 1024 * 1024]; + } + + searchString = [NSString stringWithFormat: @"(%@ %@ %@)", searchBy, operator, [size stringValue]]; + } else if ([searchBy isEqualToString: @"date"]) { + // Date + operator = [filter objectForKey: @"operator"]; + searchString = [NSString stringWithFormat: @"(%@ %@ (NSCalendarDate)\"%@\")", searchBy, operator, searchInput]; + } else if ([searchBy isEqualToString: @"date_between"]) { + // Date between + dateFrom = [filter objectForKey: @"dateFrom"]; + dateTo = [filter objectForKey: @"dateTo"]; + searchString = [NSString stringWithFormat: @"(date >= (NSCalendarDate)\"%@\" AND date <= (NSCalendarDate)\"%@\")", dateFrom, dateTo]; + } else if ([searchBy isEqualToString: @"attachment"]) { + // Attachment + // Not possible with imap search, check in getUIDsInFolder method + // The attachments must be checked in headers + } else if ([searchBy isEqualToString: @"favorite"]) { + // Favorite + flaggedOnly = YES; + } else if ([searchBy isEqualToString: @"unseen"]) { + // Unseen + unseenOnly = YES; + } else if ([searchBy isEqualToString: @"contains"]) { + // Contains + // Split on space to check each word + searchInput = [searchInput stringByReplacingOccurrencesOfString:@"'" withString:@"\\'"]; + searchInputSplit = [searchInput componentsSeparatedByString:@" "]; + if ([searchInputSplit count] > 1) { + searchString = @"("; + j = 0; + for (searchInput in searchInputSplit) { + if (j > 0) { + searchString = [NSString stringWithFormat: @"%@ OR", searchString]; + } + + searchString = [NSString stringWithFormat: @"%@ (subject doesContain: '%@' OR body doesContain: '%@')", + searchString, searchInput, searchInput]; + j++; + } + searchString = [NSString stringWithFormat: @"%@)", searchString]; + } else { + searchString = [NSString stringWithFormat: @"(subject doesContain: '%@' OR body doesContain: '%@')", + searchInput, searchInput]; + } + + } else if ([searchBy isEqualToString: @"not_contains"]) { + // Not contains + searchInput = [searchInput stringByReplacingOccurrencesOfString:@"'" withString:@"\\'"]; + searchString = [NSString stringWithFormat: @"(NOT (subject doesContain: '%@') AND NOT (body doesContain: '%@'))", + searchInput, searchInput]; + // Split on space to check each word + searchInput = [searchInput stringByReplacingOccurrencesOfString:@"'" withString:@"\\'"]; + searchInputSplit = [searchInput componentsSeparatedByString:@" "]; + if ([searchInputSplit count] > 1) { + searchString = @"("; + j = 0; + for (searchInput in searchInputSplit) { + if (j > 0) { + searchString = [NSString stringWithFormat: @"%@ AND", searchString]; + } + + searchString = [NSString stringWithFormat: @"%@ (NOT (subject doesContain: '%@') AND NOT (body doesContain: '%@'))", + searchString, searchInput, searchInput]; + j++; + } + searchString = [NSString stringWithFormat: @"%@)", searchString]; + } else { + searchString = [NSString stringWithFormat: @"(NOT (subject doesContain: '%@') AND NOT (body doesContain: '%@'))", + searchInput, searchInput]; + } + } else if ([searchBy isEqualToString: @"flags"]) { + // Flags + flags = [filter objectForKey: @"flags"]; + if (flags && [flags count] > 0) { + searchString = @"("; + + for (j = 0 ; j < [flags count] ; j++) { + if (j > 0) + searchString = [NSString stringWithFormat: @"%@ AND", searchString]; + searchString = [NSString stringWithFormat: @"%@ (flags = '%@')", + searchString, + [[flags objectAtIndex: j] stringByReplacingOccurrencesOfString: @"_$" withString:@"$"]]; + } + searchString = [NSString stringWithFormat: @"%@)", searchString]; + } + } else { + // Others + searchString = [NSString stringWithFormat: @"(%@ doesContain: '%@')", searchBy, searchInput]; + } + + if ([[filter objectForKey: @"negative"] boolValue]) + searchString = [NSString stringWithFormat: @"(not %@)", searchString]; + + + if (searchString && [searchString length] > 0) { searchQualifier = [EOQualifier qualifierWithQualifierFormat: searchString]; if (searchQualifier) [searchArray addObject: searchQualifier]; } - else - { - [self errorWithFormat: @"Missing parameters in search filter: %@", filter]; - } - } - sortingAttributes = [content objectForKey: @"sortingAttributes"]; - if (sortingAttributes) - match = [sortingAttributes objectForKey: @"match"]; // AND, OR - if ([match isEqualToString: @"OR"]) - qualifier = [[EOOrQualifier alloc] initWithQualifierArray: searchArray]; - else - qualifier = [[EOAndQualifier alloc] initWithQualifierArray: searchArray]; - [qualifier autorelease]; - [qualifiers addObject: qualifier]; + } + else + { + [self errorWithFormat: @"Missing parameters in search filter: %@", filter]; + } + } + + if ([searchArray count] > 0) { + sortingAttributes = [content objectForKey: @"sortingAttributes"]; + if (sortingAttributes) + match = [sortingAttributes objectForKey: @"match"]; // AND, OR + if ([match isEqualToString: @"OR"]) + qualifier = [[EOOrQualifier alloc] initWithQualifierArray: searchArray]; + else + qualifier = [[EOAndQualifier alloc] initWithQualifierArray: searchArray]; + [qualifier autorelease]; + [qualifiers addObject: qualifier]; + } } } @@ -719,14 +831,25 @@ - (NSDictionary *) getUIDsInFolder: (SOGoMailFolder *) folder withHeaders: (BOOL) includeHeaders +{ + return [self getUIDsInFolder: folder + withHeaders: includeHeaders + onlyAttachments: NO]; +} + +- (NSDictionary *) getUIDsInFolder: (SOGoMailFolder *) folder + withHeaders: (BOOL) includeHeaders + onlyAttachments: (BOOL) onlyAttachments { NSArray *uids, *threadedUids, *headers; NSMutableDictionary *data; + NSMutableArray *tmpHeaders, *tmpUids; + NSNumber *uid; SOGoMailAccount *account; id quota; NSRange r; - int count; + int count, i, j; data = [NSMutableDictionary dictionary]; @@ -762,11 +885,35 @@ a = [uids flattenedArray]; count = [a count]; - if (count > headersPrefetchMaxSize) + if (count > headersPrefetchMaxSize && !onlyAttachments) // Only attachment to get all messages count = headersPrefetchMaxSize; r = NSMakeRange(0, count); headers = [self getHeadersForUIDs: [a subarrayWithRange: r] inFolder: folder]; + + // This part is used to filter attachements when searching + // There is no way to filter only attachments when using imap SEARCH + if (onlyAttachments) { + tmpHeaders = [NSMutableArray arrayWithArray: headers]; + tmpUids = [NSMutableArray arrayWithArray: uids]; + + for (i = ([tmpHeaders count] - 1); i > 0; i--) { + // Search for no attachment + if (0 == [[[tmpHeaders objectAtIndex: i] objectAtIndex: 1] intValue]) { + uid = [[tmpHeaders objectAtIndex: i] objectAtIndex: 10]; // Uid + [tmpHeaders removeObjectAtIndex: i]; + + for (j = ([tmpUids count] - 1); j >= 0; j--) { + if ([uid isEqual: [tmpUids objectAtIndex: j]]) { + [tmpUids removeObjectAtIndex: j]; // -1 for header + } + } + } + } + headers = [tmpHeaders copy]; + uids = [tmpUids copy]; + } + [data setObject: headers forKey: @"headers"]; } @@ -779,6 +926,7 @@ else sortByThread = NO; } + if (uids != nil) [data setObject: uids forKey: @"uids"]; [data setObject: [NSNumber numberWithBool: sortByThread] forKey: @"threaded"]; @@ -846,8 +994,8 @@ */ - (id ) getUIDsAction { - BOOL noHeaders; - NSDictionary *data, *requestContent; + BOOL noHeaders, onlyAttachments; + NSDictionary *data, *requestContent, *filter; SOGoMailFolder *folder; WORequest *request; WOResponse *response; @@ -858,8 +1006,26 @@ folder = [self clientObject]; noHeaders = [[[requestContent objectForKey: @"sortingAttributes"] objectForKey: @"noHeaders"] boolValue]; + + if ([[folder nameInContainer] isEqualToString: @"folderOther_SP_Users"]) { + // When the folder is folderOther_SP_Users (shared main folder), return no mailboxes + return response = [self responseWithStatus: 200 + andJSONRepresentation: [NSDictionary dictionaryWithObject: [NSArray array] forKey:@"mailboxes"]]; + } + + onlyAttachments = NO; + if (requestContent + && [requestContent objectForKey: @"filters"] + && [[requestContent objectForKey: @"filters"] count] > 0) { + for (filter in [requestContent objectForKey: @"filters"]) { + if ([filter objectForKey: @"searchBy"] + && [[filter objectForKey: @"searchBy"] isEqualToString: @"attachment"]) + onlyAttachments = YES; + } + } data = [self getUIDsInFolder: folder - withHeaders: !noHeaders]; + withHeaders: !noHeaders + onlyAttachments: onlyAttachments]; if (data != nil) response = [self responseWithStatus: 200 andJSONRepresentation: data]; @@ -1060,13 +1226,14 @@ // UID [msg addObject: [message objectForKey: @"uid"]]; - [headers addObject: msg]; // isAnswered [msg addObject: [NSNumber numberWithBool: [self isMessageAnswered]]]; // isForwarded [msg addObject: [NSNumber numberWithBool: [self isMessageForwarded]]]; + + [headers addObject: msg]; [self setMessage: [msgsList nextObject]]; diff --git a/UI/Templates/MailerUI/UIxMailFolderTemplate.wox b/UI/Templates/MailerUI/UIxMailFolderTemplate.wox index e66155395..c66cd9e6b 100644 --- a/UI/Templates/MailerUI/UIxMailFolderTemplate.wox +++ b/UI/Templates/MailerUI/UIxMailFolderTemplate.wox @@ -3,68 +3,34 @@ xmlns="http://www.w3.org/1999/xhtml" xmlns:var="http://www.skyrix.com/od/binding" xmlns:label="OGo:label"> +
- - - -
-
- - - - - - - - - -
- - - settings - - - - - - - - - - - {{ app.search.match == 'AND' ? 'check' : null }} - - - - - - {{ app.search.match == 'OR' ? 'check' : null }} - - - - - - - {{ app.service.selectedFolder.$isLoading ? 'stop' : 'search' }} - + + + search + + ng-hide="mailbox.service.$virtualPath !== false || mailbox.mode.multiple">
+ + more_horiz + + @@ -418,4 +388,248 @@ md-colors="::{color: 'default-background-500'}">
+ diff --git a/UI/Templates/MailerUI/UIxMailMainFrame.wox b/UI/Templates/MailerUI/UIxMailMainFrame.wox index 9219cca94..2e9b0fade 100644 --- a/UI/Templates/MailerUI/UIxMailMainFrame.wox +++ b/UI/Templates/MailerUI/UIxMailMainFrame.wox @@ -8,7 +8,7 @@ xmlns:label="OGo:label" className="UIxPageFrame" title="moduleName" - const:jsFiles="vendor/ckeditor/build/ckeditor.js, Common/sgCkeditor.component.js, Common.js, Preferences.services.js, Contacts.services.js, Scheduler.services.js, Mailer.js, Mailer.services.js, vendor/angular-file-upload.min.js, vendor/FileSaver.min.js, vendor/punycode.js"> + const:jsFiles="vendor/ckeditor/build/ckeditor.js, Common/sgCkeditor.component.js, Common.js, Preferences.services.js, Contacts.services.js, Scheduler.services.js, Mailer.js, Mailer.services.js, vendor/angular-file-upload.min.js, vendor/FileSaver.min.js, vendor/punycode.js, vendor/mark.min.js">