mournmail 0.1.1 → 1.0.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.
- 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/summary.rb
CHANGED
@@ -1,24 +1,47 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
require "time"
|
4
2
|
require "fileutils"
|
5
3
|
require "monitor"
|
6
4
|
|
7
5
|
module Mournmail
|
8
6
|
class Summary
|
9
|
-
attr_reader :items, :last_uid
|
7
|
+
attr_reader :mailbox, :items, :last_uid
|
8
|
+
attr_accessor :uidvalidity
|
10
9
|
|
11
10
|
include MonitorMixin
|
12
11
|
|
12
|
+
LOCK_OPERATIONS = Hash.new(:unknown_mode)
|
13
|
+
LOCK_OPERATIONS[:shared] = File::LOCK_SH
|
14
|
+
LOCK_OPERATIONS[:exclusive] = File::LOCK_EX
|
15
|
+
|
16
|
+
def self.lock_cache(mailbox, mode)
|
17
|
+
File.open(Summary.cache_lock_path(mailbox), "w", 0600) do |f|
|
18
|
+
f.flock(LOCK_OPERATIONS[mode])
|
19
|
+
yield
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
13
23
|
def self.cache_path(mailbox)
|
14
24
|
File.join(Mournmail.mailbox_cache_path(mailbox), ".summary")
|
15
25
|
end
|
16
26
|
|
27
|
+
def self.cache_lock_path(mailbox)
|
28
|
+
cache_path(mailbox) + ".lock"
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.cache_tmp_path(mailbox)
|
32
|
+
cache_path(mailbox) + ".tmp"
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.cache_old_path(mailbox)
|
36
|
+
cache_path(mailbox) + ".old"
|
37
|
+
end
|
38
|
+
|
17
39
|
def self.load(mailbox)
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
40
|
+
lock_cache(mailbox, :shared) do
|
41
|
+
File.open(cache_path(mailbox)) do |f|
|
42
|
+
Marshal.load(f)
|
43
|
+
end
|
44
|
+
end
|
22
45
|
end
|
23
46
|
|
24
47
|
def self.load_or_new(mailbox)
|
@@ -34,6 +57,7 @@ module Mournmail
|
|
34
57
|
@message_id_table = {}
|
35
58
|
@uid_table = {}
|
36
59
|
@last_uid = nil
|
60
|
+
@uidvalidity = nil
|
37
61
|
end
|
38
62
|
|
39
63
|
DUMPABLE_VARIABLES = [
|
@@ -41,7 +65,8 @@ module Mournmail
|
|
41
65
|
:@items,
|
42
66
|
:@message_id_table,
|
43
67
|
:@uid_table,
|
44
|
-
:@last_uid
|
68
|
+
:@last_uid,
|
69
|
+
:@uidvalidity
|
45
70
|
]
|
46
71
|
|
47
72
|
def marshal_dump
|
@@ -59,12 +84,14 @@ module Mournmail
|
|
59
84
|
|
60
85
|
def add_item(item, message_id, in_reply_to)
|
61
86
|
synchronize do
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
87
|
+
# Disable threads
|
88
|
+
# parent = @message_id_table[in_reply_to]
|
89
|
+
# if parent
|
90
|
+
# parent.add_reply(item)
|
91
|
+
# else
|
92
|
+
# @items.push(item)
|
93
|
+
# end
|
94
|
+
@items.push(item)
|
68
95
|
if message_id
|
69
96
|
@message_id_table[message_id] = item
|
70
97
|
end
|
@@ -92,20 +119,62 @@ module Mournmail
|
|
92
119
|
end
|
93
120
|
end
|
94
121
|
|
122
|
+
def uids
|
123
|
+
synchronize do
|
124
|
+
@uid_table.keys
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def read_mail(uid)
|
129
|
+
synchronize do
|
130
|
+
item = @uid_table[uid]
|
131
|
+
if item.cache_id
|
132
|
+
File.open(Mournmail.mail_cache_path(item.cache_id)) do |f|
|
133
|
+
[Mournmail.parse_mail(f.read), false]
|
134
|
+
end
|
135
|
+
else
|
136
|
+
Mournmail.imap_connect do |imap|
|
137
|
+
imap.select(@mailbox)
|
138
|
+
data = imap.uid_fetch(uid, "BODY[]")
|
139
|
+
if data.nil? || data.empty?
|
140
|
+
raise EditorError, "No such mail: #{uid}"
|
141
|
+
end
|
142
|
+
s = data[0].attr["BODY[]"]
|
143
|
+
mail = Mournmail.parse_mail(s)
|
144
|
+
spam_mailbox = Mournmail.account_config[:spam_mailbox]
|
145
|
+
if @mailbox != Net::IMAP.encode_utf7(spam_mailbox)
|
146
|
+
item.cache_id = Mournmail.write_mail_cache(s)
|
147
|
+
Mournmail.index_mail(item.cache_id, mail)
|
148
|
+
end
|
149
|
+
[mail, true]
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
95
155
|
def save
|
96
156
|
synchronize do
|
97
157
|
path = Summary.cache_path(@mailbox)
|
98
158
|
FileUtils.mkdir_p(File.dirname(path))
|
99
|
-
|
100
|
-
|
101
|
-
|
159
|
+
Summary.lock_cache(@mailbox, :exclusive) do
|
160
|
+
cache_path = Summary.cache_path(@mailbox)
|
161
|
+
tmp_path = Summary.cache_tmp_path(@mailbox)
|
162
|
+
old_path = Summary.cache_old_path(@mailbox)
|
163
|
+
File.open(tmp_path, "w", 0600) do |f|
|
164
|
+
Marshal.dump(self, f)
|
165
|
+
end
|
166
|
+
begin
|
167
|
+
File.rename(cache_path, old_path)
|
168
|
+
rescue Errno::ENOENT
|
169
|
+
end
|
170
|
+
File.rename(tmp_path, cache_path)
|
102
171
|
end
|
103
172
|
end
|
104
173
|
end
|
105
174
|
|
106
175
|
def to_s
|
107
176
|
synchronize do
|
108
|
-
items.each_with_object(
|
177
|
+
items.each_with_object(+"") do |item, s|
|
109
178
|
s << item.to_s
|
110
179
|
end
|
111
180
|
end
|
@@ -115,6 +184,7 @@ module Mournmail
|
|
115
184
|
class SummaryItem
|
116
185
|
attr_reader :uid, :date, :from, :subject, :flags
|
117
186
|
attr_reader :replies
|
187
|
+
attr_accessor :cache_id
|
118
188
|
|
119
189
|
def initialize(uid, date, from, subject, flags)
|
120
190
|
@uid = uid
|
@@ -124,6 +194,7 @@ module Mournmail
|
|
124
194
|
@flags = flags
|
125
195
|
@line = nil
|
126
196
|
@replies = []
|
197
|
+
@cache_id = nil
|
127
198
|
end
|
128
199
|
|
129
200
|
def add_reply(reply)
|
@@ -177,7 +248,7 @@ module Mournmail
|
|
177
248
|
|
178
249
|
def format_line(limit = 78, from_limit = 16, level = 0)
|
179
250
|
space = " " * (level < 8 ? level : 8)
|
180
|
-
s =
|
251
|
+
s = +""
|
181
252
|
s << format("%6d %s%s %s[ %s ] ",
|
182
253
|
@uid, format_flags(@flags), format_date(@date), space,
|
183
254
|
ljust(format_from(@from), from_limit))
|
@@ -188,7 +259,7 @@ module Mournmail
|
|
188
259
|
|
189
260
|
def ljust(s, n)
|
190
261
|
width = 0
|
191
|
-
str =
|
262
|
+
str = +""
|
192
263
|
s.each_char do |c|
|
193
264
|
w = Buffer.display_width(c)
|
194
265
|
width += w
|
@@ -203,14 +274,14 @@ module Mournmail
|
|
203
274
|
end
|
204
275
|
|
205
276
|
def format_flags(flags)
|
206
|
-
if flags.include?(:
|
277
|
+
if !flags.include?(:Seen)
|
278
|
+
"u"
|
279
|
+
elsif flags.include?(:Deleted)
|
207
280
|
"d"
|
208
281
|
elsif flags.include?(:Flagged)
|
209
282
|
"$"
|
210
283
|
elsif flags.include?(:Answered)
|
211
284
|
"a"
|
212
|
-
elsif !flags.include?(:Seen)
|
213
|
-
"u"
|
214
285
|
else
|
215
286
|
" "
|
216
287
|
end
|
@@ -1,7 +1,3 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "tempfile"
|
4
|
-
|
5
1
|
using Mournmail::MessageRendering
|
6
2
|
|
7
3
|
module Mournmail
|
@@ -20,18 +16,34 @@ module Mournmail
|
|
20
16
|
SUMMARY_MODE_MAP.define_key("u", :summary_toggle_seen_command)
|
21
17
|
SUMMARY_MODE_MAP.define_key("$", :summary_toggle_flagged_command)
|
22
18
|
SUMMARY_MODE_MAP.define_key("d", :summary_toggle_deleted_command)
|
23
|
-
SUMMARY_MODE_MAP.define_key("x", :
|
19
|
+
SUMMARY_MODE_MAP.define_key("x", :summary_toggle_mark_command)
|
20
|
+
SUMMARY_MODE_MAP.define_key("*a", :summary_mark_all_command)
|
21
|
+
SUMMARY_MODE_MAP.define_key("*n", :summary_unmark_all_command)
|
22
|
+
SUMMARY_MODE_MAP.define_key("*r", :summary_mark_read_command)
|
23
|
+
SUMMARY_MODE_MAP.define_key("*u", :summary_mark_unread_command)
|
24
|
+
SUMMARY_MODE_MAP.define_key("*s", :summary_mark_flagged_command)
|
25
|
+
SUMMARY_MODE_MAP.define_key("*t", :summary_mark_unflagged_command)
|
26
|
+
SUMMARY_MODE_MAP.define_key("y", :summary_archive_command)
|
27
|
+
SUMMARY_MODE_MAP.define_key("o", :summary_refile_command)
|
28
|
+
SUMMARY_MODE_MAP.define_key("!", :summary_refile_spam_command)
|
29
|
+
SUMMARY_MODE_MAP.define_key("p", :summary_prefetch_command)
|
30
|
+
SUMMARY_MODE_MAP.define_key("X", :summary_expunge_command)
|
24
31
|
SUMMARY_MODE_MAP.define_key("v", :summary_view_source_command)
|
32
|
+
SUMMARY_MODE_MAP.define_key("M", :summary_merge_partial_command)
|
25
33
|
SUMMARY_MODE_MAP.define_key("q", :mournmail_quit)
|
26
34
|
SUMMARY_MODE_MAP.define_key("k", :previous_line)
|
27
35
|
SUMMARY_MODE_MAP.define_key("j", :next_line)
|
28
36
|
SUMMARY_MODE_MAP.define_key("m", :mournmail_visit_mailbox)
|
37
|
+
SUMMARY_MODE_MAP.define_key("S", :mournmail_visit_spam_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,23 +53,12 @@ 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
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
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
|
-
mark_as_seen(uid, !fetched)
|
58
|
+
mail, fetched = summary.read_mail(uid)
|
59
|
+
foreground do
|
60
|
+
show_message(mail)
|
61
|
+
mark_as_seen(uid, false)
|
61
62
|
Mournmail.current_uid = uid
|
62
63
|
Mournmail.current_mail = mail
|
63
64
|
end
|
@@ -97,12 +98,10 @@ 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
|
-
|
103
|
-
|
104
|
-
|
105
|
-
next_tick do
|
102
|
+
mail = read_current_mail[0]
|
103
|
+
body = mail.render_text
|
104
|
+
foreground do
|
106
105
|
Window.current = Mournmail.message_window
|
107
106
|
Commands.mail(run_hooks: false)
|
108
107
|
if reply_all
|
@@ -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
|
-
|
126
|
-
|
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
|
@@ -133,6 +139,7 @@ module Mournmail
|
|
133
139
|
On #{mail['date']}
|
134
140
|
#{mail['from']} wrote:
|
135
141
|
EOF
|
142
|
+
Mournmail.insert_signature
|
136
143
|
exchange_point_and_mark
|
137
144
|
run_hooks(:mournmail_draft_setup_hook)
|
138
145
|
end
|
@@ -141,14 +148,12 @@ module Mournmail
|
|
141
148
|
|
142
149
|
define_local_command(:summary_forward,
|
143
150
|
doc: "Forward the current message.") do
|
144
|
-
|
145
|
-
summary = Mournmail.current_summary
|
146
|
-
item = summary[uid]
|
151
|
+
message = current_message
|
147
152
|
Window.current = Mournmail.message_window
|
148
153
|
Commands.mail
|
149
154
|
re_search_forward(/^Subject: /)
|
150
|
-
insert("Forward: " +
|
151
|
-
insert("\nAttached-Message: #{
|
155
|
+
insert("Forward: " + message.subject)
|
156
|
+
insert("\nAttached-Message: #{message._key}")
|
152
157
|
re_search_backward(/^To: /)
|
153
158
|
end_of_line
|
154
159
|
end
|
@@ -165,27 +170,81 @@ module Mournmail
|
|
165
170
|
|
166
171
|
define_local_command(:summary_toggle_deleted,
|
167
172
|
doc: <<~EOD) do
|
168
|
-
Toggle Deleted. Type `
|
173
|
+
Toggle Deleted. Type `X` to expunge deleted messages.
|
169
174
|
EOD
|
170
175
|
toggle_flag(selected_uid, :Deleted)
|
171
176
|
end
|
172
177
|
|
178
|
+
define_local_command(:summary_toggle_mark, doc: "Toggle mark.") do
|
179
|
+
@buffer.read_only_edit do
|
180
|
+
@buffer.save_excursion do
|
181
|
+
@buffer.beginning_of_line
|
182
|
+
if @buffer.looking_at?(/( *\d+)([ *])/)
|
183
|
+
uid = @buffer.match_string(1)
|
184
|
+
old_mark = @buffer.match_string(2)
|
185
|
+
new_mark = old_mark == "*" ? " " : "*"
|
186
|
+
@buffer.replace_match(uid + new_mark)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
define_local_command(:summary_mark_all, doc: "Mark all mails.") do
|
193
|
+
gsub_buffer(/^( *\d+) /, '\\1*')
|
194
|
+
end
|
195
|
+
|
196
|
+
define_local_command(:summary_unmark_all, doc: "Unmark all mails.") do
|
197
|
+
gsub_buffer(/^( *\d+)\*/, '\\1 ')
|
198
|
+
end
|
199
|
+
|
200
|
+
define_local_command(:summary_mark_read, doc: "Mark read mails.") do
|
201
|
+
gsub_buffer(/^( *\d+) ([^u])/, '\\1*\\2')
|
202
|
+
end
|
203
|
+
|
204
|
+
define_local_command(:summary_mark_unread, doc: "Mark unread mails.") do
|
205
|
+
gsub_buffer(/^( *\d+) u/, '\\1*u')
|
206
|
+
end
|
207
|
+
|
208
|
+
define_local_command(:summary_mark_flagged, doc: "Mark flagged mails.") do
|
209
|
+
gsub_buffer(/^( *\d+) \$/, '\\1*$')
|
210
|
+
end
|
211
|
+
|
212
|
+
define_local_command(:summary_mark_unflagged,
|
213
|
+
doc: "Mark unflagged mails.") do
|
214
|
+
gsub_buffer(/^( *\d+) ([^$])/, '\\1*\\2')
|
215
|
+
end
|
216
|
+
|
173
217
|
define_local_command(:summary_expunge,
|
174
218
|
doc: <<~EOD) do
|
175
219
|
Expunge deleted messages.
|
176
220
|
EOD
|
177
221
|
buffer = Buffer.current
|
222
|
+
mailbox = Mournmail.current_mailbox
|
223
|
+
summary = Mournmail.current_summary
|
178
224
|
Mournmail.background do
|
179
225
|
Mournmail.imap_connect do |imap|
|
180
226
|
imap.expunge
|
181
227
|
end
|
182
|
-
summary = Mournmail.current_summary
|
183
228
|
summary.delete_item_if do |item|
|
184
|
-
item.flags.include?(:Deleted)
|
229
|
+
if item.flags.include?(:Deleted)
|
230
|
+
if item.cache_id
|
231
|
+
begin
|
232
|
+
File.unlink(Mournmail.mail_cache_path(item.cache_id))
|
233
|
+
rescue Errno::ENOENT
|
234
|
+
end
|
235
|
+
begin
|
236
|
+
Groonga["Messages"].delete(item.cache_id)
|
237
|
+
rescue Groonga::InvalidArgument
|
238
|
+
end
|
239
|
+
true
|
240
|
+
end
|
241
|
+
else
|
242
|
+
false
|
243
|
+
end
|
185
244
|
end
|
186
245
|
summary_text = summary.to_s
|
187
246
|
summary.save
|
188
|
-
|
247
|
+
foreground do
|
189
248
|
buffer.read_only_edit do
|
190
249
|
buffer.clear
|
191
250
|
buffer.insert(summary_text)
|
@@ -199,15 +258,14 @@ module Mournmail
|
|
199
258
|
doc: "View source of a mail.") do
|
200
259
|
uid = selected_uid
|
201
260
|
Mournmail.background do
|
202
|
-
|
203
|
-
|
204
|
-
next_tick do
|
261
|
+
mail, = read_current_mail
|
262
|
+
foreground do
|
205
263
|
source_buffer = Buffer.find_or_new("*message-source*",
|
206
264
|
file_encoding: "ascii-8bit",
|
207
265
|
undo_limit: 0, read_only: true)
|
208
266
|
source_buffer.read_only_edit do
|
209
267
|
source_buffer.clear
|
210
|
-
source_buffer.insert(
|
268
|
+
source_buffer.insert(mail.raw_source.gsub(/\r\n/, "\n"))
|
211
269
|
source_buffer.file_format = :dos
|
212
270
|
source_buffer.beginning_of_buffer
|
213
271
|
end
|
@@ -217,6 +275,207 @@ module Mournmail
|
|
217
275
|
end
|
218
276
|
end
|
219
277
|
|
278
|
+
define_local_command(:summary_merge_partial,
|
279
|
+
doc: "Merge marked message/partial.") do
|
280
|
+
uids = []
|
281
|
+
@buffer.save_excursion do
|
282
|
+
@buffer.beginning_of_buffer
|
283
|
+
while @buffer.re_search_forward(/^( *\d+)\*/, raise_error: false)
|
284
|
+
uid = @buffer.match_string(0).to_i
|
285
|
+
# @buffer.replace_match(@buffer.match_string(0) + " ")
|
286
|
+
uids.push(uid)
|
287
|
+
end
|
288
|
+
end
|
289
|
+
summary = Mournmail.current_summary
|
290
|
+
Mournmail.background do
|
291
|
+
id = nil
|
292
|
+
total = nil
|
293
|
+
mails = uids.map { |uid|
|
294
|
+
summary.read_mail(uid)[0]
|
295
|
+
}.select { |mail|
|
296
|
+
mail.main_type == "message" &&
|
297
|
+
mail.sub_type == "partial" #&&
|
298
|
+
(id ||= mail["Content-Type"].parameters["id"]) ==
|
299
|
+
mail["Content-Type"].parameters["id"] &&
|
300
|
+
(total ||= mail["Content-Type"].parameters["total"]&.to_i)
|
301
|
+
}.sort_by { |mail|
|
302
|
+
mail["Content-Type"].parameters["number"].to_i
|
303
|
+
}
|
304
|
+
if mails.length != total
|
305
|
+
raise EditorError, "No enough messages (#{mails.length} of #{total})"
|
306
|
+
end
|
307
|
+
s = mails.map { |mail| mail.body.decoded }.join
|
308
|
+
mail = Mail.new(s)
|
309
|
+
foreground do
|
310
|
+
show_message(mail)
|
311
|
+
Mournmail.current_uid = nil
|
312
|
+
Mournmail.current_mail = mail
|
313
|
+
end
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
define_local_command(:summary_archive,
|
318
|
+
doc: "Archive marked mails.") do
|
319
|
+
archive_mailbox_format =
|
320
|
+
Mournmail.account_config[:archive_mailbox_format]
|
321
|
+
if archive_mailbox_format.nil?
|
322
|
+
raise EditorError, "No archive_mailbox_format in the current account"
|
323
|
+
end
|
324
|
+
uids = marked_uids
|
325
|
+
summary = Mournmail.current_summary
|
326
|
+
now = Time.now
|
327
|
+
if archive_mailbox_format == false
|
328
|
+
mailboxes = { nil => uids }
|
329
|
+
else
|
330
|
+
mailboxes = uids.map { |uid| summary[uid] }.group_by { |item|
|
331
|
+
t = Time.parse(item.date) rescue now
|
332
|
+
t.strftime(archive_mailbox_format)
|
333
|
+
}.transform_values { |items|
|
334
|
+
items.map(&:uid)
|
335
|
+
}
|
336
|
+
end
|
337
|
+
source_mailbox = Mournmail.current_mailbox
|
338
|
+
if mailboxes.key?(source_mailbox)
|
339
|
+
raise EditorError, "Can't archive mails in archive mailboxes"
|
340
|
+
end
|
341
|
+
Mournmail.background do
|
342
|
+
Mournmail.imap_connect do |imap|
|
343
|
+
mailboxes.each do |mailbox, item_uids|
|
344
|
+
if mailbox && !imap.list("", mailbox)
|
345
|
+
imap.create(mailbox)
|
346
|
+
end
|
347
|
+
refile_mails(imap, source_mailbox, item_uids, mailbox)
|
348
|
+
end
|
349
|
+
imap.expunge
|
350
|
+
delete_from_summary(summary, uids, "Archived messages")
|
351
|
+
end
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
define_local_command(:summary_refile,
|
356
|
+
doc: "Refile marked mails.") do
|
357
|
+
|mailbox = Mournmail.read_mailbox_name("Refile mails: ")|
|
358
|
+
uids = marked_uids
|
359
|
+
summary = Mournmail.current_summary
|
360
|
+
source_mailbox = Mournmail.current_mailbox
|
361
|
+
if source_mailbox == mailbox
|
362
|
+
raise EditorError, "Can't refile to the same mailbox"
|
363
|
+
end
|
364
|
+
Mournmail.background do
|
365
|
+
Mournmail.imap_connect do |imap|
|
366
|
+
unless imap.list("", mailbox)
|
367
|
+
if foreground! { yes_or_no?("#{mailbox} doesn't exist; Create?") }
|
368
|
+
imap.create(mailbox)
|
369
|
+
else
|
370
|
+
next
|
371
|
+
end
|
372
|
+
end
|
373
|
+
refile_mails(imap, source_mailbox, uids, mailbox)
|
374
|
+
delete_from_summary(summary, uids, "Refiled messages")
|
375
|
+
end
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
define_local_command(:summary_refile_spam,
|
380
|
+
doc: "Refile marked mails as spam.") do
|
381
|
+
mailbox = Mournmail.account_config[:spam_mailbox]
|
382
|
+
if mailbox.nil?
|
383
|
+
raise EditorError, "spam_mailbox is not specified"
|
384
|
+
end
|
385
|
+
summary_refile(Net::IMAP.encode_utf7(mailbox))
|
386
|
+
end
|
387
|
+
|
388
|
+
define_local_command(:summary_prefetch,
|
389
|
+
doc: "Prefetch mails.") do
|
390
|
+
summary = Mournmail.current_summary
|
391
|
+
mailbox = Mournmail.current_mailbox
|
392
|
+
spam_mailbox = Mournmail.account_config[:spam_mailbox]
|
393
|
+
if mailbox == Net::IMAP.encode_utf7(spam_mailbox)
|
394
|
+
raise EditorError, "Can't prefetch spam"
|
395
|
+
end
|
396
|
+
target_uids = @buffer.to_s.scan(/^ *\d+/).map { |s|
|
397
|
+
s.to_i
|
398
|
+
}.select { |uid|
|
399
|
+
summary[uid].cache_id.nil?
|
400
|
+
}
|
401
|
+
Mournmail.background do
|
402
|
+
Mournmail.imap_connect do |imap|
|
403
|
+
imap.select(mailbox)
|
404
|
+
count = 0
|
405
|
+
begin
|
406
|
+
target_uids.each_slice(20) do |uids|
|
407
|
+
data = imap.uid_fetch(uids, "BODY[]")
|
408
|
+
data&.each do |i|
|
409
|
+
uid = i.attr["UID"]
|
410
|
+
s = i.attr["BODY[]"]
|
411
|
+
if s
|
412
|
+
cache_id = Mournmail.write_mail_cache(s)
|
413
|
+
Mournmail.index_mail(cache_id, Mail.new(s))
|
414
|
+
summary[uid].cache_id = cache_id
|
415
|
+
end
|
416
|
+
end
|
417
|
+
count += uids.size
|
418
|
+
progress = (count.to_f * 100 / target_uids.size).round
|
419
|
+
foreground do
|
420
|
+
message("Prefetching mails... #{progress}%", log: false)
|
421
|
+
end
|
422
|
+
end
|
423
|
+
ensure
|
424
|
+
summary.save
|
425
|
+
end
|
426
|
+
end
|
427
|
+
foreground do
|
428
|
+
message("Done")
|
429
|
+
end
|
430
|
+
end
|
431
|
+
end
|
432
|
+
|
433
|
+
define_local_command(:summary_search, doc: "Search mails.") do
|
434
|
+
|query = read_from_minibuffer("Search mail: ",
|
435
|
+
initial_value: @buffer[:query]),
|
436
|
+
page = 1|
|
437
|
+
Mournmail.background do
|
438
|
+
messages = Groonga["Messages"].select { |record|
|
439
|
+
record.match(query) { |match_record|
|
440
|
+
match_record.subject | match_record.body
|
441
|
+
}
|
442
|
+
}.paginate([["date", :desc]], page: page, size: 100)
|
443
|
+
foreground do
|
444
|
+
show_search_result(messages, query: query)
|
445
|
+
message("Searched (#{messages.current_page}/#{messages.n_pages})")
|
446
|
+
end
|
447
|
+
end
|
448
|
+
end
|
449
|
+
|
450
|
+
define_local_command(:summary_show_thread,
|
451
|
+
doc: "Show the thread of the current mail.") do
|
452
|
+
Mournmail.background do
|
453
|
+
message = current_message
|
454
|
+
messages = Groonga["Messages"].select { |m|
|
455
|
+
m.thread_id == message.thread_id
|
456
|
+
}.sort([["date", :asc]])
|
457
|
+
foreground do
|
458
|
+
show_search_result(messages, buffer_name: "*thread*")
|
459
|
+
i = messages.find_index { |m| m._key == message._key }
|
460
|
+
Buffer.current.goto_line(i + 1)
|
461
|
+
end
|
462
|
+
end
|
463
|
+
end
|
464
|
+
|
465
|
+
define_local_command(:summary_change_account,
|
466
|
+
doc: "Change the current account.") do
|
467
|
+
|account = Mournmail.read_account_name("Change account: ")|
|
468
|
+
unless CONFIG[:mournmail_accounts].key?(account)
|
469
|
+
raise EditorError, "No such account: #{account}"
|
470
|
+
end
|
471
|
+
if Mournmail.background_thread
|
472
|
+
raise EditorError, "Background thread is running"
|
473
|
+
end
|
474
|
+
mournmail_quit
|
475
|
+
Mournmail.current_account = account
|
476
|
+
mournmail
|
477
|
+
end
|
478
|
+
|
220
479
|
private
|
221
480
|
|
222
481
|
def selected_uid
|
@@ -231,6 +490,16 @@ module Mournmail
|
|
231
490
|
}
|
232
491
|
end
|
233
492
|
|
493
|
+
def marked_uids
|
494
|
+
@buffer.to_s.scan(/^ *\d+(?=\*)/).map(&:to_i)
|
495
|
+
end
|
496
|
+
|
497
|
+
def read_current_mail
|
498
|
+
summary = Mournmail.current_summary
|
499
|
+
uid = selected_uid
|
500
|
+
summary.read_mail(uid)
|
501
|
+
end
|
502
|
+
|
234
503
|
def scroll_up_or_next_uid
|
235
504
|
begin
|
236
505
|
uid = selected_uid
|
@@ -254,6 +523,20 @@ module Mournmail
|
|
254
523
|
end
|
255
524
|
end
|
256
525
|
|
526
|
+
def show_message(mail)
|
527
|
+
message_buffer = Buffer.find_or_new("*message*",
|
528
|
+
undo_limit: 0, read_only: true)
|
529
|
+
message_buffer.apply_mode(Mournmail::MessageMode)
|
530
|
+
message_buffer.read_only_edit do
|
531
|
+
message_buffer.clear
|
532
|
+
message_buffer.insert(mail.render)
|
533
|
+
message_buffer.beginning_of_buffer
|
534
|
+
end
|
535
|
+
message_buffer[:mournmail_mail] = mail
|
536
|
+
window = Mournmail.message_window
|
537
|
+
window.buffer = message_buffer
|
538
|
+
end
|
539
|
+
|
257
540
|
def mark_as_seen(uid, update_server)
|
258
541
|
summary_item = Mournmail.current_summary[uid]
|
259
542
|
if summary_item && !summary_item.flags.include?(:Seen)
|
@@ -269,7 +552,7 @@ module Mournmail
|
|
269
552
|
Mournmail.background do
|
270
553
|
summary_item.toggle_flag(flag)
|
271
554
|
Mournmail.current_summary.save
|
272
|
-
|
555
|
+
foreground do
|
273
556
|
update_flags(summary_item)
|
274
557
|
end
|
275
558
|
end
|
@@ -300,5 +583,101 @@ module Mournmail
|
|
300
583
|
@buffer.forward_line
|
301
584
|
end
|
302
585
|
end
|
586
|
+
|
587
|
+
def gsub_buffer(re, s)
|
588
|
+
@buffer.read_only_edit do
|
589
|
+
s = @buffer.to_s.gsub(re, s)
|
590
|
+
@buffer.replace(s)
|
591
|
+
end
|
592
|
+
end
|
593
|
+
|
594
|
+
def show_search_result(messages,
|
595
|
+
query: nil, buffer_name: "*search result*")
|
596
|
+
summary_text = messages.map { |m|
|
597
|
+
format("%s [ %s ] %s\n",
|
598
|
+
m.date.strftime("%m/%d %H:%M"),
|
599
|
+
ljust(m.from.to_s, 16),
|
600
|
+
ljust(m.subject.to_s, 45))
|
601
|
+
}.join
|
602
|
+
buffer = Buffer.find_or_new(buffer_name, undo_limit: 0,
|
603
|
+
read_only: true)
|
604
|
+
buffer.apply_mode(Mournmail::SearchResultMode)
|
605
|
+
buffer.read_only_edit do
|
606
|
+
buffer.clear
|
607
|
+
buffer.insert(summary_text)
|
608
|
+
buffer.beginning_of_buffer
|
609
|
+
end
|
610
|
+
buffer[:messages] = messages
|
611
|
+
buffer[:query] = query
|
612
|
+
switch_to_buffer(buffer)
|
613
|
+
end
|
614
|
+
|
615
|
+
def ljust(s, n)
|
616
|
+
width = 0
|
617
|
+
str = +""
|
618
|
+
s.gsub(/\t/, " ").each_char do |c|
|
619
|
+
w = Buffer.display_width(c)
|
620
|
+
width += w
|
621
|
+
if width > n
|
622
|
+
width -= w
|
623
|
+
break
|
624
|
+
end
|
625
|
+
str.concat(c)
|
626
|
+
break if width == n
|
627
|
+
end
|
628
|
+
str + " " * (n - width)
|
629
|
+
end
|
630
|
+
|
631
|
+
def refile_mails(imap, src_mailbox, uids, dst_mailbox)
|
632
|
+
count = 0
|
633
|
+
uids.each_slice(100) do |uid_set|
|
634
|
+
if dst_mailbox
|
635
|
+
imap.uid_copy(uid_set, dst_mailbox)
|
636
|
+
end
|
637
|
+
imap.uid_store(uid_set, "+FLAGS", [:Deleted])
|
638
|
+
count += uid_set.size
|
639
|
+
progress = (count.to_f * 100 / uids.size).round
|
640
|
+
foreground do
|
641
|
+
if dst_mailbox
|
642
|
+
message("Refiling mails to #{dst_mailbox}... #{progress}%",
|
643
|
+
log: false)
|
644
|
+
else
|
645
|
+
message("Deleting mails... #{progress}%", log: false)
|
646
|
+
end
|
647
|
+
end
|
648
|
+
end
|
649
|
+
foreground do
|
650
|
+
if dst_mailbox
|
651
|
+
message("Refiled mails to #{dst_mailbox}")
|
652
|
+
else
|
653
|
+
message("Deleted mails")
|
654
|
+
end
|
655
|
+
end
|
656
|
+
end
|
657
|
+
|
658
|
+
def current_message
|
659
|
+
uid = selected_uid
|
660
|
+
item = Mournmail.current_summary[uid]
|
661
|
+
message = Groonga["Messages"][item.cache_id]
|
662
|
+
if message.nil?
|
663
|
+
raise EditorError, "No message found"
|
664
|
+
end
|
665
|
+
message
|
666
|
+
end
|
667
|
+
|
668
|
+
def delete_from_summary(summary, uids, msg)
|
669
|
+
summary.delete_item_if do |item|
|
670
|
+
uids.include?(item.uid)
|
671
|
+
end
|
672
|
+
summary_text = summary.to_s
|
673
|
+
summary.save
|
674
|
+
foreground do
|
675
|
+
@buffer.read_only_edit do
|
676
|
+
@buffer.clear
|
677
|
+
@buffer.insert(summary_text)
|
678
|
+
end
|
679
|
+
message(msg)
|
680
|
+
end
|
681
|
+
end
|
303
682
|
end
|
304
683
|
end
|