We live in a world that never slows down. Inboxes are battlegrounds — flooded with newsletters nobody asked for, automated alerts that belong nowhere near a human, and the constant low-grade noise that buries the messages that actually matter.
goamail was built out of that frustration. The name says it all: go-a-mail, as in go away, mail — a small act of rebellion written in Go. It watches your INBOX, reads each message, and asks an AI to decide where it belongs. Junk disappears. Urgent work lands where your team will see it immediately. Everything else finds its right place, sorted by folder and flagged with a priority — fully transparent, fully configurable, nothing hidden.
No black-box magic. You write plain-language descriptions of what belongs in each folder, and the model follows your intent. You can inspect every decision in the logs, test the whole setup without touching a single message using dry-run mode, and tune the prompts until the classifier thinks like you do.
The goal is a calmer inbox, faster collaboration, and a lot less time spent on email that should never have demanded your attention in the first place.
- On startup, goamail connects to your IMAP server over TLS and authenticates.
- A background loop polls INBOX every 30 seconds, draining it one message at a time.
- Each message is decoded from MIME: all headers are extracted (routing hops, spam scores, DKIM results, X-Mailer tags), the plain-text body is truncated to
maxBodyCharscharacters, and attachment names and sizes are listed. The full binary content of attachments is never sent to the LLM. - The decoded content is sent to a configurable LLM along with your folder prompts. The LLM returns a target folder and a priority (1–5). If no folder matches, the message stays in INBOX. If the folder name starts with a digit that differs from the priority by 2 or more, a warning is logged but the message is still moved to the intended folder.
- The message is moved to that folder (created if it does not exist).
- A web UI on port 8080 lets you manage folders and their classification prompts.
- Go 1.26+
- A C compiler (
go-sqlite3uses CGo) - A GWDG Academic Cloud API key (or any OpenAI-compatible API endpoint)
Copy the example file and fill in your values:
cp credentials.json.example credentials.jsoncredentials.json is gitignored and will never be committed. The full structure:
{
"imap": {
"host": "mail.example.com:993",
"tls_mode": "tls",
"username": "you@example.com",
"auth": { "type": "password", "password": "secret" }
},
"smtp": {
"host": "smtp.example.com:587",
"tls_mode": "starttls",
"username": "you@example.com",
"auth": { "type": "password", "password": "secret" }
},
"llm": {
"url": "https://chat-ai.academiccloud.de/v1/chat/completions",
"model": "meta-llama-3.1-8b-instruct",
"temperature": 0.1,
"max_tokens": 64
}
}The llm section points at any OpenAI-compatible endpoint. Swap url and model to use a different provider or model without recompiling.
| Value | Typical port | How it works | Status |
|---|---|---|---|
"tls" (default) |
993 (IMAP) | Implicit TLS from the first byte | Working |
"starttls" |
143 (IMAP) / 587 (SMTP) | Plain connect, then upgrade via STARTTLS | Working for IMAP — SMTP sending not yet implemented |
Omitting tls_mode defaults to "tls". SMTP config is accepted but outbound sending is not yet wired up regardless of tls_mode.
Set auth.type to "password" or "oauth2".
Password auth — only password is needed:
"auth": { "type": "password", "password": "secret" }OAuth2 (OAUTHBEARER, RFC 7628) — access_token is used directly now; the other fields are stored for automatic token refresh once implemented:
| Field | Required now | Purpose |
|---|---|---|
type |
yes | must be "oauth2" |
access_token |
yes | bearer token sent to the server |
refresh_token |
recommended | used to obtain a new access token when it expires |
client_id |
recommended | OAuth2 app client ID, needed for token refresh |
client_secret |
recommended | OAuth2 app client secret, needed for token refresh |
token_url |
recommended | token endpoint URL, needed for token refresh |
Fill in all fields now so token refresh works once implemented. Replace access_token manually when it expires (typically after 1 hour).
Register an app in Azure Active Directory, grant it the
https://outlook.office365.com/IMAP.AccessAsUser.All and offline_access scopes,
then obtain tokens via the device-code or authorization-code flow.
IMAP (port 993, implicit TLS) works today:
"imap": {
"host": "outlook.office365.com:993",
"tls_mode": "tls",
"username": "you@yourdomain.com",
"auth": {
"type": "oauth2",
"client_id": "<Azure app (client) ID>",
"client_secret": "<Azure client secret>",
"token_url": "https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token",
"access_token": "<current access token>",
"refresh_token": "<refresh token>"
}
}SMTP (port 587, STARTTLS) can be pre-configured but is not yet active:
"smtp": {
"host": "smtp.office365.com:587",
"tls_mode": "starttls",
"username": "you@yourdomain.com",
"auth": {
"type": "oauth2",
"client_id": "<Azure app (client) ID>",
"client_secret": "<Azure client secret>",
"token_url": "https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token",
"access_token": "<current access token>",
"refresh_token": "<refresh token>"
}
}export GWDG_API_KEY=<your-key>The process will not start if this variable is unset.
go run .The web UI is available at http://localhost:8080.
go run . [-serve] [-dry-run] [-last N] [-since YYYY-MM-DD]
| Flag | Description |
|---|---|
-serve |
Start the web UI only for editing folder prompts — no IMAP connection, no credentials needed |
-dry-run |
Log what would be classified and moved without touching the server |
-last N |
Process the last N messages by sequence number and exit |
-since YYYY-MM-DD |
Process all messages received since the given date and exit |
Combining flags
-dry-runcan be combined with-lastor-since.-dry-runalone processes all current INBOX messages (nothing is moved).-lastand-sincecannot be used together.- Batch flags (
-last,-since) exit after processing; the web UI does not start.
Examples
# Edit folder prompts without connecting to IMAP
go run . -serve
# See what the classifier would do to the entire current inbox
go run . -dry-run
# See what would happen to the last 50 messages
go run . -dry-run -last 50
# See what would happen to all mail since May 1
go run . -dry-run -since 2026-05-01
# Actually sort the last 100 messages
go run . -last 100
# Actually sort all mail since May 1
go run . -since 2026-05-01main.go contains:
const debug = trueWhen true, verbose internal traces are logged (IMAP steps, LLM prompt and raw response, DB operations). Set to false before deploying to production.
classify.go contains two constants that control how much of each message is sent to the LLM:
const (
maxHeaderChars = 4000 // characters of raw headers
maxBodyChars = 4000 // characters of decoded plain-text body
)Headers include a curated set of classification-relevant fields: sender/recipient lines (From, To, Cc, Reply-To, Return-Path), Subject, Date, the first two Received hops for origin context, list signals (Precedence, List-Id, List-Unsubscribe), automation markers (Auto-Submitted, X-Mailer), and spam scores (X-Spam-Score, X-Spam-Status). Large binary fields (DKIM, ARC, Content-*) are excluded. The body is extracted from MIME (plain text preferred, HTML stripped as fallback). Attachments are never sent; only their filename and decoded size appear in the prompt. Raise either constant if your mail tends to have long headers or bodies worth classifying on.
Each folder has a plain-text prompt describing which emails belong there. Example:
| Folder | Prompt |
|---|---|
| Work | Emails from colleagues, project updates, meeting invites |
| Bills | Invoices, payment confirmations, subscription renewals |
| Spam | Newsletters I didn't ask for, marketing, promotions |
The LLM reads all prompts together and picks the best match. If nothing matches, the message stays in INBOX.
| Value | Meaning |
|---|---|
| 5 | Needs attention now — reply likely today |
| 4 | Should be seen soon — reply today or tomorrow |
| 3 | Informational — no action required |
| 2 | Not spam, but low interest — newsletters, bulk mail |
| 1 | Spam or junk — safe to delete |
goamail is functional but deliberately minimal today. The roadmap is shaped by real-world pain points:
Better AI Larger and more capable models will sharpen classification accuracy, especially for ambiguous messages or non-English content. We want to support model-per-folder configurations so critical folders can use a stronger model without paying that cost everywhere.
Team configuration tools Right now, folder prompts are edited one at a time through a bare web form. We want a richer interface designed for teams — shared prompt libraries, per-user overrides, role-based access, and the ability to import/export configurations so onboarding a new team member takes minutes rather than an afternoon.
AI-assisted reply drafting For messages that deserve a response, goamail should be able to draft a reply based on the email content and a configurable tone and context per folder. High-priority messages from known senders get a draft waiting in your outbox; everything else gets triaged first. Who gets a reply, in what style, and whether the draft is sent automatically or queued for review — all configurable.
OAuth2 token refresh
The plumbing is already there (refresh_token, client_id, client_secret,
token_url in the config). Automatic token refresh will remove the need to manually
replace access tokens every hour.
SMTP and forwarding
Outbound sending is scaffolded in the config (including tls_mode: "starttls" for port 587) but not yet wired up.
Once implemented, this unlocks forwarding rules — route a copy of matching messages to a team channel,
a shared inbox, or a webhook — making goamail a lightweight routing layer for the whole team.
| File | Purpose |
|---|---|
main.go |
Entry point — wires config, DB, IMAP, and HTTP; holds the debug flag |
config.go |
Credential and LLM config structs; loads credentials.json |
imap.go |
IMAP connection, authentication, folder management, fetch and move |
classify.go |
LLM classification via any OpenAI-compatible API |
web.go |
SQLite-backed folder store and HTTP handlers |
index.html |
Single-page web UI (embedded in the binary at compile time) |
data.db |
SQLite database (created on first run, gitignored) |