Class: Pikuri::Thunderbird::Gloda::Contacts

Inherits:
Object
  • Object
show all
Defined in:
lib/pikuri/thunderbird/gloda/contacts.rb

Overview

The people half of the Gloda corpus (its mail counterpart is Mail): resolve a name to an address, and check recipient novelty. Both read the user's own correspondence graph, so they belong together and away from mail search/read.

contacts = Gloda::Contacts.new(gloda: gloda)
contacts.resolve(query: 'jon', limit: 5)  # name → ranked addresses
contacts.domains_seen(['acme.com', 'paypa1.com'])  # => ['acme.com']

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.

Two graphs, one purpose

#resolve reads the derived graph — the decoded c3author / c4recipients columns, where display name and address sit together — because Gloda's contacts table is dead for this (frecency/popularity unpopulated, name unreliable). #domains_seen reads the identity graph (the identities table). Both answer "who has this user actually corresponded with?" — #resolve to fill a recipient, #domains_seen to flag a novel one.

Constant Summary collapse

LOGGER =
Pikuri.logger_for('Thunderbird::Gloda::Contacts')
SCAN =

Returns max matched messages scanned when tallying contact frequency in #resolve — bounds work on a very common name.

Returns:

  • (Integer)

    max matched messages scanned when tallying contact frequency in #resolve — bounds work on a very common name.

2000
SENT_WEIGHT =

Returns extra weight a Sent-folder appearance adds over a received one. "I have mailed this person" is the strongest real-contact signal, and a spoofer is never in your Sent — so this is what sinks a spoofed one-off below the genuine contact.

Returns:

  • (Integer)

    extra weight a Sent-folder appearance adds over a received one. "I have mailed this person" is the strongest real-contact signal, and a spoofer is never in your Sent — so this is what sinks a spoofed one-off below the genuine contact.

3
SENT_PRIORITY =

Returns Gloda's indexingPriority for a Sent folder.

Returns:

  • (Integer)

    Gloda's indexingPriority for a Sent folder.

60
EMAIL_RE =

Bracket-agnostic email token: angle brackets, when present, fall outside the class, so Jon <jon@x> and a bare jon@x (name trailing) extract to the same jon@x. Commas/semicolons/quotes bound it too, so it never swallows the next recipient or a nickname quote.

/[^\s<>,;"']+@[^\s<>,;"']+/

Instance Method Summary collapse

Constructor Details

#initialize(gloda:) ⇒ Contacts

Parameters:

  • gloda (Gloda)

    the mail store whose snapshot this queries.



54
55
56
# File 'lib/pikuri/thunderbird/gloda/contacts.rb', line 54

def initialize(gloda:)
  @gloda = gloda
end

Instance Method Details

#domains_seen(domains) ⇒ Array<String>

Which of domains the user has correspondence history with, per Gloda's identity graph (+identities+ collects the email of every author and recipient across all indexed messages, Sent included — so it answers "have I ever exchanged mail with this domain?" in both directions). Powers ComposeGuard's recipient-novelty warn.

contacts.domains_seen(['acme.com', 'paypa1.com'])  # => ['acme.com']

Parameters:

  • domains (Array<String>)

    lowercased bare domains.

Returns:

  • (Array<String>)

    the subset present in the identity graph.

Raises:

  • (SQLite3::SQLException)

    if the identities schema differs from the probed shape — the caller degrades (the schema is unverified across Thunderbird versions).



113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/pikuri/thunderbird/gloda/contacts.rb', line 113

def domains_seen(domains)
  return [] if domains.empty?

  @gloda.with_fresh_db do |db|
    domains.select do |domain|
      db.get_first_value(<<~SQL, [domain])
        SELECT 1 FROM identities
        WHERE kind = 'email'
          AND lower(substr(value, instr(value, '@') + 1)) = ?
        LIMIT 1
      SQL
    end
  end
end

#resolve(query:, limit:) ⇒ Array<Hash>

Resolve a name (or partial address) to ranked email addresses drawn from the user's own correspondence graph — the decoded author / recipient columns. Feeds MailCompose: the model has a name, compose needs an address.

contacts.resolve(query: 'martin', limit: 5)
# => [{address: 'martin@vysny.me', name: 'Martin Vysny',
#      count: 42, sent: 12, score: 78}, …]

query terms match (recall-first OR, token-wise) against the author and recipient columns only, via Pikuri::Thunderbird::Gloda's shared FTS5 index; every email token in a matching entry is extracted bracket-agnostically, grouped by lower-cased address, and ranked by appearance count with a Sent-folder bonus. Never auto-picks — several people/addresses come back and disambiguation is the caller's job (silently choosing one is the mis-send ComposeGuard exists to prevent). Reverse lookup (address → who?) is the same call with the address as query.

Parameters:

  • query (String)

    a name or partial address, e.g. "Jon Snow".

  • limit (Integer)

    max candidates returned.

Returns:

  • (Array<Hash>)

    [{address:, name: (String, nil), count:, sent:, score:}, …], best first, deduped by lower-cased address; empty when the query has no alphanumeric terms or nothing matches.



82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/pikuri/thunderbird/gloda/contacts.rb', line 82

def resolve(query:, limit:)
  terms = query.to_s.scan(/[[:alnum:]]+/)
  return [] if terms.empty?

  rows = @gloda.with_fresh_db do |db|
    db.execute(<<~SQL, [match_expr(terms), SCAN])
      SELECT c.c3author, c.c4recipients, fl.indexingPriority
      FROM fts
      JOIN messages m ON m.id = fts.rowid
      JOIN messagesText_content c ON c.docid = fts.rowid
      LEFT JOIN folderLocations fl ON fl.id = m.folderID
      WHERE fts MATCH ? AND m.deleted = 0
      LIMIT ?
    SQL
  end
  tally(rows, terms).first(limit)
end