Class: Pikuri::Thunderbird::Gloda::Mail
- Inherits:
-
Object
- Object
- Pikuri::Thunderbird::Gloda::Mail
- Defined in:
- lib/pikuri/thunderbird/gloda/mail.rb
Overview
Mail search + read over the Pikuri::Thunderbird::Gloda index service — the mail half of the corpus (its people counterpart is Contacts).
mail = Gloda::Mail.new(gloda: gloda)
hit = mail.search(query: 'invoice', limit: 10).first # ranked, deduped
mail.read(message_id: hit[:message_id]) # full decoded body
It owns no resources: it queries Pikuri::Thunderbird::Gloda's live snapshot through #with_fresh_db (so a rebuild under it is transparent), and there is nothing to close — the Pikuri::Thunderbird::Gloda it holds is closed by its own owner.
Gmail dedup
Gmail exposes each label as an IMAP folder, so a message with N labels
yields N Gloda rows (+[Gmail]/All Mail+ alone duplicates nearly
everything). #search over-fetches and dedups by headerMessageID
(falling back to a gloda:<id> handle when absent) before the top-N
cap, so duplicates can't eat result slots.
Constant Summary collapse
- OVERFETCH =
Returns over-fetch multiple before dedup+cap (so Gmail label duplicates don't shrink recall).
4- MIN_FETCH =
Returns absolute floor on the pre-dedup fetch.
40
Instance Method Summary collapse
- #initialize(gloda:) ⇒ Mail constructor
-
#read(message_id:) ⇒ Hash?
Full decoded body + headers + attachment names for one message.
-
#search(query:, limit:, from: nil, to: nil, subject: nil, after: nil, before: nil, folder: nil) ⇒ Array<Hash>
Ranked, deduped mail search.
Constructor Details
#initialize(gloda:) ⇒ Mail
34 35 36 |
# File 'lib/pikuri/thunderbird/gloda/mail.rb', line 34 def initialize(gloda:) @gloda = gloda end |
Instance Method Details
#read(message_id:) ⇒ Hash?
Full decoded body + headers + attachment names for one message.
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
# File 'lib/pikuri/thunderbird/gloda/mail.rb', line 74 def read(message_id:) gloda_id = .to_s[/\Agloda:(\d+)\z/, 1] where, bind = gloda_id ? ['m.id = ?', gloda_id.to_i] : ['m.headerMessageID = ?', ] row = @gloda.with_fresh_db do |db| db.execute(<<~SQL, [bind]).first SELECT m.id, m.headerMessageID, m.date, c.c0body, c.c1subject, c.c2attachmentNames, c.c3author, c.c4recipients, fl.name AS folder FROM messages m JOIN messagesText_content c ON c.docid = m.id LEFT JOIN folderLocations fl ON fl.id = m.folderID WHERE #{where} AND m.deleted = 0 ORDER BY m.date DESC LIMIT 1 SQL end return nil unless row id, header_msgid, date_us, body, subj, attach, , recipients, folder = row { message_id: header_msgid && !header_msgid.empty? ? header_msgid : "gloda:#{id}", date: to_time(date_us), from: , to: recipients, subject: subj, folder: folder, attachment_names: attach.to_s, body: body.to_s } end |
#search(query:, limit:, from: nil, to: nil, subject: nil, after: nil, before: nil, folder: nil) ⇒ Array<Hash>
Ranked, deduped mail search. query is matched as an OR of terms
over all decoded columns (recall-first); bm25 ranks, recency
breaks ties. Structured filters are exact predicates applied
alongside. An empty query degrades to a filter-only, date-ordered
listing.
55 56 57 58 59 60 61 62 63 64 65 66 |
# File 'lib/pikuri/thunderbird/gloda/mail.rb', line 55 def search(query:, limit:, from: nil, to: nil, subject: nil, after: nil, before: nil, folder: nil) fetch = [limit * OVERFETCH, MIN_FETCH].max rows = @gloda.with_fresh_db do |db| if query.to_s.strip.empty? filter_only(db, from:, to:, subject:, after:, before:, folder:, fetch:) else fts_search(db, query:, from:, to:, subject:, after:, before:, folder:, fetch:) end end dedup(rows).first(limit) end |