feat(messages): Phase 2 — customer (hello@) mailbox + inbound body capture
Second inbound mailbox and real message bodies for the Messages viewer.
Backend:
- Migration 154: mail_accounts table (additional inbound mailboxes beyond the
primary accounting IMAP) + received_emails.{account_key,to_address,body_html,
body_text}. Additive/guarded.
- emailIntakeService now polls the accounting mailbox AND every enabled
mail_accounts row. Extracted pollAccountOnce(cfg, {accountKey, routeToExpenses});
accounting keeps its exact attachment->expenses behavior, customer mail is
logged with its body and NOT routed to accounting. Inbound HTML is sanitized
server-side (sanitize-html) on ingest.
- adminEmail: /received gains an account filter + returns account_key/to_address
(bodies excluded from the list); new GET /received/:id returns the body;
GET/POST /accounts + /accounts/test manage the extra mailboxes.
Frontend:
- Customers inbox now pulls the hello@ mailbox; reading pane renders the
sanitized body in a strict (script-less, no same-origin) sandboxed iframe.
Accounting inbox shows bodies too. Toolbar context keys off the mailbox.
- CustomerMailboxCard in Settings -> Email (behind the messaging flag) to
configure + test the hello@ IMAP box.
No behavior change to the existing accounting inbound flow. Frontend build +
migration boot verified.
This commit is contained in:
@@ -125,7 +125,9 @@ export interface ImapPollResult {
|
||||
export interface ReceivedEmail {
|
||||
id: number;
|
||||
message_id: string | null;
|
||||
account_key?: string | null;
|
||||
from_address: string | null;
|
||||
to_address?: string | null;
|
||||
subject: string | null;
|
||||
received_at: string | null;
|
||||
attachment_count: number;
|
||||
@@ -134,11 +136,31 @@ export interface ReceivedEmail {
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
/** Single received email including its captured, server-sanitized body. */
|
||||
export interface ReceivedEmailDetail extends ReceivedEmail {
|
||||
body_html: string | null;
|
||||
body_text: string | null;
|
||||
}
|
||||
|
||||
export interface ReceivedEmailsResponse {
|
||||
items: ReceivedEmail[];
|
||||
pagination: { page: number; pageSize: number; total: number; totalPages: number };
|
||||
}
|
||||
|
||||
/** An additional inbound mailbox beyond the primary accounting IMAP. */
|
||||
export interface MailAccount {
|
||||
id?: number;
|
||||
account_key: string;
|
||||
label?: string | null;
|
||||
imap_host?: string | null;
|
||||
imap_port?: number;
|
||||
imap_secure?: boolean;
|
||||
imap_user?: string | null;
|
||||
imap_pass?: string;
|
||||
imap_folder?: string;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export const emailService = {
|
||||
// Get email configuration
|
||||
async getConfig(): Promise<EmailConfig> {
|
||||
@@ -181,10 +203,26 @@ export const emailService = {
|
||||
const response = await api.post<ImapPollResult>('/admin/email/incoming-config/poll', {});
|
||||
return response.data;
|
||||
},
|
||||
async listReceived(params: { page?: number; pageSize?: number } = {}): Promise<ReceivedEmailsResponse> {
|
||||
async listReceived(params: { page?: number; pageSize?: number; account?: string } = {}): Promise<ReceivedEmailsResponse> {
|
||||
const response = await api.get<ReceivedEmailsResponse>('/admin/email/received', { params });
|
||||
return response.data;
|
||||
},
|
||||
async getReceivedItem(id: number): Promise<ReceivedEmailDetail> {
|
||||
const response = await api.get<ReceivedEmailDetail>(`/admin/email/received/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
// Additional inbound mailboxes (e.g. the customer hello@ box).
|
||||
async listMailAccounts(): Promise<MailAccount[]> {
|
||||
const response = await api.get<{ items: MailAccount[] }>('/admin/email/accounts');
|
||||
return response.data.items;
|
||||
},
|
||||
async saveMailAccount(account: MailAccount): Promise<void> {
|
||||
await api.post('/admin/email/accounts', account);
|
||||
},
|
||||
async testMailAccount(account: Partial<MailAccount>): Promise<ImapTestResult> {
|
||||
const response = await api.post<ImapTestResult>('/admin/email/accounts/test', account);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Test email configuration
|
||||
async testEmail(testEmail: string): Promise<void> {
|
||||
|
||||
Reference in New Issue
Block a user