Module: Mailmate::CLI::Discover Private

Extended by:
Discover
Included in:
Discover
Defined in:
lib/mailmate/cli/discover.rb

Overview

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.

‘mmdiscover` — bootstrap ~/.config/mailmate/configconfig.yml,bundle_loaderconfig.yml,bundle_loader.rb from MailMate’s own Sources.plist and Identities.plist.

First-run UX: a user installs the gem, runs ‘mmdiscover`, sees their accounts and identity addresses laid out, confirms, and the gem now knows who they are without any hand-editing of YAML or `plutil` archaeology.

Constant Summary collapse

CONFIG_DIR =

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.

File.expand_path("~/.config/mailmate")
CONFIG_FILE =

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.

File.join(CONFIG_DIR, "config.yml")
LOADER_FILE =

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.

File.join(CONFIG_DIR, "bundle_loader.rb")

Instance Method Summary collapse

Instance Method Details

#parse_options(argv) ⇒ 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.



88
89
90
91
92
93
94
95
96
97
98
# File 'lib/mailmate/cli/discover.rb', line 88

def parse_options(argv)
  opts = { dry_run: false, yes: false, force: false, app_support_dir: nil }
  OptionParser.new do |o|
    o.banner = "Usage: mmdiscover [--app-support-dir PATH] [--dry-run] [--yes] [--force]"
    o.on("--app-support-dir PATH", "Override the MailMate app-support location") { |p| opts[:app_support_dir] = File.expand_path(p) }
    o.on("--dry-run", "Print what would be written; don't write") { opts[:dry_run] = true }
    o.on("-y", "--yes", "Skip the y/N prompt") { opts[:yes] = true }
    o.on("--force", "Overwrite existing config even if it has identities not in Identities.plist") { opts[:force] = true }
  end.parse!(argv)
  opts
end

#read_accounts(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.

Read Sources.plist via plutil. Returns Array<host:>.



111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/mailmate/cli/discover.rb', line 111

def read_accounts(path)
  return [] unless File.exist?(path)
  json = `plutil -convert json -o - #{shellesc(path)}`
  data = JSON.parse(json) rescue {}
  sources = data["sources"] || data["Sources"] || []
  sources.map do |s|
    name = s["name"] || s["Name"] || "(unnamed)"
    server_url = s["serverURL"] || s["ServerURL"] || ""
    host = server_url.split("@").last || "(no host)"
    { name: name, host: host }
  end
end

#read_existing_identities(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.

Read the existing config.yml’s ‘identities:` list, if the file exists. Returns Array<String> (possibly empty) or nil if the file is missing.



102
103
104
105
106
107
108
# File 'lib/mailmate/cli/discover.rb', line 102

def read_existing_identities(path)
  return nil unless File.exist?(path)
  require "yaml"
  data = YAML.safe_load_file(path) rescue nil
  return [] unless data.is_a?(Hash)
  Array(data["identities"]).map { |a| a.to_s.downcase.strip }.reject(&:empty?)
end

#read_identities(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.

Read Identities.plist via plutil. Returns Array<String> of email addresses, lowercased and deduplicated. Each identity entry has an ‘emailAddresses` field that may be a single string or a newline- delimited list.



128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/mailmate/cli/discover.rb', line 128

def read_identities(path)
  return [] unless File.exist?(path)
  json = `plutil -convert json -o - #{shellesc(path)}`
  data = JSON.parse(json) rescue {}
  ids = data["identities"] || data["Identities"] || []

  all = ids.flat_map do |i|
    raw = i["emailAddresses"] || i["EmailAddresses"] || i["emailAddress"] || ""
    raw.to_s.split(/[\s,]+/)
  end
  all.map { |a| a.strip.downcase }.reject(&:empty?).uniq
end

#run(argv) ⇒ 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.



24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
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
# File 'lib/mailmate/cli/discover.rb', line 24

