?

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. Not 587 unless 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 parsed From header address.
  • Body = the concatenated text/plain parts.
  • At = the INTERNALDATE from 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:

  1. Visit https://myaccount.google.com/security. Confirm 2-Step Verification is on.
  2. Click App passwords (only visible with 2FA enabled).
  3. 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

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:

  1. Enable implicit TLS on your server. Postfix supports smtpd_tls_wrappermode=yes bound to port 465. Dovecot supports imaps service on port 993 out of the box.
  2. Front the server with a TLS-terminating proxy. stunnel can accept implicit TLS on 465 and forward as STARTTLS on 587.
  3. 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

Further reading

  • internal/transport/email/client.go — IMAP poll, SMTP send, message parsing.
  • internal/cli/email.go — CLI wiring.
  • internal/config/config.goEmailConfig struct.
  • emersion/go-imap docs — the IMAP library.

Type to search 150+ pages. Ranking: BM25 with title/description boost.