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
-
.addrs(mail, sym) ⇒ Object
private
Address fields can carry either an email or a display name.
- .domain_level(domain, level) ⇒ Object private
- .head_values(mail, head, eml_id = nil) ⇒ Object private
- .head_values_from_mail(mail, head) ⇒ Object private
- .header_value(mail, name) ⇒ Object private
-
.resolve(mail_or_message, path) ⇒ Object
private
Resolve a path to value(s).
-
.step(value, seg) ⇒ Object
private
—- decomposition step —-.
- .strip_subject_prefixes(s) ⇒ Object private
- .subject_blob(s) ⇒ Object private
- .subject_prefix(s) ⇒ Object private
-
.thread_id_for(mail) ⇒ Object
private
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.
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. 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(, path) mail = .respond_to?(:mail) ? .mail : eml_id = .respond_to?(:eml_id) ? .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..to_s mid = mid.tr("<>", "") if mid && !mid.empty? mid.empty? ? nil : mid end |