mournmail 0.3.0 → 1.0.1
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 +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: []
|