mournmail 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ using Mournmail::MessageRendering
4
+
5
+ module Mournmail
6
+ class SearchResultMode < Mournmail::SummaryMode
7
+ SEARCH_RESULT_MODE_MAP = Keymap.new
8
+ SEARCH_RESULT_MODE_MAP.define_key(" ", :summary_read_command)
9
+ SEARCH_RESULT_MODE_MAP.define_key(:backspace, :summary_scroll_down_command)
10
+ SEARCH_RESULT_MODE_MAP.define_key("\C-h", :summary_scroll_down_command)
11
+ SEARCH_RESULT_MODE_MAP.define_key("\C-?", :summary_scroll_down_command)
12
+ SEARCH_RESULT_MODE_MAP.define_key("w", :summary_write_command)
13
+ SEARCH_RESULT_MODE_MAP.define_key("a", :summary_reply_command)
14
+ SEARCH_RESULT_MODE_MAP.define_key("A", :summary_reply_command)
15
+ SEARCH_RESULT_MODE_MAP.define_key("f", :summary_forward_command)
16
+ SEARCH_RESULT_MODE_MAP.define_key("v", :summary_view_source_command)
17
+ SEARCH_RESULT_MODE_MAP.define_key("q", :search_result_close_command)
18
+ SEARCH_RESULT_MODE_MAP.define_key("k", :previous_line)
19
+ SEARCH_RESULT_MODE_MAP.define_key("j", :next_line)
20
+ SEARCH_RESULT_MODE_MAP.define_key("<", :previous_page_command)
21
+ SEARCH_RESULT_MODE_MAP.define_key(">", :next_page_command)
22
+ SEARCH_RESULT_MODE_MAP.define_key("/", :summary_search_command)
23
+ SEARCH_RESULT_MODE_MAP.define_key("t", :summary_show_thread_command)
24
+
25
+ def initialize(buffer)
26
+ super(buffer)
27
+ buffer.keymap = SEARCH_RESULT_MODE_MAP
28
+ end
29
+
30
+ define_local_command(:summary_read, doc: "Read a mail.") do
31
+ num = scroll_up_or_current_number
32
+ return if num.nil?
33
+ Mournmail.background do
34
+ message = @buffer[:messages][num]
35
+ if message.nil?
36
+ raise EditorError, "No message found"
37
+ end
38
+ mail = Mail.new(Mournmail.read_mail_cache(message._key))
39
+ next_tick do
40
+ show_message(mail)
41
+ @buffer[:message_number] = num
42
+ end
43
+ end
44
+ end
45
+
46
+ define_local_command(:summary_scroll_down,
47
+ doc: "Scroll down the current message.") do
48
+ num = @buffer.current_line
49
+ if num == @buffer[:message_number]
50
+ window = Mournmail.message_window
51
+ if window.buffer.name == "*message*"
52
+ old_window = Window.current
53
+ begin
54
+ Window.current = window
55
+ scroll_down
56
+ return
57
+ ensure
58
+ Window.current = old_window
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ define_local_command(:search_result_close,
65
+ doc: "Close the search result.") do
66
+ if @buffer.name == "*thread*"
67
+ buf = Buffer["*search result*"] || "*summary*"
68
+ else
69
+ buf = "*summary*"
70
+ end
71
+ kill_buffer(@buffer)
72
+ switch_to_buffer(buf)
73
+ end
74
+
75
+ define_local_command(:previous_page,
76
+ doc: "Show the previous page.") do
77
+ messages = @buffer[:messages]
78
+ page = messages.current_page - 1
79
+ if page < 1
80
+ raise EditorError, "No more page."
81
+ end
82
+ summary_search(@buffer[:query], page)
83
+ end
84
+
85
+ define_local_command(:next_page,
86
+ doc: "Show the next page.") do
87
+ messages = @buffer[:messages]
88
+ page = messages.current_page + 1
89
+ if page > messages.n_pages
90
+ raise EditorError, "No more page."
91
+ end
92
+ summary_search(@buffer[:query], page)
93
+ end
94
+
95
+ private
96
+
97
+ def scroll_up_or_current_number
98
+ begin
99
+ num = @buffer.current_line
100
+ if num == @buffer[:message_number]
101
+ window = Mournmail.message_window
102
+ if window.buffer.name == "*message*"
103
+ old_window = Window.current
104
+ begin
105
+ Window.current = window
106
+ scroll_up
107
+ return nil
108
+ ensure
109
+ Window.current = old_window
110
+ end
111
+ end
112
+ end
113
+ num
114
+ rescue RangeError # may be raised by scroll_up
115
+ next_message
116
+ retry
117
+ end
118
+ end
119
+
120
+ def read_current_mail
121
+ message = @buffer[:messages][@buffer.current_line]
122
+ if message.nil?
123
+ raise EditorError, "No message found"
124
+ end
125
+ [Mail.new(Mournmail.read_mail_cache(message._key)), false]
126
+ end
127
+
128
+ def next_message
129
+ @buffer.end_of_line
130
+ if @buffer.end_of_buffer?
131
+ raise EditorError, "No more mail"
132
+ end
133
+ @buffer.forward_line
134
+ end
135
+
136
+ def current_message
137
+ message = @buffer[:messages][@buffer.current_line]
138
+ if message.nil?
139
+ raise EditorError, "No message found"
140
+ end
141
+ message
142
+ end
143
+ end
144
+ end
@@ -7,18 +7,39 @@ require "monitor"
7
7
  module Mournmail
