mournmail 0.2.0 → 0.3.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
2
  SHA256:
3
- metadata.gz: 422feadab2e36499c76f69f1836ce4e384497f0e86814cb4c20d0966e70b69b8
4
- data.tar.gz: 82423aa120aef42601ef86737e8461b96c32f786d9e7e647c80a4dfdc14a93f1
3
+ metadata.gz: f3e35ed64af5fb25eeedad4c3c901b93125baf6b561b8e2384da7a9ed602b367
4
+ data.tar.gz: 9174888f40a57d0cffe48d13fef7cc0d2892be312d3ff0b95a0df7aef098c726
5
5
  SHA512:
6
- metadata.gz: 7a4bba313e307593fad8635b6f284bbc58a79087adad0671849bbf3ec416e554ba7c4af258f4b6556606262897035a08313d5f40754cde56bf8652555049ef09
7
- data.tar.gz: 6a30de329d0dc20d86954c1cb8d881bfe15b8309931f2fbba98cdd1a690aa53d6eab1f345b86822c2a6de19905e357232fea1e2092077d866b1f9c4fc2a630d1
6
+ metadata.gz: f85ebaadd8f97a43e45856347ad0149594ab17db31c04ba66d85db61b336f3c7cd4548c725868a44522d87941297edc5ad5656901440400f834017ed2e650733
7
+ data.tar.gz: ddff53280fad3432e0bf0da11cf58c57a60586886828fa512b1bf54cba8e57afc8e2687561ae8b289d95fde271b1679f7da4de56e451b4fd6c00ec9787ccd095
@@ -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"
@@ -1,13 +1,14 @@
1
- # frozen_string_literal: true
2
-
3
1
  define_command(:mournmail, doc: "Start mournmail.") do
4
2
  Mournmail.open_groonga_db
5
3
  mournmail_visit_mailbox("INBOX")
6
4
  end
7
5
 
8
- define_command(:mournmail_visit_mailbox, doc: "Start mournmail.") do
6
+ define_command(:mournmail_visit_mailbox, doc: "Visit mailbox") do
9
7
  |mailbox = Mournmail.read_mailbox_name("Visit mailbox: ", default: "INBOX")|
10
- mournmail_summary_sync(mailbox)
8
+ summary = Mournmail::Summary.load_or_new(mailbox)
9
+ foreground do
10
+ Mournmail.show_summary(summary)
11
+ end
11
12
  end
12
13
 
13
14
  define_command(:mournmail_summary_sync, doc: "Sync summary.") do
@@ -16,30 +17,10 @@ define_command(:mournmail_summary_sync, doc: "Sync summary.") do
16
17
  message("Syncing #{mailbox} in background...")
17
18
  Mournmail.background do
18
19
  summary = Mournmail.fetch_summary(mailbox, all: all)
19
- summary_text = summary.to_s
20
20
  summary.save
21
- next_tick do
22
- buffer = Buffer.find_or_new("*summary*", undo_limit: 0,
23
- read_only: true)
24
- buffer.apply_mode(Mournmail::SummaryMode)
25
- buffer.read_only_edit do
26
- buffer.clear
27
- buffer.insert(summary_text)
28
- end
29
- switch_to_buffer(buffer)
30
- Mournmail.current_mailbox = mailbox
31
- Mournmail.current_summary = summary
32
- Mournmail.current_mail = nil
33
- Mournmail.current_uid = nil
21
+ foreground do
22
+ Mournmail.show_summary(summary)
34
23
  message("Syncing #{mailbox} in background... Done")
35
- begin
36
- buffer.beginning_of_buffer
37
- buffer.re_search_forward(/^ *\d+ u/)
38
- rescue SearchError
39
- buffer.end_of_buffer
40
- buffer.re_search_backward(/^ *\d+ /, raise_error: false)
41
- end
42
- summary_read_command
43
24
  end
44
25
  end
45
26
  end
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  module Textbringer
4
2
  CONFIG[:mournmail_directory] = File.expand_path("~/.mournmail")
5
3
  CONFIG[:mournmail_accounts] = {}
@@ -18,7 +16,16 @@ module Textbringer
18
16
  ]
19
17
  CONFIG[:mournmail_imap_connect_timeout] = 10
20
18
  CONFIG[:mournmail_keep_alive_interval] = 60
21
- CONFIG[:mournmail_file_open_comamnd] = "xdg-open"
22
- CONFIG[:mournmail_link_open_comamnd] = "xdg-open"
19
+ case RUBY_PLATFORM
20
+ when /mswin|mingw/
21
+ CONFIG[:mournmail_file_open_comamnd] = "start"
22
+ CONFIG[:mournmail_link_open_comamnd] = "start"
23
+ when /darwin/
24
+ CONFIG[:mournmail_file_open_comamnd] = "open"
25
+ CONFIG[:mournmail_link_open_comamnd] = "open"
26
+ else
27
+ CONFIG[:mournmail_file_open_comamnd] = "xdg-open"
28
+ CONFIG[:mournmail_link_open_comamnd] = "xdg-open"
29
+ end
23
30
  CONFIG[:mournmail_addresses_path] = File.expand_path("~/.addresses")
