Несколько ящиков у одного логина, общие ящики, кто ответил, вход ящик*логин
continuous-integration/drone/pr Build is passing
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>
This commit is contained in:
@@ -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, MailList, Mailbox, MailboxCreated, Message, Profile, ServerConfig } from './models';
|
||||
import { Attachment, BlacklistEntry, Contact, Draft, Folder, MailList, Mailbox, MailboxCreated, MailboxEntry, Message, Profile, ServerConfig } from './models';
|
||||
|
||||
/** The mail server REST API (same origin, /api). */
|
||||
@Injectable({ providedIn: 'root' })
|
||||
@@ -19,8 +19,22 @@ export class ApiService {
|
||||
|
||||
// mailboxes — administrators only
|
||||
mailboxes(): Observable<Mailbox[]> { return this.http.get<Mailbox[]>('/api/admin/mailboxes'); }
|
||||
createMailbox(email: string, displayName: string, password: string): Observable<MailboxCreated> {
|
||||
return this.http.post<MailboxCreated>('/api/admin/mailboxes', { email, displayName, password });
|
||||
createMailbox(email: string, displayName: string, password: string, shared = false): Observable<MailboxCreated> {
|
||||
return this.http.post<MailboxCreated>('/api/admin/mailboxes', { email, displayName, password, shared });
|
||||
}
|
||||
setMailboxMembers(id: number, members: string[]): Observable<Mailbox> {
|
||||
return this.http.put<Mailbox>(`/api/admin/mailboxes/${id}/members`, { members });
|
||||
}
|
||||
// the logged-in user's mailboxes (left menu)
|
||||
myMailboxes(): Observable<MailboxEntry[]> { return this.http.get<MailboxEntry[]>('/api/mailboxes'); }
|
||||
mailboxSession(email: string): Observable<{ token: string; email: string }> {
|
||||
return this.http.post<{ token: string; email: string }>(`/api/mailboxes/${encodeURIComponent(email)}/session`, {});
|
||||
}
|
||||
attachMailbox(email: string, password: string): Observable<MailboxEntry> {
|
||||
return this.http.post<MailboxEntry>('/api/mailboxes/attach', { email, password });
|
||||
}
|
||||
leaveMailbox(email: string): Observable<void> {
|
||||
return this.http.delete<void>(`/api/mailboxes/${encodeURIComponent(email)}`);
|
||||
}
|
||||
newMailboxPassword(id: number): Observable<MailboxCreated> {
|
||||
return this.http.post<MailboxCreated>(`/api/admin/mailboxes/${id}/password`, {});
|
||||
@@ -67,8 +81,8 @@ export class ApiService {
|
||||
// compose
|
||||
createDraft(draft: Draft): Observable<Message> { return this.http.post<Message>('/api/drafts', draft); }
|
||||
updateDraft(id: number, draft: Draft): Observable<Message> { return this.http.put<Message>(`/api/drafts/${id}`, draft); }
|
||||
send(draft: Draft, draftId: number | null): Observable<Message> {
|
||||
return this.http.post<Message>('/api/send', { ...draft, draftId });
|
||||
send(draft: Draft, draftId: number | null, replyToId: number | null = null): Observable<Message> {
|
||||
return this.http.post<Message>('/api/send', { ...draft, draftId, replyToId });
|
||||
}
|
||||
|
||||
// attachments
|
||||
|
||||
@@ -40,6 +40,18 @@ export class AuthService {
|
||||
this.openedBy.set(read('adminEmail'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch to another mailbox of the same login (left menu). Going back to the mailbox an administrator came
|
||||
* from ends the administrator's visit.
|
||||
*/
|
||||
switchTo(response: TokenResponse): void {
|
||||
if (this.openedBy() && response.email === this.openedBy()) {
|
||||
this.backToAdmin();
|
||||
return;
|
||||
}
|
||||
this.store(response);
|
||||
}
|
||||
|
||||
/** Back to the administrator's own mailbox. */
|
||||
backToAdmin(): void {
|
||||
const token = read('adminToken');
|
||||
|
||||
@@ -23,6 +23,16 @@ export interface Message {
|
||||
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 {
|
||||
@@ -63,7 +73,9 @@ export interface Mailbox {
|
||||
displayName: string;
|
||||
active: boolean;
|
||||
admin: boolean;
|
||||
trap: boolean; // what it sends stays in its own INBOX, nothing leaves the server (staging@)
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user