8
8
  class Summary
9
9
  attr_reader :items, :last_uid
10
+ attr_accessor :uidvalidity
10
11
 
11
12
  include MonitorMixin
12
13
 
14
+ LOCK_OPERATIONS = Hash.new(:unknown_mode)
15
+ LOCK_OPERATIONS[:shared] = File::LOCK_SH
16
+ LOCK_OPERATIONS[:exclusive] = File::LOCK_EX
17
+
18
+ def self.lock_cache(mailbox, mode)
19
+ File.open(Summary.cache_lock_path(mailbox), "w", 0600) do |f|
20
+ f.flock(LOCK_OPERATIONS[mode])
21
+ yield
22
+ end
23
+ end
24
+
13
25
  def self.cache_path(mailbox)
14
26
  File.join(Mournmail.mailbox_cache_path(mailbox), ".summary")
15
27
  end
16
28
 
29
+ def self.cache_lock_path(mailbox)
30
+ cache_path(mailbox) + ".lock"
31
+ end
32
+
33
+ def self.cache_tmp_path(mailbox)
34
+ cache_path(mailbox) + ".tmp"
35
+ end
36
+
17
37
  def self.load(mailbox)
18
- File.open(cache_path(mailbox)) { |f|
19
- f.flock(File::LOCK_SH)
20
- Marshal.load(f)
21
- }
38
+ lock_cache(mailbox, :shared) do
39
+ File.open(cache_path(mailbox)) do |f|
40
+ Marshal.load(f)
41
+ end
42
+ end
22
43
  end
23
44
 
24
45
  def self.load_or_new(mailbox)
@@ -34,6 +55,7 @@ module Mournmail
34
55
  @message_id_table = {}
35
56
  @uid_table = {}
36
57
  @last_uid = nil
58
+ @uidvalidity = nil
37
59
  end
38
60
 
39
61
  DUMPABLE_VARIABLES = [
@@ -41,7 +63,8 @@ module Mournmail
41
63
  :@items,
42
64
  :@message_id_table,
43
65
  :@uid_table,
44
- :@last_uid
66
+ :@last_uid,
67
+ :@uidvalidity
45
68
  ]
46
69
 
47
70
  def marshal_dump
@@ -92,13 +115,43 @@ module Mournmail
92
115
  end
93
116
  end
94
117
 
118
+ def read_mail(uid)
119
+ synchronize do
120
+ item = @uid_table[uid]
121
+ if item.cache_id
122
+ File.open(Mournmail.mail_cache_path(item.cache_id)) do |f|
123
+ [Mail.new(f.read), false]
124
+ end
125
+ else
126
+ Mournmail.imap_connect do |imap|
127
+ imap.select(@mailbox)
128
+ data = imap.uid_fetch(uid, "BODY[]")
129
+ if data.empty?
130
+ raise EditorError, "No such mail: #{uid}"
131
+ end
132
+ s = data[0].attr["BODY[]"]
133
+ mail = Mail.new(s)
134
+ if @mailbox != Mournmail.account_config[:spam_mailbox]
135
+ item.cache_id = Mournmail.write_mail_cache(s)
136
+ Mournmail.index_mail(item.cache_id, mail)
137
+ end
138
+ [mail, true]
139
+ end
140
+ end
141
+ end
142
+ end
143
+
95
144
  def save
