mournmail 0.1.1 → 1.0.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: 8cbf82ccf54ab48f7596d24aaacb4673ca8f8271f304264df7aa96a932dac2fb
4
+ data.tar.gz: facbe695e21c68a1b5c50a9bb9d50894f948e09bab2a436b2174656953b809bf
5
5
  SHA512:
6
- metadata.gz: ea1353b03b02800c953d1df9c088400ec1d5698953105f9a0b8c55f1b387d61367061e83f873a891bfbcd73c3bf7a6f17e5a0b539d1cbe16b052b104cb88b18e
7
- data.tar.gz: 97a308e32677cc6a51490d1be5f4528159760e22866ceef39d33f977ee94fadd3fd95369ab72199baf8d0f5a47cb9daad435b5d0d971e20b2013a82bcc41e2f5
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
- # 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
+ 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"
@@ -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: "Start mournmail.") do
8
- |mailbox = read_from_minibuffer("Visit mailbox: ", default: "INBOX")|
9
- mournmail_summary_sync(mailbox)
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
- 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
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: #{CONFIG[:mournmail_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
- re_search_backward(/^To:/)
81
- end_of_line
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
@@ -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[:mournmail_from] = ""
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[:mournmail_file_open_comamnd] = "xdg-open"
23
- CONFIG[:mournmail_link_open_comamnd] = "xdg-open"
24
- CONFIG[:mournmail_outbox] = nil
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
@@ -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(CONFIG[:mournmail_delivery_method],
61
- CONFIG[:mournmail_delivery_options])
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
- mailbox, uid = attached_message.strip.split("/")
68
- s, = Mournmail.read_mail(mailbox, uid.to_i)
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
- m.deliver!
74
- Mournmail.imap_connect do |imap|
75
- outbox = CONFIG[:mournmail_outbox]
76
- if outbox
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
- next_tick do
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
- next_tick do
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
- @buffer.beginning_of_buffer
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 = /^(.*")?#{Regexp.quote(s)}.*/
124
- line = File.read(CONFIG[:mournmail_addresses_path]).slice(re)
125
- if line
126
- addr = line.slice(/^\S+/)
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