Files
reactive-mail-server/backend/src/main/java/com/mailserver/imap/ImapReader.java
T
Liosha84 b72be91a8b 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>
2026-11-05 20:45:06 +01:00

195 lines
5.3 KiB
Java

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;
}
}