24
31
  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
@@ -70,10 +68,15 @@ module Mournmail
70
68
  encoding: "binary")
71
69
  end
72
70
  conf = Mournmail.account_config
71
+ options = @buffer[:mournmail_delivery_options] ||
72
+ conf[:delivery_options]
73
+ if options[:authentication] == "gmail"
74
+ options = options.merge(authentication: "xoauth2",
75
+ password: Mournmail.google_access_token)
76
+ end
73
77
  m.delivery_method(@buffer[:mournmail_delivery_method] ||
74
78
  conf[:delivery_method],
75
- @buffer[:mournmail_delivery_options] ||
76
- conf[:delivery_options])
79
+ options)
77
80
  bury_buffer(@buffer)
78
81
  background do
79
82
  begin
@@ -89,9 +92,11 @@ module Mournmail
89
92
  m.gpg(sign: pgp_sign, encrypt: pgp_encrypt)
90
93
  end
91
94
  m.deliver
92
- next_tick do
95
+ foreground do
93
96
  message("Mail sent.")
94
97
  end
98
+ cache_id = Mournmail.write_mail_cache(m.encoded)
99
+ Mournmail.index_mail(cache_id, m)
95
100
  outbox = Mournmail.account_config[:outbox_mailbox]
96
101
  if outbox
97
102
  Mournmail.imap_connect do |imap|
@@ -101,12 +106,12 @@ module Mournmail
101
106
  imap.append(outbox, m.to_s, [:Seen])
102
107
  end
103
108
  end
104
- next_tick do
109
+ foreground do
105
110
  kill_buffer(@buffer, force: true)
106
111
  Mournmail.back_to_summary
107
112
  end
108
113
  rescue Exception
109
- next_tick do
114
+ foreground do
110
115
  switch_to_buffer(@buffer)
111
116
  end
112
117
  raise
@@ -141,10 +146,15 @@ module Mournmail
141
146
  start_pos = @buffer.point
142
147
  s = @buffer.substring(start_pos, end_pos)
143
148
  if !s.empty?
144
- re = /^(.*")?#{Regexp.quote(s)}.*/
145
- line = File.read(CONFIG[:mournmail_addresses_path]).slice(re)
146
- if line
147
- addr = line.slice(/^\S+/)
149
+ re = /^(?:.*")?#{Regexp.quote(s)}.*/
150
+ addrs = File.read(CONFIG[:mournmail_addresses_path])
151
+ .scan(re).map { |line| line.slice(/^\S+/) }
152
+ if !addrs.empty?
153
+ addr = addrs.inject { |x, y|
154
+ x.chars.zip(y.chars).take_while { |i, j|
155
+ i == j
156
+ }.map { |i,| i }.join
157
+ }
148
158
  @buffer.delete_region(start_pos, end_pos)
149
159
  @buffer.insert(addr)
150
160
  else
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  module Textbringer
4
2
  Face.define :seen, foreground: "blue"
5
3
  Face.define :deleted, foreground: "green"
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  require "mail"
4
2
 
5
3
  module Mournmail
@@ -22,7 +20,7 @@ module Mournmail
22
20
  b_encoding_extra_size = "=?#{charset}?B??=".bytesize
23
21
  while !words.empty?
24
22
  limit = 78 - prepend
25
- line = String.new
23
+ line = +""
26
24
  fold_line = false
27
25
  while !fold_line && !words.empty?
28
26
  word = words.first
@@ -1,4 +1,5 @@
1
- # frozen_string_literal: true
1
+ require "uri"
2
+ require "mime/types"
2
3
 
3
4
  using Mournmail::MessageRendering
4
5
 
@@ -7,14 +8,17 @@ module Mournmail
7
8
  MESSAGE_MODE_MAP = Keymap.new
8
9
  MESSAGE_MODE_MAP.define_key("\C-m", :message_open_link_or_part_command)
9
10
  MESSAGE_MODE_MAP.define_key("s", :message_save_part_command)
11
+ MESSAGE_MODE_MAP.define_key("\t", :message_next_link_or_part_command)
10
12
 
11
13
  # See http://nihongo.jp/support/mail_guide/dev_guide.txt
12
- URI_REGEXP = /(https?|ftp):\/\/[^  \t\n>)"]*[^]  \t\n>.,:)"]+/
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 .*)\]$/
17
+ URI_OR_MIME_REGEXP = /#{URI_REGEXP}|#{MIME_REGEXP}/
13
18
 
14
19
  define_syntax :field_name, /^[A-Za-z\-]+: /
15
20
  define_syntax :quotation, /^>.*/
16
- define_syntax :mime_part,
17
- /^\[(([0-9.]+) [A-Za-z._\-]+\/[A-Za-z._\-]+.*|PGP\/MIME .*)\]$/
21
+ define_syntax :mime_part, MIME_REGEXP
18
22
  define_syntax :link, URI_REGEXP
