Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1bc0792ea | ||
|
|
53c80c57d7 | ||
|
|
7b7de905f9 | ||
|
|
a26e8fe3dd | ||
|
|
80d2988fa3 |
@@ -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");
|
||||
|
||||
@@ -23,7 +23,7 @@ services:
|
||||
[ -s /secrets/$$name ] || head -c 48 /dev/urandom | base64 | tr -dc 'A-Za-z0-9' > /secrets/$$name
|
||||
done
|
||||
# postgres reads it as uid 70, the app as uid 10001 — readable inside these containers only
|
||||
chmod 644 /secrets/DB_PASSWORD /secrets/JWT_SECRET
|
||||
chmod 644 /secrets/DB_PASSWORD /secrets/JWT_SECRET /secrets/SERVICE_TOKEN
|
||||
volumes:
|
||||
- secrets:/secrets
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ services:
|
||||
[ -s /secrets/$$name ] || head -c 48 /dev/urandom | base64 | tr -dc 'A-Za-z0-9' > /secrets/$$name
|
||||
done
|
||||
# postgres reads it as uid 70, the app as uid 10001 — readable inside these containers only
|
||||
chmod 644 /secrets/DB_PASSWORD /secrets/JWT_SECRET
|
||||
chmod 644 /secrets/DB_PASSWORD /secrets/JWT_SECRET /secrets/SERVICE_TOKEN
|
||||
volumes:
|
||||
- secrets:/secrets
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ services:
|
||||
for name in DB_PASSWORD JWT_SECRET SERVICE_TOKEN; do
|
||||
[ -s /secrets/$$name ] || head -c 48 /dev/urandom | base64 | tr -dc 'A-Za-z0-9' > /secrets/$$name
|
||||
done
|
||||
chmod 644 /secrets/DB_PASSWORD /secrets/JWT_SECRET
|
||||
chmod 644 /secrets/DB_PASSWORD /secrets/JWT_SECRET /secrets/SERVICE_TOKEN
|
||||
volumes:
|
||||
- secrets:/secrets
|
||||
|
||||
|
||||
@@ -55,6 +55,9 @@ export class ApiService {
|
||||
setMailboxActive(id: number, active: boolean): Observable<Mailbox> {
|
||||
return this.http.put<Mailbox>(`/api/admin/mailboxes/${id}/active`, { active });
|
||||
}
|
||||
deleteMailbox(id: number): Observable<void> {
|
||||
return this.http.delete<void>(`/api/admin/mailboxes/${id}`);
|
||||
}
|
||||
|
||||
// folders
|
||||
folders(): Observable<Folder[]> { return this.http.get<Folder[]>('/api/folders'); }
|
||||
|
||||
@@ -91,6 +91,9 @@ import { AuthService } from '../../core/auth.service';
|
||||
<button type="button" class="ghost-btn !py-1" (click)="toggle(m)"
|
||||
[attr.aria-label]="(m.active ? 'Switch off ' : 'Switch on ') + m.email">
|
||||
<mat-icon>{{ m.active ? 'toggle_on' : 'toggle_off' }}</mat-icon></button>
|
||||
<button type="button" class="ghost-btn !py-1 hover:text-rose-400" (click)="remove(m)"
|
||||
[attr.aria-label]="'Delete ' + m.email" title="Delete the mailbox with all its mail">
|
||||
<mat-icon>delete</mat-icon></button>
|
||||
}
|
||||
@if (membersOf() === m.id) {
|
||||
<div class="basis-full mt-1 p-2 rounded-lg border border-cyan-500/20">
|
||||
@@ -227,6 +230,14 @@ export class MailboxesSection {
|
||||
});
|
||||
}
|
||||
|
||||
remove(m: Mailbox): void {
|
||||
if (!confirm('Delete ' + m.email + ' with ALL its mail? This can not be undone.')) return;
|
||||
this.api.deleteMailbox(m.id).subscribe({
|
||||
next: () => { this.toasts.ok(m.email + ' deleted'); this.load(); },
|
||||
error: e => this.toasts.error(errorText(e))
|
||||
});
|
||||
}
|
||||
|
||||
copy(text: string): void {
|
||||
navigator.clipboard?.writeText(text).then(() => this.toasts.ok('Copied'), () => this.toasts.error('Copy it by hand'));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user