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 folders = folderRepository.findByUserId(user.getId()).collectList().block(); java.util.List 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 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 folders = folderRepository.findByUserId(user.getId()).collectList().block(); java.util.List 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 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 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(); } }