19
23
 
20
24
  def initialize(buffer)
@@ -46,12 +50,21 @@ module Mournmail
46
50
  end
47
51
  end
48
52
 
53
+ define_local_command(:message_next_link_or_part,
54
+ doc: "Go to the next link or MIME part.") do
55
+ if @buffer.re_search_forward(URI_OR_MIME_REGEXP, raise_error: false)
56
+ goto_char(@buffer.match_beginning(0))
57
+ else
58
+ @buffer.beginning_of_buffer
59
+ end
60
+ end
61
+
49
62
  private
50
63
 
51
64
  def current_part
52
65
  @buffer.save_excursion do
53
66
  @buffer.beginning_of_line
54
- if @buffer.looking_at?(/\[([0-9.]+) .*\]/)
67
+ if @buffer.looking_at?(/\[([\-0-9.]+) .*\]/)
55
68
  index = match_string(1)
56
69
  indices = index.split(".").map(&:to_i)
57
70
  @buffer[:mournmail_mail].dig_part(*indices)
@@ -75,7 +88,12 @@ module Mournmail
75
88
  end
76
89
 
77
90
  def part_default_file_name(part)
78
- base_name = part.cid.gsub(/[^A-Za-z0-9_\-]/, "_")
91
+ base_name =
92
+ begin
93
+ part.cid.gsub(/[^A-Za-z0-9_\-]/, "_")
94
+ rescue NoMethodError
95
+ "mournmail"
96
+ end
79
97
  ext = part_extension(part)
80
98
  if ext
81
99
  base_name + "." + ext
@@ -126,8 +144,24 @@ module Mournmail
126
144
  end
127
145
 
128
146
  def open_uri(uri)
129
- system(*CONFIG[:mournmail_link_open_comamnd], uri,
130
- out: File::NULL, err: File::NULL)
147
+ case uri
148
+ when /\Amailto:/
149
+ u = URI.parse(uri)
150
+ if u.headers.assoc("subject")
151
+ re = /^To:\s*\nSubject:\s*\n/
152
+ else
153
+ re = /^To:\s*\n/
154
+ end
155
+ Commands.mail
156
+ beginning_of_buffer
157
+ re_search_forward(re)
158
+ replace_match("")
159
+ insert u.to_mailtext.sub(/\n\n\z/, "")
160
+ end_of_buffer
161
+ else
162
+ system(*CONFIG[:mournmail_link_open_comamnd], uri,
163
+ out: File::NULL, err: File::NULL)
164
+ end
131
165
  end
132
166
  end
133
167
  end
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  require "mail"
4
2
 
5
3
  module Mournmail
@@ -31,9 +29,14 @@ module Mournmail
31
29
  parts.each_with_index.map { |part, i|
32
30
  part.render([*indices, i])
33
31
  }.join
34
- else
32
+ elsif main_type.nil? || (main_type == "text" && sub_type == "plain")
35
33
  s = body.decoded
36
- Mournmail.to_utf8(s, charset).gsub(/\r\n/, "\n")
34
+ Mournmail.to_utf8(s, charset)
35
+ else
36
+ type = Mail::Encodings.decode_encode(self["content-type"].to_s,
37
+ :decode) rescue
38
+ "broken/type; error=\"#{$!} (#{$!.class})\""
39
+ "[-1 #{type}]\n"
37
40
  end + pgp_signature
38
41
  end
39
42
 
@@ -42,6 +45,9 @@ module Mournmail
42
45
  mail = decrypt(verify: true)
43
46
  return mail.dig_part(i, *rest_indices)
44
47
  end
48
+ if i == -1
49
+ return self
50
+ end
45
51
  part = parts[i]
46
52
  if rest_indices.empty?
47
53
  part
@@ -76,7 +82,8 @@ module Mournmail
76
82
  def render(indices)
77
83
  index = indices.join(".")
78
84
  type = Mail::Encodings.decode_encode(self["content-type"].to_s,
79
- :decode)
85
+ :decode) rescue
86
+ "broken/type; error=\"#{$!} (#{$!.class})\""
80
87
  "[#{index} #{type}]\n" + render_content(indices)
81
88
  end
82
89
 
@@ -113,6 +120,8 @@ module Mournmail
113
120
  ""
114
121
  end
115
122
  end
123
+ rescue => e
124
+ "Broken part: #{e} (#{e.class})"
116
125
  end
117
126
  end
118
127
  end
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  using Mournmail::MessageRendering
4
2
 
5
3
  module Mournmail
@@ -21,6 +19,7 @@ module Mournmail
21
19
  SEARCH_RESULT_MODE_MAP.define_key(">", :next_page_command)
22
20
  SEARCH_RESULT_MODE_MAP.define_key("/", :summary_search_command)
23
21
  SEARCH_RESULT_MODE_MAP.define_key("t", :summary_show_thread_command)
