Рассылки, папки по +метке, ловушка только для чужих адресов, служебное API для Jambotron
continuous-integration/drone/pr Build is passing
continuous-integration/drone/pr Build is passing
- V6 mail_lists + mail_list_members: письмо на alerts-prod@ → каждому участнику своя копия в папку рассылки; закрытая рассылка — только от вошедших отправителей (550 снаружи) - bob+prod@ → ящик bob@, папка prod (создаётся сама; системные папки нельзя) - TRAP: чужие адреса → одна копия в INBOX отправителя; наши ящики и рассылки доставляются - /api/service/** (SERVICE_TOKEN из secrets-init): аккаунты (пароль один раз, выключение → вон из рассылок), рассылки - MailDirectory: одно пространство адресов для ящиков и рассылок, без «+» в именах - Настройки → LISTS для администраторов - 86 тестов; живой прогон на Postgres 17: миграции V1–V6, API, SMTP (рассылка/метка/ловушка) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -162,6 +162,34 @@ public class MailService {
|
||||
.switchIfEmpty(Mono.error(badRequest("System folders cannot be renamed or deleted")));
|
||||
}
|
||||
|
||||
private static final java.util.regex.Pattern DELIVERY_FOLDER = java.util.regex.Pattern.compile("[A-Za-z0-9][A-Za-z0-9._-]{0,49}");
|
||||
|
||||
/**
|
||||
* A "+tag" / list folder that may be used as a folder name, or null (→ INBOX). A sender never puts mail into
|
||||
* a system folder (Spam, Trash, Sent …) this way.
|
||||
*/
|
||||
static String deliveryFolderName(String raw) {
|
||||
if (raw == null || !DELIVERY_FOLDER.matcher(raw).matches() || SYSTEM_FOLDERS.stream().anyMatch(raw::equalsIgnoreCase)) {
|
||||
return null;
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
/** The folder with this name (any case), created as an own folder when missing; two at once — one wins. */
|
||||
private Mono<Folder> deliveryFolder(Long userId, String name) {
|
||||
Mono<Folder> existing = folderRepository.findByUserId(userId)
|
||||
.filter(folder -> folder.getName().equalsIgnoreCase(name))
|
||||
.next();
|
||||
return existing.switchIfEmpty(Mono.defer(() -> {
|
||||
Folder folder = new Folder();
|
||||
folder.setUserId(userId);
|
||||
folder.setName(name);
|
||||
folder.setCreatedAt(Instant.now());
|
||||
return folderRepository.save(folder)
|
||||
.onErrorResume(org.springframework.dao.DataIntegrityViolationException.class, e -> existing);
|
||||
}));
|
||||
}
|
||||
|
||||
private Mono<Folder> systemFolder(Long userId, String name) {
|
||||
return folderRepository.findByUserIdAndName(userId, name)
|
||||
.switchIfEmpty(Mono.error(new IllegalStateException(name + " folder not found")));
|
||||
@@ -253,12 +281,25 @@ public class MailService {
|
||||
public Mono<Message> receiveMessage(String senderEmail, String senderName, String recipientEmail,
|
||||
String subject, String body, boolean isHtml, boolean externalSpamFlag,
|
||||
String toHeader, String ccHeader) {
|
||||
return receiveMessage(senderEmail, senderName, recipientEmail, subject, body, isHtml, externalSpamFlag,
|
||||
toHeader, ccHeader, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param folder own folder for the message ("+tag" of the address, folder of a mailing list), created when
|
||||
* missing; null or not a valid folder name — INBOX. Spam still goes to Spam.
|
||||
*/
|
||||
public Mono<Message> receiveMessage(String senderEmail, String senderName, String recipientEmail,
|
||||
String subject, String body, boolean isHtml, boolean externalSpamFlag,
|
||||
String toHeader, String ccHeader, String targetFolder) {
|
||||
return userRepository.findByEmail(recipientEmail)
|
||||
.switchIfEmpty(Mono.error(new IllegalArgumentException("Recipient not found: " + recipientEmail)))
|
||||
.flatMap(recipient -> spamFilterService.isBlacklisted(recipient.getId(), senderEmail)
|
||||
.flatMap(blacklisted -> {
|
||||
boolean isSpam = blacklisted || externalSpamFlag;
|
||||
return systemFolder(recipient.getId(), isSpam ? "Spam" : "INBOX")
|
||||
String own = isSpam ? null : deliveryFolderName(targetFolder);
|
||||
return (own == null ? systemFolder(recipient.getId(), isSpam ? "Spam" : "INBOX")
|
||||
: deliveryFolder(recipient.getId(), own))
|
||||
.flatMap(folder -> {
|
||||
Message message = new Message();
|
||||
message.setFolderId(folder.getId());
|
||||
|
||||
Reference in New Issue
Block a user