Рассылки, папки по +метке, ловушка только для чужих адресов, служебное API для Jambotron
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:
2026-11-07 14:15:24 +01:00
co-authored by Claude Opus 5.5
parent 3603b9d315
commit cf84797e68
28 changed files with 1013 additions and 71 deletions
+8 -1
View File
@@ -1,7 +1,7 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Attachment, BlacklistEntry, Contact, Draft, Folder, Mailbox, MailboxCreated, Message, Profile, ServerConfig } from './models';
import { Attachment, BlacklistEntry, Contact, Draft, Folder, MailList, Mailbox, MailboxCreated, Message, Profile, ServerConfig } from './models';
/** The mail server REST API (same origin, /api). */
@Injectable({ providedIn: 'root' })
@@ -28,6 +28,13 @@ export class ApiService {
openMailbox(id: number): Observable<{ token: string; email: string }> {
return this.http.post<{ token: string; email: string }>(`/api/admin/mailboxes/${id}/open`, {});
}
mailLists(): Observable<MailList[]> { return this.http.get<MailList[]>('/api/admin/mailboxes/lists'); }
saveMailList(address: string, list: { folder: string; description: string; openToOutside: boolean; members: string[] }): Observable<MailList> {
return this.http.put<MailList>(`/api/admin/mailboxes/lists/${encodeURIComponent(address)}`, list);
}
deleteMailList(address: string): Observable<void> {
return this.http.delete<void>(`/api/admin/mailboxes/lists/${encodeURIComponent(address)}`);
}
setMailboxTrap(id: number, trap: boolean): Observable<Mailbox> {
return this.http.put<Mailbox>(`/api/admin/mailboxes/${id}/trap`, { trap });
}
+10
View File
@@ -69,6 +69,16 @@ export interface Mailbox {
createdAt: string;
}
/** MailLists.MailList — mail to the address → a copy to every member, into the folder (null — INBOX) */
export interface MailList {
id: number;
address: string;
folder: string | null;
description: string | null;
openToOutside: boolean;
members: string[];
}
/** password — generated by the server, shown once; null when the administrator chose it */
export interface MailboxCreated {
mailbox: Mailbox;