22
+ SEARCH_RESULT_MODE_MAP.define_key("@", :summary_change_account_command)
24
23
 
25
24
  def initialize(buffer)
26
25
  super(buffer)
@@ -32,11 +31,11 @@ module Mournmail
32
31
  return if num.nil?
33
32
  Mournmail.background do
34
33
  message = @buffer[:messages][num]
35
- if message.nil?
34
+ if message.nil? || message._key.nil?
36
35
  raise EditorError, "No message found"
37
36
  end
38
37
  mail = Mail.new(Mournmail.read_mail_cache(message._key))
39
- next_tick do
38
+ foreground do
40
39
  show_message(mail)
41
40
  @buffer[:message_number] = num
42
41
  end
@@ -1,12 +1,10 @@
1
- # frozen_string_literal: true
2
-
3
1
  require "time"
4
2
  require "fileutils"
5
3
  require "monitor"
6
4
 
7
5
  module Mournmail
8
6
  class Summary
9
- attr_reader :items, :last_uid
7
+ attr_reader :mailbox, :items, :last_uid
10
8
  attr_accessor :uidvalidity
11
9
 
12
10
  include MonitorMixin
@@ -34,6 +32,10 @@ module Mournmail
34
32
  cache_path(mailbox) + ".tmp"
35
33
  end
36
34
 
35
+ def self.cache_old_path(mailbox)
36
+ cache_path(mailbox) + ".old"
37
+ end
38
+
37
39
  def self.load(mailbox)
38
40
  lock_cache(mailbox, :shared) do
39
41
  File.open(cache_path(mailbox)) do |f|
@@ -82,12 +84,14 @@ module Mournmail
82
84
 
83
85
  def add_item(item, message_id, in_reply_to)
84
86
  synchronize do
85
- parent = @message_id_table[in_reply_to]
86
- if parent
87
- parent.add_reply(item)
88
- else
89
- @items.push(item)
90
- end
87
+ # Disable threads
88
+ # parent = @message_id_table[in_reply_to]
89
+ # if parent
90
+ # parent.add_reply(item)
91
+ # else
92
+ # @items.push(item)
93
+ # end
94
+ @items.push(item)
91
95
  if message_id
92
96
  @message_id_table[message_id] = item
93
97
  end
@@ -115,22 +119,28 @@ module Mournmail
115
119
  end
116
120
  end
117
121
 
122
+ def uids
123
+ synchronize do
124
+ @uid_table.keys
125
+ end
126
+ end
127
+
118
128
  def read_mail(uid)
119
129
  synchronize do
120
130
  item = @uid_table[uid]
121
131
  if item.cache_id
122
132
  File.open(Mournmail.mail_cache_path(item.cache_id)) do |f|
123
- [Mail.new(f.read), false]
133
+ [Mournmail.parse_mail(f.read), false]
124
134
  end
125
135
  else
126
136
  Mournmail.imap_connect do |imap|
127
137
  imap.select(@mailbox)
128
138
  data = imap.uid_fetch(uid, "BODY[]")
129
- if data.empty?
139
+ if data.nil? || data.empty?
130
140
  raise EditorError, "No such mail: #{uid}"
131
141
  end
132
142
  s = data[0].attr["BODY[]"]
133
- mail = Mail.new(s)
143
+ mail = Mournmail.parse_mail(s)
134
144
  if @mailbox != Mournmail.account_config[:spam_mailbox]
135
145
  item.cache_id = Mournmail.write_mail_cache(s)
136
146
  Mournmail.index_mail(item.cache_id, mail)
@@ -148,9 +158,14 @@ module Mournmail
148
158
  Summary.lock_cache(@mailbox, :exclusive) do
149
159
  cache_path = Summary.cache_path(@mailbox)
150
160
  tmp_path = Summary.cache_tmp_path(@mailbox)
161
+ old_path = Summary.cache_old_path(@mailbox)
151
162
  File.open(tmp_path, "w", 0600) do |f|
152
163
  Marshal.dump(self, f)
153
164
  end
165
+ begin
166
+ File.rename(cache_path, old_path)
167
+ rescue Errno::ENOENT
168
+ end
154
169
  File.rename(tmp_path, cache_path)
155
170
  end
156
171
  end
@@ -158,7 +173,7 @@ module Mournmail
158
173
 
159
174
  def to_s
160
175
  synchronize do
161
- items.each_with_object(String.new) do |item, s|
176
+ items.each_with_object(+"") do |item, s|
162
177
  s << item.to_s
163
178
  end
164
179
  end
@@ -232,7 +247,7 @@ module Mournmail
232
247
 
233
248
  def format_line(limit = 78, from_limit = 16, level = 0)
234
249
  space = " " * (level < 8 ? level : 8)
235
- s = String.new
250
+ s = +""
236
251
  s << format("%6d %s%s %s[ %s ] ",
237
252
  @uid, format_flags(@flags), format_date(@date), space,
238
253
  ljust(format_from(@from), from_limit))
