package com.mailserver.service; import com.mailserver.domain.Attachment; import com.mailserver.domain.Contact; import com.mailserver.domain.Folder; import com.mailserver.domain.Message; import com.mailserver.domain.User; import com.mailserver.dto.AttachmentResponse; import com.mailserver.dto.MessageResponse; import com.mailserver.repository.AttachmentRepository; import com.mailserver.repository.ContactRepository; import com.mailserver.repository.FolderRepository; import com.mailserver.repository.MessageRepository; import com.mailserver.repository.UserRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.web.server.ResponseStatusException; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.io.*; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.time.Instant; import java.util.ArrayList; import java.util.List; import java.util.Properties; import java.util.UUID; @Service public class MailService { private static final Logger log = LoggerFactory.getLogger(MailService.class); private final MessageRepository messageRepository; private final FolderRepository folderRepository; private final UserRepository userRepository; private final AttachmentRepository attachmentRepository; private final ContactRepository contactRepository; private final SpamFilterService spamFilterService; @Value("${mailserver.attachments.dir:./attachments}") private String attachmentsDir; public MailService(MessageRepository messageRepository, FolderRepository folderRepository, UserRepository userRepository, AttachmentRepository attachmentRepository, ContactRepository contactRepository, SpamFilterService spamFilterService) { this.messageRepository = messageRepository; this.folderRepository = folderRepository; this.userRepository = userRepository; this.attachmentRepository = attachmentRepository; this.contactRepository = contactRepository; this.spamFilterService = spamFilterService; } /** Copy of a message we sent, in the sender's "Sent" folder. */ public Mono saveSent(User fromUser, String to, String subject, String body, boolean html, String messageId) { return folderRepository.findByUserIdAndName(fromUser.getId(), "Sent") .switchIfEmpty(Mono.error(new IllegalStateException("Sent folder not found"))) .flatMap(sentFolder -> { Message message = new Message(); message.setFolderId(sentFolder.getId()); message.setSenderEmail(fromUser.getEmail()); message.setSenderName(fromUser.getDisplayName()); message.setRecipientEmail(to); message.setSubject(subject); message.setBody(body); message.setHtml(html); message.setRead(true); message.setReceivedAt(Instant.now()); message.setMessageId(messageId); message.setMessageSize((long) (subject.length() + body.length())); return messageRepository.save(message); }) .map(this::toResponse); } public Mono receiveMessage(String senderEmail, String senderName, String recipientEmail, String subject, String body, boolean isHtml) { return receiveMessage(senderEmail, senderName, recipientEmail, subject, body, isHtml, false); } public Mono receiveMessage(String senderEmail, String senderName, String recipientEmail, String subject, String body, boolean isHtml, boolean externalSpamFlag) { return userRepository.findByEmail(recipientEmail) .switchIfEmpty(Mono.error(new IllegalArgumentException("Recipient not found: " + recipientEmail))) .flatMap(recipient -> folderRepository.findByUserIdAndName(recipient.getId(), "INBOX") .switchIfEmpty(Mono.error(new IllegalStateException("INBOX not found"))) .flatMap(inbox -> spamFilterService.isBlacklisted(recipient.getId(), senderEmail) .flatMap(isBlacklisted -> { boolean isSpam = isBlacklisted || externalSpamFlag; Message message = new Message(); message.setFolderId(inbox.getId()); message.setSenderEmail(senderEmail); message.setSenderName(senderName); message.setRecipientEmail(recipientEmail); message.setSubject(subject); message.setBody(body); message.setHtml(isHtml); message.setRead(false); message.setSpam(isSpam); message.setReceivedAt(Instant.now()); message.setMessageId(UUID.randomUUID().toString()); message.setMessageSize((long) (subject.length() + body.length())); return messageRepository.save(message) .then(folderRepository.save(inbox)); }) ) ) .then(); } public Flux getMessages(Long folderId, Long userId, int page, int size) { PageRequest pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "receivedAt")); return ownedFolder(folderId, userId) .flatMapMany(folder -> messageRepository.findByFolderId(folder.getId(), pageable)) .map(this::toResponse); } public Flux searchMessages(Long folderId, Long userId, String keyword, int page, int size) { PageRequest pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "receivedAt")); return ownedFolder(folderId, userId) .flatMapMany(folder -> messageRepository.searchInFolder(folder.getId(), keyword, pageable)) .map(this::toResponse); } public Flux searchMessagesWithFilters(Long folderId, Long userId, String subject, String body, String sender, String recipient, int page, int size) { PageRequest pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "receivedAt")); return ownedFolder(folderId, userId) .flatMapMany(folder -> messageRepository.searchWithFilters(folder.getId(), subject, body, sender, recipient, pageable)) .map(this::toResponse); } public Mono getMessage(Long messageId, Long userId) { return ownedMessage(messageId, userId).map(this::toResponse); } public Mono markAsRead(Long messageId, Long userId) { return ownedMessage(messageId, userId) .flatMap(message -> { message.setRead(true); return messageRepository.save(message).then(); }); } public Mono deleteMessage(Long messageId, Long userId) { return ownedMessage(messageId, userId).flatMap(messageRepository::delete); } /** The folder, when it belongs to the user; otherwise 404 — never someone else's mail. */ private Mono ownedFolder(Long folderId, Long userId) { return folderRepository.findById(folderId) .filter(folder -> folder.getUserId().equals(userId)) .switchIfEmpty(Mono.error(new ResponseStatusException(HttpStatus.NOT_FOUND, "Folder not found"))); } private Mono ownedMessage(Long messageId, Long userId) { return messageRepository.findById(messageId) .filterWhen(message -> folderRepository.findById(message.getFolderId()) .map(folder -> folder.getUserId().equals(userId)) .defaultIfEmpty(false)) .switchIfEmpty(Mono.error(new ResponseStatusException(HttpStatus.NOT_FOUND, "Message not found"))); } private Mono ownedAttachment(Long attachmentId, Long userId) { return attachmentRepository.findById(attachmentId) .filterWhen(attachment -> ownedMessage(attachment.getMessageId(), userId).hasElement() .onErrorReturn(false)) .switchIfEmpty(Mono.error(new ResponseStatusException(HttpStatus.NOT_FOUND, "Attachment not found"))); } private MessageResponse toResponse(Message message) { MessageResponse resp = new MessageResponse(); resp.setId(message.getId()); resp.setSenderEmail(message.getSenderEmail()); resp.setSenderName(message.getSenderName()); resp.setRecipientEmail(message.getRecipientEmail()); resp.setSubject(message.getSubject()); resp.setBody(message.getBody()); resp.setHtml(message.isHtml()); resp.setRead(message.isRead()); resp.setFlagged(message.isFlagged()); resp.setSpam(message.isSpam()); resp.setMessageSize(message.getMessageSize()); resp.setReceivedAt(message.getReceivedAt()); resp.setFolderId(message.getFolderId()); return resp; } public Mono saveAttachmentReactive(Long messageId, Long userId, String originalFilename, String contentType, long sizeBytes, String filePath) { return ownedMessage(messageId, userId) .flatMap(message -> { Attachment attachment = new Attachment(); attachment.setMessageId(messageId); attachment.setFilename(originalFilename); attachment.setContentType(contentType); attachment.setSizeBytes(sizeBytes); attachment.setFilePath(filePath); return attachmentRepository.save(attachment); }) .map(this::toAttachmentResponse); } public String getAttachmentsDir() { return attachmentsDir; } /** The attachment record (file path, name, type) — only the owner's. */ public Mono getAttachment(Long attachmentId, Long userId) { return ownedAttachment(attachmentId, userId); } public Flux getMessageAttachments(Long messageId, Long userId) { return ownedMessage(messageId, userId) .flatMapMany(message -> attachmentRepository.findByMessageId(message.getId())) .map(this::toAttachmentResponse); } public Mono deleteAttachment(Long attachmentId, Long userId) { return ownedAttachment(attachmentId, userId) .flatMap(attachment -> { try { Files.deleteIfExists(Paths.get(attachment.getFilePath())); } catch (IOException e) { log.warn("Attachment file {} not deleted: {}", attachment.getFilePath(), e.getMessage()); } return attachmentRepository.delete(attachment); }); } private AttachmentResponse toAttachmentResponse(Attachment attachment) { AttachmentResponse resp = new AttachmentResponse(); resp.setId(attachment.getId()); resp.setFilename(attachment.getFilename()); resp.setContentType(attachment.getContentType()); resp.setSizeBytes(attachment.getSizeBytes()); return resp; } // --- Contacts --- public Flux getContacts(Long userId) { return contactRepository.findByUserId(userId); } public Mono addContact(Long userId, String name, String email, String phone, String company) { if (email == null || email.trim().isEmpty()) { return Mono.error(new IllegalArgumentException("Email is required")); } Contact contact = new Contact(); contact.setUserId(userId); contact.setName(name != null ? name : ""); contact.setEmail(email.trim()); contact.setPhone(phone != null ? phone : ""); contact.setCompany(company != null ? company : ""); return contactRepository.save(contact); } public Mono updateContact(Long id, Long userId, String name, String email, String phone, String company) { return contactRepository.findById(id) .switchIfEmpty(Mono.error(new IllegalArgumentException("Contact not found"))) .flatMap(contact -> { if (!contact.getUserId().equals(userId)) { return Mono.error(new IllegalArgumentException("Not your contact")); } if (name != null) contact.setName(name); if (email != null) contact.setEmail(email.trim()); if (phone != null) contact.setPhone(phone); if (company != null) contact.setCompany(company); return contactRepository.save(contact); }); } public Mono deleteContact(Long id, Long userId) { return contactRepository.findById(id) .switchIfEmpty(Mono.error(new IllegalArgumentException("Contact not found"))) .flatMap(contact -> { if (!contact.getUserId().equals(userId)) { return Mono.error(new IllegalArgumentException("Not your contact")); } return contactRepository.delete(contact); }); } }