feat(mail): Improve mail search (advanced search)

This commit is contained in:
smizrahi
2024-05-21 09:25:20 +02:00
parent 863280d6ac
commit 9eb8039698
25 changed files with 1063 additions and 206 deletions
@@ -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";
@@ -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";
+202 -35
View File
@@ -32,6 +32,7 @@
#import <Foundation/NSTimeZone.h>
#import <Foundation/NSUserDefaults.h> /* for locale string constants */
#import <Foundation/NSValue.h>
#import <Foundation/NSNumberFormatter.h>
#import <NGObjWeb/WOApplication.h>
#import <NGObjWeb/WOContext+SoObjects.h>
@@ -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 <WOActionResults>) 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]];
+271 -57
View File
@@ -3,68 +3,34 @@
xmlns="http://www.w3.org/1999/xhtml"
xmlns:var="http://www.skyrix.com/od/binding"
xmlns:label="OGo:label">
<div class="view-list hide-print" layout="column" ng-class="{'view-list--close': mailbox.centerIsClose(centerIsClose)}">
<!-- in virtual mailbox mode -->
<md-toolbar class="md-whiteframe-z1 md-hue-3"
ng-hide="mailbox.service.$virtualPath === false || mailbox.mode.multiple">
<div class="md-toolbar-tools"
layout="row" layout-align="start start">
<div class="pseudo-input-container pseudo-input-container--compact md-flex sg-no-wrap">
<label class="pseudo-input-label pseudo-input-label">
<var:string label:value="Search messages in"/>
</label>
<md-select class="pseudo-input-field" label:aria-label="Search messages in" ng-model="mailbox.service.$virtualPath">
<md-option ng-value="''">
<span ng-bind="app.accounts[0].name"><!-- main account name --></span>
</md-option>
<md-option ng-repeat="folder in
app.accounts[0].$flattenMailboxes()
track by folder.path"
ng-value="folder.path">
<span ng-class="'sg-child-level-' + folder.level"
ng-bind="folder.$displayName"><!-- mailbox name --></span>
</md-option>
</md-select>
</div>
<md-menu>
<md-button class="sg-icon-button" label:aria-label="More search options" ng-click="$mdMenu.open()">
<md-icon>settings</md-icon>
</md-button>
<md-menu-content>
<md-menu-item>
<sg-checkmark ng-model="app.search.subfolders"
sg-true-value="1"
sg-false-value="0">
<var:string label:value="Search subfolders"/>
</sg-checkmark>
</md-menu-item>
<md-menu-divider><!-- divider --></md-menu-divider>
<md-menu-item>
<md-button ng-click="app.search.match='AND'">
<md-icon label:aria-label="Match all of the following">{{ app.search.match == 'AND' ? 'check' : null }}</md-icon>
<var:string label:value="Match all of the following"/>
</md-button>
</md-menu-item>
<md-menu-item>
<md-button ng-click="app.search.match='OR'">
<md-icon label:aria-label="Match any of the following">{{ app.search.match == 'OR' ? 'check' : null }}</md-icon>
<var:string label:value="Match any of the following"/>
</md-button>
</md-menu-item>
</md-menu-content>
</md-menu>
<md-button class="sg-icon-button"
label:aria-label="Search"
ng-click="app.toggleAdvancedSearch()"
ng-disabled="app.search.params.length == 0">
<md-icon>{{ app.service.selectedFolder.$isLoading ? 'stop' : 'search' }}</md-icon>
</md-button>
<!-- in virtual mailbox mode -->
<md-toolbar class="md-whiteframe-z1 md-hue-2 _md _md-toolbar-transitions advanced-search-clickable-toolbar"
ng-hide="app.service.$virtualPath === false || mailbox.mode.multiple"
ng-click="app.showAdvancedSearch()">
<md-icon class="material-icons sg-icon-toolbar-bg">search</md-icon>
<div class="sg-toolbar-auto">
<form class="md-toolbar-tools sg-toolbar-secondary" layout="row">
<div ng-click="app.hideAdvancedSearch($event)">
<a class="sg-icon-button" ng-click="app.hideAdvancedSearch($event)" aria-label="Back">
<md-icon>arrow_back</md-icon>
</a>
</div>
<md-input-container class="md-flex" md-no-float="md-no-float">
<var:string label:value="Advanced search"/>
</md-input-container>
<a class="sg-icon-button show-advanced-search" ng-click="app.showAdvancedSearch()" aria-label="Advanced search">
<md-icon>more_horiz</md-icon>
<md-tooltip><var:string label:value="Advanced search"/></md-tooltip>
</a>
</form>
</div>
</md-toolbar>
<!-- single-selection toolbars -->
<md-toolbar class="md-accent md-hue-1"
ng-hide="mailbox.service.$virtualPath !== false || mailbox.mode.multiple">
ng-hide="mailbox.service.$virtualPath !== false || mailbox.mode.multiple">
<!-- sort mode (default) -->
<div class="md-toolbar-tools" ng-hide="mailbox.mode.search">
<md-button class="sg-icon-button" label:aria-label="Search"
@@ -184,6 +150,10 @@
<md-option value="body"><var:string label:value="Entire Message"/></md-option>
</md-select>
</md-input-container>
<a class="sg-icon-button show-advanced-search" ng-click="app.showAdvancedSearch()" aria-label="Advanced search">
<md-icon>more_horiz</md-icon>
<md-tooltip><var:string label:value="Advanced search"/></md-tooltip>
</a>
</form>
</md-toolbar>
@@ -418,4 +388,248 @@
md-colors="::{color: 'default-background-500'}"><var:string label:value="No message selected"/></div>
</md-content>
</div>
<script type="text/ng-template" id="advancedSearch">
<md-dialog flex="50" flex-sm="80" flex-xs="100">
<md-toolbar>
<div class="md-toolbar-tools">
<md-icon class="material-icons sg-icon-toolbar-bg">search</md-icon>
<div class="pseudo-input-container md-flex">
<var:string label:value="Advanced search"/>
</div>
<md-button class="md-icon-button" ng-click="dialogCtrl.closeDialog()">
<md-icon aria-label="Close dialog">close</md-icon>
</md-button>
</div>
</md-toolbar>
<md-dialog-content class="md-dialog-content">
<div class="md-toolbar-tools sg-toolbar-auto advanced-search">
<md-input-container class="advanced-search-full">
<label><var:string label:value="From"/></label>
<input type="text" var:value="dialogCtrl.mainController.searchForm.from" ng-model="dialogCtrl.mainController.searchForm.from" ng-keyup="dialogCtrl.mainController.searchFieldChange($event)" />
</md-input-container>
<md-input-container class="advanced-search-full">
<label><var:string label:value="To"/></label>
<input type="text" var:value="dialogCtrl.mainController.searchForm.to" ng-model="dialogCtrl.mainController.searchForm.to" ng-keyup="dialogCtrl.mainController.searchFieldChange($event)"/>
</md-input-container>
<md-input-container class="advanced-search-full">
<label><var:string label:value="Contains"/></label>
<input type="text" var:value="dialogCtrl.mainController.searchForm.contains" ng-model="dialogCtrl.mainController.searchForm.contains" ng-keyup="dialogCtrl.mainController.searchFieldChange($event)" />
</md-input-container>
<md-input-container class="advanced-search-full">
<label><var:string label:value="Does not contains"/></label>
<input type="text" var:value="dialogCtrl.mainController.searchForm.doesnotcontains" ng-model="dialogCtrl.mainController.searchForm.doesnotcontains" ng-keyup="dialogCtrl.mainController.searchFieldChange($event)"/>
</md-input-container>
<md-input-container class="advanced-search-full">
<label><var:string label:value="Subject"/></label>
<input type="text" var:value="dialogCtrl.mainController.searchForm.subject" ng-model="dialogCtrl.mainController.searchForm.subject" ng-keyup="dialogCtrl.mainController.searchFieldChange($event)"/>
</md-input-container>
<md-input-container class="advanced-search-full">
<label><var:string label:value="Body"/></label>
<input type="text" var:value="dialogCtrl.mainController.searchForm.body" ng-model="dialogCtrl.mainController.searchForm.body" ng-keyup="dialogCtrl.mainController.searchFieldChange($event)"/>
</md-input-container>
<div class="search-inline">
<md-select ng-model="dialogCtrl.mainController.searchForm.date" class="search-right-space">
<md-option value="anytime">
<var:string label:value="Anytime"/>
</md-option>
<md-option value="last7days">
<var:string label:value="Last 7 days"/>
</md-option>
<md-option value="last30days">
<var:string label:value="Last 30 days"/>
</md-option>
<md-option value="last6month">
<var:string label:value="Last 6 month"/>
</md-option>
<md-option value="before">
<var:string label:value="Before"/>
</md-option>
<md-option value="after">
<var:string label:value="After"/>
</md-option>
<md-option value="between">
<var:string label:value="Between"/>
</md-option>
</md-select>
<md-datepicker class="md-text search-right-space"
ng-model="dialogCtrl.mainController.searchForm.dateStart"
md-hide-icons="calendar"
ng-change="dialogCtrl.mainController.changeDate()"
ng-show="['before', 'after', 'between'].indexOf(dialogCtrl.mainController.searchForm.date) != -1">
</md-datepicker>
<div class="search-right-space search-and" ng-show="['between'].indexOf(dialogCtrl.mainController.searchForm.date) != -1">
<var:string label:value="and"/>
</div>
<md-datepicker class="md-text search-right-space "
ng-model="dialogCtrl.mainController.searchForm.dateEnd"
md-hide-icons="calendar"
ng-change="dialogCtrl.mainController.changeDate()"
ng-show="['between'].indexOf(dialogCtrl.mainController.searchForm.date) != -1">
</md-datepicker>
</div>
<md-input-container class="advanced-search-full">
<label><var:string label:value="Bcc"/></label>
<input type="text" var:value="dialogCtrl.mainController.searchForm.bcc" ng-model="dialogCtrl.mainController.searchForm.bcc" ng-keyup="dialogCtrl.mainController.searchFieldChange($event)"/>
</md-input-container>
<div class="search-inline">
<md-input-container class="md-text search-right-space">
<label><var:string label:value="Message size"/></label>
<input type="number" var:value="dialogCtrl.mainController.searchForm.size" ng-model="dialogCtrl.mainController.searchForm.size" ng-keyup="dialogCtrl.mainController.searchFieldChange($event)"/>
</md-input-container>
<md-select ng-model="dialogCtrl.mainController.searchForm.sizeOperator" class="search-right-space">
<md-option value="&gt;">
&gt;
</md-option>
<md-option value="&lt;">
&lt;
</md-option>
</md-select>
<md-select ng-model="dialogCtrl.mainController.searchForm.sizeUnit" class="search-right-space">
<md-option value="kb">
<var:string label:value="KiB"/>
</md-option>
<md-option value="mb">
<var:string label:value="MiB"/>
</md-option>
<md-option value="gb">
<var:string label:value="GiB"/>
</md-option>
</md-select>
</div>
<md-checkbox
ng-model="dialogCtrl.mainController.searchForm.attachements"
ng-true-value="1"
ng-false-value="0"
label:aria-label="With attachments">
<var:string label:value="With attachments"/>
</md-checkbox>
<md-checkbox
ng-model="dialogCtrl.mainController.searchForm.favorite"
ng-true-value="1"
ng-false-value="0"
label:aria-label="In favorites">
<var:string label:value="In favorites"/>
</md-checkbox>
<md-checkbox
ng-model="dialogCtrl.mainController.searchForm.unseen"
ng-true-value="1"
ng-false-value="0"
label:aria-label="Unseen only">
<var:string label:value="Unseen only"/>
</md-checkbox>
<md-chips class="sg-readonly advanced-search-full"
sg-focus-on="flags"
ng-model="dialogCtrl.mainController.searchForm.flags"
md-transform-chip="$chip.name">
<md-chip-template>
<span class="sg-chip-color">
<span ng-style="{ 'background-color': dialogCtrl.message.$tags[$chip][1] }"><!-- color --></span>
</span>
<span ng-bind="dialogCtrl.message.$tags[$chip][0] || $chip"><!-- tag --></span>
</md-chip-template>
<md-autocomplete
md-search-text="dialogCtrl.mainController.searchForm.tags.searchText"
md-items="tag in dialogCtrl.message.filterTags(dialogCtrl.mainController.searchForm.tags.searchText, dialogCtrl.mainController.message.flags)"
md-no-cache="true"
label:placeholder="Add a tag">
<md-item-template>
<div layout="row" layout-align="start center">
<div class="sg-color-chip"
ng-style="{ 'background-color': tag.color }"><!-- color --></div>
<div md-highlight-text="dialogCtrl.mainController.searchForm.tags.searchText"
md-highlight-flags="^i">{{ tag.description }}</div>
</div>
</md-item-template>
</md-autocomplete>
</md-chips>
</div>
<div class="search-inline advanced-search-full advanced-search-in">
<div class="pseudo-input-container pseudo-input-container--compact md-flex sg-no-wrap">
<label class="pseudo-input-label pseudo-input-label">
<var:string label:value="Search messages in"/>
</label>
<md-select class="pseudo-input-field" label:aria-label="Search messages in" ng-model="dialogCtrl.mailbox.$virtualPath">
<md-option ng-value="''">
<span ng-bind="dialogCtrl.mainController.accounts[0].name"><!-- main account name --></span>
</md-option>
<md-option ng-repeat="folder in
dialogCtrl.mainController.accounts[0].$flattenMailboxes()
track by folder.path"
ng-value="folder.path">
<span ng-class="'sg-child-level-' + folder.level"
ng-bind="folder.$displayName"><!-- mailbox name --></span>
</md-option>
</md-select>
</div>
<md-menu>
<md-button class="sg-icon-button" label:aria-label="More search options" ng-click="$mdMenu.open()">
<md-icon>settings</md-icon>
</md-button>
<md-menu-content>
<md-menu-item>
<sg-checkmark ng-model="dialogCtrl.mainController.search.subfolders"
sg-true-value="1"
sg-false-value="0">
<var:string label:value="Search subfolders"/>
</sg-checkmark>
</md-menu-item>
<md-menu-divider><!-- divider --></md-menu-divider>
<md-menu-item>
<md-button ng-click="dialogCtrl.mainController.search.match='AND'">
<md-icon label:aria-label="Match all of the following">{{ dialogCtrl.mainController.search.match == 'AND' ? 'check' : null }}</md-icon>
<var:string label:value="Match all of the following"/>
</md-button>
</md-menu-item>
<md-menu-item>
<md-button ng-click="dialogCtrl.mainController.search.match='OR'">
<md-icon label:aria-label="Match any of the following">{{ dialogCtrl.mainController.search.match == 'OR' ? 'check' : null }}</md-icon>
<var:string label:value="Match any of the following"/>
</md-button>
</md-menu-item>
</md-menu-content>
</md-menu>
</div>
</md-dialog-content>
<md-dialog-actions>
<md-button
class="md-warn"
label:aria-label="Cancel"
ng-click="dialogCtrl.closeDialog()">
<var:string label:value="Cancel"/>
</md-button>
<md-button
label:aria-label="Reset"
ng-click="dialogCtrl.mainController.reset()">
<var:string label:value="Reset"/>
</md-button>
<div class="md-flex"><!-- spacer --></div>
<md-button
label:aria-label="Search"
ng-click="dialogCtrl.search()">
<var:string label:value="Search"/>
</md-button>
</md-dialog-actions>
</md-dialog>
</script>
</container>
+3 -52
View File
@@ -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">
<script type="text/javascript">
var mailAccounts = <var:string value="mailAccounts" const:escapeHTML="NO" />;
var userNames = <var:string value="userNames" const:escapeHTML="NO" />;
@@ -122,60 +122,11 @@
</script>
<script type="text/ng-template" id="UIxMailFolderTemplate">
<md-toolbar layout="row" layout-align="space-between center" class="toolbar-main"
ng-hide="mailbox.service.$virtualPath !== false">
<md-toolbar layout="row" layout-align="space-between center" class="toolbar-main">
<var:component className="UIxTopnavToolbar"/>
</md-toolbar>
<!-- Advanced search toolbar -->
<md-toolbar layout="column" class="toolbar-main md-hue-3"
ng-show="mailbox.service.$virtualPath !== false">
<div class="md-toolbar-tools">
<div layout="column" class="md-flex">
<div class="pseudo-input-container pseudo-input-container--compact">
<label class="pseudo-input-label"><var:string label:value="Add a Criteria"/></label>
</div>
<div layout="row">
<span class="md-button sg-outline-button" ng-click="app.addSearchParam('subject')">
<var:string label:value="Subject"/>
</span>
<span class="md-button sg-outline-button" ng-click="app.addSearchParam('from')">
<var:string label:value="From"/>
</span>
<span class="md-button sg-outline-button" ng-click="app.addSearchParam('to')">
<var:string label:value="To"/>
</span>
<span class="md-button sg-outline-button" ng-click="app.addSearchParam('cc')">
<var:string label:value="Cc"/>
</span>
<span class="md-button sg-outline-button" ng-click="app.addSearchParam('body')">
<var:string label:value="Body"/>
</span>
</div>
</div>
<md-button type="button" class="sg-icon-button" ng-click="app.hideAdvancedSearch()">
<md-icon>close</md-icon>
</md-button>
</div>
<div class="md-toolbar-tools sg-toolbar-auto">
<md-chips class="md-flex"
ng-show="app.search.params.length != 0 || app.currentSearchParam"
ng-model="app.search.params"
md-transform-chip="app.newSearchParam($chip)"
md-add-on-blur="true">
<input type="text" sg-focus-on="advancedSearch"
ng-disabled="app.currentSearchParam.length == 0"
sg-placeholder="app.search.options[app.currentSearchParam]"/>
<md-chip-template>
<span class="md-caption" ng-show="$chip.negative == 0"><var:string label:value="match"/></span>
<span class="md-caption" ng-show="$chip.negative == 1"><var:string label:value="does not match"/></span>
<span class="md-caption" ng-bind="$chip.searchBy.capitalize() | loc"><!-- search by --></span>
<span ng-bind="$chip.searchInput"><!-- search input --></span>
</md-chip-template>
</md-chips>
</div>
</md-toolbar>
<div layout="row" class="md-flex sg-block-print">
<var:component className="UIxMailFolderTemplate" />
</div>
@@ -141,7 +141,7 @@
</md-card-actions>
<md-card-content>
<div class="sg-padded">
<h5 class="sg-md-headline" ng-bind="viewer.message.subject"><!-- subject --></h5>
<h5 class="sg-md-headline" ng-bind-html="viewer.message.subject"><!-- subject --></h5>
<time class="msg-date" datetime="viewer.message.date" ng-bind="::viewer.message.date"><!-- date --></time>
</div>
<div layout="row" layout-wrap="layout-wrap">
@@ -151,7 +151,7 @@
sg-email="::viewer.message.from[0].email"
size="40">person</sg-avatar-image>
<div>
<span ng-bind="::viewer.message.from[0].name"><!-- from --></span>
<span ng-bind-html="::viewer.message.from[0].name"><!-- from --></span>
<a href="#" class="md-caption"
ng-bind="::viewer.message.from[0].email"
ng-click="viewer.selectRecipient(viewer.message.from[0], $event)"><!-- from --></a>
+1 -1
View File
@@ -4,7 +4,7 @@ include ../common.make
WEBSERVER_RESOURCE_DIRS = css fonts img js
JS_FILES = js/Administration.* js/Common.* js/Contacts.* js/Mailer.* js/Main.* js/Preferences.* js/Scheduler.*
JS_LIB_FILES = js/vendor/angular-animate.* js/vendor/angular-aria.* js/vendor/angular-cookies.* js/vendor/angular-messages.* js/vendor/angular-file-upload.* js/vendor/FileSaver.* js/vendor/ng-sortable.* js/vendor/angular-material.* js/vendor/angular-sanitize.* js/vendor/angular-ui-router.* js/vendor/angular.* js/vendor/lodash.* js/vendor/qrcode.* js/vendor/punycode.js
JS_LIB_FILES = js/vendor/angular-animate.* js/vendor/angular-aria.* js/vendor/angular-cookies.* js/vendor/angular-messages.* js/vendor/angular-file-upload.* js/vendor/FileSaver.* js/vendor/ng-sortable.* js/vendor/angular-material.* js/vendor/angular-sanitize.* js/vendor/angular-ui-router.* js/vendor/angular.* js/vendor/lodash.* js/vendor/qrcode.* js/vendor/punycode.js js/vendor/mark.min.js
CSS_FILES = css/styles.css css/styles.css.map css/no-animation.css css/no-animation.css.map
.DEFAULT_GOAL := all
+2 -1
View File
@@ -141,7 +141,8 @@ module.exports = function(grunt) {
'<%= src %>/ng-sortable/dist/ng-sortable.min.js{,map}',
'<%= src %>/lodash/lodash{,.min}.js',
'<%= src %>/qrcodejs/qrcode{,.min}.js',
'<%= src %>/punycode/punycode.js'
'<%= src %>/punycode/punycode.js',
'<%= src %>/mark.js/dist/mark.min.js'
];
for (var j = 0; j < js.length; j++) {
var files = grunt.file.expand(grunt.template.process(js[j], {data: options}));
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,2 +1,2 @@
!function(){"use strict";function e(e,t){if(e.state("mail",{url:"/Mail",views:{mailboxes:{templateUrl:"UIxMailMainFrame",controller:"MailboxesController",controllerAs:"app"}},resolve:{stateAccounts:a}}).state("mail.account",{url:"/:accountId",abstract:!0,views:{mailbox:{template:"<ui-view/>"}},resolve:{stateAccount:o}}).state("mail.account.virtualMailbox",{url:"/virtual",views:{"mailbox@mail":{templateUrl:"UIxMailFolderTemplate",controller:"MailboxController",controllerAs:"mailbox"}},resolve:{stateMailbox:l}}).state("mail.account.virtualMailbox.message",{url:"/:mailboxId/:messageId",views:{message:{templateUrl:"UIxMailViewTemplate",controller:"MessageController",controllerAs:"viewer"}},resolve:{stateMailbox:n,stateMessages:i,stateMessage:c},onEnter:u,onExit:d}).state("mail.account.inbox",{url:"/inbox",onEnter:r}).state("mail.account.mailbox",{url:"/:mailboxId",views:{"mailbox@mail":{templateUrl:"UIxMailFolderTemplate",controller:"MailboxController",controllerAs:"mailbox"}},resolve:{stateMailbox:s,stateMessages:i}}).state("mail.account.mailbox.message",{url:"/:messageId",views:{message:{templateUrl:"UIxMailViewTemplate",controller:"MessageController",controllerAs:"viewer"}},params:{reload:{value:!1}},onEnter:u,onExit:d,resolve:{stateMessage:c}}),t.rules.otherwise("/Mail/0/inbox"),navigator&&navigator.registerProtocolHandler){e=window.location.origin+window.ApplicationBaseURL+"UIxMailPopupView#!/Mail/0/new?%s";try{navigator.registerProtocolHandler("mailto",e,"SOGo")}catch(e){}}}function a(e,t,a){var a=a.$findAll(e.mailAccounts),o=[];return angular.forEach(a,function(t,e){var a=t.$getMailboxes();o.push(0===e?a.then(function(e){return t}):t)}),t.all(o)}function o(t,e){return _.find(e,function(e){return e.id==t.accountId})}function s(e,t,a,o,r){var i,l=o(o(t.mailboxId)),n=function(e){var t=_.find(e,function(e){return e.path==l});return t||angular.forEach(e,function(e){!t&&e.children&&0<e.children.length&&(t=n(e.children))}),t};return r.selectedFolder&&!r.$virtualMode&&(r.selectedFolder.$isLoading=!0),a.$getMailboxes().then(function(e){if(i=n(e))return i.$topIndex=0,i.selectFolder(),i;$state.go("mail.account",{accountId:s.$account.id})})}function r(t,a,o){return t.injector().getAsync("stateAccount").then(function(e){return 0<e.$mailboxes.length?t.router.stateService.target("mail.account.mailbox",{accountId:e.id,mailboxId:a(e.$mailboxes[0].path)}):(o.selectedFolder=!1,t.router.stateService.target("mail"))})}function i(e,t,a,o){return a.$virtualMode?[]:o?(o.$unselectMessages(),o.$filter().catch(function(){return e.reject("Mailbox not found")})):e.reject("Mailbox doesn't exist")}function l(e,t){return t.$virtualMode?t.selectedFolder:e.reject("No virtual mailbox defined")}function n(e,t,a,o){var r=a(a(o.mailboxId));return t.$virtualMode?(t.selectedFolder.resetSelectedMessage(),_.find(t.selectedFolder.$mailboxes,function(e){return e.path==r})):e.reject("No virtual mailbox defined for message")}function c(e,t,a,o,r,i){var l=_.find(r.$messages,function(e){return e.uid==parseInt(a.messageId)});if(l&&l.$reload)return l.$reload({useCache:!a.reload,nocache:a.reload});o.go("mail.account.mailbox",{accountId:r.$account.id,mailboxId:t(r.path)})}function u(e,t){t.$selectedMessage=parseInt(e.messageId)}function d(e){delete e.$selectedMessage}function t(e,t,a,o,r){e.DebugEnabled||o.defaultErrorHandler(function(){}),t.onError({to:"mail.**"},function(e){"mail"!=e.to().name&&!e.ignored()&&e.error().message.indexOf("superseded")<0&&(a.error("transition error to "+e.to().name+": "+e.error().detail),r.selectedFolder=!1,o.go("mail"))})}angular.module("SOGo.MailerUI",["ngCookies","ui.router","sgCkeditor","angularFileUpload","SOGo.Common","SOGo.ContactsUI","SOGo.SchedulerUI","ngAnimate","SOGo.PreferencesUI"]).config(e).run(t),e.$inject=["$stateProvider","$urlServiceProvider"],a.$inject=["$window","$q","Account"],o.$inject=["$stateParams","stateAccounts"],s.$inject=["$q","$stateParams","stateAccount","decodeUriFilter","Mailbox"],r.$inject=["$transition$","encodeUriFilter","Mailbox"],i.$inject=["$q","$state","Mailbox","stateMailbox"],l.$inject=["$q","Mailbox"],n.$inject=["$q","Mailbox","decodeUriFilter","$stateParams"],c.$inject=["Mailbox","encodeUriFilter","$stateParams","$state","stateMailbox","stateMessages"],u.$inject=["$stateParams","stateMailbox"],d.$inject=["stateMailbox"],t.$inject=["$window","$transitions","$log","$state","Mailbox"]}();
!function(){"use strict";function e(e,t){if(e.state("mail",{url:"/Mail",views:{mailboxes:{templateUrl:"UIxMailMainFrame",controller:"MailboxesController",controllerAs:"app"}},resolve:{stateAccounts:a}}).state("mail.account",{url:"/:accountId",abstract:!0,views:{mailbox:{template:"<ui-view/>"}},resolve:{stateAccount:o}}).state("mail.account.virtualMailbox",{url:"/virtual",views:{"mailbox@mail":{templateUrl:"UIxMailFolderTemplate",controller:"MailboxController",controllerAs:"mailbox"}},resolve:{stateMailbox:l}}).state("mail.account.virtualMailbox.message",{url:"/:mailboxId/:messageId",views:{message:{templateUrl:"UIxMailViewTemplate",controller:"MessageController",controllerAs:"viewer"}},resolve:{stateMailbox:n,stateMessages:i,stateMessage:c},onEnter:u,onExit:d}).state("mail.account.inbox",{url:"/inbox",onEnter:r}).state("mail.account.mailbox",{url:"/:mailboxId",views:{"mailbox@mail":{templateUrl:"UIxMailFolderTemplate",controller:"MailboxController",controllerAs:"mailbox"}},resolve:{stateMailbox:s,stateMessages:i}}).state("mail.account.mailbox.message",{url:"/:messageId",views:{message:{templateUrl:"UIxMailViewTemplate",controller:"MessageController",controllerAs:"viewer"}},params:{reload:{value:!1}},onEnter:u,onExit:d,resolve:{stateMessage:c}}),t.rules.otherwise("/Mail/0/inbox"),navigator&&navigator.registerProtocolHandler){e=window.location.origin+window.ApplicationBaseURL+"UIxMailPopupView#!/Mail/0/new?%s";try{navigator.registerProtocolHandler("mailto",e,"SOGo")}catch(e){}}}function a(e,t,a){var a=a.$findAll(e.mailAccounts),o=[];return angular.forEach(a,function(t,e){var a=t.$getMailboxes();o.push(0===e?a.then(function(e){return t}):t)}),t.all(o)}function o(t,e){return _.find(e,function(e){return e.id==t.accountId})}function s(e,t,a,o,r){var i,l=o(o(t.mailboxId)),n=function(e){var t=_.find(e,function(e){return e.path==l});return t||angular.forEach(e,function(e){!t&&e.children&&0<e.children.length&&(t=n(e.children))}),t};return r.selectedFolder&&!r.$virtualMode&&(r.selectedFolder.$isLoading=!0),a.$getMailboxes().then(function(e){if(i=n(e))return i.$topIndex=0,i.selectFolder(),i;$state.go("mail.account",{accountId:s.$account.id})})}function r(t,a,o){return t.injector().getAsync("stateAccount").then(function(e){return 0<e.$mailboxes.length?t.router.stateService.target("mail.account.mailbox",{accountId:e.id,mailboxId:a(e.$mailboxes[0].path)}):(o.selectedFolder=!1,t.router.stateService.target("mail"))})}function i(e,t,a,o){return a.$virtualMode?[]:o?(o.$unselectMessages(),o.$filter().catch(function(){return e.reject("Mailbox not found")})):e.reject("Mailbox doesn't exist")}function l(e,t){return t.$virtualMode?t.selectedFolder:e.reject("No virtual mailbox defined")}function n(e,t,a,o){var r=a(a(o.mailboxId));return t.$virtualMode?(t.selectedFolder.resetSelectedMessage(),_.find(t.selectedFolder.$mailboxes,function(e){return e.path==r})):e.reject("No virtual mailbox defined for message")}function c(e,t,a,o,r,i){var l=_.find(r.$messages,function(e){return e.uid==parseInt(a.messageId)});if(l&&l.$reload)return l.$reload({useCache:!a.reload,nocache:a.reload});o.go("mail.account.mailbox",{accountId:r.$account.id,mailboxId:t(r.path)})}function u(e,t){t.$selectedMessage=parseInt(e.messageId)}function d(e){delete e.$selectedMessage}function t(e,t,a,o,r){e.DebugEnabled||o.defaultErrorHandler(function(){}),t.onError({to:"mail.**"},function(e){("mail"!=e.to().name&&!e.ignored()&&e.error().message.indexOf("superseded")<0||"mail.account.virtualMailbox"===e.to().name||"mail.account.virtualMailbox.message"===e.to().name)&&(a.error("transition error to "+e.to().name+": "+e.error().detail),r.selectedFolder=!1,o.go("mail"))})}angular.module("SOGo.MailerUI",["ngCookies","ui.router","sgCkeditor","angularFileUpload","SOGo.Common","SOGo.ContactsUI","SOGo.SchedulerUI","ngAnimate","SOGo.PreferencesUI"]).config(e).run(t),e.$inject=["$stateProvider","$urlServiceProvider"],a.$inject=["$window","$q","Account"],o.$inject=["$stateParams","stateAccounts"],s.$inject=["$q","$stateParams","stateAccount","decodeUriFilter","Mailbox"],r.$inject=["$transition$","encodeUriFilter","Mailbox"],i.$inject=["$q","$state","Mailbox","stateMailbox"],l.$inject=["$q","Mailbox"],n.$inject=["$q","Mailbox","decodeUriFilter","$stateParams"],c.$inject=["Mailbox","encodeUriFilter","$stateParams","$state","stateMailbox","stateMessages"],u.$inject=["$stateParams","stateMailbox"],d.$inject=["stateMailbox"],t.$inject=["$window","$transitions","$log","$state","Mailbox"]}();
//# sourceMappingURL=Mailer.js.map
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -45,6 +45,7 @@
$refreshTimeout: null,
$virtualMode: false,
$virtualPath: false,
$searchMode: false,
PRELOAD: PRELOAD,
BATCH_DELETE_LIMIT: BATCH_DELETE_LIMIT
});
@@ -169,6 +170,9 @@
this.$visibleMessages = this.$messages;
this.$selectedMessages = [];
}
if (angular.isUndefined(this.$highlightWords)) {
this.$highlightWords = [];
}
angular.extend(this, data);
if (this.path) {
this.id = this.$id();
@@ -242,6 +246,16 @@
Mailbox.selectedFolder = this;
};
/**
* @function setSearchMode
* @memberof Mailbox.prototype
* @desc Set search mode for controller
* @param {array} searchMode - a boolean
*/
Mailbox.prototype.setSearchMode = function (searchMode) {
Mailbox.$searchMode = searchMode;
};
/**
* @function getLength
* @memberof Mailbox.prototype
@@ -385,6 +399,27 @@
// Sorting preferences are common to all mailboxes
angular.extend(Mailbox.$query, sortingAttributes);
if (filters && filters.length > 0) {
// Remove highlight words
this.$highlightWords = [];
filters.forEach(filter => {
if ("subject_or_from" == filter.searchBy
|| "contains" == filter.searchBy
|| "body" == filter.searchBy
|| "from" == filter.searchBy
|| "to" == filter.searchBy
|| "subject" == filter.searchBy) {
var words = filter.searchInput.split(" ");
words.forEach(word => {
var cleanedWord = word.trim().toLowerCase();
if (!this.$highlightWords.includes(cleanedWord)) {
this.$highlightWords.push(cleanedWord);
}
});
}
});
}
angular.extend(options, { sortingAttributes: Mailbox.$query });
if (angular.isDefined(filters)) {
options.filters = _.reject(angular.copy(filters), function(filter) {
@@ -1249,4 +1284,23 @@
Mailbox.$$resource.post(this.id, action);
};
/**
* @function setHighlightWords
* @memberof Mailbox.prototype
* @desc Set highlight words when searching
* @param {array} highlightWords - a list of words
*/
Mailbox.prototype.setHighlightWords = function (highlightWords) {
this.$highlightWords = highlightWords;
};
/**
* @function getHighlightWords
* @memberof Mailbox.prototype
* @desc Get highlight words when searching
* @returns a list of words
*/
Mailbox.prototype.getHighlightWords = function () {
return this.$highlightWords;
};
})();
@@ -50,10 +50,10 @@
_.forEach(hotkeys, function(key) {
sgHotkeys.deregisterHotkey(key);
});
if (vm.mode.search) {
vm.mode.search = false;
vm.selectedFolder.$reset({ filter: true });
}
// if (vm.mode.search) {
// vm.mode.search = false;
// vm.selectedFolder.$reset({ filter: true });
// }
});
// Update window's title with unseen messages count of selected mailbox
@@ -175,6 +175,12 @@
};
this.cancelSearch = function() {
// Clean highlights
if (vm.account) {
vm.account.$getMailboxes().$$state.value.forEach((mailbox) => {
mailbox.setHighlightWords([]);
});
}
vm.mode.search = false;
vm.selectedFolder.$filter(vm.service.$query).then(function() {
if (vm.selectedFolder.$selectedMessage) {
@@ -2,35 +2,36 @@
(function() {
'use strict';
/**
* @ngInject
*/
MailboxesController.$inject = ['$scope', '$state', '$transitions', '$timeout', '$window', '$mdUtil', '$mdMedia', '$mdSidenav', '$mdDialog', '$mdToast', 'sgConstant', 'sgFocus', 'encodeUriFilter', 'Dialog', 'sgSettings', 'sgHotkeys', 'Account', 'Mailbox', 'VirtualMailbox', 'User', 'Preferences', 'stateAccounts'];
function MailboxesController($scope, $state, $transitions, $timeout, $window, $mdUtil, $mdMedia, $mdSidenav, $mdDialog, $mdToast, sgConstant, focus, encodeUriFilter, Dialog, Settings, sgHotkeys, Account, Mailbox, VirtualMailbox, User, Preferences, stateAccounts) {
MailboxesController.$inject = ['$scope', '$rootScope', '$state', '$transitions', '$timeout', '$window', '$mdUtil', '$mdMedia', '$mdSidenav', '$mdDialog', '$mdToast', 'sgConstant', 'sgFocus', 'encodeUriFilter', 'Dialog', 'sgSettings', 'sgHotkeys', 'Account', 'Mailbox', 'VirtualMailbox', 'User', 'Preferences', 'stateAccounts', 'Message'];
function MailboxesController($scope, $rootScope, $state, $transitions, $timeout, $window, $mdUtil, $mdMedia, $mdSidenav, $mdDialog, $mdToast, sgConstant, focus, encodeUriFilter, Dialog, Settings, sgHotkeys, Account, Mailbox, VirtualMailbox, User, Preferences, stateAccounts, Message) {
var vm = this,
account,
mailbox,
hotkeys = [];
$scope.closeDialog = function () {
$mdDialog.hide();
};
this.$onInit = function () {
this.service = Mailbox;
this.accounts = stateAccounts;
this.message = Message;
this.advancedSearchPanelVisible = false;
// Advanced search options
this.currentSearchParam = '';
this.reset();
this.search = {
options: {'': '', // no placeholder when no criteria is active
subject: l('Enter Subject'),
from: l('Enter From'),
to: l('Enter To'),
cc: l('Enter Cc'),
body: l('Enter Body')
},
subfolders: 1,
match: 'AND',
params: []
};
this.highlightWords = [];
this.showSubscribedOnly = Preferences.defaults.SOGoMailShowSubscribedFoldersOnly;
@@ -44,6 +45,16 @@
sgHotkeys.deregisterHotkey(key);
});
});
$rootScope.$on('showMailAdvancedSearchPanel', function () {
vm.showAdvancedSearch();
});
$rootScope.$on('resetMailAdvancedSearchPanel', function () {
vm.service.$virtualPath = false;
vm.service.$virtualMode = false;
vm.reset();
});
};
@@ -61,6 +72,13 @@
Mailbox.selectedFolderController.confirmDelete(Mailbox.selectedFolder);
}
}));
keys.push(sgHotkeys.createHotkey({
key: 'shift+s',
description: l('Advanced search'),
callback: function () {
vm.showAdvancedSearch();
}
}));
});
// Register the hotkeys
@@ -68,14 +86,168 @@
sgHotkeys.registerHotkey(key);
});
}
this.hideAdvancedSearch = function() {
this.hideAdvancedSearch = function(e) {
vm.service.$virtualPath = false;
vm.service.$virtualMode = false;
account = vm.accounts[0];
mailbox = vm.searchPreviousMailbox;
$state.go('mail.account.mailbox', { accountId: account.id, mailboxId: encodeUriFilter(mailbox.path) });
vm.search.params = [];
vm.highlightWords = [];
if (mailbox && mailbox.path) {
// Reset
mailbox.setHighlightWords([]);
mailbox.$filter({
"sort": "date",
"asc": false,
"match": "OR"
}).then(function () {
$state.go('mail.account.mailbox', { accountId: account.id, mailboxId: encodeUriFilter(mailbox.path) });
vm.$onInit(); // Reinit search fields
});
}
e.stopPropagation();
};
this.addHighlightWords = function(sentence) {
var words = sentence.split(" ");
words.forEach(word => {
var cleanedWord = word.trim().toLowerCase();
if (!this.highlightWords.includes(cleanedWord)) {
this.highlightWords.push(cleanedWord);
}
});
};
this.reset = function() {
this.highlightWords = [];
this.searchForm = {
from: '',
to: '',
contains: '',
notContains: '',
subject: '',
body: '',
date: 'anytime',
dateStart: new Date(),
dateEnd: new Date(),
bcc: '',
size: '',
sizeOperator: '>',
sizeUnit: 'mb',
attachements: 0,
favorite: 0,
unseen: 0,
tags: { searchText: '', selected: '' },
flags: [],
};
}
this.addSearchParameters = function() {
this.search.params = [];
this.highlightWords = [];
// From
if (this.searchForm.from && this.searchForm.from.length > 0) {
this.search.params.push(this.newSearchParam('from', this.searchForm.from));
this.addHighlightWords(this.searchForm.from);
}
// To
if (this.searchForm.to && this.searchForm.to.length > 0) {
this.search.params.push(this.newSearchParam('to', this.searchForm.to));
}
// Bcc
if (this.searchForm.bcc && this.searchForm.bcc.length > 0) {
this.search.params.push(this.newSearchParam('bcc', this.searchForm.bcc));
}
// Contains
if (this.searchForm.contains && this.searchForm.contains.length > 0) {
this.search.params.push(this.newSearchParam('contains', this.searchForm.contains));
this.addHighlightWords(this.searchForm.contains);
}
// Does not contains
if (this.searchForm.doesnotcontains && this.searchForm.doesnotcontains.length > 0) {
this.search.params.push(this.newSearchParam('not_contains', this.searchForm.doesnotcontains));
}
// Subject
if (this.searchForm.subject && this.searchForm.subject.length > 0) {
this.search.params.push(this.newSearchParam('subject', this.searchForm.subject));
this.addHighlightWords(this.searchForm.subject);
}
// Body
if (this.searchForm.body && this.searchForm.body.length > 0) {
this.search.params.push(this.newSearchParam('body', this.searchForm.body));
this.addHighlightWords(this.searchForm.body);
}
// Date
if (this.searchForm.date && this.searchForm.date.length > 0) {
var date = null;
var dateTo = null;
var today = new Date();
var tmp = new Date(today);
switch (this.searchForm.date) {
case 'anytime':
break;
case 'last7days':
tmp.setDate(tmp.getDate() - 7);
date = this.formatDate(tmp);
this.search.params.push(this.newSearchParam('date', date, '>='));
break;
case 'last30days':
tmp.setDate(tmp.getDate() - 30);
date = this.formatDate(tmp);
this.search.params.push(this.newSearchParam('date', date, '>='));
break;
case 'last6month':
tmp.setMonth(tmp.getMonth() - 6);
date = this.formatDate(tmp);
this.search.params.push(this.newSearchParam('date', date, '>='));
break;
case 'before':
date = this.formatDate(this.searchForm.dateStart);
this.search.params.push(this.newSearchParam('date', date, '<'));
break;
case 'after':
date = this.formatDate(this.searchForm.dateStart);
this.search.params.push(this.newSearchParam('date', date, '>='));
break;
case 'between':
date = this.formatDate(this.searchForm.dateStart);
dateTo = this.formatDate(this.searchForm.dateEnd);
this.search.params.push(this.newSearchDateBetweenParam(date, dateTo));
break;
}
}
// Size
if (this.searchForm.size && this.searchForm.size > 0) {
this.search.params.push(this.newSearchParam('size', this.searchForm.size.toString(), this.searchForm.sizeOperator));
}
// Attachment
if (this.searchForm.attachements) {
this.search.params.push(this.newSearchParam('attachment', '1', '='));
}
// Favorite
if (this.searchForm.favorite) {
this.search.params.push(this.newSearchParam('favorite', '1', '='));
}
// Unseen
if (this.searchForm.unseen) {
this.search.params.push(this.newSearchParam('unseen', '1', '='));
}
// Flags
if (this.searchForm.flags && this.searchForm.flags.length > 0) {
this.search.params.push(this.newSearchFlagsParam());
}
this.toggleAdvancedSearch();
}
this.searchFieldChange = function (event) {
if (13 == event.keyCode) {
this.addSearchParameters();
$mdDialog.hide();
vm.advancedSearchPanelVisible = false;
}
};
this.toggleAdvancedSearch = function() {
@@ -109,6 +281,7 @@
if (Mailbox.$virtualPath.length) {
root = vm.accounts[0].$getMailboxByPath(Mailbox.$virtualPath);
root.setHighlightWords(vm.highlightWords);
mailboxes.push(root);
if (vm.search.subfolders && root.children.length)
_visit(root.children);
@@ -119,6 +292,9 @@
});
}
mailboxes.forEach((mailbox) => {
mailbox
});
vm.virtualMailbox.setMailboxes(mailboxes);
vm.virtualMailbox.startSearch(vm.search.match, vm.search.params);
if ($state.$current.name != 'mail.account.virtualMailbox')
@@ -126,24 +302,49 @@
}
};
this.addSearchParam = function(v) {
this.currentSearchParam = v;
focus('advancedSearch');
return false;
this.formatDate = function(date) {
var year = date.getFullYear();
var month = (date.getMonth() + 1).toString().padStart(2, '0');
var day = date.getDate().toString().padStart(2, '0');
return year + '-' + month + '-' + day;
};
this.newSearchParam = function(pattern) {
if (pattern.length && this.currentSearchParam.length) {
var n = 0, searchParam = this.currentSearchParam;
this.changeDate = function() {
if ('between' == this.searchForm.date) {
if (this.searchForm.dateStart > this.searchForm.dateEnd) {
this.searchForm.dateEnd = this.searchForm.dateStart;
}
}
};
this.newSearchParam = function (searchParam, pattern, operator = '>') {
if (pattern.length && searchParam.length) {
var n = 0;
if (pattern.startsWith("!")) {
n = 1;
pattern = pattern.substring(1).trim();
}
this.currentSearchParam = '';
return { searchBy: searchParam, searchInput: pattern, negative: n };
switch (searchParam) {
case 'size':
return { searchBy: searchParam, searchInput: pattern, negative: n, operator: operator, sizeUnit: this.searchForm.sizeUnit };
case 'date':
return { searchBy: searchParam, searchInput: pattern, negative: n, operator: operator };
default:
return { searchBy: searchParam, searchInput: pattern, negative: n };
}
}
};
this.newSearchDateBetweenParam = function (dateFrom, dateTo) {
return { searchBy: 'date_between', searchInput: "*", dateFrom: dateFrom, dateTo: dateTo, negative: 0 };
};
this.newSearchFlagsParam = function () {
return { searchBy: 'flags', searchInput: "*", flags: vm.searchForm.flags, negative: 0 };
};
this.toggleAccountState = function (account) {
account.$expanded = !account.$expanded;
if (!this.debounceSaveState) {
@@ -192,12 +393,46 @@
}
}
};
this.showAdvancedSearch = function() {
Mailbox.$virtualPath = '';
// Close sidenav on small devices
if (!$mdMedia(sgConstant['gt-md']))
$mdSidenav('left').close();
if (!vm.advancedSearchPanelVisible) {
vm.advancedSearchPanelVisible = true;
if (Mailbox.selectedFolder.path)
Mailbox.$virtualPath = Mailbox.selectedFolder.path;
// Close sidenav on small devices
if (!$mdMedia(sgConstant['gt-md']))
$mdSidenav('left').close();
$mdDialog.show({
template: document.getElementById('advancedSearch').innerHTML,
parent: angular.element(document.body),
controller: function () {
var dialogCtrl = this;
this.$onInit = function () {
// Pass main controller
this.mainController = vm;
this.mailbox = Mailbox;
this.message = Message;
};
dialogCtrl.closeDialog = function () {
$mdDialog.hide();
vm.advancedSearchPanelVisible = false;
};
dialogCtrl.search = function () {
this.mainController.addSearchParameters();
$mdDialog.hide();
vm.advancedSearchPanelVisible = false;
};
},
controllerAs: 'dialogCtrl',
clickOutsideToClose: false,
escapeToClose: false,
});
}
};
this.newFolder = function(parentFolder) {
@@ -309,5 +544,7 @@
angular
.module('SOGo.MailerUI')
.controller('MailboxesController', MailboxesController);
})();
@@ -357,9 +357,11 @@
// Don't report any state error
});
$transitions.onError({ to: 'mail.**' }, function(transition) {
if (transition.to().name != 'mail' &&
if ((transition.to().name != 'mail' &&
!transition.ignored() &&
transition.error().message.indexOf('superseded') < 0) {
transition.error().message.indexOf('superseded') < 0)
|| transition.to().name === "mail.account.virtualMailbox"
|| transition.to().name === "mail.account.virtualMailbox.message") {
$log.error('transition error to ' + transition.to().name + ': ' + transition.error().detail);
// Unselect everything
Mailbox.selectedFolder = false;
@@ -473,6 +473,8 @@
}
};
if (this.$parts)
// Use the cache
return this.$parts;
@@ -480,12 +482,80 @@
else if (this.parts)
_visit(this.parts);
// Highlight words
if (parts && this.$mailbox && this.$mailbox.getHighlightWords().length > 0) {
var i = 0, j = 0;
for (i = 0; i < parts.length; i++) {
if (parts[i]
&& parts[i].type
&& ("UIxMailPartHTMLViewer" == parts[i].type
|| "UIxMailPartTextViewer" == parts[i].type)) {
// Content
parts[i].content = this.highlightSearchTerms(parts[i].content);
// Title
this.subject = this.getHighlightSubject();
// From
this.from = this.getHighlightFrom();
}
}
}
// Cache result
this.$parts = parts;
return parts;
};
/**
* @function highlightSearchTerms
* @memberof Message.prototype
* @desc Returns the data with highlight search
* @returns the data with highlighted search terms
*/
Message.prototype.highlightSearchTerms = function (data) {
var i = 0;
if (this.$mailbox.getHighlightWords()
&& this.$mailbox.getHighlightWords().length > 0
&& data
&& -1 === data.indexOf("data-markjs")) {
var dom = document.createElement("DIV");
dom.innerHTML = data;
var markInstance = new Mark(dom);
markInstance.mark(this.$mailbox.getHighlightWords());
data = dom.innerHTML;
dom.remove();
}
return data;
};
/**
* @function getHighlightSubject
* @memberof Message.prototype
* @desc Returns the subject with highlight search
* @returns the subject with highlighted search terms
*/
Message.prototype.getHighlightSubject = function () {
return this.highlightSearchTerms(this.subject);
};
/**
* @function getHighlightFrom
* @memberof Message.prototype
* @desc Returns the from with highlight search
* @returns the from with highlighted search terms
*/
Message.prototype.getHighlightFrom = function () {
var i = 0;
for (i = 0; i < this.from.length; i++) {
this.from[i].full = this.highlightSearchTerms(this.from[i].full);
this.from[i].name = this.highlightSearchTerms(this.from[i].name);
}
return this.from;
};
/**
* @function $editableContent
* @memberof Message.prototype
@@ -53,8 +53,8 @@
/**
* @ngInject
*/
sgMailboxListItemController.$inject = ['$scope', '$element', '$state', '$timeout', '$mdToast', '$mdPanel', '$mdMedia', '$mdSidenav', 'sgConstant', 'Dialog', 'Mailbox', 'encodeUriFilter'];
function sgMailboxListItemController($scope, $element, $state, $timeout, $mdToast, $mdPanel, $mdMedia, $mdSidenav, sgConstant, Dialog, Mailbox, encodeUriFilter) {
sgMailboxListItemController.$inject = ['$scope', '$rootScope', '$element', '$state', '$timeout', '$mdToast', '$mdPanel', '$mdMedia', '$mdSidenav', 'sgConstant', 'Dialog', 'Mailbox', 'encodeUriFilter'];
function sgMailboxListItemController($scope, $rootScope, $element, $state, $timeout, $mdToast, $mdPanel, $mdMedia, $mdSidenav, sgConstant, Dialog, Mailbox, encodeUriFilter) {
var $ctrl = this;
@@ -84,10 +84,12 @@
this.selectFolder = function($event) {
$rootScope.$broadcast('resetMailAdvancedSearchPanel'); // Reset advanced search panel (broadcast event to MailboxesController)
if (this.editMode || this.mailbox == Mailbox.selectedFolder || this.mailbox.isNoSelect())
return;
Mailbox.$virtualPath = false;
if (Mailbox.$virtualMode) {
this.mailbox.setHighlightWords([]);
if (Mailbox.selectedFolder) {
Mailbox.$virtualMode = false;
Mailbox.selectedFolder.$reset({ filter: true });
}
@@ -283,6 +285,8 @@
// Close sidenav on small devices
if (!$mdMedia(sgConstant['gt-md']))
$mdSidenav('left').close();
$rootScope.$broadcast('showMailAdvancedSearchPanel'); // Show advanced search panel (broadcast event to MailboxesController)
};
this.share = function() {
@@ -141,11 +141,8 @@
if ($ctrl.mailboxNameElement)
$ctrl.mailboxNameElement.innerHTML = $ctrl.message.$mailbox.$displayName;
// Sender or recipient when in Sent or Draft mailbox
if ($ctrl.MailboxService.selectedFolder.isSentFolder || $ctrl.MailboxService.selectedFolder.isDraftsFolder)
$ctrl.senderElement.innerHTML = $ctrl.message.$shortAddress('to', Preferences.defaults.SOGoMailDisplayFullEmail).encodeEntities();
else
$ctrl.senderElement.innerHTML = $ctrl.message.$shortAddress('from', Preferences.defaults.SOGoMailDisplayFullEmail).encodeEntities();
// Subject and sender or recipient when in Sent or Draft mailbox
$ctrl.defineSubjectAndSenderElements();
// Priority icon
if ($ctrl.message.priority && $ctrl.message.priority.level < 3) {
@@ -169,9 +166,6 @@
$ctrl.threadButton.classList.add('ng-hide');
}
// Subject
$ctrl.subjectElement.innerHTML = $ctrl.message.subject.encodeEntities();
// Message size
$ctrl.sizeElement.innerHTML = $ctrl.message.size;
@@ -196,6 +190,23 @@
this.MailboxService = Mailbox;
};
this.defineSubjectAndSenderElements = function() {
if ($ctrl && $ctrl.message) {
// Subject
$ctrl.subjectElement.innerHTML = $ctrl.message.getHighlightSubject();
// Sender or recipient when in Sent or Draft mailbox
if ($ctrl.MailboxService.selectedFolder.isSentFolder || $ctrl.MailboxService.selectedFolder.isDraftsFolder)
$ctrl.senderElement.innerHTML = $ctrl.message.highlightSearchTerms($ctrl.message.$shortAddress('to', Preferences.defaults.SOGoMailDisplayFullEmail).encodeEntities());
else
$ctrl.senderElement.innerHTML = $ctrl.message.highlightSearchTerms($ctrl.message.$shortAddress('from', Preferences.defaults.SOGoMailDisplayFullEmail).encodeEntities());
}
};
this.$doCheck = function () {
$ctrl.defineSubjectAndSenderElements();
};
this.toggleThread = function() {
if (this.message.collapsed)
this.threadIconElement.classList.add('md-rotate-180-ccw');
File diff suppressed because one or more lines are too long
+2 -1
View File
@@ -16,7 +16,8 @@
"lodash": "^4.17.21",
"ng-sortable": "1.3.7",
"qrcodejs": "^1.0.0",
"punycode": "^2.3.0"
"punycode": "^2.3.0",
"mark.js": "^8.11.0"
},
"devDependencies": {
"autoprefixer": "^9.8.5",
@@ -18,10 +18,37 @@ md-toolbar {
transition-duration: 0s;
}
.md-toolbar-tools {
.md-toolbar-tools, .md-toolbar-tools .md-datepicker-input, .md-chips {
font-size: $subhead-font-size-base;
}
.md-toolbar-tools md-select:not([disabled]):focus .md-select-value {
border-bottom-color: unset;
color: unset;
border-bottom-width: 1px;
}
.search-right-space {
margin-right: 10px!important;
padding-right: 0px;
}
.search-and {
display: flex;
justify-content: flex-end;
align-items: center;
padding-bottom: 12px;
}
.search-inline {
display: flex;
justify-content: center;
}
.md-toolbar-tools .md-datepicker-input {
margin-top: 5px;
}
md-toolbar,
.md-toolbar-tools {
// Animate the first icon button of a "secondary" toolbar
@@ -144,7 +171,3 @@ hgroup {
min-height: $bl * 6;
height: $bl * 6;
}
//.sg-toolbar-search {
// padding: $toolbar-padding 0;
//}
@@ -452,4 +452,79 @@ md-dialog md-dialog-actions.sg-mail-editor-attachments {
.sogo-raw-html-embed .raw-html-embed__preview-placeholder {
display: none !important;
}
}
// Search highlight
mark {
background: yellow;
color: black;
}
// Advanced search
.advanced-search {
flex-direction: column;
align-items: baseline;
}
.advanced-search-full {
width: 100%;
}
.advanced-search md-input-container,
.advanced-search md-select,
.advanced-search md-datepicker,
.advanced-search .search-and,
.advanced-search a {
margin: 10px 0;
padding: 2px;
}
.advanced-search-in {
margin-top: 10px;
}
.search-inline md-menu,
.search-inline button {
height: auto !important;
margin: auto !important;
padding: 0 !important;
}
.advanced-search-more {
display: flex;
}
.advanced-search-more-no-bottom {
margin-bottom: 0!important;
margin-top: 20px !important;
}
.advanced-search-more > :first-child {
width: 100%;
}
.advanced-search-clickable-toolbar {
cursor: pointer;
}
.advanced-search-clickable-toolbar form {
padding-left: 0!important;
}
.advanced-search-clickable-toolbar form >div:first-child {
height: 100%;
display: flex;
flex-direction: row;
align-items: center;
padding-left: 16px;
}
.advanced-search-more button {
margin-right: 0!important;
}
.show-advanced-search {
cursor: pointer;
padding-left: 10px;
padding-right: 10px;
}