Files
reactive-mail-server/backend/src/main/java/com/mailserver/service/MessageSources.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

39 lines
1.3 KiB
Java

package com.mailserver.service;
import org.springframework.r2dbc.core.DatabaseClient;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
/**
* The message exactly as received or appended (table message_sources). IMAP serves it as BODY[]; a message without
* one gets its MIME text built once ({@link com.mailserver.imap.MimeSource}). A changed draft loses it.
*/
@Component
public class MessageSources {
private final DatabaseClient db;
public MessageSources(DatabaseClient db) {
this.db = db;
}
public Mono<byte[]> get(Long messageId) {
return db.sql("SELECT raw FROM message_sources WHERE message_id = :id")
.bind("id", messageId)
.map(row -> row.get("raw", byte[].class))
.one();
}
public Mono<Void> put(Long messageId, byte[] raw) {
return db.sql("INSERT INTO message_sources (message_id, raw) VALUES (:id, :raw) "
+ "ON CONFLICT (message_id) DO UPDATE SET raw = EXCLUDED.raw")
.bind("id", messageId)
.bind("raw", raw)
.then();
}
public Mono<Void> remove(Long messageId) {
return db.sql("DELETE FROM message_sources WHERE message_id = :id").bind("id", messageId).then();
}
}