mournmail 0.1.1 → 0.2.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 624dd4f9dacde981ede7170c87da6c8ddb3a6aae
4
- data.tar.gz: 9b1890ed2a07cdd551e4688bb88d7a80b1ff2c51
2
+ SHA256:
3
+ metadata.gz: 422feadab2e36499c76f69f1836ce4e384497f0e86814cb4c20d0966e70b69b8
4
+ data.tar.gz: 82423aa120aef42601ef86737e8461b96c32f786d9e7e647c80a4dfdc14a93f1
5
5
  SHA512:
6
- metadata.gz: ea1353b03b02800c953d1df9c088400ec1d5698953105f9a0b8c55f1b387d61367061e83f873a891bfbcd73c3bf7a6f17e5a0b539d1cbe16b052b104cb88b18e
7
- data.tar.gz: 97a308e32677cc6a51490d1be5f4528159760e22866ceef39d33f977ee94fadd3fd95369ab72199baf8d0f5a47cb9daad435b5d0d971e20b2013a82bcc41e2f5
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
- # 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")
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
 
@@ -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"
@@ -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 = read_from_minibuffer("Visit mailbox: ", default: "INBOX")|
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(/^\d+ u/)
37
+ buffer.re_search_forward(/^ *\d+ u/)
37
38
  rescue SearchError
38
39
  buffer.end_of_buffer
39
- buffer.re_search_backward(/^\d+ /, raise_error: false)
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: #{CONFIG[:mournmail_from]}
77
+ From: #{conf[:from]}
75
78
  To:
76
79
  Subject:
77
80
  User-Agent: Mournmail/#{Mournmail::VERSION} Textbringer/#{Textbringer::VERSION} Ruby/#{RUBY_VERSION}
@@ -2,9 +2,7 @@
2
2
 
3
3
  module Textbringer
4
4
  CONFIG[:mournmail_directory] = File.expand_path("~/.mournmail")
5
- CONFIG[:mournmail_from] = ""
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
@@ -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
- m.delivery_method(CONFIG[:mournmail_delivery_method],
61
- CONFIG[:mournmail_delivery_options])
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
- mailbox, uid = attached_message.strip.split("/")
68
- s, = Mournmail.read_mail(mailbox, uid.to_i)
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
- m.deliver!
74
- Mournmail.imap_connect do |imap|
75
- outbox = CONFIG[:mournmail_outbox]
76
- if outbox
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
- @buffer.beginning_of_buffer
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, /^\[([0-9.]+) [A-Za-z._\-]+\/[A-Za-z._\-]+.*\]$/
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
- Mournmail.current_mail.dig_part(*indices)
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
- return mail.render_body(indices)
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
- 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
+ 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
- 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"
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.raw_source)
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 self["content-disposition"]&.disposition_type == "attachment"
107
+ elsif attachment?
101
108
  ""
102
109
  else
103
110
  if main_type == "text" && sub_type == "plain"