Java tutorial
/* * ***** BEGIN LICENSE BLOCK ***** * Zimbra Collaboration Suite Server * Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2013, 2014, 2015, 2016 Synacor, Inc. * * This program 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, * version 2 of the License. * * This program 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. * If not, see <https://www.gnu.org/licenses/>. * ***** END LICENSE BLOCK ***** */ package com.zimbra.cs.imap; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.codec.binary.Base64; import com.google.common.base.Charsets; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.zimbra.common.localconfig.LC; import com.zimbra.common.util.ZimbraLog; import com.zimbra.cs.imap.ImapSearch.AllSearch; import com.zimbra.cs.imap.ImapSearch.AndOperation; import com.zimbra.cs.imap.ImapSearch.ContentSearch; import com.zimbra.cs.imap.ImapSearch.DateSearch; import com.zimbra.cs.imap.ImapSearch.FlagSearch; import com.zimbra.cs.imap.ImapSearch.HeaderSearch; import com.zimbra.cs.imap.ImapSearch.LogicalOperation; import com.zimbra.cs.imap.ImapSearch.ModifiedSearch; import com.zimbra.cs.imap.ImapSearch.NotOperation; import com.zimbra.cs.imap.ImapSearch.OrOperation; import com.zimbra.cs.imap.ImapSearch.RelativeDateSearch; import com.zimbra.cs.imap.ImapSearch.SequenceSearch; import com.zimbra.cs.imap.ImapSearch.SizeSearch; /** * @since Apr 30, 2005 */ abstract class ImapRequest { private static final boolean[] TAG_CHARS = new boolean[128]; private static final boolean[] ATOM_CHARS = new boolean[128]; private static final boolean[] ASTRING_CHARS = new boolean[128]; private static final boolean[] PATTERN_CHARS = new boolean[128]; private static final boolean[] FETCH_CHARS = new boolean[128]; private static final boolean[] NUMBER_CHARS = new boolean[128]; private static final boolean[] SEQUENCE_CHARS = new boolean[128]; private static final boolean[] BASE64_CHARS = new boolean[128]; private static final boolean[] SEARCH_CHARS = new boolean[128]; static { for (int i = 0x21; i < 0x7F; i++) { if (i != '(' && i != ')' && i != '{' && i != '%' && i != '*' && i != '"' && i != '\\') { SEARCH_CHARS[i] = FETCH_CHARS[i] = PATTERN_CHARS[i] = ASTRING_CHARS[i] = ATOM_CHARS[i] = TAG_CHARS[i] = true; } } ATOM_CHARS[']'] = false; TAG_CHARS['+'] = false; PATTERN_CHARS['%'] = PATTERN_CHARS['*'] = true; FETCH_CHARS['['] = false; SEARCH_CHARS['*'] = true; for (int i = 'a'; i <= 'z'; i++) { BASE64_CHARS[i] = true; } for (int i = 'A'; i <= 'Z'; i++) { BASE64_CHARS[i] = true; } for (int i = '0'; i <= '9'; i++) { BASE64_CHARS[i] = NUMBER_CHARS[i] = SEQUENCE_CHARS[i] = true; } SEQUENCE_CHARS['*'] = SEQUENCE_CHARS[':'] = SEQUENCE_CHARS[','] = SEQUENCE_CHARS['$'] = true; BASE64_CHARS['+'] = BASE64_CHARS['/'] = true; } private static final int LAST_PUNCT = 0; private static final int LAST_DIGIT = 1; private static final int LAST_STAR = 2; private static final Set<String> SYSTEM_FLAGS = ImmutableSet.of("ANSWERED", "FLAGGED", "DELETED", "SEEN", "DRAFT"); private static final Map<String, String> NEGATED_SEARCH = ImmutableMap.<String, String>builder() .put("ANSWERED", "UNANSWERED").put("DELETED", "UNDELETED").put("DRAFT", "UNDRAFT") .put("FLAGGED", "UNFLAGGED").put("KEYWORD", "UNKEYWORD").put("RECENT", "OLD").put("OLD", "RECENT") .put("SEEN", "UNSEEN").put("UNANSWERED", "ANSWERED").put("UNDELETED", "DELETED").put("UNDRAFT", "DRAFT") .put("UNFLAGGED", "FLAGGED").put("UNKEYWORD", "KEYWORD").put("UNSEEN", "SEEN").build(); private static final boolean SINGLE_CLAUSE = true; private static final boolean MULTIPLE_CLAUSES = false; private static final String SUBCLAUSE = ""; private static final Map<String, Integer> MONTH_NUMBER; static { ImmutableMap.Builder<String, Integer> builder = ImmutableMap.builder(); String[] names = { "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" }; for (int i = 0; i < names.length; i++) { builder.put(names[i], i); } MONTH_NUMBER = builder.build(); } static final boolean NONZERO = false; static final boolean ZERO_OK = true; final ImapHandler mHandler; String tag; List<Part> parts = new ArrayList<Part>(); int index; int offset; private boolean isAppend; private boolean isLogin; private final int maxNestingInSearchRequest; ImapRequest(ImapHandler handler) { mHandler = handler; maxNestingInSearchRequest = LC.imap_max_nesting_in_search_request.intValue(); } ImapRequest rewind() { index = offset = 0; return this; } protected abstract class Part { abstract int size(); abstract byte[] getBytes() throws IOException; abstract String getString() throws ImapParseException; abstract Literal getLiteral() throws ImapParseException; boolean isString() { return false; } boolean isLiteral() { return false; } void cleanup() { } } private final class StringPart extends Part { private final String str; StringPart(String s) { str = s; } @Override int size() { return str.length(); } @Override byte[] getBytes() { return str.getBytes(); } @Override boolean isString() { return true; } @Override String getString() { return str; } @Override public String toString() { return str; } @Override Literal getLiteral() throws ImapParseException { throw new ImapParseException(tag, "not inside literal"); } } private final class LiteralPart extends Part { private final Literal lit; LiteralPart(Literal l) { lit = l; } @Override int size() { return lit.size(); } @Override byte[] getBytes() throws IOException { return lit.getBytes(); } @Override boolean isLiteral() { return true; } @Override Literal getLiteral() { return lit; } @Override void cleanup() { lit.cleanup(); } @Override public String getString() throws ImapParseException { throw new ImapParseException(tag, "not inside string"); } @Override public String toString() { try { return new String(lit.getBytes(), Charsets.US_ASCII); } catch (IOException e) { return "???"; } } } void addPart(Literal literal) { addPart(new LiteralPart(literal)); } void addPart(String line) { if (parts.isEmpty()) { String cmd = getCommand(line); if ("APPEND".equalsIgnoreCase(cmd)) { isAppend = true; } else if ("LOGIN".equalsIgnoreCase(cmd)) { isLogin = true; } } addPart(new StringPart(line)); } void addPart(Part part) { parts.add(part); } void cleanup() { for (Part part : parts) { part.cleanup(); } parts.clear(); } protected boolean isAppend() { return isAppend; } protected boolean isLogin() { return isLogin; } protected String getCommand(String requestLine) { int i = requestLine.indexOf(' ') + 1; if (i > 0) { int j = requestLine.indexOf(' ', i); if (j > 0) { return requestLine.substring(i, j); } } return null; } String getCurrentLine() throws ImapParseException { return parts.get(index).getString(); } byte[] toByteArray() throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); for (Part part : parts) { byte[] content = part.getBytes(); baos.write(content, 0, content.length); if (part.isString()) { baos.write(ImapHandler.LINE_SEPARATOR_BYTES, 0, 2); } } return baos.toByteArray(); } String getTag() { if (tag == null && index == 0 && offset == 0 && parts.size() > 0) { try { readTag(); } catch (ImapParseException ignore) { } index = 0; offset = 0; } return tag; } /** * Returns whether the specified IMAP extension is enabled for this session. * * @see ImapHandler#extensionEnabled(String) */ boolean extensionEnabled(String extension) { return mHandler == null || mHandler.extensionEnabled(extension); } /** * Records the "tag" for the request. This tag will later be used to indicate that the server has finished * processing the request. It may also be used when generating a parse exception. */ void setTag(String value) { tag = value; } String readContent(boolean[] acceptable) throws ImapParseException { return readContent(acceptable, false); } String readContent(boolean[] acceptable, boolean emptyOK) throws ImapParseException { String content = getCurrentLine(); int i; for (i = offset; i < content.length(); i++) { char c = content.charAt(i); if (c > 0x7F || !acceptable[c]) { break; } } if (i == offset && !emptyOK) { throw new ImapParseException(tag, "zero-length content"); } String result = content.substring(offset, i); offset += result.length(); return result; } /** * Returns whether the read position is at the very end of the request. */ boolean eof() { return index >= parts.size() || offset >= parts.get(index).size(); } /** * Returns the character at the read position, or -1 if we're at the end of a literal or of a line. */ int peekChar() throws ImapParseException { if (index >= parts.size()) { return -1; } String str = parts.get(index).getString(); return offset < str.length() ? str.charAt(offset) : -1; } String peekATOM() { int i = index; int o = offset; try { return readATOM(); } catch (ImapParseException ipe) { return null; } finally { index = i; offset = o; } } void skipSpace() throws ImapParseException { skipChar(' '); } void skipChar(char c) throws ImapParseException { if (index >= parts.size()) { throw new ImapParseException(tag, "unexpected end of line; expected '" + c + "'"); } String str = parts.get(index).getString(); if (offset >= str.length()) { throw new ImapParseException(tag, "unexpected end of line; expected '" + c + "'"); } char got = str.charAt(offset); if (got == c) { offset++; } else { throw new ImapParseException(tag, "wrong character; expected '" + c + "' but got '" + got + "'"); } } void skipNIL() throws ImapParseException { skipAtom("NIL"); } void skipAtom(String atom) throws ImapParseException { if (!readATOM().equals(atom)) { throw new ImapParseException(tag, "did not find expected " + atom); } } String readAtom() throws ImapParseException { return readContent(ATOM_CHARS); } String readATOM() throws ImapParseException { return readContent(ATOM_CHARS).toUpperCase(); } String readQuoted(Charset charset) throws ImapParseException { String result = readQuoted(); if (charset == null || Charsets.ISO_8859_1.equals(charset) || Charsets.US_ASCII.equals(charset)) { return result; } else { return new String(result.getBytes(Charsets.ISO_8859_1), charset); } } String readQuoted() throws ImapParseException { String content = getCurrentLine(); StringBuilder result = null; skipChar('"'); int backslash = offset - 1; boolean escaped = false; for (int i = offset; i < content.length(); i++) { char c = content.charAt(i); if (c > 0x7F || c == 0x00 || c == '\r' || c == '\n' || (escaped && c != '\\' && c != '"')) { throw new ImapParseException(tag, "illegal character '" + c + "' in quoted string"); } else if (!escaped && c == '\\') { if (result == null) { result = new StringBuilder(); } result.append(content.substring(backslash + 1, i)); backslash = i; escaped = true; } else if (!escaped && c == '"') { offset = i + 1; String range = content.substring(backslash + 1, i); return (result == null ? range : result.append(range).toString()); } else { escaped = false; } } throw new ImapParseException(tag, "unexpected end of line in quoted string"); } abstract Literal readLiteral() throws IOException, ImapParseException; private String readLiteral(Charset charset) throws IOException, ImapParseException { return new String(readLiteral().getBytes(), charset); } Literal readLiteral8() throws IOException, ImapParseException { if (peekChar() == '~' && extensionEnabled("BINARY")) { skipChar('~'); } return readLiteral(); } String readAstring() throws IOException, ImapParseException { return readAstring(null); } String readAstring(Charset charset) throws IOException, ImapParseException { return readAstring(charset, ASTRING_CHARS); } private String readAstring(Charset charset, boolean[] acceptable) throws IOException, ImapParseException { int c = peekChar(); if (c == -1) { throw new ImapParseException(tag, "unexpected end of line"); } else if (c == '{') { return readLiteral(charset != null ? charset : Charsets.UTF_8); } else if (c != '"') { return readContent(acceptable); } else { return readQuoted(charset); } } private String readAquoted() throws ImapParseException { int c = peekChar(); if (c == -1) { throw new ImapParseException(tag, "unexpected end of line"); } else if (c != '"') { return readContent(ASTRING_CHARS); } else { return readQuoted(); } } private String readString(Charset charset) throws IOException, ImapParseException { int c = peekChar(); if (c == -1) { throw new ImapParseException(tag, "unexpected end of line"); } else if (c == '{') { return readLiteral(charset != null ? charset : Charsets.UTF_8); } else { return readQuoted(charset); } } private String readNstring(Charset charset) throws IOException, ImapParseException { int c = peekChar(); if (c == -1) { throw new ImapParseException(tag, "unexpected end of line"); } else if (c == '{') { return readLiteral(charset != null ? charset : Charsets.UTF_8); } else if (c != '"') { skipNIL(); return null; } else { return readQuoted(charset); } } String readTag() throws ImapParseException { return tag = readContent(TAG_CHARS); } static String parseTag(String src) throws ImapParseException { int i; for (i = 0; i < src.length(); i++) { char c = src.charAt(i); if (c > 0x7F || !TAG_CHARS[c]) { break; } } if (i > 0) { return src.substring(0, i); } else { throw new ImapParseException(); } } String readNumber() throws ImapParseException { return readNumber(ZERO_OK); } String readNumber(boolean zeroOK) throws ImapParseException { String number = readContent(NUMBER_CHARS); if (number.startsWith("0") && (!zeroOK || number.length() > 1)) { throw new ImapParseException(tag, "invalid number: " + number); } return number; } int parseInteger(String number) throws ImapParseException { try { return Integer.parseInt(number); } catch (NumberFormatException nfe) { throw new ImapParseException(tag, "number out of range: " + number); } } long parseLong(String number) throws ImapParseException { try { return Long.parseLong(number); } catch (NumberFormatException nfe) { throw new ImapParseException(tag, "number out of range: " + number); } } byte[] readBase64(boolean skipEquals) throws ImapParseException { // in some cases, "=" means to just return null and be done with it if (skipEquals && peekChar() == '=') { skipChar('='); return null; } String encoded = readContent(BASE64_CHARS, true); int padding = (4 - (encoded.length() % 4)) % 4; if (padding == 3) { throw new ImapParseException(tag, "invalid base64-encoded content"); } while (padding-- > 0) { skipChar('='); encoded += "="; } return new Base64().decode(encoded.getBytes(Charsets.US_ASCII)); } String readSequence(boolean specialsOK) throws ImapParseException { return validateSequence(readContent(SEQUENCE_CHARS), specialsOK); } String readSequence() throws ImapParseException { return validateSequence(readContent(SEQUENCE_CHARS), true); } private String validateSequence(String value, boolean specialsOK) throws ImapParseException { // "$" is OK per RFC 5182 [SEARCHRES] if (value.equals("$") && specialsOK && extensionEnabled("SEARCHRES")) { return value; } int i, last = LAST_PUNCT; boolean colon = false; for (i = 0; i < value.length(); i++) { char c = value.charAt(i); if (c > 0x7F || c == '$' || !SEQUENCE_CHARS[c] || (c == '*' && !specialsOK)) { throw new ImapParseException(tag, "illegal character '" + c + "' in sequence"); } else if (c == '*') { if (last == LAST_DIGIT) { throw new ImapParseException(tag, "malformed sequence"); } last = LAST_STAR; } else if (c == ',') { if (last == LAST_PUNCT) { throw new ImapParseException(tag, "malformed sequence"); } last = LAST_PUNCT; colon = false; } else if (c == ':') { if (colon || last == LAST_PUNCT) { throw new ImapParseException(tag, "malformed sequence"); } last = LAST_PUNCT; colon = true; } else { if (last == LAST_STAR || c == '0' && last == LAST_PUNCT) { throw new ImapParseException(tag, "malformed sequence"); } last = LAST_DIGIT; } } if (last == LAST_PUNCT) { throw new ImapParseException(tag, "malformed sequence"); } return value; } String readFolder() throws IOException, ImapParseException { return readFolder(false); } String readFolderPattern() throws IOException, ImapParseException { return readFolder(true); } private String readFolder(boolean isPattern) throws IOException, ImapParseException { String raw = readAstring(null, isPattern ? PATTERN_CHARS : ASTRING_CHARS); if (raw == null || raw.indexOf("&") == -1) return raw; try { return ImapPath.FOLDER_ENCODING_CHARSET.decode(ByteBuffer.wrap(raw.getBytes(Charsets.US_ASCII))) .toString(); } catch (Exception e) { ZimbraLog.imap.debug("ignoring error while decoding folder name: %s", raw, e); return raw; } } List<String> readFlags() throws ImapParseException { List<String> tags = new ArrayList<String>(); String content = getCurrentLine(); boolean parens = (peekChar() == '('); if (parens) { skipChar('('); } else if (offset == content.length()) { throw new ImapParseException(tag, "missing flag list"); } if (!parens || peekChar() != ')') { while (offset < content.length()) { if (!tags.isEmpty()) { skipSpace(); } if (peekChar() == '\\') { skipChar('\\'); String name = readAtom(); if (!SYSTEM_FLAGS.contains(name.toUpperCase())) { throw new ImapParseException(tag, "invalid flag: \\" + name); } tags.add('\\' + name); } else { tags.add(readAtom()); } if (parens && peekChar() == ')') { break; } } } if (parens) { skipChar(')'); } return tags; } private Date readDate() throws ImapParseException { return readDate(false, false); } Date readDate(boolean datetime, boolean checkRange) throws ImapParseException { String dateStr = (peekChar() == '"' ? readQuoted() : readAtom()); if (dateStr.length() < (datetime ? 26 : 10)) { throw new ImapParseException(tag, "invalid date format"); } Calendar cal = new GregorianCalendar(); cal.clear(); int pos = 0, count; if (datetime && dateStr.charAt(0) == ' ') { pos++; } count = 2 - pos - (datetime || dateStr.charAt(1) != '-' ? 0 : 1); validateDigits(dateStr, pos, count, cal, Calendar.DAY_OF_MONTH); pos += count; validateChar(dateStr, pos, '-'); pos++; validateMonth(dateStr, pos, cal); pos += 3; validateChar(dateStr, pos, '-'); pos++; validateDigits(dateStr, pos, 4, cal, Calendar.YEAR); pos += 4; if (datetime) { validateChar(dateStr, pos, ' '); pos++; validateDigits(dateStr, pos, 2, cal, Calendar.HOUR); pos += 2; validateChar(dateStr, pos, ':'); pos++; validateDigits(dateStr, pos, 2, cal, Calendar.MINUTE); pos += 2; validateChar(dateStr, pos, ':'); pos++; validateDigits(dateStr, pos, 2, cal, Calendar.SECOND); pos += 2; validateChar(dateStr, pos, ' '); pos++; boolean zonesign = dateStr.charAt(pos) == '+'; validateChar(dateStr, pos, zonesign ? '+' : '-'); pos++; int zonehrs = validateDigits(dateStr, pos, 2, cal, -1); pos += 2; int zonemins = validateDigits(dateStr, pos, 2, cal, -1); pos += 2; cal.set(Calendar.ZONE_OFFSET, (zonesign ? 1 : -1) * (60 * zonehrs + zonemins) * 60000); cal.set(Calendar.DST_OFFSET, 0); } if (pos != dateStr.length()) { throw new ImapParseException(tag, "excess characters at end of date string"); } Date date = cal.getTime(); if (checkRange && date.getTime() < 0) { throw new ImapParseException(tag, "date out of range"); } return date; } private int validateDigits(String str, int pos, int count, Calendar cal, int field) throws ImapParseException { if (str.length() < pos + count) { throw new ImapParseException(tag, "unexpected end of date string"); } int value = 0; for (int i = 0; i < count; i++) { char c = str.charAt(pos + i); if (c < '0' || c > '9') { throw new ImapParseException(tag, "invalid digit in date string"); } value = value * 10 + (c - '0'); } if (field >= 0) { cal.set(field, value); } return value; } private void validateChar(String str, int pos, char c) throws ImapParseException { if (str.length() < pos + 1) { throw new ImapParseException(tag, "unexpected end of date string"); } if (str.charAt(pos) != c) { throw new ImapParseException(tag, "unexpected character in date string"); } } private void validateMonth(String str, int pos, Calendar cal) throws ImapParseException { Integer month = MONTH_NUMBER.get(str.substring(pos, pos + 3).toUpperCase()); if (month == null) { throw new ImapParseException(tag, "invalid month string"); } cal.set(Calendar.MONTH, month); } Map<String, String> readParameters(boolean nil) throws IOException, ImapParseException { if (peekChar() != '(') { if (!nil) { throw new ImapParseException(tag, "did not find expected '('"); } skipNIL(); return null; } Map<String, String> params = new HashMap<String, String>(); skipChar('('); do { String name = readString(Charsets.UTF_8); skipSpace(); params.put(name, readNstring(Charsets.UTF_8)); if (peekChar() == ')') { break; } skipSpace(); } while (true); skipChar(')'); return params; } int readFetch(List<ImapPartSpecifier> parts) throws IOException, ImapParseException { boolean list = peekChar() == '('; int attributes = 0; if (list) skipChar('('); do { String item = readContent(FETCH_CHARS).toUpperCase(); if (!list && item.equals("ALL")) { attributes = ImapHandler.FETCH_ALL; } else if (!list && item.equals("FULL")) { attributes = ImapHandler.FETCH_FULL; } else if (!list && item.equals("FAST")) { attributes = ImapHandler.FETCH_FAST; } else if (item.equals("BODY") && peekChar() != '[') { attributes |= ImapHandler.FETCH_BODY; } else if (item.equals("BODYSTRUCTURE")) { attributes |= ImapHandler.FETCH_BODYSTRUCTURE; } else if (item.equals("ENVELOPE")) { attributes |= ImapHandler.FETCH_ENVELOPE; } else if (item.equals("FLAGS")) { attributes |= ImapHandler.FETCH_FLAGS; } else if (item.equals("INTERNALDATE")) { attributes |= ImapHandler.FETCH_INTERNALDATE; } else if (item.equals("UID")) { attributes |= ImapHandler.FETCH_UID; } else if (item.equals("MODSEQ") && extensionEnabled("CONDSTORE")) { attributes |= ImapHandler.FETCH_MODSEQ; } else if (item.equals("RFC822.SIZE")) { attributes |= ImapHandler.FETCH_RFC822_SIZE; } else if (item.equals("RFC822.HEADER")) { parts.add(new ImapPartSpecifier(item, "", "HEADER")); } else if (item.equals("RFC822")) { attributes |= ImapHandler.FETCH_MARK_READ; parts.add(new ImapPartSpecifier(item, "", "")); } else if (item.equals("RFC822.TEXT")) { attributes |= ImapHandler.FETCH_MARK_READ; parts.add(new ImapPartSpecifier(item, "", "TEXT")); } else if (item.equals("BINARY.SIZE") && extensionEnabled("BINARY")) { String sectionPart = ""; skipChar('['); while (peekChar() != ']') { if (!sectionPart.equals("")) { sectionPart += "."; skipChar('.'); } sectionPart += readNumber(NONZERO); } skipChar(']'); if (sectionPart.isEmpty()) { attributes |= ImapHandler.FETCH_BINARY_SIZE; } else { parts.add(new ImapPartSpecifier(item, sectionPart, "")); } } else if (item.equals("BODY") || item.equals("BODY.PEEK") || ((item.equals("BINARY") || item.equals("BINARY.PEEK")) && extensionEnabled("BINARY"))) { if (!item.endsWith(".PEEK")) { attributes |= ImapHandler.FETCH_MARK_READ; } boolean binary = item.startsWith("BINARY"); skipChar('['); ImapPartSpecifier pspec = readPartSpecifier(binary, true); skipChar(']'); if (peekChar() == '<') { try { skipChar('<'); int partialStart = Integer.parseInt(readNumber()); skipChar('.'); int partialCount = Integer.parseInt(readNumber(NONZERO)); skipChar('>'); pspec.setPartial(partialStart, partialCount); } catch (NumberFormatException e) { throw new ImapParseException(tag, "invalid partial fetch specifier"); } } parts.add(pspec); } else { throw new ImapParseException(tag, "unknown FETCH attribute \"" + item + '"'); } if (list && peekChar() != ')') { skipSpace(); } } while (list && peekChar() != ')'); if (list) { skipChar(')'); } return attributes; } ImapPartSpecifier readPartSpecifier(boolean binary, boolean literals) throws ImapParseException, IOException { String sectionPart = "", sectionText = ""; List<String> headers = null; boolean done = false; while (Character.isDigit((char) peekChar())) { sectionPart += (sectionPart.equals("") ? "" : ".") + readNumber(NONZERO); if (!(done = (peekChar() != '.'))) { skipChar('.'); } } if (!done && peekChar() != ']') { if (binary) { throw new ImapParseException(tag, "section-text not permitted for BINARY"); } sectionText = readATOM(); if (sectionText.equals("HEADER.FIELDS") || sectionText.equals("HEADER.FIELDS.NOT")) { headers = new ArrayList<String>(); skipSpace(); skipChar('('); while (peekChar() != ')') { if (!headers.isEmpty()) { skipSpace(); } headers.add((literals ? readAstring() : readAquoted()).toUpperCase()); } if (headers.isEmpty()) { throw new ImapParseException(tag, "header-list may not be empty"); } skipChar(')'); } else if (sectionText.equals("MIME")) { if (sectionPart.isEmpty()) { throw new ImapParseException(tag, "\"MIME\" is not a valid section-spec"); } } else if (!sectionText.equals("HEADER") && !sectionText.equals("TEXT")) { throw new ImapParseException(tag, "unknown section-text \"" + sectionText + '"'); } } ImapPartSpecifier pspec = new ImapPartSpecifier(binary ? "BINARY" : "BODY", sectionPart, sectionText); pspec.setHeaders(headers); return pspec; } private ImapSearch readSearchClause(Charset charset, boolean single, LogicalOperation parent, int depth) throws IOException, ImapParseException { depth++; if (depth > maxNestingInSearchRequest) { ZimbraLog.imap.debug("search nesting too deep (depth=%s) Max allowed=%s", depth, maxNestingInSearchRequest); throw new ImapSearchTooComplexException(tag, "Search query too complex"); } boolean first = true; int nots = 0; do { if (!first) { skipSpace(); } int c = peekChar(); // key will be "" iff we're opening a new subclause... String key = (c == '(' ? SUBCLAUSE : readContent(SEARCH_CHARS).toUpperCase()); LogicalOperation target = parent; if (key.equals("NOT")) { nots++; first = false; continue; } else if ((nots % 2) != 0) { if (NEGATED_SEARCH.containsKey(key)) { key = NEGATED_SEARCH.get(key); } else { parent.addChild(target = new NotOperation()); } } nots = 0; ImapSearch child; if (key.equals("ALL")) { child = new AllSearch(); } else if (key.equals("ANSWERED")) { child = new FlagSearch("\\Answered"); } else if (key.equals("DELETED")) { child = new FlagSearch("\\Deleted"); } else if (key.equals("DRAFT")) { child = new FlagSearch("\\Draft"); } else if (key.equals("FLAGGED")) { child = new FlagSearch("\\Flagged"); } else if (key.equals("RECENT")) { child = new FlagSearch("\\Recent"); } else if (key.equals("NEW")) { child = new AndOperation(new FlagSearch("\\Recent"), new NotOperation(new FlagSearch("\\Seen"))); } else if (key.equals("OLD")) { child = new NotOperation(new FlagSearch("\\Recent")); } else if (key.equals("SEEN")) { child = new FlagSearch("\\Seen"); } else if (key.equals("UNANSWERED")) { child = new NotOperation(new FlagSearch("\\Answered")); } else if (key.equals("UNDELETED")) { child = new NotOperation(new FlagSearch("\\Deleted")); } else if (key.equals("UNDRAFT")) { child = new NotOperation(new FlagSearch("\\Draft")); } else if (key.equals("UNFLAGGED")) { child = new NotOperation(new FlagSearch("\\Flagged")); } else if (key.equals("UNSEEN")) { child = new NotOperation(new FlagSearch("\\Seen")); } else if (key.equals("BCC")) { skipSpace(); child = new HeaderSearch(HeaderSearch.Header.BCC, readAstring(charset)); } else if (key.equals("BEFORE")) { skipSpace(); child = new DateSearch(DateSearch.Relation.before, readDate()); } else if (key.equals("BODY")) { skipSpace(); child = new ContentSearch(readAstring(charset)); } else if (key.equals("CC")) { skipSpace(); child = new HeaderSearch(HeaderSearch.Header.CC, readAstring(charset)); } else if (key.equals("FROM")) { skipSpace(); child = new HeaderSearch(HeaderSearch.Header.FROM, readAstring(charset)); } else if (key.equals("HEADER")) { skipSpace(); HeaderSearch.Header relation = HeaderSearch.Header.parse(readAstring()); skipSpace(); child = new HeaderSearch(relation, readAstring(charset)); } else if (key.equals("KEYWORD")) { skipSpace(); child = new FlagSearch(readAtom()); } else if (key.equals("LARGER")) { skipSpace(); child = new SizeSearch(SizeSearch.Relation.larger, parseLong(readNumber())); } else if (key.equals("MODSEQ") && extensionEnabled("CONDSTORE")) { skipSpace(); if (peekChar() == '"') { readFolder(); skipSpace(); readATOM(); skipSpace(); } child = new ModifiedSearch(parseInteger(readNumber(ZERO_OK))); } else if (key.equals("ON")) { skipSpace(); child = new DateSearch(DateSearch.Relation.date, readDate()); } else if (key.equals("OLDER") && extensionEnabled("WITHIN")) { skipSpace(); child = new RelativeDateSearch(DateSearch.Relation.before, parseInteger(readNumber())); } else if (key.equals("SENTBEFORE")) { // FIXME: SENTBEFORE, SENTON, and SENTSINCE reference INTERNALDATE, not the Date header skipSpace(); child = new DateSearch(DateSearch.Relation.before, readDate()); } else if (key.equals("SENTON")) { skipSpace(); child = new DateSearch(DateSearch.Relation.date, readDate()); } else if (key.equals("SENTSINCE")) { skipSpace(); child = new DateSearch(DateSearch.Relation.after, readDate()); } else if (key.equals("SINCE")) { skipSpace(); child = new DateSearch(DateSearch.Relation.after, readDate()); } else if (key.equals("SMALLER")) { skipSpace(); child = new SizeSearch(SizeSearch.Relation.smaller, parseLong(readNumber())); } else if (key.equals("SUBJECT")) { skipSpace(); child = new HeaderSearch(HeaderSearch.Header.SUBJECT, readAstring(charset)); } else if (key.equals("TEXT")) { skipSpace(); child = new ContentSearch(readAstring(charset)); } else if (key.equals("TO")) { skipSpace(); child = new HeaderSearch(HeaderSearch.Header.TO, readAstring(charset)); } else if (key.equals("UID")) { skipSpace(); child = new SequenceSearch(tag, readSequence(), true); } else if (key.equals("UNKEYWORD")) { skipSpace(); child = new NotOperation(new FlagSearch(readAtom())); } else if (key.equals("YOUNGER") && extensionEnabled("WITHIN")) { skipSpace(); child = new RelativeDateSearch(DateSearch.Relation.after, parseInteger(readNumber())); } else if (key.equals(SUBCLAUSE)) { skipChar('('); child = readSearchClause(charset, MULTIPLE_CLAUSES, new AndOperation(), depth); skipChar(')'); } else if (Character.isDigit(key.charAt(0)) || key.charAt(0) == '*' || key.charAt(0) == '$') { child = new SequenceSearch(tag, validateSequence(key, true), false); } else if (key.equals("OR")) { skipSpace(); child = readSearchClause(charset, SINGLE_CLAUSE, new OrOperation(), depth); skipSpace(); readSearchClause(charset, SINGLE_CLAUSE, (LogicalOperation) child, depth); } else { throw new ImapParseException(tag, "unknown search tag: " + key); } target.addChild(child); first = false; } while (peekChar() != -1 && peekChar() != ')' && (nots > 0 || !single)); if (nots > 0) { throw new ImapParseException(tag, "missing search-key after NOT"); } depth--; return parent; } ImapSearch readSearch(Charset charset) throws IOException, ImapParseException { return readSearchClause(charset, MULTIPLE_CLAUSES, new AndOperation(), 0); } Charset readCharset() throws IOException, ImapParseException { String charset = readAstring(); try { return Charset.forName(charset); } catch (Exception e) { } throw new ImapParseException(tag, "BADCHARSET", "unknown charset: " + charset.replace('\r', ' ').replace('\n', ' '), true); } }