mournmail 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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.
|