Internet Mail (Email)
Internet Mail
ENiGMA½ can send and receive internet email directly from the message system. Users compose private messages addressed to user@domain.com and they are delivered via your configured SMTP transport. Inbound email arrives by polling an IMAP mailbox; messages addressed to username@yourdomain.com are routed to the matching local user.
This feature is implemented as an email scanner/tosser module, following the same pattern as FTN/BSO and ActivityPub.
Outbound email requires the
email.transportblock to be configured. See Email Configuration.
Sending Email
Once outbound transport is configured, users can address a private message to any internet email address:
bob@example.comAlice Smith <alice@example.com>
The system detects the email flavor automatically using the same address parser that handles FTN and ActivityPub addresses. The message is delivered via Nodemailer and marked exported in the message database.
If delivery fails (e.g. transport not configured, SMTP error), the message is marked ExportFailed and a warning is logged.
Receiving Email (Inbound IMAP)
ENiGMA½ polls an IMAP mailbox at a configurable interval. When a new message arrives addressed to username@yourdomain.com, the username portion before @ is matched against local BBS users. Matched messages are delivered to the user’s private mail area.
Messages that cannot be matched to a local user are saved as .eml files in mail/email/failed/ for sysop review.
Set up a dedicated mailbox (e.g.
bbs@yourdomain.com) and configure your mail provider to accept*@yourdomain.cominto it, or use per-user aliases — whatever fits your provider. ENiGMA½ only needs IMAP access to a single inbox.
Configuration
All email configuration lives under the email block in config.hjson.
Outbound Configuration Reference
| Key | Default | Description |
|---|---|---|
outbound.fromDomain |
(unset) | When set, outbound mail is sent as "UserName" <sanitized@fromDomain> where the local-part is derived from the BBS user’s name. When unset, all outbound mail uses defaultFrom. |
outbound.usernameReplaceChar |
_ |
Character used to replace invalid characters when deriving the local-part from a BBS username (e.g. spaces → _). |
When outbound.fromDomain is set, the From: header reflects the sending BBS user while the SMTP Sender: header and envelope MAIL FROM are set to defaultFrom. This matches the standard “on behalf of” pattern used by mailing lists and keeps bounces deliverable to the authenticated mailbox.
Your SMTP provider must allow the authenticated account to send as other local-parts within the configured domain. Verify this in your provider’s settings (most providers allow this for any address in a verified domain).
The sanitized local-part is checked against
users.badUserNamesbefore use. If a user’s sanitized name collides with a reserved name, that message falls back todefaultFrom.
Inbound Configuration Reference
| Key | Default | Description |
|---|---|---|
inbound.enabled |
false |
Set to true to enable IMAP polling |
inbound.imap.host |
— | IMAP server hostname |
inbound.imap.port |
993 |
IMAP port |
inbound.imap.secure |
true |
Use implicit TLS (port 993). Set false for STARTTLS on port 143 |
inbound.imap.user |
— | IMAP login username |
inbound.imap.password |
— | IMAP login password |
inbound.imap.pollIntervalMs |
300000 |
How often to check for new messages (ms). Set to 0 to use IMAP IDLE (push-like, persistent connection) |
inbound.imap.processedFolder |
(none) | IMAP folder to move successfully imported messages into. If omitted, messages stay in INBOX marked \Seen
|
inbound.imap.failedFolder |
(none) | IMAP folder to move messages that could not be imported (unknown local recipient, parse error). If omitted, failed messages stay in INBOX marked \Seen. Either way, a copy is saved locally as .eml in mail/email/failed/ for sysop review |
inbound.imap.maxMessagesPerRun |
50 |
Maximum messages to import per poll cycle |
Server-side message lifecycle: the inbound poller marks every processed message
\Seen— both imports that succeeded and imports that failed. This is intentional: a message that cannot be matched (e.g. addressed to a deleted local user) would otherwise be re-fetched on every poll and duplicated intomail/email/failed/indefinitely. Marking seen breaks that loop. Messages are never deleted by ENiGMA½ — retention ofprocessedFolder/failedFolder/ INBOX is entirely up to you or your provider.
Polling vs. IMAP IDLE
Polling (pollIntervalMs > 0) |
IDLE (pollIntervalMs: 0) |
|
|---|---|---|
| Delivery latency | Up to pollIntervalMs
|
Near-instant |
| Connection | Connect, fetch, disconnect | Persistent connection |
| Complexity | Simple, stateless | Requires stable network; auto-reconnects on drop |
| Best for | Most setups | Low-latency installs with stable connectivity |
For most BBS operators, the default 5-minute polling interval is more than sufficient.
Example Configurations
Minimal outbound-only setup
If you only want users to be able to send internet email:
email: {
defaultFrom: "Sysop <sysop@yourbbs.net>"
transport: {
host: smtp.yourdomain.com
port: 587
secure: false
auth: {
user: bbs@yourdomain.com
pass: yourpassword
}
}
}
Full send + receive setup
email: {
defaultFrom: "Sysop <sysop@yourbbs.net>"
// Optional: send as "<UserName>" <username@yourbbs.net> instead of
// always using defaultFrom. Requires your SMTP provider to allow
// the authenticated account to send as other local-parts.
outbound: {
fromDomain: yourbbs.net
}
transport: {
host: smtp.yourdomain.com
port: 587
secure: false
auth: {
user: bbs@yourdomain.com
pass: yourpassword
}
}
inbound: {
enabled: true
imap: {
host: imap.yourdomain.com
port: 993
secure: true
user: bbs@yourdomain.com
password: yourpassword
// Check every 5 minutes (default)
pollIntervalMs: 300000
// Move successfully imported messages here on the IMAP server
processedFolder: "BBS-Processed"
// Move messages that couldn't be matched to a local user here
// (optional — defaults to leaving them in INBOX marked \Seen)
failedFolder: "BBS-Failed"
}
}
}
Using IMAP IDLE (push-like)
inbound: {
enabled: true
imap: {
host: imap.yourdomain.com
port: 993
secure: true
user: bbs@yourdomain.com
password: yourpassword
// 0 = use IMAP IDLE instead of polling
pollIntervalMs: 0
processedFolder: "BBS-Processed"
}
}
Using a service provider (Fastmail, ProtonMail Bridge, etc.)
Any provider that exposes standard IMAP/SMTP works. Example using Fastmail:
email: {
defaultFrom: "Sysop <bbs@yourdomain.com>"
transport: {
host: smtp.fastmail.com
port: 587
secure: false
auth: {
user: bbs@yourdomain.com
pass: yourapppassword
}
}
inbound: {
enabled: true
imap: {
host: imap.fastmail.com
port: 993
secure: true
user: bbs@yourdomain.com
password: yourapppassword
pollIntervalMs: 300000
processedFolder: "BBS-Processed"
}
}
}
Many providers (Gmail, Outlook) require an app password or OAuth2 token rather than your account password for IMAP/SMTP access. Generate one in your provider’s security settings.
Failed Message Handling
Messages that cannot be delivered to a local user (unknown username, parse error) are:
- Saved locally as raw
.emlfiles inmail/email/failed/. The filename includes a timestamp and a short reason code (e.g.1712345678901_no_user.eml). - Marked
\Seenon the IMAP server so they are not re-fetched on the next poll. - Moved to
inbound.imap.failedFolderif configured, otherwise left in INBOX (read).
Sysops can inspect the local .eml files with any email client or text editor to diagnose routing issues. Using failedFolder keeps the server-side INBOX tidy and makes it easy to re-run a message (e.g. after creating the missing local user) by moving it back into INBOX and clearing its \Seen flag.
See Also
- Email Configuration — SMTP transport setup
- Message Networks — Overview of all supported networks
- Configuring a Message Area — Private mail area setup
