import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MatIconModule } from '@angular/material/icon'; import { ApiService } from '../../core/api.service'; import { MailList, Mailbox, errorText } from '../../core/models'; import { ToastService } from '../../ui/toast.service'; /** * Settings → LISTS (administrators): alerts-prod@ → a copy to every member, into the list's folder. * Lists kept by Jambotron are edited here too, but Jambotron puts its own member set back on its next sync. */ @Component({ selector: 'app-lists-section', imports: [FormsModule, MatIconModule], changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'block xl:col-span-2' }, template: `

forward_to_inbox LISTS

Mail to a list goes to every member — each gets their own copy (own read/unread) in the list's folder. list+tag@ puts it into folder «tag» instead. Closed list: only logged-in senders (Kuma, Jambotron).

@ {{ domain() }}
Members
@for (m of mailboxes(); track m.id) { }
@if (editing()) { }
@if (error()) { }
` }) export class ListsSection { private api = inject(ApiService); private toasts = inject(ToastService); readonly lists = signal([]); readonly mailboxes = signal([]); readonly members = signal>(new Set()); readonly local = signal(''); readonly domains = signal([]); readonly domain = computed(() => this.domains()[0] ?? ''); readonly editing = signal(false); readonly error = signal(''); readonly busy = signal(false); folder = ''; description = ''; openToOutside = false; constructor() { this.api.config().subscribe({ next: c => this.domains.set(c.domains), error: () => {} }); this.api.mailboxes().subscribe({ next: list => this.mailboxes.set(list), error: () => {} }); this.load(); } load(): void { this.api.mailLists().subscribe({ next: list => this.lists.set(list), error: e => this.error.set(errorText(e)) }); } flip(email: string): void { const next = new Set(this.members()); if (next.has(email)) { next.delete(email); } else { next.add(email); } this.members.set(next); } edit(l: MailList): void { this.local.set(l.address.split('@')[0]); this.domains.set([l.address.split('@')[1], ...this.domains().filter(d => d !== l.address.split('@')[1])]); this.folder = l.folder ?? ''; this.description = l.description ?? ''; this.openToOutside = l.openToOutside; this.members.set(new Set(l.members)); this.editing.set(true); this.error.set(''); } reset(): void { this.local.set(''); this.folder = this.description = ''; this.openToOutside = false; this.members.set(new Set()); this.editing.set(false); } save(): void { const local = this.local().replace(/@.*$/, ''); if (!local) { this.error.set('Enter an address'); return; } const address = local + '@' + this.domain(); this.error.set(''); this.busy.set(true); this.api.saveMailList(address, { folder: this.folder.trim(), description: this.description.trim(), openToOutside: this.openToOutside, members: [...this.members()] }).subscribe({ next: saved => { this.busy.set(false); this.toasts.ok('List ' + saved.address + ' saved'); this.reset(); this.load(); }, error: e => { this.busy.set(false); this.error.set(errorText(e)); } }); } remove(l: MailList): void { if (!confirm('Delete list ' + l.address + '? Mail to it will be refused.')) return; this.api.deleteMailList(l.address).subscribe({ next: () => { this.toasts.ok('List ' + l.address + ' deleted'); this.load(); }, error: e => this.toasts.error(errorText(e)) }); } }