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.
- checksums.yaml +5 -5
- data/README.md +46 -27
- data/lib/mournmail.rb +2 -0
- data/lib/mournmail/commands.rb +7 -4
- data/lib/mournmail/config.rb +2 -4
- data/lib/mournmail/draft_mode.rb +56 -13
- data/lib/mournmail/mail_encoded_word_patch.rb +73 -0
- data/lib/mournmail/message_mode.rb +3 -2
- data/lib/mournmail/message_rendering.rb +22 -15
- data/lib/mournmail/search_result_mode.rb +144 -0
- data/lib/mournmail/summary.rb +63 -8
- data/lib/mournmail/summary_mode.rb +406 -38
- data/lib/mournmail/utils.rb +279 -38
- data/lib/mournmail/version.rb +1 -1
- data/mournmail.gemspec +1 -1
- metadata +6 -5
@@ -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
|
data/lib/mournmail/summary.rb
CHANGED
@@ -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
|
-
|
19
|
-
|
20
|
-
|
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
|
-
|
100
|
-
|
101
|
-
|
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", :
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
@@ -141,14 +147,12 @@ module Mournmail
|
|
141
147
|
|
142
148
|
define_local_command(:summary_forward,
|
143
149
|
doc: "Forward the current message.") do
|
144
|
-
|
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: " +
|
151
|
-
insert("\nAttached-Message: #{
|
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
|
-
|
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(
|
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
|