Почтовый кабинет: регистрация, папки, черновики, вложения, грид @jambotron/aria-grid, интерфейс по synthwave-шаблону

Бэкенд: регистрация со входом сразу и /api/config, профиль и смена пароля, свои папки, PATCH письма
(прочитано, флажок, перенос; Спам ставит отметку), корзина и очистка, черновики, несколько адресатов и копия,
вложения черновика уходят с письмом, вложения и заголовки To/Cc входящих сохраняются, письма заблокированных —
в Спам. Исправлено: загрузка файла (415), колонка content_id (500 на любых вложениях), SPA-маршрут с точкой.
Фронтенд переписан: все таблицы — грид, оформление — шаблон synthwave-design-showcase.html.

α
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-05 16:41:29 +01:00
parent f605fbfd85
commit 29b1beabe0
70 changed files with 3468 additions and 1737 deletions
+77
View File
@@ -0,0 +1,77 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Attachment, BlacklistEntry, Contact, Draft, Folder, Message, Profile, ServerConfig } from './models';
/** The mail server REST API (same origin, /api). */
@Injectable({ providedIn: 'root' })
export class ApiService {
private http = inject(HttpClient);
config(): Observable<ServerConfig> { return this.http.get<ServerConfig>('/api/config'); }
// profile
me(): Observable<Profile> { return this.http.get<Profile>('/api/me'); }
updateMe(displayName: string): Observable<Profile> { return this.http.put<Profile>('/api/me', { displayName }); }
changePassword(currentPassword: string, newPassword: string): Observable<unknown> {
return this.http.put('/api/me/password', { currentPassword, newPassword });
}
// folders
folders(): Observable<Folder[]> { return this.http.get<Folder[]>('/api/folders'); }
createFolder(name: string): Observable<Folder> { return this.http.post<Folder>('/api/folders', { name }); }
renameFolder(id: number, name: string): Observable<unknown> { return this.http.put(`/api/folders/${id}`, { name }); }
deleteFolder(id: number): Observable<unknown> { return this.http.delete(`/api/folders/${id}`); }
emptyFolder(id: number): Observable<{ deleted: number }> {
return this.http.delete<{ deleted: number }>(`/api/folders/${id}/messages`);
}
// messages
messages(folderId: number): Observable<Message[]> {
return this.http.get<Message[]>('/api/messages', { params: { folderId } });
}
search(folderId: number, keyword: string): Observable<Message[]> {
return this.http.get<Message[]>('/api/messages/search', { params: { folderId, keyword } });
}
message(id: number): Observable<Message> { return this.http.get<Message>(`/api/messages/${id}`); }
updateMessage(id: number, change: { read?: boolean; flagged?: boolean; folderId?: number }): Observable<Message> {
return this.http.patch<Message>(`/api/messages/${id}`, change);
}
deleteMessage(id: number): Observable<unknown> { return this.http.delete(`/api/messages/${id}`); }
// 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 });
}
// attachments
attachments(messageId: number): Observable<Attachment[]> {
return this.http.get<Attachment[]>(`/api/messages/${messageId}/attachments`);
}
upload(messageId: number, file: File): Observable<Attachment> {
const form = new FormData();
form.append('file', file, file.name);
return this.http.post<Attachment>(`/api/messages/${messageId}/attachments`, form);
}
deleteAttachment(id: number): Observable<unknown> { return this.http.delete(`/api/attachments/${id}`); }
download(id: number): Observable<Blob> {
return this.http.get(`/api/attachments/${id}/download`, { responseType: 'blob' });
}
// contacts
contacts(): Observable<Contact[]> { return this.http.get<Contact[]>('/api/contacts'); }
addContact(contact: Contact): Observable<unknown> { return this.http.post('/api/contacts', contact); }
updateContact(id: number, contact: Contact): Observable<unknown> { return this.http.put(`/api/contacts/${id}`, contact); }
deleteContact(id: number): Observable<unknown> { return this.http.delete(`/api/contacts/${id}`); }
// blacklist
blacklist(): Observable<BlacklistEntry[]> { return this.http.get<BlacklistEntry[]>('/api/spam/blacklist'); }
block(senderEmail: string, reason: string): Observable<unknown> {
return this.http.post('/api/spam/blacklist', { senderEmail, reason });
}
unblock(senderEmail: string): Observable<unknown> {
return this.http.delete('/api/spam/blacklist', { params: { senderEmail } });
}
}