75.2 IMAP: новый сервер на Reactor Netty (STARTTLS, 993, IDLE, UIDPLUS, MOVE)
Старый IMAP (выключен, .block() на сетевом потоке) удалён, написан заново:
- ImapServer: декодер целых команд с литералами ({n} после "+ Ready", {n+}),
команды по одной на рабочем потоке; STARTTLS (без выполнения открытого текста
после него) и неявный TLS; IDLE с опросом раз в 5 с до DONE
- ImapSession: IMAP4rev1 + LITERAL+, SASL-IR, IDLE, UIDPLUS, MOVE, UNSELECT,
NAMESPACE, SPECIAL-USE, ID; FETCH (ENVELOPE, BODYSTRUCTURE, BODY[section]<partial>),
SEARCH, STORE, COPY/MOVE с COPYUID, APPEND с APPENDUID, EXPUNGE/UID EXPUNGE
- MimeTree: части письма по байтам — размеры BODYSTRUCTURE совпадают с BODY[]
- V4: UID на папку выдаёт триггер (перенос из веба тоже получает новый UID),
флаги \Answered/\Deleted, message_sources — письмо как пришло (SMTP, APPEND)
или собранное один раз (MimeSource); правка черновика сбрасывает его
- тесты: настоящий клиент Angus IMAP, сырые литералы/IDLE/подмешивание (47 всего)
- живая проверка: обновление V3 -> V4 на PostgreSQL с письмами, Python imaplib:
STARTTLS, 993, папки, вложения, поиск, APPEND/COPY/MOVE/EXPUNGE, IDLE — 0 ошибок
α
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
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<String> list() {
|
||||
List<String> 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user