diff --git a/UI/MailerUI/English.lproj/Localizable.strings b/UI/MailerUI/English.lproj/Localizable.strings index 345d4c09b..0c16e118b 100644 --- a/UI/MailerUI/English.lproj/Localizable.strings +++ b/UI/MailerUI/English.lproj/Localizable.strings @@ -13,6 +13,7 @@ "Print" = "Print"; "Stop" = "Stop"; "Write" = "Write"; +"Search" = "Search"; "Send" = "Send"; "Contacts" = "Contacts"; @@ -41,6 +42,7 @@ "Attachment" = "Attachment"; "Unread" = "Unread"; "Flagged" = "Flagged"; +"Search inbox" = "Search inbox"; /* Main Frame */ @@ -94,8 +96,9 @@ "To" = "To"; "Cc" = "Cc"; "Bcc" = "Bcc"; -"Reply-To" = "Reply-To"; +"Reply-To" = "Reply-To"; "Add address" = "Add address"; +"Body" = "Body"; "Open" = "Open"; "Select All" = "Select All"; @@ -237,6 +240,18 @@ "As Not Junk" = "As Not Junk"; "Run Junk Mail Controls" = "Run Junk Mail Controls"; +"Search messages in" = "Search messages in"; +"Search" = "Search"; +"Search subfolders" = "Search subfolders"; +"Match any of the following" = "Match any of the following"; +"Match all of the following" = "Match all of the following"; +"contains" = "contains"; +"does not contain" = "does not contain"; +"No matches found" = "No matches found"; +"results found" = "results found"; +"result found" = "result found"; +"Please specify at least one filter" = "Please specify at least one filter"; + /* Folder operations */ "Name :" = "Name :"; "Enter the new name of your folder :" diff --git a/UI/MailerUI/GNUmakefile b/UI/MailerUI/GNUmakefile index e354d983f..851eb9abf 100644 --- a/UI/MailerUI/GNUmakefile +++ b/UI/MailerUI/GNUmakefile @@ -23,6 +23,7 @@ MailerUI_OBJC_FILES += \ UIxMailPopupView.m \ UIxMailMoveToPopUp.m \ UIxMailFilterPanel.m \ + UIxMailSearch.m \ \ UIxMailAccountActions.m \ UIxMailFolderActions.m \ diff --git a/UI/MailerUI/Toolbars/SOGoMailFolder.toolbar b/UI/MailerUI/Toolbars/SOGoMailFolder.toolbar index 1c9d1968d..c536a49fc 100644 --- a/UI/MailerUI/Toolbars/SOGoMailFolder.toolbar +++ b/UI/MailerUI/Toolbars/SOGoMailFolder.toolbar @@ -63,5 +63,12 @@ image = "tb-mail-print-flat-24x24.png"; label = "Print"; tooltip = "Print this message"; }, + + { link = "#"; + onclick = "return onSearchMail(event);"; + cssClass = ""; + image = "search-messages.png"; + label = "Search"; + tooltip = "Search inbox"; } ) ) diff --git a/UI/MailerUI/UIxMailListActions.m b/UI/MailerUI/UIxMailListActions.m index 33585e932..163ead021 100644 --- a/UI/MailerUI/UIxMailListActions.m +++ b/UI/MailerUI/UIxMailListActions.m @@ -54,6 +54,7 @@ #import #import #import +#import #import #import #import @@ -322,27 +323,21 @@ return @"ARRIVAL"; } -- (NSString *) imap4SortKey -{ - NSString *sort; - - sort = [[context request] formValueForKey: @"sort"]; - - return [sort uppercaseString]; -} - - (NSString *) imap4SortOrdering { - NSString *sort, *ascending; - NSString *module; + WORequest *request; + NSString *sort, *module; NSMutableDictionary *moduleSettings; - BOOL asc; + NSDictionary *urlParams, *sortingAttributes; SOGoUser *activeUser; SOGoUserSettings *us; + BOOL asc; - sort = [self imap4SortKey]; - ascending = [[context request] formValueForKey: @"asc"]; - asc = [ascending boolValue]; + request = [context request]; + urlParams = [[request contentAsString] objectFromJSONString]; + sortingAttributes = [urlParams objectForKey:@"sortingAttributes"]; + sort = [[sortingAttributes objectForKey:@"sort"] uppercaseString]; + asc = [[sortingAttributes objectForKey:@"asc"] boolValue]; activeUser = [context activeUser]; module = @"Mail"; @@ -393,37 +388,47 @@ - (EOQualifier *) searchQualifier { - NSString *criteria, *value; - EOQualifier *qualifier; - WORequest *request; - - request = [context request]; - criteria = [request formValueForKey: @"search"]; - value = [request formValueForKey: @"value"]; - qualifier = nil; - if ([value length]) - { - if ([criteria isEqualToString: @"subject"]) - qualifier = [EOQualifier qualifierWithQualifierFormat: - @"(subject doesContain: %@)", value]; - else if ([criteria isEqualToString: @"sender"]) - qualifier = [EOQualifier qualifierWithQualifierFormat: - @"(from doesContain: %@)", value]; - else if ([criteria isEqualToString: @"subject_or_sender"]) - qualifier = [EOQualifier qualifierWithQualifierFormat: - @"((subject doesContain: %@)" - @" OR (from doesContain: %@))", - value, value]; - else if ([criteria isEqualToString: @"to_or_cc"]) - qualifier = [EOQualifier qualifierWithQualifierFormat: - @"((to doesContain: %@)" - @" OR (cc doesContain: %@))", - value, value]; - else if ([criteria isEqualToString: @"entire_message"]) - qualifier = [EOQualifier qualifierWithQualifierFormat: - @"(body doesContain: %@)", value]; - } + EOQualifier *qualifier, *searchQualifier; + WORequest *request; + NSDictionary *sortingAttributes, *content; + NSArray *filters; + NSString *searchBy, *searchArgument, *searchInput, *searchString, *match; + NSMutableArray *searchArray; + int nbFilters = 0, i; + request = [context request]; + content = [[request contentAsString] objectFromJSONString]; + qualifier = nil; + searchString = nil; + + if ([content objectForKey:@"filters"]) + { + filters = [content objectForKey:@"filters"]; + sortingAttributes = [content objectForKey:@"sortingAttributes"]; + nbFilters = [filters count]; + match = [NSString stringWithString:[sortingAttributes objectForKey:@"match"]]; // AND, OR + + searchArray = [NSMutableArray arrayWithCapacity:nbFilters]; + for (i = 0; i < nbFilters; i++) + { + searchBy = [NSString stringWithString:[[filters objectAtIndex:i] objectForKey:@"searchBy"]]; + searchArgument = [NSString stringWithString:[[filters objectAtIndex:i] objectForKey:@"searchArgument"]]; + searchInput = [NSString stringWithString:[[filters objectAtIndex:i] objectForKey:@"searchInput"]]; + + if ([[[filters objectAtIndex:i] objectForKey:@"negative"] boolValue]) + searchString = [NSString stringWithFormat:@"(not (%@ %@: '%@'))", searchBy, searchArgument, searchInput]; + else + searchString = [NSString stringWithFormat:@"(%@ %@: '%@')", searchBy, searchArgument, searchInput]; + + searchQualifier = [EOQualifier qualifierWithQualifierFormat:searchString]; + [searchArray addObject:searchQualifier]; + } + if ([match isEqualToString:@"OR"]) + qualifier = [[EOOrQualifier alloc] initWithQualifierArray: searchArray]; + else + qualifier = [[EOAndQualifier alloc] initWithQualifierArray: searchArray]; + } + return qualifier; } @@ -433,25 +438,20 @@ if (!sortedUIDs) { - notDeleted = [EOQualifier qualifierWithQualifierFormat: - @"(not (flags = %@))", - @"deleted"]; + notDeleted = [EOQualifier qualifierWithQualifierFormat: @"(not (flags = %@))", @"deleted"]; qualifier = [self searchQualifier]; if (qualifier) - { - fetchQualifier = [[EOAndQualifier alloc] initWithQualifiers: - notDeleted, qualifier, - nil]; - [fetchQualifier autorelease]; - } + { + fetchQualifier = [[EOAndQualifier alloc] initWithQualifiers: notDeleted, qualifier, nil]; + [fetchQualifier autorelease]; + } else - fetchQualifier = notDeleted; - - sortedUIDs - = [mailFolder fetchUIDsMatchingQualifier: fetchQualifier - sortOrdering: [self imap4SortOrdering] - threaded: sortByThread]; - + fetchQualifier = notDeleted; + + sortedUIDs = [mailFolder fetchUIDsMatchingQualifier: fetchQualifier + sortOrdering: [self imap4SortOrdering] + threaded: sortByThread]; + [sortedUIDs retain]; } @@ -459,7 +459,8 @@ } /** - * Returns a flatten representation of the messages threads as triples of + * Returns a + of the messages threads as triples of * metadata, including the message UID, thread level and root position. * @param _sortedUIDs the interleaved arrays representation of the messages UIDs * @return an flatten array representation of the messages UIDs @@ -632,11 +633,14 @@ // Retrieve messages UIDs using form parameters "sort" and "asc" uids = [self getSortedUIDsInFolder: folder]; + + // Get rid of the extra parenthesis + // uids = [[[[uids stringValue] stringByReplacingOccurrencesOfString:@"(" withString:@""] stringByReplacingOccurrencesOfString:@")" withString:@""] componentsSeparatedByString:@","]; if (includeHeaders) { // Also retrieve the first headers, up to 'headersPrefetchMaxSize' - count = [uids count]; + count = [[uids flattenedArray] count]; if (count > headersPrefetchMaxSize) count = headersPrefetchMaxSize; r = NSMakeRange(0, count); headers = [self getHeadersForUIDs: [[uids flattenedArray] subarrayWithRange: r] @@ -671,7 +675,7 @@ - (id ) getUIDsAction { - NSDictionary *data; + NSDictionary *data, *requestContent; NSString *noHeaders; SOGoMailFolder *folder; WORequest *request; @@ -679,11 +683,14 @@ request = [context request]; response = [context response]; + requestContent = [[request contentAsString] objectFromJSONString]; + [response setHeader: @"text/plain; charset=utf-8" - forKey: @"content-type"]; + forKey: @"content-type"]; + folder = [self clientObject]; - noHeaders = [request formValueForKey: @"no_headers"]; + noHeaders = [[requestContent objectForKey: @"sortingAttributes"] objectForKey:@"no_headers"]; data = [self getUIDsInFolder: folder withHeaders: ([noHeaders length] == 0)]; diff --git a/UI/MailerUI/UIxMailMainFrame.m b/UI/MailerUI/UIxMailMainFrame.m index d87fa20af..d94f90aae 100644 --- a/UI/MailerUI/UIxMailMainFrame.m +++ b/UI/MailerUI/UIxMailMainFrame.m @@ -119,6 +119,17 @@ return [names jsonRepresentation]; } +- (NSString *) userNames +{ + NSArray *accounts, *userNames; + + accounts = [[self clientObject] mailAccounts]; + userNames = [accounts objectsForKey: @"userName" notFoundMarker: nil]; + + return [userNames jsonRepresentation]; + +} + - (NSString *) pageFormURL { NSString *u; diff --git a/UI/MailerUI/UIxMailSearch.h b/UI/MailerUI/UIxMailSearch.h new file mode 100644 index 000000000..8eb7b03ba --- /dev/null +++ b/UI/MailerUI/UIxMailSearch.h @@ -0,0 +1,29 @@ +/* UIxMailSearch.h - this file is part of SOGo + * + * Copyright (C) 2006-2014 Inverse inc. + * + * + * This file is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This file is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include + +@interface UIxMailSearch : UIxComponent +{ + id item; + +} +@end \ No newline at end of file diff --git a/UI/MailerUI/UIxMailSearch.m b/UI/MailerUI/UIxMailSearch.m new file mode 100644 index 000000000..a89467df2 --- /dev/null +++ b/UI/MailerUI/UIxMailSearch.m @@ -0,0 +1,91 @@ +/* UIxMailSearch.m - this file is part of SOGo + * + * Copyright (C) 2006-2014 Inverse inc. + * + * This file is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This file is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#import +#import +#import + +#import +#import +#import + +#import + +@implementation UIxMailSearch + +- (id) init +{ + item = nil; + + return self; +} + +- (void) dealloc +{ + [item release]; +} + +- (void) setItem: (NSString *) newItem +{ + ASSIGN(item, newItem); +} + +- (NSString *) item +{ + return item; +} + +- (NSArray *) mailAccountsList +{ + SOGoMailAccount *mAccount; + SOGoMailAccounts *mAccounts; + NSString *userName, *option, *aString; + NSArray *accountFolders; + NSMutableArray *mailboxes; + NSDictionary *accountName; + int nbMailboxes, nbMailAccounts, i, j; + + // Number of accounts linked with the current user + mAccounts = [self clientObject]; + nbMailAccounts = [[mAccounts mailAccounts] count]; + + mailboxes = [NSMutableArray array]; + for (i = 0; i < nbMailAccounts; i++) + { + accountName = [[[mAccounts mailAccounts] objectAtIndex:i] objectForKey:@"name"]; // Keys on this account = (name, port, encryption, mailboxes, serverName, identities, userName) + userName = [[[mAccounts mailAccounts] objectAtIndex:i] objectForKey:@"userName"]; + + aString = [NSString stringWithFormat:@"%i", i]; + mAccount = [mAccounts lookupName:aString inContext: context acquire: NO]; + accountFolders = [mAccount allFoldersMetadata]; + + // Number of mailboxes inside the current account + nbMailboxes = [accountFolders count]; + [mailboxes addObject:accountName]; + for (j = 0; j < nbMailboxes; j++) + { + option = [NSString stringWithFormat:@"%@%@", userName, [[accountFolders objectAtIndex:j] objectForKey:@"displayName"]]; + [mailboxes addObject:option]; + } + } + return mailboxes; +} + +@end \ No newline at end of file diff --git a/UI/MailerUI/product.plist b/UI/MailerUI/product.plist index 8b9ed30cb..4956cf286 100644 --- a/UI/MailerUI/product.plist +++ b/UI/MailerUI/product.plist @@ -327,6 +327,10 @@ pageName = "UIxMailMainFrame"; actionName = "saveColumnsState"; }; + search = { + protectedBy = "View"; + pageName = "UIxMailSearch"; + }; }; }; diff --git a/UI/Templates/MailerUI/UIxMailMainFrame.wox b/UI/Templates/MailerUI/UIxMailMainFrame.wox index 6e83548a6..b796d8ad3 100644 --- a/UI/Templates/MailerUI/UIxMailMainFrame.wox +++ b/UI/Templates/MailerUI/UIxMailMainFrame.wox @@ -9,9 +9,11 @@ title="title" const:userDefaultsKeys="SOGoMailMessageCheck,SOGoMailSortByThreads,SOGoMailListViewColumnsOrder,SOGoMailDisplayRemoteInlineImages" const:userSettingsKeys="Mail" - const:jsFiles="dtree.js,MailerUIdTree.js,SOGoAutoCompletion.js,SOGoResizableTable.js,SOGoMailDataSource.js,SOGoDataTable.js,jquery-ui.js"> + const:jsFiles="dtree.js,MailerUIdTree.js,SOGoAutoCompletion.js,SOGoResizableTable.js,SOGoMailDataSource.js,SOGoDataTable.js,jquery-ui.js, UIxMailSearch.js" + const:cssFiles="UIxMailSearch.css"> diff --git a/UI/Templates/MailerUI/UIxMailSearch.wox b/UI/Templates/MailerUI/UIxMailSearch.wox new file mode 100644 index 000000000..9e5707420 --- /dev/null +++ b/UI/Templates/MailerUI/UIxMailSearch.wox @@ -0,0 +1,89 @@ + +
+ + + + + + + + + + + + + + + + + +
+ + + + + + + +
+
+ +
+
+
+ + + + +
+
+
+
+
+
+ + + + + + + + + + + + + + + + +
+
+
+ + + + +
+
+
+
diff --git a/UI/WebServerResources/MailerUI.js b/UI/WebServerResources/MailerUI.js index f9fa6973e..3ce06319c 100644 --- a/UI/WebServerResources/MailerUI.js +++ b/UI/WebServerResources/MailerUI.js @@ -432,6 +432,43 @@ function onDocumentKeydown(event) { } } +/* Search mail, call the template and open inside a dialog windoĆ’w */ +function onSearchMail(event) { + var element = Event.findElement(event); + if (element.disabled == false || element.disabled == undefined) { + element.disabled = true; + element.writeAttribute("id", "toolbarSearchButton"); + if ($("searchMailView")) { + $("searchMailView").style.display = "block"; + $("bgDialogDiv").style.display = "block"; + initSearchMailView(); + } + else { + var urlstr = ApplicationBaseURL + "/search"; + + // Return the template for the searchMail feature + triggerAjaxRequest(urlstr, displaySearchMailCallback); + } + } +} + +function displaySearchMailCallback(http) { + if (http.readyState == 4 && http.status == 200) { + var fields = createElement("div", null); // (tagName, id, classes, attributes, htmlAttributes, parentNode) + var title = _("Search mail"); + var id = _("searchMailView"); + fields.innerHTML = http.responseText; + + var dialog = createDialog(id, title, null, fields, "searchMail"); // (id, title, legend, content, positionClass) + document.body.appendChild(dialog); + + if (Prototype.Browser.IE) + jQuery('#bgDialogDiv').css('opacity', 0.4); + jQuery(dialog).fadeIn('fast'); + initSearchMailView(); + } +} + /* bulk delete of messages */ function deleteSelectedMessages(sender) { @@ -780,7 +817,7 @@ function composeNewMessage() { function openMailbox(mailbox, reload) { if (mailbox != Mailer.currentMailbox || reload) { var url = ApplicationBaseURL + encodeURI(mailbox.unescapeHTML()); - var urlParams = new Hash(); + var urlParams = {}; if (!reload) { var messageContent = $("messageContent"); @@ -791,13 +828,28 @@ function openMailbox(mailbox, reload) { var searchValue = search["mail"]["value"]; if (searchValue && searchValue.length > 0) { - urlParams.set("search", search["mail"]["criteria"]); - urlParams.set("value", escape(searchValue.utf8encode())); + var searchCriteria = []; + if (search["mail"]["criteria"] == "subject") + searchCriteria.push("subject"); + else if (search["mail"]["criteria"] == "sender") + searchCriteria.push("from"); + else if (search["mail"]["criteria"] == "subject_or_sender") + searchCriteria.push("subject", "from"); + else if (search["mail"]["criteria"] == "to_or_cc") + searchCriteria.push("to", "cc"); + else if (search["mail"]["criteria"] == "entire_message") + searchCriteria.push("body"); + + var filters = []; + for (i = 0; i < searchCriteria.length; i++) + filters.push({"searchBy": searchCriteria[i], "searchArgument": "doesContain", "searchInput": searchValue}); + + urlParams.filters = filters; } var sortAttribute = sorting["attribute"]; if (sortAttribute && sortAttribute.length > 0) { - urlParams.set("sort", sorting["attribute"]); - urlParams.set("asc", sorting["ascending"]); + var sortingAttributes = {"sort":sorting["attribute"], "asc":sorting["ascending"], "match":"OR"}; + urlParams.sortingAttributes = sortingAttributes; var sortHeader = $(sorting["attribute"] + "Header"); if (sortHeader) { @@ -816,18 +868,15 @@ function openMailbox(mailbox, reload) { var messageList = $("messageListBody").down('TBODY'); var key = mailbox; - if (urlParams.keys().length > 0) { - var p = urlParams.keys().collect(function(key) { return key + "=" + urlParams.get(key); }).join("&"); - key += "?" + p; - } if (reload) { // Don't change data source, only query UIDs from server and refresh // the view. Cases that end up here: // - performed a search // - clicked on Get Mail button - urlParams.set("no_headers", "1"); - Mailer.dataTable.load(urlParams); + urlParams.sortingAttributes.no_headers= "1"; + var content = Object.toJSON(urlParams); + Mailer.dataTable.load(content); Mailer.dataTable.refresh(); } else { @@ -843,7 +892,8 @@ function openMailbox(mailbox, reload) { } else // Fetch UIDs and headers from server - dataSource.load(urlParams); + var content = Object.toJSON(urlParams); + dataSource.load(content); // Cache data source Mailer.dataSources.set(key, dataSource); // Update unseen count @@ -851,8 +901,9 @@ function openMailbox(mailbox, reload) { } else { // Data source is cached, query only UIDs from server - urlParams.set("no_headers", "1"); - dataSource.load(urlParams); + urlParams.sortingAttributes.no_headers= "1"; + var content = Object.toJSON(urlParams); + dataSource.load(content); } // Associate data source with data table and render the view Mailer.dataTable.setSource(dataSource); diff --git a/UI/WebServerResources/SOGoDataTable.js b/UI/WebServerResources/SOGoDataTable.js index fed13db47..ee04b19f7 100644 --- a/UI/WebServerResources/SOGoDataTable.js +++ b/UI/WebServerResources/SOGoDataTable.js @@ -118,8 +118,7 @@ var SOGoDataTableInterface = { load: function(urlParams) { if (!this.dataSource) return; // log ("DataTable.load() with parameters [" + urlParams.keys().join(' ') + "]"); - if (Object.isHash(urlParams) && urlParams.keys().length > 0) this.dataSource.load(urlParams); - else this.dataSource.load(new Hash()); + this.dataSource.load(urlParams); }, visibleRowCount: function() { diff --git a/UI/WebServerResources/SOGoMailDataSource.js b/UI/WebServerResources/SOGoMailDataSource.js index c587b2982..7826ffe32 100644 --- a/UI/WebServerResources/SOGoMailDataSource.js +++ b/UI/WebServerResources/SOGoMailDataSource.js @@ -72,22 +72,13 @@ SOGoMailDataSource = Class.create({ // log ("MailDataSource.init() " + this.uids.length + " UIDs, " + this.cache.keys().length + " headers"); }, - load: function(urlParams) { - var params; + load: function(content) { this.loaded = false; - if (urlParams.keys().length > 0) { - params = urlParams.keys().collect(function(key) { return key + "=" + urlParams.get(key); }).join("&"); - } - else - params = ""; - this.id = this.url + "?" + params; - -// log ("MailDataSource.load() " + params); triggerAjaxRequest(this.url + "/uids", this._loadCallback.bind(this), null, - params, - { "Content-type": "application/x-www-form-urlencoded" }); + content, + { "content-type": "application/json" }); }, _loadCallback: function(http) { diff --git a/UI/WebServerResources/UIxMailSearch.css b/UI/WebServerResources/UIxMailSearch.css new file mode 100644 index 000000000..37302c61e --- /dev/null +++ b/UI/WebServerResources/UIxMailSearch.css @@ -0,0 +1,206 @@ + +/*************** Table adjustment *****************/ + +TABLE#searchMailHeader +{ width: 100%; + margin-bottom: 1em; } + +DIV#searchFiltersList +{ + border: 1px solid #909090; + padding-top:2px; + border-radius: 3px; + max-height:105px; + height:105px; + overflow-y:auto; + overflow-x:hidden; + width:100%; +} + +DIV#resultsTable +{ + border: 1px solid #909090; + margin-top:5px; + border-radius: 3px; + overflow-y: auto; + width:100%; +} + +TD#mailAccountsCell { + overflow:hidden; + white-space: nowrap; +} + +TR.filterRow, DIV#searchFiltersList > TABLE +{ + width:100%; +} + +TR.filterRow > TD +{ + width: 20%; + vertical-align:middle; +} + +TR.filterRow > TD.buttonsCell +{ + width:5%; +} + +TR.filterRow > TD.inputsCell +{ + width:55%; +} + +.td_table_1, .td_table_2, .td_table_3, .td_table_4 { + cursor:default; +} + +TD.sortasc { + background:#bfc2bf; + background-image: url(/SOGo.woa/WebServerResources/arrow-up.png); + background-repeat: no-repeat; + background-position:right center; +} + +TD.sortdesc { + background:#bfc2bf; + background-image: url(/SOGo.woa/WebServerResources/arrow-down.png); + background-repeat: no-repeat; + background-position:right center; +} + +.td_header { + white-space:initial !important; + border-bottom: solid #909090 1px; + border-right: solid #909090 1px; + height:18px; + width:20%; + cursor:pointer; + background: -webkit-linear-gradient(left top, #f0f1f0 , #e6e7e6); /* For Safari 5.1 to 6.0 */ + background: -o-linear-gradient(bottom right, #f0f1f0, #e6e7e6); /* For Opera 11.1 to 12.0 */ + background: -moz-linear-gradient(bottom right, #f0f1f0, #e6e7e6); /* For Firefox 3.6 to 15 */ + background: linear-gradient(to bottom right, #f0f1f0 , #e6e7e6); /* Standard syntax */ +} + +.td_header:hover +{text-decoration: underline; } + +#buttonExpandHeader { + width: 20px; +} + +/*************** Button adjustment *****************/ + +#headerButtons +{ + width:175px; + white-space: nowrap; +} + +#searchButton, #cancelButton +{ + margin-top:0; +} + +.searchByList, .searchArgumentsList, .searchInput +{ + width:98%; + paddin:0; + margin:0; +} + +DIV#optionsButtons +{ + position:relative; + height:22px; +} + +A#deleteButton, A#openButton +{ + float:left; + margin-top:5px; +} + +.button +{ + font-style:normal; +} + +#resizeFrame +{ + text-align: -webkit-right; + margin-top:1em; +} + +#resizeButton +{ + display:inline-block; + text-align: -webkit-center; +} + +#resultsFound { + position:absolute; + bottom:20px; + right:10px; +} + +#listCollapse +{ position: relative; + border: 1px solid transparent; } + +#listCollapse img +{ position: absolute; } + +#listCollapse img.collapse +{ clip: rect(0 18px 18px 0); + top: 0; + left: 0; } + +#listCollapse img.collapse:hover +{ clip: rect(0 36px 18px 18px); + top: 0; + left: -18px; } + +#listCollapse img.rise +{ clip: rect(18px 18px 36px 0); + top: -18px; + left: 0; } + +#listCollapse img.rise:hover +{ clip: rect(18px 36px 36px 18px); + top: -18px; + left: -18px; } + +#filterButtons +{ + width:40px; +} + +IMG.addFilterButton, IMG.removeFilterButton +{ + z-index: 1; + cursor:pointer; +} + +/* Glow */ +.glow { + display: inline-block; + -webkit-transition-duration: 0.3s; + transition-duration: 0.3s; + -webkit-transition-property: box-shadow; + transition-property: box-shadow; + -webkit-transform: translateZ(0); + transform: translateZ(0); + box-shadow: 0 0 1px rgba(0, 0, 0, 0); +} +.glow:hover, .glow:focus, .glow:active { + box-shadow: 0 0 8px rgba(0, 0, 0, 0.6); +} + +/*************** Lists *****************/ +.hidden +{ display:none; } + + + diff --git a/UI/WebServerResources/UIxMailSearch.js b/UI/WebServerResources/UIxMailSearch.js new file mode 100644 index 000000000..c0196a6ad --- /dev/null +++ b/UI/WebServerResources/UIxMailSearch.js @@ -0,0 +1,492 @@ +/* -*- Mode: js2; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +var searchParams = { + searchLocation: "", + subfolder: true, + filterMatching: "AND", + filters: [] +}; + +// This variable allowed the user to stop the ongoing search +var stopOngoingSearch = false; + +/************ Search mail header ************/ + +function onSearchClick() { +// This function updates the searchParams + var filterRows = $$(".filterRow"); + var searchButton = $("searchButton").down().innerHTML; + var mailAccountsList = $("mailAccountsList").options; + + if (searchButton == _("Search")) { + searchParams.filters = []; + stopOngoingSearch = false; + + // Get the mailboxe(s) + for (i = 0; i < mailAccountsList.length ; i++) { + if (mailAccountsList[i].selected) { + searchParams.searchLocation = mailAccountsList[i].innerHTML; + break; + } + } + + for (i = 0; i < filterRows.length; i++){ + // Get the information from every filter row before triggering the AJAX call + var filter = {}; + var searchByOptions = filterRows[i].down(".searchByList").options; + var searchArgumentsOptions = filterRows[i].down(".searchArgumentsList").options; + var searchInput = filterRows[i].down(".searchInput"); + + // Get the searchBy + for (j = 0; j < searchByOptions.length ; j++) { + if (searchByOptions[j].selected) { + filter.searchBy = searchByOptions[j].innerHTML; + break; + } + } + + // Get the searchArgument + for (j = 0; j < searchArgumentsOptions.length ; j++) { + if (searchArgumentsOptions[j].selected) { + filter.searchArgument = searchArgumentsOptions[j].innerHTML; + filter.negative = false; + if (filter.searchArgument == "contains") { + filter.searchArgument = "doesContain"; + } + else if (filter.searchArgument == "does not contain") { + filter.searchArgument = "doesContain"; + filter.negative = true; + } + break; + } + } + + // Get the input text + filter.searchInput = searchInput.getValue(); + + // Add the filter inside the searchParams.filters if the input is not empty + if (!filter.searchInput.empty()) + searchParams.filters.push(filter); + } + // Send the request only if there is at least one filter + if (searchParams.filters.length > 0) { + $("searchButton").down().innerHTML = _("Stop"); + searchMails(); + } + else + alert(_("Please specify at least one filter")); + } + else { + stopOngoingSearch = true; + onSearchEnd(); + } +} + +function searchMails() { + // Variables for the subfolders search + var optionsList = $("mailAccountsList").options; + var nbOptions = optionsList.length; + var selectedIndex = optionsList.selectedIndex; + var accountNumber, accountUser, folderPath, folderName; + var mailAccountIndex = mailAccounts.indexOf(searchParams.searchLocation); + + if (mailAccountIndex != -1) { + accountNumber = "/" + mailAccountIndex; + folderName = accountNumber + "/folderINBOX"; + accountUser = userNames[mailAccountIndex]; + folderPath = accountUser; + } + else { + var searchLocation = searchParams.searchLocation.split("/"); + accountUser = searchLocation[0]; + accountNumber = "/" + userNames.indexOf(accountUser); + + var position = searchLocation.length; + folderName = accountNumber + "/folder" + searchLocation[1].asCSSIdentifier(); + for (i = 2; i < position; i++) + folderName += "/folder" + searchLocation[i]; + + folderPath = optionsList[selectedIndex].innerHTML; + } + + var subfolders = []; + if (searchParams.subfolder === true) { + for (i = 0; i < nbOptions; i++) { + if ((optionsList[i].innerHTML.search(folderPath) != -1) && (i != selectedIndex)) { + var splitArray = optionsList[i].innerHTML.split("/"); + // Remove the user information since it is not required + splitArray.splice(0, 1); + var subfolder = []; + var level = splitArray.length; + for(j = 0; j < level; j++) { + subfolder += "/folder" + splitArray[j]; + } + subfolders.push(accountNumber + subfolder); + } + } + } + + var urlstr = (ApplicationBaseURL + folderName + "/uids"); + var callbackData = {"folderName" : folderName, "subfolders" : subfolders, "newSearch" : true}; + var object = {"filters":searchParams.filters, "sortingAttributes":{"match":searchParams.filterMatching}}; + var content = Object.toJSON(object); + document.searchMailsAjaxRequest = triggerAjaxRequest(urlstr, searchMailsCallback, callbackData, content, {"content-type": "application/json"}); +} + +function searchMailsCallback(http) { + if (http.readyState == 4 && http.status == 200 && !stopOngoingSearch) { + var response = http.responseText.evalJSON(); + var table = $("searchMailFooter").down("tbody"); + + // Erase all previous entries before proceeding with the current request + if (http.callbackData.newSearch) { + var oldEntries = table.rows; + var count = oldEntries.length - 1; + for (var x = count; x >= 0; x--){ + $(oldEntries[x]).remove(); + } + + + } + + // ["To", "Attachment", "Flagged", "Subject", "From", "Unread", "Priority", "Date", "Size", "rowClasses", "labels", "rowID", "uid"] + if (response.headers.length > 1) { + if ($("noSearchResults")) + $("noSearchResults").remove(); + + for (var i = 1; i < response.headers.length; i++) { // Starts at 1 because the position 0 in the array are the headers of the table + var row = document.createElement("tr"); + Element.addClassName(row, "resultsRow"); + row.setAttribute("uid", response.headers[i][12]); + row.setAttribute("folderName", http.callbackData.folderName); + + var cell1 = document.createElement("td"); + Element.addClassName(cell1, "td_table_1"); + cell1.innerHTML = response.headers[i][3]; + row.appendChild(cell1); + + var cell2 = document.createElement("td"); + Element.addClassName(cell2, "td_table_2"); + cell2.innerHTML = response.headers[i][4]; + row.appendChild(cell2); + + var cell3 = document.createElement("td"); + Element.addClassName(cell3, "td_table_3"); + cell3.innerHTML = response.headers[i][0]; + row.appendChild(cell3); + + var cell4 = document.createElement("td"); + Element.addClassName(cell4, "td_table_4"); + cell4.innerHTML = response.headers[i][7]; + row.appendChild(cell4); + + var cell5 = document.createElement("td"); + Element.addClassName(cell5, "td_table_5"); + cell5.setAttribute("colspan", "2"); + var folderPath = http.callbackData.folderName.split("/"); + var folderLocation = folderPath[folderPath.length - 1]; // get the last element of the array (location) + folderLocation = folderLocation.substr(6); // strip down the prefix folder + cell5.innerHTML = folderLocation; + row.appendChild(cell5); + + table.appendChild(row); + } + + } + else if (http.callbackData.newSearch) { + if (!table.down("tr")) { + var row = table.insertRow(0); + var cell = row.insertCell(0); + var element = document.createElement("span"); + + cell.setAttribute("id", "noSearchResults"); + cell.setAttribute("colspan", "4"); + element.innerHTML = _("No matches found"); + cell.appendChild(element); + } + } + + if (http.callbackData.subfolders.length > 0) { + var folderName = http.callbackData.subfolders[0]; + var subfolders = http.callbackData.subfolders; + subfolders.splice(0, 1); + + var urlstr = (ApplicationBaseURL + folderName + "/uids"); + var callbackData = {"folderName" : folderName, "subfolders" : subfolders, "newSearch" : false}; + + // TODO - need to add these following contents ; asc, no-headers, sort + var object = {"filters":searchParams.filters, "sortingAttributes":{"match":searchParams.filterMatching}}; + var content = Object.toJSON(object); + document.searchMailsAjaxRequest = triggerAjaxRequest(urlstr, searchMailsCallback, callbackData, content, {"content-type": "application/json"}); + } + else { + onSearchEnd(); + } + } +} + +function onSearchEnd() { + $("searchButton").down().innerHTML = _("Search"); + var nbResults = $$(".resultsRow").length; + if (nbResults == 1) + $("resultsFound").innerHTML = nbResults + " " + _("result found"); + else if (nbResults > 0) + $("resultsFound").innerHTML = nbResults + " " + _("results found"); + else + $("resultsFound").innerHTML = ""; + + TableKit.reloadSortableTable($("searchMailFooter")); + $("buttonExpandHeader").addClassName("nosort"); +} + +function onCancelClick() { + disposeDialog(); + $("searchMailView").remove(); + $("toolbarSearchButton").disabled = false; + +} + +function onSearchSubfoldersCheck(event) { + searchParams.subfolder = (event.checked ? true : false); +} + +function onMatchFilters(event) { + searchParams.filterMatching = ((event.getAttribute("id") == "matchAllFilters") ? "AND" : "OR"); +} + +/**** Search mail body ****/ + +function onAddFilter() { + var table = $("searchFiltersList").down("TABLE"); + var searchByList = $("searchByList").getElementsByTagName("li"); + var stringArgumentsList = $("stringArgumentsList").getElementsByTagName("li"); + + var rowCount = table.rows.length; + var row = table.insertRow(rowCount); + Element.addClassName(row, "filterRow"); + + var cell1 = row.insertCell(0); + var element1 = document.createElement("select"); + Element.addClassName(element1, "searchByList"); + element1.setAttribute("id", "searchByListRow" + rowCount); + for (var i = 0; i < searchByList.length; i++) { + var option = document.createElement("option"); + option.innerHTML = searchByList[i].innerHTML; + element1.appendChild(option); + } + cell1.appendChild(element1); + + var cell2 = row.insertCell(1); + var element2 = document.createElement("select"); + Element.addClassName(element2, "searchArgumentsList"); + element2.setAttribute("id", "searchArgumentsListRow" + rowCount); + for (var i = 0; i < stringArgumentsList.length; i++) { + var option = document.createElement("option"); + option.innerHTML = stringArgumentsList[i].innerHTML; + element2.appendChild(option); + } + cell2.appendChild(element2); + + var cell3 = row.insertCell(2); + Element.addClassName(cell3, "inputsCell"); + var element3 = document.createElement("input"); + Element.addClassName(element3, "searchInput"); + element3.setAttribute("type", "text"); + element3.setAttribute("name", "searchInput"); + element3.setAttribute("id", "searchInputRow" + rowCount); + cell3.appendChild(element3); + + var cell4 = row.insertCell(3); + Element.addClassName(cell4, "buttonsCell"); + cell4.setAttribute("align", "center"); + + var buttonsDiv = document.createElement("div"); + var imageAddFilter = document.createElement("img"); + var imageRemoveFilter = document.createElement("img"); + imageAddFilter.setAttribute("src", "/SOGo.woa/WebServerResources/add-icon.png"); + imageRemoveFilter.setAttribute("src", "/SOGo.woa/WebServerResources/remove-icon.png"); + Element.addClassName(imageAddFilter, "addFilterButton"); + Element.addClassName(imageAddFilter, "glow"); + Element.addClassName(imageRemoveFilter, "removeFilterButton"); + Element.addClassName(imageRemoveFilter, "glow"); + imageAddFilter.setAttribute("name", "addFilter"); + imageAddFilter.setAttribute("id", "addFilterButtonRow" + rowCount); + $(imageAddFilter).on("click", onAddFilter); + imageRemoveFilter.setAttribute("name", "removeFilter"); + imageRemoveFilter.setAttribute("id", "removeFilterButtonRow" + rowCount); + $(imageRemoveFilter).on("click", onRemoveFilter); + buttonsDiv.setAttribute("id", "filterButtons"); + + buttonsDiv.appendChild(imageAddFilter); + buttonsDiv.appendChild(imageRemoveFilter); + + cell4.appendChild(buttonsDiv); +} + +function onRemoveFilter() { + var rows = $("searchFiltersList").getElementsByTagName("tr"); + var currentRow = this.up(".filterRow"); + + if(rows.length > 1) + $(currentRow).remove(); +} + +/**** Search mail Footer ****/ + +function onResultSelectionChange(event) { + var table = $("searchMailFooter").down("tbody"); + + if (event && (event.target.innerHTML != _("No matches found"))) { + var node = getTarget(event); + + if (node.tagName == "SPAN") + node = node.parentNode; + + // Update rows selection + onRowClick(event, node); + } +} + +/**** Search mail optionsButtons ****/ + +function onOpenClick(event) { +// This function is linked with the openButton and the doubleClick on a message + var selectedRow = $("searchMailFooter").down("._selected"); + var msguid = selectedRow.getAttribute("uid"); + var folderName = selectedRow.getAttribute("folderName"); + var accountUser = userNames[0]; + + var url = "/SOGo/so/" + accountUser + "/Mail" + folderName + "/" + msguid + "/popupview"; + if (selectedRow) { + openMessageWindow(msguid, url); + } +} + +function onDeleteClick(event) { + var messageList = $("resultsTable").down("TABLE"); + var row = $(messageList).getSelectedRows()[0]; + if (row) { + var rowIds = row.getAttribute("uid"); + var uids = new Array(); // message IDs + var paths = new Array(); // row IDs + var unseenCount = 0; + var refreshFolder = false; + if (rowIds) { + messageList.deselectAll(); + if (unseenCount < 1) { + if (row.hasClassName("mailer_unreadmail")) + unseenCount--; + else + unseenCount = 1; + + $(row).remove(); + } + var uid = rowIds; + var path = Mailer.currentMailbox + "/" + uid; + uids.push(uid); + paths.push(path); + deleteMessageRequestCount++; + + deleteCachedMessage(path); + if (Mailer.currentMessages[Mailer.currentMailbox] == uid) { + if (messageContent) messageContent.innerHTML = ''; + Mailer.currentMessages[Mailer.currentMailbox] = null; + } + Mailer.dataTable.remove(uid); + updateMessageListCounter(0 - rowIds.length, true); + if (unseenCount < 0) { + var node = mailboxTree.getMailboxNode(Mailer.currentMailbox); + if (node) { + updateUnseenCount(node, unseenCount, true); + } + } + var url = ApplicationBaseURL + encodeURI(Mailer.currentMailbox) + "/batchDelete"; + var parameters = "uid=" + uids.join(","); + var data = { "id": uids, "mailbox": Mailer.currentMailbox, "path": paths, "refreshUnseenCount": (unseenCount > 0), "refreshFolder": refreshFolder }; + triggerAjaxRequest(url, deleteMessageCallback, data, parameters, + { "Content-type": "application/x-www-form-urlencoded" }); + } + } + return false; + +} + +function deleteMessageCallback (http){ + if (isHttpStatus204(http.status) || http.status == 200) { + var data = http.callbackData; + if (http.status == 200) { + // The answer contains quota information + var rdata = http.responseText.evalJSON(true); + if (rdata.quotas && data["mailbox"].startsWith('/0/')) + updateQuotas(rdata.quotas); + } + if (data["refreshUnseenCount"]) + // TODO : the unseen count should be returned when calling the batchDelete remote action, + // in order to avoid this extra AJAX call. + getUnseenCountForFolder(data["mailbox"]); + if (data["refreshFolder"]) + Mailer.dataTable.refresh(); + } + else if (!http.callbackData["withoutTrash"]) { + showConfirmDialog(_("Warning"), + _("The messages could not be moved to the trash folder. Would you like to delete them immediately?"), + deleteMessagesWithoutTrash.bind(document, http.callbackData), + function() { refreshCurrentFolder(); disposeDialog(); }); + } + else { + var html = new Element('div').update(http.responseText); + log ("Messages deletion failed (" + http.status + ") : "); + log (html.down('p').innerHTML); + showAlertDialog(_("Operation failed")); + refreshCurrentFolder(); + } + onSearchEnd(); +} + +function onResizeClick() { + var searchFiltersList = jQuery("#searchFiltersList"); + var img = $("listCollapse").select('img').first(); + var dialogWindowHeight = $("searchMailView").getHeight(); + var state = "collapse"; + + if (searchFiltersList[0].visible()) { + state = "rise"; + searchFiltersList.fadeOut(300, function() { + adjustResultsTable(state); + img.removeClassName('collapse').addClassName('rise'); + }); + } + else { + adjustResultsTable(state); + searchFiltersList.fadeIn(); + img.removeClassName('rise').addClassName('collapse'); + } +} + +function adjustResultsTable(state) { + var resultsTable = $("resultsTable"); + var height = "innerHeight" in $("searchMailView") ? $("searchMailView").innerHeight : $("searchMailView").offsetHeight; + if (state == "collapse") { + height -= 266; + } + else + height -= 152; + $(resultsTable).style.height = height + "px"; +} + +/*************** Init ********************/ + +function initSearchMailView () { + + // Add one filterRow + onAddFilter(); + adjustResultsTable("collapse"); + + // Observers : Event.on(element, eventName[, selector], callback) + $("searchMailFooter").down("tbody").on("mousedown", "tr", onResultSelectionChange); + $("searchMailFooter").down("tbody").on("dblclick", "tr", onOpenClick); + Event.observe(window, "resize", function() { + var state = ($("searchFiltersList").visible() ? "collapse": "rise"); + adjustResultsTable(state); + }); +} diff --git a/UI/WebServerResources/generic.css b/UI/WebServerResources/generic.css index 1564af868..a17eed385 100644 --- a/UI/WebServerResources/generic.css +++ b/UI/WebServerResources/generic.css @@ -659,6 +659,19 @@ DIV.dialog > DIV top: 7px; } +DIV.dialog.searchMail { + position: relative; + padding: 0px; + opacity: 1; + width: 75%; + height: 75%; + margin: 2em auto; +} + +DIV.dialog.searchMail > DIV { + min-height:500px; +} + DIV.dialog.none { position: relative; margin: 0px; diff --git a/UI/WebServerResources/generic.js b/UI/WebServerResources/generic.js index ccb1060cd..048b6c38f 100644 --- a/UI/WebServerResources/generic.js +++ b/UI/WebServerResources/generic.js @@ -1998,7 +1998,7 @@ function createDialog(id, title, legend, content, positionClass) { var newDialog = createElement("div", id, ["dialog", positionClass]); newDialog.setStyle({"display": "none"}); - if (positionClass == "none") { + if (positionClass == "none" || positionClass == "searchMail") { var bgDiv = $("bgDialogDiv"); if (bgDiv) { bgDiv.show(); diff --git a/UI/WebServerResources/tablekit.js b/UI/WebServerResources/tablekit.js index 370879520..4287e17fc 100644 --- a/UI/WebServerResources/tablekit.js +++ b/UI/WebServerResources/tablekit.js @@ -242,6 +242,12 @@ Object.extend(TableKit, { if(op.resizable) {TableKit.Resizable.init(table);} if(op.editable) {TableKit.Editable.init(table);} }, + reloadSortableTable : function(table){ + table = $(table); + TableKit.unloadTable(table); + var op = TableKit.option('sortable', table.id); + if(op.sortable) {TableKit.Sortable.init(table);} + }, reload : function() { for(var k in TableKit.tables) { TableKit.reloadTable(k);