mournmail 0.3.0 → 1.0.1

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
2
  SHA256:
3
- metadata.gz: f3e35ed64af5fb25eeedad4c3c901b93125baf6b561b8e2384da7a9ed602b367
4
- data.tar.gz: 9174888f40a57d0cffe48d13fef7cc0d2892be312d3ff0b95a0df7aef098c726
3
+ metadata.gz: d2cbdbd1f9d9fe5f74f7d098cbf1eae74e113d544705005a9ca95f652799c7e3
4
+ data.tar.gz: 384b036461c6ea691d7559bb17fec0c2439ebebf51e5694e49481844d25336a5
5
5
  SHA512:
6
- metadata.gz: f85ebaadd8f97a43e45856347ad0149594ab17db31c04ba66d85db61b336f3c7cd4548c725868a44522d87941297edc5ad5656901440400f834017ed2e650733
7
- data.tar.gz: ddff53280fad3432e0bf0da11cf58c57a60586886828fa512b1bf54cba8e57afc8e2687561ae8b289d95fde271b1679f7da4de56e451b4fd6c00ec9787ccd095
6
+ metadata.gz: 18831f3e5fff6e0a181791c56a926c866cfef9f2b210e5f47f7f0bca04c73695130cdd3f9924b9a7e359e1f479e878ea1c8c157092162e366513e62a9111eca3
7
+ data.tar.gz: b252222b12d59b599df9f81f5b610dff5649c0440782e89ca273602039738b8a3708f6499560ad461c88844a14c0bcb2e9216ddb21dbdd00471a82de52e71569
data/README.md CHANGED
@@ -33,7 +33,11 @@ CONFIG[:mournmail_accounts] = {
33
33
  },
34
34
  spam_mailbox: "spam",
35
35
  outbox_mailbox: "outbox",
36
- archive_mailbox_format: "archive/%Y"
36
+ archive_mailbox_format: "archive/%Y",
37
+ signature: <<~EOF
38
+ --
39
+ Shugo Maeda <shugo@example.com>
40
+ EOF
37
41
  },
38
42
  "gmail.com" => {
39
43
  from: "Example <example@gmail.com>",
@@ -54,7 +58,7 @@ CONFIG[:mournmail_accounts] = {
54
58
  password: File.read(File.expand_path("~/.textbringer/gmail_passwd")).chomp,
55
59
  ssl: true
56
60
  },
57
- spam_mailbox: "迷惑メール",
61
+ spam_mailbox: "[Gmail]/迷惑メール",
58
62
  archive_mailbox_format: false
59
63
  },
60
64
  }
@@ -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
@@ -11,6 +11,14 @@ define_command(:mournmail_visit_mailbox, doc: "Visit mailbox") do
11
11
  end
12
12
  end
13
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))
20
+ end
21
+
14
22
  define_command(:mournmail_summary_sync, doc: "Sync summary.") do
15
23
  |mailbox = (Mournmail.current_mailbox || "INBOX"),
16
24
  all = current_prefix_arg|
@@ -61,9 +69,10 @@ define_command(:mail, doc: "Write a new mail.") do
61
69
  User-Agent: Mournmail/#{Mournmail::VERSION} Textbringer/#{Textbringer::VERSION} Ruby/#{RUBY_VERSION}
62
70
  --text follows this line--
63
71
  EOF
64
- re_search_backward(/^To:/)
65
- end_of_line
72
+ beginning_of_buffer
73
+ re_search_forward(/^To: */)
66
74
  if run_hooks
75
+ Mournmail.insert_signature
67
76
  run_hooks(:mournmail_draft_setup_hook)
68
77
  end
69
78
  end
@@ -14,6 +14,13 @@ module Textbringer
14
14
  "X-Mailer",
15
15
  "Content-Type"
16
16
  ]
17
+ CONFIG[:mournmail_quote_header_fields] = [
18
+ "Subject",
19
+ "Date",
20
+ "From",
21
+ "To",
22
+ "Cc"
23
+ ]
17
24
  CONFIG[:mournmail_imap_connect_timeout] = 10
18
25
  CONFIG[:mournmail_keep_alive_interval] = 60
19
26
  case RUBY_PLATFORM
