kevin 17d8dc1567 fix(imap): 序号按到达顺序(id ASC)分配,INTERNALDATE 使用到达时间
- ListAllByUserAndFolder 由 date DESC 改为 id ASC:新邮件永远追加到末尾
  (seq = EXISTS 数),与 Dovecot/Courier 一致;带旧 Date 头的新邮件不再
  插入中间导致既有邮件序号位移,seq 增量同步客户端不会漏收/标错
- INTERNALDATE 改用 CreatedAt(服务器接收时间, RFC 3501 §2.3.4),
  零值降级用 Date 头;不再返回可被伪造的 Date 头
- Web 分页排序补 id DESC tiebreak,与 IMAP 全链路一致
- ListDeletedByUserAndFolder 同步改为 id ASC
- 测试:TestSeqStoreClientSelfNumbered 替换为 TestSeqOrderArrival,
  断言到达顺序序号映射、旧 Date 头新邮件落在末尾、INTERNALDATE=CreatedAt
2026-08-20 15:18:55 +08:00

MailGo

English | 中文

A lightweight mail system written in Go, integrating SMTP / IMAP / POP3 protocol servers and a web admin interface. Web frontend layout: top navigation + left folder sidebar + mail list (three-column design).

Features

  • Mail protocols: SMTP (send), IMAP (sync), POP3 (receive), all with TLS support
  • External delivery: authenticated users can send mail to external addresses (Gmail/Outlook, etc.), with a built-in outbound queue, concurrent worker pool delivery (default 4 workers + max 2 concurrent connections per recipient domain), direct MX delivery, STARTTLS, exponential backoff retry, bounce notifications and DKIM signing
  • Multi-language UI (i18n): Chinese / English / Japanese, selectable in user settings (or auto-follow the browser language via Accept-Language; English is the fallback), covering all user and admin pages, 12-hour time formats (AM/PM) and error messages
  • Web mail: folder data-driven (Web and IMAP share the same MailboxService — whatever IMAP LIST returns is what the UI shows), system folders (Inbox / Sent / Drafts / Trash) plus custom folder management, delete moves to Trash (with restore / permanent delete / empty Trash), unread badges and search filters, select-all / batch delete, sender avatars, rich text editing (Quill.js), attachment upload/download
  • Admin console: domain management, user management, automatic DKIM key generation, DNS configuration hints, full mail browsing, outbound queue management, IP ban management, dashboard statistics, all timestamps displayed as 12-hour format (AM/PM) in the web timezone
  • Protocol call logs: every SMTP/IMAP/POP3 connection automatically records source IP, username, success/failure, failure reason and operation summary; filterable by protocol/status/IP/user/time for analyzing password brute-force, relay abuse and other attacks (kept 30 days by default, auto-cleaned)
  • IMAP new-mail push: real-time push after local delivery (SMTP/Web compose) — clients hanging on IDLE receive new-mail notifications instantly (no polling); read/star/delete changes made by other clients are also synced in real time (IMAP STORE/EXPUNGE, POP3 delete, Web mark-read/delete)
  • Live connection monitoring: admin console shows active SMTP/IMAP/POP3 connections (source IP, username, TLS, duration), auto-refreshing every 5 seconds; "Disconnect and ban" one-click bans all connections of that IP (ban for 180 days, unban anytime)
  • External auth: OAuth2 (Google / GitHub), LDAP (optional, disabled by default)
  • Security: BCrypt password hashing, automatic IP ban on login failures (tiered bans: the first 3 times reaching the failure threshold only count without banning, from the 4th time on it bans with escalating durations 30 min → 3 h → 3 months → 6 months, cleared on successful login; no grace for enumeration attacks: failures with a non-existent username skip the grace period and are banned on first trigger), outbound rate limiting (anti-abuse), relay denied to unauthenticated users (anti-open-relay), admins can unban
  • Multiple databases: SQLite by default, switchable to MySQL
  • Cross-platform: Linux production deployment + Windows local debugging

Screenshots

Inbox Mail view
Inbox Mail view
Compose Settings Login
Compose Settings Login

Screenshots are rendered with demo data; the actual UI may differ from your deployment.

Quick Start

Build

go build -o mailgo .

Start

./mailgo

On first start the config file and database are created automatically, along with the default admin account:

Item Value
Email admin@example.com
Initial password Randomly generated (16 chars, digits + upper/lowercase letters), printed once in the startup log; or pre-set via the MAILGO_ADMIN_PASSWORD environment variable

