mournmail 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in mournmail.gemspec
4
+ gemspec
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
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
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,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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