Module: Mailmate::Attributes Private

Defined in:
lib/mailmate/attributes.rb

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.

Defined Under Namespace

Classes: AddressValue

Constant Summary collapse

SHORTHANDS =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

{
  "#recipient"   => %w[to cc bcc],
  "#any-address" => %w[from to cc bcc],
  "#mailer"      => %w[x-mailer user-agent x-newsreader],
  "#date"        => :date,
  "#date-received" => :date_received,
  "#date-sent"   => :date,
}.freeze
INDEX_DATE_HEADS =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

—- head resolution —-

%w[#date #date-sent #date-received #date-last-viewed].freeze

Class Method Summary collapse

Class Method Details

.addrs(mail, sym) ⇒ 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.

Address fields can carry either an email or a display name. We return *one Address-shaped string* per recipient, plus separately accessible display names — ‘step` decomposes further on `.name` / `.address`.



145
146
147
148
149
150
151
152
# File 'lib/mailmate/attributes.rb', line 145

def self.addrs(mail, sym)
  field = mail[sym]
  return nil unless field
  list = Array(field.respond_to?(:addrs) ? field.addrs : nil)
  return [field.value.to_s] if list.empty?
  # Each Mail::Address has #display_name, #address, #name
  list.map { |a| AddressValue.new(a) }
end

.domain_level(domain, level) ⇒ 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.



278
279
280
281
282
283
284
285
286
287
# File 'lib/mailmate/attributes.rb', line 278

def self.domain_level(domain, level)
  parts = domain.to_s.split(".")
  return nil if parts.empty?
  case level
  when "top-level"    then parts[-1]
  when "second-level" then parts[-2]
  when "third-level"  then parts[-3]
  when "final-level"  then parts[0]
  end
end

.head_values(mail, head, eml_id = nil) ⇒ 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.



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/mailmate/attributes.rb', line 55

def self.head_values(mail, head, eml_id = nil)
  # Index-backed paths: never need the Mail object.
  if INDEX_DATE_HEADS.include?(head) && eml_id
    s = (IndexReader.for(head).value_for(eml_id) rescue nil)
    return nil if s.nil? || s.empty?
    begin
      return Time.parse(s)
    rescue ArgumentError
      return nil
    end
  end

  case head
  when "##thread-id"
    # Heuristic thread-id: root Message-ID of the References chain (or
    # In-Reply-To, or the message's own Message-ID). Requires the headers
    # — return nil cleanly if we're in index-only mode without a Mail.
    return nil if mail.nil?
    thread_id_for(mail)
  when "#flags"
    # Returns the raw flag tokens as strings (e.g. ["\\Seen", "$Forwarded"]).
    # Empty array if the message has no flags / isn't indexed.
    # Resolved via the binary `Database.noindex/Headers/#flags` index.
    return [] if eml_id.nil?
    IndexReader.for("#flags").flags_for(eml_id)
  when "##tags"
    return [] if eml_id.nil?
    # ##tags is a related index (multiValue) for user-facing tag names.
    # Best-effort: fall back to #flags if ##tags isn't present.
    begin
      IndexReader.for("##tags").flags_for(eml_id)
    rescue ArgumentError
      IndexReader.for("#flags").flags_for(eml_id)
    end
  else
    # All other heads need the Mail object. In index-only mode, mail is
    # nil — return nil so comparisons fail cleanly rather than crashing.
    return nil if mail.nil?
    head_values_from_mail(mail, head)
  end
end

.head_values_from_mail(mail, head) ⇒ 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.



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/mailmate/attributes.rb', line 97

def self.head_values_from_mail(mail, head)
  case head
  when "from"
    addrs(mail, :from)
  when "to"
    addrs(mail, :to)
  when "cc"
    addrs(mail, :cc)
  when "bcc"
    addrs(mail, :bcc)
  when "reply-to"
    addrs(mail, :reply_to)
  when "subject"
    mail.subject.to_s
  when "list-id"
    v = header_value(mail, "list-id")
    v && [v]
  when "in-reply-to"
    v = header_value(mail, "in-reply-to")
    v && [v]
  when "message-id"
    mail.message_id
  when "x-mailer", "user-agent", "x-newsreader"
    v = header_value(mail, head)
    v && [v]
  when "#recipient", "#any-address"
    SHORTHANDS[head].flat_map { |h| addrs(mail, h.to_sym) || [] }
  when "#mailer"
    SHORTHANDS[head].map { |h| header_value(mail, h) }.compact
  when "#date", "#date-sent"
    mail.date
  when "#date-received"
    # Best available proxy without IMAP: prefer the latest Received: timestamp,
    # falling back to the Date header. Receiving servers stamp Received headers
    # in reverse-chrono order; the topmost is the most recent.
    recv = mail.received
    recv = [recv].flatten.compact.first
    recv&.date_time && Time.parse(recv.date_time.to_s) rescue mail.date
  else
    # Unknown header — try as a raw header lookup.
    v = header_value(mail, head)
    v && [v]
  end
end

.header_value(mail, name) ⇒ 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.



173
174
175
176
177
# File 'lib/mailmate/attributes.rb', line 173

def self.header_value(mail, name)
  h = mail[name]
  return nil unless h
  h.value.to_s
end

.resolve(mail_or_message, 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.

Resolve a path to value(s). Returns nil/[] when nothing. ‘mail_or_message` may be a Mail::Message OR a Mailmate::Message (which carries the `eml_id` needed for index-based attributes like `#flags`).



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

def self.resolve(mail_or_message, path)
  mail   = mail_or_message.respond_to?(:mail) ? mail_or_message.mail : mail_or_message
  eml_id = mail_or_message.respond_to?(:eml_id) ? mail_or_message.eml_id : nil

  head, *rest = path
  values = head_values(mail, head, eml_id)
  values = Array(values).flatten.compact
  return nil if values.empty?

  rest.each do |seg|
    values = values.flat_map { |v| step(v, seg) }.compact
    return nil if values.empty?
  end

  values.size == 1 ? values.first : values
end

.step(value, seg) ⇒ 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.

—- decomposition step —-



181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# File 'lib/mailmate/attributes.rb', line 181

def self.step(value, seg)
  case value
  when AddressValue
    case seg
    when "name"    then value.name
    when "address" then value.address
    when "domain"
      a = value.address.to_s
      a.include?("@") ? a.split("@", 2).last : nil
    when "user"
      a = value.address.to_s
      a.include?("@") ? a.split("@", 2).first : a
    when "top-level", "second-level", "third-level", "final-level"
      a = value.address.to_s
      dom = a.include?("@") ? a.split("@", 2).last : nil
      dom && domain_level(dom, seg)
    else nil
    end
  when String
    case seg
    when "flag", "tag"
      # `#flags.flag` / `##tags.tag` — each value of the multi-value head
      # is already an individual flag/tag string. Passthrough.
      value
    when "body" # e.g. subject.body — strip Re:/Fwd: prefixes and [bracketed] blobs
      strip_subject_prefixes(value)
    when "blob"
      subject_blob(value)
    when "prefix"
      subject_prefix(value)
    when "identifier" # list-id <foo@bar>
      value =~ /<([^>]+)>/ ? Regexp.last_match(1) : value.strip
    when "description" # list-id "Description" <foo@bar>
      value =~ /^\s*"([^"]+)"|^([^<]+?)\s*</ ? (Regexp.last_match(1) || Regexp.last_match(2)).strip : nil
    when "user"
      value.include?("@") ? value.split("@", 2).first : value
    when "domain"
      value.include?("@") ? value.split("@", 2).last : nil
    when "top-level", "second-level", "third-level", "final-level"
      dom = value.include?("@") ? value.split("@", 2).last : value
      domain_level(dom, seg)
    else nil
    end
  when Time, DateTime
    t = value.respond_to?(:to_time) ? value.to_time : value
    case seg
    when "year"   then t.year.to_s
    when "month"  then t.month.to_s
    when "day"    then t.day.to_s
    when "hour"   then t.hour.to_s
    else nil
    end
  end
