mournmail 0.3.2 → 1.0.2
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/lib/mournmail/commands.rb +11 -2
- data/lib/mournmail/config.rb +25 -0
- data/lib/mournmail/draft_mode.rb +23 -2
- data/lib/mournmail/message_mode.rb +24 -10
- data/lib/mournmail/message_rendering.rb +78 -18
- data/lib/mournmail/summary.rb +2 -1
- data/lib/mournmail/summary_mode.rb +19 -12
- data/lib/mournmail/utils.rb +68 -17
- data/lib/mournmail/version.rb +1 -1
- data/mournmail.gemspec +3 -1
- metadata +33 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 140daea6492012b5093229b435fdbe2c98548430b8b8a74a0714483dd039c085
|
4
|
+
data.tar.gz: cb3fb9f8bd0a43fce73f3bd2d3e1d9515acaf74c1a3246c429c027ea7f9db140
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dbfe86922c8b715349fd6d22443e0222b58b6b4c6ea64e78a3cfc01b1caa8f9b4852796a381556f536a9cc8608db8203e69de17c7ce47d4172e03392c34a0847
|
7
|
+
data.tar.gz: 4437171b2b8baa965d829c30f1807d0c23b459dd0b5c5756b54fd728177d0e3af2df6702939b155f80913fb254d49303aee6123047509a0f44643e471feb03d0
|
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
|
}
|
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,22 @@ 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-- /
|
39
|
+
CONFIG[:mournmail_allowed_attachment_extensions] = [
|
40
|
+
"txt",
|
41
|
+
"md",
|
42
|
+
"htm",
|
43
|
+
"html",
|
44
|
+
"pdf",
|
45
|
+
"jpg",
|
46
|
+
"jpeg",
|
47
|
+
"png",
|
48
|
+
"gif",
|
49
|
+
"doc",
|
50
|
+
"docx",
|
51
|
+
"xls",
|
52
|
+
"xlsx",
|
53
|
+
"ppt",
|
54
|
+
"zip"
|
55
|
+
]
|
31
56
|
end
|
data/lib/mournmail/draft_mode.rb
CHANGED
@@ -8,6 +8,7 @@ module Mournmail
|
|
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
10
|
MAIL_MODE_MAP.define_key("\C-c\t", :insert_signature_command)
|
11
|
+
MAIL_MODE_MAP.define_key("\C-c@", :draft_change_account_command)
|
11
12
|
|
12
13
|
define_syntax :field_name, /^[A-Za-z\-]+: /
|
13
14
|
define_syntax :quotation, /^>.*/
|
@@ -68,12 +69,15 @@ module Mournmail
|
|
68
69
|
content: File.read(file),
|
69
70
|
encoding: "binary")
|
70
71
|
end
|
71
|
-
|
72
|
+
account = @buffer[:mournmail_delivery_account] ||
|
73
|
+
Mournmail.current_account
|
74
|
+
conf = CONFIG[:mournmail_accounts][account]
|
72
75
|
options = @buffer[:mournmail_delivery_options] ||
|
73
76
|
conf[:delivery_options]
|
74
77
|
if options[:authentication] == "gmail"
|
78
|
+
token = Mournmail.google_access_token(account)
|
75
79
|
options = options.merge(authentication: "xoauth2",
|
76
|
-
password:
|
80
|
+
password: token)
|
77
81
|
end
|
78
82
|
m.delivery_method(@buffer[:mournmail_delivery_method] ||
|
79
83
|
conf[:delivery_method],
|
@@ -186,6 +190,23 @@ module Mournmail
|
|
186
190
|
@buffer.insert(CONFIG[:signature])
|
187
191
|
end
|
188
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
|
+
|
189
210
|
private
|
190
211
|
|
191
212
|
def end_of_header
|
@@ -12,7 +12,7 @@ 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>)"]*[
|
15
|
+
URI_REGEXP = /(https?|ftp):\/\/[^ \t\n>)"]*[^\] \t\n>.,:)"]+|#{MAILTO_REGEXP}/
|
16
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
|
|
@@ -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
|
@@ -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
|
@@ -125,23 +129,33 @@ module Mournmail
|
|
125
129
|
raise EditorError, "Can't open a multipart entity."
|
126
130
|
end
|
127
131
|
ext = part_file_name(part).slice(/\.([^.]+)\z/, 1)
|
132
|
+
if part.main_type != "text" || part.sub_type == "html"
|
133
|
+
if ext.nil?
|
134
|
+
raise EditorError, "The extension of the filename is not specified"
|
135
|
+
end
|
136
|
+
if !CONFIG[:mournmail_allowed_attachment_extensions].include?(ext)
|
137
|
+
raise EditorError, ".#{ext} is not allowed"
|
138
|
+
end
|
139
|
+
end
|
128
140
|
if ext
|
129
141
|
file_name = ["mournmail", "." + ext]
|
130
142
|
else
|
131
143
|
file_name = "mournmail"
|
132
144
|
end
|
133
|
-
|
145
|
+
@attached_file = Tempfile.open(file_name, binmode: true)
|
134
146
|
s = part.decoded
|
135
|
-
if part.
|
147
|
+
if part.content_type == "text/html"
|
148
|
+
s = s.sub(/<meta http-equiv="content-type".*?>/i, "")
|
149
|
+
elsif part.charset
|
136
150
|
s = s.encode(part.charset)
|
137
151
|
end
|
138
|
-
|
139
|
-
|
140
|
-
if
|
141
|
-
find_file(
|
152
|
+
@attached_file.write(s)
|
153
|
+
@attached_file.close
|
154
|
+
if part.main_type == "text" && part.sub_type != "html"
|
155
|
+
find_file(@attached_file.path)
|
142
156
|
else
|
143
157
|
background do
|
144
|
-
system(*CONFIG[:mournmail_file_open_comamnd],
|
158
|
+
system(*CONFIG[:mournmail_file_open_comamnd], @attached_file.path,
|
145
159
|
out: File::NULL, err: File::NULL)
|
146
160
|
end
|
147
161
|
end
|
@@ -8,8 +8,8 @@ module Mournmail
|
|
8
8
|
render_header + "\n" + render_body(indices)
|
9
9
|
end
|
10
10
|
|
11
|
-
def render_header
|
12
|
-
|
11
|
+
def render_header(fields = CONFIG[:mournmail_display_header_fields])
|
12
|
+
fields.map { |name|
|
13
13
|
val = self[name]&.to_s&.gsub(/\t/, " ")
|
14
14
|
val ? "#{name}: #{val}\n" : ""
|
15
15
|
}.join
|
@@ -41,11 +41,36 @@ module Mournmail
|
|
41
41
|
else
|
42
42
|
type = Mail::Encodings.decode_encode(self["content-type"].to_s,
|
43
43
|
:decode) rescue
|
44
|
-
|
44
|
+
"broken/type; error=\"#{$!} (#{$!.class})\""
|
45
45
|
"[0 #{type}]\n"
|
46
46
|
end + pgp_signature
|
47
47
|
end
|
48
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
|
+
|
49
74
|
def dig_part(i, *rest_indices)
|
50
75
|
if HAVE_MAIL_GPG && encrypted?
|
51
76
|
mail = decrypt(verify: true)
|
@@ -91,7 +116,39 @@ module Mournmail
|
|
91
116
|
:decode) rescue
|
92
117
|
"broken/type; error=\"#{$!} (#{$!.class})\""
|
93
118
|
"[#{index} #{type}]\n" +
|
94
|
-
|
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
|
+
""
|
95
152
|
end
|
96
153
|
|
97
154
|
def dig_part(i, *rest_indices)
|
@@ -110,26 +167,29 @@ module Mournmail
|
|
110
167
|
|
111
168
|
private
|
112
169
|
|
113
|
-
def render_content(indices)
|
170
|
+
def render_content(indices, no_content)
|
114
171
|
if multipart?
|
115
172
|
parts.each_with_index.map { |part, i|
|
116
|
-
|
117
|
-
|
173
|
+
part.render([*indices, i],
|
174
|
+
no_content || sub_type == "alternative" && i > 0)
|
118
175
|
}.join
|
119
|
-
elsif main_type == "message" && sub_type == "rfc822"
|
120
|
-
mail = Mail.new(body.raw_source)
|
121
|
-
mail.render(indices)
|
122
|
-
elsif attachment?
|
123
|
-
""
|
124
176
|
else
|
125
|
-
|
126
|
-
|
127
|
-
|
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?
|
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
|
128
190
|
else
|
129
|
-
|
191
|
+
""
|
130
192
|
end
|
131
|
-
else
|
132
|
-
""
|
133
193
|
end
|
134
194
|
end
|
135
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
|
@@ -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
@@ -11,6 +11,7 @@ require 'google/api_client/client_secrets'
|
|
11
11
|
require 'google/api_client/auth/storage'
|
12
12
|
require 'google/api_client/auth/storages/file_store'
|
13
13
|
require 'launchy'
|
14
|
+
require "socket"
|
14
15
|
|
15
16
|
class Net::SMTP
|
16
17
|
def auth_xoauth2(user, secret)
|
@@ -225,38 +226,67 @@ module Mournmail
|
|
225
226
|
end
|
226
227
|
end
|
227
228
|
|
228
|
-
|
229
|
-
|
229
|
+
class GoogleAuthCallbackServer
|
230
|
+
def initialize
|
231
|
+
@servers = Socket.tcp_server_sockets("127.0.0.1", 0)
|
232
|
+
end
|
233
|
+
|
234
|
+
def port
|
235
|
+
@servers.first.local_address.ip_port
|
236
|
+
end
|
237
|
+
|
238
|
+
def receive_code
|
239
|
+
Socket.accept_loop(@servers) do |sock, addr|
|
240
|
+
line = sock.gets
|
241
|
+
query_string = line.slice(%r'\AGET [^?]*\?(.*) HTTP/1.1\r\n', 1)
|
242
|
+
params = CGI.parse(query_string)
|
243
|
+
code = params["code"][0]
|
244
|
+
while line = sock.gets
|
245
|
+
break if line == "\r\n"
|
246
|
+
end
|
247
|
+
sock.print("HTTP/1.1 200 OK\r\n")
|
248
|
+
sock.print("Content-Type: text/plain\r\n")
|
249
|
+
sock.print("\r\n")
|
250
|
+
if code
|
251
|
+
sock.print("Authenticated!")
|
252
|
+
else
|
253
|
+
sock.print("Authentication failed!")
|
254
|
+
end
|
255
|
+
return code
|
256
|
+
ensure
|
257
|
+
sock.close
|
258
|
+
end
|
259
|
+
ensure
|
260
|
+
@servers.each(&:close)
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
def self.google_access_token(account = current_account)
|
265
|
+
auth_path = File.expand_path("cache/#{account}/google_auth.json",
|
230
266
|
CONFIG[:mournmail_directory])
|
231
267
|
FileUtils.mkdir_p(File.dirname(auth_path))
|
232
268
|
store = Google::APIClient::FileStore.new(auth_path)
|
233
269
|
storage = Google::APIClient::Storage.new(store)
|
234
270
|
storage.authorize
|
235
271
|
if storage.authorization.nil?
|
236
|
-
|
272
|
+
conf = CONFIG[:mournmail_accounts][account]
|
273
|
+
path = File.expand_path(conf[:client_secret_path])
|
237
274
|
client_secrets = Google::APIClient::ClientSecrets.load(path)
|
275
|
+
callback_server = GoogleAuthCallbackServer.new
|
238
276
|
auth_client = client_secrets.to_authorization
|
239
277
|
auth_client.update!(
|
240
278
|
:scope => 'https://mail.google.com/',
|
241
|
-
:redirect_uri =>
|
279
|
+
:redirect_uri => "http://127.0.0.1:#{callback_server.port}/"
|
242
280
|
)
|
243
281
|
auth_uri = auth_client.authorization_uri.to_s
|
244
|
-
|
282
|
+
foreground! do
|
245
283
|
begin
|
246
284
|
Launchy.open(auth_uri)
|
247
285
|
rescue Launchy::CommandNotFoundError
|
248
|
-
|
286
|
+
show_google_auth_uri(auth_uri)
|
249
287
|
end
|
250
|
-
|
251
|
-
|
252
|
-
Window.redisplay
|
253
|
-
read_from_minibuffer("Code: ").chomp
|
254
|
-
ensure
|
255
|
-
if buffer
|
256
|
-
kill_buffer(buffer, force: true)
|
257
|
-
end
|
258
|
-
end
|
259
|
-
}
|
288
|
+
end
|
289
|
+
auth_client.code = callback_server.receive_code
|
260
290
|
auth_client.fetch_access_token!
|
261
291
|
old_umask = File.umask(077)
|
262
292
|
begin
|
@@ -382,7 +412,8 @@ module Mournmail
|
|
382
412
|
dir = File.dirname(path)
|
383
413
|
base = File.basename(path)
|
384
414
|
begin
|
385
|
-
f = Tempfile.create(["#{base}-", ".tmp"], dir
|
415
|
+
f = Tempfile.create(["#{base}-", ".tmp"], dir,
|
416
|
+
external_encoding: "ASCII-8BIT", binmode: true)
|
386
417
|
begin
|
387
418
|
f.write(s)
|
388
419
|
ensure
|
@@ -554,4 +585,24 @@ module Mournmail
|
|
554
585
|
def self.parse_mail(s)
|
555
586
|
Mail.new(s.scrub("??"))
|
556
587
|
end
|
588
|
+
|
589
|
+
def self.read_account_name(prompt, **opts)
|
590
|
+
f = ->(s) {
|
591
|
+
complete_for_minibuffer(s, CONFIG[:mournmail_accounts].keys)
|
592
|
+
}
|
593
|
+
read_from_minibuffer(prompt, completion_proc: f, **opts)
|
594
|
+
end
|
595
|
+
|
596
|
+
def self.insert_signature
|
597
|
+
account = Buffer.current[:mournmail_delivery_account] ||
|
598
|
+
Mournmail.current_account
|
599
|
+
signature = CONFIG[:mournmail_accounts][account][:signature]
|
600
|
+
if signature
|
601
|
+
Buffer.current.save_excursion do
|
602
|
+
end_of_buffer
|
603
|
+
insert("\n")
|
604
|
+
insert(signature)
|
605
|
+
end
|
606
|
+
end
|
607
|
+
end
|
557
608
|
end
|
data/lib/mournmail/version.rb
CHANGED
data/mournmail.gemspec
CHANGED
@@ -22,6 +22,8 @@ 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"
|
@@ -30,5 +32,5 @@ Gem::Specification.new do |spec|
|
|
30
32
|
spec.add_runtime_dependency "html2text"
|
31
33
|
|
32
34
|
spec.add_development_dependency "bundler"
|
33
|
-
spec.add_development_dependency "rake", "
|
35
|
+
spec.add_development_dependency "rake", ">= 12.0"
|
34
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.2
|
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: 2022-09-08 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
|
@@ -126,14 +154,14 @@ dependencies:
|
|
126
154
|
name: rake
|
127
155
|
requirement: !ruby/object:Gem::Requirement
|
128
156
|
requirements:
|
129
|
-
- - "
|
157
|
+
- - ">="
|
130
158
|
- !ruby/object:Gem::Version
|
131
159
|
version: '12.0'
|
132
160
|
type: :development
|
133
161
|
prerelease: false
|
134
162
|
version_requirements: !ruby/object:Gem::Requirement
|
135
163
|
requirements:
|
136
|
-
- - "
|
164
|
+
- - ">="
|
137
165
|
- !ruby/object:Gem::Version
|
138
166
|
version: '12.0'
|
139
167
|
description: A message user agent for Textbringer.
|
@@ -186,7 +214,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
186
214
|
- !ruby/object:Gem::Version
|
187
215
|
version: '0'
|
188
216
|
requirements: []
|
189
|
-
rubygems_version: 3.
|
217
|
+
rubygems_version: 3.4.0.dev
|
190
218
|
signing_key:
|
191
219
|
specification_version: 4
|
192
220
|
summary: A message user agent for Textbringer.
|