Администраторы почты из Jambotron: каждый ADMIN управляет ящиками
continuous-integration/drone/pr Build is passing

users.is_admin (V8) — ставит только служебное API (PUT /api/service/accounts
с "admin"), ящик сам себя администратором не сделает. MailAdmins: список из
конфигурации + выданные Jambotron (загружаются при старте, меняются сразу).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-07 19:44:32 +01:00
co-authored by Claude Opus 5.5
parent 01c1e5504a
commit 7e09282a88
7 changed files with 107 additions and 14 deletions
@@ -40,10 +40,11 @@ public class ServiceController {
private static final Logger log = LoggerFactory.getLogger(ServiceController.class);
/** @param displayName null — unchanged (new account: the name before @) */
public record AccountRequest(@Size(max = 100) String displayName, Boolean active) {
/** @param admin mail administrator (Jambotron ADMIN); null — leave as it is */
public record AccountRequest(@Size(max = 100) String displayName, Boolean active, Boolean admin) {
}
public record AccountView(String email, String displayName, boolean active, List<String> lists) {
public record AccountView(String email, String displayName, boolean active, boolean admin, List<String> lists) {
}
/** @param password generated, only when the account was just created or got a new password */
@@ -59,10 +60,12 @@ public class ServiceController {
private final MailLists lists;
private final MailDirectory directory;
private final MailboxAccess access;
private final com.mailserver.config.MailAdmins admins;
public ServiceController(@Value("${mailserver.service-token:}") String token, UserService users, MailLists lists,
MailDirectory directory, MailboxAccess access) {
MailDirectory directory, MailboxAccess access, com.mailserver.config.MailAdmins admins) {
this.access = access;
this.admins = admins;
this.token = token.isBlank() ? null : token.trim().getBytes(StandardCharsets.UTF_8);
this.users = users;
this.lists = lists;
@@ -84,13 +87,14 @@ public class ServiceController {
@PathVariable String email, @RequestBody Mono<AccountRequest> body) {
check(auth);
String address = directory.address(email);
return body.defaultIfEmpty(new AccountRequest(null, null)).flatMap(request -> users.findOptional(address)
return body.defaultIfEmpty(new AccountRequest(null, null, null)).flatMap(request -> users.findOptional(address)
.flatMap(user -> update(user, request).flatMap(this::view)
.map(view -> ResponseEntity.ok(new AccountResult(view, null))))
.switchIfEmpty(Mono.defer(() -> {
String password = MailDirectory.randomPassword();
return directory.createMailbox(address, request.displayName(), password)
.flatMap(user -> Boolean.FALSE.equals(request.active()) ? update(user, request) : Mono.just(user))
.flatMap(user -> Boolean.FALSE.equals(request.active()) || Boolean.TRUE.equals(request.admin())
? update(user, request) : Mono.just(user))
.doOnNext(user -> log.info("Mailbox {} created by Jambotron", user.getEmail()))
.flatMap(this::view)
.map(view -> ResponseEntity.status(HttpStatus.CREATED).body(new AccountResult(view, password)));
@@ -152,8 +156,14 @@ public class ServiceController {
}
private Mono<User> update(User user, AccountRequest request) {
Mono<User> named = request.displayName() == null || request.displayName().isBlank() ? Mono.just(user)
Mono<User> renamed = request.displayName() == null || request.displayName().isBlank() ? Mono.just(user)
: users.updateDisplayName(user, request.displayName());
Mono<User> named = request.admin() == null || request.admin() == user.isAdmin() ? renamed
: renamed.flatMap(u -> users.setAdmin(u, request.admin()))
.doOnNext(u -> {
admins.grant(u.getEmail(), u.isAdmin());
log.info("Mailbox {} {} mail administrator by Jambotron", u.getEmail(), u.isAdmin() ? "made" : "is no longer");
});
if (request.active() == null || request.active() == user.isActive()) {
return named;
}
@@ -166,7 +176,7 @@ public class ServiceController {
private Mono<AccountView> view(User user) {
return lists.listsOf(user.getId()).collectList()
.map(in -> new AccountView(user.getEmail(), user.getDisplayName(), user.isActive(), in));
.map(in -> new AccountView(user.getEmail(), user.getDisplayName(), user.isActive(), admins.isAdmin(user.getEmail()), in));
}
/** 404 when the API is off, 401 for a wrong token (constant-time compare). */