@@ -243,7 +258,7 @@ module Mournmail
243
258
 
244
259
  def ljust(s, n)
245
260
  width = 0
246
- str = String.new
261
+ str = +""
247
262
  s.each_char do |c|
248
263
  w = Buffer.display_width(c)
249
264
  width += w
@@ -258,14 +273,14 @@ module Mournmail
258
273
  end
259
274
 
260
275
  def format_flags(flags)
261
- if flags.include?(:Deleted)
276
+ if !flags.include?(:Seen)
277
+ "u"
278
+ elsif flags.include?(:Deleted)
262
279
  "d"
263
280
  elsif flags.include?(:Flagged)
264
281
  "$"
265
282
  elsif flags.include?(:Answered)
266
283
  "a"
267
- elsif !flags.include?(:Seen)
268
- "u"
269
284
  else
270
285
  " "
271
286
  end
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  using Mournmail::MessageRendering
4
2
 
5
3
  module Mournmail
@@ -56,9 +54,9 @@ module Mournmail
56
54
  summary = Mournmail.current_summary
57
55
  Mournmail.background do
58
56
  mail, fetched = summary.read_mail(uid)
59
- next_tick do
57
+ foreground do
60
58
  show_message(mail)
61
- mark_as_seen(uid, !fetched)
59
+ mark_as_seen(uid, false)
62
60
  Mournmail.current_uid = uid
63
61
  Mournmail.current_mail = mail
64
62
  end
@@ -101,7 +99,7 @@ module Mournmail
101
99
  Mournmail.background do
102
100
  mail = read_current_mail[0]
103
101
  body = mail.render_body
104
- next_tick do
102
+ foreground do
105
103
  Window.current = Mournmail.message_window
106
104
  Commands.mail(run_hooks: false)
107
105
  if reply_all
@@ -243,7 +241,7 @@ module Mournmail
243
241
  end
244
242
  summary_text = summary.to_s
245
243
  summary.save
246
- next_tick do
244
+ foreground do
247
245
  buffer.read_only_edit do
248
246
  buffer.clear
249
247
  buffer.insert(summary_text)
@@ -258,7 +256,7 @@ module Mournmail
258
256
  uid = selected_uid
259
257
  Mournmail.background do
260
258
  mail, = read_current_mail
261
- next_tick do
259
+ foreground do
262
260
  source_buffer = Buffer.find_or_new("*message-source*",
263
261
  file_encoding: "ascii-8bit",
264
262
  undo_limit: 0, read_only: true)
@@ -305,7 +303,7 @@ module Mournmail
305
303
  end
306
304
  s = mails.map { |mail| mail.body.decoded }.join
307
305
  mail = Mail.new(s)
308
- next_tick do
306
+ foreground do
309
307
  show_message(mail)
310
308
  Mournmail.current_uid = nil
311
309
  Mournmail.current_mail = mail
@@ -363,7 +361,7 @@ module Mournmail
363
361
  Mournmail.background do
364
362
  Mournmail.imap_connect do |imap|
365
363
  unless imap.list("", mailbox)
366
- if next_tick! { yes_or_no?("#{mailbox} doesn't exist; Create?") }
364
+ if foreground! { yes_or_no?("#{mailbox} doesn't exist; Create?") }
367
365
  imap.create(mailbox)
368
366
  else
369
367
  next
@@ -391,7 +389,7 @@ module Mournmail
391
389
  begin
392
390
  target_uids.each_slice(20) do |uids|
393
391
  data = imap.uid_fetch(uids, "BODY[]")
394
- data.each do |i|
392
+ data&.each do |i|
395
393
  uid = i.attr["UID"]
396
394
  s = i.attr["BODY[]"]
397
395
  if s
@@ -404,7 +402,7 @@ module Mournmail
404
402
  end
405
403
  count += uids.size
406
404
  progress = (count.to_f * 100 / target_uids.size).round
407
- next_tick do
405
+ foreground do
408
406
  message("Prefetching mails... #{progress}%", log: false)
409
407
  end
410
408
  end
@@ -412,7 +410,7 @@ module Mournmail
412
410
  summary.save
413
411
  end
414
412
  end
415
- next_tick do
413
+ foreground do
416
414
  message("Done")
417
415
  end
418
416
  end
@@ -428,7 +426,7 @@ module Mournmail
428
426
  match_record.subject | match_record.body
429
427
  }
430
428
  }.paginate([["date", :desc]], page: page, size: 100)
431
- next_tick do
429
+ foreground do
432
430
  show_search_result(messages, query: query)
433
431
  message("Searched (#{messages.current_page}/#{messages.n_pages})")
434
432
  end
@@ -442,7 +440,7 @@ module Mournmail
442
440
  messages = Groonga["Messages"].select { |m|
443
441
  m.thread_id == message.thread_id
444
442
  }.sort([["date", :asc]])
445
- next_tick do
443
+ foreground do
446
444
  show_search_result(messages, buffer_name: "*thread*")