96
145
  synchronize do
97
146
  path = Summary.cache_path(@mailbox)
98
147
  FileUtils.mkdir_p(File.dirname(path))
99
- File.open(Summary.cache_path(@mailbox), "w", 0600) do |f|
100
- f.flock(File::LOCK_EX)
101
- Marshal.dump(self, f)
148
+ Summary.lock_cache(@mailbox, :exclusive) do
149
+ cache_path = Summary.cache_path(@mailbox)
150
+ tmp_path = Summary.cache_tmp_path(@mailbox)
151
+ File.open(tmp_path, "w", 0600) do |f|
152
+ Marshal.dump(self, f)
153
+ end
154
+ File.rename(tmp_path, cache_path)
102
155
  end
103
156
  end
104
157
  end
@@ -115,6 +168,7 @@ module Mournmail
115
168
  class SummaryItem
116
169
  attr_reader :uid, :date, :from, :subject, :flags
117
170
  attr_reader :replies
171
+ attr_accessor :cache_id
118
172
 
119
173
  def initialize(uid, date, from, subject, flags)
120
174
  @uid = uid
@@ -124,6 +178,7 @@ module Mournmail
124
178
  @flags = flags
125
179
  @line = nil
126
180
  @replies = []
181
+ @cache_id = nil
127
182
  end
128
183
 
129
184
  def add_reply(reply)
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "tempfile"
4
-
5
3
  using Mournmail::MessageRendering
6
4
 
7
5
  module Mournmail
@@ -20,18 +18,32 @@ module Mournmail
20
18
  SUMMARY_MODE_MAP.define_key("u", :summary_toggle_seen_command)
21
19
  SUMMARY_MODE_MAP.define_key("$", :summary_toggle_flagged_command)
22
20
  SUMMARY_MODE_MAP.define_key("d", :summary_toggle_deleted_command)
23
- SUMMARY_MODE_MAP.define_key("x", :summary_expunge_command)
21
+ SUMMARY_MODE_MAP.define_key("x", :summary_toggle_mark_command)
22
+ SUMMARY_MODE_MAP.define_key("*a", :summary_mark_all_command)
23
+ SUMMARY_MODE_MAP.define_key("*n", :summary_unmark_all_command)
24
+ SUMMARY_MODE_MAP.define_key("*r", :summary_mark_read_command)
25
+ SUMMARY_MODE_MAP.define_key("*u", :summary_mark_unread_command)
26
+ SUMMARY_MODE_MAP.define_key("*s", :summary_mark_flagged_command)
27
+ SUMMARY_MODE_MAP.define_key("*t", :summary_mark_unflagged_command)
28
+ SUMMARY_MODE_MAP.define_key("y", :summary_archive_command)
29
+ SUMMARY_MODE_MAP.define_key("o", :summary_refile_command)
30
+ SUMMARY_MODE_MAP.define_key("p", :summary_prefetch_command)
31
+ SUMMARY_MODE_MAP.define_key("X", :summary_expunge_command)
24
32
  SUMMARY_MODE_MAP.define_key("v", :summary_view_source_command)
33
+ SUMMARY_MODE_MAP.define_key("M", :summary_merge_partial_command)
25
34
  SUMMARY_MODE_MAP.define_key("q", :mournmail_quit)
26
35
  SUMMARY_MODE_MAP.define_key("k", :previous_line)
27
36
  SUMMARY_MODE_MAP.define_key("j", :next_line)
28
37
  SUMMARY_MODE_MAP.define_key("m", :mournmail_visit_mailbox)
38
+ SUMMARY_MODE_MAP.define_key("/", :summary_search_command)
39
+ SUMMARY_MODE_MAP.define_key("t", :summary_show_thread_command)
40
+ SUMMARY_MODE_MAP.define_key("@", :summary_change_account_command)
29
41
 