def run(argv)
  opts = parse_options(argv)
  app_support_dir = opts[:app_support_dir] || File.expand_path("~/Library/Application Support/MailMate")

  unless File.directory?(app_support_dir)
    warn "mmdiscover: MailMate app-support directory not found at #{app_support_dir}"
    warn "  Pass --app-support-dir if your install is elsewhere, or check that MailMate is installed."
    return 1
  end

  sources_plist    = File.join(app_support_dir, "Sources.plist")
  identities_plist = File.join(app_support_dir, "Identities.plist")

  accounts   = read_accounts(sources_plist)
  identities = read_identities(identities_plist)

  $stdout.puts "Found #{accounts.size} account#{"s" if accounts.size != 1} in Sources.plist:"
  accounts.each { |a| $stdout.puts "  #{a[:name].ljust(35)}(#{a[:host]})" }
  $stdout.puts
  $stdout.puts "Found #{identities.size} identity address#{"es" if identities.size != 1} in Identities.plist:"
  identities.each { |addr| $stdout.puts "  #{addr}" }
  $stdout.puts

  # Compare against existing config, if any, and surface differences.
  # Without --force we won't silently overwrite manual additions
  # (e.g. aliases that aren't outgoing identities in MailMate).
  existing_identities = read_existing_identities(CONFIG_FILE)
  if existing_identities && !opts[:force] && !opts[:dry_run]
    only_in_existing = existing_identities - identities
    only_in_new      = identities - existing_identities

    if !only_in_existing.empty? || !only_in_new.empty?
      $stdout.puts "Existing #{CONFIG_FILE} has a different identity set:"
      only_in_existing.each { |a| $stdout.puts "  - #{a}  (in current config, NOT in Identities.plist)" }
      only_in_new.each      { |a| $stdout.puts "  + #{a}  (in Identities.plist, NOT in current config)" }
      $stdout.puts
      $stdout.puts "Re-run with --force to overwrite, or hand-merge if you have manual additions to preserve."
      return 0
    end
  end

  if opts[:dry_run]
    $stdout.puts "(--dry-run) Would write #{CONFIG_FILE} and #{LOADER_FILE}; skipping."
    return 0
  end

  unless opts[:yes]
    $stdout.print "Write these to #{CONFIG_FILE}? [y/N] "
    response = $stdin.gets&.strip&.downcase
    unless %w[y yes].include?(response)
      $stdout.puts "Aborted."
      return 0
    end
  end

  FileUtils.mkdir_p(CONFIG_DIR)
  write_config(identities, app_support_dir, opts[:app_support_dir])
  write_bundle_loader

  $stdout.puts "Wrote #{CONFIG_FILE}"
  $stdout.puts "Wrote #{LOADER_FILE}"
  0
end

#shellesc(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.



165
166
167
# File 'lib/mailmate/cli/discover.rb', line 165

def shellesc(s)
  "'#{s.gsub("'", "'\\\\''")}'"
end

#write_bundle_loaderObject

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.



154
155
156
157
158
159
160
161
162
163
# File 'lib/mailmate/cli/discover.rb', line 154

def write_bundle_loader
  gem_lib = File.expand_path("../..", __dir__) # → ~/code/claude/mailmate/lib
  File.open(LOADER_FILE, "w") do |f|
    f.puts "# Mailmate gem bundle-loader — written by mmdiscover."
    f.puts "# MailMate bundle handlers `load` this file then `require \"mailmate\"`."
    f.puts "# Re-run mmdiscover to refresh if the gem's location changes."
    f.puts
    f.puts "$LOAD_PATH.unshift #{gem_lib.inspect} unless $LOAD_PATH.include?(#{gem_lib.inspect})"
  end
end

#write_config(identities, _app_support_dir, app_support_override) ⇒ 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.



141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/mailmate/cli/discover.rb', line 141

def write_config(identities, _app_support_dir, app_support_override)
  File.open(CONFIG_FILE, "w") do |f|
    f.puts "# Mailmate gem configuration — written by mmdiscover on #{Time.now.strftime("%Y-%m-%d %H:%M")}"
    f.puts
    if app_support_override
      f.puts "app_support_dir: #{app_support_override}"
      f.puts
    end
    f.puts "identities:"
    identities.each { |addr| f.puts "  - #{addr}" }
  end
end