Initial commit: mail-server with WebFlux backend and Angular v22 Synthwave frontend
Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
package com.mailserver.controller;
|
||||
|
||||
import com.mailserver.dto.LoginRequest;
|
||||
import com.mailserver.dto.LoginResponse;
|
||||
import com.mailserver.domain.User;
|
||||
import com.mailserver.service.UserService;
|
||||
import com.mailserver.util.JwtUtils;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class AuthenticationController {
|
||||
|
||||
private final UserService userService;
|
||||
private final JwtUtils jwtUtils;
|
||||
|
||||
public AuthenticationController(UserService userService, JwtUtils jwtUtils) {
|
||||
this.userService = userService;
|
||||
this.jwtUtils = jwtUtils;
|
||||
}
|
||||
|
||||
@PostMapping("/login")
|
||||
public Mono<ResponseEntity<?>> login(@Valid @RequestBody Mono<LoginRequest> requestMono) {
|
||||
return requestMono.flatMap(request ->
|
||||
userService.findByEmail(request.getEmail())
|
||||
.switchIfEmpty(Mono.error(new IllegalArgumentException("Invalid credentials")))
|
||||
.flatMap(user -> {
|
||||
if (!user.isActive()) {
|
||||
return Mono.just(ResponseEntity.badRequest().body(Map.of("error", "User is deactivated")));
|
||||
}
|
||||
if (!userService.checkPassword(user, request.getPassword())) {
|
||||
return Mono.just(ResponseEntity.badRequest().body(Map.of("error", "Invalid credentials")));
|
||||
}
|
||||
String token = jwtUtils.generateToken(user.getEmail());
|
||||
LoginResponse response = new LoginResponse();
|
||||
response.setToken(token);
|
||||
response.setEmail(user.getEmail());
|
||||
return Mono.just(ResponseEntity.ok(response));
|
||||
})
|
||||
.onErrorResume(IllegalArgumentException.class, e ->
|
||||
Mono.just(ResponseEntity.badRequest().body(Map.of("error", "Invalid credentials")))
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
package com.mailserver.controller;
|
||||
|
||||
import com.mailserver.dto.AttachmentResponse;
|
||||
import com.mailserver.dto.FolderResponse;
|
||||
import com.mailserver.dto.MessageResponse;
|
||||
import com.mailserver.dto.RegisterRequest;
|
||||
import com.mailserver.dto.SendRequest;
|
||||
import com.mailserver.service.MailService;
|
||||
import com.mailserver.service.UserService;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DataBufferUtils;
|
||||
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.codec.multipart.FilePart;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class MailController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(MailController.class);
|
||||
|
||||
private final UserService userService;
|
||||
private final MailService mailService;
|
||||
|
||||
public MailController(UserService userService, MailService mailService) {
|
||||
this.userService = userService;
|
||||
this.mailService = mailService;
|
||||
}
|
||||
|
||||
private Mono<String> getCurrentUserEmail(Mono<Authentication> authenticationMono) {
|
||||
return authenticationMono.map(Authentication::getName);
|
||||
}
|
||||
|
||||
// --- Users ---
|
||||
|
||||
@PostMapping("/register")
|
||||
public Mono<?> register(@Valid @RequestBody Mono<RegisterRequest> requestMono) {
|
||||
return requestMono.flatMap(request ->
|
||||
userService.register(request.getEmail(), request.getPassword(), request.getDisplayName())
|
||||
.map(user -> org.springframework.http.ResponseEntity.status(HttpStatus.CREATED)
|
||||
.body(Map.of("message", "User registered")))
|
||||
.onErrorResume(IllegalArgumentException.class, e ->
|
||||
Mono.just(org.springframework.http.ResponseEntity.badRequest()
|
||||
.body(Map.of("error", e.getMessage()))))
|
||||
);
|
||||
}
|
||||
|
||||
@GetMapping("/folders")
|
||||
public Flux<FolderResponse> getFolders(Mono<Authentication> authenticationMono) {
|
||||
return authenticationMono.flatMapMany(auth ->
|
||||
userService.findByEmail(auth.getName())
|
||||
.flatMapMany(user -> userService.getFolders(user.getId()))
|
||||
);
|
||||
}
|
||||
|
||||
// --- Messages ---
|
||||
|
||||
@GetMapping("/messages")
|
||||
public Flux<MessageResponse> getMessages(
|
||||
Mono<Authentication> authenticationMono,
|
||||
@RequestParam Long folderId,
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "20") int size) {
|
||||
return authenticationMono.flatMapMany(auth ->
|
||||
userService.findByEmail(auth.getName())
|
||||
.flatMapMany(user -> mailService.getMessages(folderId, user.getId(), page, size))
|
||||
);
|
||||
}
|
||||
|
||||
@GetMapping("/messages/search")
|
||||
public Flux<MessageResponse> searchMessages(
|
||||
Mono<Authentication> authenticationMono,
|
||||
@RequestParam Long folderId,
|
||||
@RequestParam(required = false) String keyword,
|
||||
@RequestParam(required = false) String subject,
|
||||
@RequestParam(required = false) String body,
|
||||
@RequestParam(required = false) String sender,
|
||||
@RequestParam(required = false) String recipient,
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "20") int size) {
|
||||
return authenticationMono.flatMapMany(auth ->
|
||||
userService.findByEmail(auth.getName())
|
||||
.flatMapMany(user -> {
|
||||
if (keyword != null && !keyword.trim().isEmpty()) {
|
||||
return mailService.searchMessages(folderId, user.getId(), keyword.trim(), page, size);
|
||||
}
|
||||
return mailService.searchMessagesWithFilters(folderId, user.getId(),
|
||||
subject, body, sender, recipient, page, size);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@GetMapping("/messages/{id}")
|
||||
public Mono<MessageResponse> getMessage(@PathVariable Long id, Mono<Authentication> authenticationMono) {
|
||||
return authenticationMono.flatMap(auth ->
|
||||
userService.findByEmail(auth.getName())
|
||||
.flatMap(user -> mailService.getMessage(id, user.getId()))
|
||||
);
|
||||
}
|
||||
|
||||
@PostMapping("/send")
|
||||
public Mono<Object> sendMessage(
|
||||
Mono<Authentication> authenticationMono,
|
||||
@Valid @RequestBody Mono<SendRequest> requestMono) {
|
||||
return authenticationMono.zipWith(requestMono)
|
||||
.flatMap(pair -> {
|
||||
String email = pair.getT1().getName();
|
||||
SendRequest request = pair.getT2();
|
||||
return mailService.sendMessage(email, request)
|
||||
.map(response -> (Object) org.springframework.http.ResponseEntity.ok(response))
|
||||
.onErrorResume(e -> {
|
||||
log.error("Ошибка при отправке письма: {}", e.getMessage(), e);
|
||||
return Mono.just((Object) org.springframework.http.ResponseEntity.badRequest()
|
||||
.body(Map.of("error", e.getMessage())));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@PutMapping("/messages/{id}/read")
|
||||
public Mono<?> markAsRead(@PathVariable Long id, Mono<Authentication> authenticationMono) {
|
||||
return authenticationMono.flatMap(auth ->
|
||||
userService.findByEmail(auth.getName())
|
||||
.flatMap(user -> mailService.markAsRead(id, user.getId())
|
||||
.then(Mono.just(org.springframework.http.ResponseEntity.ok(Map.of("message", "Marked as read")))))
|
||||
);
|
||||
}
|
||||
|
||||
@DeleteMapping("/messages/{id}")
|
||||
public Mono<?> deleteMessage(@PathVariable Long id, Mono<Authentication> authenticationMono) {
|
||||
return authenticationMono.flatMap(auth ->
|
||||
userService.findByEmail(auth.getName())
|
||||
.flatMap(user -> mailService.deleteMessage(id, user.getId())
|
||||
.then(Mono.just(org.springframework.http.ResponseEntity.ok(Map.of("message", "Message deleted")))))
|
||||
);
|
||||
}
|
||||
|
||||
// --- Simulate incoming message (для тестов) ---
|
||||
|
||||
@PostMapping("/simulate/receive")
|
||||
public Mono<?> simulateReceive(@RequestBody Mono<Map<String, String>> bodyMono) {
|
||||
return bodyMono.flatMap(body ->
|
||||
mailService.receiveMessage(
|
||||
body.get("senderEmail"),
|
||||
body.get("senderName"),
|
||||
body.get("recipientEmail"),
|
||||
body.get("subject"),
|
||||
body.get("body"),
|
||||
"true".equals(body.get("html"))
|
||||
)
|
||||
.then(Mono.just(org.springframework.http.ResponseEntity.ok(Map.of("message", "Message received"))))
|
||||
.onErrorResume(e ->
|
||||
Mono.just(org.springframework.http.ResponseEntity.badRequest().body(Map.of("error", e.getMessage()))))
|
||||
);
|
||||
}
|
||||
|
||||
// --- Attachments ---
|
||||
|
||||
@PostMapping(value = "/messages/{messageId}/attachments", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
public Mono<?> uploadAttachment(
|
||||
@PathVariable Long messageId,
|
||||
Mono<Authentication> authenticationMono,
|
||||
@RequestBody Mono<Flux<FilePart>> filePartsMono) {
|
||||
return authenticationMono.zipWith(filePartsMono)
|
||||
.flatMap(pair -> {
|
||||
String email = pair.getT1().getName();
|
||||
return userService.findByEmail(email)
|
||||
.flatMapMany(user -> pair.getT2())
|
||||
.next()
|
||||
.flatMap(file -> {
|
||||
String originalFilename = file.filename();
|
||||
String uniqueName = UUID.randomUUID() + "_" + originalFilename;
|
||||
Path filePath = Paths.get(mailService.getAttachmentsDir(), uniqueName);
|
||||
try {
|
||||
Files.createDirectories(filePath.getParent());
|
||||
} catch (IOException e) {
|
||||
return Mono.error(new RuntimeException("Failed to create directory", e));
|
||||
}
|
||||
|
||||
return DataBufferUtils.join(file.content())
|
||||
.flatMap(dataBuffer -> {
|
||||
byte[] bytes = new byte[dataBuffer.readableByteCount()];
|
||||
dataBuffer.read(bytes);
|
||||
DataBufferUtils.release(dataBuffer);
|
||||
try {
|
||||
Files.write(filePath, bytes);
|
||||
} catch (IOException e) {
|
||||
return Mono.error(new RuntimeException("Failed to save file", e));
|
||||
}
|
||||
return userService.findByEmail(email)
|
||||
.flatMap(user -> mailService.saveAttachmentReactive(
|
||||
messageId, user.getId(),
|
||||
originalFilename, file.headers().getContentType().toString(),
|
||||
bytes.length, filePath.toString()))
|
||||
.map(response -> org.springframework.http.ResponseEntity.status(HttpStatus.CREATED).body(response));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@GetMapping("/messages/{messageId}/attachments")
|
||||
public Flux<AttachmentResponse> getMessageAttachments(
|
||||
@PathVariable Long messageId,
|
||||
Mono<Authentication> authenticationMono) {
|
||||
return authenticationMono.flatMapMany(auth ->
|
||||
userService.findByEmail(auth.getName())
|
||||
.flatMapMany(user -> mailService.getMessageAttachments(messageId, user.getId()))
|
||||
);
|
||||
}
|
||||
|
||||
@GetMapping("/attachments/{attachmentId}/download")
|
||||
public Mono<Object> downloadAttachment(
|
||||
@PathVariable Long attachmentId,
|
||||
Mono<Authentication> authenticationMono) {
|
||||
return authenticationMono.flatMap(auth ->
|
||||
userService.findByEmail(auth.getName())
|
||||
.flatMap(user -> {
|
||||
try {
|
||||
AttachmentResponse info = mailService.getAttachmentInfo(attachmentId, user.getId());
|
||||
Path filePath = Paths.get(mailService.getAttachmentFilePath(attachmentId, user.getId()));
|
||||
byte[] bytes;
|
||||
try {
|
||||
bytes = Files.readAllBytes(filePath);
|
||||
} catch (IOException e) {
|
||||
return Mono.just((Object) org.springframework.http.ResponseEntity.badRequest()
|
||||
.body(Map.of("error", "Failed to read file: " + e.getMessage())));
|
||||
}
|
||||
DataBuffer buffer = new DefaultDataBufferFactory().wrap(bytes);
|
||||
return Mono.just((Object) org.springframework.http.ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION,
|
||||
"attachment; filename=\"" + info.getFilename() + "\"")
|
||||
.contentType(MediaType.parseMediaType(
|
||||
info.getContentType() != null ? info.getContentType() : "application/octet-stream"))
|
||||
.body(buffer)
|
||||
);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return Mono.just((Object) org.springframework.http.ResponseEntity.badRequest().body(Map.of("error", e.getMessage())));
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@DeleteMapping("/attachments/{attachmentId}")
|
||||
public Mono<?> deleteAttachment(
|
||||
@PathVariable Long attachmentId,
|
||||
Mono<Authentication> authenticationMono) {
|
||||
return authenticationMono.flatMap(auth ->
|
||||
userService.findByEmail(auth.getName())
|
||||
.flatMap(user -> mailService.deleteAttachment(attachmentId, user.getId())
|
||||
.then(Mono.just(org.springframework.http.ResponseEntity.ok(Map.of("message", "Attachment deleted")))))
|
||||
.onErrorResume(IllegalArgumentException.class, e ->
|
||||
Mono.just(org.springframework.http.ResponseEntity.badRequest().body(Map.of("error", e.getMessage()))))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.mailserver.controller;
|
||||
|
||||
import com.mailserver.domain.BlacklistEntry;
|
||||
import com.mailserver.service.SpamFilterService;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Контроллер для управления чёрным списком отправителей.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/spam")
|
||||
public class SpamController {
|
||||
|
||||
private final SpamFilterService spamFilterService;
|
||||
|
||||
public SpamController(SpamFilterService spamFilterService) {
|
||||
this.spamFilterService = spamFilterService;
|
||||
}
|
||||
|
||||
private String getCurrentUserEmail(Authentication authentication) {
|
||||
return authentication.getName();
|
||||
}
|
||||
|
||||
@GetMapping("/blacklist")
|
||||
public Flux<BlacklistEntry> getBlacklist(Mono<Authentication> authenticationMono) {
|
||||
return authenticationMono.flatMapMany(auth ->
|
||||
spamFilterService.getBlacklist(getCurrentUserEmail(auth))
|
||||
);
|
||||
}
|
||||
|
||||
@PostMapping("/blacklist")
|
||||
public Mono<BlacklistEntry> addToBlacklist(Mono<Authentication> authenticationMono, @RequestBody Mono<Map<String, String>> requestMono) {
|
||||
return authenticationMono.zipWith(requestMono).flatMap(pair -> {
|
||||
String email = getCurrentUserEmail(pair.getT1());
|
||||
String senderEmail = pair.getT2().get("senderEmail");
|
||||
String reason = pair.getT2().get("reason");
|
||||
if (senderEmail == null || senderEmail.trim().isEmpty()) {
|
||||
return Mono.error(new IllegalArgumentException("senderEmail is required"));
|
||||
}
|
||||
return spamFilterService.addToBlacklist(email, senderEmail.trim(), reason);
|
||||
});
|
||||
}
|
||||
|
||||
@DeleteMapping("/blacklist")
|
||||
public Mono<Void> removeFromBlacklist(Mono<Authentication> authenticationMono, @RequestParam String senderEmail) {
|
||||
return authenticationMono.flatMap(auth ->
|
||||
spamFilterService.removeFromBlacklist(getCurrentUserEmail(auth), senderEmail)
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user