⚠️ This account is flagged "must change password on first login" — change it immediately on the Settings page.

Access

Page URL
User mailbox http://localhost:8080/
Admin console http://localhost:8080/admin

Configuration File

Config file path (TOML format):

System Path
Linux /etc/mail_go/mail_go.toml
Windows ./win/etc/mail_go/mail_go.toml

Auto-generated on first start; missing fields are auto-filled. A restart is required for changes to take effect.

Full Configuration Reference

[database]
driver = "sqlite"                          # sqlite | mysql
dsn = "/srv/mail_go/mail.db"               # SQLite: file path; MySQL: DSN string

[storage]
base_dir = "/srv/mail_go"                  # data root directory
attach_dir = "/srv/mail_go/attachments"    # attachment storage directory

[web]
addr = ":8080"                             # listen address; TCP port or Unix socket
secret_key = ""                            # web session signing key; if empty, a random
                                           # key is generated on first start and written
                                           # to this file (back it up: leaking it allows
                                           # forging sessions, losing it invalidates all)
cookie_secure = true                       # session cookie sent over HTTPS only (Secure
                                           # flag); set to false only for local HTTP debug
protocol_log_keep_days = 30                # retention days for SMTP/IMAP/POP3 protocol
                                           # call logs; expired entries are cleaned up by a
                                           # background task; 0 disables cleanup

[smtp]
addr = ":25"                               # SMTP plaintext port
tls_addr = ":465"                          # SMTPS port (requires TLS certs)
domain = "example.com"                     # mail domain
tls_cert = ""                              # TLS certificate path (empty = TLS disabled)
tls_key = ""                               # TLS private key path
max_message_bytes = 67108864               # max 64MB per message

[imap]
addr = ":143"                              # IMAP plaintext port
tls_addr = ":993"                          # IMAPS port
tls_cert = ""
tls_key = ""

[pop3]
addr = ":110"                              # POP3 plaintext port
tls_addr = ":995"                          # POP3S port
tls_cert = ""
tls_key = ""

[auth]
oauth2_enabled = false                     # enable OAuth2 login
oauth2_provider = ""                       # google | github
oauth2_client_id = ""
oauth2_client_secret = ""
oauth2_redirect_url = ""
ldap_enabled = false                       # enable LDAP login
ldap_server = ""                           # e.g. ldap://localhost:389
ldap_bind_dn = ""                          # e.g. cn=admin,dc=example,dc=com
ldap_bind_password = ""
ldap_search_base = ""                      # e.g. ou=users,dc=example,dc=com
ldap_search_filter = ""                    # e.g. (uid=%s)
ldap_use_tls = false

[ban]
max_fail_attempts = 5                      # login failure threshold
ban_duration_min = 30                      # 1st ban duration (minutes); then escalates:
                                           # 2nd 3h → 3rd 3 months → 4th+ 6 months (cap)
                                           # first 3 threshold hits only count, no ban;
                                           # cleared on successful login

[caddy]
data_dir = ""                              # Caddy data directory (the one containing
                                           # certificates/), used for one-click cert import
                                           # in the admin console; auto-detected when empty
                                           # (e.g. /var/lib/caddy/.local/share/caddy)

[outbound]
hostname = ""                              # EHLO hostname; defaults to [smtp] domain
poll_interval = 15                         # outbound queue scan interval (seconds)
workers = 4                                # concurrent delivery workers (parallel sends,
                                           # improves throughput for bulk mail; 0/1 serial)
batch_size = 50                            # max messages picked per scan
max_concurrent_per_domain = 2              # max concurrent connections to one recipient
                                           # domain (or relay), prevents spam-lookalike
                                           # bursts; 0 = unlimited
max_attempts = 12                          # max delivery attempts per message
retry_base_min = 5                         # retry backoff base (minutes), exponential:
                                           # 5/10/20/40...
max_recipients = 50                        # max external recipients per message
max_per_min = 30                           # max outgoing messages per user per minute
max_per_day = 500                          # max outgoing messages per user per day;
                                           # 0 disables external delivery
connect_timeout = 30                       # remote MX connect timeout (seconds)
relay_host = ""                            # smarthost; empty = direct MX delivery
relay_port = 587                           # 465 = implicit TLS, other ports use STARTTLS
relay_user = ""                            # relay auth username (AUTH PLAIN)
relay_password = ""                        # relay auth password
relay_starttls = true                      # use STARTTLS on non-465 ports
relay_tls_insecure = false                 # skip relay TLS certificate verification; certs
                                           # are verified by default (protects relay
                                           # credentials); only set to true for self-signed
                                           # intranet relays when you accept the risk
