Module: Mailmate::CLI::Message Private

Extended by:
Message
Included in:
Message
Defined in:
lib/mailmate/cli/message.rb

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

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.

Returns:

  • (Boolean)


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.message})"
    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 &nbsp; 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 parse_options(argv)
  opts = { raw: false, text_only: false, mailmate: false, markdown: false }
  OptionParser.new do |o|
    o.banner = "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

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.message_id}"
  if mail.attachments.any?
    $stdout.puts "attachments:"
    mail.attachments.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 = parse_options(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