Integrations
Email Channel
Connect a mailbox as a HelpStack channel: inbound mail is polled over IMAP and replies are sent over SMTP.
Quick facts#
| Channel type | EMAIL |
| Inbound | IMAP polling worker (npm run email:poll) |
| Outbound | SMTP via nodemailer (channel credentials) |
| Credentials | Stored encrypted (AES-256-GCM) per channel |
| Transport security | IMAP over TLS |
| Poll interval | ~10s (dev) / ~60s (prod) |
Inbound: IMAP polling#
A background poller connects to each active EMAIL channel's IMAP server (TLS) and, on each cycle:
- Fetches recent unprocessed mail.
- Parses text, HTML, and attachments.
- Deduplicates by message-id + preview (so the same mail isn't ingested twice).
- Creates a conversation keyed by the sender's email address.
- Uploads attachments to storage.
- Marks the mail as processed.
How "processed" is tracked
The poller prefers a custom IMAP keyword $HelpStackProcessed, which leaves the \Seen flag untouched (so the message still looks unread in other clients). If the server does not support custom keywords, it falls back to the \Seen flag.
Recommendation: use a dedicated mailbox for the channel. If the server doesn't support custom keywords, HelpStack relies on
\Seento avoid re-ingesting mail — sharing the mailbox with a human reader (who marks things read/unread) can cause missed or duplicated ingestion.
Threading
Threading information is stored on the conversation metadata so replies thread correctly:
| Metadata field | Source |
|---|---|
emailSubject | Subject of the inbound mail |
emailMessageId | Message-ID header |
emailReferences | References / In-Reply-To chain |
Running the poller
npm run email:poll
Run this worker alongside the app (and the message queue worker) so inbound email is ingested. In production it polls roughly every 60s; in development roughly every 10s.
Environment tunables#
| Variable | Default | Purpose |
|---|---|---|
EMAIL_MAX_BODY_BYTES | ~100 KB | Max body size ingested; longer bodies are truncated |
EMAIL_MIN_ATTACHMENT_BYTES | ~1 KB | Attachments smaller than this are skipped (filters tracking pixels) |
EMAIL_MAX_AGE_DAYS | 5 | Ignore mail older than this many days |
IMAP_ALLOW_SELF_SIGNED | — | Dev only — accept self-signed IMAP TLS certs |
Encryption#
Each channel's IMAP/SMTP credentials are stored encrypted with AES-256-GCM. The dashboard provides a Test button that validates the configured credentials (connects and authenticates) before you save/rely on them.
Verified credential fields for an email channel (credentials):
host, port, username, password, from,
secure?, imapHost?, imapPort?, imapSecure?, imapAllowSelfSigned?
Outbound: SMTP via nodemailer#
Replies are sent over SMTP using nodemailer with the channel's own SMTP credentials: host, port, secure, user, pass, and from. Outbound sending is handled by the message queue's senders.
Channel SMTP vs. system notification SMTP
These are two separate mail paths — don't confuse them:
| Channel SMTP | System notification SMTP | |
|---|---|---|
| Purpose | Customer replies on an EMAIL channel | Platform/system notification emails |
| Credentials | Per-channel, encrypted | Env vars |
| Configured via | Channel settings | SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, SMTP_SECURE, SMTP_FROM |
| Default from | The channel's from | noreply@helpstack.eu |
System notification emails (e.g. account/system messages) always use the env-configured SMTP and the noreply@helpstack.eu default sender, independent of any channel.