voicemail: index messages, live event, email with the recording

services/voicemail.ts rescans a mailbox on Asterisk's MessageWaiting event
(and every mailbox on AMI connect) instead of watching files, upserts
voicemail_messages by msg_id, marks vanished ones deleted, sends
voicemail.new to the owner only, and emails the wav. Delete-after-email
uses AMI VoicemailRemove, and only when the audio was attached. Asterisk
24 has VoicemailMove/VoicemailRemove, so 04 §5.6 drops the renumbering
plan. New VOICEMAIL_DIR setting; e2e m4-t2 checks the email arrives
within 10 s with a playable attachment (SMTP sink shared with m3-t4).

Task: M4-T2
This commit is contained in:
iamdoubz committed 2026-10-17 22:33:52 -05:00
1 parent 0d29131d18
commit 8d752ba705
16 files changed
+642 -58

No files matched your search

+1
View File
@@ -10,3 +10,4 @@ export * from './live.ts';
export * from './trunks.ts';
export * from './dids.ts';
export * from './outbound.ts';
export * from './voicemail.ts';
+3
View File
@@ -1,5 +1,6 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Live state shapes sent over the /api/live WebSocket (04 §5.1, §5.2).
import type { VoicemailDto } from './voicemail.ts';
export type ExtensionState =
'idle' | 'inuse' | 'busy' | 'unavailable' | 'ringing' | 'onhold' | 'ringinuse';
@@ -59,6 +60,8 @@ export type LiveMessage =
| { type: 'trunk.updated'; data: TrunkLive }
| { type: 'trunk.removed'; data: { trunkId: string } }
| { type: 'emergency.call'; data: EmergencyCall }
/** Only to the mailbox owner. */
| { type: 'voicemail.new'; data: VoicemailDto & { userId: string } }
| { type: 'ami.status'; data: { connected: boolean } }
| {
type: 'system.configApplied';
+15
View File
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Voicemail messages (02 voicemail_messages, 04 §5.6).
/** Asterisk's mailbox folders that we index. */
export const VOICEMAIL_FOLDERS = ['INBOX', 'Old', 'Urgent'] as const;
export type VoicemailFolder = (typeof VOICEMAIL_FOLDERS)[number];
export type VoicemailDto = {
id: string;
folder: VoicemailFolder;
callerNumber: string | null;
callerName: string | null;
durationS: number;
receivedAt: string;
};