Class: Pikuri::Thunderbird::Gloda::Mail

Inherits:
Object
  • Object
show all
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).

Returns:

  • (Integer)

    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.

Returns:

  • (Integer)

    absolute floor on the pre-dedup fetch.

40

Instance Method Summary collapse

Constructor Details

#initialize(gloda:) ⇒ Mail

Parameters:

  • gloda (Gloda)

    the index service whose snapshot this queries.



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.

Parameters:

  • message_id (String)

    the #search handle (an RFC Message-ID or a gloda:<id> fallback).

Returns:

  • (Hash, nil)

    {message_id:, date:, from:, to:, subject:, folder:, attachment_names:, body:}, or nil if not found.



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 = message_id.to_s[/\Agloda:(\d+)\z/, 1]
  where, bind = gloda_id ? ['m.id = ?', gloda_id.to_i] : ['m.headerMessageID = ?', message_id]
  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, author, recipients, folder = row
  {
    message_id: header_msgid && !header_msgid.empty? ? header_msgid : "gloda:#{id}",
    date: to_time(date_us), from: author, 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.

Parameters:

  • query (String)

    free text over body/subject/author/recipients/ attachment-names.

  • limit (Integer)

    max hits after dedup.

  • from (String, nil) (defaults to: nil)

    substring match on the author.

  • to (String, nil) (defaults to: nil)

    substring match on the recipients.

  • subject (String, nil) (defaults to: nil)

    substring match on the subject.

  • after (Time, nil) (defaults to: nil)

    only messages on/after this instant.

  • before (Time, nil) (defaults to: nil)

    only messages on/before this instant.

  • folder (String, nil) (defaults to: nil)

    exact Gloda folder name.

Returns:

  • (Array<Hash>)

    [{message_id:, date: Time, from:, to:, subject:, folder:, snippet:}, …], best first, deduped.



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