30
- define_syntax :seen, /^ *\d+ .*/
31
- define_syntax :unseen, /^ *\d+ u.*/
32
- define_syntax :flagged, /^ *\d+ \$.*/
33
- define_syntax :deleted, /^ *\d+ d.*/
34
- define_syntax :answered, /^ *\d+ a.*/
42
+ define_syntax :seen, /^ *\d+[ *] .*/
43
+ define_syntax :unseen, /^ *\d+[ *]u.*/
44
+ define_syntax :flagged, /^ *\d+[ *]\$.*/
45
+ define_syntax :deleted, /^ *\d+[ *]d.*/
46
+ define_syntax :answered, /^ *\d+[ *]a.*/
35
47
 
36
48
  def initialize(buffer)
37
49
  super(buffer)
@@ -41,22 +53,11 @@ module Mournmail
41
53
  define_local_command(:summary_read, doc: "Read a mail.") do
42
54
  uid = scroll_up_or_next_uid
43
55
  return if uid.nil?
56
+ summary = Mournmail.current_summary
44
57
  Mournmail.background do
45
- mailbox = Mournmail.current_mailbox
46
- s, fetched = Mournmail.read_mail(mailbox, uid)
47
- mail = Mail.new(s)
48
- message = mail.render
58
+ mail, fetched = summary.read_mail(uid)
49
59
  next_tick do
50
- message_buffer = Buffer.find_or_new("*message*",
51
- undo_limit: 0, read_only: true)
52
- message_buffer.apply_mode(Mournmail::MessageMode)
53
- message_buffer.read_only_edit do
54
- message_buffer.clear
55
- message_buffer.insert(message)
56
- message_buffer.beginning_of_buffer
57
- end
58
- window = Mournmail.message_window
59
- window.buffer = message_buffer
60
+ show_message(mail)
60
61
  mark_as_seen(uid, !fetched)
61
62
  Mournmail.current_uid = uid
62
63
  Mournmail.current_mail = mail
@@ -97,10 +98,8 @@ module Mournmail
97
98
  define_local_command(:summary_reply,
98
99
  doc: "Reply to the current message.") do
99
100
  |reply_all = current_prefix_arg|
100
- uid = selected_uid
101
101
  Mournmail.background do
102
- mailbox = Mournmail.current_mailbox
103
- mail = Mail.new(Mournmail.read_mail(mailbox, uid)[0])
102
+ mail = read_current_mail[0]
104
103
  body = mail.render_body
105
104
  next_tick do
106
105
  Window.current = Mournmail.message_window
@@ -117,13 +116,20 @@ module Mournmail
117
116
  insert(mail.reply_to&.join(", ") || mail.from&.join(", "))
118
117
  end
119
118
  re_search_forward(/^Subject: /)
120
- subject = mail["subject"].to_s
119
+ subject = mail["subject"].to_s.gsub(/\t/, " ")
121
120
  if /\Are:/i !~ subject
122
121
  insert("Re: ")
123
122
  end
124
123
  insert(subject)
125
- if mail['message-id']
126
- insert("\nIn-Reply-To: #{mail['message-id']}")
124
+ references = mail.references ?
125
+ Array(mail.references) : Array(mail.in_reply_to)
126
+ if mail.message_id
127
+ insert("\nIn-Reply-To: <#{mail.message_id}>")
128
+ references.push(mail.message_id)
129
+ end
130
+ if !references.empty?
131
+ refs = references.map { |id| "<#{id}>" }.join(" ")
132
+ insert("\nReferences: " + refs)
127
133
  end
128
134
  end_of_buffer
129
135
  push_mark
@@ -141,14 +147,12 @@ module Mournmail
141
147
 
142
148
  define_local_command(:summary_forward,
143
149
  doc: "Forward the current message.") do
144
- uid = selected_uid
145
- summary = Mournmail.current_summary
146
- item = summary[uid]
150
+ message = current_message
147
151
  Window.current = Mournmail.message_window
148
152
  Commands.mail
149
153
  re_search_forward(/^Subject: /)
