mournmail 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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