Files
asterisk-voicemail/docs/CONFIGURATION.md
jp b8c60605fd Update all documentation to current MySQL + React SPA + vm_api.py architecture
README, docs/{ARCHITECTURE,CONFIGURATION,INSTALL,OPERATIONS,SECURITY,TESTING}
and CHANGELOG were stale: they described the original SQLite + CardDAV +
zero-JS vm_web portal. Brought them in line with the actual code:
- MySQL (PyMySQL) as the default store; SQLite fallback only
- vm_contacts resolves from the MySQL contacts table (file/google/carddav removed)
- React SPA frontend + vm_api.py JSON API (:8098); vm_web.py now serves /audio/
- Apache vhost proxies /api/ -> :8098, /audio/ -> :8099, serves the SPA
- CSP relaxed to script-src 'self' 'unsafe-inline' (UI is now JavaScript)
- Added CHANGELOG 1.2.0 entry for the React/vm_api frontend work
2026-08-13 20:20:39 +01:00

7.1 KiB

Configuration

The backend services run from /opt/vm-transcribe/. Live secret-bearing files live there (some git-ignored). The repo ships *.example templates under config/.

Most settings can also be set via environment variables, which is how the systemd unit wires production values. Env vars override file values when both are present unless noted.


Environment variables

Shared / store (vm_store.py, used by mailcmd, web, api, import)

Variable Default Meaning
VM_DB /var/lib/vm-transcribe/voicemail.db SQLite path — used only as a fallback when no MySQL is configured.
VM_AUDIO_DIR /var/lib/vm-transcribe/audio Where copied recordings live (<sha[:2]>/<sha>.wav).
VM_LOG /var/log/asterisk/vm_mailcmd.log Log path.
VM_MODEL_CACHE /opt/vm-transcribe/models Whisper model download/load dir.
VM_WHISPER_MODEL base.en Model name. tiny/base/small trade speed for accuracy.
VM_SPOOL /var/spool/asterisk/voicemail Spool root to scan (import).
VM_ASTERISK_CONF /etc/asterisk/voicemail.conf For PIN lookup.

MySQL (default store + contacts)

Set these as env vars (the API unit reads /opt/vm-transcribe/api.env) or put them in /opt/vm-transcribe/db_secret (mode 640 root:root, KEY=VALUE lines). When a credentials source exists, vm_store uses MySQL; otherwise it falls back to SQLite.

Variable Default Meaning
VM_MYSQL_HOST localhost MySQL host.
VM_MYSQL_USER asterisk Least-privilege DB user (owns the asterisk DB; not root).
VM_MYSQL_PASSWORD (from db_secret) DB password.
VM_MYSQL_DB asterisk Database name — the same DB Asterisk's CDR uses.

db_secret key names: MYSQL_HOST, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DB.

Portal / API hardening

Var Default Meaning
VM_SESSION_HOURS 12 Session lifetime.
VM_MAX_FAILS 5 Failed attempts per (mailbox, IP) before locking.
VM_LOCK_MINUTES 15 Lock duration.
VM_INSECURE_COOKIE unset Set to 1/yes/true only for plain-HTTP testing; normally cookies are Secure.
VM_BASE_PATH "" Set to e.g. /voicemail if served under a sub-path.
VM_CORS_ORIGINS * Comma list of allowed CORS origins for the JSON API.

voicemail.conf (Asterisk)

One line is all the pipeline needs:

mailcmd=/opt/vm-transcribe/venv/bin/python3 /opt/vm-transcribe/vm_mailcmd.py

Place it in the [general] section (or any context). After editing:

sudo asterisk -rx 'voicemail reload'

install.sh backs up the file and adds this line automatically.


telegram.conf

[telegram]
enabled = yes
token   = 123456789:AAH...          ; from @BotFather
default_chat_id =                    ; fallback for unmapped mailboxes (blank = none)
send_audio      = yes               ; voice note with caption
send_transcript = yes               ; full transcript as a follow-up message
timeout         = 20                ; seconds per send attempt

[mailbox:1001]
chat_id = 123456789                 ; single recipient

