mournmail 0.3.0 → 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +6 -2
- data/exe/mournmail_reindex +16 -0
- data/lib/mournmail/commands.rb +11 -2
- data/lib/mournmail/config.rb +8 -0
- data/lib/mournmail/draft_mode.rb +29 -3
- data/lib/mournmail/message_mode.rb +21 -11
- data/lib/mournmail/message_rendering.rb +96 -24
- data/lib/mournmail/summary.rb +2 -1
- data/lib/mournmail/summary_mode.rb +20 -13
- data/lib/mournmail/utils.rb +95 -36
- data/lib/mournmail/version.rb +1 -1
- data/mournmail.gemspec +4 -1
- metadata +53 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d2cbdbd1f9d9fe5f74f7d098cbf1eae74e113d544705005a9ca95f652799c7e3
|
4
|
+
data.tar.gz: 384b036461c6ea691d7559bb17fec0c2439ebebf51e5694e49481844d25336a5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 18831f3e5fff6e0a181791c56a926c866cfef9f2b210e5f47f7f0bca04c73695130cdd3f9924b9a7e359e1f479e878ea1c8c157092162e366513e62a9111eca3
|
7
|
+
data.tar.gz: b252222b12d59b599df9f81f5b610dff5649c0440782e89ca273602039738b8a3708f6499560ad461c88844a14c0bcb2e9216ddb21dbdd00471a82de52e71569
|
data/README.md
CHANGED
@@ -33,7 +33,11 @@ CONFIG[:mournmail_accounts] = {
|
|
33
33
|
},
|
34
34
|
spam_mailbox: "spam",
|
35
35
|
outbox_mailbox: "outbox",
|
36
|
-
archive_mailbox_format: "archive/%Y"
|
36
|
+
archive_mailbox_format: "archive/%Y",
|
37
|
+
signature: <<~EOF
|
38
|
+
--
|
39
|
+
Shugo Maeda <shugo@example.com>
|
40
|
+
EOF
|
37
41
|
},
|
38
42
|
"gmail.com" => {
|
39
43
|
from: "Example <example@gmail.com>",
|
@@ -54,7 +58,7 @@ CONFIG[:mournmail_accounts] = {
|
|
54
58
|
password: File.read(File.expand_path("~/.textbringer/gmail_passwd")).chomp,
|
55
59
|
ssl: true
|
56
60
|
},
|
57
|
-
spam_mailbox: "
|
61
|
+
spam_mailbox: "[Gmail]/迷惑メール",
|
58
62
|
archive_mailbox_format: false
|
59
63
|
},
|
60
64
|
}
|
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "groonga"
|
4
|
+
|
5
|
+
if ARGV.size < 1
|
6
|
+
STDERR.puts "Usage: mournmail_reindex <path to messages.db>"
|
7
|
+
exit 1
|
8
|
+
end
|
9
|
+
|
10
|
+
Groonga::Database.open(ARGV[0])
|
11
|
+
Groonga["Messages"].columns.each do |c|
|
12
|
+
print "Reindexing #{c.name}... "
|
13
|
+
STDOUT.flush
|
14
|
+
c.reindex
|
15
|
+
puts "done"
|
16
|
+
end
|
data/lib/mournmail/commands.rb
CHANGED
@@ -11,6 +11,14 @@ define_command(:mournmail_visit_mailbox, doc: "Visit mailbox") do
|
|
11
11
|
end
|
12
12
|
end
|
13
13
|
|
14
|
+
define_command(:mournmail_visit_spam_mailbox, doc: "Visit spam mailbox") do
|
15
|
+
mailbox = Mournmail.account_config[:spam_mailbox]
|
16
|
+
if mailbox.nil?
|
17
|
+
raise EditorError, "spam_mailbox is not specified"
|
18
|
+
end
|
19
|
+
mournmail_visit_mailbox(Net::IMAP.encode_utf7(mailbox))
|
20
|
+
end
|
21
|
+
|
14
22
|
define_command(:mournmail_summary_sync, doc: "Sync summary.") do
|
15
23
|
|mailbox = (Mournmail.current_mailbox || "INBOX"),
|
16
24
|
all = current_prefix_arg|
|
@@ -61,9 +69,10 @@ define_command(:mail, doc: "Write a new mail.") do
|
|
61
69
|
User-Agent: Mournmail/#{Mournmail::VERSION} Textbringer/#{Textbringer::VERSION} Ruby/#{RUBY_VERSION}
|
62
70
|
--text follows this line--
|
63
71
|
EOF
|
64
|
-
|
65
|
-
|
72
|
+
beginning_of_buffer
|
73
|
+
re_search_forward(/^To: */)
|
66
74
|
if run_hooks
|
75
|
+
Mournmail.insert_signature
|
67
76
|
run_hooks(:mournmail_draft_setup_hook)
|
68
77
|
end
|
69
78
|
end
|
data/lib/mournmail/config.rb
CHANGED
@@ -14,6 +14,13 @@ module Textbringer
|
|
14
14
|
"X-Mailer",
|
15
15
|
"Content-Type"
|
16
16
|
]
|
17
|
+
CONFIG[:mournmail_quote_header_fields] = [
|
18
|
+
"Subject",
|
19
|
+
"Date",
|
20
|
+
"From",
|
21
|
+
"To",
|
22
|
+
"Cc"
|
23
|
+
]
|
17
24
|
CONFIG[:mournmail_imap_connect_timeout] = 10
|
18
25
|
CONFIG[:mournmail_keep_alive_interval] = 60
|
19
26
|
case RUBY_PLATFORM
|
@@ -28,4 +35,5 @@ module Textbringer
|
|
28
35
|
CONFIG[:mournmail_link_open_comamnd] = "xdg-open"
|
29
36
|
end
|
30
37
|
CONFIG[:mournmail_addresses_path] = File.expand_path("~/.addresses")
|
38
|
+
CONFIG[:mournmail_signature_regexp] = /^\n-- /
|
31
39
|
end
|
data/lib/mournmail/draft_mode.rb
CHANGED
@@ -7,6 +7,8 @@ module Mournmail
|
|
7
7
|
MAIL_MODE_MAP.define_key("\t", :draft_complete_or_insert_tab_command)
|
8
8
|
MAIL_MODE_MAP.define_key("\C-c\C-xv", :draft_pgp_sign_command)
|
9
9
|
MAIL_MODE_MAP.define_key("\C-c\C-xe", :draft_pgp_encrypt_command)
|
10
|
+
MAIL_MODE_MAP.define_key("\C-c\t", :insert_signature_command)
|
11
|
+
MAIL_MODE_MAP.define_key("\C-c@", :draft_change_account_command)
|
10
12
|
|
11
13
|
define_syntax :field_name, /^[A-Za-z\-]+: /
|
12
14
|
define_syntax :quotation, /^>.*/
|
@@ -67,18 +69,21 @@ module Mournmail
|
|
67
69
|
content: File.read(file),
|
68
70
|
encoding: "binary")
|
69
71
|
end
|
70
|
-
|
72
|
+
account = @buffer[:mournmail_delivery_account] ||
|
73
|
+
Mournmail.current_account
|
74
|
+
conf = CONFIG[:mournmail_accounts][account]
|
71
75
|
options = @buffer[:mournmail_delivery_options] ||
|
72
76
|
conf[:delivery_options]
|
73
77
|
if options[:authentication] == "gmail"
|
78
|
+
token = Mournmail.google_access_token(account)
|
74
79
|
options = options.merge(authentication: "xoauth2",
|
75
|
-
password:
|
80
|
+
password: token)
|
76
81
|
end
|
77
82
|
m.delivery_method(@buffer[:mournmail_delivery_method] ||
|
78
83
|
conf[:delivery_method],
|
79
84
|
options)
|
80
85
|
bury_buffer(@buffer)
|
81
|
-
background do
|
86
|
+
Mournmail.background do
|
82
87
|
begin
|
83
88
|
if !attached_messages.empty?
|
84
89
|
attached_messages.each do |attached_message|
|
@@ -181,6 +186,27 @@ module Mournmail
|
|
181
186
|
end
|
182
187
|
end
|
183
188
|
|
189
|
+
define_local_command(:insert_signature, doc: "Insert signature.") do
|
190
|
+
@buffer.insert(CONFIG[:signature])
|
191
|
+
end
|
192
|
+
|
193
|
+
define_local_command(:draft_change_account, doc: "Change account.") do
|
194
|
+
|account = Mournmail.read_account_name("Change account: ")|
|
195
|
+
from = CONFIG[:mournmail_accounts][account][:from]
|
196
|
+
@buffer[:mournmail_delivery_account] = account
|
197
|
+
@buffer.save_excursion do
|
198
|
+
@buffer.beginning_of_buffer
|
199
|
+
@buffer.re_search_forward(/^From:.*/)
|
200
|
+
@buffer.replace_match("From: " + from)
|
201
|
+
@buffer.end_of_buffer
|
202
|
+
if @buffer.re_search_backward(CONFIG[:mournmail_signature_regexp],
|
203
|
+
raise_error: false)
|
204
|
+
@buffer.delete_region(@buffer.point, @buffer.point_max)
|
205
|
+
end
|
206
|
+
Mournmail.insert_signature
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
184
210
|
private
|
185
211
|
|
186
212
|
def end_of_header
|
@@ -12,8 +12,8 @@ module Mournmail
|
|
12
12
|
|
13
13
|
# See http://nihongo.jp/support/mail_guide/dev_guide.txt
|
14
14
|
MAILTO_REGEXP = URI.regexp("mailto")
|
15
|
-
URI_REGEXP = /(https?|ftp):\/\/[^ \t\n>)"]*[
|
16
|
-
MIME_REGEXP = /^\[(([
|
15
|
+
URI_REGEXP = /(https?|ftp):\/\/[^ \t\n>)"]*[^\] \t\n>.,:)"]+|#{MAILTO_REGEXP}/
|
16
|
+
MIME_REGEXP = /^\[(([0-9.]+) [A-Za-z._\-]+\/[A-Za-z._\-]+.*|PGP\/MIME .*)\]$/
|
17
17
|
URI_OR_MIME_REGEXP = /#{URI_REGEXP}|#{MIME_REGEXP}/
|
18
18
|
|
19
19
|
define_syntax :field_name, /^[A-Za-z\-]+: /
|
@@ -24,6 +24,7 @@ module Mournmail
|
|
24
24
|
def initialize(buffer)
|
25
25
|
super(buffer)
|
26
26
|
buffer.keymap = MESSAGE_MODE_MAP
|
27
|
+
@attached_file = nil
|
27
28
|
end
|
28
29
|
|
29
30
|
define_local_command(:message_open_link_or_part,
|
@@ -52,6 +53,9 @@ module Mournmail
|
|
52
53
|
|
53
54
|
define_local_command(:message_next_link_or_part,
|
54
55
|
doc: "Go to the next link or MIME part.") do
|
56
|
+
if @buffer.looking_at?(URI_OR_MIME_REGEXP)
|
57
|
+
@buffer.forward_char
|
58
|
+
end
|
55
59
|
if @buffer.re_search_forward(URI_OR_MIME_REGEXP, raise_error: false)
|
56
60
|
goto_char(@buffer.match_beginning(0))
|
57
61
|
else
|
@@ -64,7 +68,7 @@ module Mournmail
|
|
64
68
|
def current_part
|
65
69
|
@buffer.save_excursion do
|
66
70
|
@buffer.beginning_of_line
|
67
|
-
if @buffer.looking_at?(/\[([
|
71
|
+
if @buffer.looking_at?(/\[([0-9.]+) .*\]/)
|
68
72
|
index = match_string(1)
|
69
73
|
indices = index.split(".").map(&:to_i)
|
70
74
|
@buffer[:mournmail_mail].dig_part(*indices)
|
@@ -76,8 +80,8 @@ module Mournmail
|
|
76
80
|
|
77
81
|
def part_file_name(part)
|
78
82
|
file_name =
|
79
|
-
part["content-disposition"]&.parameters&.[]("filename") ||
|
80
|
-
part["content-type"]&.parameters&.[]("name") ||
|
83
|
+
(part["content-disposition"]&.parameters&.[]("filename") rescue nil) ||
|
84
|
+
(part["content-type"]&.parameters&.[]("name") rescue nil) ||
|
81
85
|
part_default_file_name(part)
|
82
86
|
decoded_file_name = Mail::Encodings.decode_encode(file_name, :decode)
|
83
87
|
if /\A([A-Za-z0-9_\-]+)'(?:[A-Za-z0-9_\-])*'(.*)/ =~ decoded_file_name
|
@@ -130,14 +134,20 @@ module Mournmail
|
|
130
134
|
else
|
131
135
|
file_name = "mournmail"
|
132
136
|
end
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
137
|
+
@attached_file = Tempfile.open(file_name, binmode: true)
|
138
|
+
s = part.decoded
|
139
|
+
if part.content_type == "text/html"
|
140
|
+
s = s.sub(/<meta http-equiv="content-type".*?>/i, "")
|
141
|
+
elsif part.charset
|
142
|
+
s = s.encode(part.charset)
|
143
|
+
end
|
144
|
+
@attached_file.write(s)
|
145
|
+
@attached_file.close
|
146
|
+
if part.main_type == "text" && part.sub_type != "html"
|
147
|
+
find_file(@attached_file.path)
|
138
148
|
else
|
139
149
|
background do
|
140
|
-
system(*CONFIG[:mournmail_file_open_comamnd],
|
150
|
+
system(*CONFIG[:mournmail_file_open_comamnd], @attached_file.path,
|
141
151
|
out: File::NULL, err: File::NULL)
|
142
152
|
end
|
143
153
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require "mail"
|
2
|
+
require "html2text"
|
2
3
|
|
3
4
|
module Mournmail
|
4
5
|
module MessageRendering
|
@@ -7,8 +8,8 @@ module Mournmail
|
|
7
8
|
render_header + "\n" + render_body(indices)
|
8
9
|
end
|
9
10
|
|
10
|
-
def render_header
|
11
|
-
|
11
|
+
def render_header(fields = CONFIG[:mournmail_display_header_fields])
|
12
|
+
fields.map { |name|
|
12
13
|
val = self[name]&.to_s&.gsub(/\t/, " ")
|
13
14
|
val ? "#{name}: #{val}\n" : ""
|
14
15
|
}.join
|
@@ -27,28 +28,58 @@ module Mournmail
|
|
27
28
|
end
|
28
29
|
if multipart?
|
29
30
|
parts.each_with_index.map { |part, i|
|
30
|
-
|
31
|
+
no_content = sub_type == "alternative" && i > 0
|
32
|
+
part.render([*indices, i], no_content)
|
31
33
|
}.join
|
32
|
-
elsif main_type.nil? ||
|
33
|
-
s = body.decoded
|
34
|
-
|
34
|
+
elsif main_type.nil? || main_type == "text"
|
35
|
+
s = Mournmail.to_utf8(body.decoded, charset)
|
36
|
+
if sub_type == "html"
|
37
|
+
"[0 text/html]\n" + Html2Text.convert(s)
|
38
|
+
else
|
39
|
+
s
|
40
|
+
end
|
35
41
|
else
|
36
42
|
type = Mail::Encodings.decode_encode(self["content-type"].to_s,
|
37
43
|
:decode) rescue
|
38
|
-
|
39
|
-
"[
|
44
|
+
"broken/type; error=\"#{$!} (#{$!.class})\""
|
45
|
+
"[0 #{type}]\n"
|
40
46
|
end + pgp_signature
|
41
47
|
end
|
42
48
|
|
49
|
+
def render_text(indices = [])
|
50
|
+
if HAVE_MAIL_GPG && encrypted?
|
51
|
+
mail = decrypt(verify: true)
|
52
|
+
return mail.render_text(indices)
|
53
|
+
end
|
54
|
+
if multipart?
|
55
|
+
parts.each_with_index.map { |part, i|
|
56
|
+
if sub_type == "alternative" && i > 0
|
57
|
+
""
|
58
|
+
else
|
59
|
+
part.render_text([*indices, i])
|
60
|
+
end
|
61
|
+
}.join("\n")
|
62
|
+
elsif main_type.nil? || main_type == "text"
|
63
|
+
s = Mournmail.to_utf8(body.decoded, charset)
|
64
|
+
if sub_type == "html"
|
65
|
+
Html2Text.convert(s)
|
66
|
+
else
|
67
|
+
s
|
68
|
+
end
|
69
|
+
else
|
70
|
+
""
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
43
74
|
def dig_part(i, *rest_indices)
|
44
75
|
if HAVE_MAIL_GPG && encrypted?
|
45
76
|
mail = decrypt(verify: true)
|
46
77
|
return mail.dig_part(i, *rest_indices)
|
47
78
|
end
|
48
|
-
if i ==
|
79
|
+
if i == 0
|
49
80
|
return self
|
50
81
|
end
|
51
|
-
part = parts[i]
|
82
|
+
part = parts[i - 1]
|
52
83
|
if rest_indices.empty?
|
53
84
|
part
|
54
85
|
else
|
@@ -79,12 +110,45 @@ module Mournmail
|
|
79
110
|
end
|
80
111
|
|
81
112
|
refine ::Mail::Part do
|
82
|
-
def render(indices)
|
83
|
-
index = indices.join(".")
|
113
|
+
def render(indices, no_content = false)
|
114
|
+
index = indices.map { |i| i + 1 }.join(".")
|
84
115
|
type = Mail::Encodings.decode_encode(self["content-type"].to_s,
|
85
116
|
:decode) rescue
|
86
117
|
"broken/type; error=\"#{$!} (#{$!.class})\""
|
87
|
-
"[#{index} #{type}]\n" +
|
118
|
+
"[#{index} #{type}]\n" +
|
119
|
+
render_content(indices, no_content)
|
120
|
+
end
|
121
|
+
|
122
|
+
def render_text(indices)
|
123
|
+
if multipart?
|
124
|
+
parts.each_with_index.map { |part, i|
|
125
|
+
if sub_type == "alternative" && i > 0
|
126
|
+
""
|
127
|
+
else
|
128
|
+
part.render_text([*indices, i])
|
129
|
+
end
|
130
|
+
}.join("\n")
|
131
|
+
else
|
132
|
+
if main_type == "message" && sub_type == "rfc822"
|
133
|
+
mail = Mail.new(body.raw_source)
|
134
|
+
mail.render_header(CONFIG[:mournmail_quote_header_fields]) +
|
135
|
+
"\n" + mail.render_text(indices)
|
136
|
+
elsif attachment?
|
137
|
+
""
|
138
|
+
else
|
139
|
+
if main_type == "text"
|
140
|
+
if sub_type == "html"
|
141
|
+
Html2Text.convert(decoded).sub(/(?<!\n)\z/, "\n")
|
142
|
+
else
|
143
|
+
decoded.sub(/(?<!\n)\z/, "\n").gsub(/\r\n/, "\n")
|
144
|
+
end
|
145
|
+
else
|
146
|
+
""
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
rescue => e
|
151
|
+
""
|
88
152
|
end
|
89
153
|
|
90
154
|
def dig_part(i, *rest_indices)
|
@@ -92,7 +156,7 @@ module Mournmail
|
|
92
156
|
mail = Mail.new(body.to_s)
|
93
157
|
mail.dig_part(i, *rest_indices)
|
94
158
|
else
|
95
|
-
part = parts[i]
|
159
|
+
part = parts[i - 1]
|
96
160
|
if rest_indices.empty?
|
97
161
|
part
|
98
162
|
else
|
@@ -103,21 +167,29 @@ module Mournmail
|
|
103
167
|
|
104
168
|
private
|
105
169
|
|
106
|
-
def render_content(indices)
|
170
|
+
def render_content(indices, no_content)
|
107
171
|
if multipart?
|
108
172
|
parts.each_with_index.map { |part, i|
|
109
|
-
part.render([*indices, i]
|
173
|
+
part.render([*indices, i],
|
174
|
+
no_content || sub_type == "alternative" && i > 0)
|
110
175
|
}.join
|
111
|
-
elsif main_type == "message" && sub_type == "rfc822"
|
112
|
-
mail = Mail.new(body.raw_source)
|
113
|
-
mail.render(indices)
|
114
|
-
elsif attachment?
|
115
|
-
""
|
116
176
|
else
|
117
|
-
|
118
|
-
|
119
|
-
|
177
|
+
return "" if no_content
|
178
|
+
if main_type == "message" && sub_type == "rfc822"
|
179
|
+
mail = Mail.new(body.raw_source)
|
180
|
+
mail.render(indices)
|
181
|
+
elsif attachment?
|
120
182
|
""
|
183
|
+
else
|
184
|
+
if main_type == "text"
|
185
|
+
if sub_type == "html"
|
186
|
+
Html2Text.convert(decoded).sub(/(?<!\n)\z/, "\n")
|
187
|
+
else
|
188
|
+
decoded.sub(/(?<!\n)\z/, "\n").gsub(/\r\n/, "\n")
|
189
|
+
end
|
190
|
+
else
|
191
|
+
""
|
192
|
+
end
|
121
193
|
end
|
122
194
|
end
|
123
195
|
rescue => e
|
data/lib/mournmail/summary.rb
CHANGED
@@ -141,7 +141,8 @@ module Mournmail
|
|
141
141
|
end
|
142
142
|
s = data[0].attr["BODY[]"]
|
143
143
|
mail = Mournmail.parse_mail(s)
|
144
|
-
|
144
|
+
spam_mailbox = Mournmail.account_config[:spam_mailbox]
|
145
|
+
if @mailbox != Net::IMAP.encode_utf7(spam_mailbox)
|
145
146
|
item.cache_id = Mournmail.write_mail_cache(s)
|
146
147
|
Mournmail.index_mail(item.cache_id, mail)
|
147
148
|
end
|
@@ -25,6 +25,7 @@ module Mournmail
|
|
25
25
|
SUMMARY_MODE_MAP.define_key("*t", :summary_mark_unflagged_command)
|
26
26
|
SUMMARY_MODE_MAP.define_key("y", :summary_archive_command)
|
27
27
|
SUMMARY_MODE_MAP.define_key("o", :summary_refile_command)
|
28
|
+
SUMMARY_MODE_MAP.define_key("!", :summary_refile_spam_command)
|
28
29
|
SUMMARY_MODE_MAP.define_key("p", :summary_prefetch_command)
|
29
30
|
SUMMARY_MODE_MAP.define_key("X", :summary_expunge_command)
|
30
31
|
SUMMARY_MODE_MAP.define_key("v", :summary_view_source_command)
|
@@ -33,6 +34,7 @@ module Mournmail
|
|
33
34
|
SUMMARY_MODE_MAP.define_key("k", :previous_line)
|
34
35
|
SUMMARY_MODE_MAP.define_key("j", :next_line)
|
35
36
|
SUMMARY_MODE_MAP.define_key("m", :mournmail_visit_mailbox)
|
37
|
+
SUMMARY_MODE_MAP.define_key("S", :mournmail_visit_spam_mailbox)
|
36
38
|
SUMMARY_MODE_MAP.define_key("/", :summary_search_command)
|
37
39
|
SUMMARY_MODE_MAP.define_key("t", :summary_show_thread_command)
|
38
40
|
SUMMARY_MODE_MAP.define_key("@", :summary_change_account_command)
|
@@ -98,7 +100,7 @@ module Mournmail
|
|
98
100
|
|reply_all = current_prefix_arg|
|
99
101
|
Mournmail.background do
|
100
102
|
mail = read_current_mail[0]
|
101
|
-
body = mail.
|
103
|
+
body = mail.render_text
|
102
104
|
foreground do
|
103
105
|
Window.current = Mournmail.message_window
|
104
106
|
Commands.mail(run_hooks: false)
|
@@ -137,6 +139,7 @@ module Mournmail
|
|
137
139
|
On #{mail['date']}
|
138
140
|
#{mail['from']} wrote:
|
139
141
|
EOF
|
142
|
+
Mournmail.insert_signature
|
140
143
|
exchange_point_and_mark
|
141
144
|
run_hooks(:mournmail_draft_setup_hook)
|
142
145
|
end
|
@@ -167,7 +170,7 @@ module Mournmail
|
|
167
170
|
|
168
171
|
define_local_command(:summary_toggle_deleted,
|
169
172
|
doc: <<~EOD) do
|
170
|
-
Toggle Deleted. Type `
|
173
|
+
Toggle Deleted. Type `X` to expunge deleted messages.
|
171
174
|
EOD
|
172
175
|
toggle_flag(selected_uid, :Deleted)
|
173
176
|
end
|
@@ -373,10 +376,23 @@ module Mournmail
|
|
373
376
|
end
|
374
377
|
end
|
375
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
|
+
|
376
388
|
define_local_command(:summary_prefetch,
|
377
389
|
doc: "Prefetch mails.") do
|
378
390
|
summary = Mournmail.current_summary
|
379
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
|
380
396
|
target_uids = @buffer.to_s.scan(/^ *\d+/).map { |s|
|
381
397
|
s.to_i
|
382
398
|
}.select { |uid|
|
@@ -394,9 +410,7 @@ module Mournmail
|
|
394
410
|
s = i.attr["BODY[]"]
|
395
411
|
if s
|
396
412
|
cache_id = Mournmail.write_mail_cache(s)
|
397
|
-
|
398
|
-
Mournmail.index_mail(cache_id, Mail.new(s))
|
399
|
-
end
|
413
|
+
Mournmail.index_mail(cache_id, Mail.new(s))
|
400
414
|
summary[uid].cache_id = cache_id
|
401
415
|
end
|
402
416
|
end
|
@@ -450,7 +464,7 @@ module Mournmail
|
|
450
464
|
|
451
465
|
define_local_command(:summary_change_account,
|
452
466
|
doc: "Change the current account.") do
|
453
|
-
|account = read_account_name("Change account: ")|
|
467
|
+
|account = Mournmail.read_account_name("Change account: ")|
|
454
468
|
unless CONFIG[:mournmail_accounts].key?(account)
|
455
469
|
raise EditorError, "No such account: #{account}"
|
456
470
|
end
|
@@ -651,13 +665,6 @@ module Mournmail
|
|
651
665
|
message
|
652
666
|
end
|
653
667
|
|
654
|
-
def read_account_name(prompt, **opts)
|
655
|
-
f = ->(s) {
|
656
|
-
complete_for_minibuffer(s, CONFIG[:mournmail_accounts].keys)
|
657
|
-
}
|
658
|
-
read_from_minibuffer(prompt, completion_proc: f, **opts)
|
659
|
-
end
|
660
|
-
|
661
668
|
def delete_from_summary(summary, uids, msg)
|
662
669
|
summary.delete_item_if do |item|
|
663
670
|
uids.include?(item.uid)
|
data/lib/mournmail/utils.rb
CHANGED
@@ -66,7 +66,9 @@ module Mournmail
|
|
66
66
|
define_variable :current_uid, attr: :accessor
|
67
67
|
define_variable :current_mail, attr: :accessor
|
68
68
|
define_variable :background_thread, attr: :accessor
|
69
|
+
define_variable :background_thread_mutex, initial_value: Mutex.new
|
69
70
|
define_variable :keep_alive_thread, attr: :accessor
|
71
|
+
define_variable :keep_alive_thread_mutex, initial_value: Mutex.new
|
70
72
|
define_variable :imap
|
71
73
|
define_variable :imap_mutex, initial_value: Mutex.new
|
72
74
|
define_variable :mailboxes, initial_value: []
|
@@ -75,42 +77,52 @@ module Mournmail
|
|
75
77
|
define_variable :groonga_db
|
76
78
|
|
77
79
|
def self.background(skip_if_busy: false)
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
self.background_thread = nil
|
80
|
+
@background_thread_mutex.synchronize do
|
81
|
+
if background_thread&.alive?
|
82
|
+
if skip_if_busy
|
83
|
+
return
|
84
|
+
else
|
85
|
+
raise EditorError, "Another background thread is running"
|
86
|
+
end
|
86
87
|
end
|
87
|
-
|
88
|
+
self.background_thread = Utils.background {
|
89
|
+
begin
|
90
|
+
yield
|
91
|
+
ensure
|
92
|
+
self.background_thread = nil
|
93
|
+
end
|
94
|
+
}
|
95
|
+
end
|
88
96
|
end
|
89
97
|
|
90
98
|
def self.start_keep_alive_thread
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
imap
|
99
|
+
@keep_alive_thread_mutex.synchronize do
|
100
|
+
if keep_alive_thread
|
101
|
+
raise EditorError, "Keep alive thread already running"
|
102
|
+
end
|
103
|
+
self.keep_alive_thread = Thread.start {
|
104
|
+
loop do
|
105
|
+
sleep(CONFIG[:mournmail_keep_alive_interval])
|
106
|
+
background(skip_if_busy: true) do
|
107
|
+
begin
|
108
|
+
imap_connect do |imap|
|
109
|
+
imap.noop
|
110
|
+
end
|
111
|
+
rescue => e
|
112
|
+
message("Error in IMAP NOOP: #{e.class}: #{e.message}")
|
101
113
|
end
|
102
|
-
rescue => e
|
103
|
-
message("Error in IMAP NOOP: #{e.class}: #{e.message}")
|
104
114
|
end
|
105
115
|
end
|
106
|
-
|
107
|
-
|
116
|
+
}
|
117
|
+
end
|
108
118
|
end
|
109
119
|
|
110
120
|
def self.stop_keep_alive_thread
|
111
|
-
|
112
|
-
keep_alive_thread
|
113
|
-
|
121
|
+
@keep_alive_thread_mutex.synchronize do
|
122
|
+
if keep_alive_thread
|
123
|
+
keep_alive_thread&.kill
|
124
|
+
self.keep_alive_thread = nil
|
125
|
+
end
|
114
126
|
end
|
115
127
|
end
|
116
128
|
|
@@ -213,29 +225,38 @@ module Mournmail
|
|
213
225
|
end
|
214
226
|
end
|
215
227
|
|
216
|
-
def self.google_access_token
|
217
|
-
auth_path = File.expand_path("cache/#{
|
228
|
+
def self.google_access_token(account = current_account)
|
229
|
+
auth_path = File.expand_path("cache/#{account}/google_auth.json",
|
218
230
|
CONFIG[:mournmail_directory])
|
219
231
|
FileUtils.mkdir_p(File.dirname(auth_path))
|
220
232
|
store = Google::APIClient::FileStore.new(auth_path)
|
221
233
|
storage = Google::APIClient::Storage.new(store)
|
222
234
|
storage.authorize
|
223
235
|
if storage.authorization.nil?
|
224
|
-
|
236
|
+
conf = CONFIG[:mournmail_accounts][account]
|
237
|
+
path = File.expand_path(conf[:client_secret_path])
|
225
238
|
client_secrets = Google::APIClient::ClientSecrets.load(path)
|
226
239
|
auth_client = client_secrets.to_authorization
|
227
240
|
auth_client.update!(
|
228
241
|
:scope => 'https://mail.google.com/',
|
229
242
|
:redirect_uri => 'urn:ietf:wg:oauth:2.0:oob'
|
230
243
|
)
|
231
|
-
|
232
244
|
auth_uri = auth_client.authorization_uri.to_s
|
233
|
-
Launchy.open(auth_uri)
|
234
|
-
|
235
245
|
auth_client.code = foreground! {
|
236
|
-
|
237
|
-
|
238
|
-
|
246
|
+
begin
|
247
|
+
Launchy.open(auth_uri)
|
248
|
+
rescue Launchy::CommandNotFoundError
|
249
|
+
buffer = show_google_auth_uri(auth_uri)
|
250
|
+
end
|
251
|
+
begin
|
252
|
+
Window.echo_area.clear_message
|
253
|
+
Window.redisplay
|
254
|
+
read_from_minibuffer("Code: ").chomp
|
255
|
+
ensure
|
256
|
+
if buffer
|
257
|
+
kill_buffer(buffer, force: true)
|
258
|
+
end
|
259
|
+
end
|
239
260
|
}
|
240
261
|
auth_client.fetch_access_token!
|
241
262
|
old_umask = File.umask(077)
|
@@ -249,6 +270,23 @@ module Mournmail
|
|
249
270
|
end
|
250
271
|
auth_client.access_token
|
251
272
|
end
|
273
|
+
|
274
|
+
def self.show_google_auth_uri(auth_uri)
|
275
|
+
buffer = Buffer.find_or_new("*message*",
|
276
|
+
undo_limit: 0, read_only: true)
|
277
|
+
buffer.apply_mode(Mournmail::MessageMode)
|
278
|
+
buffer.read_only_edit do
|
279
|
+
buffer.clear
|
280
|
+
buffer.insert(<<~EOF)
|
281
|
+
Open the following URI in your browser and type obtained code:
|
282
|
+
|
283
|
+
#{auth_uri}
|
284
|
+
EOF
|
285
|
+
end
|
286
|
+
window = Mournmail.message_window
|
287
|
+
window.buffer = buffer
|
288
|
+
buffer
|
289
|
+
end
|
252
290
|
|
253
291
|
def self.xoauth2_string(user, access_token)
|
254
292
|
"user=#{user}\1auth=Bearer #{access_token}\1\1"
|
@@ -345,7 +383,8 @@ module Mournmail
|
|
345
383
|
dir = File.dirname(path)
|
346
384
|
base = File.basename(path)
|
347
385
|
begin
|
348
|
-
f = Tempfile.create(["#{base}-", ".tmp"], dir
|
386
|
+
f = Tempfile.create(["#{base}-", ".tmp"], dir,
|
387
|
+
external_encoding: "ASCII-8BIT", binmode: true)
|
349
388
|
begin
|
350
389
|
f.write(s)
|
351
390
|
ensure
|
@@ -517,4 +556,24 @@ module Mournmail
|
|
517
556
|
def self.parse_mail(s)
|
518
557
|
Mail.new(s.scrub("??"))
|
519
558
|
end
|
559
|
+
|
560
|
+
def self.read_account_name(prompt, **opts)
|
561
|
+
f = ->(s) {
|
562
|
+
complete_for_minibuffer(s, CONFIG[:mournmail_accounts].keys)
|
563
|
+
}
|
564
|
+
read_from_minibuffer(prompt, completion_proc: f, **opts)
|
565
|
+
end
|
566
|
+
|
567
|
+
def self.insert_signature
|
568
|
+
account = Buffer.current[:mournmail_delivery_account] ||
|
569
|
+
Mournmail.current_account
|
570
|
+
signature = CONFIG[:mournmail_accounts][account][:signature]
|
571
|
+
if signature
|
572
|
+
Buffer.current.save_excursion do
|
573
|
+
end_of_buffer
|
574
|
+
insert("\n")
|
575
|
+
insert(signature)
|
576
|
+
end
|
577
|
+
end
|
578
|
+
end
|
520
579
|
end
|
data/lib/mournmail/version.rb
CHANGED
data/mournmail.gemspec
CHANGED
@@ -22,12 +22,15 @@ Gem::Specification.new do |spec|
|
|
22
22
|
spec.require_paths = ["lib"]
|
23
23
|
|
24
24
|
spec.add_runtime_dependency "textbringer"
|
25
|
+
spec.add_runtime_dependency "net-smtp"
|
26
|
+
spec.add_runtime_dependency "net-imap"
|
25
27
|
spec.add_runtime_dependency "mail"
|
26
28
|
spec.add_runtime_dependency "mime-types"
|
27
29
|
spec.add_runtime_dependency "rroonga"
|
28
30
|
spec.add_runtime_dependency "google-api-client"
|
29
31
|
spec.add_runtime_dependency "launchy"
|
32
|
+
spec.add_runtime_dependency "html2text"
|
30
33
|
|
31
34
|
spec.add_development_dependency "bundler"
|
32
|
-
spec.add_development_dependency "rake", "
|
35
|
+
spec.add_development_dependency "rake", ">= 12.0"
|
33
36
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mournmail
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shugo Maeda
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-01-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: textbringer
|
@@ -24,6 +24,34 @@ dependencies:
|
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: net-smtp
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: net-imap
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
27
55
|
- !ruby/object:Gem::Dependency
|
28
56
|
name: mail
|
29
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -94,6 +122,20 @@ dependencies:
|
|
94
122
|
- - ">="
|
95
123
|
- !ruby/object:Gem::Version
|
96
124
|
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: html2text
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :runtime
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
97
139
|
- !ruby/object:Gem::Dependency
|
98
140
|
name: bundler
|
99
141
|
requirement: !ruby/object:Gem::Requirement
|
@@ -112,20 +154,21 @@ dependencies:
|
|
112
154
|
name: rake
|
113
155
|
requirement: !ruby/object:Gem::Requirement
|
114
156
|
requirements:
|
115
|
-
- - "
|
157
|
+
- - ">="
|
116
158
|
- !ruby/object:Gem::Version
|
117
159
|
version: '12.0'
|
118
160
|
type: :development
|
119
161
|
prerelease: false
|
120
162
|
version_requirements: !ruby/object:Gem::Requirement
|
121
163
|
requirements:
|
122
|
-
- - "
|
164
|
+
- - ">="
|
123
165
|
- !ruby/object:Gem::Version
|
124
166
|
version: '12.0'
|
125
167
|
description: A message user agent for Textbringer.
|
126
168
|
email:
|
127
169
|
- shugo@ruby-lang.org
|
128
|
-
executables:
|
170
|
+
executables:
|
171
|
+
- mournmail_reindex
|
129
172
|
extensions: []
|
130
173
|
extra_rdoc_files: []
|
131
174
|
files:
|
@@ -136,6 +179,7 @@ files:
|
|
136
179
|
- Rakefile
|
137
180
|
- bin/console
|
138
181
|
- bin/setup
|
182
|
+
- exe/mournmail_reindex
|
139
183
|
- lib/mournmail.rb
|
140
184
|
- lib/mournmail/commands.rb
|
141
185
|
- lib/mournmail/config.rb
|
@@ -155,7 +199,7 @@ homepage: https://github.com/shugo/mournmail
|
|
155
199
|
licenses:
|
156
200
|
- MIT
|
157
201
|
metadata: {}
|
158
|
-
post_install_message:
|
202
|
+
post_install_message:
|
159
203
|
rdoc_options: []
|
160
204
|
require_paths:
|
161
205
|
- lib
|
@@ -170,8 +214,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
170
214
|
- !ruby/object:Gem::Version
|
171
215
|
version: '0'
|
172
216
|
requirements: []
|
173
|
-
rubygems_version: 3.
|
174
|
-
signing_key:
|
217
|
+
rubygems_version: 3.4.0.dev
|
218
|
+
signing_key:
|
175
219
|
specification_version: 4
|
176
220
|
summary: A message user agent for Textbringer.
|
177
221
|
test_files: []
|