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,59 @@
|
||||
package com.mailserver.config;
|
||||
|
||||
import com.mailserver.imap.DbImapStore;
|
||||
import com.mailserver.imap.ImapServer;
|
||||
import com.mailserver.imap.ImapStore;
|
||||
import com.mailserver.repository.AttachmentRepository;
|
||||
import com.mailserver.repository.MessageRepository;
|
||||
import com.mailserver.repository.UserRepository;
|
||||
import com.mailserver.service.MailService;
|
||||
import com.mailserver.service.MessageSources;
|
||||
import com.mailserver.service.UserService;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.r2dbc.core.DatabaseClient;
|
||||
import org.springframework.util.unit.DataSize;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
/** IMAP (75.2): STARTTLS on mailserver.imap.port, implicit TLS on mailserver.imaps.port when set. */
|
||||
@Configuration
|
||||
public class ImapConfig {
|
||||
|
||||
@Bean
|
||||
public ImapStore imapStore(DatabaseClient db, UserRepository users, UserService userService, MailService mailService,
|
||||
MessageRepository messages, AttachmentRepository attachments, MessageSources sources,
|
||||
@Value("${mailserver.smtp.hostname:localhost}") String hostname) {
|
||||
return new DbImapStore(db, users, userService, mailService, messages, attachments, sources, hostname);
|
||||
}
|
||||
|
||||
@Bean(initMethod = "start", destroyMethod = "stop")
|
||||
@ConditionalOnProperty(name = "mailserver.imap.enabled", havingValue = "true", matchIfMissing = true)
|
||||
public ImapServer imapServer(ImapStore store, MailServerConfig.SmtpTlsContext tls,
|
||||
@Value("${mailserver.imap.host:0.0.0.0}") String host,
|
||||
@Value("${mailserver.imap.port:3143}") int port,
|
||||
@Value("${mailserver.smtp.hostname:localhost}") String hostname,
|
||||
@Value("${mailserver.imap.auth-without-tls:false}") boolean authWithoutTls,
|
||||
@Value("${mailserver.imap.max-literal:50MB}") DataSize maxLiteral,
|
||||
@Value("${mailserver.imap.idle-timeout:30m}") Duration idleTimeout,
|
||||
@Value("${mailserver.imap.idle-poll:5s}") Duration idlePoll) {
|
||||
return new ImapServer(new ImapServer.Settings(host, port, hostname, authWithoutTls, tls.context(), false,
|
||||
(int) maxLiteral.toBytes(), idleTimeout, idlePoll), store);
|
||||
}
|
||||
|
||||
@Bean(initMethod = "start", destroyMethod = "stop")
|
||||
@ConditionalOnExpression("'${mailserver.imap.enabled:true}' == 'true' and ${mailserver.imaps.port:0} > 0 and ${mailserver.tls.enabled:true}")
|
||||
public ImapServer imapsServer(ImapStore store, MailServerConfig.SmtpTlsContext tls,
|
||||
@Value("${mailserver.imap.host:0.0.0.0}") String host,
|
||||
@Value("${mailserver.imaps.port}") int port,
|
||||
@Value("${mailserver.smtp.hostname:localhost}") String hostname,
|
||||
@Value("${mailserver.imap.max-literal:50MB}") DataSize maxLiteral,
|
||||
@Value("${mailserver.imap.idle-timeout:30m}") Duration idleTimeout,
|
||||
@Value("${mailserver.imap.idle-poll:5s}") Duration idlePoll) {
|
||||
return new ImapServer(new ImapServer.Settings(host, port, hostname, false, tls.context(), true,
|
||||
(int) maxLiteral.toBytes(), idleTimeout, idlePoll), store);
|
||||
}
|
||||
}
|
||||
@@ -1,580 +0,0 @@
|
||||
package com.mailserver.config;
|
||||
|
||||
import com.mailserver.repository.FolderRepository;
|
||||
import com.mailserver.repository.MessageRepository;
|
||||
import com.mailserver.repository.UserRepository;
|
||||
import com.mailserver.service.UserService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.PrintWriter;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Встроенный IMAP-сервер для почтового сервера.
|
||||
* Поддерживает основные команды IMAP4rev1: LOGIN, SELECT, FETCH, SEARCH, STORE, EXPUNGE, LOGOUT.
|
||||
* Запускается при старте приложения и останавливается при завершении.
|
||||
*/
|
||||
@Configuration
|
||||
public class ImapServerConfig {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ImapServerConfig.class);
|
||||
|
||||
@Value("${mailserver.imap.enabled:true}")
|
||||
private boolean enabled;
|
||||
|
||||
@Value("${mailserver.imap.port:143}")
|
||||
private int port;
|
||||
|
||||
@Value("${mailserver.imap.hostname:localhost}")
|
||||
private String hostname;
|
||||
|
||||
private final UserRepository userRepository;
|
||||
private final FolderRepository folderRepository;
|
||||
private final MessageRepository messageRepository;
|
||||
private final UserService userService;
|
||||
|
||||
private ServerSocket serverSocket;
|
||||
private ExecutorService executorService;
|
||||
|
||||
public ImapServerConfig(UserRepository userRepository, FolderRepository folderRepository,
|
||||
MessageRepository messageRepository, UserService userService) {
|
||||
this.userRepository = userRepository;
|
||||
this.folderRepository = folderRepository;
|
||||
this.messageRepository = messageRepository;
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void start() {
|
||||
if (!enabled) {
|
||||
log.info("IMAP-сервер отключён (mailserver.imap.enabled=false)");
|
||||
return;
|
||||
}
|
||||
|
||||
executorService = Executors.newCachedThreadPool();
|
||||
|
||||
try {
|
||||
serverSocket = new ServerSocket(port);
|
||||
log.info("IMAP-сервер запущен на порту {} (хост: {})", port, hostname);
|
||||
|
||||
// Принимаем подключения в цикле
|
||||
while (!serverSocket.isClosed()) {
|
||||
try {
|
||||
Socket clientSocket = serverSocket.accept();
|
||||
executorService.submit(() -> handleClient(clientSocket));
|
||||
} catch (IOException e) {
|
||||
if (!serverSocket.isClosed()) {
|
||||
log.error("Ошибка при принятии подключения", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
if (serverSocket == null || !serverSocket.isClosed()) {
|
||||
log.error("Не удалось запустить IMAP-сервер на порту {}", port, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void stop() {
|
||||
log.info("Остановка IMAP-сервера...");
|
||||
try {
|
||||
if (serverSocket != null && !serverSocket.isClosed()) {
|
||||
serverSocket.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("Ошибка при остановке IMAP-сервера", e);
|
||||
}
|
||||
|
||||
if (executorService != null) {
|
||||
executorService.shutdown();
|
||||
try {
|
||||
if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||
executorService.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
executorService.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
log.info("IMAP-сервер остановлен");
|
||||
}
|
||||
|
||||
private void handleClient(Socket socket) {
|
||||
String clientAddress = socket.getInetAddress().getHostAddress();
|
||||
log.info("Новое IMAP-подключение от {}", clientAddress);
|
||||
|
||||
try (socket;
|
||||
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
|
||||
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8))) {
|
||||
|
||||
// Отправляем приветствие
|
||||
out.println("* OK IMAP4rev1 Server Ready");
|
||||
|
||||
String username = null;
|
||||
String line;
|
||||
|
||||
while (username == null && (line = in.readLine()) != null) {
|
||||
String[] parts = parseCommand(line);
|
||||
if (parts == null) continue;
|
||||
|
||||
String tag = parts[0];
|
||||
String command = parts[1].toUpperCase();
|
||||
|
||||
switch (command) {
|
||||
case "LOGIN" -> {
|
||||
if (parts.length >= 4) {
|
||||
String email = parts[2].toLowerCase();
|
||||
String password = parts[3];
|
||||
|
||||
// Проверяем пароль через BCrypt
|
||||
com.mailserver.domain.User user = userRepository.findByEmail(email).block();
|
||||
if (user != null && userService.checkPassword(user, password)) {
|
||||
username = email;
|
||||
out.println(tag + " OK LOGIN completed");
|
||||
log.info("Пользователь {} успешно авторизован через IMAP", email);
|
||||
} else {
|
||||
out.println(tag + " NO LOGIN failed: invalid credentials");
|
||||
log.warn("Неудачная попытка IMAP-авторизации для пользователя {}", email);
|
||||
}
|
||||
} else {
|
||||
out.println(tag + " BAD Invalid LOGIN command");
|
||||
}
|
||||
}
|
||||
case "CAPABILITY" -> {
|
||||
out.println("* CAPABILITY IMAP4rev1 UNSELECT IDLE NAMESPACE LITERAL+");
|
||||
out.println(tag + " OK Capability completed");
|
||||
}
|
||||
case "LOGOUT" -> {
|
||||
out.println("* BYE IMAP4rev1 server terminating");
|
||||
out.println(tag + " OK LOGOUT completed");
|
||||
return;
|
||||
}
|
||||
default -> {
|
||||
if (username == null) {
|
||||
out.println(tag + " BAD Authentication required");
|
||||
} else {
|
||||
handleAuthenticatedCommand(tag, command, parts, username, out, in);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("Ошибка при обработке IMAP-подключения от {}", clientAddress, e);
|
||||
} finally {
|
||||
log.info("IMAP-подключение от {} закрыто", clientAddress);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleAuthenticatedCommand(String tag, String command, String[] parts,
|
||||
String username, PrintWriter out, BufferedReader in) {
|
||||
switch (command) {
|
||||
case "SELECT" -> handleSelect(tag, parts, username, out);
|
||||
case "EXAMINE" -> handleSelect(tag, parts, username, out); // EXAMINE = SELECT в read-only режиме
|
||||
case "FETCH" -> handleFetch(tag, parts, username, out);
|
||||
case "SEARCH" -> handleSearch(tag, parts, username, out);
|
||||
case "STORE" -> handleStore(tag, parts, username, out);
|
||||
case "EXPUNGE" -> handleExpunge(tag, username, out);
|
||||
case "LIST" -> handleList(tag, username, out);
|
||||
case "LSUB" -> handleList(tag, username, out); // LSUB = LIST для подписок
|
||||
case "STATUS" -> handleStatus(tag, parts, username, out);
|
||||
case "ID" -> {
|
||||
out.println("* ID (\"name\" \"MailServer IMAP\" \"version\" \"1.0\")");
|
||||
out.println(tag + " OK ID completed");
|
||||
}
|
||||
default -> {
|
||||
out.println(tag + " BAD Unknown command: " + command);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleSelect(String tag, String[] parts, String username, PrintWriter out) {
|
||||
if (parts.length < 3) {
|
||||
out.println(tag + " BAD Invalid SELECT command");
|
||||
return;
|
||||
}
|
||||
|
||||
String folderName = parts[2].replace("\"", "");
|
||||
com.mailserver.domain.User userTemp = userRepository.findByEmail(username).block();
|
||||
if (userTemp == null) {
|
||||
out.println(tag + " NO User not found");
|
||||
return;
|
||||
}
|
||||
com.mailserver.domain.Folder folder = folderRepository.findByUserIdAndName(
|
||||
userTemp.getId(), folderName).block();
|
||||
|
||||
if (folder == null) {
|
||||
out.println(tag + " NO [NONEXISTENT] Mailbox does not exist");
|
||||
return;
|
||||
}
|
||||
|
||||
long messageCount = messageRepository.findByFolderId(folder.getId(),
|
||||
org.springframework.data.domain.PageRequest.of(0, 1)).count().block();
|
||||
|
||||
out.println("* " + messageCount + " EXISTS");
|
||||
out.println("* 0 RECENT");
|
||||
out.println("* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)");
|
||||
out.println(tag + " [OK] [UNSEEN 1] " + (messageCount > 0 ? "There are unseen messages in the mailbox." : "The mailbox has no unseen messages."));
|
||||
out.println(tag + " OK [READ-WRITE] SELECT completed");
|
||||
}
|
||||
|
||||
private void handleFetch(String tag, String[] parts, String username, PrintWriter out) {
|
||||
if (parts.length < 4) {
|
||||
out.println(tag + " BAD Invalid FETCH command");
|
||||
return;
|
||||
}
|
||||
|
||||
String messageId = parts[2];
|
||||
String item = parts[3];
|
||||
|
||||
com.mailserver.domain.User user = userRepository.findByEmail(username).block();
|
||||
if (user == null) {
|
||||
out.println(tag + " NO User not found");
|
||||
return;
|
||||
}
|
||||
|
||||
// Получаем все сообщения пользователя
|
||||
java.util.List<com.mailserver.domain.Folder> folders = folderRepository.findByUserId(user.getId()).collectList().block();
|
||||
java.util.List<com.mailserver.domain.Message> allMessages = new java.util.ArrayList<>();
|
||||
for (com.mailserver.domain.Folder folder : folders) {
|
||||
allMessages.addAll(messageRepository.findByFolderId(folder.getId(),
|
||||
org.springframework.data.domain.PageRequest.of(0, 1000)).collectList().block());
|
||||
}
|
||||
|
||||
// Находим сообщение по UID или индексу
|
||||
com.mailserver.domain.Message message = null;
|
||||
if (item.contains("UID")) {
|
||||
// UID FETCH
|
||||
try {
|
||||
long uid = Long.parseLong(messageId);
|
||||
for (com.mailserver.domain.Message msg : allMessages) {
|
||||
if (msg.getId().equals(uid)) {
|
||||
message = msg;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
out.println(tag + " BAD Invalid UID");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// FETCH по индексу (1-based)
|
||||
try {
|
||||
int index = Integer.parseInt(messageId) - 1;
|
||||
if (index >= 0 && index < allMessages.size()) {
|
||||
message = allMessages.get(index);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
out.println(tag + " BAD Invalid message set");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (message == null) {
|
||||
out.println(tag + " NO Message not found");
|
||||
return;
|
||||
}
|
||||
|
||||
// Формируем ответ FETCH
|
||||
StringBuilder flags = new StringBuilder();
|
||||
if (message.isRead()) flags.append("\\Seen ");
|
||||
if (message.isFlagged()) flags.append("\\Flagged ");
|
||||
if (message.isSpam()) flags.append("\\Deleted ");
|
||||
if (message.getBody() != null && !message.getBody().isEmpty()) flags.append("\\Answered ");
|
||||
|
||||
out.println("* " + messageId + " FETCH (");
|
||||
out.println(" UID " + message.getId());
|
||||
out.println(" FLAGS (" + flags.toString().trim() + ")");
|
||||
out.println(" INTERNALDATE \"" + formatImapDate(message.getReceivedAt()) + "\"");
|
||||
out.println(" RFC822.SIZE " + (message.getMessageSize() != null ? message.getMessageSize() : 0));
|
||||
out.println(" ENVELOPE (");
|
||||
out.println(" \"" + escapeString(message.getReceivedAt() != null ? message.getReceivedAt().toString() : "") + "\"");
|
||||
out.println(" \"" + escapeString(message.getSubject() != null ? message.getSubject() : "") + "\"");
|
||||
out.println(" (\"" + escapeString(message.getSenderName() != null ? message.getSenderName() : "") + "\" 0 \"\" \"" + escapeString(message.getSenderEmail() != null ? message.getSenderEmail() : "") + "\")");
|
||||
out.println(" (\"" + escapeString(message.getRecipientEmail() != null ? message.getRecipientEmail() : "") + "\")");
|
||||
out.println(" (\"" + escapeString(message.getRecipientEmail() != null ? message.getRecipientEmail() : "") + "\")");
|
||||
out.println(" (\"" + escapeString(message.getSenderEmail() != null ? message.getSenderEmail() : "") + "\")");
|
||||
out.println(" NIL");
|
||||
out.println(" NIL");
|
||||
out.println(" NIL");
|
||||
out.println(" )");
|
||||
|
||||
// Если запрошено тело сообщения
|
||||
if (item.contains("BODY[]") || item.contains("RFC822")) {
|
||||
out.println(" BODY[] {");
|
||||
String body = message.getBody() != null ? message.getBody() : "";
|
||||
out.println(body.length());
|
||||
out.println(body);
|
||||
out.println(")");
|
||||
}
|
||||
|
||||
out.println(")");
|
||||
out.println(tag + " OK FETCH completed");
|
||||
}
|
||||
|
||||
private void handleSearch(String tag, String[] parts, String username, PrintWriter out) {
|
||||
com.mailserver.domain.User user = userRepository.findByEmail(username).block();
|
||||
if (user == null) {
|
||||
out.println(tag + " NO User not found");
|
||||
return;
|
||||
}
|
||||
|
||||
com.mailserver.domain.Folder inbox = folderRepository.findByUserIdAndName(user.getId(), "INBOX").block();
|
||||
if (inbox == null) {
|
||||
out.println(tag + " OK SEARCH completed");
|
||||
return;
|
||||
}
|
||||
|
||||
// Простой поиск по ключевому слову
|
||||
String keyword = null;
|
||||
for (int i = 2; i < parts.length; i++) {
|
||||
if (!parts[i].equals("ALL") && !parts[i].equals("FROM") && !parts[i].equals("SUBJECT") && !parts[i].equals("BODY")) {
|
||||
keyword = parts[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
java.util.List<Long> messageIds;
|
||||
if (keyword != null && !keyword.isEmpty()) {
|
||||
var results = messageRepository.searchInFolder(inbox.getId(), keyword,
|
||||
org.springframework.data.domain.PageRequest.of(0, 100)).collectList().block();
|
||||
messageIds = results.stream()
|
||||
.map(com.mailserver.domain.Message::getId)
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
} else {
|
||||
var results = messageRepository.findByFolderId(inbox.getId(),
|
||||
org.springframework.data.domain.PageRequest.of(0, 100)).collectList().block();
|
||||
messageIds = results.stream()
|
||||
.map(com.mailserver.domain.Message::getId)
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
}
|
||||
|
||||
StringBuilder ids = new StringBuilder();
|
||||
for (Long id : messageIds) {
|
||||
ids.append(id).append(" ");
|
||||
}
|
||||
|
||||
out.println("* SEARCH " + ids.toString().trim());
|
||||
out.println(tag + " OK SEARCH completed");
|
||||
}
|
||||
|
||||
private void handleStore(String tag, String[] parts, String username, PrintWriter out) {
|
||||
if (parts.length < 5) {
|
||||
out.println(tag + " BAD Invalid STORE command");
|
||||
return;
|
||||
}
|
||||
|
||||
String messageId = parts[2];
|
||||
String item = parts[3].toUpperCase();
|
||||
String value = parts[4].toUpperCase();
|
||||
|
||||
com.mailserver.domain.User user = userRepository.findByEmail(username).block();
|
||||
if (user == null) {
|
||||
out.println(tag + " NO User not found");
|
||||
return;
|
||||
}
|
||||
|
||||
// Получаем все сообщения пользователя
|
||||
java.util.List<com.mailserver.domain.Folder> folders = folderRepository.findByUserId(user.getId()).collectList().block();
|
||||
java.util.List<com.mailserver.domain.Message> allMessages = new java.util.ArrayList<>();
|
||||
for (com.mailserver.domain.Folder folder : folders) {
|
||||
allMessages.addAll(messageRepository.findByFolderId(folder.getId(),
|
||||
org.springframework.data.domain.PageRequest.of(0, 1000)).collectList().block());
|
||||
}
|
||||
|
||||
com.mailserver.domain.Message message = null;
|
||||
try {
|
||||
int index = Integer.parseInt(messageId) - 1;
|
||||
if (index >= 0 && index < allMessages.size()) {
|
||||
message = allMessages.get(index);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
out.println(tag + " BAD Invalid message set");
|
||||
return;
|
||||
}
|
||||
|
||||
if (message == null) {
|
||||
out.println(tag + " NO Message not found");
|
||||
return;
|
||||
}
|
||||
|
||||
boolean saved = false;
|
||||
if (item.contains("+FLAGS") || item.contains("FLAGS")) {
|
||||
if (value.contains("\\SEEN")) {
|
||||
message.setRead(true);
|
||||
saved = true;
|
||||
}
|
||||
if (value.contains("\\FLAGGED")) {
|
||||
message.setFlagged(true);
|
||||
saved = true;
|
||||
}
|
||||
if (value.contains("\\DELETED")) {
|
||||
message.setSpam(true); // Используем spam как флаг удаления для IMAP
|
||||
saved = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (item.contains("-FLAGS")) {
|
||||
if (value.contains("\\SEEN")) {
|
||||
message.setRead(false);
|
||||
saved = true;
|
||||
}
|
||||
if (value.contains("\\FLAGGED")) {
|
||||
message.setFlagged(false);
|
||||
saved = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (saved) {
|
||||
messageRepository.save(message).block();
|
||||
}
|
||||
|
||||
out.println("* " + messageId + " FETCH (FLAGS (" + formatFlags(message) + "))");
|
||||
out.println(tag + " OK STORE completed");
|
||||
}
|
||||
|
||||
private void handleExpunge(String tag, String username, PrintWriter out) {
|
||||
com.mailserver.domain.User user = userRepository.findByEmail(username).block();
|
||||
if (user == null) {
|
||||
out.println(tag + " NO User not found");
|
||||
return;
|
||||
}
|
||||
|
||||
java.util.List<com.mailserver.domain.Folder> folders = folderRepository.findByUserId(user.getId()).collectList().block();
|
||||
for (com.mailserver.domain.Folder folder : folders) {
|
||||
var messages = messageRepository.findByFolderId(folder.getId(),
|
||||
org.springframework.data.domain.PageRequest.of(0, 1000)).collectList().block();
|
||||
for (com.mailserver.domain.Message message : messages) {
|
||||
if (message.isSpam()) {
|
||||
messageRepository.delete(message).block();
|
||||
folder.setMessageCount(Math.max(0, folder.getMessageCount() - 1));
|
||||
folderRepository.save(folder).block();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out.println("* 0 EXPUNGE");
|
||||
out.println(tag + " OK EXPUNGE completed");
|
||||
}
|
||||
|
||||
private void handleList(String tag, String username, PrintWriter out) {
|
||||
com.mailserver.domain.User user = userRepository.findByEmail(username).block();
|
||||
if (user == null) {
|
||||
out.println(tag + " NO User not found");
|
||||
return;
|
||||
}
|
||||
|
||||
java.util.List<com.mailserver.domain.Folder> folders = folderRepository.findByUserId(user.getId()).collectList().block();
|
||||
for (com.mailserver.domain.Folder folder : folders) {
|
||||
out.println("* LIST (\\HasNoChildren) \".\" \"" + folder.getName() + "\"");
|
||||
}
|
||||
|
||||
out.println(tag + " OK LIST completed");
|
||||
}
|
||||
|
||||
private void handleStatus(String tag, String[] parts, String username, PrintWriter out) {
|
||||
if (parts.length < 4) {
|
||||
out.println(tag + " BAD Invalid STATUS command");
|
||||
return;
|
||||
}
|
||||
|
||||
String folderName = parts[2].replace("\"", "");
|
||||
com.mailserver.domain.User user = userRepository.findByEmail(username).block();
|
||||
if (user == null) {
|
||||
out.println(tag + " NO User not found");
|
||||
return;
|
||||
}
|
||||
|
||||
com.mailserver.domain.Folder folder = folderRepository.findByUserIdAndName(user.getId(), folderName).block();
|
||||
if (folder == null) {
|
||||
out.println(tag + " NO [NONEXISTENT] Mailbox does not exist");
|
||||
return;
|
||||
}
|
||||
|
||||
long messageCount = messageRepository.findByFolderId(folder.getId(),
|
||||
org.springframework.data.domain.PageRequest.of(0, 1)).count().block();
|
||||
long unreadCount = messageRepository.countUnreadByFolderId(folder.getId()).block();
|
||||
|
||||
out.println("* STATUS " + folderName + " (MESSAGES " + messageCount + " UNSEEN " + unreadCount + " UIDNEXT " + (messageCount + 1) + " UIDVALIDITY 1)");
|
||||
out.println(tag + " OK STATUS completed");
|
||||
}
|
||||
|
||||
private String[] parseCommand(String line) {
|
||||
if (line == null || line.trim().isEmpty()) return null;
|
||||
|
||||
line = line.trim();
|
||||
int spaceIndex = line.indexOf(' ');
|
||||
if (spaceIndex == -1) return new String[]{line, ""};
|
||||
|
||||
String tag = line.substring(0, spaceIndex);
|
||||
String rest = line.substring(spaceIndex + 1).trim();
|
||||
|
||||
// Разбираем команду и аргументы
|
||||
String[] parts = new String[20]; // Максимум 20 аргументов
|
||||
parts[0] = tag;
|
||||
int index = 1;
|
||||
|
||||
int i = 0;
|
||||
while (i < rest.length() && index < parts.length) {
|
||||
if (rest.charAt(i) == '"') {
|
||||
// Строка в кавычках
|
||||
int end = rest.indexOf('"', i + 1);
|
||||
if (end == -1) break;
|
||||
parts[index++] = rest.substring(i + 1, end);
|
||||
i = end + 1;
|
||||
} else if (Character.isWhitespace(rest.charAt(i))) {
|
||||
i++;
|
||||
} else {
|
||||
int end = rest.indexOf(' ', i);
|
||||
if (end == -1) end = rest.length();
|
||||
parts[index++] = rest.substring(i, end);
|
||||
i = end;
|
||||
}
|
||||
}
|
||||
|
||||
// Обрезаем массив до нужного размера
|
||||
String[] result = new String[index];
|
||||
System.arraycopy(parts, 0, result, 0, index);
|
||||
return result;
|
||||
}
|
||||
|
||||
private String formatImapDate(Instant instant) {
|
||||
if (instant == null) return "";
|
||||
// Формат IMAP: "dd-MMM-yyyy hh:mm:ss +0000"
|
||||
String[] months = {"Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
||||
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
|
||||
java.time.LocalDate date = instant.atZone(java.time.ZoneOffset.UTC).toLocalDate();
|
||||
java.time.LocalTime time = instant.atZone(java.time.ZoneOffset.UTC).toLocalTime();
|
||||
|
||||
return String.format("%02d-%s-%04d %02d:%02d:%02d +0000",
|
||||
date.getDayOfMonth(), months[date.getMonthValue() - 1], date.getYear(),
|
||||
time.getHour(), time.getMinute(), time.getSecond());
|
||||
}
|
||||
|
||||
private String escapeString(String str) {
|
||||
if (str == null) return "";
|
||||
return str.replace("\\", "\\\\").replace("\"", "\\\"");
|
||||
}
|
||||
|
||||
private String formatFlags(com.mailserver.domain.Message message) {
|
||||
StringBuilder flags = new StringBuilder();
|
||||
if (message.isRead()) flags.append("\\Seen ");
|
||||
if (message.isFlagged()) flags.append("\\Flagged ");
|
||||
if (message.isSpam()) flags.append("\\Deleted ");
|
||||
return flags.toString().trim();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user