447
445
  i = messages.find_index { |m| m._key == message._key }
448
446
  Buffer.current.goto_line(i + 1)
@@ -453,6 +451,12 @@ module Mournmail
453
451
  define_local_command(:summary_change_account,
454
452
  doc: "Change the current account.") do
455
453
  |account = read_account_name("Change account: ")|
454
+ unless CONFIG[:mournmail_accounts].key?(account)
455
+ raise EditorError, "No such account: #{account}"
456
+ end
457
+ if Mournmail.background_thread
458
+ raise EditorError, "Background thread is running"
459
+ end
456
460
  mournmail_quit
457
461
  Mournmail.current_account = account
458
462
  mournmail
@@ -534,7 +538,7 @@ module Mournmail
534
538
  Mournmail.background do
535
539
  summary_item.toggle_flag(flag)
536
540
  Mournmail.current_summary.save
537
- next_tick do
541
+ foreground do
538
542
  update_flags(summary_item)
539
543
  end
540
544
  end
@@ -596,7 +600,7 @@ module Mournmail
596
600
 
597
601
  def ljust(s, n)
598
602
  width = 0
599
- str = String.new
603
+ str = +""
600
604
  s.gsub(/\t/, " ").each_char do |c|
601
605
  w = Buffer.display_width(c)
602
606
  width += w
@@ -619,7 +623,7 @@ module Mournmail
619
623
  imap.uid_store(uid_set, "+FLAGS", [:Deleted])
620
624
  count += uid_set.size
621
625
  progress = (count.to_f * 100 / uids.size).round
622
- next_tick do
626
+ foreground do
623
627
  if dst_mailbox
624
628
  message("Refiling mails to #{dst_mailbox}... #{progress}%",
625
629
  log: false)
@@ -628,7 +632,7 @@ module Mournmail
628
632
  end
629
633
  end
630
634
  end
631
- next_tick do
635
+ foreground do
632
636
  if dst_mailbox
633
637
  message("Refiled mails to #{dst_mailbox}")
634
638
  else
@@ -660,7 +664,7 @@ module Mournmail
660
664
  end
661
665
  summary_text = summary.to_s
662
666
  summary.save
663
- next_tick do
667
+ foreground do
664
668
  @buffer.read_only_edit do
665
669
  @buffer.clear
666
670
  @buffer.insert(summary_text)
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  require "mail"
4
2
  require "net/imap"
5
3
  require "time"
@@ -7,7 +5,38 @@ require "tempfile"
7
5
  require "fileutils"
8
6
  require "timeout"
9
7
  require "digest"
8
+ require "nkf"
10
9
  require "groonga"
10
+ require 'google/api_client/client_secrets'
11
+ require 'google/api_client/auth/storage'
12
+ require 'google/api_client/auth/storages/file_store'
13
+ require 'launchy'
14
+
15
+ class Net::SMTP
16
+ def auth_xoauth2(user, secret)
17
+ check_auth_args user, secret
18
+ res = critical {
19
+ s = Mournmail.xoauth2_string(user, secret)
20
+ get_response('AUTH XOAUTH2 ' + base64_encode(s))
21
+ }
22
+ check_auth_response res
23
+ res
24
+ end
25
+ end
26
+
27
+ class Net::IMAP::Xoauth2Authenticator
28
+ def process(data)
29
+ Mournmail.xoauth2_string(@user, @access_token)
30
+ end
31
+
32
+ private
33
+
34
+ def initialize(user, access_token)
35
+ @user = user
36
+ @access_token = access_token
37
+ end
38
+ end
39
+ Net::IMAP.add_authenticator("XOAUTH2", Net::IMAP::Xoauth2Authenticator)
11
40
 
12
41
  module Mournmail
13
42
  begin
@@ -148,12 +177,17 @@ module Mournmail
148
177
  end
149
178
  if @imap.nil? || @imap.disconnected?
150
179
  conf = account_config
180
+ auth_type = conf[:imap_options][:auth_type] || "PLAIN"
181
+ password = conf[:imap_options][:password]
182
+ if auth_type == "gmail"
183
+ auth_type = "XOAUTH2"
184
+ password = google_access_token
185
+ end
151
186
  Timeout.timeout(CONFIG[:mournmail_imap_connect_timeout]) do
152
187
  @imap = Net::IMAP.new(conf[:imap_host],
153
188
  conf[:imap_options])
154
- @imap.authenticate(conf[:imap_options][:auth_type] || "PLAIN",
155
- conf[:imap_options][:user_name],
156
- conf[:imap_options][:password])
189
+ @imap.authenticate(auth_type, conf[:imap_options][:user_name],
190
+ password)
157
191
  @mailboxes = @imap.list("", "*").map { |mbox|
158
192
  Net::IMAP.decode_utf7(mbox.name)
159
193
  }
@@ -179,40 +213,113 @@ module Mournmail
179
213
  end
180
214
  end
181
215
 