150
- insert("Forward: " + Mournmail.decode_eword(item.subject))
151
- insert("\nAttached-Message: #{Mournmail.current_mailbox}/#{uid}")
154
+ insert("Forward: " + message.subject)
155
+ insert("\nAttached-Message: #{message._key}")
152
156
  re_search_backward(/^To: /)
153
157
  end_of_line
154
158
  end
@@ -170,18 +174,72 @@ module Mournmail
170
174
  toggle_flag(selected_uid, :Deleted)
171
175
  end
172
176
 
177
+ define_local_command(:summary_toggle_mark, doc: "Toggle mark.") do
178
+ @buffer.read_only_edit do
179
+ @buffer.save_excursion do
180
+ @buffer.beginning_of_line
181
+ if @buffer.looking_at?(/( *\d+)([ *])/)
182
+ uid = @buffer.match_string(1)
183
+ old_mark = @buffer.match_string(2)
184
+ new_mark = old_mark == "*" ? " " : "*"
185
+ @buffer.replace_match(uid + new_mark)
186
+ end
187
+ end
188
+ end
189
+ end
190
+
191
+ define_local_command(:summary_mark_all, doc: "Mark all mails.") do
192
+ gsub_buffer(/^( *\d+) /, '\\1*')
193
+ end
194
+
195
+ define_local_command(:summary_unmark_all, doc: "Unmark all mails.") do
196
+ gsub_buffer(/^( *\d+)\*/, '\\1 ')
197
+ end
198
+
199
+ define_local_command(:summary_mark_read, doc: "Mark read mails.") do
200
+ gsub_buffer(/^( *\d+) ([^u])/, '\\1*\\2')
201
+ end
202
+
203
+ define_local_command(:summary_mark_unread, doc: "Mark unread mails.") do
204
+ gsub_buffer(/^( *\d+) u/, '\\1*u')
205
+ end
206
+
207
+ define_local_command(:summary_mark_flagged, doc: "Mark flagged mails.") do
208
+ gsub_buffer(/^( *\d+) \$/, '\\1*$')
209
+ end
210
+
211
+ define_local_command(:summary_mark_unflagged,
212
+ doc: "Mark unflagged mails.") do
213
+ gsub_buffer(/^( *\d+) ([^$])/, '\\1*\\2')
214
+ end
215
+
173
216
  define_local_command(:summary_expunge,
174
217
  doc: <<~EOD) do
175
218
  Expunge deleted messages.
176
219
  EOD
177
220
  buffer = Buffer.current
221
+ mailbox = Mournmail.current_mailbox
222
+ summary = Mournmail.current_summary
178
223
  Mournmail.background do
179
224
  Mournmail.imap_connect do |imap|
180
225
  imap.expunge
181
226
  end
182
- summary = Mournmail.current_summary
183
227
  summary.delete_item_if do |item|
184
- item.flags.include?(:Deleted)
228
+ if item.flags.include?(:Deleted)
229
+ if item.cache_id
230
+ begin
231
+ File.unlink(Mournmail.mail_cache_path(item.cache_id))
232
+ rescue Errno::ENOENT
233
+ end
234
+ begin
235
+ Groonga["Messages"].delete(item.cache_id)
236
+ rescue Groonga::InvalidArgument
237
+ end
238
+ true
239
+ end
240
+ else
241
+ false
242
+ end
185
243
  end
186
244
  summary_text = summary.to_s
187
245
  summary.save
@@ -199,15 +257,14 @@ module Mournmail
199
257
  doc: "View source of a mail.") do
200
258
  uid = selected_uid
201
259
  Mournmail.background do
202
- mailbox = Mournmail.current_mailbox
203
- source, = Mournmail.read_mail(mailbox, uid)
260
+ mail, = read_current_mail
204
261
  next_tick do
205
262
  source_buffer = Buffer.find_or_new("*message-source*",
206
263
  file_encoding: "ascii-8bit",
207
264
  undo_limit: 0, read_only: true)
208
265
  source_buffer.read_only_edit do
209
266
  source_buffer.clear
210
- source_buffer.insert(source.gsub(/\r\n/, "\n"))
267
+ source_buffer.insert(mail.raw_source.gsub(/\r\n/, "\n"))
211
268
  source_buffer.file_format = :dos
