Files
reactive-mail-server/frontend/src/app/core/models.ts
T
Liosha84andClaude Opus 5.5 c765c51cf0
continuous-integration/drone/pr Build is passing
Несколько ящиков у одного логина, общие ящики, кто ответил, вход ящик*логин
- V7: users.is_shared, mailbox_access (MEMBER от админа/Jambotron, ATTACHED по паролю ящика), messages.answered_by
- /api/mailboxes: мои ящики с непрочитанными, сессия другого ящика (JWT с claim by), подключить по паролю
  (LoginGuard), убрать из меню
- общий ящик: без своего входа (веб/SMTP/IMAP), участники открывают; «прочитано» общее; ответ из веба → answered_by
- SMTP/IMAP: ящик*логин + пароль логина (как master user в Dovecot)
- открытый чужой ящик (общий, подключённый, OPEN админа): профиль и пароль не меняются (403)
- админ: создать shared, участники ящика (PUT /{id}/members); служебное API: PUT /shared/{address};
  выключение аккаунта убирает и из общих ящиков
- UI: MAILBOXES в левом меню (переключение, ⊕ подключить, × убрать), shared/участники в настройках,
  «Answered by» у письма
- 88 тестов; живой прогон на Postgres 17 (апгрейд V6→V7 на данных), скриншоты UI

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 14:27:11 +01:00

151 lines
4.2 KiB
TypeScript

export interface Folder {
id: number;
name: string;
system: boolean;
messageCount: number;
unreadCount: number;
}
export interface Message {
id: number;
folderId: number;
senderEmail: string;
senderName?: string | null;
recipientEmail: string;
ccEmail?: string | null;
subject: string;
body: string;
html: boolean;
read: boolean;
flagged: boolean;
spam: boolean;
messageSize?: number | null;
receivedAt: string;
hasAttachments: boolean;
attachmentCount: number;
answeredBy?: string | null; // login that answered it from the web client (shared mailboxes)
}
/** MailboxAccess.Entry — a mailbox of the logged-in user (left menu) */
export interface MailboxEntry {
email: string;
displayName: string;
kind: 'OWN' | 'MEMBER' | 'ATTACHED';
shared: boolean;
unread: number;
}
export interface Attachment {
id: number;
filename: string;
contentType: string;
sizeBytes: number;
}
export interface Contact {
id?: number;
name: string;
email: string;
phone?: string;
company?: string;
}
export interface BlacklistEntry {
id: number;
senderEmail: string;
reason?: string | null;
createdAt?: string;
}
export interface Profile {
email: string;
displayName: string;
usedBytes: number;
quotaBytes: number;
createdAt: string;
admin?: boolean; // may manage mailboxes (Settings → Mailboxes)
}
/** MailboxAdminController.MailboxView */
export interface Mailbox {
id: number;
email: string;
displayName: string;
active: boolean;
admin: boolean;
trap: boolean; // what it sends to outside addresses stays in its own INBOX (staging@)
shared: boolean; // shared mailbox: no login of its own, members open it
members: string[]; // who may open it
usedBytes: number;
quotaBytes: number;
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;
password: string | null;
}
export interface ServerConfig {
registrationEnabled: boolean;
domains: string[];
}
export interface Draft {
to: string;
cc: string;
subject: string;
body: string;
html: boolean;
}
/** System folders: label and icon in the web client. */
export const FOLDER_VIEW: Record<string, { label: string; icon: string }> = {
INBOX: { label: 'Inbox', icon: 'inbox' },
Drafts: { label: 'Drafts', icon: 'edit_note' },
Sent: { label: 'Sent', icon: 'send' },
Spam: { label: 'Spam', icon: 'report' },
Trash: { label: 'Trash', icon: 'delete' }
};
export function folderLabel(folder: Folder): string {
return folder.system ? (FOLDER_VIEW[folder.name]?.label ?? folder.name) : folder.name;
}
export function folderIcon(folder: Folder): string {
return folder.system ? (FOLDER_VIEW[folder.name]?.icon ?? 'folder') : 'folder';
}
export function formatBytes(bytes: number | null | undefined): string {
const value = bytes ?? 0;
if (value < 1024) return `${value} B`;
if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB`;
if (value < 1024 * 1024 * 1024) return `${(value / 1024 / 1024).toFixed(1)} MB`;
return `${(value / 1024 / 1024 / 1024).toFixed(2)} GB`;
}
/** Text of an API error: our {error}, Spring's {message}, or the status. */
export function errorText(error: unknown): string {
const e = error as { error?: { error?: string; message?: string; status?: number } | string; status?: number; message?: string };
if (e?.error && typeof e.error === 'object') {
// Spring's error body: {status, error: "Bad Request", message: "No such mailbox: …"} — the reason is in message
const body = e.error;
const springBody = typeof body.status === 'number';
return (springBody ? body.message || body.error : body.error || body.message) || `Error ${e.status ?? ''}`.trim();
}
if (typeof e?.error === 'string' && e.error) return e.error;
if (e?.status === 0) return 'Server is not reachable';
return e?.message || 'Something went wrong';
}