216
+ def self.google_access_token
217
+ auth_path = File.expand_path("cache/#{current_account}/google_auth.json",
218
+ CONFIG[:mournmail_directory])
219
+ FileUtils.mkdir_p(File.dirname(auth_path))
220
+ store = Google::APIClient::FileStore.new(auth_path)
221
+ storage = Google::APIClient::Storage.new(store)
222
+ storage.authorize
223
+ if storage.authorization.nil?
224
+ path = File.expand_path(account_config[:client_secret_path])
225
+ client_secrets = Google::APIClient::ClientSecrets.load(path)
226
+ auth_client = client_secrets.to_authorization
227
+ auth_client.update!(
228
+ :scope => 'https://mail.google.com/',
229
+ :redirect_uri => 'urn:ietf:wg:oauth:2.0:oob'
230
+ )
231
+
232
+ auth_uri = auth_client.authorization_uri.to_s
233
+ Launchy.open(auth_uri)
234
+
235
+ auth_client.code = foreground! {
236
+ Window.echo_area.clear_message
237
+ Window.redisplay
238
+ read_from_minibuffer("Code: ").chomp
239
+ }
240
+ auth_client.fetch_access_token!
241
+ old_umask = File.umask(077)
242
+ begin
243
+ storage.write_credentials(auth_client)
244
+ ensure
245
+ File.umask(old_umask)
246
+ end
247
+ else
248
+ auth_client = storage.authorization
249
+ end
250
+ auth_client.access_token
251
+ end
252
+
253
+ def self.xoauth2_string(user, access_token)
254
+ "user=#{user}\1auth=Bearer #{access_token}\1\1"
255
+ end
256
+
182
257
  def self.fetch_summary(mailbox, all: false)
258
+ if all
259
+ summary = Mournmail::Summary.new(mailbox)
260
+ else
261
+ summary = Mournmail::Summary.load_or_new(mailbox)
262
+ end
183
263
  imap_connect do |imap|
184
264
  imap.select(mailbox)
185
- if all
186
- summary = Mournmail::Summary.new(mailbox)
187
- else
188
- summary = Mournmail::Summary.load_or_new(mailbox)
189
- end
190
265
  uidvalidity = imap.responses["UIDVALIDITY"].last
