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.

revert markdown

sspaeti ebe28a52 544fd471

+21 -33
+5 -14
internal/editor/editor.go
··· 133 133 return s 134 134 } 135 135 136 - // ReactionBody builds the plain text body for an emoji reaction. 137 - // Returns markdown format (for HTML rendering) and plain text format (for text/plain part). 138 - // Markdown version includes syntax for italics and links. 139 - // Plain text version is clean without markdown symbols. 140 - func ReactionBody(emoji, fromName, originalFrom, originalBody string) (markdown, plainText string) { 136 + // ReactionBody builds the markdown body for an emoji reaction. 137 + // Returns markdown that will be used for both text/plain and text/html parts (same as regular replies). 138 + // Includes the quoted original message using the same quoting logic as regular replies. 139 + func ReactionBody(emoji, fromName, originalFrom, originalBody string) string { 141 140 quoted := buildQuotedReply(originalFrom, originalBody) 142 - 143 - // Markdown version (will be rendered to HTML) 144 - markdown = fmt.Sprintf("%s\n\n_%s reacted via [neomd](https://neomd.ssp.sh)_\n\n%s", emoji, fromName, quoted) 145 - 146 - // Plain text version (no markdown syntax) 147 - plainQuoted := fmt.Sprintf("---\n\n%s wrote:\n\n%s\n\n---\n\n", originalFrom, quoteLines(originalBody)) 148 - plainText = fmt.Sprintf("%s\n\n%s reacted via neomd (https://neomd.ssp.sh)\n\n%s", emoji, fromName, plainQuoted) 149 - 150 - return markdown, plainText 141 + return fmt.Sprintf("%s\n\n_%s reacted via [neomd](https://neomd.ssp.sh)_\n\n%s", emoji, fromName, quoted) 151 142 } 152 143 153 144 // ParseHeaders scans raw editor content for # [neomd: key: value] lines and
+4 -5
internal/smtp/sender.go
··· 292 292 293 293 // BuildReactionMessage constructs a minimal reaction email with threading headers. 294 294 // Used for emoji reactions sent as replies to emails. 295 - // plainTextBody contains the reaction for plain text email clients (no markdown syntax). 296 - // markdownBody contains the reaction text with markdown syntax for HTML rendering. 295 + // markdownBody is used for both text/plain and text/html parts (same as BuildMessageWithThreading). 297 296 // inReplyTo is the Message-ID of the original email. 298 297 // references is the References chain from the original email (may be empty). 299 - func BuildReactionMessage(from, to, cc, subject, plainTextBody, markdownBody, inReplyTo, references string) ([]byte, error) { 298 + func BuildReactionMessage(from, to, cc, subject, markdownBody, inReplyTo, references string) ([]byte, error) { 300 299 // Convert markdown to HTML (same as regular replies) 301 300 htmlBody, err := render.ToHTML(markdownBody) 302 301 if err != nil { ··· 313 312 } 314 313 } 315 314 // No attachments for reactions 316 - // Use plainTextBody for text/plain part (no markdown syntax), htmlBody for text/html part 317 - return buildMessageWithBCC(from, to, cc, "", subject, plainTextBody, htmlBody, nil, inReplyTo, refChain) 315 + // Use markdown for text/plain part, rendered HTML for text/html part (same as regular replies) 316 + return buildMessageWithBCC(from, to, cc, "", subject, markdownBody, htmlBody, nil, inReplyTo, refChain) 318 317 } 319 318 320 319 // inlineImage holds a local image path and its assigned Content-ID.
+6 -8
internal/smtp/sender_test.go
··· 687 687 } 688 688 } 689 689 func TestBuildReactionMessage_ThreadingHeaders(t *testing.T) { 690 - plainText := "👍\n\nSimon reacted via neomd (https://neomd.ssp.sh)\n\n---\n\nJohn wrote:\n\n> Hello" 691 690 markdown := "👍\n\n_Simon reacted via [neomd](https://neomd.ssp.sh)_\n\n---\n\n> **John** wrote:\n>\n> Hello" 692 691 inReplyTo := "<original@example.com>" 693 692 references := "<first@example.com> <second@example.com>" ··· 697 696 "john@example.com", 698 697 "", 699 698 "Re: Test", 700 - plainText, 701 699 markdown, 702 700 inReplyTo, 703 701 references, ··· 750 748 if strings.Contains(ct, "text/plain") { 751 749 foundPlainText = true 752 750 bodyStr := string(body) 753 - // Plain text should not have markdown syntax 754 - if strings.Contains(bodyStr, "_") || strings.Contains(bodyStr, "[neomd]") { 755 - t.Errorf("text/plain part contains markdown syntax: %s", bodyStr) 751 + // Plain text contains markdown syntax (same as regular replies) 752 + if !strings.Contains(bodyStr, "_Simon reacted via [neomd]") { 753 + t.Errorf("text/plain missing markdown footer, got: %s", bodyStr) 756 754 } 757 - // Should contain plain text version 758 - if !strings.Contains(bodyStr, "Simon reacted via neomd") { 759 - t.Errorf("text/plain missing footer, got: %s", bodyStr) 755 + // Should contain quoted reply 756 + if !strings.Contains(bodyStr, "> **John** wrote:") { 757 + t.Errorf("text/plain missing quoted reply, got: %s", bodyStr) 760 758 } 761 759 } 762 760 if strings.Contains(ct, "text/html") {
+6 -6
internal/ui/model.go
··· 809 809 fromName = extractEmailAddr(from) 810 810 } 811 811 812 - // Build reaction bodies: markdown (for HTML rendering) and plain text (for text/plain part) 813 - bodyMarkdown, bodyPlainText := editor.ReactionBody(emoji.emoji, fromName, e.From, m.openBody) 812 + // Build reaction body in markdown (used for both text/plain and text/html parts, same as regular replies) 813 + bodyMarkdown := editor.ReactionBody(emoji.emoji, fromName, e.From, m.openBody) 814 814 815 815 // Get SMTP account 816 816 smtpAcct := m.activeAccount() ··· 833 833 834 834 return m, tea.Batch( 835 835 m.spinner.Tick, 836 - m.sendReactionCmd(smtpAcct, from, to, subject, bodyMarkdown, bodyPlainText, e), 836 + m.sendReactionCmd(smtpAcct, from, to, subject, bodyMarkdown, e), 837 837 ) 838 838 } 839 839 840 - func (m Model) sendReactionCmd(smtpAcct config.AccountConfig, from, to, subject, bodyMarkdown, bodyPlainText string, originalEmail *imap.Email) tea.Cmd { 840 + func (m Model) sendReactionCmd(smtpAcct config.AccountConfig, from, to, subject, bodyMarkdown string, originalEmail *imap.Email) tea.Cmd { 841 841 h, p := splitAddr(smtpAcct.SMTP) 842 842 cfg := smtp.Config{ 843 843 Host: h, ··· 861 861 } 862 862 863 863 // Build reaction message with threading headers 864 - // markdown will be converted to HTML, plainText used for text/plain part 864 + // markdown used for both text/plain and text/html parts (same as regular replies) 865 865 raw, err := smtp.BuildReactionMessage( 866 866 from, to, "", subject, 867 - bodyPlainText, bodyMarkdown, 867 + bodyMarkdown, 868 868 originalEmail.MessageID, 869 869 references, 870 870 )