212
269
  source_buffer.beginning_of_buffer
213
270
  end
@@ -217,6 +274,190 @@ module Mournmail
217
274
  end
218
275
  end
219
276
 
277
+ define_local_command(:summary_merge_partial,
278
+ doc: "Merge marked message/partial.") do
279
+ uids = []
280
+ @buffer.save_excursion do
281
+ @buffer.beginning_of_buffer
282
+ while @buffer.re_search_forward(/^( *\d+)\*/, raise_error: false)
283
+ uid = @buffer.match_string(0).to_i
284
+ # @buffer.replace_match(@buffer.match_string(0) + " ")
285
+ uids.push(uid)
286
+ end
287
+ end
288
+ summary = Mournmail.current_summary
289
+ Mournmail.background do
290
+ id = nil
291
+ total = nil
292
+ mails = uids.map { |uid|
293
+ summary.read_mail(uid)[0]
294
+ }.select { |mail|
295
+ mail.main_type == "message" &&
296
+ mail.sub_type == "partial" #&&
297
+ (id ||= mail["Content-Type"].parameters["id"]) ==
298
+ mail["Content-Type"].parameters["id"] &&
299
+ (total ||= mail["Content-Type"].parameters["total"]&.to_i)
300
+ }.sort_by { |mail|
301
+ mail["Content-Type"].parameters["number"].to_i
302
+ }
303
+ if mails.length != total
304
+ raise EditorError, "No enough messages (#{mails.length} of #{total})"
305
+ end
306
+ s = mails.map { |mail| mail.body.decoded }.join
307
+ mail = Mail.new(s)
308
+ next_tick do
309
+ show_message(mail)
310
+ Mournmail.current_uid = nil
311
+ Mournmail.current_mail = mail
312
+ end
313
+ end
314
+ end
315
+
316
+ define_local_command(:summary_archive,
317
+ doc: "Archive marked mails.") do
318
+ archive_mailbox_format =
319
+ Mournmail.account_config[:archive_mailbox_format]
320
+ if archive_mailbox_format.nil?
321
+ raise EditorError, "No archive_mailbox_format in the current account"
322
+ end
323
+ uids = marked_uids
324
+ summary = Mournmail.current_summary
325
+ now = Time.now
326
+ if archive_mailbox_format == false
327
+ mailboxes = { nil => uids }
328
+ else
329
+ mailboxes = uids.map { |uid| summary[uid] }.group_by { |item|
330
+ t = Time.parse(item.date) rescue now
331
+ t.strftime(archive_mailbox_format)
332
+ }.transform_values { |items|
333
+ items.map(&:uid)
334
+ }
335
+ end
336
+ source_mailbox = Mournmail.current_mailbox
337
+ if mailboxes.key?(source_mailbox)
338
+ raise EditorError, "Can't archive mails in archive mailboxes"
339
+ end
340
+ Mournmail.background do
341
+ Mournmail.imap_connect do |imap|
342
+ mailboxes.each do |mailbox, item_uids|
343
+ if mailbox && !imap.list("", mailbox)
344
+ imap.create(mailbox)
345
+ end
346
+ refile_mails(imap, source_mailbox, item_uids, mailbox)
347
+ end
348
+ imap.expunge
349
+ delete_from_summary(summary, uids, "Archived messages")
350
+ end
351
+ end
352
+ end
353
+
354
+ define_local_command(:summary_refile,
355
+ doc: "Refile marked mails.") do
356
+ |mailbox = Mournmail.read_mailbox_name("Refile mails: ")|
357
+ uids = marked_uids
358
+ summary = Mournmail.current_summary
359
+ source_mailbox = Mournmail.current_mailbox
360
+ if source_mailbox == mailbox
361
+ raise EditorError, "Can't refile to the same mailbox"
362
+ end
363
+ Mournmail.background do
364
+ Mournmail.imap_connect do |imap|
365
+ unless imap.list("", mailbox)
366
+ if next_tick! { yes_or_no?("#{mailbox} doesn't exist; Create?") }
367
+ imap.create(mailbox)
368
+ else
369
+ next
370
+ end
371
+ end
372
+ refile_mails(imap, source_mailbox, uids, mailbox)
373
+ delete_from_summary(summary, uids, "Refiled messages")
374
+ end
375
+ end
376
+ end
377
+
378
+ define_local_command(:summary_prefetch,
379
+ doc: "Prefetch mails.") do
380
+ summary = Mournmail.current_summary
381
+ mailbox = Mournmail.current_mailbox
382
+ target_uids = @buffer.to_s.scan(/^ *\d+/).map { |s|
383
+ s.to_i
384
+ }.select { |uid|
385
+ summary[uid].cache_id.nil?
386
+ }
387
+ Mournmail.background do
388
+ Mournmail.imap_connect do |imap|
389
+ imap.select(mailbox)
390
+ count = 0
391
+ begin
392
+ target_uids.each_slice(20) do |uids|
393
+ data = imap.uid_fetch(uids, "BODY[]")
394
+ data.each do |i|
395
+ uid = i.attr["UID"]
396
+ s = i.attr["BODY[]"]
397
+ if s
398
+ cache_id = Mournmail.write_mail_cache(s)
399
+ if mailbox != Mournmail.account_config[:spam_mailbox]
400
+ Mournmail.index_mail(cache_id, Mail.new(s))
401
+ end
402
+ summary[uid].cache_id = cache_id
403
+ end
404
+ end
405
+ count += uids.size
406
+ progress = (count.to_f * 100 / target_uids.size).round
407
+ next_tick do
408
+ message("Prefetching mails... #{progress}%", log: false)
409
+ end
410
+ end
411
+ ensure
412
+ summary.save
413
+ end
414
+ end
415
+ next_tick do
416
+ message("Done")
417
+ end
418
+ end
419
+ end
420
+
421
+ define_local_command(:summary_search, doc: "Search mails.") do
422
+ |query = read_from_minibuffer("Search mail: ",
423
+ initial_value: @buffer[:query]),
424
+ page = 1|
425
+ Mournmail.background do
426
+ messages = Groonga["Messages"].select { |record|
427
+ record.match(query) { |match_record|
428
+ match_record.subject | match_record.body
429
+ }
430
+ }.paginate([["date", :desc]], page: page, size: 100)
431
+ next_tick do
432
+ show_search_result(messages, query: query)
433
+ message("Searched (#{messages.current_page}/#{messages.n_pages})")
434
+ end
435
+ end
436
+ end
437
+
438
+ define_local_command(:summary_show_thread,
439
+ doc: "Show the thread of the current mail.") do
440
+ Mournmail.background do
441
+ message = current_message
442
+ messages = Groonga["Messages"].select { |m|
443
+ m.thread_id == message.thread_id
444
+ }.sort([["date", :asc]])
445
+ next_tick do
446
+ show_search_result(messages, buffer_name: "*thread*")
447
+ i = messages.find_index { |m| m._key == message._key }
448
+ Buffer.current.goto_line(i + 1)
449
+ end
450
+ end
451
+ end
452
+
453
+ define_local_command(:summary_change_account,
454
+ doc: "Change the current account.") do
455
+ |account = read_account_name("Change account: ")|
456
+ mournmail_quit
457
+ Mournmail.current_account = account
458
+ mournmail
459
+ end
460
+
220
461
  private
