HelpStackDocs

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 typeEMAIL
InboundIMAP polling worker (npm run email:poll)
OutboundSMTP via nodemailer (channel credentials)
CredentialsStored encrypted (AES-256-GCM) per channel
Transport securityIMAP 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:

  1. Fetches mail it has not processed yet, read or unread, from the last 24 hours (the channel's lookback window, see below).
  2. Downloads the text, the HTML and attachments up to the size limits (not the whole raw message; see Attachments below) and parses them.
  3. Deduplicates by message-id + preview (so the same mail isn't ingested twice).
  4. Creates a conversation keyed by the sender's email address (or, for a contact-form mail sent from your own address, the customer it names; see below).
  5. Uploads attachments to storage.
  6. Marks the mail as processed.

How "processed" is tracked

The poller marks each ingested mail with a custom IMAP keyword $HelpStackProcessed and searches for mail without it. The \Seen (read) flag is never touched by ingestion, so:

  • mail stays unread in Outlook, Gmail or Apple Mail until someone reads it there (or in HelpStack, with read sync on), and
  • a mail someone opens in their mail client before HelpStack's next check is still picked up.

Gmail, Google Workspace, Microsoft 365 and most current IMAP servers support custom keywords. If a server does not, the poller falls back to the \Seen flag: it only picks up unread mail and marks it read once ingested.

On a server without keyword support, use a dedicated mailbox for the channel. HelpStack then relies on \Seen to avoid re-ingesting mail, so a human reading the mailbox can cause missed mail, and read sync is unavailable.

Lookback window

Each check searches the last 24 hours by default. An admin can change this per channel (1 to 168 hours) under Mailbox sync in the channel's connection settings. The window never reaches back before the moment the mailbox was connected, so connecting a mailbox does not import its history. Import Recent Mail moves that starting point back and lifts the lookback window for an hour so the import can finish. If HelpStack could not check the mailbox for longer than the lookback window (an outage), the next check reaches back to a day before the last successful check, up to 7 days, so mail from that gap is not skipped.

Mail that HelpStack already ingested is never ingested twice: the message-id check skips it, even when a mailbox switches from the \Seen fallback to keyword tracking. When an existing channel switches, mail that was already read in the mailbox before the switch is not imported (your team has likely handled it there); unread mail is imported as usual.

Read and unread sync

Off by default. When an admin turns on Sync read and unread status (Mailbox sync, per channel):

  • HelpStack to mailbox: reading a conversation in HelpStack marks its emails read in the mailbox; Mark as unread marks them unread again. This runs in the background a few seconds later.
  • Mailbox to HelpStack: HelpStack follows changes made in the mailbox. When someone reads an email there and all of the conversation's emails from the last lookback window are now read, the conversation is marked read in HelpStack. If someone marks one of those emails unread in the mailbox, the conversation is marked unread. Nothing changes without a change in the mailbox, so a conversation you marked unread in HelpStack stays unread, and a newly arrived unread email does not change anything by itself.

Only emails HelpStack ingested while keyword tracking was active are tracked. Read sync is not available on servers without keyword support.

Following the mailbox (replies sent from your mail client)

For teams that also work in Outlook, Gmail or another mail client next to HelpStack. Off by default; an admin turns on Follow what happens in the mailbox (Mailbox sync, per channel). Every few minutes HelpStack then checks:

  • Replies sent from your mail client. HelpStack looks for new mail in the mailbox's Sent folder (found by its standard marker, whatever it is called, for example "Sent Items" or "[Gmail]/Poslano"). A reply that answers an email of a conversation appears in that conversation, marked Sent from email, with the quoted earlier messages removed. Attachments of such a reply are not downloaded. If the reply is newer than the customer's last message, the conversation is marked read and a pending AI draft is set aside, marked Draft not sent, so nobody sends a second answer. A set-aside draft is not counted as rejected in AI performance.
  • Email moved out of the Inbox. When every email of a conversation has been moved out of the Inbox (filed into a folder, archived or deleted), the conversation is marked read. Filing an email this way also counts as dealing with it: a pending AI draft is set aside (it shows Draft not sent) and the conversation leaves the Mailbox view's Inbox, until the customer writes again.

This only reads the mailbox: HelpStack never marks, moves or deletes anything there because of this setting. (Writing read and unread state back is the separate Sync read and unread status setting above.) It follows conversations active in the last 30 days.

Turning it on starts a one-off catch-up over those 30 days, so conversations that were already answered or filed stop showing as unread. You can run it again with Catch up now; the line next to the button shows the result of the last run. Reading new mail is never delayed by it, and like every other mailbox check it pauses while Gmail is limiting the mailbox.

Moving an email to a folder

On email channels that read their mailbox over IMAP (including Gmail), an agent can move the customer's email out of the Inbox into a folder of the mailbox, from HelpStack: Move to folder in a conversation's ⋮ menu, or in the Mailbox view's selection bar for several conversations of one channel at once (up to 100). The picker lists the mailbox's own folders (never the Inbox itself), with the folder last used on that channel first and pre-selected, and a search box for long lists. It happens only when someone clicks it; nothing is moved automatically.

The email is moved, never deleted, and you see it in the new folder in Outlook or Gmail too. Afterwards the conversation counts as filed: it is marked read, a pending AI draft is set aside (Draft not sent), it leaves the Mailbox view's Inbox and shows Handled in email, until the customer writes again. A conversation whose email is no longer in the Inbox (moved earlier, or older mail) is reported as skipped. When the mail server is limiting the mailbox or cannot be reached, HelpStack says so and changes nothing.

Handled in email appears on every open conversation that was replied to from your mail client or filed out of the Inbox after the customer's latest message, in the conversations list, in the Mailbox view and in the conversation header. Hover it to see which and when. The conversation stays open; the label goes away by itself when the customer writes again.

Attachments: size limits, storage and fetching from the mailbox

HelpStack downloads only what it needs from each email: the text, the HTML and attachments up to 10 MB each and 25 MB per email. A larger attachment still appears in the conversation, with its name and size and a Fetch from mailbox button; clicking it downloads that one file from your mailbox and then shows it as usual.

Attachment files are kept in HelpStack storage for 60 days. After that the file is removed from HelpStack storage, the attachment stays listed, and Fetch from mailbox brings it back from your mailbox (a fetched copy is kept for another 7 days). If the email has since been deleted from the mailbox, the attachment shows No longer available in the mailbox. An email you archived in Gmail is still found. If HelpStack cannot reach the mailbox right now (for example after a password change), you see a message saying so and can try again later; the attachment is not marked unavailable.

When Gmail limits the mailbox

Gmail limits how much a mailbox can download over IMAP per day. When it reports that a mailbox is over that limit, HelpStack pauses checking that mailbox (30 minutes at first, longer if it happens again, at most 6 hours) instead of retrying every minute, which would keep the limit in place. The channel's Connection tab then shows Gmail temporarily limited this mailbox; retrying at HH:MM. No mail is lost: when checking resumes, HelpStack catches up on everything that arrived during the pause. Fetching an attachment from that mailbox is unavailable until then. Other email providers that report the same kind of limit are handled the same way.

Contact forms sent from your own address

Website contact forms often send their mail from your own mailbox address and put the visitor in Reply-To or in the message body. When an inbound mail's From address is the channel's own address, HelpStack treats it as relayed and uses, in order:

  1. the first Reply-To address that is not your own, or
  2. an Email: line in the message body.

The conversation, and every reply, then goes to that customer. Their name comes from a Name: (or Ime in priimek:) line in the body, else the Reply-To display name, else the address. If nothing can be found, the mail is ingested as before.

Threading

Threading information is stored on the conversation metadata so replies thread correctly:

Metadata fieldSource
emailSubjectSubject of the inbound mail
emailMessageIdMessage-ID header
emailReferencesReferences / 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#

VariableDefaultPurpose
EMAIL_MAX_BODY_BYTES~100 KBMax body size ingested; longer bodies are truncated
EMAIL_MIN_ATTACHMENT_BYTES~1 KBAttachments smaller than this are skipped (filters tracking pixels)
EMAIL_MAX_ATTACHMENT_BYTES10 MBAttachments larger than this are not downloaded automatically; agents fetch them from the mailbox
EMAIL_MAX_MESSAGE_ATTACHMENT_BYTES25 MBMax total of attachments downloaded per email; the rest are fetched on request
EMAIL_ATTACHMENT_RETENTION_DAYS60Days attachment files are kept in storage; afterwards they are fetched from the mailbox on request
EMAIL_MAX_AGE_DAYS5Ignore 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 SMTPSystem notification SMTP
PurposeCustomer replies on an EMAIL channelPlatform/system notification emails
CredentialsPer-channel, encryptedEnv vars
Configured viaChannel settingsSMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, SMTP_SECURE, SMTP_FROM
Default fromThe channel's fromnoreply@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.