[mailbox:1002]
chat_id = 5551, 5552                ; comma list fans out to several people
send_transcript = no                ; any global key can be overridden per mailbox

[mailbox:1003]
chat_id = -1001234567890            ; negative = group chat
send_audio = no                     ; text-only DM

Routing rules (evaluated per voicemail):

  1. Match the [mailbox:N] section for the voicemail's mailbox number.
  2. chat_id may be a single id, a comma-separated list (fan-out), or a group id (negative).
  3. If no mailbox section matches, fall back to default_chat_id.
  4. If enabled = no or no route resolves, nothing is sent — email still goes out.
  5. The file is re-read on every voicemail; no reload needed after editing.

Delivery fallback chain (per chat id, tried in order): sendVoicesendDocumentsendMessage (text-only). So a Telegram outage or a bad audio file still delivers the summary.

Contact resolution note: a user must message the bot once before it can DM them. Telegram does not permit bots to initiate conversations.


contacts.conf

[contacts]
enabled = yes
backends = mysql        ; ONLY 'mysql' is implemented (MySQL contacts table).
                        # The schema still parses file/google/carddav entries,
                        # but the registry is {"mysql": lookup_mysql} so any
                        # other value is ignored / logged as unknown.
cache_path = /var/lib/vm-transcribe/contacts_cache.json
cache_ttl  = 86400

[file]      ; NOT USED — kept for reference only
[google]    ; NOT USED — kept for reference only
[carddav]   ; NOT USED — kept for reference only

Contacts are resolved exclusively from the MySQL contacts table on the asterisk DB. Numbers are stored canonical E.164; normalize_uk() collapses +44… and 0… (and 0044…) so both forms match. Matching uses the full digit string (up to 15 digits), not just last-9. Creating/updating a contact via the API backfills messages.contact_name/contact_email for matching callerids.

Load the table from a vCard/CSV export with:

sudo -u asterisk /opt/vm-transcribe/venv/bin/python3 \
     /opt/vm-transcribe/vm_import_contacts.py /path/to/contacts.vcf

Portal settings (per mailbox, in MySQL)

These are edited from the React portal's Settings page by each user; they are not config files. Every value defaults to yes.

Setting Effect
summarise Run the summariser (if off, the raw transcript is shown).
telegram Forward new voicemails to the mailbox's Telegram route (requires telegram.conf).
email_notify Send the enriched email (if off, the message is stored but not emailed).
highlight Colour the caller chip when a name is resolved from contacts.
max_inbox Keep at most N messages; oldest beyond N are auto-deleted (0 = unlimited).

systemd units

Two units, both running as asterisk and binding loopback only.

vm-api.service (:8098 JSON API)

Environment=VM_SESSION_HOURS=12
EnvironmentFile=-/opt/vm-transcribe/api.env     ; MySQL creds + VM_* overrides
# Hardening: ProtectSystem=full, ProtectHome=read-only, NoNewPrivileges,
# PrivateTmp, IPAddressAllow=127.0.0.1
ExecStart=/opt/vm-transcribe/venv/bin/uvicorn vm_api:app --host 127.0.0.1 --port 8098 --workers 2

vm-portal.service (:8099 legacy HTML / audio)

Environment=VM_DB=/var/lib/vm-transcribe/voicemail.db
Environment=VM_AUDIO_DIR=/var/lib/vm-transcribe/audio
Environment=VM_ASTERISK_CONF=/etc/asterisk/voicemail.conf
Environment=VM_SESSION_HOURS=12
ExecStart=/opt/vm-transcribe/venv/bin/python -m uvicorn vm_web:app \
    --host 127.0.0.1 --port 8099 --proxy-headers --forwarded-allow-ips 127.0.0.1
# Hardening: NoNewPrivileges, PrivateTmp, ProtectSystem=full, ProtectHome,
# ReadWritePaths=/var/lib/vm-transcribe /var/log/asterisk,
# IPAddressAllow=localhost, IPAddressDeny=any

A service restart clears the in-memory lockout counters. Testing lockout will lock you out of your own verification for VM_LOCK_MINUTES.