@@ -28,4 +35,5 @@ module Textbringer
28
35
  CONFIG[:mournmail_link_open_comamnd] = "xdg-open"
29
36
  end
30
37
  CONFIG[:mournmail_addresses_path] = File.expand_path("~/.addresses")
38
+ CONFIG[:mournmail_signature_regexp] = /^\n-- /
31
39
  end
@@ -7,6 +7,8 @@ module Mournmail
7
7
  MAIL_MODE_MAP.define_key("\t", :draft_complete_or_insert_tab_command)
8
8
  MAIL_MODE_MAP.define_key("\C-c\C-xv", :draft_pgp_sign_command)
9
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, /^>.*/
@@ -67,18 +69,21 @@ module Mournmail
67
69
  content: File.read(file),
68
70
  encoding: "binary")
69
71
  end
70
- conf = Mournmail.account_config
72
+ account = @buffer[:mournmail_delivery_account] ||
73
+ Mournmail.current_account
74
+ conf = CONFIG[:mournmail_accounts][account]
71
75
  options = @buffer[:mournmail_delivery_options] ||
72
76
  conf[:delivery_options]
73
77
  if options[:authentication] == "gmail"
78
+ token = Mournmail.google_access_token(account)
74
79
  options = options.merge(authentication: "xoauth2",
75
- password: Mournmail.google_access_token)
80
+ password: token)
76
81
  end
77
82
  m.delivery_method(@buffer[:mournmail_delivery_method] ||
78
83
  conf[:delivery_method],
79
84
  options)
80
85
  bury_buffer(@buffer)
81
- background do
86
+ Mournmail.background do
82
87
  begin
83
88
  if !attached_messages.empty?
84
89
  attached_messages.each do |attached_message|
@@ -181,6 +186,27 @@ module Mournmail
181
186
  end
182
187
  end
183
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
+
184
210
  private
185
211
 
186
212
  def end_of_header
@@ -12,8 +12,8 @@ module Mournmail
12
12
 
13
13
  # See http://nihongo.jp/support/mail_guide/dev_guide.txt
14
14
  MAILTO_REGEXP = URI.regexp("mailto")
15
- URI_REGEXP = /(https?|ftp):\/\/[^  \t\n>)"]*[^]  \t\n>.,:)"]+|#{MAILTO_REGEXP}/
16
- MIME_REGEXP = /^\[(([\-0-9.]+) [A-Za-z._\-]+\/[A-Za-z._\-]+.*|PGP\/MIME .*)\]$/
15
+ URI_REGEXP = /(https?|ftp):\/\/[^  \t\n>)"]*[^\]  \t\n>.,:)"]+|#{MAILTO_REGEXP}/
16
+ MIME_REGEXP = /^\[(([0-9.]+) [A-Za-z._\-]+\/[A-Za-z._\-]+.*|PGP\/MIME .*)\]$/
17
17
  URI_OR_MIME_REGEXP = /#{URI_REGEXP}|#{MIME_REGEXP}/
18
18
 
19
19
  define_syntax :field_name, /^[A-Za-z\-]+: /
@@ -24,6 +24,7 @@ module Mournmail
24
24
  def initialize(buffer)
25
25
  super(buffer)
26
26
  buffer.keymap = MESSAGE_MODE_MAP
27
+ @attached_file = nil
27
28
  end
28
29
 
