Module: Mailmate::EmlLookup

Defined in:
lib/mailmate/eml_lookup.rb

Overview

Map an ‘.eml` body-part ID to its absolute on-disk path.

Two implementations:

- The fast path uses MailMate's `#source` index
  (`Database.noindex/Headers/#source.{cache,offsets}`), which carries the
  `imap://account@host/path` URL for every indexed message. O(1) lookup.
- The fallback walks the IMAP tree with `Dir.glob`, the same shape as the
  `find -name <id>.eml` shell-out the original scripts used. Slower
  (seconds on a cold cache) but always works.

The fast path wins ~99% of the time. The fallback exists for messages MailMate hasn’t yet indexed (e.g. immediately after a fresh IMAP push) and for edge cases where ‘#source` is unreadable.

Class Method Summary collapse

Class Method Details

.eml_id_for_message_id(message_id) ⇒ Object

Reverse-lookup: given an RFC Message-ID (with or without angle brackets), return the local eml-id (integer) or nil. Backed by a value→id map built once per index snapshot (one O(n) pass, then O(1) lookups) — the map is pinned to the IndexReader object it was built from, so when the index rebuilds (file changed on disk) the map rebuilds with it. Matters for the persistent MCP server, where resolve_id is called repeatedly.



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/mailmate/eml_lookup.rb', line 33

def self.eml_id_for_message_id(message_id)
  needle = message_id.to_s.strip
  return nil if needle.empty?

  candidates = needle.start_with?("<") && needle.end_with?(">") ?
                 [needle, needle[1..-2]] :
                 [needle, "<#{needle}>"]

  map = mid_map
  candidates.each do |c|
    id = map[c]
    return id if id
  end
  nil
rescue ArgumentError
  nil
end

.mid_mapObject

value→eml-id map over the message-id index. ‘||=` keeps the FIRST id recorded for a duplicated Message-ID, matching the old scan’s first-match-wins semantics (duplicates are real: Sent + Received copies, Gmail label copies).



55
56
57
58
59
60
61
62
# File 'lib/mailmate/eml_lookup.rb', line 55

def self.mid_map
  reader = Mailmate::IndexReader.for("message-id")
  return @mid_map[:map] if @mid_map && @mid_map[:reader].equal?(reader)
  map = {}
  reader.each_record { |eml_id, value| map[value] ||= eml_id }
  @mid_map = { reader: reader, map: map }
  map
end

.path_for(eml_id) ⇒ Object

Returns an absolute path string, or nil if not found.



23
24
25
# File 'lib/mailmate/eml_lookup.rb', line 23

def self.path_for(eml_id)
  via_index(eml_id) || via_glob(eml_id)
end

.resolve_id(input) ⇒ Object

Resolve an identifier to a local eml-id. Accepts:

- eml-id (all digits)              e.g. "183715"
- RFC Message-ID, brackets optional e.g. "<abc@example.com>"
- message://… URL                  e.g. "message://%3Cabc%40example.com%3E"
                                        or "message://183715"
- mid:… URL                        e.g. "mid:%3Cabc%40example.com%3E"
                                        or "mid:183715"

Strips the URL wrapper first (message:// or mid:) so the digit check below catches the local-eml-id payload too — same leniency MailMate’s own URL handler offers. The %3C…%3E payload form is portable across machines; the bare-integer form is local-only.



75
76
77
78
79
80
# File 'lib/mailmate/eml_lookup.rb', line 75

def self.resolve_id(input)
  s = input.to_s.strip
  s = URI.decode_www_form_component(s.sub(%r{\A(?:message://|mid:)}, "")) if s.match?(%r{\A(?:message://|mid:)})
  return s.to_i if s =~ /\A\d+\z/
  eml_id_for_message_id(s)
end

.source_url_for(eml_id) ⇒ Object

Lookup the ‘imap://…` URL recorded in `#source` for this eml_id. Returns nil if the index doesn’t have a record for it.



105
106
107
108
109
110
# File 'lib/mailmate/eml_lookup.rb', line 105

def self.source_url_for(eml_id)
  Mailmate::IndexReader.for("#source").value_for(eml_id)
rescue ArgumentError
  # #source index missing (non-default MailMate install? fresh sync?).
  nil
end

.url_to_path(url, eml_id) ⇒ Object

Convert an ‘imap://account@host/mailbox/path` URL to the on-disk absolute path of the .eml file. Internal but exposed for tests.



114
115
116
117
118
119
120
121
# File 'lib/mailmate/eml_lookup.rb', line 114

def self.url_to_path(url, eml_id)
  stripped = url.sub(%r{\Aimap://}, "")
  , mailbox_path = stripped.split("/", 2)
  return nil if .nil? || mailbox_path.nil? || mailbox_path.empty?

  mailbox_dirs = mailbox_path.split("/").map { |seg| "#{seg}.mailbox" }.join("/")
  File.join(Mailmate.config.imap_root, , mailbox_dirs, "Messages", "#{eml_id}.eml")
end

.via_glob(eml_id) ⇒ Object

Force the glob fallback.



98
99
100
101
# File 'lib/mailmate/eml_lookup.rb', line 98

def self.via_glob(eml_id)
  matches = Dir.glob("#{Mailmate.config.imap_root}/*/**/Messages/#{eml_id}.eml")
  matches.first
end

.via_index(eml_id) ⇒ Object

Force the index path (useful for tests and benchmarking).

Returns nil — not just on missing-index — when the indexed path no longer points at an existing file. That happens whenever MailMate’s ‘#source` index lags the filesystem; the common case is right after `mm-modify`’s fast-path move renames the .eml on disk and MailMate hasn’t rescanned yet. Treating stale entries as misses lets ‘path_for` fall through to the glob walker and recover the file at its new home.



90
91
92
93
94
95
# File 'lib/mailmate/eml_lookup.rb', line 90

def self.via_index(eml_id)
  url = source_url_for(eml_id)
  return nil if url.nil?
  path = url_to_path(url, eml_id)
  path if path && File.exist?(path)
end