ip_family = "ipv4"                         # outbound address family: ipv4 (default) | ipv6 | auto
source_ip = ""                             # outbound source IP binding (e.g. static IPv6);
                                           # empty = kernel picks

Common Configuration Scenarios

1. Switching to MySQL

[database]
driver = "mysql"
dsn = "mailgo:YourPassword@tcp(127.0.0.1:3306)/mailgo?charset=utf8mb4&parseTime=True&loc=Local"

MySQL DSN format: user:password@tcp(host:port)/dbname?params

The database and user must be created in MySQL first:

CREATE DATABASE mailgo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'mailgo'@'localhost' IDENTIFIED BY 'YourPassword';
GRANT ALL PRIVILEGES ON mailgo.* TO 'mailgo'@'localhost';
FLUSH PRIVILEGES;

2. Web over Unix Socket

[web]
addr = "/run/mail_go/web.sock"

When addr starts with /, Gin listens on a Unix socket automatically.

3. Setting the Session Key (Container / Multi-Instance)

The session signing key can be overridden via the MAILGO_SECRET_KEY environment variable (takes precedence over the config file and is never written to disk):

MAILGO_SECRET_KEY="$(openssl rand -hex 32)" mail_go

Requirement: at least 16 bytes; when empty, the value from the config file is used (auto-generated on first start). After changing the key, all logged-in sessions are invalidated immediately and users must log in again.

Nginx reverse proxy configuration:

server {
    listen 80;
    server_name mail.example.com;

    location / {
        proxy_pass http://unix:/run/mail_go/web.sock;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Make sure the socket directory exists and is writable:

mkdir -p /run/mail_go
chown mailgo:mailgo /run/mail_go

4. Enabling TLS Encryption

Enable TLS for SMTP/IMAP/POP3 (all three protocols can share one certificate, or be configured separately):

[smtp]
tls_cert = "/etc/mail_go/certs/server.crt"
tls_key = "/etc/mail_go/certs/server.key"

[imap]
tls_cert = "/etc/mail_go/certs/server.crt"
tls_key = "/etc/mail_go/certs/server.key"

[pop3]
tls_cert = "/etc/mail_go/certs/server.crt"
tls_key = "/etc/mail_go/certs/server.key"

Once TLS is configured, the corresponding encrypted ports (465/993/995) start automatically. Checking the TLS box when adding a domain in the admin console switches the ports automatically.

You can get free certificates from Let's Encrypt:

certbot certonly --standalone -d mail.example.com
# Certificate: /etc/letsencrypt/live/mail.example.com/fullchain.pem
# Private key: /etc/letsencrypt/live/mail.example.com/privkey.pem

One-Click Certificate Import from Caddy

If this machine already serves HTTPS for the domain via Caddy (Caddy issues and renews certificates automatically), you can click "Fetch certificate from Caddy" on the Domain Management → Edit Domain page in the admin console to import the certificate and private key from Caddy's storage into the mail server (automatically enabling TLS for that domain), without manually copying PEM files. Wildcard certificates are supported (e.g. *.example.com matches mail.example.com). Certificates support hot reload: they take effect immediately after import (or manual upload) without restarting the service — SMTP/IMAP/POP3 re-check and reload changed certificate files on every TLS handshake.

Because Caddy's certificate directory is only readable by the caddy user, install.sh installs a root-privileged certificate sync job (mailgo-caddy-sync.{path,timer}) that mirrors Caddy's certificate tree to /srv/mail_go/tls/caddy, so mail_go can always read it after renewals; an ACL grant is also applied as a fallback for direct reads. This is configured automatically during installation, or can be run manually:

sudo ./install.sh setup-caddy-cert          # auto-detect Caddy data dir, configure sync + ACL
sudo ./install.sh setup-caddy-cert /path/to/caddy/data   # or specify the data dir manually

If Caddy's data directory is not in a common location, specify it explicitly in the config file:

[caddy]
data_dir = "/var/lib/caddy/.local/share/caddy"

5. Enabling OAuth2 Login (Google Example)

[auth]
oauth2_enabled = true
oauth2_provider = "google"
oauth2_client_id = "your-client-id.apps.googleusercontent.com"
oauth2_client_secret = "your-client-secret"
oauth2_redirect_url = "https://mail.example.com/auth/oauth2/callback"

You need to create an OAuth 2.0 client in the Google Cloud Console and set the authorized redirect URI to https://your-domain/auth/oauth2/callback.

6. Enabling LDAP Authentication

[auth]
ldap_enabled = true
ldap_server = "ldap://ldap.example.com:389"
ldap_bind_dn = "cn=admin,dc=example,dc=com"
ldap_bind_password = "ldap_admin_password"
ldap_search_base = "ou=users,dc=example,dc=com"
ldap_search_filter = "(uid=%s)"
ldap_use_tls = true

7. Sending Mail to External Recipients (External Delivery)

Authenticated users (Web mail / SMTP submission) can send mail to external addresses. The system delivers directly via the recipient domain's MX records, handling STARTTLS and exponential backoff retries automatically, and DKIM-signs outgoing mail (using the key generated in the admin domain management).

[smtp]
domain = "example.com"        # must be the real mail domain (EHLO/bounce address)

[outbound]
hostname = "mail.example.com" # recommended to match the mail hostname
poll_interval = 15
max_attempts = 12
max_per_day = 500             # set to 0 to disable external delivery entirely

Required DNS / server prerequisites (see todo.md and the admin DNS hints page for details):

Item Description
MX example.com MX 10 mail.example.com
SPF example.com TXT "v=spf1 mx -all"
DKIM default._domainkey.example.com TXT "v=DKIM1; k=rsa; p=<public key>" (auto-generated in the admin console)
DMARC _dmarc.example.com TXT "v=DMARC1; p=none; rua=mailto:postmaster@example.com"
PTR Server IP reverse-resolves to the mail hostname (request from your hosting provider)
Network Port 25 outbound is not blocked by the cloud provider

Security policy: external recipients are only accepted from authenticated users; MAIL FROM must match the logged-in user; per-user per-minute/day outbound limits apply; failed mail bounces back to the sender's inbox; admins can view delivery status in the Outbound Queue and manually retry or cancel.

IPv4/IPv6: Only IPv4 outbound is used by default (ip_family = "ipv4"), because many recipients (e.g. Gmail) reject IPv6 addresses without PTR, while IPv4 usually has matching forward/reverse PTR. To use IPv6: ask your provider to configure PTR for the static address (pointing to mail.example.com), then set ip_family = "ipv6" and bind source_ip to that static address (to avoid the kernel picking rotating temporary privacy addresses).

8. Relaying Outbound Mail Through a Smarthost

When the server IP is on a residential/dynamic IP range, it is often listed in policy lists such as Spamhaus PBL, and recipients like Microsoft (Outlook/Hotmail) reject the mail outright. In that case, it is recommended to hand outbound mail to a third-party SMTP relay (Mailgun / SendGrid / Amazon SES / Aliyun Direct Mail, etc.), configured in [outbound] — all external delivery automatically routes through the relay:

[outbound]
relay_host = "smtp.example-relay.com"
relay_port = 587          # 465 = implicit TLS
relay_user = "your-api-user"
relay_password = "your-api-key"
relay_starttls = true

The relay uses AUTH PLAIN authentication; local recipients still use local delivery, unaffected.

9. Setting the Initial Admin Password

When the admin account is created on first start, the initial password is randomly generated by default and printed once in the startup log; to pre-set it (e.g. for deployment scripts), use the MAILGO_ADMIN_PASSWORD environment variable:

MAILGO_ADMIN_PASSWORD="$(openssl rand -base64 12)" ./mailgo

This variable only takes effect when the admin account is first created (ignored if the account already exists); either way, the password must be changed on first login.


Port Quick Reference

Protocol Plaintext port TLS port Description
SMTP 25 465 Mail sending
IMAP 143 993 Mailbox sync
POP3 110 995 Mail retrieval
Web 8080 Web UI (Unix socket supported)

Directory Structure

mailgo/
├── main.go                          # program entry
├── config/
│   ├── config.go                    # config loading and merging
│   └── defaults.go                  # default constants
├── internal/
│   ├── db/
│   │   ├── db.go                    # database initialization (SQLite/MySQL)
│   │   └── models.go                # GORM model definitions
│   ├── store/
│   │   ├── stores.go                # Store aggregator
│   │   ├── user_store.go            # user data operations
│   │   ├── mail_store.go            # mail data operations
│   │   ├── mailbox_store.go         # folder (IMAP mailbox) data operations
│   │   ├── domain_store.go          # domain data operations
│   │   ├── attachment_store.go      # attachment data operations
│   │   ├── outbound_store.go        # outbound queue data operations
│   │   ├── ban_store.go             # ban data operations
│   │   └── protocol_log_store.go    # protocol call log data operations
│   ├── smtp_server/server.go        # SMTP server
│   ├── outbound/
│   │   ├── mailer.go                # MX lookup and SMTP outbound client
│   │   ├── manager.go               # outbound queue, retry, rate limiting, bounces
│   │   └── sign.go                  # DKIM signing
│   ├── imap_server/
│   │   ├── server.go                # IMAP server (listener/capabilities/cross-session push)
│   │   ├── service.go               # MailboxService (shared by IMAP sessions and Web)
│   │   └── session.go               # IMAP Session implementation (SELECT/FETCH/STORE/EXPUNGE)
│   ├── pop3_server/server.go        # POP3 server
│   ├── connhub/hub.go               # protocol connection registry (live connection monitoring)
│   ├── storage/attachment.go        # attachment file storage
│   ├── dkim/keys.go                 # DKIM key generation
│   ├── auth/
│   │   ├── provider.go              # auth interface
│   │   ├── oauth2.go                # OAuth2 authentication
│   │   └── ldap.go                  # LDAP authentication
│   └── web/
│       ├── server.go                # web routing and template loading
│       ├── handlers/
│       │   ├── auth.go              # login/logout/OAuth2/LDAP
│       │   ├── mail.go              # mailbox operations
│       │   └── admin.go             # admin console
│       ├── middleware/
│       │   ├── auth.go              # session authentication
│       │   ├── admin.go             # admin authorization
│       │   └── ban.go               # IP ban check
│       └── templates/               # HTML templates
│           ├── *.html               # user pages
│           └── admin/*.html         # admin pages
└── .gitignore

Runtime Data Directories (Linux)

Path Description
/etc/mail_go/ config file
/srv/mail_go/ database + attachment storage

Runtime Data Directories (Windows Debug)

Path Description
./win/etc/mail_go/ config file
./win/srv/mail_go/ database + attachment storage

Linux Service Deployment

systemd Unit File

Create /etc/systemd/system/mailgo.service:

[Unit]
Description=MailGo Mail Server
After=network.target mysql.service

[Service]
Type=simple
User=mailgo
Group=mailgo
WorkingDirectory=/opt/mailgo
ExecStart=/opt/mailgo/mailgo
Restart=on-failure
RestartSec=5
LimitNOFILE=65536

# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/srv/mail_go /run/mail_go /etc/mail_go

[Install]
WantedBy=multi-user.target

Starting the Service

# Create the system user
sudo useradd -r -s /sbin/nologin -d /srv/mail_go mailgo

# Set directory ownership
sudo chown -R mailgo:mailgo /srv/mail_go /etc/mail_go

# Enable and start
sudo systemctl daemon-reload
sudo systemctl enable mailgo
sudo systemctl start mailgo

# View logs
sudo journalctl -u mailgo -f

DNS Configuration

The mail service requires the following DNS records (the admin console → DNS hints page shows the full configuration):

Type Name Value Description
A mail server IP mail server address
MX @ mail.example.com mail routing
TXT @ v=spf1 mx ~all SPF anti-spam
TXT default._domainkey DKIM public key (auto-generated in the admin console) DKIM signature verification
TXT _dmarc v=DMARC1; p=none; rua=mailto:admin@example.com DMARC policy

Tech Stack

Component Technology
Language Go 1.25+
Web framework Gin
Template engine html/template
ORM GORM
Database SQLite (default) / MySQL
Config format TOML
SMTP github.com/emersion/go-smtp
IMAP github.com/emersion/go-imap/v2
POP3 hand-implemented TCP protocol
Password hashing golang.org/x/crypto/bcrypt
Rich text Quill.js (embedded via go:embed, works offline)
OAuth2 golang.org/x/oauth2
LDAP github.com/go-ldap/ldap/v3
DKIM RSA 2048 auto-generated

Default Account

Role Email Initial password Default quota
Admin admin@example.com Randomly generated (printed once in the startup log) or set via MAILGO_ADMIN_PASSWORD 5 GB

⚠️ Password change is mandatory on first login (also triggered when an admin resets the password); change it immediately after production deployment.

License

MIT

S
Description
No description provided
Readme
41 MiB
0 Stars 1 Watchers 1 Forks
Languages
Go 78.8%
HTML 18.8%
Shell 2.4%