···11# Changelog
2233+# 2026-04-09
44+- **Fix: non-standard IMAP/SMTP ports** — neomd now correctly handles non-standard ports (e.g., Proton Mail Bridge on `127.0.0.1:1143` and `127.0.0.1:1025`); previously hardcoded port-based logic ignored the user's `starttls` config and refused unencrypted connections to any port other than 993/143 (IMAP) or 465/587 (SMTP); new behavior: user's explicit `starttls = true` always forces STARTTLS, standard ports use their defaults (993→TLS, 143→STARTTLS, 465→TLS, 587→STARTTLS), non-standard ports default to TLS for security (user must set `starttls = true` if their provider uses STARTTLS on a custom port); fixes "refusing unencrypted connection to 127.0.0.1:1143" error reported by Proton Bridge users; comprehensive test coverage added for all port/config combinations
55+36# 2026-04-08
47- **Fix: pre-send `e` losing email body** — pressing `e` in the pre-send review to re-edit now correctly reopens the editor with the existing body; previously it opened a blank compose with only the signature, silently discarding the email content (including reply history)
58- **Draft backups** — every compose session is automatically backed up to `~/.cache/neomd/drafts/` before the temp file is deleted; keeps a rolling 20 backups (configurable via `draft_backup_count` in `[ui]`, set to `-1` to disable); no more lost emails after crashes or accidental closes
+4
README.md
···152152153153For the full configuration reference including multiple accounts, OAuth2 authentication, `[[senders]]` aliases, folder customization, signatures, and UI options, see [docs/configuration.md](docs/configuration.md).
154154155155+**Provider-specific guides:**
156156+- Gmail: [docs/gmail.md](docs/gmail.md) — folder name mapping and OAuth2 setup
157157+- Proton Mail Bridge: [docs/proton-bridge.md](docs/proton-bridge.md) — non-standard port configuration
158158+155159### Onboarding
156160157161On first launch, **auto-screening is paused** because your screener lists are empty — neomd won't move anything until you've classified your first sender. Your Inbox loads normally so you can explore.
+28-2
cmd/neomd/main.go
···5353 imapClients := make([]*goIMAP.Client, 0, len(accounts))
5454 for _, acc := range accounts {
5555 h, p := splitAddr(acc.IMAP)
5656+ // Determine TLS/STARTTLS: respect explicit user config, otherwise infer from port.
5757+ // Security: non-standard ports default to TLS (e.g., Proton Mail Bridge on 1143).
5858+ useTLS, useSTARTTLS := inferIMAPSecurity(p, acc.STARTTLS)
5659 imapCfg := goIMAP.Config{
5760 Host: h,
5861 Port: p,
5962 User: acc.User,
6063 Password: acc.Password,
6161- TLS: p == "993",
6262- STARTTLS: p == "143",
6464+ TLS: useTLS,
6565+ STARTTLS: useSTARTTLS,
6366 }
6467 if acc.IsOAuth2() {
6568 if acc.OAuth2ClientID == "" {
···136139 }
137140 return addr[:i], addr[i+1:]
138141}
142142+143143+// inferIMAPSecurity determines TLS/STARTTLS settings based on port and user config.
144144+// Returns (useTLS, useSTARTTLS).
145145+//
146146+// Logic:
147147+// - If userSTARTTLS is true: always use STARTTLS (user explicitly enabled it)
148148+// - Standard ports: 993 → TLS, 143 → STARTTLS
149149+// - Non-standard ports: default to TLS (e.g., Proton Mail Bridge on 1143)
150150+func inferIMAPSecurity(port string, userSTARTTLS bool) (useTLS, useSTARTTLS bool) {
151151+ if userSTARTTLS {
152152+ // User explicitly set starttls=true in config — honor it.
153153+ return false, true
154154+ }
155155+ switch port {
156156+ case "993":
157157+ return true, false // Standard IMAPS (implicit TLS)
158158+ case "143":
159159+ return false, true // Standard IMAP (STARTTLS upgrade)
160160+ default:
161161+ // Non-standard port (e.g., Proton Mail Bridge): default to TLS for security.
162162+ return true, false
163163+ }
164164+}
+78
cmd/neomd/main_test.go
···11+package main
22+33+import "testing"
44+55+func TestInferIMAPSecurity(t *testing.T) {
66+ tests := []struct {
77+ name string
88+ port string
99+ userSTARTTLS bool
1010+ wantTLS bool
1111+ wantSTARTTLS bool
1212+ description string
1313+ }{
1414+ // Standard ports
1515+ {
1616+ name: "standard IMAPS port 993",
1717+ port: "993",
1818+ userSTARTTLS: false,
1919+ wantTLS: true,
2020+ wantSTARTTLS: false,
2121+ description: "Port 993 should use implicit TLS",
2222+ },
2323+ {
2424+ name: "standard IMAP port 143",
2525+ port: "143",
2626+ userSTARTTLS: false,
2727+ wantTLS: false,
2828+ wantSTARTTLS: true,
2929+ description: "Port 143 should use STARTTLS",
3030+ },
3131+ // Non-standard ports (Proton Mail Bridge, etc.)
3232+ {
3333+ name: "Proton Mail Bridge IMAP port 1143",
3434+ port: "1143",
3535+ userSTARTTLS: false,
3636+ wantTLS: true,
3737+ wantSTARTTLS: false,
3838+ description: "Non-standard port 1143 should default to TLS",
3939+ },
4040+ {
4141+ name: "custom port 1143 with STARTTLS override",
4242+ port: "1143",
4343+ userSTARTTLS: true,
4444+ wantTLS: false,
4545+ wantSTARTTLS: true,
4646+ description: "User override should force STARTTLS even on non-standard port",
4747+ },
4848+ // User config overrides
4949+ {
5050+ name: "port 993 with STARTTLS override",
5151+ port: "993",
5252+ userSTARTTLS: true,
5353+ wantTLS: false,
5454+ wantSTARTTLS: true,
5555+ description: "User setting starttls=true should override port-based inference",
5656+ },
5757+ {
5858+ name: "port 143 with STARTTLS override",
5959+ port: "143",
6060+ userSTARTTLS: true,
6161+ wantTLS: false,
6262+ wantSTARTTLS: true,
6363+ description: "Port 143 with starttls=true should use STARTTLS (same as default)",
6464+ },
6565+ }
6666+6767+ for _, tt := range tests {
6868+ t.Run(tt.name, func(t *testing.T) {
6969+ gotTLS, gotSTARTTLS := inferIMAPSecurity(tt.port, tt.userSTARTTLS)
7070+ if gotTLS != tt.wantTLS {
7171+ t.Errorf("%s: got TLS=%v, want TLS=%v", tt.description, gotTLS, tt.wantTLS)
7272+ }
7373+ if gotSTARTTLS != tt.wantSTARTTLS {
7474+ t.Errorf("%s: got STARTTLS=%v, want STARTTLS=%v", tt.description, gotSTARTTLS, tt.wantSTARTTLS)
7575+ }
7676+ })
7777+ }
7878+}
+1
cmd/sendtest/main.go
···7272 User: acc.User,
7373 Password: acc.Password,
7474 From: acc.From,
7575+ STARTTLS: acc.STARTTLS,
7576 }
76777778 for _, to := range recipients {
+46-1
docs/configuration.md
···88[[accounts]]
99name = "Personal"
1010imap = "imap.example.com:993" # :993 = TLS, :143 = STARTTLS
1111-smtp = "smtp.example.com:587"
1111+smtp = "smtp.example.com:587" # :587 = STARTTLS, :465 = TLS
1212user = "me@example.com"
1313password = "app-password"
1414from = "Me <me@example.com>"
1515+starttls = false # optional: force STARTTLS (see TLS/STARTTLS section below)
15161617# OAuth2 authenticated accounts are supported, it just need the relevant fields. Note that the password field is not required.
1718[[accounts]]
···9495Values containing other text or multiple `$` signs are left as-is, so passwords that happen to contain `$` are never mangled.
95969697Credentials are stored only in `~/.config/neomd/config.toml` (mode 0600) and never written elsewhere; all IMAP connections use TLS (port 993) or STARTTLS (port 143).
9898+9999+### TLS and STARTTLS Configuration
100100+101101+Neomd automatically determines the correct encryption method based on the port and the optional `starttls` config field:
102102+103103+**IMAP ports:**
104104+- `993` → Implicit TLS (standard IMAPS)
105105+- `143` → STARTTLS upgrade (standard IMAP)
106106+- Non-standard ports (e.g., `1143` for Proton Mail Bridge) → TLS by default
107107+- Set `starttls = true` to force STARTTLS on any port
108108+109109+**SMTP ports:**
110110+- `465` → Implicit TLS (SMTPS)
111111+- `587` → STARTTLS upgrade (modern submission standard)
112112+- Non-standard ports (e.g., `1025` for Proton Mail Bridge) → TLS by default
113113+- Set `starttls = true` to force STARTTLS on any port
114114+115115+**Examples:**
116116+117117+Standard provider (Gmail, Hostpoint, etc.):
118118+```toml
119119+[[accounts]]
120120+imap = "imap.gmail.com:993"
121121+smtp = "smtp.gmail.com:587"
122122+starttls = false # optional, default behavior works
123123+```
124124+125125+Proton Mail Bridge (local bridge on non-standard ports):
126126+```toml
127127+[[accounts]]
128128+imap = "127.0.0.1:1143" # Uses TLS automatically
129129+smtp = "127.0.0.1:1025" # Uses TLS; set starttls=true if bridge uses STARTTLS
130130+starttls = false
131131+```
132132+133133+Custom server with STARTTLS on non-standard port:
134134+```toml
135135+[[accounts]]
136136+imap = "mail.custom.com:2143"
137137+smtp = "mail.custom.com:2587"
138138+starttls = true # Forces STARTTLS instead of TLS
139139+```
140140+141141+See `docs/proton-bridge.md` for complete Proton Mail Bridge setup instructions.
9714298143## Sending and Discarding
99144
+67
docs/proton-bridge.md
···11+# Configuring neomd with Proton Mail Bridge
22+33+Proton Mail Bridge allows you to use neomd with ProtonMail accounts by running a local IMAP/SMTP bridge.
44+55+## Installation
66+77+1. Install Proton Mail Bridge: https://proton.me/mail/bridge
88+2. Launch the bridge and configure your ProtonMail account
99+3. Note the IMAP and SMTP connection details (typically `127.0.0.1:1143` and `127.0.0.1:1025`)
1010+1111+## neomd Configuration
1212+1313+Add the following to your `~/.config/neomd/config.toml`:
1414+1515+```toml
1616+[[accounts]]
1717+ name = "ProtonMail"
1818+ imap = "127.0.0.1:1143"
1919+ smtp = "127.0.0.1:1025"
2020+ user = "your-proton-email@proton.me"
2121+ password = "bridge-password-here" # Get this from Proton Bridge settings
2222+ from = "Your Name <your-proton-email@proton.me>"
2323+ starttls = false # Proton Bridge uses TLS on non-standard ports
2424+```
2525+2626+## Key Configuration Details
2727+2828+- **IMAP Port**: Proton Bridge defaults to `1143` with TLS
2929+- **SMTP Port**: Proton Bridge defaults to `1025` with STARTTLS
3030+ - If you need STARTTLS for SMTP, set `starttls = true`
3131+- **Password**: Use the bridge-generated password (not your ProtonMail password)
3232+- **TLS/STARTTLS**: neomd automatically detects the correct security mode based on:
3333+ - Standard ports (993→TLS, 143→STARTTLS for IMAP; 465→TLS, 587→STARTTLS for SMTP)
3434+ - Non-standard ports default to TLS unless `starttls = true` is set
3535+ - Explicit `starttls = true` always forces STARTTLS
3636+3737+## Troubleshooting
3838+3939+### "refusing unencrypted connection to 127.0.0.1:1143"
4040+4141+This error occurred in older versions of neomd that didn't respect the `starttls` config or handle non-standard ports correctly. **This is now fixed** (v0.4.15+).
4242+4343+If you still see this error:
4444+1. Ensure you're running the latest version: `neomd --version`
4545+2. Check your config has the correct IMAP/SMTP addresses
4646+3. Verify Proton Bridge is running: `ps aux | grep bridge`
4747+4848+### Connection Refused
4949+5050+- Make sure Proton Mail Bridge is running
5151+- Verify the bridge ports in its settings (they may differ from the defaults)
5252+- Check firewall settings allow localhost connections
5353+5454+## Custom Ports
5555+5656+If your Proton Bridge uses different ports, adjust the config accordingly:
5757+5858+```toml
5959+imap = "127.0.0.1:YOUR_IMAP_PORT"
6060+smtp = "127.0.0.1:YOUR_SMTP_PORT"
6161+```
6262+6363+For non-standard ports, neomd defaults to TLS. If your bridge uses STARTTLS on a custom port, add:
6464+6565+```toml
6666+starttls = true
6767+```
+29-7
internal/smtp/sender.go
···3232 User string
3333 Password string
3434 From string // "Name <email>"
3535+ STARTTLS bool // User's explicit starttls config preference
35363637 // TokenSource is used for OAuth2 accounts instead of Password.
3738 TokenSource func() (string, error)
···99100 return err
100101 }
101102 addr := cfg.Host + ":" + cfg.Port
102102- switch cfg.Port {
103103- case "465": // Implicit TLS (SMTPS)
103103+ if inferSMTPUseTLS(cfg.Port, cfg.STARTTLS) {
104104 return sendTLS(addr, cfg.Host, auth, fromAddr, toAddrs, raw)
105105- default: // STARTTLS (587) or plain (25)
106106- return sendSTARTTLS(addr, auth, fromAddr, toAddrs, raw)
107105 }
106106+ return sendSTARTTLS(addr, auth, fromAddr, toAddrs, raw)
108107}
109108110109// SendRaw delivers a pre-built raw MIME message (e.g. from BuildMessage).
···117116 return err
118117 }
119118 addr := cfg.Host + ":" + cfg.Port
120120- switch cfg.Port {
119119+ if inferSMTPUseTLS(cfg.Port, cfg.STARTTLS) {
120120+ return sendTLS(addr, cfg.Host, auth, fromAddr, toAddrs, raw)
121121+ }
122122+ return sendSTARTTLS(addr, auth, fromAddr, toAddrs, raw)
123123+}
124124+125125+// inferSMTPUseTLS determines whether to use implicit TLS or STARTTLS based on
126126+// port and user config. Returns true for TLS, false for STARTTLS.
127127+//
128128+// Logic:
129129+// - If userSTARTTLS is true: always use STARTTLS (user explicitly enabled it)
130130+// - Standard ports: 465 → TLS, 587 → STARTTLS
131131+// - Non-standard ports: default to TLS (e.g., Proton Mail Bridge on 1025 uses STARTTLS,
132132+// but user must set starttls=true for that)
133133+func inferSMTPUseTLS(port string, userSTARTTLS bool) bool {
134134+ if userSTARTTLS {
135135+ // User explicitly set starttls=true in config — use STARTTLS.
136136+ return false
137137+ }
138138+ switch port {
121139 case "465":
122122- return sendTLS(addr, cfg.Host, auth, fromAddr, toAddrs, raw)
140140+ return true // SMTPS (implicit TLS)
141141+ case "587":
142142+ return false // Submission with STARTTLS (modern standard)
123143 default:
124124- return sendSTARTTLS(addr, auth, fromAddr, toAddrs, raw)
144144+ // Non-standard port: default to TLS for security.
145145+ // User must explicitly set starttls=true if their provider uses STARTTLS.
146146+ return true
125147 }
126148}
127149
+72
internal/smtp/sender_test.go
···385385 })
386386 }
387387}
388388+389389+func TestInferSMTPUseTLS(t *testing.T) {
390390+ tests := []struct {
391391+ name string
392392+ port string
393393+ userSTARTTLS bool
394394+ wantTLS bool
395395+ description string
396396+ }{
397397+ // Standard ports
398398+ {
399399+ name: "standard SMTPS port 465",
400400+ port: "465",
401401+ userSTARTTLS: false,
402402+ wantTLS: true,
403403+ description: "Port 465 should use implicit TLS",
404404+ },
405405+ {
406406+ name: "standard submission port 587",
407407+ port: "587",
408408+ userSTARTTLS: false,
409409+ wantTLS: false,
410410+ description: "Port 587 should use STARTTLS",
411411+ },
412412+ // Non-standard ports (Proton Mail Bridge, etc.)
413413+ {
414414+ name: "Proton Mail Bridge SMTP port 1025",
415415+ port: "1025",
416416+ userSTARTTLS: false,
417417+ wantTLS: true,
418418+ description: "Non-standard port 1025 should default to TLS (user must set starttls=true if needed)",
419419+ },
420420+ {
421421+ name: "custom port 1025 with STARTTLS override",
422422+ port: "1025",
423423+ userSTARTTLS: true,
424424+ wantTLS: false,
425425+ description: "User setting starttls=true should force STARTTLS on non-standard port",
426426+ },
427427+ // User config overrides
428428+ {
429429+ name: "port 465 with STARTTLS override",
430430+ port: "465",
431431+ userSTARTTLS: true,
432432+ wantTLS: false,
433433+ description: "User setting starttls=true should override port 465 default",
434434+ },
435435+ {
436436+ name: "port 587 with STARTTLS override",
437437+ port: "587",
438438+ userSTARTTLS: true,
439439+ wantTLS: false,
440440+ description: "Port 587 with starttls=true should use STARTTLS (same as default)",
441441+ },
442442+ {
443443+ name: "port 587 with starttls false",
444444+ port: "587",
445445+ userSTARTTLS: false,
446446+ wantTLS: false,
447447+ description: "Port 587 should use STARTTLS even when starttls=false (port takes precedence)",
448448+ },
449449+ }
450450+451451+ for _, tt := range tests {
452452+ t.Run(tt.name, func(t *testing.T) {
453453+ got := inferSMTPUseTLS(tt.port, tt.userSTARTTLS)
454454+ if got != tt.wantTLS {
455455+ t.Errorf("%s: got TLS=%v, want TLS=%v", tt.description, got, tt.wantTLS)
456456+ }
457457+ })
458458+ }
459459+}