29
30
  define_local_command(:message_open_link_or_part,
@@ -52,6 +53,9 @@ module Mournmail
52
53
 
53
54
  define_local_command(:message_next_link_or_part,
54
55
  doc: "Go to the next link or MIME part.") do
56
+ if @buffer.looking_at?(URI_OR_MIME_REGEXP)
57
+ @buffer.forward_char
58
+ end
55
59
  if @buffer.re_search_forward(URI_OR_MIME_REGEXP, raise_error: false)
56
60
  goto_char(@buffer.match_beginning(0))
57
61
  else
@@ -64,7 +68,7 @@ module Mournmail
64
68
  def current_part
65
69
  @buffer.save_excursion do
66
70
  @buffer.beginning_of_line
67
- if @buffer.looking_at?(/\[([\-0-9.]+) .*\]/)
71
+ if @buffer.looking_at?(/\[([0-9.]+) .*\]/)
68
72
  index = match_string(1)
69
73
  indices = index.split(".").map(&:to_i)
70
74
  @buffer[:mournmail_mail].dig_part(*indices)
@@ -76,8 +80,8 @@ module Mournmail
76
80
 
77
81
  def part_file_name(part)
78
82
  file_name =
79
- part["content-disposition"]&.parameters&.[]("filename") ||
80
- part["content-type"]&.parameters&.[]("name") ||
83
+ (part["content-disposition"]&.parameters&.[]("filename") rescue nil) ||
84
+ (part["content-type"]&.parameters&.[]("name") rescue nil) ||
81
85
  part_default_file_name(part)
82
86
  decoded_file_name = Mail::Encodings.decode_encode(file_name, :decode)
83
87
  if /\A([A-Za-z0-9_\-]+)'(?:[A-Za-z0-9_\-])*'(.*)/ =~ decoded_file_name
@@ -130,14 +134,20 @@ module Mournmail
130
134
  else
131
135
  file_name = "mournmail"
132
136
  end
133
- f = Tempfile.open(file_name)
134
- f.write(part.decoded)
135
- f.close
136
- if ext == "txt"
137
- find_file(f.path)
137
+ @attached_file = Tempfile.open(file_name, binmode: true)
138
+ s = part.decoded
139
+ if part.content_type == "text/html"
140
+ s = s.sub(/<meta http-equiv="content-type".*?>/i, "")
141
+ elsif part.charset
142
+ s = s.encode(part.charset)
143
+ end
144
+ @attached_file.write(s)
145
+ @attached_file.close
146
+ if part.main_type == "text" && part.sub_type != "html"
147
+ find_file(@attached_file.path)
138
148
  else
139
149
  background do
140
- system(*CONFIG[:mournmail_file_open_comamnd], f.path,
150
+ system(*CONFIG[:mournmail_file_open_comamnd], @attached_file.path,
141
151
  out: File::NULL, err: File::NULL)
142
152
  end
143
153
  end
@@ -1,4 +1,5 @@
1
1
  require "mail"
2
+ require "html2text"
2
3
 
3
4
  module Mournmail
4
5
  module MessageRendering
@@ -7,8 +8,8 @@ module Mournmail
7
8
  render_header + "\n" + render_body(indices)
8
9
  end
9
10
 
10
- def render_header
11
- CONFIG[:mournmail_display_header_fields].map { |name|
11
+ def render_header(fields = CONFIG[:mournmail_display_header_fields])
12
+ fields.map { |name|
12
13
  val = self[name]&.to_s&.gsub(/\t/, " ")
13
14
  val ? "#{name}: #{val}\n" : ""
14
15
  }.join
@@ -27,28 +28,58 @@ module Mournmail
27
28
  end
28
29
  if multipart?
29
30
  parts.each_with_index.map { |part, i|
30
- part.render([*indices, i])
31
+ no_content = sub_type == "alternative" && i > 0
32
+ part.render([*indices, i], no_content)
31
33
  }.join
32
- elsif main_type.nil? || (main_type == "text" && sub_type == "plain")
33
- s = body.decoded
34
- Mournmail.to_utf8(s, charset)
34
+ elsif main_type.nil? || main_type == "text"
35
+ s = Mournmail.to_utf8(body.decoded, charset)
36
+ if sub_type == "html"
37
+ "[0 text/html]\n" + Html2Text.convert(s)
38
+ else
39
+ s
40
+ end
35
41
  else
36
42
  type = Mail::Encodings.decode_encode(self["content-type"].to_s,
37
43
  :decode) rescue
38
- "broken/type; error=\"#{$!} (#{$!.class})\""
39
- "[-1 #{type}]\n"
44
+ "broken/type; error=\"#{$!} (#{$!.class})\""
45
+ "[0 #{type}]\n"
40
46
  end + pgp_signature
41
47
  end
42
48
 
49
+ def render_text(indices = [])
50
+ if HAVE_MAIL_GPG && encrypted?
51
+ mail = decrypt(verify: true)
52
+ return mail.render_text(indices)
53
+ end
54
+ if multipart?
55
+ parts.each_with_index.map { |part, i|
56
+ if sub_type == "alternative" && i > 0
57
+ ""
58
+ else
59
+ part.render_text([*indices, i])
60
+ end
61
+ }.join("\n")
62
+ elsif main_type.nil? || main_type == "text"
63
+ s = Mournmail.to_utf8(body.decoded, charset)
64
+ if sub_type == "html"
65
+ Html2Text.convert(s)
66
+ else
67
+ s
68
+ end
69
+ else
70
+ ""
71
+ end
72
+ end
73
+
43
74
  def dig_part(i, *rest_indices)
44
75
  if HAVE_MAIL_GPG && encrypted?
45
76
  mail = decrypt(verify: true)
46
77
  return mail.dig_part(i, *rest_indices)
47
78
  end
48
- if i == -1
79
+ if i == 0
49
80
  return self
50
81
  end
51
- part = parts[i]
82
+ part = parts[i - 1]
52
83
  if rest_indices.empty?
53
84
  part
54
85
  else
@@ -79,12 +110,45 @@ module Mournmail
79
110
  end
80
111
 
81
112
  refine ::Mail::Part do
82
- def render(indices)
83
- index = indices.join(".")
113
+ def render(indices, no_content = false)
114
+ index = indices.map { |i| i + 1 }.join(".")
84
115
  type = Mail::Encodings.decode_encode(self["content-type"].to_s,
85
116
  :decode) rescue
86
117
  "broken/type; error=\"#{$!} (#{$!.class})\""
87
- "[#{index} #{type}]\n" + render_content(indices)
118
+ "[#{index} #{type}]\n" +
119
+ render_content(indices, no_content)
120
+ end
121
+
122
+ def render_text(indices)
123
+ if multipart?
124
+ parts.each_with_index.map { |part, i|
125
+ if sub_type == "alternative" && i > 0
126
+ ""
127
+ else
128
+ part.render_text([*indices, i])
129
+ end
130
+ }.join("\n")
131
+ else
132
+ if main_type == "message" && sub_type == "rfc822"
133
+ mail = Mail.new(body.raw_source)
134
+ mail.render_header(CONFIG[:mournmail_quote_header_fields]) +
135
+ "\n" + mail.render_text(indices)
136
+ elsif attachment?
137
+ ""
138
+ else
139
+ if main_type == "text"
140
+ if sub_type == "html"
141
+ Html2Text.convert(decoded).sub(/(?<!\n)\z/, "\n")
142
+ else
143
+ decoded.sub(/(?<!\n)\z/, "\n").gsub(/\r\n/, "\n")
144
+ end
145
+ else
146
+ ""
147
+ end
148
+ end
149
+ end
150
+ rescue => e
151
+ ""
88
152
  end
89
153
 
90
154
  def dig_part(i, *rest_indices)
@@ -92,7 +156,7 @@ module Mournmail
92
156
  mail = Mail.new(body.to_s)
93
157
  mail.dig_part(i, *rest_indices)
94
158
  else
95
- part = parts[i]
159
+ part = parts[i - 1]
96
160
  if rest_indices.empty?
97
161
  part
98
162
  else
@@ -103,21 +167,29 @@ module Mournmail
103
167
 
104
168
  private
105
169
 
106
- def render_content(indices)
170
+ def render_content(indices, no_content)
107
171
  if multipart?
108
172
  parts.each_with_index.map { |part, i|
109
- part.render([*indices, i])
173
+ part.render([*indices, i],
174
+ no_content || sub_type == "alternative" && i > 0)
110
175
  }.join
111
- elsif main_type == "message" && sub_type == "rfc822"
112
- mail = Mail.new(body.raw_source)
113
- mail.render(indices)
114
- elsif attachment?
115
- ""
116
176
  else
117
- if main_type == "text" && sub_type == "plain"
118
- decoded.sub(/(?<!\n)\z/, "\n").gsub(/\r\n/, "\n")
119
- else
177
+ return "" if no_content
178
+ if main_type == "message" && sub_type == "rfc822"
179
+ mail = Mail.new(body.raw_source)
180
+ mail.render(indices)
181
+ elsif attachment?
120
182
  ""
183
+ else
184
+ if main_type == "text"
185
+ if sub_type == "html"
186
+ Html2Text.convert(decoded).sub(/(?<!\n)\z/, "\n")
187
+ else
188
+ decoded.sub(/(?<!\n)\z/, "\n").gsub(/\r\n/, "\n")
189
+ end
190
+ else
191
+ ""
192
+ end
121
193
  end
122
194
  end
123
195
  rescue => e
@@ -141,7 +141,8 @@ module Mournmail
141
141
  end
142
142
  s = data[0].attr["BODY[]"]
143
143
  mail = Mournmail.parse_mail(s)
144
- if @mailbox != Mournmail.account_config[:spam_mailbox]
144
+ spam_mailbox = Mournmail.account_config[:spam_mailbox]
145
+ if @mailbox != Net::IMAP.encode_utf7(spam_mailbox)
145
146
  item.cache_id = Mournmail.write_mail_cache(s)
146
147
  Mournmail.index_mail(item.cache_id, mail)
147
148
  end
@@ -25,6 +25,7 @@ module Mournmail
25
25
  SUMMARY_MODE_MAP.define_key("*t", :summary_mark_unflagged_command)
26
26
  SUMMARY_MODE_MAP.define_key("y", :summary_archive_command)
27
27
  SUMMARY_MODE_MAP.define_key("o", :summary_refile_command)
28
+ SUMMARY_MODE_MAP.define_key("!", :summary_refile_spam_command)
28
29
  SUMMARY_MODE_MAP.define_key("p", :summary_prefetch_command)
29
30
  SUMMARY_MODE_MAP.define_key("X", :summary_expunge_command)
30
31
  SUMMARY_MODE_MAP.define_key("v", :summary_view_source_command)
@@ -33,6 +34,7 @@ module Mournmail
33
34
  SUMMARY_MODE_MAP.define_key("k", :previous_line)
34
35
  SUMMARY_MODE_MAP.define_key("j", :next_line)
35
36
  SUMMARY_MODE_MAP.define_key("m", :mournmail_visit_mailbox)
37
+ SUMMARY_MODE_MAP.define_key("S", :mournmail_visit_spam_mailbox)
36
38
  SUMMARY_MODE_MAP.define_key("/", :summary_search_command)
37
39
  SUMMARY_MODE_MAP.define_key("t", :summary_show_thread_command)
38
40
  SUMMARY_MODE_MAP.define_key("@", :summary_change_account_command)
@@ -98,7 +100,7 @@ module Mournmail
98
100
  |reply_all = current_prefix_arg|
99
101
  Mournmail.background do
100
102
  mail = read_current_mail[0]
101
- body = mail.render_body
103
+ body = mail.render_text
102
104
  foreground do
103
105
  Window.current = Mournmail.message_window
104
106
  Commands.mail(run_hooks: false)
@@ -137,6 +139,7 @@ module Mournmail
137
139
  On #{mail['date']}
138
140
  #{mail['from']} wrote:
139
141
  EOF
142
+ Mournmail.insert_signature
140
143
  exchange_point_and_mark
141
144
  run_hooks(:mournmail_draft_setup_hook)
142
145
  end
@@ -167,7 +170,7 @@ module Mournmail
167
170
 
168
171
  define_local_command(:summary_toggle_deleted,
169
172
  doc: <<~EOD) do
170
- Toggle Deleted. Type `x` to expunge deleted messages.
173
+ Toggle Deleted. Type `X` to expunge deleted messages.
171
174
  EOD
172
175
  toggle_flag(selected_uid, :Deleted)
173
176
  end
@@ -373,10 +376,23 @@ module Mournmail
373
376
  end
374
377
  end
375
378
 
379
+ define_local_command(:summary_refile_spam,
380
+ doc: "Refile marked mails as spam.") do
381
+ mailbox = Mournmail.account_config[:spam_mailbox]
382
+ if mailbox.nil?
383
+ raise EditorError, "spam_mailbox is not specified"
384
+ end
385
+ summary_refile(Net::IMAP.encode_utf7(mailbox))
386
+ end
387
+
376
388
  define_local_command(:summary_prefetch,
377
389
  doc: "Prefetch mails.") do
378
390
  summary = Mournmail.current_summary
379
391
  mailbox = Mournmail.current_mailbox
392
+ spam_mailbox = Mournmail.account_config[:spam_mailbox]
393
+ if mailbox == Net::IMAP.encode_utf7(spam_mailbox)
394
+ raise EditorError, "Can't prefetch spam"
395
+ end
380
396
  target_uids = @buffer.to_s.scan(/^ *\d+/).map { |s|
381
397
  s.to_i
382
398
  }.select { |uid|
@@ -394,9 +410,7 @@ module Mournmail
394
410
  s = i.attr["BODY[]"]
395
411
  if s
396
412
  cache_id = Mournmail.write_mail_cache(s)
397
- if mailbox != Mournmail.account_config[:spam_mailbox]
398
- Mournmail.index_mail(cache_id, Mail.new(s))
399
- end
413
+ Mournmail.index_mail(cache_id, Mail.new(s))
400
414
  summary[uid].cache_id = cache_id
401
415
  end
402
416
  end
@@ -450,7 +464,7 @@ module Mournmail
450
464
 
451
465
  define_local_command(:summary_change_account,
452
466
  doc: "Change the current account.") do
453
- |account = read_account_name("Change account: ")|
467
+ |account = Mournmail.read_account_name("Change account: ")|
454
468
  unless CONFIG[:mournmail_accounts].key?(account)
455
469
  raise EditorError, "No such account: #{account}"
456
470
  end
@@ -651,13 +665,6 @@ module Mournmail
651
665
  message
652
666
  end
653
667
 
654
- def read_account_name(prompt, **opts)
655
- f = ->(s) {
656
- complete_for_minibuffer(s, CONFIG[:mournmail_accounts].keys)
657
- }
658
- read_from_minibuffer(prompt, completion_proc: f, **opts)
659
- end
660
-
661
668
  def delete_from_summary(summary, uids, msg)
662
669
  summary.delete_item_if do |item|
663
670
  uids.include?(item.uid)
@@ -66,7 +66,9 @@ module Mournmail
66
66
  define_variable :current_uid, attr: :accessor
67
67
  define_variable :current_mail, attr: :accessor
68
68
  define_variable :background_thread, attr: :accessor
69
+ define_variable :background_thread_mutex, initial_value: Mutex.new
69
70
  define_variable :keep_alive_thread, attr: :accessor
71
+ define_variable :keep_alive_thread_mutex, initial_value: Mutex.new
70
72
  define_variable :imap
71
73
  define_variable :imap_mutex, initial_value: Mutex.new
72
74
  define_variable :mailboxes, initial_value: []
@@ -75,42 +77,52 @@ module Mournmail
75
77
  define_variable :groonga_db
76
78
 
77
79
  def self.background(skip_if_busy: false)
78
- if background_thread&.alive?
79
- return if skip_if_busy
80
- end
81
- self.background_thread = Utils.background {
82
- begin
83
- yield
84
- ensure
85
- self.background_thread = nil
80
+ @background_thread_mutex.synchronize do
81
+ if background_thread&.alive?
82
+ if skip_if_busy
83
+ return
84
+ else
85
+ raise EditorError, "Another background thread is running"
86
+ end
86
87
  end
87
- }
88
+ self.background_thread = Utils.background {
89
+ begin
90
+ yield
91
+ ensure
92
+ self.background_thread = nil
93
+ end
94
+ }
95
+ end
88
96
  end
89
97
 
90
98
  def self.start_keep_alive_thread
91
- if keep_alive_thread
92
- raise EditorError, "Keep alive thread already running"
93
- end
94
- self.keep_alive_thread = Thread.start {
95
- loop do
96
- sleep(CONFIG[:mournmail_keep_alive_interval])
97
- background(skip_if_busy: true) do
98
- begin
99
- imap_connect do |imap|
100
- imap.noop
99
+ @keep_alive_thread_mutex.synchronize do
100
+ if keep_alive_thread
101
+ raise EditorError, "Keep alive thread already running"
102
+ end
103
+ self.keep_alive_thread = Thread.start {
104
+ loop do
105
+ sleep(CONFIG[:mournmail_keep_alive_interval])
106
+ background(skip_if_busy: true) do
107
+ begin
108
+ imap_connect do |imap|
109
+ imap.noop
110
+ end
111
+ rescue => e
112
+ message("Error in IMAP NOOP: #{e.class}: #{e.message}")
101
113
  end
102
- rescue => e
103
- message("Error in IMAP NOOP: #{e.class}: #{e.message}")
104
114
  end
105
115
  end
106
- end
107
- }
116
+ }
117
+ end
108
118
  end
109
119
 
110
120
  def self.stop_keep_alive_thread
111
- if keep_alive_thread
112
- keep_alive_thread&.kill
113
- self.keep_alive_thread = nil
121
+ @keep_alive_thread_mutex.synchronize do
122
+ if keep_alive_thread
123
+ keep_alive_thread&.kill
124
+ self.keep_alive_thread = nil
125
+ end
114
126
  end
115
127
  end
116
128
 
@@ -213,29 +225,38 @@ module Mournmail
213
225
  end
214
226
  end
215
227
 
216
- def self.google_access_token
217
- auth_path = File.expand_path("cache/#{current_account}/google_auth.json",
228
+ def self.google_access_token(account = current_account)
229
+ auth_path = File.expand_path("cache/#{account}/google_auth.json",
218
230
  CONFIG[:mournmail_directory])
219
231
  FileUtils.mkdir_p(File.dirname(auth_path))
220
232
  store = Google::APIClient::FileStore.new(auth_path)
221
233
  storage = Google::APIClient::Storage.new(store)
222
234
  storage.authorize
223
235
  if storage.authorization.nil?
224
- path = File.expand_path(account_config[:client_secret_path])
236
+ conf = CONFIG[:mournmail_accounts][account]
237
+ path = File.expand_path(conf[:client_secret_path])
225
238
  client_secrets = Google::APIClient::ClientSecrets.load(path)
226
239
  auth_client = client_secrets.to_authorization
227
240
  auth_client.update!(
228
241
  :scope => 'https://mail.google.com/',
229
242
  :redirect_uri => 'urn:ietf:wg:oauth:2.0:oob'
230
243
  )
231
-
232
244
  auth_uri = auth_client.authorization_uri.to_s
233
- Launchy.open(auth_uri)
234
-
235
245
  auth_client.code = foreground! {
236
- Window.echo_area.clear_message
237
- Window.redisplay
238
- read_from_minibuffer("Code: ").chomp
246
+ begin
247
+ Launchy.open(auth_uri)
248
+ rescue Launchy::CommandNotFoundError
249
+ buffer = show_google_auth_uri(auth_uri)
250
+ end
251
+ begin
252
+ Window.echo_area.clear_message
253
+ Window.redisplay
254
+ read_from_minibuffer("Code: ").chomp
255
+ ensure
256
+ if buffer
257
+ kill_buffer(buffer, force: true)
258
+ end
259
+ end
239
260
  }
240
261
  auth_client.fetch_access_token!
241
262
  old_umask = File.umask(077)
@@ -249,6 +270,23 @@ module Mournmail
249
270
  end
250
271
  auth_client.access_token
251
272
  end
273
+
274
+ def self.show_google_auth_uri(auth_uri)
275
+ buffer = Buffer.find_or_new("*message*",
276
+ undo_limit: 0, read_only: true)
277
+ buffer.apply_mode(Mournmail::MessageMode)
278
+ buffer.read_only_edit do
279
+ buffer.clear
280
+ buffer.insert(<<~EOF)
281
+ Open the following URI in your browser and type obtained code:
282
+
283
+ #{auth_uri}
284
+ EOF
285
+ end
286
+ window = Mournmail.message_window
287
+ window.buffer = buffer
288
+ buffer
289
+ end
252
290
 
253
291
  def self.xoauth2_string(user, access_token)
254
292
  "user=#{user}\1auth=Bearer #{access_token}\1\1"
@@ -345,7 +383,8 @@ module Mournmail
345
383
  dir = File.dirname(path)
346
384
  base = File.basename(path)
347
385
  begin
348
- f = Tempfile.create(["#{base}-", ".tmp"], dir)
386
+ f = Tempfile.create(["#{base}-", ".tmp"], dir,
387
+ external_encoding: "ASCII-8BIT", binmode: true)
349
388
  begin
350
389
  f.write(s)
351
390
  ensure
@@ -517,4 +556,24 @@ module Mournmail
517
556
  def self.parse_mail(s)
518
557
  Mail.new(s.scrub("??"))
519
558
  end
559
+
560
+ def self.read_account_name(prompt, **opts)
561
+ f = ->(s) {
562
+ complete_for_minibuffer(s, CONFIG[:mournmail_accounts].keys)
563
+ }
564
+ read_from_minibuffer(prompt, completion_proc: f, **opts)
565
+ end
566
+
567
+ def self.insert_signature
568
+ account = Buffer.current[:mournmail_delivery_account] ||
569
+ Mournmail.current_account
570
+ signature = CONFIG[:mournmail_accounts][account][:signature]
571
+ if signature
572
+ Buffer.current.save_excursion do
573
+ end_of_buffer
574
+ insert("\n")
575
+ insert(signature)
576
+ end
577
+ end
578
+ end
520
579
  end
@@ -1,3 +1,3 @@
1
1
  module Mournmail
2
- VERSION = "0.3.0"
2
+ VERSION = "1.0.1"
3
3
  end
data/mournmail.gemspec CHANGED
@@ -22,12 +22,15 @@ Gem::Specification.new do |spec|
22
22
  spec.require_paths = ["lib"]
23
23
 
24
24
  spec.add_runtime_dependency "textbringer"
25
+ spec.add_runtime_dependency "net-smtp"
26
+ spec.add_runtime_dependency "net-imap"
25
27
  spec.add_runtime_dependency "mail"
26
28
  spec.add_runtime_dependency "mime-types"
27
29
  spec.add_runtime_dependency "rroonga"
28
30
  spec.add_runtime_dependency "google-api-client"
29
31
  spec.add_runtime_dependency "launchy"
32
+ spec.add_runtime_dependency "html2text"
30
33
 
31
34
  spec.add_development_dependency "bundler"
32
- spec.add_development_dependency "rake", "~> 12.0"
35
+ spec.add_development_dependency "rake", ">= 12.0"
33
36
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mournmail
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shugo Maeda
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-03-10 00:00:00.000000000 Z
11
+ date: 2022-01-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: textbringer
@@ -24,6 +24,34 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: net-smtp
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: net-imap
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
27
55
  - !ruby/object:Gem::Dependency
28
56
  name: mail
29
57
  requirement: !ruby/object:Gem::Requirement
@@ -94,6 +122,20 @@ dependencies:
94
122
  - - ">="
95
123
  - !ruby/object:Gem::Version
96
124
  version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: html2text
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
97
139
  - !ruby/object:Gem::Dependency
98
140
  name: bundler
99
141
  requirement: !ruby/object:Gem::Requirement
@@ -112,20 +154,21 @@ dependencies:
112
154
  name: rake
113
155
  requirement: !ruby/object:Gem::Requirement
114
156
  requirements:
115
- - - "~>"
157
+ - - ">="
116
158
  - !ruby/object:Gem::Version
117
159
  version: '12.0'
118
160
  type: :development
119
161
  prerelease: false
120
162
  version_requirements: !ruby/object:Gem::Requirement
121
163
  requirements:
122
- - - "~>"
164
+ - - ">="
123
165
  - !ruby/object:Gem::Version
124
166
  version: '12.0'
125
167
  description: A message user agent for Textbringer.
126
168
  email:
127
169
  - shugo@ruby-lang.org
128
- executables: []
170
+ executables:
171
+ - mournmail_reindex
129
172
  extensions: []
130
173
  extra_rdoc_files: []
131
174
  files:
@@ -136,6 +179,7 @@ files:
136
179
  - Rakefile
137
180
  - bin/console
138
181
  - bin/setup
182
+ - exe/mournmail_reindex
139
183
  - lib/mournmail.rb
140
184
  - lib/mournmail/commands.rb
141
185
  - lib/mournmail/config.rb
@@ -155,7 +199,7 @@ homepage: https://github.com/shugo/mournmail
155
199
  licenses:
156
200
  - MIT
157
201
  metadata: {}
158
- post_install_message:
202
+ post_install_message:
159
203
  rdoc_options: []
160
204
  require_paths:
161
205
  - lib
@@ -170,8 +214,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
170
214
  - !ruby/object:Gem::Version
171
215
  version: '0'
172
216
  requirements: []
173
- rubygems_version: 3.2.0.pre1
174
- signing_key:
217
+ rubygems_version: 3.4.0.dev
218
+ signing_key:
175
219
  specification_version: 4
176
220
  summary: A message user agent for Textbringer.
177
221
  test_files: []