Compare commits

...

1 Commits

Author SHA1 Message Date
zzz
497e5a7873 Draft: WIP: Add search box to susimail
Searches subject and recipients only. Not a full-text search. No index is generated or persisted.
Search is per-folder only, from the folder (LIST) view state only.
Supports iteration and paging within results.
This is a POST search only. No js/XHR support.

Splits up the single HTML form in folder view to 5 forms, as necessary to
do this sanely, and also as prep for js search later.

WIP, may contain bugs and some small unrelated changes, not for 2.5.0.

TODO: Testing, cleanup, verify all state transitions handled, more planning for js, and then js.
2024-02-17 10:32:18 -05:00
5 changed files with 382 additions and 67 deletions

View File

@@ -40,7 +40,7 @@ function addClickHandler1(elem)
function addClickHandler2(elem) function addClickHandler2(elem)
{ {
elem.addEventListener("click", function() { elem.addEventListener("click", function() {
var form = document.forms[0]; var form = document.forms[3];
form.delete.disabled = false; form.delete.disabled = false;
form.markall.disabled = true; form.markall.disabled = true;
form.clearselection.disabled = false; form.clearselection.disabled = false;
@@ -57,7 +57,7 @@ function addClickHandler2(elem)
function addClickHandler3(elem) function addClickHandler3(elem)
{ {
elem.addEventListener("click", function() { elem.addEventListener("click", function() {
var form = document.forms[0]; var form = document.forms[3];
form.delete.disabled = true; form.delete.disabled = true;
form.markall.disabled = false; form.markall.disabled = false;
form.clearselection.disabled = true; form.clearselection.disabled = true;
@@ -82,7 +82,7 @@ function deleteboxclicked() {
var hasOne = false; var hasOne = false;
var hasAll = true; var hasAll = true;
var hasNone = true; var hasNone = true;
var form = document.forms[0]; var form = document.forms[3];
for(i = 0; i < form.elements.length; i++) { for(i = 0; i < form.elements.length; i++) {
var elem = form.elements[i]; var elem = form.elements[i];
if (elem.type == 'checkbox') { if (elem.type == 'checkbox') {

View File

@@ -61,12 +61,28 @@ public class Folder<O extends Object> {
UP; UP;
} }
/**
* @since 0.9.63
*/
public interface Selector<O> {
/**
* @return non-null
*/
public String getSelectionKey();
/**
* @return true if selected
*/
public boolean select(O element);
}
private int pages, pageSize, currentPage; private int pages, pageSize, currentPage;
private O[] elements; private O[] elements;
private final Map<String, Comparator<O>> sorter; private final Map<String, Comparator<O>> sorter;
private SortOrder sortingDirection; private SortOrder sortingDirection;
private Comparator<O> currentSorter; private Comparator<O> currentSorter;
private String currentSortID; private String currentSortID;
private Selector<O> currentSelector;
private List<O> selected;
public Folder() public Folder()
{ {
@@ -109,10 +125,18 @@ public class Folder<O extends Object> {
/** /**
* Returns the number of pages in the folder. * Returns the number of pages in the folder.
* Minimum of 1 even if empty. * Minimum of 1 even if empty.
* If the selector is non-null, this will be the number of pages in the selected results.
*
* @return Returns the number of pages. * @return Returns the number of pages.
*/ */
public synchronized int getPages() { public synchronized int getPages() {
return pages; if (currentSelector == null || elements == null)
return pages;
int ps = getPageSize();
int rv = selected.size() / ps;
if (rv * ps < elements.length)
rv++;
return rv;
} }
/** /**
@@ -177,6 +201,7 @@ public class Folder<O extends Object> {
if (elements.length > 0) { if (elements.length > 0) {
this.elements = elements; this.elements = elements;
sort(); sort();
select();
} else { } else {
this.elements = null; this.elements = null;
} }
@@ -285,6 +310,27 @@ public class Folder<O extends Object> {
return list.iterator(); return list.iterator();
} }
/**
* Returns an iterator containing the elements on the current page.
* This iterator is over a copy of the current page, and so
* is thread safe w.r.t. other operations on this folder,
* but will not reflect subsequent changes, and iter.remove()
* will not change the folder.
*
* @return Iterator containing the elements on the current page.
* @since 0.9.63
*/
public synchronized Iterator<O> currentPageSelectorIterator()
{
if (currentSelector == null)
return currentPageIterator();
int pageSize = getPageSize();
int offset = ( currentPage - 1 ) * pageSize;
if (selected == null || offset > selected.size())
return Collections.<O>emptyList().iterator();
return selected.subList(offset, Math.min(selected.size(), offset + pageSize)).iterator();
}
/** /**
* Turns folder to next page. * Turns folder to next page.
*/ */
@@ -372,25 +418,52 @@ public class Folder<O extends Object> {
} }
/** /**
* Returns the element on the current page on the given position. * Warning, this does not do the actual selection, this is done in the iterator.
* Resets page to 1 if selector changed.
* *
* @param x Position of the element on the current page. * @param selector may be null
* @return Element on the current page on the given position. * @since 0.9.63
*/ */
/**** unused, we now fetch by UIDL, not position public synchronized void setSelector(Selector<O> selector) {
public synchronized O getElementAtPosXonCurrentPage( int x ) if ((currentSelector != null && selector == null) ||
{ (currentSelector == null && selector != null) ||
O result = null; (currentSelector != null && selector != null &&
if( elements != null ) { !currentSelector.getSelectionKey().equals(selector.getSelectionKey()))) {
int pageSize = getPageSize(); currentPage = 1;
int offset = ( currentPage - 1 ) * pageSize; }
offset += x; currentSelector = selector;
if( offset >= 0 && offset < elements.length ) if (selector != null)
result = elements[offset]; select();
else
selected = null;
}
/**
* @return current selector or null
* @since 0.9.63
*/
public synchronized Selector<O> getCurrentSelector() {
return currentSelector;
}
/**
* Select and cache results
* @since 0.9.63
*/
private synchronized void select() {
if (selected == null)
selected = new ArrayList<O>();
else
selected.clear();
if (elements == null || currentSelector == null)
return;
int sz = getSize();
for (int i = 0; i < sz; i++) {
if (currentSelector.select(elements[i])) {
selected.add(elements[i]);
}
} }
return result;
} }
****/
/** /**
* Returns the first element of the sorted folder. * Returns the first element of the sorted folder.
@@ -467,6 +540,47 @@ public class Folder<O extends Object> {
} }
return result; return result;
} }
/**
* Retrieves the next element in the sorted array.
*
* @param element
* @return The next element
*/
public synchronized O getNextSelectedElement(O element)
{
if (currentSelector == null)
return getNextElement(element);
O result = null;
int i = selected.indexOf(element);
if (i != -1 && selected != null) {
i++;
if (i < selected.size())
result = selected.get(i);
}
return result;
}
/**
* Retrieves the previous element in the sorted array.
*
* @param element
* @return The previous element
*/
public synchronized O getPreviousSelectedElement(O element)
{
if (currentSelector == null)
return getPreviousElement(element);
O result = null;
int i = selected.indexOf(element);
if (i != -1 && selected != null) {
i--;
if (i >= 0 && i < selected.size())
result = selected.get(i);
}
return result;
}
/** /**
* Retrieves element at index i. * Retrieves element at index i.
* *

View File

@@ -145,6 +145,7 @@ public class WebMail extends HttpServlet
private static final String NEXT_PAGE_NUM = "nextpagenum"; private static final String NEXT_PAGE_NUM = "nextpagenum";
private static final String CURRENT_SORT = "currentsort"; private static final String CURRENT_SORT = "currentsort";
private static final String CURRENT_FOLDER = "folder"; private static final String CURRENT_FOLDER = "folder";
private static final String CURRENT_SEARCH = "currentsearch";
private static final String NEW_FOLDER = "newfolder"; private static final String NEW_FOLDER = "newfolder";
private static final String DRAFT_EXISTS = "draftexists"; private static final String DRAFT_EXISTS = "draftexists";
private static final String DEBUG_STATE = "currentstate"; private static final String DEBUG_STATE = "currentstate";
@@ -167,6 +168,7 @@ public class WebMail extends HttpServlet
private static final String REALLYDELETE = "really_delete"; private static final String REALLYDELETE = "really_delete";
private static final String MOVE_TO = "moveto"; private static final String MOVE_TO = "moveto";
private static final String SWITCH_TO = "switchto"; private static final String SWITCH_TO = "switchto";
private static final String SEARCH = "s";
// also a GET param // also a GET param
private static final String SHOW = "show"; private static final String SHOW = "show";
private static final String DOWNLOAD = "download"; private static final String DOWNLOAD = "download";
@@ -412,6 +414,8 @@ public class WebMail extends HttpServlet
buf.append(" beforePopup\""); buf.append(" beforePopup\"");
else else
buf.append('"'); buf.append('"');
if (name.equals(NEW_UPLOAD))
buf.append(" id=\"" + NEW_UPLOAD + '"');
// These are icons only now, via the CSS, so add a tooltip // These are icons only now, via the CSS, so add a tooltip
if (name.equals(FIRSTPAGE) || name.equals(PREVPAGE) || name.equals(NEXTPAGE) || name.equals(LASTPAGE) || if (name.equals(FIRSTPAGE) || name.equals(PREVPAGE) || name.equals(NEXTPAGE) || name.equals(LASTPAGE) ||
name.equals(PREV) || name.equals(LIST) || name.equals(NEXT)) name.equals(PREV) || name.equals(LIST) || name.equals(NEXT))
@@ -1177,7 +1181,8 @@ public class WebMail extends HttpServlet
buttonPressed( request, MARKALL ) || buttonPressed( request, MARKALL ) ||
buttonPressed( request, CLEAR ) || buttonPressed( request, CLEAR ) ||
buttonPressed( request, INVERT ) || buttonPressed( request, INVERT ) ||
buttonPressed( request, REFRESH )) { buttonPressed( request, SORT ) ||
buttonPressed( request, REFRESH ) ||
buttonPressed( request, SEARCH )) { buttonPressed( request, SEARCH )) {
state = State.LIST; state = State.LIST;
} else if (buttonPressed(request, PREV) || } else if (buttonPressed(request, PREV) ||
@@ -1205,6 +1210,9 @@ public class WebMail extends HttpServlet
state = State.SHOW; state = State.SHOW;
} else if (buttonPressed(request, DRAFT_ATTACHMENT)) { } else if (buttonPressed(request, DRAFT_ATTACHMENT)) {
// GET params // GET params
state = State.NEW;
} else if (buttonPressed(request, SEARCH)) {
// GET params for XHR
return State.LIST; return State.LIST;
} }
@@ -1809,7 +1817,7 @@ public class WebMail extends HttpServlet
} }
/** /**
* @param id a content-id, without the surrounding &lt;&gt; or trailing @ part * Recursive.
* @param id a content-id, without the surrounding &lt;&gt; * @param id a content-id, without the surrounding &lt;&gt;
* @return the part or null * @return the part or null
* @since 0.9.62 * @since 0.9.62
@@ -1818,14 +1826,6 @@ public class WebMail extends HttpServlet
if( part == null ) if( part == null )
return null; return null;
if (id.equals(part.cid)) if (id.equals(part.cid))
if (part.cid != null) {
// strip @ and try again,
int idx = part.cid.indexOf('@');
if (idx > 0) {
if (id.equals(part.cid.substring(0, idx)))
return part;
}
}
return part; return part;
if( part.multipart || part.message ) { if( part.multipart || part.message ) {
for( MailPart p : part.parts ) { for( MailPart p : part.parts ) {
@@ -2191,8 +2191,9 @@ public class WebMail extends HttpServlet
boolean forceMobileConsole = ctx.getBooleanProperty(RC_PROP_FORCE_MOBILE_CONSOLE); boolean forceMobileConsole = ctx.getBooleanProperty(RC_PROP_FORCE_MOBILE_CONSOLE);
boolean isMobile = (forceMobileConsole || isMobile(httpRequest.getHeader("User-Agent"))); boolean isMobile = (forceMobileConsole || isMobile(httpRequest.getHeader("User-Agent")));
response.setHeader("X-Frame-Options", "SAMEORIGIN"); httpRequest.setCharacterEncoding("UTF-8");
response.setHeader("X-Frame-Options", "SAMEORIGIN"); response.setHeader("X-Frame-Options", "SAMEORIGIN");
// very strict CSP for HTML emails in iframes
// TODO self paths, review // TODO self paths, review
if (httpRequest.getParameter(RAW_ATTACHMENT) != null || if (httpRequest.getParameter(RAW_ATTACHMENT) != null ||
httpRequest.getParameter(CID_ATTACHMENT) != null || httpRequest.getParameter(CID_ATTACHMENT) != null ||
@@ -2211,7 +2212,7 @@ public class WebMail extends HttpServlet
response.setHeader("Content-Security-Policy", "default-src 'self'; style-src 'self'; script-src 'self'; form-action 'self'; frame-ancestors 'self'; object-src 'none'; media-src 'none'; img-src 'self'"); response.setHeader("Content-Security-Policy", "default-src 'self'; style-src 'self'; script-src 'self'; form-action 'self'; frame-ancestors 'self'; object-src 'none'; media-src 'none'; img-src 'self'");
response.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8");
} }
response.setHeader("X-XSS-Protection", "1; mode=block"); response.setHeader("Permissions-Policy", "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), display-capture(), fullscreen=(self), geolocation=(), gyroscope=(), interest-cohort=(), magnetometer=(), microphone=(), midi=(), payment=(), usb=(), vibrate=(), vr=()");
response.setHeader("X-XSS-Protection", "1; mode=block"); response.setHeader("X-XSS-Protection", "1; mode=block");
response.setHeader("X-Content-Type-Options", "nosniff"); response.setHeader("X-Content-Type-Options", "nosniff");
response.setHeader("Referrer-Policy", "no-referrer"); response.setHeader("Referrer-Policy", "no-referrer");
@@ -2568,15 +2569,31 @@ public class WebMail extends HttpServlet
out.println("<script src=\"/js/iframeResizer.js?" + CoreVersion.VERSION + "\" type=\"text/javascript\"></script>"); out.println("<script src=\"/js/iframeResizer.js?" + CoreVersion.VERSION + "\" type=\"text/javascript\"></script>");
} }
out.println("<script src=\"/susimail/js/notifications.js?" + CoreVersion.VERSION + "\" type=\"text/javascript\"></script>"); out.println("<script src=\"/susimail/js/notifications.js?" + CoreVersion.VERSION + "\" type=\"text/javascript\"></script>");
String nonce = state == State.AUTH ? LOGIN_NONCE : out.print("</head>\n<body>");
Long.toString(ctx.random().nextLong()); out.println("<div class=\"page\" id=\"page\">");
sessionObject.addNonce(nonce);
out.println( if (state != State.LIST) {
"<div class=\"page\">" + // For all states except LIST, we have one big form for the whole page.
"<form method=\"POST\" enctype=\"multipart/form-data\" action=\"" + myself + "\" accept-charset=\"UTF-8\">\n" + // LIST has several forms, we will output them in showFolder().
"<input type=\"hidden\" name=\"" + SUSI_NONCE + "\" value=\"" + nonce + "\">\n" + String nonce = state == State.AUTH ? LOGIN_NONCE :
// we use this to know if the user thought he was logged in at the time Long.toString(ctx.random().nextLong());
"<input type=\"hidden\" name=\"" + DEBUG_STATE + "\" value=\"" + state + "\">"); sessionObject.addNonce(nonce);
out.println("<form method=\"POST\" enctype=\"multipart/form-data\" action=\"" + myself + "\" accept-charset=\"UTF-8\">\n" +
"<input type=\"hidden\" name=\"" + SUSI_NONCE + "\" value=\"" + nonce + "\">\n" +
// we use this to know if the user thought he was logged in at the time
"<input type=\"hidden\" name=\"" + DEBUG_STATE + "\" value=\"" + state + "\">");
if (state != State.AUTH && state != State.CONFIG && state != State.LOADING) {
// maintain the search param when changing pages or folders or
// going to message view and back
String search = request.getParameter(CURRENT_SEARCH);
if (search == null || search.length() == 0) {
Folder.Selector selector = mc.getFolder().getCurrentSelector();
if (selector != null)
search = selector.getSelectionKey();
}
if (search != null && search.length() > 0)
out.println("<input type=\"hidden\" name=\"" + CURRENT_SEARCH + "\" value=\"" + DataHelper.escapeHTML(search) + "\">\n");
}
} }
if (state == State.NEW) { if (state == State.NEW) {
String newUIDL = request.getParameter(NEW_UIDL); String newUIDL = request.getParameter(NEW_UIDL);
@@ -2600,8 +2617,6 @@ public class WebMail extends HttpServlet
} catch (NumberFormatException nfe) {} } catch (NumberFormatException nfe) {}
} }
out.println("<input type=\"hidden\" name=\"" + CUR_PAGE + "\" value=\"" + page + "\">"); out.println("<input type=\"hidden\" name=\"" + CUR_PAGE + "\" value=\"" + page + "\">");
}
if (state == State.SHOW || state == State.NEW || state == State.LIST) {
} }
// Save sort order in case it changes later // Save sort order in case it changes later
String curSort = folder.getCurrentSortBy(); String curSort = folder.getCurrentSortBy();
@@ -2652,6 +2667,7 @@ public class WebMail extends HttpServlet
out.println("</b></p>"); out.println("</b></p>");
} }
out.println("</div>" ); out.println("</div>" );
}
/* /*
* now write body * now write body
@@ -2689,7 +2705,8 @@ public class WebMail extends HttpServlet
else if( state == State.CONFIG ) else if( state == State.CONFIG )
showConfig(out, folder); showConfig(out, folder);
out.println("</form>\n");
if (state != State.LIST)
out.println("</form>\n"); out.println("</form>\n");
out.println("<div class=\"footer\"><p class=\"footer\">\n" + out.println("<div class=\"footer\"><p class=\"footer\">\n" +
@@ -3426,18 +3443,9 @@ public class WebMail extends HttpServlet
* @param request * @param request
*/ */
private static void showFolder( PrintWriter out, SessionObject sessionObject, MailCache mc, RequestWrapper request ) private static void showFolder( PrintWriter out, SessionObject sessionObject, MailCache mc, RequestWrapper request )
out.println("<div class=\"topbuttons\">");
out.println( button( NEW, _t("New") ) + spacer);
// In theory, these are valid and will apply to the first checked message,
// but that's not obvious and did it work?
//button( REPLY, _t("Reply") ) +
//button( REPLYALL, _t("Reply All") ) +
//button( FORWARD, _t("Forward") ) + spacer +
//button( DELETE, _t("Delete") ) + spacer +
{ {
String folderName = mc.getFolderName(); String folderName = mc.getFolderName();
String floc; String floc;
out.println((sessionObject.isFetching ? button2(REFRESH, _t("Check Mail")) : button(REFRESH, _t("Check Mail"))) + spacer);
if (folderName.equals(DIR_FOLDER)) { if (folderName.equals(DIR_FOLDER)) {
floc = ""; floc = "";
} else if (folderName.equals(DIR_DRAFTS)) { } else if (folderName.equals(DIR_DRAFTS)) {
@@ -3446,11 +3454,75 @@ public class WebMail extends HttpServlet
floc = '&' + CURRENT_FOLDER + '=' + folderName; floc = '&' + CURRENT_FOLDER + '=' + folderName;
} }
boolean isSpamFolder = folderName.equals(DIR_SPAM); boolean isSpamFolder = folderName.equals(DIR_SPAM);
//if (Config.hasConfigFile()) boolean showToColumn = folderName.equals(DIR_DRAFTS) || folderName.equals(DIR_SENT);
// out.println(button( RELOAD, _t("Reload Config") ) + spacer); // For all states except LIST, we have one big form for the whole page.
out.println(button( LOGOUT, _t("Logout") )); // Here, for LIST, we set up 4-5 forms
int page = 1; // to deal with html rules and have a search box that works right.
// 1: new/checkmail/logout, inside topbuttons div
// 2: search, inside topbuttons div
// 3: change folder and page buttons, inside topbuttons div, surrounds pagenav table
// 4: mail delete boxes and bottombuttons div, surrounds mailbox table
// 5: change folder and page buttons, inside 2nd topbuttons div, surrounds pagenav table,
// only shown if more than 30 mails on a page
//
// Create the common form header
I2PAppContext ctx = I2PAppContext.getGlobalContext();
String nonce = Long.toString(ctx.random().nextLong());
sessionObject.addNonce(nonce);
// for all but search
String form = "<form method=\"POST\" enctype=\"multipart/form-data\" action=\"" + myself + "\" accept-charset=\"UTF-8\">\n";
StringBuilder fbf = new StringBuilder(256);
fbf.append("<input type=\"hidden\" name=\"").append(SUSI_NONCE).append("\" value=\"").append(nonce).append("\">\n")
.append("<input type=\"hidden\" name=\"").append(DEBUG_STATE).append("\" value=\"").append(State.LIST).append("\">\n"); .append("<input type=\"hidden\" name=\"").append(DEBUG_STATE).append("\" value=\"").append(State.LIST).append("\">\n");
Folder<String> folder = mc.getFolder();
String curSort = folder.getCurrentSortBy();
SortOrder curOrder = folder.getCurrentSortingDirection();
// UP is reverse sort. DOWN is normal sort.
String fullSort = curOrder == SortOrder.UP ? '-' + curSort : curSort;
fbf.append("<input type=\"hidden\" name=\"").append(CURRENT_SORT).append("\" value=\"").append(fullSort).append("\">\n")
.append("<input type=\"hidden\" name=\"").append(CURRENT_FOLDER).append("\" value=\"").append(folderName).append("\">\n");
String cursearch = request.getParameter(CURRENT_SEARCH);
String search = request.getParameter(SEARCH);
if (cursearch != null && cursearch.length() > 0) {
if (search == null) {
// we came from somewhere else, set search to cursearch, will set selector below
search = cursearch;
} else {
// we came from here, search wins, will set selector below
}
}
if (search != null && search.length() > 0) {
fbf.append("<input type=\"hidden\" name=\"").append(CURRENT_SEARCH).append("\" value=\"").append(DataHelper.escapeHTML(search)).append("\">\n");
Folder.Selector olds = folder.getCurrentSelector();
if (olds == null || !olds.getSelectionKey().equals(search)) {
folder.setSelector(new SearchSelector(mc, search));
}
} else if ((search == null || search.length() == 0) && folder.getCurrentSelector() != null) {
folder.setSelector(null);
}
String hidden = fbf.toString();
out.println("<div class=\"topbuttons\">");
// form 1
out.print(form);
out.print(hidden);
out.println( button( NEW, _t("New") ) + spacer);
if (folderName.equals(DIR_FOLDER))
out.println((sessionObject.isFetching ? button2(REFRESH, _t("Check Mail")) : button(REFRESH, _t("Check Mail"))) + spacer);
out.println(button( LOGOUT, _t("Logout") ));
out.println("</form>");
if (folder.getSize() > 1 || (search != null && search.length() > 0)) {
// form 2
out.println("<form class=\"search\" id = \"search\" action=\"" + myself + "\" method=\"POST\">");
out.print(hidden);
out.write("<input type=\"text\" name=\"" + SEARCH + "\" size=\"20\" class=\"search\" id=\"searchbox\"");
if (search != null)
out.write(" value=\"" + DataHelper.escapeHTML(search) + '"');
out.println("><a class=\"cancel\" id=\"searchcancel\" href=\"" + myself + "\"></a>");
out.println("</form>");
}
int page = 1; int page = 1;
if (folder.getPages() > 1) { if (folder.getPages() > 1) {
String sp = request.getParameter(CUR_PAGE); String sp = request.getParameter(CUR_PAGE);
@@ -3460,13 +3532,18 @@ public class WebMail extends HttpServlet
} catch (NumberFormatException nfe) {} } catch (NumberFormatException nfe) {}
} }
folder.setCurrentPage(page); folder.setCurrentPage(page);
}
// form 3
out.print(form);
out.print(hidden); out.print(hidden);
showPageButtons(out, sessionObject.user, folderName, page, folder.getPages(), true);
out.println("</form>"); out.println("</form>");
out.println("</div>"); out.println("</div>");
String curSort = folder.getCurrentSortBy();
SortOrder curOrder = folder.getCurrentSortingDirection(); // form 4
out.println("<table id=\"mailbox\" cellspacing=\"0\" cellpadding=\"5\">\n" + out.print(form);
"<tr><td colspan=\"9\"><hr></td></tr>\n<tr><th title=\"" + _t("Mark for deletion") + "\">&nbsp;</th>" + out.print(hidden);
out.println("<table id=\"mailbox\" cellspacing=\"0\" cellpadding=\"5\">\n");
out.println("<tr><td colspan=\"9\"><hr></td></tr>\n<tr><th title=\"" + _t("Mark for deletion") + "\">&nbsp;</th>" + out.println("<tr><td colspan=\"9\"><hr></td></tr>\n<tr><th title=\"" + _t("Mark for deletion") + "\">&nbsp;</th>" +
thSpacer + "<th>" + sortHeader(SORT_SENDER, showToColumn ? _t("To") : _t("From"), sessionObject.imgPath, curSort, curOrder, page, folderName) + "</th>" + thSpacer + "<th>" + sortHeader(SORT_SENDER, showToColumn ? _t("To") : _t("From"), sessionObject.imgPath, curSort, curOrder, page, folderName) + "</th>" +
thSpacer + "<th>" + sortHeader(SORT_SUBJECT, _t("Subject"), sessionObject.imgPath, curSort, curOrder, page, folderName) + "</th>" + thSpacer + "<th>" + sortHeader(SORT_SUBJECT, _t("Subject"), sessionObject.imgPath, curSort, curOrder, page, folderName) + "</th>" +
@@ -3474,7 +3551,7 @@ public class WebMail extends HttpServlet
"</th>" + "</th>" +
thSpacer + "<th>" + sortHeader(SORT_SIZE, _t("Size"), sessionObject.imgPath, curSort, curOrder, page, folderName) + "</th></tr>" ); thSpacer + "<th>" + sortHeader(SORT_SIZE, _t("Size"), sessionObject.imgPath, curSort, curOrder, page, folderName) + "</th></tr>" );
int bg = 0; int bg = 0;
for (Iterator<String> it = folder.currentPageIterator(); it != null && it.hasNext(); ) { int i = 0;
for (Iterator<String> it = folder.currentPageSelectorIterator(); it != null && it.hasNext(); ) { for (Iterator<String> it = folder.currentPageSelectorIterator(); it != null && it.hasNext(); ) {
String uidl = it.next(); String uidl = it.next();
Mail mail = mc.getMail(uidl, MailCache.FetchMode.HEADER_CACHE_ONLY); Mail mail = mc.getMail(uidl, MailCache.FetchMode.HEADER_CACHE_ONLY);
@@ -3586,14 +3663,82 @@ public class WebMail extends HttpServlet
out.print("<br>"); out.print("<br>");
out.print(button(CONFIGURE, _t("Settings"))); out.print(button(CONFIGURE, _t("Settings")));
out.println("</td></tr>"); out.println("</td></tr>");
if (folder.getPages() > 1 && i > 30) { out.println( "</table>");
out.println("</form>");
int ps = folder.getPageSize();
if (ps > 30 && (folder.getCurrentPage() - 1) * ps < folder.getSize() - 30) { if (ps > 30 && (folder.getCurrentPage() - 1) * ps < folder.getSize() - 30) {
// show the buttons again if page is big // show the buttons again if page is big
out.println("<div class=\"topbuttons\">");
// form 5
out.print(form);
out.print(hidden); out.print(hidden);
showPageButtons(out, sessionObject.user, folderName, page, folder.getPages(), false);
out.println("</form>"); out.println("</form>");
out.println("</div>"); out.println("</div>");
} }
} }
/**
* Folder callback to search mails for matching terms.
* Only subject and sender (or recipients for drafts).
* Mail bodies are not in-memory and would be very slow.
*
* @since 0.9.63
*/
private static class SearchSelector implements Folder.Selector<String> {
private final String key;
private final MailCache mc;
private final String[] terms;
private final boolean isDrafts;
/**
* @param search non-null, non-empty, and %-encoded, will be decoded here
*/
public SearchSelector(MailCache cache, String search) {
mc = cache;
isDrafts = mc.getFolderName().equals(DIR_DRAFTS);
key = search;
terms = DataHelper.split(search, " ");
// decode
for (int i = 0; i < terms.length; i++) {
terms[i] = terms[i].toLowerCase(Locale.US);
}
}
public String getSelectionKey() {
return key;
}
public boolean select(String uidl) {
Mail mail = mc.getMail(uidl, MailCache.FetchMode.HEADER_CACHE_ONLY);
if (mail == null)
return false;
String subj = mail.subject.toLowerCase(Locale.US);
String sender = isDrafts ? null : mail.sender;
if (sender != null)
sender = sender.toLowerCase(Locale.US);
String[] to = isDrafts ? mail.to : null;
for (String term : terms) {
if (subj.contains(term))
return true;
if (sender != null && sender.contains(term))
return true;
if (to != null) {
for (int i = 0; i < to.length; i++) {
if (to[i].toLowerCase(Locale.US).contains(term))
return true;
}
}
}
return false;
}
@Override
public String toString() {
return "Search selector for '" + key + "'";
}
}
/** /**
* Folder selector, then, if pages greater than 1: * Folder selector, then, if pages greater than 1:
@@ -3726,7 +3871,11 @@ public class WebMail extends HttpServlet
// processRequest() will P-R-G the PREV and NEXT so we have a consistent URL // processRequest() will P-R-G the PREV and NEXT so we have a consistent URL
out.println("<div id=\"messagenav\">"); out.println("<div id=\"messagenav\">");
Folder<String> folder = mc.getFolder(); Folder<String> folder = mc.getFolder();
String uidl = folder.getPreviousElement(showUIDL); if (hasHeader) {
String uidl;
if (folder.getCurrentSelector() != null)
uidl = folder.getPreviousSelectedElement(showUIDL);
else
uidl = folder.getPreviousElement(showUIDL); uidl = folder.getPreviousElement(showUIDL);
String text = _t("Previous"); String text = _t("Previous");
if (uidl == null || folder.isFirstElement(showUIDL)) { if (uidl == null || folder.isFirstElement(showUIDL)) {
@@ -3741,7 +3890,11 @@ public class WebMail extends HttpServlet
int page = folder.getPageOf(showUIDL); int page = folder.getPageOf(showUIDL);
out.println("<input type=\"hidden\" name=\"" + CUR_PAGE + "\" value=\"" + page + "\">"); out.println("<input type=\"hidden\" name=\"" + CUR_PAGE + "\" value=\"" + page + "\">");
out.println(button( LIST, _t("Back to Folder") ) + spacer); out.println(button( LIST, _t("Back to Folder") ) + spacer);
String uidl = folder.getNextElement(showUIDL); if (hasHeader) {
String uidl;
if (folder.getCurrentSelector() != null)
uidl = folder.getNextSelectedElement(showUIDL);
else
uidl = folder.getNextElement(showUIDL); uidl = folder.getNextElement(showUIDL);
String text = _t("Next"); String text = _t("Next");
if (uidl == null || folder.isLastElement(showUIDL)) { if (uidl == null || folder.isLastElement(showUIDL)) {

View File

@@ -244,6 +244,30 @@ div.topbuttons br {
border-radius: 15px; border-radius: 15px;
} }
#searchbox {
background: #f8f8ff url(/themes/console/images/buttons/search.png) 7px center no-repeat !important;
margin: 2px 4px 2px 24px !important;
padding: 4px 32px 4px 32px !important;
color: #47475f;
}
#searchbox:focus, #searchbox:active {
color: #19191f;
}
#searchcancel {
background: url(../images/delete.png) 0px center no-repeat;
margin: 9px 4px 2px 2px;
padding: 6px 12px;
color: transparent;
border: none;
float: left;
}
#searchcancel.disabled {
display: none;
}
table#pagenav { table#pagenav {
float: right; float: right;
width: 200px; width: 200px;

View File

@@ -959,7 +959,7 @@ hr {
} }
#composemail table { #composemail table {
width: auto; width: 90%;
margin: auto; margin: auto;
} }
@@ -1187,6 +1187,30 @@ h3#config {
float: none !important; float: none !important;
} }
#searchbox {
background: #f8f8ff url(/themes/console/images/buttons/search.png) 7px center no-repeat !important;
margin: 2px 4px 2px 24px !important;
padding: 4px 32px 4px 32px !important;
color: #47475f;
}
#searchbox:focus, #searchbox:active {
color: #19191f;
}
#searchcancel {
background: url(../images/delete.png) 0px center no-repeat;
margin: 9px 4px 2px 2px;
padding: 6px 12px;
color: transparent;
border: none;
float: left;
}
#searchcancel.disabled {
display: none;
}
input.moveto { input.moveto {
float: left; float: left;
margin-left: 14px; margin-left: 14px;