Module: Mailmate::CLI::Message Private
Overview
This module is part of a private API. You should avoid using this module if possible, as it may be removed or be changed in the future.
‘mmmessage` — print a decoded MailMate message by its eml-id. Ports the standalone mailmate-message script. Headers + plain-text body by default; `–raw` for the original .eml bytes; `–text-only` for just the body.
Instance Method Summary collapse
-
#html_like?(mail, body) ⇒ Boolean
private
Heuristic for “this body is HTML even though Mail couldn’t structure it” — covers single-part text/html messages where mail.html_part is nil and we’d otherwise dump the raw markup.
-
#html_to_markdown(html) ⇒ Object
private
HTML → clean markdown for terminal reading.
- #parse_options(argv) ⇒ Object private
- #print_headers(mail, eml_id, path) ⇒ Object private
- #run(argv) ⇒ Object private
- #text_body(mail, markdown: false) ⇒ Object private
- #usage_error(msg) ⇒ Object private
Instance Method Details
#html_like?(mail, body) ⇒ Boolean
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Heuristic for “this body is HTML even though Mail couldn’t structure it” — covers single-part text/html messages where mail.html_part is nil and we’d otherwise dump the raw markup.
126 127 128 129 |
# File 'lib/mailmate/cli/message.rb', line 126 def html_like?(mail, body) ct = mail.content_type.to_s.downcase ct.include?("text/html") || body =~ /\A\s*<(?:!doctype html|html|body|head)\b/i end |
#html_to_markdown(html) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
HTML → clean markdown for terminal reading. Three preprocessing / postprocessing passes beyond plain reverse_markdown:
1. Drop <style> and <script> blocks before conversion — pure clutter
that reverse_markdown otherwise dumps as inline text.
2. Strip zero-width spacers that newsletters use to control inbox
preview text (U+034F, U+200B/C/D, U+FEFF). Without this, you get
long runs of `͏ ` in the output.
3. Collapse 3+ consecutive blank lines into a single blank line.
139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 |
# File 'lib/mailmate/cli/message.rb', line 139 def html_to_markdown(html) begin require "nokogiri" require "reverse_markdown" rescue LoadError => e warn "mmmessage --markdown needs the reverse_markdown gem (which pulls nokogiri)." warn "Install it with: gem install reverse_markdown" warn "(underlying: #{e.})" exit 3 end doc = Nokogiri::HTML(html) doc.css("style, script").remove md = ReverseMarkdown.convert(doc.to_html) # U+034F combining grapheme joiner, U+200B ZWSP, U+200C ZWNJ, # U+200D ZWJ, U+FEFF BOM/ZWNBSP — newsletter preview-text padding. md.gsub!(/[\u034F\u200B\u200C\u200D\uFEFF]/, "") # Convert non-breaking spaces to regular spaces so rstrip can collapse # them. Newsletter preview-text padding often uses runs of which # Ruby's .rstrip leaves alone otherwise. md.gsub!(/[\u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]/, " ") # Strip trailing whitespace per line - the spaces between the # now-removed zero-width chars otherwise leave long whitespace runs. md = md.lines.map(&:rstrip).join("\n") md.gsub!(/\n{3,}/, "\n\n") md.strip end |
#parse_options(argv) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
# File 'lib/mailmate/cli/message.rb', line 56 def (argv) opts = { raw: false, text_only: false, mailmate: false, markdown: false } OptionParser.new do |o| o. = "Usage: mmmessage <id> [--raw|--text-only|--mailmate|--markdown]" o.separator "" o.separator "<id> can be a local eml-id (e.g. 183715), an RFC Message-ID" o.separator "(with or without angle brackets, e.g. <abc@example.com>), or" o.separator "a message://%3C...%3E URL. The latter two are portable across" o.separator "machines and survive copy/paste between desktop/laptop/iPad." o.on("--raw", "Output raw .eml bytes") { opts[:raw] = true } o.on("--text-only", "Output decoded body only (no headers block)") { opts[:text_only] = true } o.on("--mailmate", "Open in MailMate's UI instead of printing (alias for `mmopen <id>`)") { opts[:mailmate] = true } o.on("--markdown", "Render HTML body as clean markdown (no-op for plain-text messages)") { opts[:markdown] = true } end.parse!(argv) opts end |
#print_headers(mail, eml_id, path) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
# File 'lib/mailmate/cli/message.rb', line 80 def print_headers(mail, eml_id, path) imap_root = Mailmate.config.imap_root mailbox = path.sub("#{imap_root}/", "").sub(%r{/Messages/[^/]+\.eml\z}, "") $stdout.puts "eml-id: #{eml_id}" $stdout.puts "path: #{path}" $stdout.puts "mailbox: #{mailbox}" $stdout.puts "from: #{Array(mail.from).join(", ")}" if mail.from $stdout.puts "to: #{Array(mail.to).join(", ")}" if mail.to $stdout.puts "cc: #{Array(mail.cc).join(", ")}" if mail.cc $stdout.puts "subject: #{mail.subject}" $stdout.puts "date: #{Mailmate.localize(mail.date)&.iso8601}" $stdout.puts "message-id: #{mail.}" if mail..any? $stdout.puts "attachments:" mail..each do |a| sz = begin a.body.decoded.bytesize rescue StandardError 0 end $stdout.puts " - #{a.filename || "(no name)"} #{a.mime_type} #{sz}b" end end $stdout.puts end |
#run(argv) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
# File 'lib/mailmate/cli/message.rb', line 16 def run(argv) opts = (argv) input = argv.first return usage_error("missing <id>") if input.nil? || input.empty? # Accept either eml-id (all digits) or RFC Message-ID (with or without # angle brackets). Lets you copy/paste from any machine without caring # whether the local eml-id matches. eml_id = Mailmate::EmlLookup.resolve_id(input) if eml_id.nil? || eml_id.zero? warn "Not found: #{input.inspect} (couldn't resolve as eml-id or Message-ID)" return 1 end path = Mailmate::EmlLookup.path_for(eml_id) unless path warn "Not found: #{eml_id}.eml" return 1 end if opts[:mailmate] require_relative "open" # Hand off to mmopen — same id resolution already done, but Open re-resolves # so the two paths stay symmetric. Tiny double-resolve is fine. return Mailmate::CLI::Open.run([input]) end if opts[:raw] $stdout.binmode $stdout.write(File.binread(path)) return 0 end mail = Mail.read(path) print_headers(mail, eml_id, path) unless opts[:text_only] $stdout.puts text_body(mail, markdown: opts[:markdown]) 0 end |
#text_body(mail, markdown: false) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
# File 'lib/mailmate/cli/message.rb', line 106 def text_body(mail, markdown: false) if mail.text_part # text/plain is already plain — markdown flag is a no-op here. mail.text_part.decoded.force_encoding("UTF-8").scrub elsif mail.html_part html = mail.html_part.decoded.force_encoding("UTF-8").scrub markdown ? html_to_markdown(html) : "[no text/plain part — HTML rendered below; use --raw for original]\n\n#{html}" else body = mail.body.decoded.to_s.force_encoding("UTF-8").scrub if markdown && html_like?(mail, body) html_to_markdown(body) else body end end end |
#usage_error(msg) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
73 74 75 76 77 78 |
# File 'lib/mailmate/cli/message.rb', line 73 def usage_error(msg) warn "mmmessage: #{msg}" warn "Usage: mmmessage <id> [--raw|--text-only|--mailmate|--markdown]" warn " <id> is either an eml-id (digits) or an RFC Message-ID." 2 end |