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 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"