mournmail 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +64 -0
- data/Rakefile +2 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/mournmail/commands.rb +85 -0
- data/lib/mournmail/config.rb +26 -0
- data/lib/mournmail/draft_mode.rb +139 -0
- data/lib/mournmail/faces.rb +13 -0
- data/lib/mournmail/message_mode.rb +132 -0
- data/lib/mournmail/message_rendering.rb +112 -0
- data/lib/mournmail/summary.rb +262 -0
- data/lib/mournmail/summary_mode.rb +303 -0
- data/lib/mournmail/utils.rb +161 -0
- data/lib/mournmail/version.rb +5 -0
- data/lib/mournmail.rb +12 -0
- data/lib/textbringer_plugin.rb +3 -0
- data/mournmail.gemspec +30 -0
- metadata +135 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: bce6a83b1d41699944c7440f0737318437931a7e
|
4
|
+
data.tar.gz: 592a006b174161b564ce302739e19382ef07fda9
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: bda483a4723883895517a781308ee8305fc60bc5fab09e6c8ef3181ab770d04304a7f72ccc0b85f23cfa198ae1574ba5ec80c6c9fa961c7a7c17e1da31dc0dd0
|
7
|
+
data.tar.gz: 79f6879cc3b2737855a3dac77768bda6b440a0303400daa7c8085ae4fb0639c440e6ec0246cfcfc8f4bbefecf8dab6657c57291120e162ee3700f8d88262d939
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2017 Shugo Maeda
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
# Mournmail
|
2
|
+
|
3
|
+
Mournmail is a message user agent for
|
4
|
+
[Textbringer](https://github.com/shugo/textbringer).
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
$ gem install mournmail
|
9
|
+
|
10
|
+
## Configuration
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
# The default value of From:
|
14
|
+
CONFIG[:mournmail_from] = "Shugo Maeda <shugo@example.com>"
|
15
|
+
# The default charset
|
16
|
+
CONFIG[:mournmail_charset] = "ISO-2022-JP"
|
17
|
+
# The delivery method for Mail#delivery_method
|
18
|
+
CONFIG[:mournmail_delivery_method] = :smtp
|
19
|
+
# The options for Mail#delivery_method
|
20
|
+
CONFIG[:mournmail_delivery_options] = {
|
21
|
+
:address => "smtp.example.com",
|
22
|
+
:port => 465,
|
23
|
+
:domain => Socket.gethostname,
|
24
|
+
:user_name => "shugo",
|
25
|
+
:password => File.read("/path/to/smtp_passwd").chomp,
|
26
|
+
:authentication => "login",
|
27
|
+
:tls => true,
|
28
|
+
:ca_file => "/path/to/cacert.pem"
|
29
|
+
}
|
30
|
+
# The host for Net::IMAP#new
|
31
|
+
CONFIG[:mournmail_imap_host] = "imap.example.com"
|
32
|
+
# The options for Net::IMAP.new and
|
33
|
+
# Net::IMAP#authenticate (auth_type, user_name, and password)
|
34
|
+
CONFIG[:mournmail_imap_options] = {
|
35
|
+
ssl: {
|
36
|
+
:ca_file => File.expand_path("/path/to/cacert.pem")
|
37
|
+
},
|
38
|
+
auth_type: "PLAIN",
|
39
|
+
user_name: "shugo",
|
40
|
+
password: File.read("/path/to/imap_passwd").chomp
|
41
|
+
}
|
42
|
+
```
|
43
|
+
|
44
|
+
## Usage
|
45
|
+
|
46
|
+
Type `M-x mail` to send a mail.
|
47
|
+
|
48
|
+
Type `M-x mournmail` to visit INBOX.
|
49
|
+
|
50
|
+
## Development
|
51
|
+
|
52
|
+
After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
53
|
+
|
54
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
55
|
+
|
56
|
+
## Contributing
|
57
|
+
|
58
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/shugo/mournmail.
|
59
|
+
|
60
|
+
|
61
|
+
## License
|
62
|
+
|
63
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
64
|
+
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "mournmail"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
define_command(:mournmail, doc: "Start mournmail.") do
|
4
|
+
mournmail_visit_mailbox("INBOX")
|
5
|
+
end
|
6
|
+
|
7
|
+
define_command(:mournmail_visit_mailbox, doc: "Start mournmail.") do
|
8
|
+
|mailbox = read_from_minibuffer("Visit mailbox: ", default: "INBOX")|
|
9
|
+
mournmail_summary_sync(mailbox)
|
10
|
+
end
|
11
|
+
|
12
|
+
define_command(:mournmail_summary_sync, doc: "Sync summary.") do
|
13
|
+
|mailbox = (Mournmail.current_mailbox || "INBOX"),
|
14
|
+
all = current_prefix_arg|
|
15
|
+
message("Syncing #{mailbox} in background...")
|
16
|
+
Mournmail.background do
|
17
|
+
summary = Mournmail.fetch_summary(mailbox, all: all)
|
18
|
+
summary_text = summary.to_s
|
19
|
+
summary.save
|
20
|
+
next_tick do
|
21
|
+
buffer = Buffer.find_or_new("*summary*", undo_limit: 0,
|
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
|
33
|
+
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
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
define_command(:mournmail_quit, doc: "Quit mournmail.") do
|
47
|
+
th = Mournmail.background_thread
|
48
|
+
if th
|
49
|
+
return unless yes_or_no?("A background process is running. Kill it?")
|
50
|
+
th.kill
|
51
|
+
end
|
52
|
+
delete_other_windows
|
53
|
+
if buffer = Buffer["*summary*"]
|
54
|
+
kill_buffer(buffer)
|
55
|
+
end
|
56
|
+
if buffer = Buffer["*message*"]
|
57
|
+
kill_buffer(buffer)
|
58
|
+
end
|
59
|
+
Mournmail.background do
|
60
|
+
Mournmail.imap_disconnect
|
61
|
+
end
|
62
|
+
Mournmail.current_mailbox = nil
|
63
|
+
Mournmail.current_summary = nil
|
64
|
+
Mournmail.current_mail = nil
|
65
|
+
Mournmail.current_uid = nil
|
66
|
+
end
|
67
|
+
|
68
|
+
define_command(:mail, doc: "Write a new mail.") do
|
69
|
+
|run_hooks: true|
|
70
|
+
buffer = Buffer.new_buffer("*draft*")
|
71
|
+
switch_to_buffer(buffer)
|
72
|
+
draft_mode
|
73
|
+
insert <<~EOF
|
74
|
+
From: #{CONFIG[:mournmail_from]}
|
75
|
+
To:
|
76
|
+
Subject:
|
77
|
+
User-Agent: Mournmail/#{Mournmail::VERSION} Textbringer/#{Textbringer::VERSION} Ruby/#{RUBY_VERSION}
|
78
|
+
--text follows this line--
|
79
|
+
EOF
|
80
|
+
re_search_backward(/^To:/)
|
81
|
+
end_of_line
|
82
|
+
if run_hooks
|
83
|
+
run_hooks(:mournmail_draft_setup_hook)
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Textbringer
|
4
|
+
CONFIG[:mournmail_directory] = File.expand_path("~/.mournmail")
|
5
|
+
CONFIG[:mournmail_from] = ""
|
6
|
+
CONFIG[:mournmail_delivery_method] = :smtp
|
7
|
+
CONFIG[:mournmail_delivery_options] = {}
|
8
|
+
CONFIG[:mournmail_charset] = "utf-8"
|
9
|
+
CONFIG[:mournmail_save_directory] = "/tmp"
|
10
|
+
CONFIG[:mournmail_display_header_fields] = [
|
11
|
+
"Subject",
|
12
|
+
"Date",
|
13
|
+
"From",
|
14
|
+
"To",
|
15
|
+
"Cc",
|
16
|
+
"Reply-To",
|
17
|
+
"User-Agent",
|
18
|
+
"X-Mailer",
|
19
|
+
"Content-Type"
|
20
|
+
]
|
21
|
+
CONFIG[:mournmail_imap_connect_timeout] = 10
|
22
|
+
CONFIG[:mournmail_file_open_comamnd] = "xdg-open"
|
23
|
+
CONFIG[:mournmail_link_open_comamnd] = "xdg-open"
|
24
|
+
CONFIG[:mournmail_outbox] = nil
|
25
|
+
CONFIG[:mournmail_addresses_path] = File.expand_path("~/.addresses")
|
26
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mournmail
|
4
|
+
class DraftMode < Textbringer::Mode
|
5
|
+
MAIL_MODE_MAP = Keymap.new
|
6
|
+
MAIL_MODE_MAP.define_key("\C-c\C-c", :draft_send_command)
|
7
|
+
MAIL_MODE_MAP.define_key("\C-c\C-k", :draft_kill_command)
|
8
|
+
MAIL_MODE_MAP.define_key("\C-c\C-x\C-i", :draft_attach_file_command)
|
9
|
+
MAIL_MODE_MAP.define_key("\t", :draft_complete_or_insert_tab_command)
|
10
|
+
|
11
|
+
define_syntax :field_name, /^[A-Za-z\-]+: /
|
12
|
+
define_syntax :quotation, /^>.*/
|
13
|
+
define_syntax :header_end, /^--text follows this line--$/
|
14
|
+
|
15
|
+
def initialize(buffer)
|
16
|
+
super(buffer)
|
17
|
+
buffer.keymap = MAIL_MODE_MAP
|
18
|
+
end
|
19
|
+
|
20
|
+
define_local_command(:draft_send,
|
21
|
+
doc: "Send a mail and exit from mail buffer.") do
|
22
|
+
unless y_or_n?("Send this mail?")
|
23
|
+
return
|
24
|
+
end
|
25
|
+
s = @buffer.to_s
|
26
|
+
charset = CONFIG[:mournmail_charset]
|
27
|
+
begin
|
28
|
+
s.encode(charset)
|
29
|
+
rescue Encoding::UndefinedConversionError
|
30
|
+
charset = "utf-8"
|
31
|
+
end
|
32
|
+
m = Mail.new(charset: charset)
|
33
|
+
header, body = s.split(/^--text follows this line--\n/, 2)
|
34
|
+
attached_files = []
|
35
|
+
attached_messages = []
|
36
|
+
header.scan(/^([!-9;-~]+):[ \t]*(.*(?:\n[ \t].*)*)\n/) do |name, val|
|
37
|
+
case name
|
38
|
+
when "Attached-File"
|
39
|
+
attached_files.push(val.strip)
|
40
|
+
when "Attached-Message"
|
41
|
+
attached_messages.push(val.strip)
|
42
|
+
else
|
43
|
+
m[name] = val
|
44
|
+
end
|
45
|
+
end
|
46
|
+
if body.empty?
|
47
|
+
return if !yes_or_no?("Body is empty. Really send?")
|
48
|
+
else
|
49
|
+
if attached_files.empty? && attached_messages.empty?
|
50
|
+
m.body = body
|
51
|
+
else
|
52
|
+
part = Mail::Part.new(content_type: "text/plain", body: body)
|
53
|
+
part.charset = charset
|
54
|
+
m.body << part
|
55
|
+
end
|
56
|
+
end
|
57
|
+
attached_files.each do |file|
|
58
|
+
m.add_file(file)
|
59
|
+
end
|
60
|
+
m.delivery_method(CONFIG[:mournmail_delivery_method],
|
61
|
+
CONFIG[:mournmail_delivery_options])
|
62
|
+
bury_buffer(@buffer)
|
63
|
+
background do
|
64
|
+
begin
|
65
|
+
if !attached_messages.empty?
|
66
|
+
attached_messages.each do |attached_message|
|
67
|
+
mailbox, uid = attached_message.strip.split("/")
|
68
|
+
s = Mournmail.read_mail(mailbox, uid.to_i)
|
69
|
+
part = Mail::Part.new(content_type: "message/rfc822", body: s)
|
70
|
+
m.body << part
|
71
|
+
end
|
72
|
+
end
|
73
|
+
m.deliver!
|
74
|
+
Mournmail.imap_connect do |imap|
|
75
|
+
outbox = CONFIG[:mournmail_outbox]
|
76
|
+
if outbox
|
77
|
+
imap.append(outbox, m.to_s, [:Seen])
|
78
|
+
end
|
79
|
+
end
|
80
|
+
next_tick do
|
81
|
+
kill_buffer(@buffer, force: true)
|
82
|
+
Mournmail.back_to_summary
|
83
|
+
message("Mail sent.")
|
84
|
+
end
|
85
|
+
rescue Exception
|
86
|
+
next_tick do
|
87
|
+
switch_to_buffer(@buffer)
|
88
|
+
end
|
89
|
+
raise
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
define_local_command(:draft_kill, doc: "Kill the draft buffer.") do
|
95
|
+
if yes_or_no?("Kill current draft?")
|
96
|
+
kill_buffer(@buffer, force: true)
|
97
|
+
Mournmail.back_to_summary
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
define_local_command(:draft_attach_file, doc: "Attach a file.") do
|
102
|
+
|file_name = read_file_name("Attach file: ")|
|
103
|
+
@buffer.save_excursion do
|
104
|
+
@buffer.beginning_of_buffer
|
105
|
+
@buffer.re_search_forward(/^--text follows this line--$/)
|
106
|
+
@buffer.beginning_of_line
|
107
|
+
@buffer.insert("Attached-File: #{file_name}\n")
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
define_local_command(:draft_complete_or_insert_tab,
|
112
|
+
doc: "Complete a mail address or insert a tab.") do
|
113
|
+
is_address_field = @buffer.save_excursion {
|
114
|
+
@buffer.beginning_of_line
|
115
|
+
@buffer.looking_at?(/(To|Cc|Bcc):/i)
|
116
|
+
}
|
117
|
+
if is_address_field
|
118
|
+
end_pos = @buffer.point
|
119
|
+
@buffer.skip_re_backward(/[^ :,]/)
|
120
|
+
start_pos = @buffer.point
|
121
|
+
s = @buffer.substring(start_pos, end_pos)
|
122
|
+
if !s.empty?
|
123
|
+
re = /^(.*")?#{Regexp.quote(s)}.*/
|
124
|
+
line = File.read(CONFIG[:mournmail_addresses_path]).slice(re)
|
125
|
+
if line
|
126
|
+
addr = line.slice(/^\S+/)
|
127
|
+
@buffer.delete_region(start_pos, end_pos)
|
128
|
+
@buffer.insert(addr)
|
129
|
+
else
|
130
|
+
@buffer.goto_char(end_pos)
|
131
|
+
message("No match")
|
132
|
+
end
|
133
|
+
end
|
134
|
+
else
|
135
|
+
@buffer.insert("\t")
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Textbringer
|
4
|
+
Face.define :seen, foreground: "blue"
|
5
|
+
Face.define :deleted, foreground: "green"
|
6
|
+
Face.define :answered, foreground: "blue"
|
7
|
+
Face.define :unseen, bold: true
|
8
|
+
Face.define :flagged, foreground: "yellow", bold: true
|
9
|
+
Face.define :field_name, foreground: "magenta", bold: true
|
10
|
+
Face.define :quotation, foreground: "yellow"
|
11
|
+
Face.define :header_end, foreground: "yellow"
|
12
|
+
Face.define :mime_part, foreground: "blue", bold: true
|
13
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
using Mournmail::MessageRendering
|
4
|
+
|
5
|
+
module Mournmail
|
6
|
+
class MessageMode < Textbringer::Mode
|
7
|
+
MESSAGE_MODE_MAP = Keymap.new
|
8
|
+
MESSAGE_MODE_MAP.define_key("\C-m", :message_open_link_or_part_command)
|
9
|
+
MESSAGE_MODE_MAP.define_key("s", :message_save_part_command)
|
10
|
+
|
11
|
+
# See http://nihongo.jp/support/mail_guide/dev_guide.txt
|
12
|
+
URI_REGEXP = /(https?|ftp):\/\/[^ \t\n>)"]*[^] \t\n>.,:)"]+/
|
13
|
+
|
14
|
+
define_syntax :field_name, /^[A-Za-z\-]+: /
|
15
|
+
define_syntax :quotation, /^>.*/
|
16
|
+
define_syntax :mime_part, /^\[([0-9.]+) [A-Za-z._\-]+\/[A-Za-z._\-]+.*\]$/
|
17
|
+
define_syntax :link, URI_REGEXP
|
18
|
+
|
19
|
+
def initialize(buffer)
|
20
|
+
super(buffer)
|
21
|
+
buffer.keymap = MESSAGE_MODE_MAP
|
22
|
+
end
|
23
|
+
|
24
|
+
define_local_command(:message_open_link_or_part,
|
25
|
+
doc: "Open a link or part.") do
|
26
|
+
part = current_part
|
27
|
+
if part
|
28
|
+
open_part(part)
|
29
|
+
else
|
30
|
+
uri = current_uri
|
31
|
+
if uri
|
32
|
+
open_uri(uri)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
define_local_command(:message_save_part, doc: "Save the current part.") do
|
38
|
+
part = current_part
|
39
|
+
return if part.nil?
|
40
|
+
default_path = File.expand_path(part_file_name(part),
|
41
|
+
CONFIG[:mournmail_save_directory])
|
42
|
+
path = read_file_name("Save: ", default: default_path)
|
43
|
+
if !File.exist?(path) || yes_or_no?("File exists; overwrite?")
|
44
|
+
File.write(path, part.decoded)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def current_part
|
51
|
+
@buffer.save_excursion do
|
52
|
+
@buffer.beginning_of_line
|
53
|
+
if @buffer.looking_at?(/\[([0-9.]+) .*\]/)
|
54
|
+
index = match_string(1)
|
55
|
+
indices = index.split(".").map(&:to_i)
|
56
|
+
Mournmail.current_mail.dig_part(*indices)
|
57
|
+
else
|
58
|
+
nil
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def part_file_name(part)
|
64
|
+
file_name =
|
65
|
+
part["content-disposition"]&.parameters&.[]("filename") ||
|
66
|
+
part["content-type"]&.parameters&.[]("name") ||
|
67
|
+
part_default_file_name(part)
|
68
|
+
decoded_file_name = Mail::Encodings.decode_encode(file_name, :decode)
|
69
|
+
if /\A([A-Za-z0-9_\-]+)'(?:[A-Za-z0-9_\-])*'(.*)/ =~ decoded_file_name
|
70
|
+
$2.encode("utf-8", $1)
|
71
|
+
else
|
72
|
+
decoded_file_name
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def part_default_file_name(part)
|
77
|
+
base_name = part.cid.gsub(/[^A-Za-z0-9_\-]/, "_")
|
78
|
+
ext = part_extension(part)
|
79
|
+
if ext
|
80
|
+
base_name + "." + ext
|
81
|
+
else
|
82
|
+
base_name
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def part_extension(part)
|
87
|
+
mime_type = part["content-type"].string
|
88
|
+
MIME::Types[mime_type]&.first&.preferred_extension
|
89
|
+
end
|
90
|
+
|
91
|
+
def current_uri
|
92
|
+
@buffer.save_excursion do
|
93
|
+
pos = @buffer.point
|
94
|
+
@buffer.beginning_of_line
|
95
|
+
pos2 = @buffer.re_search_forward(URI_REGEXP, raise_error: false)
|
96
|
+
if pos2 && match_beginning(0) <= pos && pos < match_end(0)
|
97
|
+
match_string(0)
|
98
|
+
else
|
99
|
+
nil
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def open_part(part)
|
105
|
+
if part.multipart?
|
106
|
+
raise EditorError, "Can't open a multipart entity."
|
107
|
+
end
|
108
|
+
ext = part_file_name(part).slice(/\.([^.]+)\z/, 1)
|
109
|
+
if ext
|
110
|
+
file_name = ["mournmail", "." + ext]
|
111
|
+
else
|
112
|
+
file_name = "mournmail"
|
113
|
+
end
|
114
|
+
f = Tempfile.open(file_name)
|
115
|
+
f.write(part.decoded)
|
116
|
+
f.close
|
117
|
+
if ext == "txt"
|
118
|
+
find_file(f.path)
|
119
|
+
else
|
120
|
+
background do
|
121
|
+
system(*CONFIG[:mournmail_file_open_comamnd], f.path,
|
122
|
+
out: File::NULL, err: File::NULL)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def open_uri(uri)
|
128
|
+
system(*CONFIG[:mournmail_link_open_comamnd], uri,
|
129
|
+
out: File::NULL, err: File::NULL)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "mail"
|
4
|
+
require "mail-iso-2022-jp"
|
5
|
+
|
6
|
+
module Mournmail
|
7
|
+
module MessageRendering
|
8
|
+
refine ::Mail::Message do
|
9
|
+
def render(indices = [])
|
10
|
+
render_header + "\n" + render_body(indices)
|
11
|
+
end
|
12
|
+
|
13
|
+
def render_header
|
14
|
+
CONFIG[:mournmail_display_header_fields].map { |name|
|
15
|
+
val = self[name]
|
16
|
+
val ? "#{name}: #{val}\n" : ""
|
17
|
+
}.join
|
18
|
+
end
|
19
|
+
|
20
|
+
def render_body(indices = [])
|
21
|
+
if HAVE_MAIL_GPG && encrypted?
|
22
|
+
mail = decrypt(verify: true)
|
23
|
+
return mail.render_body(indices)
|
24
|
+
end
|
25
|
+
if multipart?
|
26
|
+
parts.each_with_index.map { |part, i|
|
27
|
+
part.render([*indices, i])
|
28
|
+
}.join
|
29
|
+
else
|
30
|
+
s = body.decoded
|
31
|
+
if /\Autf-8\z/i =~ charset
|
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
|
+
end + pgp_signature
|
37
|
+
end
|
38
|
+
|
39
|
+
def dig_part(i, *rest_indices)
|
40
|
+
if HAVE_MAIL_GPG && encrypted?
|
41
|
+
mail = decrypt(verify: true)
|
42
|
+
return mail.dig_part(i, *rest_indices)
|
43
|
+
end
|
44
|
+
part = parts[i]
|
45
|
+
if rest_indices.empty?
|
46
|
+
part
|
47
|
+
else
|
48
|
+
part.dig_part(*rest_indices)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def pgp_signature
|
55
|
+
if HAVE_MAIL_GPG && signed?
|
56
|
+
verified = verify
|
57
|
+
validity = verified.signature_valid? ? "Good" : "Bad"
|
58
|
+
from = verified.signatures.map { |sig|
|
59
|
+
sig.from rescue sig.fingerprint
|
60
|
+
}.join(", ")
|
61
|
+
"#{validity} signature from #{from}\n"
|
62
|
+
else
|
63
|
+
""
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
refine ::Mail::Part do
|
69
|
+
def render(indices)
|
70
|
+
index = indices.join(".")
|
71
|
+
type = Mail::Encodings.decode_encode(self["content-type"].to_s,
|
72
|
+
:decode)
|
73
|
+
"[#{index} #{type}]\n" + render_content(indices)
|
74
|
+
end
|
75
|
+
|
76
|
+
def dig_part(i, *rest_indices)
|
77
|
+
if main_type == "message" && sub_type == "rfc822"
|
78
|
+
mail = Mail.new(body.raw_source)
|
79
|
+
mail.dig_part(i, *rest_indices)
|
80
|
+
else
|
81
|
+
part = parts[i]
|
82
|
+
if rest_indices.empty?
|
83
|
+
part
|
84
|
+
else
|
85
|
+
part.dig_part(*rest_indices)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def render_content(indices)
|
93
|
+
if multipart?
|
94
|
+
parts.each_with_index.map { |part, i|
|
95
|
+
part.render([*indices, i])
|
96
|
+
}.join
|
97
|
+
elsif main_type == "message" && sub_type == "rfc822"
|
98
|
+
mail = Mail.new(body.raw_source)
|
99
|
+
mail.render(indices)
|
100
|
+
elsif self["content-disposition"]&.disposition_type == "attachment"
|
101
|
+
""
|
102
|
+
else
|
103
|
+
if main_type == "text" && sub_type == "plain"
|
104
|
+
decoded.sub(/(?<!\n)\z/, "\n").gsub(/\r\n/, "\n")
|
105
|
+
else
|
106
|
+
""
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|