A minimal email TUI where you read with Markdown and write in Neovim. neomd.ssp.sh/docs
email markdown neovim tui
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix heuristic and pattern matching for pixel regexpp

+5 -29
+2 -3
SECURITY.md
··· 89 89 - The inbox list shows a `⊙` indicator (orange) for emails that contained tracking pixels, visible after first read. 90 90 - The reader header shows `⊙ N spy pixel(s) blocked (domain.com, ...)` with the tracker domains. 91 91 92 - **Browser view (`O`) protection:** 93 - - The HTML template includes a `Content-Security-Policy` meta tag that restricts image sources to `file:`, `data:`, and `cid:` only. Remote images (including tracking pixels) are blocked even when viewing the full HTML version in a browser. 92 + **Browser view (`O`):** When you explicitly open an email in the browser, remote images are loaded — this is intentional, as you're choosing to see the full email. Tracking pixels are only blocked in the TUI. 94 93 95 - **Code:** [`internal/imap/client.go`](https://github.com/ssp-data/neomd/blob/main/internal/imap/client.go) — `cleanMarkdown()`, `SpyPixelInfo` · [`internal/render/html.go`](https://github.com/ssp-data/neomd/blob/main/internal/render/html.go) — `htmlTemplate` (CSP) · [`internal/ui/inbox.go`](https://github.com/ssp-data/neomd/blob/main/internal/ui/inbox.go) — `⊙` indicator · [`internal/ui/reader.go`](https://github.com/ssp-data/neomd/blob/main/internal/ui/reader.go) — `renderEmailHeader()` 94 + **Code:** [`internal/imap/client.go`](https://github.com/ssp-data/neomd/blob/main/internal/imap/client.go) — `detectSpyPixels()`, `SpyPixelInfo` · [`internal/ui/inbox.go`](https://github.com/ssp-data/neomd/blob/main/internal/ui/inbox.go) — `⊙` indicator · [`internal/ui/reader.go`](https://github.com/ssp-data/neomd/blob/main/internal/ui/reader.go) — `renderEmailHeader()` 96 95 97 96 --- 98 97
+1 -1
docs/content/docs/reading.md
··· 40 40 - `⊙` indicator in the inbox list (orange, next to the attachment `@` column) 41 41 - `⊙ N spy pixel(s) blocked (domain.com)` in the reader header with tracker domains 42 42 43 - The browser view (`O`) also blocks remote images via a Content-Security-Policy header, so tracking pixels are blocked even when viewing the full HTML version. 43 + When you press `O` to open in the browser, remote images load normally — you're explicitly choosing to see the full email. Tracking pixel blocking is a TUI-level protection. 44 44 45 45 ### This is how it looks: 46 46
-23
internal/render/html.go
··· 20 20 <head> 21 21 <meta charset="UTF-8"> 22 22 <meta name="viewport" content="width=device-width,initial-scale=1.0"> 23 - <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src file: data: cid:; font-src 'none';"> 24 23 <style> 25 24 body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;line-height:1.6;color:#333;margin:0;padding:8px 16px;text-align:left} 26 25 a{color:#3150AA;text-decoration:underline} ··· 60 59 ), 61 60 goldmark.WithRendererOptions(html.WithHardWraps()), 62 61 ) 63 - 64 - // cspMeta is the Content-Security-Policy meta tag injected into all browser views. 65 - // Blocks remote images (tracking pixels), scripts, and fonts. 66 - const cspMeta = `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src file: data: cid:; font-src 'none';">` 67 - 68 - // InjectCSP inserts a CSP meta tag into an HTML document for safe browser viewing. 69 - // If the document has a <head>, the tag is inserted after it. Otherwise it's prepended. 70 - func InjectCSP(html string) string { 71 - if idx := strings.Index(strings.ToLower(html), "<head>"); idx >= 0 { 72 - insert := idx + len("<head>") 73 - return html[:insert] + "\n" + cspMeta + html[insert:] 74 - } 75 - if idx := strings.Index(strings.ToLower(html), "<html"); idx >= 0 { 76 - // Find the end of the <html...> tag 77 - end := strings.IndexByte(html[idx:], '>') 78 - if end >= 0 { 79 - insert := idx + end + 1 80 - return html[:insert] + "<head>" + cspMeta + "</head>" + html[insert:] 81 - } 82 - } 83 - return cspMeta + "\n" + html 84 - } 85 62 86 63 // ToHTML converts a Markdown string to a complete HTML email document. 87 64 func ToHTML(markdown string) (string, error) {
+2 -2
internal/ui/model.go
··· 3175 3175 3176 3176 var htmlBody string 3177 3177 if m.openHTMLBody != "" { 3178 - htmlBody = render.InjectCSP(m.openHTMLBody) 3178 + htmlBody = m.openHTMLBody 3179 3179 } else { 3180 3180 var err error 3181 3181 htmlBody, err = render.ToHTML(m.openBody) ··· 3252 3252 3253 3253 var htmlBody string 3254 3254 if m.openHTMLBody != "" { 3255 - htmlBody = render.InjectCSP(m.openHTMLBody) 3255 + htmlBody = m.openHTMLBody 3256 3256 } else { 3257 3257 var err error 3258 3258 htmlBody, err = render.ToHTML(m.openBody)