mournmail 0.1.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/README.md +50 -27
- data/exe/mournmail_reindex +16 -0
- data/lib/mournmail.rb +2 -2
- data/lib/mournmail/commands.rb +23 -30
- data/lib/mournmail/config.rb +21 -8
- data/lib/mournmail/draft_mode.rb +101 -22
- data/lib/mournmail/faces.rb +0 -2
- data/lib/mournmail/mail_encoded_word_patch.rb +71 -0
- data/lib/mournmail/message_mode.rb +60 -15
- data/lib/mournmail/message_rendering.rb +124 -36
- data/lib/mournmail/search_result_mode.rb +143 -0
- data/lib/mournmail/summary.rb +94 -23
- data/lib/mournmail/summary_mode.rb +427 -48
- data/lib/mournmail/utils.rb +474 -61
- data/lib/mournmail/version.rb +1 -3
- data/lib/textbringer_plugin.rb +0 -2
- data/mournmail.gemspec +7 -3
- metadata +76 -18
data/lib/mournmail/utils.rb
CHANGED
@@ -1,11 +1,42 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
require "mail"
|
4
|
-
require "mail-iso-2022-jp"
|
5
2
|
require "net/imap"
|
6
3
|
require "time"
|
4
|
+
require "tempfile"
|
7
5
|
require "fileutils"
|
8
6
|
require "timeout"
|
7
|
+
require "digest"
|
8
|
+
require "nkf"
|
9
|
+
require "groonga"
|
10
|
+
require 'google/api_client/client_secrets'
|
11
|
+
require 'google/api_client/auth/storage'
|
12
|
+
require 'google/api_client/auth/storages/file_store'
|
13
|
+
require 'launchy'
|
14
|
+
|
15
|
+
class Net::SMTP
|
16
|
+
def auth_xoauth2(user, secret)
|
17
|
+
check_auth_args user, secret
|
18
|
+
res = critical {
|
19
|
+
s = Mournmail.xoauth2_string(user, secret)
|
20
|
+
get_response('AUTH XOAUTH2 ' + base64_encode(s))
|
21
|
+
}
|
22
|
+
check_auth_response res
|
23
|
+
res
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class Net::IMAP::Xoauth2Authenticator
|
28
|
+
def process(data)
|
29
|
+
Mournmail.xoauth2_string(@user, @access_token)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def initialize(user, access_token)
|
35
|
+
@user = user
|
36
|
+
@access_token = access_token
|
37
|
+
end
|
38
|
+
end
|
39
|
+
Net::IMAP.add_authenticator("XOAUTH2", Net::IMAP::Xoauth2Authenticator)
|
9
40
|
|
10
41
|
module Mournmail
|
11
42
|
begin
|
@@ -15,31 +46,84 @@ module Mournmail
|
|
15
46
|
HAVE_MAIL_GPG = false
|
16
47
|
end
|
17
48
|
|
18
|
-
def self.define_variable(name,
|
49
|
+
def self.define_variable(name, initial_value: nil, attr: nil)
|
19
50
|
var_name = "@" + name.to_s
|
20
51
|
if !instance_variable_defined?(var_name)
|
21
|
-
instance_variable_set(var_name,
|
52
|
+
instance_variable_set(var_name, initial_value)
|
53
|
+
end
|
54
|
+
case attr
|
55
|
+
when :accessor
|
56
|
+
singleton_class.send(:attr_accessor, name)
|
57
|
+
when :reader
|
58
|
+
singleton_class.send(:attr_reader, name)
|
59
|
+
when :writer
|
60
|
+
singleton_class.send(:attr_writer, name)
|
22
61
|
end
|
23
|
-
singleton_class.send(:attr_accessor, name)
|
24
62
|
end
|
25
63
|
|
26
|
-
define_variable :current_mailbox
|
27
|
-
define_variable :current_summary
|
28
|
-
define_variable :current_uid
|
29
|
-
define_variable :current_mail
|
30
|
-
define_variable :background_thread
|
64
|
+
define_variable :current_mailbox, attr: :accessor
|
65
|
+
define_variable :current_summary, attr: :accessor
|
66
|
+
define_variable :current_uid, attr: :accessor
|
67
|
+
define_variable :current_mail, attr: :accessor
|
68
|
+
define_variable :background_thread, attr: :accessor
|
69
|
+
define_variable :background_thread_mutex, initial_value: Mutex.new
|
70
|
+
define_variable :keep_alive_thread, attr: :accessor
|
71
|
+
define_variable :keep_alive_thread_mutex, initial_value: Mutex.new
|
72
|
+
define_variable :imap
|
73
|
+
define_variable :imap_mutex, initial_value: Mutex.new
|
74
|
+
define_variable :mailboxes, initial_value: []
|
75
|
+
define_variable :current_account
|
76
|
+
define_variable :account_config
|
77
|
+
define_variable :groonga_db
|
31
78
|
|
32
|
-
def self.background
|
33
|
-
|
34
|
-
|
79
|
+
def self.background(skip_if_busy: false)
|
80
|
+
@background_thread_mutex.synchronize do
|
81
|
+
if background_thread&.alive?
|
82
|
+
if skip_if_busy
|
83
|
+
return
|
84
|
+
else
|
85
|
+
raise EditorError, "Another background thread is running"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
self.background_thread = Utils.background {
|
89
|
+
begin
|
90
|
+
yield
|
91
|
+
ensure
|
92
|
+
self.background_thread = nil
|
93
|
+
end
|
94
|
+
}
|
35
95
|
end
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
96
|
+
end
|
97
|
+
|
98
|
+
def self.start_keep_alive_thread
|
99
|
+
@keep_alive_thread_mutex.synchronize do
|
100
|
+
if keep_alive_thread
|
101
|
+
raise EditorError, "Keep alive thread already running"
|
41
102
|
end
|
42
|
-
|
103
|
+
self.keep_alive_thread = Thread.start {
|
104
|
+
loop do
|
105
|
+
sleep(CONFIG[:mournmail_keep_alive_interval])
|
106
|
+
background(skip_if_busy: true) do
|
107
|
+
begin
|
108
|
+
imap_connect do |imap|
|
109
|
+
imap.noop
|
110
|
+
end
|
111
|
+
rescue => e
|
112
|
+
message("Error in IMAP NOOP: #{e.class}: #{e.message}")
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
}
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def self.stop_keep_alive_thread
|
121
|
+
@keep_alive_thread_mutex.synchronize do
|
122
|
+
if keep_alive_thread
|
123
|
+
keep_alive_thread&.kill
|
124
|
+
self.keep_alive_thread = nil
|
125
|
+
end
|
126
|
+
end
|
43
127
|
end
|
44
128
|
|
45
129
|
def self.message_window
|
@@ -74,19 +158,51 @@ module Mournmail
|
|
74
158
|
escape_binary(s)
|
75
159
|
end
|
76
160
|
|
77
|
-
|
78
|
-
|
161
|
+
def self.current_account
|
162
|
+
init_current_account
|
163
|
+
@current_account
|
164
|
+
end
|
165
|
+
|
166
|
+
def self.account_config
|
167
|
+
init_current_account
|
168
|
+
@account_config
|
169
|
+
end
|
170
|
+
|
171
|
+
def self.init_current_account
|
172
|
+
if @current_account.nil?
|
173
|
+
@current_account, @account_config = CONFIG[:mournmail_accounts].first
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def self.current_account=(name)
|
178
|
+
unless CONFIG[:mournmail_accounts].key?(name)
|
179
|
+
raise ArgumentError, "No such account: #{name}"
|
180
|
+
end
|
181
|
+
@current_account = name
|
182
|
+
@account_config = CONFIG[:mournmail_accounts][name]
|
183
|
+
end
|
79
184
|
|
80
185
|
def self.imap_connect
|
81
186
|
@imap_mutex.synchronize do
|
187
|
+
if keep_alive_thread.nil?
|
188
|
+
start_keep_alive_thread
|
189
|
+
end
|
82
190
|
if @imap.nil? || @imap.disconnected?
|
191
|
+
conf = account_config
|
192
|
+
auth_type = conf[:imap_options][:auth_type] || "PLAIN"
|
193
|
+
password = conf[:imap_options][:password]
|
194
|
+
if auth_type == "gmail"
|
195
|
+
auth_type = "XOAUTH2"
|
196
|
+
password = google_access_token
|
197
|
+
end
|
83
198
|
Timeout.timeout(CONFIG[:mournmail_imap_connect_timeout]) do
|
84
|
-
@imap = Net::IMAP.new(
|
85
|
-
|
86
|
-
@imap.authenticate(
|
87
|
-
|
88
|
-
|
89
|
-
|
199
|
+
@imap = Net::IMAP.new(conf[:imap_host],
|
200
|
+
conf[:imap_options])
|
201
|
+
@imap.authenticate(auth_type, conf[:imap_options][:user_name],
|
202
|
+
password)
|
203
|
+
@mailboxes = @imap.list("", "*").map { |mbox|
|
204
|
+
Net::IMAP.decode_utf7(mbox.name)
|
205
|
+
}
|
90
206
|
if Mournmail.current_mailbox
|
91
207
|
@imap.select(Mournmail.current_mailbox)
|
92
208
|
end
|
@@ -101,6 +217,7 @@ module Mournmail
|
|
101
217
|
|
102
218
|
def self.imap_disconnect
|
103
219
|
@imap_mutex.synchronize do
|
220
|
+
stop_keep_alive_thread
|
104
221
|
if @imap
|
105
222
|
@imap.disconnect rescue nil
|
106
223
|
@imap = nil
|
@@ -108,58 +225,354 @@ module Mournmail
|
|
108
225
|
end
|
109
226
|
end
|
110
227
|
|
228
|
+
def self.google_access_token(account = current_account)
|
229
|
+
auth_path = File.expand_path("cache/#{account}/google_auth.json",
|
230
|
+
CONFIG[:mournmail_directory])
|
231
|
+
FileUtils.mkdir_p(File.dirname(auth_path))
|
232
|
+
store = Google::APIClient::FileStore.new(auth_path)
|
233
|
+
storage = Google::APIClient::Storage.new(store)
|
234
|
+
storage.authorize
|
235
|
+
if storage.authorization.nil?
|
236
|
+
conf = CONFIG[:mournmail_accounts][account]
|
237
|
+
path = File.expand_path(conf[:client_secret_path])
|
238
|
+
client_secrets = Google::APIClient::ClientSecrets.load(path)
|
239
|
+
auth_client = client_secrets.to_authorization
|
240
|
+
auth_client.update!(
|
241
|
+
:scope => 'https://mail.google.com/',
|
242
|
+
:redirect_uri => 'urn:ietf:wg:oauth:2.0:oob'
|
243
|
+
)
|
244
|
+
auth_uri = auth_client.authorization_uri.to_s
|
245
|
+
auth_client.code = foreground! {
|
246
|
+
begin
|
247
|
+
Launchy.open(auth_uri)
|
248
|
+
rescue Launchy::CommandNotFoundError
|
249
|
+
buffer = show_google_auth_uri(auth_uri)
|
250
|
+
end
|
251
|
+
begin
|
252
|
+
Window.echo_area.clear_message
|
253
|
+
Window.redisplay
|
254
|
+
read_from_minibuffer("Code: ").chomp
|
255
|
+
ensure
|
256
|
+
if buffer
|
257
|
+
kill_buffer(buffer, force: true)
|
258
|
+
end
|
259
|
+
end
|
260
|
+
}
|
261
|
+
auth_client.fetch_access_token!
|
262
|
+
old_umask = File.umask(077)
|
263
|
+
begin
|
264
|
+
storage.write_credentials(auth_client)
|
265
|
+
ensure
|
266
|
+
File.umask(old_umask)
|
267
|
+
end
|
268
|
+
else
|
269
|
+
auth_client = storage.authorization
|
270
|
+
end
|
271
|
+
auth_client.access_token
|
272
|
+
end
|
273
|
+
|
274
|
+
def self.show_google_auth_uri(auth_uri)
|
275
|
+
buffer = Buffer.find_or_new("*message*",
|
276
|
+
undo_limit: 0, read_only: true)
|
277
|
+
buffer.apply_mode(Mournmail::MessageMode)
|
278
|
+
buffer.read_only_edit do
|
279
|
+
buffer.clear
|
280
|
+
buffer.insert(<<~EOF)
|
281
|
+
Open the following URI in your browser and type obtained code:
|
282
|
+
|
283
|
+
#{auth_uri}
|
284
|
+
EOF
|
285
|
+
end
|
286
|
+
window = Mournmail.message_window
|
287
|
+
window.buffer = buffer
|
288
|
+
buffer
|
289
|
+
end
|
290
|
+
|
291
|
+
def self.xoauth2_string(user, access_token)
|
292
|
+
"user=#{user}\1auth=Bearer #{access_token}\1\1"
|
293
|
+
end
|
294
|
+
|
111
295
|
def self.fetch_summary(mailbox, all: false)
|
296
|
+
if all
|
297
|
+
summary = Mournmail::Summary.new(mailbox)
|
298
|
+
else
|
299
|
+
summary = Mournmail::Summary.load_or_new(mailbox)
|
300
|
+
end
|
112
301
|
imap_connect do |imap|
|
113
302
|
imap.select(mailbox)
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
303
|
+
uidvalidity = imap.responses["UIDVALIDITY"].last
|
304
|
+
if uidvalidity && summary.uidvalidity &&
|
305
|
+
uidvalidity != summary.uidvalidity
|
306
|
+
clear = foreground! {
|
307
|
+
yes_or_no?("UIDVALIDITY has been changed; Clear cache?")
|
308
|
+
}
|
309
|
+
if clear
|
310
|
+
summary = Mournmail::Summary.new(mailbox)
|
311
|
+
end
|
118
312
|
end
|
119
|
-
|
120
|
-
|
313
|
+
summary.uidvalidity = uidvalidity
|
314
|
+
uids = imap.uid_search("ALL")
|
315
|
+
new_uids = uids - summary.uids
|
316
|
+
return summary if new_uids.empty?
|
121
317
|
summary.synchronize do
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
318
|
+
new_uids.each_slice(1000) do |uid_chunk|
|
319
|
+
data = imap.uid_fetch(uid_chunk, ["UID", "ENVELOPE", "FLAGS"])
|
320
|
+
data&.each do |i|
|
321
|
+
uid = i.attr["UID"]
|
322
|
+
next if summary[uid]
|
323
|
+
env = i.attr["ENVELOPE"]
|
324
|
+
flags = i.attr["FLAGS"]
|
325
|
+
item = Mournmail::SummaryItem.new(uid, env.date, env.from,
|
326
|
+
env.subject, flags)
|
327
|
+
summary.add_item(item, env.message_id, env.in_reply_to)
|
328
|
+
end
|
130
329
|
end
|
131
330
|
end
|
132
331
|
summary
|
133
332
|
end
|
333
|
+
rescue SocketError, Timeout::Error => e
|
334
|
+
foreground do
|
335
|
+
message(e.message)
|
336
|
+
end
|
337
|
+
summary
|
338
|
+
end
|
339
|
+
|
340
|
+
def self.show_summary(summary)
|
341
|
+
buffer = Buffer.find_or_new("*summary*", undo_limit: 0,
|
342
|
+
read_only: true)
|
343
|
+
buffer.apply_mode(Mournmail::SummaryMode)
|
344
|
+
buffer.read_only_edit do
|
345
|
+
buffer.clear
|
346
|
+
buffer.insert(summary.to_s)
|
347
|
+
end
|
348
|
+
switch_to_buffer(buffer)
|
349
|
+
Mournmail.current_mailbox = summary.mailbox
|
350
|
+
Mournmail.current_summary = summary
|
351
|
+
Mournmail.current_mail = nil
|
352
|
+
Mournmail.current_uid = nil
|
353
|
+
begin
|
354
|
+
buffer.beginning_of_buffer
|
355
|
+
buffer.re_search_forward(/^ *\d+ u/)
|
356
|
+
rescue SearchError
|
357
|
+
buffer.end_of_buffer
|
358
|
+
buffer.re_search_backward(/^ *\d+ /, raise_error: false)
|
359
|
+
end
|
360
|
+
summary_read_command
|
134
361
|
end
|
135
362
|
|
136
363
|
def self.mailbox_cache_path(mailbox)
|
137
|
-
|
138
|
-
|
139
|
-
|
364
|
+
File.expand_path("cache/#{current_account}/mailboxes/#{mailbox}",
|
365
|
+
CONFIG[:mournmail_directory])
|
366
|
+
end
|
367
|
+
|
368
|
+
def self.mail_cache_path(cache_id)
|
369
|
+
dir = cache_id[0, 2]
|
370
|
+
File.expand_path("cache/#{current_account}/mails/#{dir}/#{cache_id}",
|
371
|
+
CONFIG[:mournmail_directory])
|
140
372
|
end
|
141
373
|
|
142
|
-
def self.
|
143
|
-
path =
|
374
|
+
def self.read_mail_cache(cache_id)
|
375
|
+
path = Mournmail.mail_cache_path(cache_id)
|
376
|
+
File.read(path)
|
377
|
+
end
|
378
|
+
|
379
|
+
def self.write_mail_cache(s)
|
380
|
+
header = s.slice(/.*\r\n\r\n/m)
|
381
|
+
cache_id = Digest::SHA256.hexdigest(header)
|
382
|
+
path = mail_cache_path(cache_id)
|
383
|
+
dir = File.dirname(path)
|
384
|
+
base = File.basename(path)
|
144
385
|
begin
|
145
|
-
|
146
|
-
|
147
|
-
|
386
|
+
f = Tempfile.create(["#{base}-", ".tmp"], dir,
|
387
|
+
external_encoding: "ASCII-8BIT", binmode: true)
|
388
|
+
begin
|
389
|
+
f.write(s)
|
390
|
+
ensure
|
391
|
+
f.close
|
148
392
|
end
|
149
393
|
rescue Errno::ENOENT
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
394
|
+
FileUtils.mkdir_p(File.dirname(path))
|
395
|
+
retry
|
396
|
+
end
|
397
|
+
File.rename(f.path, path)
|
398
|
+
cache_id
|
399
|
+
end
|
400
|
+
|
401
|
+
def self.index_mail(cache_id, mail)
|
402
|
+
messages_db = Groonga["Messages"]
|
403
|
+
unless messages_db.has_key?(cache_id)
|
404
|
+
thread_id = find_thread_id(mail, messages_db)
|
405
|
+
list_id = (mail["List-Id"] || mail["X-ML-Name"])
|
406
|
+
messages_db.add(cache_id,
|
407
|
+
message_id: header_text(mail.message_id),
|
408
|
+
thread_id: header_text(thread_id),
|
409
|
+
date: mail.date&.to_time,
|
410
|
+
subject: header_text(mail.subject),
|
411
|
+
from: header_text(mail["From"]),
|
412
|
+
to: header_text(mail["To"]),
|
413
|
+
cc: header_text(mail["Cc"]),
|
414
|
+
list_id: header_text(list_id),
|
415
|
+
body: body_text(mail))
|
416
|
+
end
|
417
|
+
end
|
418
|
+
|
419
|
+
class << self
|
420
|
+
private
|
421
|
+
|
422
|
+
def find_thread_id(mail, messages_db)
|
423
|
+
references = Array(mail.references) | Array(mail.in_reply_to)
|
424
|
+
if references.empty?
|
425
|
+
mail.message_id
|
426
|
+
elsif /\Aredmine\.issue-/.match?(references.first)
|
427
|
+
references.first
|
428
|
+
else
|
429
|
+
parent = messages_db.select { |m|
|
430
|
+
references.inject(nil) { |cond, ref|
|
431
|
+
if cond.nil?
|
432
|
+
m.message_id == ref
|
433
|
+
else
|
434
|
+
cond | (m.message_id == ref)
|
435
|
+
end
|
436
|
+
}
|
437
|
+
}.first
|
438
|
+
if parent
|
439
|
+
parent.thread_id
|
440
|
+
else
|
441
|
+
mail.message_id
|
155
442
|
end
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
443
|
+
end
|
444
|
+
end
|
445
|
+
|
446
|
+
def header_text(s)
|
447
|
+
force_utf8(s.to_s)
|
448
|
+
end
|
449
|
+
|
450
|
+
def body_text(mail)
|
451
|
+
if mail.multipart?
|
452
|
+
mail.parts.map { |part|
|
453
|
+
part_text(part)
|
454
|
+
}.join("\n")
|
455
|
+
else
|
456
|
+
s = mail.body.decoded
|
457
|
+
to_utf8(s, mail.charset).gsub(/\r\n/, "\n")
|
458
|
+
end
|
459
|
+
rescue
|
460
|
+
""
|
461
|
+
end
|
462
|
+
|
463
|
+
def part_text(part)
|
464
|
+
if part.multipart?
|
465
|
+
part.parts.map { |part|
|
466
|
+
part_text(part)
|
467
|
+
}.join("\n")
|
468
|
+
elsif part.main_type == "message" && part.sub_type == "rfc822"
|
469
|
+
mail = Mail.new(part.body.raw_source)
|
470
|
+
body_text(mail)
|
471
|
+
elsif part.attachment?
|
472
|
+
force_utf8(part.filename.to_s)
|
473
|
+
else
|
474
|
+
if part.main_type == "text" && part.sub_type == "plain"
|
475
|
+
force_utf8(part.decoded).sub(/(?<!\n)\z/, "\n").gsub(/\r\n/, "\n")
|
476
|
+
else
|
477
|
+
""
|
161
478
|
end
|
162
|
-
|
479
|
+
end
|
480
|
+
rescue
|
481
|
+
""
|
482
|
+
end
|
483
|
+
end
|
484
|
+
|
485
|
+
def self.read_mailbox_name(prompt, **opts)
|
486
|
+
f = ->(s) {
|
487
|
+
complete_for_minibuffer(s, @mailboxes)
|
488
|
+
}
|
489
|
+
mailbox = read_from_minibuffer(prompt, completion_proc: f, **opts)
|
490
|
+
Net::IMAP.encode_utf7(mailbox)
|
491
|
+
end
|
492
|
+
|
493
|
+
def self.force_utf8(s)
|
494
|
+
s.dup.force_encoding(Encoding::UTF_8).scrub("?")
|
495
|
+
end
|
496
|
+
|
497
|
+
def self.to_utf8(s, charset)
|
498
|
+
if /\Autf-8\z/i.match?(charset)
|
499
|
+
force_utf8(s)
|
500
|
+
else
|
501
|
+
begin
|
502
|
+
s.encode(Encoding::UTF_8, charset, replace: "?")
|
503
|
+
rescue
|
504
|
+
force_utf8(NKF.nkf("-w", s))
|
505
|
+
end
|
506
|
+
end.gsub(/\r\n/, "\n")
|
507
|
+
end
|
508
|
+
|
509
|
+
def self.open_groonga_db
|
510
|
+
db_path = File.expand_path("groonga/#{current_account}/messages.db",
|
511
|
+
CONFIG[:mournmail_directory])
|
512
|
+
if File.exist?(db_path)
|
513
|
+
@groonga_db = Groonga::Database.open(db_path)
|
514
|
+
else
|
515
|
+
@groonga_db = create_groonga_db(db_path)
|
516
|
+
end
|
517
|
+
end
|
518
|
+
|
519
|
+
def self.create_groonga_db(db_path)
|
520
|
+
FileUtils.mkdir_p(File.dirname(db_path), mode: 0700)
|
521
|
+
db = Groonga::Database.create(path: db_path)
|
522
|
+
|
523
|
+
Groonga::Schema.create_table("Messages", :type => :hash) do |table|
|
524
|
+
table.short_text("message_id")
|
525
|
+
table.short_text("thread_id")
|
526
|
+
table.time("date")
|
527
|
+
table.short_text("subject")
|
528
|
+
table.short_text("from")
|
529
|
+
table.short_text("to")
|
530
|
+
table.short_text("cc")
|
531
|
+
table.short_text("list_id")
|
532
|
+
table.text("body")
|
533
|
+
end
|
534
|
+
|
535
|
+
Groonga::Schema.create_table("Terms",
|
536
|
+
type: :patricia_trie,
|
537
|
+
normalizer: :NormalizerAuto,
|
538
|
+
default_tokenizer: "TokenBigram") do |table|
|
539
|
+
table.index("Messages.subject")
|
540
|
+
table.index("Messages.from")
|
541
|
+
table.index("Messages.to")
|
542
|
+
table.index("Messages.cc")
|
543
|
+
table.index("Messages.list_id")
|
544
|
+
table.index("Messages.body")
|
545
|
+
end
|
546
|
+
|
547
|
+
db
|
548
|
+
end
|
549
|
+
|
550
|
+
def self.close_groonga_db
|
551
|
+
if @groonga_db
|
552
|
+
@groonga_db.close
|
553
|
+
end
|
554
|
+
end
|
555
|
+
|
556
|
+
def self.parse_mail(s)
|
557
|
+
Mail.new(s.scrub("??"))
|
558
|
+
end
|
559
|
+
|
560
|
+
def self.read_account_name(prompt, **opts)
|
561
|
+
f = ->(s) {
|
562
|
+
complete_for_minibuffer(s, CONFIG[:mournmail_accounts].keys)
|
563
|
+
}
|
564
|
+
read_from_minibuffer(prompt, completion_proc: f, **opts)
|
565
|
+
end
|
566
|
+
|
567
|
+
def self.insert_signature
|
568
|
+
account = Buffer.current[:mournmail_delivery_account] ||
|
569
|
+
Mournmail.current_account
|
570
|
+
signature = CONFIG[:mournmail_accounts][account][:signature]
|
571
|
+
if signature
|
572
|
+
Buffer.current.save_excursion do
|
573
|
+
end_of_buffer
|
574
|
+
insert("\n")
|
575
|
+
insert(signature)
|
163
576
|
end
|
164
577
|
end
|
165
578
|
end
|