end

.strip_subject_prefixes(s) ⇒ 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.



236
237
238
239
240
241
242
243
244
245
246
247
248
249
# File 'lib/mailmate/attributes.rb', line 236

def self.strip_subject_prefixes(s)
  v = s.to_s.dup
  # Remove leading "Re:", "Fwd:", "[Tag]" sequences
  loop do
    if v.sub!(/\A\s*(?i:re|fw|fwd|sv|aw|antw|wg|tr)(?:\[\d+\])?\s*[::]\s*/, "")
      next
    end
    if v.sub!(/\A\s*\[[^\[\]]+\]\s*/, "")
      next
    end
    break
  end
  v
end

.subject_blob(s) ⇒ 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.



251
252
253
254
# File 'lib/mailmate/attributes.rb', line 251

def self.subject_blob(s)
  m = s.to_s.match(/\A(?:\s*(?i:re|fw|fwd|sv|aw|antw|wg|tr)(?:\[\d+\])?\s*[::]\s*)*\s*\[([^\[\]]+)\]/)
  m && m[1]
end

.subject_prefix(s) ⇒ 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.



256
257
258
259
# File 'lib/mailmate/attributes.rb', line 256

def self.subject_prefix(s)
  m = s.to_s.match(/\A((?:\s*(?i:re|fw|fwd|sv|aw|antw|wg|tr)(?:\[\d+\])?\s*[::]\s*)+)/)
  m && m[1].strip
end

.thread_id_for(mail) ⇒ 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.

Thread-id heuristic: use the FIRST Message-ID in the References header as the thread root, falling back to In-Reply-To, falling back to the message’s own Message-ID. Matches what most threading algorithms do.



264
265
266
267
268
269
270
271
272
273
274
275
276
# File 'lib/mailmate/attributes.rb', line 264

def self.thread_id_for(mail)
  refs = header_value(mail, "references").to_s
  if (m = refs.match(/<([^>]+)>/))
    return m[1]
  end
  irt = header_value(mail, "in-reply-to").to_s
  if (m = irt.match(/<([^>]+)>/))
    return m[1]
  end
  mid = mail.message_id.to_s
  mid = mid.tr("<>", "") if mid && !mid.empty?
  mid.empty? ? nil : mid
end