Set up an email channel
Use this when a customer wants Munin to send and receive email under one of their addresses (e.g. support@acme.com).
TL;DR
- Decide outbound: their own SMTP server, or send through Munin's configured Mailer (Resend).
- Decide inbound: poll IMAP, or rely on the customer forwarding to a
MUNIN_EMAIL_REPLY_DOMAINaddress. - Call
conv_email_setup_channelwith the full config. - Call
conv_email_test_channelto verify creds without sending mail. - Confirm the channel appears in
conv_list_channelswithactive: true.
Step 1 — gather the config
Required from the operator:
- Addressing:
fromAddress(must be a real mailbox they control), optionalfromName(e.g. "Acme Support"). - Outbound mode:
smtp— host, port, secure (TLS yes/no), username, password. Most providers: port 587 withsecure: false(STARTTLS) or 465 withsecure: true.mailer— no extra config; uses the Munin instance's configured Mailer. Best for self-host without an SMTP relay.
- Inbound (optional): IMAP host, port, secure, username, password, mailbox name (defaults to
INBOX).
Passwords are stored encrypted via pgcrypto. If you re-call conv_email_setup_channel later with empty password fields, the prior encrypted password is preserved — useful when the operator only wants to update non-secret fields.
Step 2 — create the channel
Call conv_email_setup_channel (admin):
{
"name": "Acme Support",
"config": {
"addressing": {
"fromAddress": "support@acme.com",
"fromName": "Acme Support",
"replyToTemplate": "support+conv-{conversationId}@acme.com"
},
"outbound": {
"provider": "smtp",
"host": "smtp.acme.com",
"port": 587,
"secure": false,
"username": "support@acme.com",
"password": "<plaintext-once>"
},
"inbound": {
"provider": "imap",
"host": "imap.acme.com",
"port": 993,
"secure": true,
"username": "support@acme.com",
"password": "<plaintext-once>",
"mailbox": "INBOX"
}
}
}
Returns the channel ID, type 'email', and the redacted DTO (passwords show as ••••).
To update an existing channel, pass channelId and only the fields you want to change. Empty password strings preserve the stored secret.
Step 3 — verify
Call conv_email_test_channel with { channelId }. It performs:
- An SMTP
verify()(no mail sent). - An IMAP
connect()thenlogout()(no fetch).
Returns { smtp: 'ok' | 'error: <message>', imap: 'ok' | 'error: <message>' | 'not configured' }.
If smtp reports auth failure, the most common causes are app-password-required (Gmail, iCloud), the wrong port for the cipher (587 STARTTLS vs 465 implicit-TLS), or a region-specific endpoint (Microsoft 365 enforces smtp.office365.com).
Step 4 — confirm registration
Call conv_list_channels. Look for the new row with type: 'email', active: true, and the addressing block.
What happens next
- Outbound: when an admin uses
conv_send_messageon a conversation tied to this channel, the message is enqueued inconv_message_deliveries. TheOutboundDeliveryWorkerdrains that queue and the email adapter sends via SMTP (or the Mailer). - Inbound (if IMAP is configured): the
InboundPollWorkerticks every 60s, fetches new UIDs, threads each message into an existing conversation (viaIn-Reply-To+Referencesheaders) or opens a new one. End-user senders are auto-created asconv_contacts.
Troubleshooting
- No outbound delivery — check
conv_message_deliveriesrows for the channel.status='dead'means 5 attempts failed; theerrorcolumn has the SMTP response.conv_email_test_channelis the fastest way to check creds. - Inbound stuck —
conv_inbound_state.cursor.lastUidshows the high-water mark;last_polled_atshows the most recent tick;last_errorcarries any IMAP error. - Email lands in spam at the recipient — confirm SPF / DKIM / DMARC for the
fromAddressdomain. Munin doesn't manage DNS.