221
462
 
222
463
  def selected_uid
@@ -231,6 +472,16 @@ module Mournmail
231
472
  }
232
473
  end
233
474
 
475
+ def marked_uids
476
+ @buffer.to_s.scan(/^ *\d+(?=\*)/).map(&:to_i)
477
+ end
478
+
479
+ def read_current_mail
480
+ summary = Mournmail.current_summary
481
+ uid = selected_uid
482
+ summary.read_mail(uid)
483
+ end
484
+
234
485
  def scroll_up_or_next_uid
235
486
  begin
236
487
  uid = selected_uid
@@ -254,6 +505,20 @@ module Mournmail
254
505
  end
255
506
  end
256
507
 
508
+ def show_message(mail)
509
+ message_buffer = Buffer.find_or_new("*message*",
510
+ undo_limit: 0, read_only: true)
511
+ message_buffer.apply_mode(Mournmail::MessageMode)
512
+ message_buffer.read_only_edit do
513
+ message_buffer.clear
514
+ message_buffer.insert(mail.render)
515
+ message_buffer.beginning_of_buffer
516
+ end
517
+ message_buffer[:mournmail_mail] = mail
518
+ window = Mournmail.message_window
519
+ window.buffer = message_buffer
520
+ end
521
+
257
522
  def mark_as_seen(uid, update_server)
