mournmail 0.2.0 → 0.3.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
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.