Settings → MAILBOXES: удалить ящик вместе с письмами
continuous-integration/drone/pr Build is passing
continuous-integration/drone/pr Build is passing
DELETE /api/admin/mailboxes/{id} (только администратор, не свой ящик):
файлы вложений, затем строка users — папки, письма, исходники, контакты,
участие в рассылках и доступы уходят каскадом. Кнопка с подтверждением.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -158,6 +158,19 @@ public class MailboxAdminController {
|
||||
}))).flatMap(this::view);
|
||||
}
|
||||
|
||||
/** Deleted for good with all its mail (Settings → MAILBOXES → Delete); not your own mailbox. */
|
||||
@DeleteMapping("/{id}")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
public Mono<Void> delete(Mono<Authentication> auth, @PathVariable Long id) {
|
||||
return admin(auth).flatMap(me -> mailbox(id).flatMap(user -> {
|
||||
if (user.getEmail().equalsIgnoreCase(me)) {
|
||||
return Mono.error(bad("You can not delete your own mailbox"));
|
||||
}
|
||||
return mail.deleteMailbox(user)
|
||||
.doOnSuccess(v -> log.warn("Mailbox {} deleted with all its mail by administrator {}", user.getEmail(), me));
|
||||
}));
|
||||
}
|
||||
|
||||
/** Trap: what the mailbox sends is stored in its own INBOX, nothing leaves the server (staging@). */
|
||||
@PutMapping("/{id}/trap")
|
||||
public Mono<MailboxView> trap(Mono<Authentication> auth, @PathVariable Long id, @RequestBody Mono<TrapRequest> body) {
|
||||
|
||||
@@ -12,6 +12,10 @@ public interface AttachmentRepository extends R2dbcRepository<Attachment, Long>
|
||||
|
||||
Mono<Long> countByMessageId(Long messageId);
|
||||
|
||||
/** All attachments of a mailbox — their files are removed with the mailbox. */
|
||||
@Query("SELECT a.* FROM attachments a JOIN messages m ON m.id = a.message_id JOIN folders f ON f.id = m.folder_id WHERE f.user_id = :#{#userId}")
|
||||
Flux<Attachment> findByMailbox(Long userId);
|
||||
|
||||
@Modifying
|
||||
@Query("UPDATE attachments SET message_id = :#{#to} WHERE message_id = :#{#from}")
|
||||
Mono<Integer> moveAll(Long from, Long to);
|
||||
|
||||
@@ -401,6 +401,16 @@ public class MailService {
|
||||
.then(messageRepository.delete(message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Mailbox deleted for good: attachment files first, then the user row — folders, messages, sources, contacts,
|
||||
* list memberships and access rows go with it (ON DELETE CASCADE).
|
||||
*/
|
||||
public Mono<Void> deleteMailbox(User user) {
|
||||
return attachmentRepository.findByMailbox(user.getId())
|
||||
.doOnNext(attachment -> deleteFile(attachment.getFilePath()))
|
||||
.then(userRepository.delete(user));
|
||||
}
|
||||
|
||||
private static void deleteFile(String path) {
|
||||
try {
|
||||
Files.deleteIfExists(Paths.get(path));
|
||||
|
||||
@@ -173,6 +173,19 @@ class MailboxAdminControllerTest {
|
||||
.isEqualTo(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
@Test
|
||||
void administratorDeletesAMailboxButNotHisOwnOthersCanNot() {
|
||||
when(mail.deleteMailbox(any())).thenReturn(Mono.empty());
|
||||
|
||||
controller.delete(as("admin@jambocheck.com"), 2L).block();
|
||||
verify(mail).deleteMailbox(alice);
|
||||
|
||||
assertThat(status(() -> controller.delete(as("admin@jambocheck.com"), 1L).block())).isEqualTo(HttpStatus.BAD_REQUEST);
|
||||
assertThat(status(() -> controller.delete(as("alice@jambocheck.com"), 1L).block())).isEqualTo(HttpStatus.FORBIDDEN);
|
||||
assertThat(status(() -> controller.delete(as("admin@jambocheck.com"), 99L).block())).isEqualTo(HttpStatus.NOT_FOUND);
|
||||
verify(mail, never()).deleteMailbox(admin);
|
||||
}
|
||||
|
||||
@Test
|
||||
void administratorOpensAnyMailboxOthersCanNot() {
|
||||
when(jwt.generateToken("alice@jambocheck.com", "admin@jambocheck.com")).thenReturn("alice-token");
|
||||
|
||||
Reference in New Issue
Block a user