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:
@@ -59,7 +59,8 @@ public class MailService {
|
||||
public MailService(MessageRepository messageRepository, FolderRepository folderRepository,
|
||||
UserRepository userRepository, AttachmentRepository attachmentRepository,
|
||||
ContactRepository contactRepository,
|
||||
SpamFilterService spamFilterService) {
|
||||
SpamFilterService spamFilterService, MessageSources sources) {
|
||||
this.sources = sources;
|
||||
this.messageRepository = messageRepository;
|
||||
this.folderRepository = folderRepository;
|
||||
this.userRepository = userRepository;
|
||||
@@ -69,6 +70,8 @@ public class MailService {
|
||||
}
|
||||
|
||||
/** System folders in the order the web client shows them; the rest — own folders, by name. */
|
||||
private final MessageSources sources;
|
||||
|
||||
public static final List<String> SYSTEM_FOLDERS = List.of("INBOX", "Drafts", "Sent", "Spam", "Trash");
|
||||
|
||||
/** Messages per folder in one list — the web client sorts and pages them itself. */
|
||||
@@ -212,6 +215,8 @@ public class MailService {
|
||||
message.setMessageSize((long) (subject.length() + body.length()));
|
||||
return messageRepository.save(message);
|
||||
})
|
||||
// IMAP: the draft changed — its MIME text is built anew
|
||||
.flatMap(saved -> sources.remove(saved.getId()).thenReturn(saved))
|
||||
.flatMap(this::withAttachmentCount);
|
||||
}
|
||||
|
||||
@@ -226,6 +231,7 @@ public class MailService {
|
||||
/** After sending: the draft's attachments move to the copy in Sent, the draft itself is removed. */
|
||||
public Mono<Void> finishDraft(Long draftId, Long sentMessageId) {
|
||||
return attachmentRepository.moveAll(draftId, sentMessageId)
|
||||
.then(sources.remove(sentMessageId))
|
||||
.then(messageRepository.deleteById(draftId));
|
||||
}
|
||||
|
||||
@@ -467,7 +473,7 @@ public class MailService {
|
||||
attachment.setContentType(contentType);
|
||||
attachment.setSizeBytes(sizeBytes);
|
||||
attachment.setFilePath(filePath);
|
||||
return attachmentRepository.save(attachment);
|
||||
return sources.remove(messageId).then(attachmentRepository.save(attachment));
|
||||
})
|
||||
.map(this::toAttachmentResponse);
|
||||
}
|
||||
@@ -491,7 +497,7 @@ public class MailService {
|
||||
return ownedAttachment(attachmentId, userId)
|
||||
.flatMap(attachment -> {
|
||||
deleteFile(attachment.getFilePath());
|
||||
return attachmentRepository.delete(attachment);
|
||||
return sources.remove(attachment.getMessageId()).then(attachmentRepository.delete(attachment));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user