258
523
  summary_item = Mournmail.current_summary[uid]
259
524
  if summary_item && !summary_item.flags.include?(:Seen)
@@ -300,5 +565,108 @@ module Mournmail
300
565
  @buffer.forward_line
301
566
  end
302
567
  end
568
+
569
+ def gsub_buffer(re, s)
570
+ @buffer.read_only_edit do
571
+ s = @buffer.to_s.gsub(re, s)
572
+ @buffer.replace(s)
573
+ end
574
+ end
575
+
576
+ def show_search_result(messages,
577
+ query: nil, buffer_name: "*search result*")
578
+ summary_text = messages.map { |m|
579
+ format("%s [ %s ] %s\n",
580
+ m.date.strftime("%m/%d %H:%M"),
581
+ ljust(m.from.to_s, 16),
582
+ ljust(m.subject.to_s, 45))
583
+ }.join
584
+ buffer = Buffer.find_or_new(buffer_name, undo_limit: 0,
585
+ read_only: true)
586
+ buffer.apply_mode(Mournmail::SearchResultMode)
587
+ buffer.read_only_edit do
588
+ buffer.clear
589
+ buffer.insert(summary_text)
590
+ buffer.beginning_of_buffer
591
+ end
592
+ buffer[:messages] = messages
593
+ buffer[:query] = query
594
+ switch_to_buffer(buffer)
595
+ end
596
+
597
+ def ljust(s, n)
598
+ width = 0
599
+ str = String.new
600
+ s.gsub(/\t/, " ").each_char do |c|
601
+ w = Buffer.display_width(c)
602
+ width += w
603
+ if width > n
604
+ width -= w
605
+ break
606
+ end
607
+ str.concat(c)
608
+ break if width == n
609
+ end
610
+ str + " " * (n - width)
611
+ end
612
+
613
+ def refile_mails(imap, src_mailbox, uids, dst_mailbox)
614
+ count = 0
615
+ uids.each_slice(100) do |uid_set|
616
+ if dst_mailbox
617
+ imap.uid_copy(uid_set, dst_mailbox)
618
+ end
619
+ imap.uid_store(uid_set, "+FLAGS", [:Deleted])
620
+ count += uid_set.size
621
+ progress = (count.to_f * 100 / uids.size).round
622
+ next_tick do
623
+ if dst_mailbox
624
+ message("Refiling mails to #{dst_mailbox}... #{progress}%",
625
+ log: false)
626
+ else
627
+ message("Deleting mails... #{progress}%", log: false)
628
+ end
629
+ end
630
+ end
631
+ next_tick do
632
+ if dst_mailbox
633
+ message("Refiled mails to #{dst_mailbox}")
634
+ else
635
+ message("Deleted mails")
636
+ end
637
+ end
638
+ end
639
+
640
+ def current_message
641
+ uid = selected_uid
642
+ item = Mournmail.current_summary[uid]
643
+ message = Groonga["Messages"][item.cache_id]
644
+ if message.nil?
645
+ raise EditorError, "No message found"
646
+ end
647
+ message
648
+ end
649
+
650
+ def read_account_name(prompt, **opts)
651
+ f = ->(s) {
652
+ complete_for_minibuffer(s, CONFIG[:mournmail_accounts].keys)
653
+ }
654
+ read_from_minibuffer(prompt, completion_proc: f, **opts)
655
+ end
656
+
657
+ def delete_from_summary(summary, uids, msg)
658
+ summary.delete_item_if do |item|
659
+ uids.include?(item.uid)
660
+ end
661
+ summary_text = summary.to_s
662
+ summary.save
663
+ next_tick do
664
+ @buffer.read_only_edit do
665
+ @buffer.clear
666
+ @buffer.insert(summary_text)
667
+ end
668
+ message(msg)
669
+ end
670
+ end
303
671
  end
304
672
  end