191
- if summary.uidvalidity.nil?
192
- summary.uidvalidity = uidvalidity
193
- elsif uidvalidity && uidvalidity != summary.uidvalidity
194
- clear = next_tick {
266
+ if uidvalidity && summary.uidvalidity &&
267
+ uidvalidity != summary.uidvalidity
268
+ clear = foreground! {
195
269
  yes_or_no?("UIDVALIDITY has been changed; Clear cache?")
196
270
  }
197
271
  if clear
198
272
  summary = Mournmail::Summary.new(mailbox)
199
273
  end
200
274
  end
201
- first_uid = (summary.last_uid || 0) + 1
202
- data = imap.uid_fetch(first_uid..-1, ["UID", "ENVELOPE", "FLAGS"])
275
+ summary.uidvalidity = uidvalidity
276
+ uids = imap.uid_search("ALL")
277
+ new_uids = uids - summary.uids
278
+ return summary if new_uids.empty?
203
279
  summary.synchronize do
204
- data&.each do |i|
205
- uid = i.attr["UID"]
206
- next if summary[uid]
207
- env = i.attr["ENVELOPE"]
208
- flags = i.attr["FLAGS"]
209
- item = Mournmail::SummaryItem.new(uid, env.date, env.from,
210
- env.subject, flags)
211
- summary.add_item(item, env.message_id, env.in_reply_to)
280
+ new_uids.each_slice(1000) do |uid_chunk|
281
+ data = imap.uid_fetch(uid_chunk, ["UID", "ENVELOPE", "FLAGS"])
282
+ data&.each do |i|
283
+ uid = i.attr["UID"]
284
+ next if summary[uid]
285
+ env = i.attr["ENVELOPE"]
286
+ flags = i.attr["FLAGS"]
287
+ item = Mournmail::SummaryItem.new(uid, env.date, env.from,
288
+ env.subject, flags)
289
+ summary.add_item(item, env.message_id, env.in_reply_to)
290
+ end
212
291
  end
213
292
  end
214
293
  summary
215
294
  end
295
+ rescue SocketError, Timeout::Error => e
296
+ foreground do
297
+ message(e.message)
298
+ end
299
+ summary
300
+ end
301
+
302
+ def self.show_summary(summary)
303
+ buffer = Buffer.find_or_new("*summary*", undo_limit: 0,
304
+ read_only: true)
305
+ buffer.apply_mode(Mournmail::SummaryMode)
306
+ buffer.read_only_edit do
307
+ buffer.clear
308
+ buffer.insert(summary.to_s)
309
+ end
310
+ switch_to_buffer(buffer)
311
+ Mournmail.current_mailbox = summary.mailbox
312
+ Mournmail.current_summary = summary
313
+ Mournmail.current_mail = nil
314
+ Mournmail.current_uid = nil
315
+ begin
316
+ buffer.beginning_of_buffer
317
+ buffer.re_search_forward(/^ *\d+ u/)
318
+ rescue SearchError
319
+ buffer.end_of_buffer
320
+ buffer.re_search_backward(/^ *\d+ /, raise_error: false)
321
+ end
322
+ summary_read_command
216
323
  end
217
324
 
218
325
  def self.mailbox_cache_path(mailbox)
@@ -277,6 +384,8 @@ module Mournmail
277
384
  references = Array(mail.references) | Array(mail.in_reply_to)
278
385
  if references.empty?
279
386
  mail.message_id
387
+ elsif /\Aredmine\.issue-/.match?(references.first)
388
+ references.first
280
389
  else
281
390
  parent = messages_db.select { |m|
282
391
  references.inject(nil) { |cond, ref|
@@ -343,17 +452,17 @@ module Mournmail
343
452
  end
344
453
 
345
454
  def self.force_utf8(s)
346
- s.force_encoding(Encoding::UTF_8).scrub("?")
455
+ s.dup.force_encoding(Encoding::UTF_8).scrub("?")
347
456
  end
348
457
 
349
458
  def self.to_utf8(s, charset)
350
- if /\Autf-8\z/i =~ charset
459
+ if /\Autf-8\z/i.match?(charset)
351
460
  force_utf8(s)
352
461
  else
353
462
  begin
354
463
  s.encode(Encoding::UTF_8, charset, replace: "?")
355
- rescue Encoding::ConverterNotFoundError
356
- force_utf8(s)
464
+ rescue
465
+ force_utf8(NKF.nkf("-w", s))
357
466
  end
358
467
  end.gsub(/\r\n/, "\n")
359
468
  end
@@ -404,4 +513,8 @@ module Mournmail
404
513
  @groonga_db.close
405
514
  end
406
515
  end
516
+
517
+ def self.parse_mail(s)
518
+ Mail.new(s.scrub("??"))
519
+ end
407
520
  end
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  module Mournmail
4
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
5
3
  end
@@ -1,3 +1 @@
1
- # frozen_string_literal: true
2
-
3
1
  require_relative "mournmail"
@@ -23,8 +23,11 @@ Gem::Specification.new do |spec|
23
23
 
24
24
  spec.add_runtime_dependency "textbringer"
25
25
  spec.add_runtime_dependency "mail"
26
+ spec.add_runtime_dependency "mime-types"
26
27
  spec.add_runtime_dependency "rroonga"
28
+ spec.add_runtime_dependency "google-api-client"
29
+ spec.add_runtime_dependency "launchy"
27
30
 
28
- spec.add_development_dependency "bundler", "~> 1.14"
29
- spec.add_development_dependency "rake", "~> 10.0"
31
+ spec.add_development_dependency "bundler"
32
+ spec.add_development_dependency "rake", "~> 12.0"
30
33
  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.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shugo Maeda
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-05-03 00:00:00.000000000 Z
11
+ date: 2020-03-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: textbringer
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: mime-types
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'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: rroonga
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -52,34 +66,62 @@ dependencies:
52
66
  - - ">="
53
67
  - !ruby/object:Gem::Version
54
68
  version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: google-api-client
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: launchy
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
55
97
  - !ruby/object:Gem::Dependency
56
98
  name: bundler
57
99
  requirement: !ruby/object:Gem::Requirement
58
100
  requirements:
59
- - - "~>"
101
+ - - ">="
60
102
  - !ruby/object:Gem::Version
61
- version: '1.14'
103
+ version: '0'
62
104
  type: :development
63
105
  prerelease: false
64
106
  version_requirements: !ruby/object:Gem::Requirement
65
107
  requirements:
66
- - - "~>"
108
+ - - ">="
67
109
  - !ruby/object:Gem::Version
68
- version: '1.14'
110
+ version: '0'
69
111
  - !ruby/object:Gem::Dependency
70
112
  name: rake
71
113
  requirement: !ruby/object:Gem::Requirement
72
114
  requirements:
73
115
  - - "~>"
74
116
  - !ruby/object:Gem::Version
75
- version: '10.0'
117
+ version: '12.0'
76
118
  type: :development
77
119
  prerelease: false
78
120
  version_requirements: !ruby/object:Gem::Requirement
79
121
  requirements:
80
122
  - - "~>"
81
123
  - !ruby/object:Gem::Version
82
- version: '10.0'
124
+ version: '12.0'
83
125
  description: A message user agent for Textbringer.
84
126
  email:
85
127
  - shugo@ruby-lang.org
@@ -128,8 +170,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
128
170
  - !ruby/object:Gem::Version
129
171
  version: '0'
130
172
  requirements: []
131
- rubyforge_project:
132
- rubygems_version: 2.7.6
173
+ rubygems_version: 3.2.0.pre1
133
174
  signing_key:
134
175
  specification_version: 4
135
176
  summary: A message user agent for Textbringer.