mournmail 0.1.1 → 1.0.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 +50 -27
- data/exe/mournmail_reindex +16 -0
- data/lib/mournmail.rb +2 -2
- data/lib/mournmail/commands.rb +23 -30
- data/lib/mournmail/config.rb +21 -8
- data/lib/mournmail/draft_mode.rb +101 -22
- data/lib/mournmail/faces.rb +0 -2
- data/lib/mournmail/mail_encoded_word_patch.rb +71 -0
- data/lib/mournmail/message_mode.rb +60 -15
- data/lib/mournmail/message_rendering.rb +124 -36
- data/lib/mournmail/search_result_mode.rb +143 -0
- data/lib/mournmail/summary.rb +94 -23
- data/lib/mournmail/summary_mode.rb +427 -48
- data/lib/mournmail/utils.rb +474 -61
- data/lib/mournmail/version.rb +1 -3
- data/lib/textbringer_plugin.rb +0 -2
- data/mournmail.gemspec +7 -3
- metadata +76 -18
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 8cbf82ccf54ab48f7596d24aaacb4673ca8f8271f304264df7aa96a932dac2fb
|
4
|
+
data.tar.gz: facbe695e21c68a1b5c50a9bb9d50894f948e09bab2a436b2174656953b809bf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 49c6ba544f435ea5eb150b510e26d18ab0bcd0fddd1a1a147dc0aee84bde05656bb153f44628ee0828b2e02c8f6ccc27b75c7e61b34ca0511c0eb2f074240e1e
|
7
|
+
data.tar.gz: 43accf4207ec48914c79940d2c45ce931beab826759b3e64174257891bf70d741913800aa976b41c19a2b47d1a0c8e32a00215995889058fdee5c82b4bc6e5e0
|
data/README.md
CHANGED
@@ -10,34 +10,57 @@ 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
|
+
signature: <<~EOF
|
38
|
+
--
|
39
|
+
Shugo Maeda <shugo@example.com>
|
40
|
+
EOF
|
41
|
+
},
|
42
|
+
"gmail.com" => {
|
43
|
+
from: "Example <example@gmail.com>",
|
44
|
+
delivery_method: :smtp,
|
45
|
+
delivery_options: {
|
46
|
+
address: "smtp.gmail.com",
|
47
|
+
port: 587,
|
48
|
+
domain: Socket.gethostname,
|
49
|
+
user_name: "example@gmail.com",
|
50
|
+
password: File.read("/path/to/gmail_passwd").chomp,
|
51
|
+
authentication: "login",
|
52
|
+
enable_starttls_auto: true
|
53
|
+
},
|
54
|
+
imap_host: "imap.gmail.com",
|
55
|
+
imap_options: {
|
56
|
+
auth_type: "PLAIN",
|
57
|
+
user_name: "example@gmail.com",
|
58
|
+
password: File.read(File.expand_path("~/.textbringer/gmail_passwd")).chomp,
|
59
|
+
ssl: true
|
60
|
+
},
|
61
|
+
spam_mailbox: "[Gmail]/迷惑メール",
|
62
|
+
archive_mailbox_format: false
|
37
63
|
},
|
38
|
-
auth_type: "PLAIN",
|
39
|
-
user_name: "shugo",
|
40
|
-
password: File.read("/path/to/imap_passwd").chomp
|
41
64
|
}
|
42
65
|
```
|
43
66
|
|
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "groonga"
|
4
|
+
|
5
|
+
if ARGV.size < 1
|
6
|
+
STDERR.puts "Usage: mournmail_reindex <path to messages.db>"
|
7
|
+
exit 1
|
8
|
+
end
|
9
|
+
|
10
|
+
Groonga::Database.open(ARGV[0])
|
11
|
+
Groonga["Messages"].columns.each do |c|
|
12
|
+
print "Reindexing #{c.name}... "
|
13
|
+
STDOUT.flush
|
14
|
+
c.reindex
|
15
|
+
puts "done"
|
16
|
+
end
|
data/lib/mournmail.rb
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
require_relative "mournmail/version"
|
4
2
|
require_relative "mournmail/config"
|
5
3
|
require_relative "mournmail/faces"
|
@@ -9,4 +7,6 @@ require_relative "mournmail/summary"
|
|
9
7
|
require_relative "mournmail/draft_mode"
|
10
8
|
require_relative "mournmail/summary_mode"
|
11
9
|
require_relative "mournmail/message_mode"
|
10
|
+
require_relative "mournmail/search_result_mode"
|
12
11
|
require_relative "mournmail/commands"
|
12
|
+
require_relative "mournmail/mail_encoded_word_patch"
|
data/lib/mournmail/commands.rb
CHANGED
@@ -1,12 +1,22 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
define_command(:mournmail, doc: "Start mournmail.") do
|
2
|
+
Mournmail.open_groonga_db
|
4
3
|
mournmail_visit_mailbox("INBOX")
|
5
4
|
end
|
6
5
|
|
7
|
-
define_command(:mournmail_visit_mailbox, doc: "
|
8
|
-
|mailbox =
|
9
|
-
|
6
|
+
define_command(:mournmail_visit_mailbox, doc: "Visit mailbox") do
|
7
|
+
|mailbox = Mournmail.read_mailbox_name("Visit mailbox: ", default: "INBOX")|
|
8
|
+
summary = Mournmail::Summary.load_or_new(mailbox)
|
9
|
+
foreground do
|
10
|
+
Mournmail.show_summary(summary)
|
11
|
+
end
|
12
|
+
end
|
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))
|
10
20
|
end
|
11
21
|
|
12
22
|
define_command(:mournmail_summary_sync, doc: "Sync summary.") do
|
@@ -15,30 +25,10 @@ define_command(:mournmail_summary_sync, doc: "Sync summary.") do
|
|
15
25
|
message("Syncing #{mailbox} in background...")
|
16
26
|
Mournmail.background do
|
17
27
|
summary = Mournmail.fetch_summary(mailbox, all: all)
|
18
|
-
summary_text = summary.to_s
|
19
28
|
summary.save
|
20
|
-
|
21
|
-
|
22
|
-
read_only: true)
|
23
|
-
buffer.apply_mode(Mournmail::SummaryMode)
|
24
|
-
buffer.read_only_edit do
|
25
|
-
buffer.clear
|
26
|
-
buffer.insert(summary_text)
|
27
|
-
end
|
28
|
-
switch_to_buffer(buffer)
|
29
|
-
Mournmail.current_mailbox = mailbox
|
30
|
-
Mournmail.current_summary = summary
|
31
|
-
Mournmail.current_mail = nil
|
32
|
-
Mournmail.current_uid = nil
|
29
|
+
foreground do
|
30
|
+
Mournmail.show_summary(summary)
|
33
31
|
message("Syncing #{mailbox} in background... Done")
|
34
|
-
begin
|
35
|
-
buffer.beginning_of_buffer
|
36
|
-
buffer.re_search_forward(/^\d+ u/)
|
37
|
-
rescue SearchError
|
38
|
-
buffer.end_of_buffer
|
39
|
-
buffer.re_search_backward(/^\d+ /, raise_error: false)
|
40
|
-
end
|
41
|
-
summary_read_command
|
42
32
|
end
|
43
33
|
end
|
44
34
|
end
|
@@ -63,6 +53,7 @@ define_command(:mournmail_quit, doc: "Quit mournmail.") do
|
|
63
53
|
Mournmail.current_summary = nil
|
64
54
|
Mournmail.current_mail = nil
|
65
55
|
Mournmail.current_uid = nil
|
56
|
+
Mournmail.close_groonga_db
|
66
57
|
end
|
67
58
|
|
68
59
|
define_command(:mail, doc: "Write a new mail.") do
|
@@ -70,16 +61,18 @@ define_command(:mail, doc: "Write a new mail.") do
|
|
70
61
|
buffer = Buffer.new_buffer("*draft*")
|
71
62
|
switch_to_buffer(buffer)
|
72
63
|
draft_mode
|
64
|
+
conf = Mournmail.account_config
|
73
65
|
insert <<~EOF
|
74
|
-
From: #{
|
66
|
+
From: #{conf[:from]}
|
75
67
|
To:
|
76
68
|
Subject:
|
77
69
|
User-Agent: Mournmail/#{Mournmail::VERSION} Textbringer/#{Textbringer::VERSION} Ruby/#{RUBY_VERSION}
|
78
70
|
--text follows this line--
|
79
71
|
EOF
|
80
|
-
|
81
|
-
|
72
|
+
beginning_of_buffer
|
73
|
+
re_search_forward(/^To: */)
|
82
74
|
if run_hooks
|
75
|
+
Mournmail.insert_signature
|
83
76
|
run_hooks(:mournmail_draft_setup_hook)
|
84
77
|
end
|
85
78
|
end
|
data/lib/mournmail/config.rb
CHANGED
@@ -1,10 +1,6 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
module Textbringer
|
4
2
|
CONFIG[:mournmail_directory] = File.expand_path("~/.mournmail")
|
5
|
-
CONFIG[:
|
6
|
-
CONFIG[:mournmail_delivery_method] = :smtp
|
7
|
-
CONFIG[:mournmail_delivery_options] = {}
|
3
|
+
CONFIG[:mournmail_accounts] = {}
|
8
4
|
CONFIG[:mournmail_charset] = "utf-8"
|
9
5
|
CONFIG[:mournmail_save_directory] = "/tmp"
|
10
6
|
CONFIG[:mournmail_display_header_fields] = [
|
@@ -18,9 +14,26 @@ module Textbringer
|
|
18
14
|
"X-Mailer",
|
19
15
|
"Content-Type"
|
20
16
|
]
|
17
|
+
CONFIG[:mournmail_quote_header_fields] = [
|
18
|
+
"Subject",
|
19
|
+
"Date",
|
20
|
+
"From",
|
21
|
+
"To",
|
22
|
+
"Cc"
|
23
|
+
]
|
21
24
|
CONFIG[:mournmail_imap_connect_timeout] = 10
|
22
|
-
CONFIG[:
|
23
|
-
|
24
|
-
|
25
|
+
CONFIG[:mournmail_keep_alive_interval] = 60
|
26
|
+
case RUBY_PLATFORM
|
27
|
+
when /mswin|mingw/
|
28
|
+
CONFIG[:mournmail_file_open_comamnd] = "start"
|
29
|
+
CONFIG[:mournmail_link_open_comamnd] = "start"
|
30
|
+
when /darwin/
|
31
|
+
CONFIG[:mournmail_file_open_comamnd] = "open"
|
32
|
+
CONFIG[:mournmail_link_open_comamnd] = "open"
|
33
|
+
else
|
34
|
+
CONFIG[:mournmail_file_open_comamnd] = "xdg-open"
|
35
|
+
CONFIG[:mournmail_link_open_comamnd] = "xdg-open"
|
36
|
+
end
|
25
37
|
CONFIG[:mournmail_addresses_path] = File.expand_path("~/.addresses")
|
38
|
+
CONFIG[:mournmail_signature_regexp] = /^\n-- /
|
26
39
|
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
|
@@ -7,6 +5,10 @@ module Mournmail
|
|
7
5
|
MAIL_MODE_MAP.define_key("\C-c\C-k", :draft_kill_command)
|
8
6
|
MAIL_MODE_MAP.define_key("\C-c\C-x\C-i", :draft_attach_file_command)
|
9
7
|
MAIL_MODE_MAP.define_key("\t", :draft_complete_or_insert_tab_command)
|
8
|
+
MAIL_MODE_MAP.define_key("\C-c\C-xv", :draft_pgp_sign_command)
|
9
|
+
MAIL_MODE_MAP.define_key("\C-c\C-xe", :draft_pgp_encrypt_command)
|
10
|
+
MAIL_MODE_MAP.define_key("\C-c\t", :insert_signature_command)
|
11
|
+
MAIL_MODE_MAP.define_key("\C-c@", :draft_change_account_command)
|
10
12
|
|
11
13
|
define_syntax :field_name, /^[A-Za-z\-]+: /
|
12
14
|
define_syntax :quotation, /^>.*/
|
@@ -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,35 +65,58 @@ 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")
|
71
|
+
end
|
72
|
+
account = @buffer[:mournmail_delivery_account] ||
|
73
|
+
Mournmail.current_account
|
74
|
+
conf = CONFIG[:mournmail_accounts][account]
|
75
|
+
options = @buffer[:mournmail_delivery_options] ||
|
76
|
+
conf[:delivery_options]
|
77
|
+
if options[:authentication] == "gmail"
|
78
|
+
token = Mournmail.google_access_token(account)
|
79
|
+
options = options.merge(authentication: "xoauth2",
|
80
|
+
password: token)
|
59
81
|
end
|
60
|
-
m.delivery_method(
|
61
|
-
|
82
|
+
m.delivery_method(@buffer[:mournmail_delivery_method] ||
|
83
|
+
conf[:delivery_method],
|
84
|
+
options)
|
62
85
|
bury_buffer(@buffer)
|
63
|
-
background do
|
86
|
+
Mournmail.background do
|
64
87
|
begin
|
65
88
|
if !attached_messages.empty?
|
66
89
|
attached_messages.each do |attached_message|
|
67
|
-
|
68
|
-
s
|
90
|
+
cache_id = attached_message.strip
|
91
|
+
s = File.read(Mournmail.mail_cache_path(cache_id))
|
69
92
|
part = Mail::Part.new(content_type: "message/rfc822", body: s)
|
70
93
|
m.body << part
|
71
94
|
end
|
72
95
|
end
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
96
|
+
if pgp_sign || pgp_encrypt
|
97
|
+
m.gpg(sign: pgp_sign, encrypt: pgp_encrypt)
|
98
|
+
end
|
99
|
+
m.deliver
|
100
|
+
foreground do
|
101
|
+
message("Mail sent.")
|
102
|
+
end
|
103
|
+
cache_id = Mournmail.write_mail_cache(m.encoded)
|
104
|
+
Mournmail.index_mail(cache_id, m)
|
105
|
+
outbox = Mournmail.account_config[:outbox_mailbox]
|
106
|
+
if outbox
|
107
|
+
Mournmail.imap_connect do |imap|
|
108
|
+
unless imap.list("", outbox)
|
109
|
+
imap.create(outbox)
|
110
|
+
end
|
77
111
|
imap.append(outbox, m.to_s, [:Seen])
|
78
112
|
end
|
79
113
|
end
|
80
|
-
|
114
|
+
foreground do
|
81
115
|
kill_buffer(@buffer, force: true)
|
82
116
|
Mournmail.back_to_summary
|
83
|
-
message("Mail sent.")
|
84
117
|
end
|
85
118
|
rescue Exception
|
86
|
-
|
119
|
+
foreground do
|
87
120
|
switch_to_buffer(@buffer)
|
88
121
|
end
|
89
122
|
raise
|
@@ -101,9 +134,7 @@ module Mournmail
|
|
101
134
|
define_local_command(:draft_attach_file, doc: "Attach a file.") do
|
102
135
|
|file_name = read_file_name("Attach file: ")|
|
103
136
|
@buffer.save_excursion do
|
104
|
-
|
105
|
-
@buffer.re_search_forward(/^--text follows this line--$/)
|
106
|
-
@buffer.beginning_of_line
|
137
|
+
end_of_header
|
107
138
|
@buffer.insert("Attached-File: #{file_name}\n")
|
108
139
|
end
|
109
140
|
end
|
@@ -120,10 +151,15 @@ module Mournmail
|
|
120
151
|
start_pos = @buffer.point
|
121
152
|
s = @buffer.substring(start_pos, end_pos)
|
122
153
|
if !s.empty?
|
123
|
-
re = /^(
|
124
|
-
|
125
|
-
|
126
|
-
|
154
|
+
re = /^(?:.*")?#{Regexp.quote(s)}.*/
|
155
|
+
addrs = File.read(CONFIG[:mournmail_addresses_path])
|
156
|
+
.scan(re).map { |line| line.slice(/^\S+/) }
|
157
|
+
if !addrs.empty?
|
158
|
+
addr = addrs.inject { |x, y|
|
159
|
+
x.chars.zip(y.chars).take_while { |i, j|
|
160
|
+
i == j
|
161
|
+
}.map { |i,| i }.join
|
162
|
+
}
|
127
163
|
@buffer.delete_region(start_pos, end_pos)
|
128
164
|
@buffer.insert(addr)
|
129
165
|
else
|
@@ -135,5 +171,48 @@ module Mournmail
|
|
135
171
|
@buffer.insert("\t")
|
136
172
|
end
|
137
173
|
end
|
174
|
+
|
175
|
+
define_local_command(:draft_pgp_sign, doc: "PGP sign.") do
|
176
|
+
@buffer.save_excursion do
|
177
|
+
end_of_header
|
178
|
+
@buffer.insert("PGP-Sign: yes\n")
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
define_local_command(:draft_pgp_encrypt, doc: "PGP encrypt.") do
|
183
|
+
@buffer.save_excursion do
|
184
|
+
end_of_header
|
185
|
+
@buffer.insert("PGP-Encrypt: yes\n")
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
define_local_command(:insert_signature, doc: "Insert signature.") do
|
190
|
+
@buffer.insert(CONFIG[:signature])
|
191
|
+
end
|
192
|
+
|
193
|
+
define_local_command(:draft_change_account, doc: "Change account.") do
|
194
|
+
|account = Mournmail.read_account_name("Change account: ")|
|
195
|
+
from = CONFIG[:mournmail_accounts][account][:from]
|
196
|
+
@buffer[:mournmail_delivery_account] = account
|
197
|
+
@buffer.save_excursion do
|
198
|
+
@buffer.beginning_of_buffer
|
199
|
+
@buffer.re_search_forward(/^From:.*/)
|
200
|
+
@buffer.replace_match("From: " + from)
|
201
|
+
@buffer.end_of_buffer
|
202
|
+
if @buffer.re_search_backward(CONFIG[:mournmail_signature_regexp],
|
203
|
+
raise_error: false)
|
204
|
+
@buffer.delete_region(@buffer.point, @buffer.point_max)
|
205
|
+
end
|
206
|
+
Mournmail.insert_signature
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
private
|
211
|
+
|
212
|
+
def end_of_header
|
213
|
+
@buffer.beginning_of_buffer
|
214
|
+
@buffer.re_search_forward(/^--text follows this line--$/)
|
215
|
+
@buffer.beginning_of_line
|
216
|
+
end
|
138
217
|
end
|
139
218
|
end
|