mournmail 0.3.0 → 1.0.1

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
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: []