package com.mailserver.imap; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; /** * Reads one IMAP command: the decoder has already collected it whole, with its literals inline * ("{n}\r\n" + n bytes, or "{n+}\r\n" + n bytes). */ final class ImapReader { static final class SyntaxError extends RuntimeException { SyntaxError(String message) { super(message); } } private final byte[] b; private int pos; ImapReader(byte[] command) { this(command, true); } private ImapReader(byte[] command, boolean stripLineEnd) { int end = command.length; if (!stripLineEnd) { this.b = command; return; } if (end > 0 && command[end - 1] == '\n') { end--; } if (end > 0 && command[end - 1] == '\r') { end--; } this.b = java.util.Arrays.copyOf(command, end); } boolean atEnd() { return pos >= b.length; } int peek() { return pos < b.length ? b[pos] & 0xff : -1; } void sp() { if (peek() != ' ') { throw new SyntaxError("Expected a space"); } while (peek() == ' ') { pos++; } } boolean optionalSp() { if (peek() == ' ') { sp(); return true; } return false; } void expect(char c) { if (peek() != c) { throw new SyntaxError("Expected '" + c + "'"); } pos++; } /** Characters up to a space, parenthesis, or the end (an atom; also tags, sequence sets, flags). */ String atom() { int start = pos; while (pos < b.length) { int c = b[pos] & 0xff; if (c == ' ' || c == '(' || c == ')' || c == '{' || c == '"' || c < 0x20 || c == 0x7f) { break; } pos++; } if (pos == start) { throw new SyntaxError("Expected an atom"); } return new String(b, start, pos - start, StandardCharsets.UTF_8); } /** Atom, quoted string or literal. */ String astring() { int c = peek(); if (c == '"' || c == '{') { return new String(stringBytes(), StandardCharsets.UTF_8); } return atom(); } /** Quoted string or literal, raw bytes. */ byte[] stringBytes() { int c = peek(); if (c == '"') { pos++; java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream(); while (true) { if (pos >= b.length) { throw new SyntaxError("Unterminated quoted string"); } byte x = b[pos++]; if (x == '"') { return out.toByteArray(); } if (x == '\\' && pos < b.length) { x = b[pos++]; } out.write(x); } } if (c == '{') { int close = indexOf('}', pos); if (close < 0) { throw new SyntaxError("Bad literal"); } String size = new String(b, pos + 1, close - pos - 1, StandardCharsets.US_ASCII); if (size.endsWith("+")) { size = size.substring(0, size.length() - 1); } int length; try { length = Integer.parseInt(size); } catch (NumberFormatException e) { throw new SyntaxError("Bad literal size"); } pos = close + 1; if (peek() == '\r') { pos++; } expect('\n'); if (pos + length > b.length) { throw new SyntaxError("Literal shorter than announced"); } byte[] data = java.util.Arrays.copyOfRange(b, pos, pos + length); pos += length; return data; } throw new SyntaxError("Expected a string"); } /** "(a b c)" → [a, b, c] (atoms or strings); an atom without parentheses → [atom]. */ List list() { List items = new ArrayList<>(); if (peek() != '(') { items.add(astring()); return items; } pos++; while (peek() == ' ') { pos++; } while (peek() != ')') { if (atEnd()) { throw new SyntaxError("Unterminated list"); } items.add(astring()); while (peek() == ' ') { pos++; } } pos++; return items; } /** Everything left, as text (used for FETCH attributes and SEARCH keys parsed by their own readers). */ String rest() { String text = new String(b, pos, b.length - pos, StandardCharsets.UTF_8); pos = b.length; return text; } /** A reader over the rest, as is (SEARCH may hold literals). */ ImapReader restReader() { ImapReader reader = new ImapReader(java.util.Arrays.copyOfRange(b, pos, b.length), false); pos = b.length; return reader; } private int indexOf(char c, int from) { for (int i = from; i < b.length; i++) { if (b[i] == c) { return i; } } return -1; } }