Overview
The email transport (internal/transport/email/) is a pair: IMAP inbound (via github.com/emersion/go-imap/v2) and SMTP outbound (via Go's standard-library net/smtp).
It polls INBOX for UNSEEN messages, flags them SEEN after handoff to the handler, and replies via net/smtp.SendMail.
TLS posture
Both ends are full TLS. The transport uses imapclient.DialTLS on the IMAP side and smtp.SendMail with PlainAuth over an already-TLS-wrapped connection on the SMTP side. STARTTLS-only IMAP or SMTP servers are not currently supported — the daemon refuses to send plaintext credentials over an unencrypted socket.
Standard TLS ports:
- IMAP:
993 - SMTP submission:
465(implicit TLS) — full TLS. Not587unless your provider does implicit TLS on 587 as well.
Some providers (Google Workspace, Fastmail) accept SMTP submission on 465 with implicit TLS. Verify your provider before configuring.
Configuration
email:
imap_addr: "imap.example.com:993"
imap_username: "bot@example.com"
imap_password: "..."
mailbox: "INBOX"
poll_interval: "30s"
smtp_addr: "smtp.example.com:465"
smtp_username: "bot@example.com"
smtp_password: "..."
from: "bot@example.com"
reply_header: ""
| Field | Default | Effect |
|---|---|---|
imap_addr |
required | host:port for TLS IMAP. |
imap_username |
required | IMAP username. |
imap_password |
required | IMAP password. |
mailbox |
INBOX |
Mailbox to poll. |
poll_interval |
30s |
How often to look for UNSEEN mail. |
smtp_addr |
required | host:port for SMTP submission. |
smtp_username |
required | SMTP username. |
smtp_password |
required | SMTP password. |
from |
required | Envelope + header From address. |
reply_header |
empty | Prepended to every outbound message body. |
Command-line
rousseau email \
--imap-addr imap.example.com:993 \
--imap-username bot@example.com \
--imap-password ... \
--smtp-addr smtp.example.com:465 \
--smtp-username bot@example.com \
--smtp-password ... \
--from bot@example.com
Outbound message shape
Replies are RFC 5322-compliant. rousseau writes:
From: bot@example.com
To: sender@example.com
Subject: Re: <inbound subject>
Content-Type: text/plain; charset=utf-8
MIME-Version: 1.0
<reply_header><body>
UTF-8 is unconditional. HTML output is out of scope; there is no template engine wired in.
Inbound message shape
UNSEEN messages are parsed into an IncomingMessage with:
From= the parsedFromheader address.Body= the concatenatedtext/plainparts.At= theINTERNALDATEfrom IMAP.
Attachments, text/html, and inline images are ignored.
Mailbox choice
mailbox: "INBOX" is the default. Point at a Gmail label ("[Gmail]/label") or a Fastmail folder for finer filtering — anything the IMAP server exposes works.
Provider-specific setup
Gmail app password walkthrough. Regular Gmail passwords will not authenticate over IMAP/SMTP when 2FA is on. Generate an app password:
- Visit https://myaccount.google.com/security. Confirm 2-Step Verification is on.
- Click App passwords (only visible with 2FA enabled).
- Name the app "rousseau-agent", generate. Copy the 16-character password (spaces optional).
Config:
email:
imap_addr: imap.gmail.com:993
imap_username: your.address@gmail.com
imap_password: "aaaa bbbb cccc dddd"
smtp_addr: smtp.gmail.com:465
smtp_username: your.address@gmail.com
smtp_password: "aaaa bbbb cccc dddd"
from: your.address@gmail.com
Fastmail supports app passwords under Settings > Password & Security > App passwords. Create a password scoped to Mail (IMAP/POP/SMTP):
email:
imap_addr: imap.fastmail.com:993
imap_username: your.address@fastmail.com
imap_password: "..."
smtp_addr: smtp.fastmail.com:465
smtp_username: your.address@fastmail.com
smtp_password: "..."
from: your.address@fastmail.com
Microsoft 365 has deprecated basic authentication (username + password) for most tenants. Rousseau does not yet support Modern Auth / OAuth (roadmap). Options:
- Enable Authenticated SMTP per-mailbox in the M365 admin center (possible on some tenants).
- Use a relay: run rousseau against a self-hosted IMAP+SMTP that forwards through M365 via SMTP with an app password.
- Wait for OAuth support to land.
Any self-hosted mail server that speaks IMAP over TLS on 993 and SMTP submission over implicit TLS on 465 works out of the box. Postfix + Dovecot with smtpd_tls_wrappermode=yes on port 465 is a classic setup.
email:
imap_addr: mail.internal:993
imap_username: rousseau
imap_password: "..."
smtp_addr: mail.internal:465
smtp_username: rousseau
smtp_password: "..."
from: rousseau@internal
If your server is STARTTLS-only (port 587 SMTP submission), rousseau will refuse to authenticate — the transport does not send plaintext credentials. See the migration section below.
Migrating from STARTTLS-only servers
Rousseau uses implicit TLS on both IMAP (993) and SMTP (465). If your existing mail infrastructure only offers STARTTLS on 143 (IMAP) or 587 (SMTP submission), you have three options:
- Enable implicit TLS on your server. Postfix supports
smtpd_tls_wrappermode=yesbound to port 465. Dovecot supportsimapsservice on port 993 out of the box. - Front the server with a TLS-terminating proxy.
stunnelcan accept implicit TLS on 465 and forward as STARTTLS on 587. - Wait for STARTTLS support. Roadmap item; see
docs/GAP_ANALYSIS_2026.md.
Plain vs HTML rendering
Outbound is text/plain; charset=utf-8. No HTML template. This is deliberate — plain text is universally rendered, does not embed tracking pixels, and never breaks in a text-only email client. If you want HTML output, wrap the transport and rewrite SendMail:
// Custom transport that emits multipart/alternative.
type MyEmailClient struct{ email.Client }
func (c *MyEmailClient) Deliver(ctx context.Context, to, body string) error {
html := markdown.ToHTML([]byte(body), nil, nil)
// ... construct multipart/alternative message, call net/smtp.SendMail ...
}
Rousseau's core stays plain-text; HTML is a caller concern.
Failure modes
| Symptom | Fix |
|---|---|
imapclient.DialTLS errors |
Confirm port 993 is open outbound, TLS certificate is valid. |
SMTP AUTH failed |
PlainAuth requires the auth server hostname to match smtp_addr. Providers with load balancers may present a different name. |
| Messages never flagged SEEN | Handler returned an error. Fix the underlying issue; rousseau does not retry indefinitely. |
| Duplicate replies | Two rousseau instances on the same mailbox; only one should run. |
AUTHENTICATE failed: Application-specific password required |
Gmail with 2FA on, and the account password was used instead of an app password. See Gmail walkthrough above. |
Troubleshooting
dial tcp: connect: connection refused
Wrong port. Ensure imap_addr uses :993 (not :143) and smtp_addr uses :465 (not :587 for STARTTLS-only servers).
Bot replies to spam
Any message in INBOX with UNSEEN is handled. Filter spam at the mailbox level (server-side rules, Gmail spam filter) or configure a mailbox: different from INBOX and route mail into it with a server-side rule.
SendMail succeeds but the message never arrives
Check the SMTP server's mail log. Common causes: DKIM signing failure (the From: domain does not match a domain your server can sign), reverse DNS mismatch, receiving domain's SPF blocks your IP.
Unicode in message body renders as ?????
Something along the path stripped UTF-8. Verify Content-Type: text/plain; charset=utf-8 is in the sent message (rousseau always sets it) and that no relay is transcoding.
Poll takes seconds even after config change
poll_interval is only re-read at daemon start. Restart to pick up the new value.
Related pages
- Getting Started: First Transport — end-to-end walkthrough.
- Configuration — the
emailconfig block. - Transports — sibling transports.
- Deployment — running Email in a Podman container.
- Cron — send scheduled digests via email.
Further reading
internal/transport/email/client.go— IMAP poll, SMTP send, message parsing.internal/cli/email.go— CLI wiring.internal/config/config.go—EmailConfigstruct.- emersion/go-imap docs — the IMAP library.