mournmail 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/README.md +46 -27
- data/lib/mournmail.rb +2 -0
- data/lib/mournmail/commands.rb +7 -4
- data/lib/mournmail/config.rb +2 -4
- data/lib/mournmail/draft_mode.rb +56 -13
- data/lib/mournmail/mail_encoded_word_patch.rb +73 -0
- data/lib/mournmail/message_mode.rb +3 -2
- data/lib/mournmail/message_rendering.rb +22 -15
- data/lib/mournmail/search_result_mode.rb +144 -0
- data/lib/mournmail/summary.rb +63 -8
- data/lib/mournmail/summary_mode.rb +406 -38
- data/lib/mournmail/utils.rb +279 -38
- data/lib/mournmail/version.rb +1 -1
- data/mournmail.gemspec +1 -1
- metadata +6 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 422feadab2e36499c76f69f1836ce4e384497f0e86814cb4c20d0966e70b69b8
|
4
|
+
data.tar.gz: 82423aa120aef42601ef86737e8461b96c32f786d9e7e647c80a4dfdc14a93f1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7a4bba313e307593fad8635b6f284bbc58a79087adad0671849bbf3ec416e554ba7c4af258f4b6556606262897035a08313d5f40754cde56bf8652555049ef09
|
7
|
+
data.tar.gz: 6a30de329d0dc20d86954c1cb8d881bfe15b8309931f2fbba98cdd1a690aa53d6eab1f345b86822c2a6de19905e357232fea1e2092077d866b1f9c4fc2a630d1
|
data/README.md
CHANGED
@@ -10,34 +10,53 @@ Mournmail is a message user agent for
|
|
10
10
|
## Configuration
|
11
11
|
|
12
12
|
```ruby
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
:
|
13
|
+
CONFIG[:mournmail_accounts] = {
|
14
|
+
"example.com" => {
|
15
|
+
from: "Shugo Maeda <shugo@example.com>",
|
16
|
+
delivery_method: :smtp,
|
17
|
+
delivery_options: {
|
18
|
+
address: "smtp.example.com",
|
19
|
+
port: 465,
|
20
|
+
domain: Socket.gethostname,
|
21
|
+
user_name: "shugo",
|
22
|
+
password: File.read("/path/to/smtp_passwd").chomp,
|
23
|
+
authentication: "login",
|
24
|
+
tls: true,
|
25
|
+
ca_file: "/path/to/ca.pem"
|
26
|
+
},
|
27
|
+
imap_host: "imap.example.com",
|
28
|
+
imap_options: {
|
29
|
+
auth_type: "PLAIN",
|
30
|
+
user_name: "shugo",
|
31
|
+
password: File.read("/path/to/imap_passwd").chomp,
|
32
|
+
ssl: { ca_file: "/path/to/ca.pem" }
|
33
|
+
},
|
34
|
+
spam_mailbox: "spam",
|
35
|
+
outbox_mailbox: "outbox",
|
36
|
+
archive_mailbox_format: "archive/%Y"
|
37
|
+
},
|
38
|
+
"gmail.com" => {
|
39
|
+
from: "Example <example@gmail.com>",
|
40
|
+
delivery_method: :smtp,
|
41
|
+
delivery_options: {
|
42
|
+
address: "smtp.gmail.com",
|
43
|
+
port: 587,
|
44
|
+
domain: Socket.gethostname,
|
45
|
+
user_name: "example@gmail.com",
|
46
|
+
password: File.read("/path/to/gmail_passwd").chomp,
|
47
|
+
authentication: "login",
|
48
|
+
enable_starttls_auto: true
|
49
|
+
},
|
50
|
+
imap_host: "imap.gmail.com",
|
51
|
+
imap_options: {
|
52
|
+
auth_type: "PLAIN",
|
53
|
+
user_name: "example@gmail.com",
|
54
|
+
password: File.read(File.expand_path("~/.textbringer/gmail_passwd")).chomp,
|
55
|
+
ssl: true
|
56
|
+
},
|
57
|
+
spam_mailbox: "迷惑メール",
|
58
|
+
archive_mailbox_format: false
|
37
59
|
},
|
38
|
-
auth_type: "PLAIN",
|
39
|
-
user_name: "shugo",
|
40
|
-
password: File.read("/path/to/imap_passwd").chomp
|
41
60
|
}
|
42
61
|
```
|
43
62
|
|
data/lib/mournmail.rb
CHANGED
@@ -9,4 +9,6 @@ require_relative "mournmail/summary"
|
|
9
9
|
require_relative "mournmail/draft_mode"
|
10
10
|
require_relative "mournmail/summary_mode"
|
11
11
|
require_relative "mournmail/message_mode"
|
12
|
+
require_relative "mournmail/search_result_mode"
|
12
13
|
require_relative "mournmail/commands"
|
14
|
+
require_relative "mournmail/mail_encoded_word_patch"
|
data/lib/mournmail/commands.rb
CHANGED
@@ -1,11 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
define_command(:mournmail, doc: "Start mournmail.") do
|
4
|
+
Mournmail.open_groonga_db
|
4
5
|
mournmail_visit_mailbox("INBOX")
|
5
6
|
end
|
6
7
|
|
7
8
|
define_command(:mournmail_visit_mailbox, doc: "Start mournmail.") do
|
8
|
-
|mailbox =
|
9
|
+
|mailbox = Mournmail.read_mailbox_name("Visit mailbox: ", default: "INBOX")|
|
9
10
|
mournmail_summary_sync(mailbox)
|
10
11
|
end
|
11
12
|
|
@@ -33,10 +34,10 @@ define_command(:mournmail_summary_sync, doc: "Sync summary.") do
|
|
33
34
|
message("Syncing #{mailbox} in background... Done")
|
34
35
|
begin
|
35
36
|
buffer.beginning_of_buffer
|
36
|
-
buffer.re_search_forward(
|
37
|
+
buffer.re_search_forward(/^ *\d+ u/)
|
37
38
|
rescue SearchError
|
38
39
|
buffer.end_of_buffer
|
39
|
-
buffer.re_search_backward(
|
40
|
+
buffer.re_search_backward(/^ *\d+ /, raise_error: false)
|
40
41
|
end
|
41
42
|
summary_read_command
|
42
43
|
end
|
@@ -63,6 +64,7 @@ define_command(:mournmail_quit, doc: "Quit mournmail.") do
|
|
63
64
|
Mournmail.current_summary = nil
|
64
65
|
Mournmail.current_mail = nil
|
65
66
|
Mournmail.current_uid = nil
|
67
|
+
Mournmail.close_groonga_db
|
66
68
|
end
|
67
69
|
|
68
70
|
define_command(:mail, doc: "Write a new mail.") do
|
@@ -70,8 +72,9 @@ define_command(:mail, doc: "Write a new mail.") do
|
|
70
72
|
buffer = Buffer.new_buffer("*draft*")
|
71
73
|
switch_to_buffer(buffer)
|
72
74
|
draft_mode
|
75
|
+
conf = Mournmail.account_config
|
73
76
|
insert <<~EOF
|
74
|
-
From: #{
|
77
|
+
From: #{conf[:from]}
|
75
78
|
To:
|
76
79
|
Subject:
|
77
80
|
User-Agent: Mournmail/#{Mournmail::VERSION} Textbringer/#{Textbringer::VERSION} Ruby/#{RUBY_VERSION}
|
data/lib/mournmail/config.rb
CHANGED
@@ -2,9 +2,7 @@
|
|
2
2
|
|
3
3
|
module Textbringer
|
4
4
|
CONFIG[:mournmail_directory] = File.expand_path("~/.mournmail")
|
5
|
-
CONFIG[:
|
6
|
-
CONFIG[:mournmail_delivery_method] = :smtp
|
7
|
-
CONFIG[:mournmail_delivery_options] = {}
|
5
|
+
CONFIG[:mournmail_accounts] = {}
|
8
6
|
CONFIG[:mournmail_charset] = "utf-8"
|
9
7
|
CONFIG[:mournmail_save_directory] = "/tmp"
|
10
8
|
CONFIG[:mournmail_display_header_fields] = [
|
@@ -19,8 +17,8 @@ module Textbringer
|
|
19
17
|
"Content-Type"
|
20
18
|
]
|
21
19
|
CONFIG[:mournmail_imap_connect_timeout] = 10
|
20
|
+
CONFIG[:mournmail_keep_alive_interval] = 60
|
22
21
|
CONFIG[:mournmail_file_open_comamnd] = "xdg-open"
|
23
22
|
CONFIG[:mournmail_link_open_comamnd] = "xdg-open"
|
24
|
-
CONFIG[:mournmail_outbox] = nil
|
25
23
|
CONFIG[:mournmail_addresses_path] = File.expand_path("~/.addresses")
|
26
24
|
end
|
data/lib/mournmail/draft_mode.rb
CHANGED
@@ -7,6 +7,8 @@ module Mournmail
|
|
7
7
|
MAIL_MODE_MAP.define_key("\C-c\C-k", :draft_kill_command)
|
8
8
|
MAIL_MODE_MAP.define_key("\C-c\C-x\C-i", :draft_attach_file_command)
|
9
9
|
MAIL_MODE_MAP.define_key("\t", :draft_complete_or_insert_tab_command)
|
10
|
+
MAIL_MODE_MAP.define_key("\C-c\C-xv", :draft_pgp_sign_command)
|
11
|
+
MAIL_MODE_MAP.define_key("\C-c\C-xe", :draft_pgp_encrypt_command)
|
10
12
|
|
11
13
|
define_syntax :field_name, /^[A-Za-z\-]+: /
|
12
14
|
define_syntax :quotation, /^>.*/
|
@@ -22,6 +24,7 @@ module Mournmail
|
|
22
24
|
unless y_or_n?("Send this mail?")
|
23
25
|
return
|
24
26
|
end
|
27
|
+
run_hooks(:mournmail_pre_send_hook)
|
25
28
|
s = @buffer.to_s
|
26
29
|
charset = CONFIG[:mournmail_charset]
|
27
30
|
begin
|
@@ -30,15 +33,22 @@ module Mournmail
|
|
30
33
|
charset = "utf-8"
|
31
34
|
end
|
32
35
|
m = Mail.new(charset: charset)
|
36
|
+
m.transport_encoding = "8bit"
|
33
37
|
header, body = s.split(/^--text follows this line--\n/, 2)
|
34
38
|
attached_files = []
|
35
39
|
attached_messages = []
|
40
|
+
pgp_sign = false
|
41
|
+
pgp_encrypt = false
|
36
42
|
header.scan(/^([!-9;-~]+):[ \t]*(.*(?:\n[ \t].*)*)\n/) do |name, val|
|
37
43
|
case name
|
38
44
|
when "Attached-File"
|
39
45
|
attached_files.push(val.strip)
|
40
46
|
when "Attached-Message"
|
41
47
|
attached_messages.push(val.strip)
|
48
|
+
when "PGP-Sign"
|
49
|
+
pgp_sign = val.strip == "yes"
|
50
|
+
when "PGP-Encrypt"
|
51
|
+
pgp_encrypt = val.strip == "yes"
|
42
52
|
else
|
43
53
|
m[name] = val
|
44
54
|
end
|
@@ -55,32 +65,45 @@ module Mournmail
|
|
55
65
|
end
|
56
66
|
end
|
57
67
|
attached_files.each do |file|
|
58
|
-
m.add_file(file)
|
68
|
+
m.add_file(filename: File.basename(file),
|
69
|
+
content: File.read(file),
|
70
|
+
encoding: "binary")
|
59
71
|
end
|
60
|
-
|
61
|
-
|
72
|
+
conf = Mournmail.account_config
|
73
|
+
m.delivery_method(@buffer[:mournmail_delivery_method] ||
|
74
|
+
conf[:delivery_method],
|
75
|
+
@buffer[:mournmail_delivery_options] ||
|
76
|
+
conf[:delivery_options])
|
62
77
|
bury_buffer(@buffer)
|
63
78
|
background do
|
64
79
|
begin
|
65
80
|
if !attached_messages.empty?
|
66
81
|
attached_messages.each do |attached_message|
|
67
|
-
|
68
|
-
s
|
82
|
+
cache_id = attached_message.strip
|
83
|
+
s = File.read(Mournmail.mail_cache_path(cache_id))
|
69
84
|
part = Mail::Part.new(content_type: "message/rfc822", body: s)
|
70
85
|
m.body << part
|
71
86
|
end
|
72
87
|
end
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
88
|
+
if pgp_sign || pgp_encrypt
|
89
|
+
m.gpg(sign: pgp_sign, encrypt: pgp_encrypt)
|
90
|
+
end
|
91
|
+
m.deliver
|
92
|
+
next_tick do
|
93
|
+
message("Mail sent.")
|
94
|
+
end
|
95
|
+
outbox = Mournmail.account_config[:outbox_mailbox]
|
96
|
+
if outbox
|
97
|
+
Mournmail.imap_connect do |imap|
|
98
|
+
unless imap.list("", outbox)
|
99
|
+
imap.create(outbox)
|
100
|
+
end
|
77
101
|
imap.append(outbox, m.to_s, [:Seen])
|
78
102
|
end
|
79
103
|
end
|
80
104
|
next_tick do
|
81
105
|
kill_buffer(@buffer, force: true)
|
82
106
|
Mournmail.back_to_summary
|
83
|
-
message("Mail sent.")
|
84
107
|
end
|
85
108
|
rescue Exception
|
86
109
|
next_tick do
|
@@ -101,9 +124,7 @@ module Mournmail
|
|
101
124
|
define_local_command(:draft_attach_file, doc: "Attach a file.") do
|
102
125
|
|file_name = read_file_name("Attach file: ")|
|
103
126
|
@buffer.save_excursion do
|
104
|
-
|
105
|
-
@buffer.re_search_forward(/^--text follows this line--$/)
|
106
|
-
@buffer.beginning_of_line
|
127
|
+
end_of_header
|
107
128
|
@buffer.insert("Attached-File: #{file_name}\n")
|
108
129
|
end
|
109
130
|
end
|
@@ -135,5 +156,27 @@ module Mournmail
|
|
135
156
|
@buffer.insert("\t")
|
136
157
|
end
|
137
158
|
end
|
159
|
+
|
160
|
+
define_local_command(:draft_pgp_sign, doc: "PGP sign.") do
|
161
|
+
@buffer.save_excursion do
|
162
|
+
end_of_header
|
163
|
+
@buffer.insert("PGP-Sign: yes\n")
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
define_local_command(:draft_pgp_encrypt, doc: "PGP encrypt.") do
|
168
|
+
@buffer.save_excursion do
|
169
|
+
end_of_header
|
170
|
+
@buffer.insert("PGP-Encrypt: yes\n")
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
private
|
175
|
+
|
176
|
+
def end_of_header
|
177
|
+
@buffer.beginning_of_buffer
|
178
|
+
@buffer.re_search_forward(/^--text follows this line--$/)
|
179
|
+
@buffer.beginning_of_line
|
180
|
+
end
|
138
181
|
end
|
139
182
|
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "mail"
|
4
|
+
|
5
|
+
module Mournmail
|
6
|
+
module MailEncodedWordPatch
|
7
|
+
private
|
8
|
+
|
9
|
+
def fold(prepend = 0) # :nodoc:
|
10
|
+
charset = normalized_encoding
|
11
|
+
decoded_string = decoded.to_s
|
12
|
+
if charset != "UTF-8" ||
|
13
|
+
decoded_string.ascii_only? ||
|
14
|
+
!decoded_string.respond_to?(:encoding) ||
|
15
|
+
decoded_string.encoding != Encoding::UTF_8 ||
|
16
|
+
Regexp.new('\p{Han}|\p{Hiragana}|\p{Katakana}') !~ decoded_string
|
17
|
+
# Use Q encoding
|
18
|
+
return super(prepend)
|
19
|
+
end
|
20
|
+
words = decoded_string.split(/[ \t]/)
|
21
|
+
folded_lines = []
|
22
|
+
b_encoding_extra_size = "=?#{charset}?B??=".bytesize
|
23
|
+
while !words.empty?
|
24
|
+
limit = 78 - prepend
|
25
|
+
line = String.new
|
26
|
+
fold_line = false
|
27
|
+
while !fold_line && !words.empty?
|
28
|
+
word = words.first
|
29
|
+
s = (line.empty? ? "" : " ").dup
|
30
|
+
if word.ascii_only?
|
31
|
+
s << word
|
32
|
+
break if !line.empty? && line.bytesize + s.bytesize > limit
|
33
|
+
words.shift
|
34
|
+
if prepend + line.bytesize + s.bytesize > 998
|
35
|
+
words.unshift(s.slice!(998 - prepend .. -1))
|
36
|
+
fold_line = true
|
37
|
+
end
|
38
|
+
else
|
39
|
+
words.shift
|
40
|
+
encoded_text = base64_encode(word)
|
41
|
+
min_size = line.bytesize + s.bytesize + b_encoding_extra_size
|
42
|
+
new_size = min_size + encoded_text.bytesize
|
43
|
+
if new_size > limit
|
44
|
+
n = ((limit - min_size) * 3.0 / 4.0).floor
|
45
|
+
if n <= 0
|
46
|
+
words.unshift(word) if !line.empty?
|
47
|
+
break
|
48
|
+
end
|
49
|
+
truncated = word.byteslice(0, n).scrub("")
|
50
|
+
rest = word.byteslice(truncated.bytesize,
|
51
|
+
word.bytesize - truncated.bytesize)
|
52
|
+
words.unshift(rest)
|
53
|
+
encoded_text = base64_encode(truncated)
|
54
|
+
fold_line = true
|
55
|
+
end
|
56
|
+
encoded_word = "=?#{charset}?B?#{encoded_text}?="
|
57
|
+
s << encoded_word
|
58
|
+
end
|
59
|
+
line << s
|
60
|
+
end
|
61
|
+
folded_lines << line
|
62
|
+
prepend = 1 # Space will be prepended
|
63
|
+
end
|
64
|
+
folded_lines
|
65
|
+
end
|
66
|
+
|
67
|
+
def base64_encode(word)
|
68
|
+
Mail::Encodings::Base64.encode(word).gsub(/[\r\n]/, "")
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
Mail::UnstructuredField.prepend(Mournmail::MailEncodedWordPatch)
|
@@ -13,7 +13,8 @@ module Mournmail
|
|
13
13
|
|
14
14
|
define_syntax :field_name, /^[A-Za-z\-]+: /
|
15
15
|
define_syntax :quotation, /^>.*/
|
16
|
-
define_syntax :mime_part,
|
16
|
+
define_syntax :mime_part,
|
17
|
+
/^\[(([0-9.]+) [A-Za-z._\-]+\/[A-Za-z._\-]+.*|PGP\/MIME .*)\]$/
|
17
18
|
define_syntax :link, URI_REGEXP
|
18
19
|
|
19
20
|
def initialize(buffer)
|
@@ -53,7 +54,7 @@ module Mournmail
|
|
53
54
|
if @buffer.looking_at?(/\[([0-9.]+) .*\]/)
|
54
55
|
index = match_string(1)
|
55
56
|
indices = index.split(".").map(&:to_i)
|
56
|
-
|
57
|
+
@buffer[:mournmail_mail].dig_part(*indices)
|
57
58
|
else
|
58
59
|
nil
|
59
60
|
end
|
@@ -1,7 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "mail"
|
4
|
-
require "mail-iso-2022-jp"
|
5
4
|
|
6
5
|
module Mournmail
|
7
6
|
module MessageRendering
|
@@ -12,7 +11,7 @@ module Mournmail
|
|
12
11
|
|
13
12
|
def render_header
|
14
13
|
CONFIG[:mournmail_display_header_fields].map { |name|
|
15
|
-
val = self[name]
|
14
|
+
val = self[name]&.to_s&.gsub(/\t/, " ")
|
16
15
|
val ? "#{name}: #{val}\n" : ""
|
17
16
|
}.join
|
18
17
|
end
|
@@ -20,7 +19,13 @@ module Mournmail
|
|
20
19
|
def render_body(indices = [])
|
21
20
|
if HAVE_MAIL_GPG && encrypted?
|
22
21
|
mail = decrypt(verify: true)
|
23
|
-
|
22
|
+
if mail.signatures.empty?
|
23
|
+
sig = ""
|
24
|
+
else
|
25
|
+
sig = "[PGP/MIME signature]\n" +
|
26
|
+
signature_of(mail)
|
27
|
+
end
|
28
|
+
return "[PGP/MIME encrypted message]\n" + mail.render(indices) + sig
|
24
29
|
end
|
25
30
|
if multipart?
|
26
31
|
parts.each_with_index.map { |part, i|
|
@@ -28,11 +33,7 @@ module Mournmail
|
|
28
33
|
}.join
|
29
34
|
else
|
30
35
|
s = body.decoded
|
31
|
-
|
32
|
-
s.force_encoding(Encoding::UTF_8).scrub("?")
|
33
|
-
else
|
34
|
-
s.encode(Encoding::UTF_8, charset, replace: "?")
|
35
|
-
end.gsub(/\r\n/, "\n")
|
36
|
+
Mournmail.to_utf8(s, charset).gsub(/\r\n/, "\n")
|
36
37
|
end + pgp_signature
|
37
38
|
end
|
38
39
|
|
@@ -54,15 +55,21 @@ module Mournmail
|
|
54
55
|
def pgp_signature
|
55
56
|
if HAVE_MAIL_GPG && signed?
|
56
57
|
verified = verify
|
57
|
-
|
58
|
-
from = verified.signatures.map { |sig|
|
59
|
-
sig.from rescue sig.fingerprint
|
60
|
-
}.join(", ")
|
61
|
-
"#{validity} signature from #{from}\n"
|
58
|
+
signature_of(verified)
|
62
59
|
else
|
63
60
|
""
|
64
61
|
end
|
65
62
|
end
|
63
|
+
|
64
|
+
def signature_of(m)
|
65
|
+
validity = m.signature_valid? ? "Good" : "Bad"
|
66
|
+
from = m.signatures.map { |sig|
|
67
|
+
sig.from rescue sig.fingerprint
|
68
|
+
}.join(", ")
|
69
|
+
s = "#{validity} signature from #{from}"
|
70
|
+
message(s)
|
71
|
+
s + "\n"
|
72
|
+
end
|
66
73
|
end
|
67
74
|
|
68
75
|
refine ::Mail::Part do
|
@@ -75,7 +82,7 @@ module Mournmail
|
|
75
82
|
|
76
83
|
def dig_part(i, *rest_indices)
|
77
84
|
if main_type == "message" && sub_type == "rfc822"
|
78
|
-
mail = Mail.new(body.
|
85
|
+
mail = Mail.new(body.to_s)
|
79
86
|
mail.dig_part(i, *rest_indices)
|
80
87
|
else
|
81
88
|
part = parts[i]
|
@@ -97,7 +104,7 @@ module Mournmail
|
|
97
104
|
elsif main_type == "message" && sub_type == "rfc822"
|
98
105
|
mail = Mail.new(body.raw_source)
|
99
106
|
mail.render(indices)
|
100
|
-
elsif
|
107
|
+
elsif attachment?
|
101
108
|
""
|
102
109
|
else
|
103
110
|
if main_type == "text" && sub_type == "plain"
|