mournmail 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/mournmail.rb +0 -2
- data/lib/mournmail/commands.rb +7 -26
- data/lib/mournmail/config.rb +11 -4
- data/lib/mournmail/draft_mode.rb +21 -11
- data/lib/mournmail/faces.rb +0 -2
- data/lib/mournmail/mail_encoded_word_patch.rb +1 -3
- data/lib/mournmail/message_mode.rb +42 -8
- data/lib/mournmail/message_rendering.rb +14 -5
- data/lib/mournmail/search_result_mode.rb +3 -4
- data/lib/mournmail/summary.rb +33 -18
- data/lib/mournmail/summary_mode.rb +23 -19
- data/lib/mournmail/utils.rb +141 -28
- data/lib/mournmail/version.rb +1 -3
- data/lib/textbringer_plugin.rb +0 -2
- data/mournmail.gemspec +5 -2
- metadata +51 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f3e35ed64af5fb25eeedad4c3c901b93125baf6b561b8e2384da7a9ed602b367
|
4
|
+
data.tar.gz: 9174888f40a57d0cffe48d13fef7cc0d2892be312d3ff0b95a0df7aef098c726
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f85ebaadd8f97a43e45856347ad0149594ab17db31c04ba66d85db61b336f3c7cd4548c725868a44522d87941297edc5ad5656901440400f834017ed2e650733
|
7
|
+
data.tar.gz: ddff53280fad3432e0bf0da11cf58c57a60586886828fa512b1bf54cba8e57afc8e2687561ae8b289d95fde271b1679f7da4de56e451b4fd6c00ec9787ccd095
|
data/lib/mournmail.rb
CHANGED
data/lib/mournmail/commands.rb
CHANGED
@@ -1,13 +1,14 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
define_command(:mournmail, doc: "Start mournmail.") do
|
4
2
|
Mournmail.open_groonga_db
|
5
3
|
mournmail_visit_mailbox("INBOX")
|
6
4
|
end
|
7
5
|
|
8
|
-
define_command(:mournmail_visit_mailbox, doc: "
|
6
|
+
define_command(:mournmail_visit_mailbox, doc: "Visit mailbox") do
|
9
7
|
|mailbox = Mournmail.read_mailbox_name("Visit mailbox: ", default: "INBOX")|
|
10
|
-
|
8
|
+
summary = Mournmail::Summary.load_or_new(mailbox)
|
9
|
+
foreground do
|
10
|
+
Mournmail.show_summary(summary)
|
11
|
+
end
|
11
12
|
end
|
12
13
|
|
13
14
|
define_command(:mournmail_summary_sync, doc: "Sync summary.") do
|
@@ -16,30 +17,10 @@ define_command(:mournmail_summary_sync, doc: "Sync summary.") do
|
|
16
17
|
message("Syncing #{mailbox} in background...")
|
17
18
|
Mournmail.background do
|
18
19
|
summary = Mournmail.fetch_summary(mailbox, all: all)
|
19
|
-
summary_text = summary.to_s
|
20
20
|
summary.save
|
21
|
-
|
22
|
-
|
23
|
-
read_only: true)
|
24
|
-
buffer.apply_mode(Mournmail::SummaryMode)
|
25
|
-
buffer.read_only_edit do
|
26
|
-
buffer.clear
|
27
|
-
buffer.insert(summary_text)
|
28
|
-
end
|
29
|
-
switch_to_buffer(buffer)
|
30
|
-
Mournmail.current_mailbox = mailbox
|
31
|
-
Mournmail.current_summary = summary
|
32
|
-
Mournmail.current_mail = nil
|
33
|
-
Mournmail.current_uid = nil
|
21
|
+
foreground do
|
22
|
+
Mournmail.show_summary(summary)
|
34
23
|
message("Syncing #{mailbox} in background... Done")
|
35
|
-
begin
|
36
|
-
buffer.beginning_of_buffer
|
37
|
-
buffer.re_search_forward(/^ *\d+ u/)
|
38
|
-
rescue SearchError
|
39
|
-
buffer.end_of_buffer
|
40
|
-
buffer.re_search_backward(/^ *\d+ /, raise_error: false)
|
41
|
-
end
|
42
|
-
summary_read_command
|
43
24
|
end
|
44
25
|
end
|
45
26
|
end
|
data/lib/mournmail/config.rb
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
module Textbringer
|
4
2
|
CONFIG[:mournmail_directory] = File.expand_path("~/.mournmail")
|
5
3
|
CONFIG[:mournmail_accounts] = {}
|
@@ -18,7 +16,16 @@ module Textbringer
|
|
18
16
|
]
|
19
17
|
CONFIG[:mournmail_imap_connect_timeout] = 10
|
20
18
|
CONFIG[:mournmail_keep_alive_interval] = 60
|
21
|
-
|
22
|
-
|
19
|
+
case RUBY_PLATFORM
|
20
|
+
when /mswin|mingw/
|
21
|
+
CONFIG[:mournmail_file_open_comamnd] = "start"
|
22
|
+
CONFIG[:mournmail_link_open_comamnd] = "start"
|
23
|
+
when /darwin/
|
24
|
+
CONFIG[:mournmail_file_open_comamnd] = "open"
|
25
|
+
CONFIG[:mournmail_link_open_comamnd] = "open"
|
26
|
+
else
|
27
|
+
CONFIG[:mournmail_file_open_comamnd] = "xdg-open"
|
28
|
+
CONFIG[:mournmail_link_open_comamnd] = "xdg-open"
|
29
|
+
end
|
23
30
|
CONFIG[:mournmail_addresses_path] = File.expand_path("~/.addresses")
|
24
31
|
end
|
data/lib/mournmail/draft_mode.rb
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
module Mournmail
|
4
2
|
class DraftMode < Textbringer::Mode
|
5
3
|
MAIL_MODE_MAP = Keymap.new
|
@@ -70,10 +68,15 @@ module Mournmail
|
|
70
68
|
encoding: "binary")
|
71
69
|
end
|
72
70
|
conf = Mournmail.account_config
|
71
|
+
options = @buffer[:mournmail_delivery_options] ||
|
72
|
+
conf[:delivery_options]
|
73
|
+
if options[:authentication] == "gmail"
|
74
|
+
options = options.merge(authentication: "xoauth2",
|
75
|
+
password: Mournmail.google_access_token)
|
76
|
+
end
|
73
77
|
m.delivery_method(@buffer[:mournmail_delivery_method] ||
|
74
78
|
conf[:delivery_method],
|
75
|
-
|
76
|
-
conf[:delivery_options])
|
79
|
+
options)
|
77
80
|
bury_buffer(@buffer)
|
78
81
|
background do
|
79
82
|
begin
|
@@ -89,9 +92,11 @@ module Mournmail
|
|
89
92
|
m.gpg(sign: pgp_sign, encrypt: pgp_encrypt)
|
90
93
|
end
|
91
94
|
m.deliver
|
92
|
-
|
95
|
+
foreground do
|
93
96
|
message("Mail sent.")
|
94
97
|
end
|
98
|
+
cache_id = Mournmail.write_mail_cache(m.encoded)
|
99
|
+
Mournmail.index_mail(cache_id, m)
|
95
100
|
outbox = Mournmail.account_config[:outbox_mailbox]
|
96
101
|
if outbox
|
97
102
|
Mournmail.imap_connect do |imap|
|
@@ -101,12 +106,12 @@ module Mournmail
|
|
101
106
|
imap.append(outbox, m.to_s, [:Seen])
|
102
107
|
end
|
103
108
|
end
|
104
|
-
|
109
|
+
foreground do
|
105
110
|
kill_buffer(@buffer, force: true)
|
106
111
|
Mournmail.back_to_summary
|
107
112
|
end
|
108
113
|
rescue Exception
|
109
|
-
|
114
|
+
foreground do
|
110
115
|
switch_to_buffer(@buffer)
|
111
116
|
end
|
112
117
|
raise
|
@@ -141,10 +146,15 @@ module Mournmail
|
|
141
146
|
start_pos = @buffer.point
|
142
147
|
s = @buffer.substring(start_pos, end_pos)
|
143
148
|
if !s.empty?
|
144
|
-
re = /^(
|
145
|
-
|
146
|
-
|
147
|
-
|
149
|
+
re = /^(?:.*")?#{Regexp.quote(s)}.*/
|
150
|
+
addrs = File.read(CONFIG[:mournmail_addresses_path])
|
151
|
+
.scan(re).map { |line| line.slice(/^\S+/) }
|
152
|
+
if !addrs.empty?
|
153
|
+
addr = addrs.inject { |x, y|
|
154
|
+
x.chars.zip(y.chars).take_while { |i, j|
|
155
|
+
i == j
|
156
|
+
}.map { |i,| i }.join
|
157
|
+
}
|
148
158
|
@buffer.delete_region(start_pos, end_pos)
|
149
159
|
@buffer.insert(addr)
|
150
160
|
else
|
data/lib/mournmail/faces.rb
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
require "mail"
|
4
2
|
|
5
3
|
module Mournmail
|
@@ -22,7 +20,7 @@ module Mournmail
|
|
22
20
|
b_encoding_extra_size = "=?#{charset}?B??=".bytesize
|
23
21
|
while !words.empty?
|
24
22
|
limit = 78 - prepend
|
25
|
-
line =
|
23
|
+
line = +""
|
26
24
|
fold_line = false
|
27
25
|
while !fold_line && !words.empty?
|
28
26
|
word = words.first
|
@@ -1,4 +1,5 @@
|
|
1
|
-
|
1
|
+
require "uri"
|
2
|
+
require "mime/types"
|
2
3
|
|
3
4
|
using Mournmail::MessageRendering
|
4
5
|
|
@@ -7,14 +8,17 @@ module Mournmail
|
|
7
8
|
MESSAGE_MODE_MAP = Keymap.new
|
8
9
|
MESSAGE_MODE_MAP.define_key("\C-m", :message_open_link_or_part_command)
|
9
10
|
MESSAGE_MODE_MAP.define_key("s", :message_save_part_command)
|
11
|
+
MESSAGE_MODE_MAP.define_key("\t", :message_next_link_or_part_command)
|
10
12
|
|
11
13
|
# See http://nihongo.jp/support/mail_guide/dev_guide.txt
|
12
|
-
|
14
|
+
MAILTO_REGEXP = URI.regexp("mailto")
|
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
|
+
URI_OR_MIME_REGEXP = /#{URI_REGEXP}|#{MIME_REGEXP}/
|
13
18
|
|
14
19
|
define_syntax :field_name, /^[A-Za-z\-]+: /
|
15
20
|
define_syntax :quotation, /^>.*/
|
16
|
-
define_syntax :mime_part,
|
17
|
-
/^\[(([0-9.]+) [A-Za-z._\-]+\/[A-Za-z._\-]+.*|PGP\/MIME .*)\]$/
|
21
|
+
define_syntax :mime_part, MIME_REGEXP
|
18
22
|
define_syntax :link, URI_REGEXP
|
19
23
|
|
20
24
|
def initialize(buffer)
|
@@ -46,12 +50,21 @@ module Mournmail
|
|
46
50
|
end
|
47
51
|
end
|
48
52
|
|
53
|
+
define_local_command(:message_next_link_or_part,
|
54
|
+
doc: "Go to the next link or MIME part.") do
|
55
|
+
if @buffer.re_search_forward(URI_OR_MIME_REGEXP, raise_error: false)
|
56
|
+
goto_char(@buffer.match_beginning(0))
|
57
|
+
else
|
58
|
+
@buffer.beginning_of_buffer
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
49
62
|
private
|
50
63
|
|
51
64
|
def current_part
|
52
65
|
@buffer.save_excursion do
|
53
66
|
@buffer.beginning_of_line
|
54
|
-
if @buffer.looking_at?(/\[([0-9.]+) .*\]/)
|
67
|
+
if @buffer.looking_at?(/\[([\-0-9.]+) .*\]/)
|
55
68
|
index = match_string(1)
|
56
69
|
indices = index.split(".").map(&:to_i)
|
57
70
|
@buffer[:mournmail_mail].dig_part(*indices)
|
@@ -75,7 +88,12 @@ module Mournmail
|
|
75
88
|
end
|
76
89
|
|
77
90
|
def part_default_file_name(part)
|
78
|
-
base_name =
|
91
|
+
base_name =
|
92
|
+
begin
|
93
|
+
part.cid.gsub(/[^A-Za-z0-9_\-]/, "_")
|
94
|
+
rescue NoMethodError
|
95
|
+
"mournmail"
|
96
|
+
end
|
79
97
|
ext = part_extension(part)
|
80
98
|
if ext
|
81
99
|
base_name + "." + ext
|
@@ -126,8 +144,24 @@ module Mournmail
|
|
126
144
|
end
|
127
145
|
|
128
146
|
def open_uri(uri)
|
129
|
-
|
130
|
-
|
147
|
+
case uri
|
148
|
+
when /\Amailto:/
|
149
|
+
u = URI.parse(uri)
|
150
|
+
if u.headers.assoc("subject")
|
151
|
+
re = /^To:\s*\nSubject:\s*\n/
|
152
|
+
else
|
153
|
+
re = /^To:\s*\n/
|
154
|
+
end
|
155
|
+
Commands.mail
|
156
|
+
beginning_of_buffer
|
157
|
+
re_search_forward(re)
|
158
|
+
replace_match("")
|
159
|
+
insert u.to_mailtext.sub(/\n\n\z/, "")
|
160
|
+
end_of_buffer
|
161
|
+
else
|
162
|
+
system(*CONFIG[:mournmail_link_open_comamnd], uri,
|
163
|
+
out: File::NULL, err: File::NULL)
|
164
|
+
end
|
131
165
|
end
|
132
166
|
end
|
133
167
|
end
|
@@ -1,5 +1,3 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
require "mail"
|
4
2
|
|
5
3
|
module Mournmail
|
@@ -31,9 +29,14 @@ module Mournmail
|
|
31
29
|
parts.each_with_index.map { |part, i|
|
32
30
|
part.render([*indices, i])
|
33
31
|
}.join
|
34
|
-
|
32
|
+
elsif main_type.nil? || (main_type == "text" && sub_type == "plain")
|
35
33
|
s = body.decoded
|
36
|
-
Mournmail.to_utf8(s, charset)
|
34
|
+
Mournmail.to_utf8(s, charset)
|
35
|
+
else
|
36
|
+
type = Mail::Encodings.decode_encode(self["content-type"].to_s,
|
37
|
+
:decode) rescue
|
38
|
+
"broken/type; error=\"#{$!} (#{$!.class})\""
|
39
|
+
"[-1 #{type}]\n"
|
37
40
|
end + pgp_signature
|
38
41
|
end
|
39
42
|
|
@@ -42,6 +45,9 @@ module Mournmail
|
|
42
45
|
mail = decrypt(verify: true)
|
43
46
|
return mail.dig_part(i, *rest_indices)
|
44
47
|
end
|
48
|
+
if i == -1
|
49
|
+
return self
|
50
|
+
end
|
45
51
|
part = parts[i]
|
46
52
|
if rest_indices.empty?
|
47
53
|
part
|
@@ -76,7 +82,8 @@ module Mournmail
|
|
76
82
|
def render(indices)
|
77
83
|
index = indices.join(".")
|
78
84
|
type = Mail::Encodings.decode_encode(self["content-type"].to_s,
|
79
|
-
:decode)
|
85
|
+
:decode) rescue
|
86
|
+
"broken/type; error=\"#{$!} (#{$!.class})\""
|
80
87
|
"[#{index} #{type}]\n" + render_content(indices)
|
81
88
|
end
|
82
89
|
|
@@ -113,6 +120,8 @@ module Mournmail
|
|
113
120
|
""
|
114
121
|
end
|
115
122
|
end
|
123
|
+
rescue => e
|
124
|
+
"Broken part: #{e} (#{e.class})"
|
116
125
|
end
|
117
126
|
end
|
118
127
|
end
|
@@ -1,5 +1,3 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
using Mournmail::MessageRendering
|
4
2
|
|
5
3
|
module Mournmail
|
@@ -21,6 +19,7 @@ module Mournmail
|
|
21
19
|
SEARCH_RESULT_MODE_MAP.define_key(">", :next_page_command)
|
22
20
|
SEARCH_RESULT_MODE_MAP.define_key("/", :summary_search_command)
|
23
21
|
SEARCH_RESULT_MODE_MAP.define_key("t", :summary_show_thread_command)
|
22
|
+
SEARCH_RESULT_MODE_MAP.define_key("@", :summary_change_account_command)
|
24
23
|
|
25
24
|
def initialize(buffer)
|
26
25
|
super(buffer)
|
@@ -32,11 +31,11 @@ module Mournmail
|
|
32
31
|
return if num.nil?
|
33
32
|
Mournmail.background do
|
34
33
|
message = @buffer[:messages][num]
|
35
|
-
if message.nil?
|
34
|
+
if message.nil? || message._key.nil?
|
36
35
|
raise EditorError, "No message found"
|
37
36
|
end
|
38
37
|
mail = Mail.new(Mournmail.read_mail_cache(message._key))
|
39
|
-
|
38
|
+
foreground do
|
40
39
|
show_message(mail)
|
41
40
|
@buffer[:message_number] = num
|
42
41
|
end
|
data/lib/mournmail/summary.rb
CHANGED
@@ -1,12 +1,10 @@
|
|
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
|
10
8
|
attr_accessor :uidvalidity
|
11
9
|
|
12
10
|
include MonitorMixin
|
@@ -34,6 +32,10 @@ module Mournmail
|
|
34
32
|
cache_path(mailbox) + ".tmp"
|
35
33
|
end
|
36
34
|
|
35
|
+
def self.cache_old_path(mailbox)
|
36
|
+
cache_path(mailbox) + ".old"
|
37
|
+
end
|
38
|
+
|
37
39
|
def self.load(mailbox)
|
38
40
|
lock_cache(mailbox, :shared) do
|
39
41
|
File.open(cache_path(mailbox)) do |f|
|
@@ -82,12 +84,14 @@ module Mournmail
|
|
82
84
|
|
83
85
|
def add_item(item, message_id, in_reply_to)
|
84
86
|
synchronize do
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
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)
|
91
95
|
if message_id
|
92
96
|
@message_id_table[message_id] = item
|
93
97
|
end
|
@@ -115,22 +119,28 @@ module Mournmail
|
|
115
119
|
end
|
116
120
|
end
|
117
121
|
|
122
|
+
def uids
|
123
|
+
synchronize do
|
124
|
+
@uid_table.keys
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
118
128
|
def read_mail(uid)
|
119
129
|
synchronize do
|
120
130
|
item = @uid_table[uid]
|
121
131
|
if item.cache_id
|
122
132
|
File.open(Mournmail.mail_cache_path(item.cache_id)) do |f|
|
123
|
-
[
|
133
|
+
[Mournmail.parse_mail(f.read), false]
|
124
134
|
end
|
125
135
|
else
|
126
136
|
Mournmail.imap_connect do |imap|
|
127
137
|
imap.select(@mailbox)
|
128
138
|
data = imap.uid_fetch(uid, "BODY[]")
|
129
|
-
if data.empty?
|
139
|
+
if data.nil? || data.empty?
|
130
140
|
raise EditorError, "No such mail: #{uid}"
|
131
141
|
end
|
132
142
|
s = data[0].attr["BODY[]"]
|
133
|
-
mail =
|
143
|
+
mail = Mournmail.parse_mail(s)
|
134
144
|
if @mailbox != Mournmail.account_config[:spam_mailbox]
|
135
145
|
item.cache_id = Mournmail.write_mail_cache(s)
|
136
146
|
Mournmail.index_mail(item.cache_id, mail)
|
@@ -148,9 +158,14 @@ module Mournmail
|
|
148
158
|
Summary.lock_cache(@mailbox, :exclusive) do
|
149
159
|
cache_path = Summary.cache_path(@mailbox)
|
150
160
|
tmp_path = Summary.cache_tmp_path(@mailbox)
|
161
|
+
old_path = Summary.cache_old_path(@mailbox)
|
151
162
|
File.open(tmp_path, "w", 0600) do |f|
|
152
163
|
Marshal.dump(self, f)
|
153
164
|
end
|
165
|
+
begin
|
166
|
+
File.rename(cache_path, old_path)
|
167
|
+
rescue Errno::ENOENT
|
168
|
+
end
|
154
169
|
File.rename(tmp_path, cache_path)
|
155
170
|
end
|
156
171
|
end
|
@@ -158,7 +173,7 @@ module Mournmail
|
|
158
173
|
|
159
174
|
def to_s
|
160
175
|
synchronize do
|
161
|
-
items.each_with_object(
|
176
|
+
items.each_with_object(+"") do |item, s|
|
162
177
|
s << item.to_s
|
163
178
|
end
|
164
179
|
end
|
@@ -232,7 +247,7 @@ module Mournmail
|
|
232
247
|
|
233
248
|
def format_line(limit = 78, from_limit = 16, level = 0)
|
234
249
|
space = " " * (level < 8 ? level : 8)
|
235
|
-
s =
|
250
|
+
s = +""
|
236
251
|
s << format("%6d %s%s %s[ %s ] ",
|
237
252
|
@uid, format_flags(@flags), format_date(@date), space,
|
238
253
|
ljust(format_from(@from), from_limit))
|
@@ -243,7 +258,7 @@ module Mournmail
|
|
243
258
|
|
244
259
|
def ljust(s, n)
|
245
260
|
width = 0
|
246
|
-
str =
|
261
|
+
str = +""
|
247
262
|
s.each_char do |c|
|
248
263
|
w = Buffer.display_width(c)
|
249
264
|
width += w
|
@@ -258,14 +273,14 @@ module Mournmail
|
|
258
273
|
end
|
259
274
|
|
260
275
|
def format_flags(flags)
|
261
|
-
if flags.include?(:
|
276
|
+
if !flags.include?(:Seen)
|
277
|
+
"u"
|
278
|
+
elsif flags.include?(:Deleted)
|
262
279
|
"d"
|
263
280
|
elsif flags.include?(:Flagged)
|
264
281
|
"$"
|
265
282
|
elsif flags.include?(:Answered)
|
266
283
|
"a"
|
267
|
-
elsif !flags.include?(:Seen)
|
268
|
-
"u"
|
269
284
|
else
|
270
285
|
" "
|
271
286
|
end
|
@@ -1,5 +1,3 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
using Mournmail::MessageRendering
|
4
2
|
|
5
3
|
module Mournmail
|
@@ -56,9 +54,9 @@ module Mournmail
|
|
56
54
|
summary = Mournmail.current_summary
|
57
55
|
Mournmail.background do
|
58
56
|
mail, fetched = summary.read_mail(uid)
|
59
|
-
|
57
|
+
foreground do
|
60
58
|
show_message(mail)
|
61
|
-
mark_as_seen(uid,
|
59
|
+
mark_as_seen(uid, false)
|
62
60
|
Mournmail.current_uid = uid
|
63
61
|
Mournmail.current_mail = mail
|
64
62
|
end
|
@@ -101,7 +99,7 @@ module Mournmail
|
|
101
99
|
Mournmail.background do
|
102
100
|
mail = read_current_mail[0]
|
103
101
|
body = mail.render_body
|
104
|
-
|
102
|
+
foreground do
|
105
103
|
Window.current = Mournmail.message_window
|
106
104
|
Commands.mail(run_hooks: false)
|
107
105
|
if reply_all
|
@@ -243,7 +241,7 @@ module Mournmail
|
|
243
241
|
end
|
244
242
|
summary_text = summary.to_s
|
245
243
|
summary.save
|
246
|
-
|
244
|
+
foreground do
|
247
245
|
buffer.read_only_edit do
|
248
246
|
buffer.clear
|
249
247
|
buffer.insert(summary_text)
|
@@ -258,7 +256,7 @@ module Mournmail
|
|
258
256
|
uid = selected_uid
|
259
257
|
Mournmail.background do
|
260
258
|
mail, = read_current_mail
|
261
|
-
|
259
|
+
foreground do
|
262
260
|
source_buffer = Buffer.find_or_new("*message-source*",
|
263
261
|
file_encoding: "ascii-8bit",
|
264
262
|
undo_limit: 0, read_only: true)
|
@@ -305,7 +303,7 @@ module Mournmail
|
|
305
303
|
end
|
306
304
|
s = mails.map { |mail| mail.body.decoded }.join
|
307
305
|
mail = Mail.new(s)
|
308
|
-
|
306
|
+
foreground do
|
309
307
|
show_message(mail)
|
310
308
|
Mournmail.current_uid = nil
|
311
309
|
Mournmail.current_mail = mail
|
@@ -363,7 +361,7 @@ module Mournmail
|
|
363
361
|
Mournmail.background do
|
364
362
|
Mournmail.imap_connect do |imap|
|
365
363
|
unless imap.list("", mailbox)
|
366
|
-
if
|
364
|
+
if foreground! { yes_or_no?("#{mailbox} doesn't exist; Create?") }
|
367
365
|
imap.create(mailbox)
|
368
366
|
else
|
369
367
|
next
|
@@ -391,7 +389,7 @@ module Mournmail
|
|
391
389
|
begin
|
392
390
|
target_uids.each_slice(20) do |uids|
|
393
391
|
data = imap.uid_fetch(uids, "BODY[]")
|
394
|
-
data
|
392
|
+
data&.each do |i|
|
395
393
|
uid = i.attr["UID"]
|
396
394
|
s = i.attr["BODY[]"]
|
397
395
|
if s
|
@@ -404,7 +402,7 @@ module Mournmail
|
|
404
402
|
end
|
405
403
|
count += uids.size
|
406
404
|
progress = (count.to_f * 100 / target_uids.size).round
|
407
|
-
|
405
|
+
foreground do
|
408
406
|
message("Prefetching mails... #{progress}%", log: false)
|
409
407
|
end
|
410
408
|
end
|
@@ -412,7 +410,7 @@ module Mournmail
|
|
412
410
|
summary.save
|
413
411
|
end
|
414
412
|
end
|
415
|
-
|
413
|
+
foreground do
|
416
414
|
message("Done")
|
417
415
|
end
|
418
416
|
end
|
@@ -428,7 +426,7 @@ module Mournmail
|
|
428
426
|
match_record.subject | match_record.body
|
429
427
|
}
|
430
428
|
}.paginate([["date", :desc]], page: page, size: 100)
|
431
|
-
|
429
|
+
foreground do
|
432
430
|
show_search_result(messages, query: query)
|
433
431
|
message("Searched (#{messages.current_page}/#{messages.n_pages})")
|
434
432
|
end
|
@@ -442,7 +440,7 @@ module Mournmail
|
|
442
440
|
messages = Groonga["Messages"].select { |m|
|
443
441
|
m.thread_id == message.thread_id
|
444
442
|
}.sort([["date", :asc]])
|
445
|
-
|
443
|
+
foreground do
|
446
444
|
show_search_result(messages, buffer_name: "*thread*")
|
447
445
|
i = messages.find_index { |m| m._key == message._key }
|
448
446
|
Buffer.current.goto_line(i + 1)
|
@@ -453,6 +451,12 @@ module Mournmail
|
|
453
451
|
define_local_command(:summary_change_account,
|
454
452
|
doc: "Change the current account.") do
|
455
453
|
|account = read_account_name("Change account: ")|
|
454
|
+
unless CONFIG[:mournmail_accounts].key?(account)
|
455
|
+
raise EditorError, "No such account: #{account}"
|
456
|
+
end
|
457
|
+
if Mournmail.background_thread
|
458
|
+
raise EditorError, "Background thread is running"
|
459
|
+
end
|
456
460
|
mournmail_quit
|
457
461
|
Mournmail.current_account = account
|
458
462
|
mournmail
|
@@ -534,7 +538,7 @@ module Mournmail
|
|
534
538
|
Mournmail.background do
|
535
539
|
summary_item.toggle_flag(flag)
|
536
540
|
Mournmail.current_summary.save
|
537
|
-
|
541
|
+
foreground do
|
538
542
|
update_flags(summary_item)
|
539
543
|
end
|
540
544
|
end
|
@@ -596,7 +600,7 @@ module Mournmail
|
|
596
600
|
|
597
601
|
def ljust(s, n)
|
598
602
|
width = 0
|
599
|
-
str =
|
603
|
+
str = +""
|
600
604
|
s.gsub(/\t/, " ").each_char do |c|
|
601
605
|
w = Buffer.display_width(c)
|
602
606
|
width += w
|
@@ -619,7 +623,7 @@ module Mournmail
|
|
619
623
|
imap.uid_store(uid_set, "+FLAGS", [:Deleted])
|
620
624
|
count += uid_set.size
|
621
625
|
progress = (count.to_f * 100 / uids.size).round
|
622
|
-
|
626
|
+
foreground do
|
623
627
|
if dst_mailbox
|
624
628
|
message("Refiling mails to #{dst_mailbox}... #{progress}%",
|
625
629
|
log: false)
|
@@ -628,7 +632,7 @@ module Mournmail
|
|
628
632
|
end
|
629
633
|
end
|
630
634
|
end
|
631
|
-
|
635
|
+
foreground do
|
632
636
|
if dst_mailbox
|
633
637
|
message("Refiled mails to #{dst_mailbox}")
|
634
638
|
else
|
@@ -660,7 +664,7 @@ module Mournmail
|
|
660
664
|
end
|
661
665
|
summary_text = summary.to_s
|
662
666
|
summary.save
|
663
|
-
|
667
|
+
foreground do
|
664
668
|
@buffer.read_only_edit do
|
665
669
|
@buffer.clear
|
666
670
|
@buffer.insert(summary_text)
|
data/lib/mournmail/utils.rb
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
require "mail"
|
4
2
|
require "net/imap"
|
5
3
|
require "time"
|
@@ -7,7 +5,38 @@ require "tempfile"
|
|
7
5
|
require "fileutils"
|
8
6
|
require "timeout"
|
9
7
|
require "digest"
|
8
|
+
require "nkf"
|
10
9
|
require "groonga"
|
10
|
+
require 'google/api_client/client_secrets'
|
11
|
+
require 'google/api_client/auth/storage'
|
12
|
+
require 'google/api_client/auth/storages/file_store'
|
13
|
+
require 'launchy'
|
14
|
+
|
15
|
+
class Net::SMTP
|
16
|
+
def auth_xoauth2(user, secret)
|
17
|
+
check_auth_args user, secret
|
18
|
+
res = critical {
|
19
|
+
s = Mournmail.xoauth2_string(user, secret)
|
20
|
+
get_response('AUTH XOAUTH2 ' + base64_encode(s))
|
21
|
+
}
|
22
|
+
check_auth_response res
|
23
|
+
res
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class Net::IMAP::Xoauth2Authenticator
|
28
|
+
def process(data)
|
29
|
+
Mournmail.xoauth2_string(@user, @access_token)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def initialize(user, access_token)
|
35
|
+
@user = user
|
36
|
+
@access_token = access_token
|
37
|
+
end
|
38
|
+
end
|
39
|
+
Net::IMAP.add_authenticator("XOAUTH2", Net::IMAP::Xoauth2Authenticator)
|
11
40
|
|
12
41
|
module Mournmail
|
13
42
|
begin
|
@@ -148,12 +177,17 @@ module Mournmail
|
|
148
177
|
end
|
149
178
|
if @imap.nil? || @imap.disconnected?
|
150
179
|
conf = account_config
|
180
|
+
auth_type = conf[:imap_options][:auth_type] || "PLAIN"
|
181
|
+
password = conf[:imap_options][:password]
|
182
|
+
if auth_type == "gmail"
|
183
|
+
auth_type = "XOAUTH2"
|
184
|
+
password = google_access_token
|
185
|
+
end
|
151
186
|
Timeout.timeout(CONFIG[:mournmail_imap_connect_timeout]) do
|
152
187
|
@imap = Net::IMAP.new(conf[:imap_host],
|
153
188
|
conf[:imap_options])
|
154
|
-
@imap.authenticate(conf[:imap_options][:
|
155
|
-
|
156
|
-
conf[:imap_options][:password])
|
189
|
+
@imap.authenticate(auth_type, conf[:imap_options][:user_name],
|
190
|
+
password)
|
157
191
|
@mailboxes = @imap.list("", "*").map { |mbox|
|
158
192
|
Net::IMAP.decode_utf7(mbox.name)
|
159
193
|
}
|
@@ -179,40 +213,113 @@ module Mournmail
|
|
179
213
|
end
|
180
214
|
end
|
181
215
|
|
216
|
+
def self.google_access_token
|
217
|
+
auth_path = File.expand_path("cache/#{current_account}/google_auth.json",
|
218
|
+
CONFIG[:mournmail_directory])
|
219
|
+
FileUtils.mkdir_p(File.dirname(auth_path))
|
220
|
+
store = Google::APIClient::FileStore.new(auth_path)
|
221
|
+
storage = Google::APIClient::Storage.new(store)
|
222
|
+
storage.authorize
|
223
|
+
if storage.authorization.nil?
|
224
|
+
path = File.expand_path(account_config[:client_secret_path])
|
225
|
+
client_secrets = Google::APIClient::ClientSecrets.load(path)
|
226
|
+
auth_client = client_secrets.to_authorization
|
227
|
+
auth_client.update!(
|
228
|
+
:scope => 'https://mail.google.com/',
|
229
|
+
:redirect_uri => 'urn:ietf:wg:oauth:2.0:oob'
|
230
|
+
)
|
231
|
+
|
232
|
+
auth_uri = auth_client.authorization_uri.to_s
|
233
|
+
Launchy.open(auth_uri)
|
234
|
+
|
235
|
+
auth_client.code = foreground! {
|
236
|
+
Window.echo_area.clear_message
|
237
|
+
Window.redisplay
|
238
|
+
read_from_minibuffer("Code: ").chomp
|
239
|
+
}
|
240
|
+
auth_client.fetch_access_token!
|
241
|
+
old_umask = File.umask(077)
|
242
|
+
begin
|
243
|
+
storage.write_credentials(auth_client)
|
244
|
+
ensure
|
245
|
+
File.umask(old_umask)
|
246
|
+
end
|
247
|
+
else
|
248
|
+
auth_client = storage.authorization
|
249
|
+
end
|
250
|
+
auth_client.access_token
|
251
|
+
end
|
252
|
+
|
253
|
+
def self.xoauth2_string(user, access_token)
|
254
|
+
"user=#{user}\1auth=Bearer #{access_token}\1\1"
|
255
|
+
end
|
256
|
+
|
182
257
|
def self.fetch_summary(mailbox, all: false)
|
258
|
+
if all
|
259
|
+
summary = Mournmail::Summary.new(mailbox)
|
260
|
+
else
|
261
|
+
summary = Mournmail::Summary.load_or_new(mailbox)
|
262
|
+
end
|
183
263
|
imap_connect do |imap|
|
184
264
|
imap.select(mailbox)
|
185
|
-
if all
|
186
|
-
summary = Mournmail::Summary.new(mailbox)
|
187
|
-
else
|
188
|
-
summary = Mournmail::Summary.load_or_new(mailbox)
|
189
|
-
end
|
190
265
|
uidvalidity = imap.responses["UIDVALIDITY"].last
|
191
|
-
if summary.uidvalidity
|
192
|
-
|
193
|
-
|
194
|
-
clear = next_tick {
|
266
|
+
if uidvalidity && summary.uidvalidity &&
|
267
|
+
uidvalidity != summary.uidvalidity
|
268
|
+
clear = foreground! {
|
195
269
|
yes_or_no?("UIDVALIDITY has been changed; Clear cache?")
|
196
270
|
}
|
197
271
|
if clear
|
198
272
|
summary = Mournmail::Summary.new(mailbox)
|
199
273
|
end
|
200
274
|
end
|
201
|
-
|
202
|
-
|
275
|
+
summary.uidvalidity = uidvalidity
|
276
|
+
uids = imap.uid_search("ALL")
|
277
|
+
new_uids = uids - summary.uids
|
278
|
+
return summary if new_uids.empty?
|
203
279
|
summary.synchronize do
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
280
|
+
new_uids.each_slice(1000) do |uid_chunk|
|
281
|
+
data = imap.uid_fetch(uid_chunk, ["UID", "ENVELOPE", "FLAGS"])
|
282
|
+
data&.each do |i|
|
283
|
+
uid = i.attr["UID"]
|
284
|
+
next if summary[uid]
|
285
|
+
env = i.attr["ENVELOPE"]
|
286
|
+
flags = i.attr["FLAGS"]
|
287
|
+
item = Mournmail::SummaryItem.new(uid, env.date, env.from,
|
288
|
+
env.subject, flags)
|
289
|
+
summary.add_item(item, env.message_id, env.in_reply_to)
|
290
|
+
end
|
212
291
|
end
|
213
292
|
end
|
214
293
|
summary
|
215
294
|
end
|
295
|
+
rescue SocketError, Timeout::Error => e
|
296
|
+
foreground do
|
297
|
+
message(e.message)
|
298
|
+
end
|
299
|
+
summary
|
300
|
+
end
|
301
|
+
|
302
|
+
def self.show_summary(summary)
|
303
|
+
buffer = Buffer.find_or_new("*summary*", undo_limit: 0,
|
304
|
+
read_only: true)
|
305
|
+
buffer.apply_mode(Mournmail::SummaryMode)
|
306
|
+
buffer.read_only_edit do
|
307
|
+
buffer.clear
|
308
|
+
buffer.insert(summary.to_s)
|
309
|
+
end
|
310
|
+
switch_to_buffer(buffer)
|
311
|
+
Mournmail.current_mailbox = summary.mailbox
|
312
|
+
Mournmail.current_summary = summary
|
313
|
+
Mournmail.current_mail = nil
|
314
|
+
Mournmail.current_uid = nil
|
315
|
+
begin
|
316
|
+
buffer.beginning_of_buffer
|
317
|
+
buffer.re_search_forward(/^ *\d+ u/)
|
318
|
+
rescue SearchError
|
319
|
+
buffer.end_of_buffer
|
320
|
+
buffer.re_search_backward(/^ *\d+ /, raise_error: false)
|
321
|
+
end
|
322
|
+
summary_read_command
|
216
323
|
end
|
217
324
|
|
218
325
|
def self.mailbox_cache_path(mailbox)
|
@@ -277,6 +384,8 @@ module Mournmail
|
|
277
384
|
references = Array(mail.references) | Array(mail.in_reply_to)
|
278
385
|
if references.empty?
|
279
386
|
mail.message_id
|
387
|
+
elsif /\Aredmine\.issue-/.match?(references.first)
|
388
|
+
references.first
|
280
389
|
else
|
281
390
|
parent = messages_db.select { |m|
|
282
391
|
references.inject(nil) { |cond, ref|
|
@@ -343,17 +452,17 @@ module Mournmail
|
|
343
452
|
end
|
344
453
|
|
345
454
|
def self.force_utf8(s)
|
346
|
-
s.force_encoding(Encoding::UTF_8).scrub("?")
|
455
|
+
s.dup.force_encoding(Encoding::UTF_8).scrub("?")
|
347
456
|
end
|
348
457
|
|
349
458
|
def self.to_utf8(s, charset)
|
350
|
-
if /\Autf-8\z/i
|
459
|
+
if /\Autf-8\z/i.match?(charset)
|
351
460
|
force_utf8(s)
|
352
461
|
else
|
353
462
|
begin
|
354
463
|
s.encode(Encoding::UTF_8, charset, replace: "?")
|
355
|
-
rescue
|
356
|
-
force_utf8(s)
|
464
|
+
rescue
|
465
|
+
force_utf8(NKF.nkf("-w", s))
|
357
466
|
end
|
358
467
|
end.gsub(/\r\n/, "\n")
|
359
468
|
end
|
@@ -404,4 +513,8 @@ module Mournmail
|
|
404
513
|
@groonga_db.close
|
405
514
|
end
|
406
515
|
end
|
516
|
+
|
517
|
+
def self.parse_mail(s)
|
518
|
+
Mail.new(s.scrub("??"))
|
519
|
+
end
|
407
520
|
end
|
data/lib/mournmail/version.rb
CHANGED
data/lib/textbringer_plugin.rb
CHANGED
data/mournmail.gemspec
CHANGED
@@ -23,8 +23,11 @@ Gem::Specification.new do |spec|
|
|
23
23
|
|
24
24
|
spec.add_runtime_dependency "textbringer"
|
25
25
|
spec.add_runtime_dependency "mail"
|
26
|
+
spec.add_runtime_dependency "mime-types"
|
26
27
|
spec.add_runtime_dependency "rroonga"
|
28
|
+
spec.add_runtime_dependency "google-api-client"
|
29
|
+
spec.add_runtime_dependency "launchy"
|
27
30
|
|
28
|
-
spec.add_development_dependency "bundler"
|
29
|
-
spec.add_development_dependency "rake", "~>
|
31
|
+
spec.add_development_dependency "bundler"
|
32
|
+
spec.add_development_dependency "rake", "~> 12.0"
|
30
33
|
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: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shugo Maeda
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-03-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: textbringer
|
@@ -38,6 +38,20 @@ dependencies:
|
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: mime-types
|
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'
|
41
55
|
- !ruby/object:Gem::Dependency
|
42
56
|
name: rroonga
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -52,34 +66,62 @@ dependencies:
|
|
52
66
|
- - ">="
|
53
67
|
- !ruby/object:Gem::Version
|
54
68
|
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: google-api-client
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: launchy
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
55
97
|
- !ruby/object:Gem::Dependency
|
56
98
|
name: bundler
|
57
99
|
requirement: !ruby/object:Gem::Requirement
|
58
100
|
requirements:
|
59
|
-
- - "
|
101
|
+
- - ">="
|
60
102
|
- !ruby/object:Gem::Version
|
61
|
-
version: '
|
103
|
+
version: '0'
|
62
104
|
type: :development
|
63
105
|
prerelease: false
|
64
106
|
version_requirements: !ruby/object:Gem::Requirement
|
65
107
|
requirements:
|
66
|
-
- - "
|
108
|
+
- - ">="
|
67
109
|
- !ruby/object:Gem::Version
|
68
|
-
version: '
|
110
|
+
version: '0'
|
69
111
|
- !ruby/object:Gem::Dependency
|
70
112
|
name: rake
|
71
113
|
requirement: !ruby/object:Gem::Requirement
|
72
114
|
requirements:
|
73
115
|
- - "~>"
|
74
116
|
- !ruby/object:Gem::Version
|
75
|
-
version: '
|
117
|
+
version: '12.0'
|
76
118
|
type: :development
|
77
119
|
prerelease: false
|
78
120
|
version_requirements: !ruby/object:Gem::Requirement
|
79
121
|
requirements:
|
80
122
|
- - "~>"
|
81
123
|
- !ruby/object:Gem::Version
|
82
|
-
version: '
|
124
|
+
version: '12.0'
|
83
125
|
description: A message user agent for Textbringer.
|
84
126
|
email:
|
85
127
|
- shugo@ruby-lang.org
|
@@ -128,8 +170,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
128
170
|
- !ruby/object:Gem::Version
|
129
171
|
version: '0'
|
130
172
|
requirements: []
|
131
|
-
|
132
|
-
rubygems_version: 2.7.6
|
173
|
+
rubygems_version: 3.2.0.pre1
|
133
174
|
signing_key:
|
134
175
|
specification_version: 4
|
135
176
|
summary: A message user agent for Textbringer.
|