sup 0.5 → 0.6

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of sup might be problematic. Click here for more details.

data/CONTRIBUTORS ADDED
@@ -0,0 +1,13 @@
1
+ William Morgan <wmorgan-sup at the masanjin dot nets>
2
+ Ismo Puustinen <ismo at the iki dot fis>
3
+ Marcus Williams <marcus-sup at the bar-coded dot nets>
4
+ Lionel Ott <white.magic at the gmx dot des>
5
+ Nicolas Pouillard <nicolas.pouillard at the gmail dot coms>
6
+ Marc Hartstein <marc.hartstein at the alum.vassar dot edus>
7
+ Ben Walton <bwalton at the artsci.utoronto dot cas>
8
+ Grant Hollingworth <grant at the antiflux dot orgs>
9
+ Jeff Balogh <its.jeff.balogh at the gmail dot coms>
10
+ Christopher Warrington <chrisw at the rice dot edus>
11
+ Giorgio Lando <patroclo7 at the gmail dot coms>
12
+ Decklin Foster <decklin at the red-bean dot coms>
13
+ Ian Taylor <ian at the lorf dot orgs>
data/History.txt CHANGED
@@ -1,3 +1,13 @@
1
+ == 0.6 / 2008-08-04
2
+ * new hooks: mark-as-spam, reply-to, reply-from
3
+ * configurable colors. finally!
4
+ * many bugfixes
5
+ * more vi keys added, and 'q' now asks before quitting
6
+ * attachment markers (little @ signs!) in thread-index-mode
7
+ * maildir speedups
8
+ * attachment name searchability
9
+ * archive-and-mark-read command in inbox-mode
10
+
1
11
  == 0.5 / 2008-04-22
2
12
  * new hooks: extra-contact-addresses, startup
3
13
  * '!!' now loads all threads in current search
data/Manifest.txt CHANGED
@@ -1,3 +1,4 @@
1
+ CONTRIBUTORS
1
2
  HACKING
2
3
  History.txt
3
4
  LICENSE
@@ -17,7 +18,6 @@ doc/FAQ.txt
17
18
  doc/Hooks.txt
18
19
  doc/NewUserGuide.txt
19
20
  doc/Philosophy.txt
20
- doc/TODO
21
21
  lib/sup.rb
22
22
  lib/sup/account.rb
23
23
  lib/sup/buffer.rb
data/Rakefile CHANGED
@@ -24,7 +24,7 @@ Hoe.new('sup', version) do |p|
24
24
  p.url = p.paragraphs_of('README.txt', 0).first.split(/\n/)[2].gsub(/^\s+/, "")
25
25
  p.changes = p.paragraphs_of('History.txt', 0..0).join("\n\n")
26
26
  p.email = "wmorgan-sup@masanjin.net"
27
- p.extra_deps = [['ferret', '>= 0.10.13'], ['ncurses', '>= 0.9.1'], ['rmail', '>= 0.17'], 'highline', 'net-ssh', ['trollop', '>= 1.7'], 'lockfile', 'mime-types', 'gettext']
27
+ p.extra_deps = [['ferret', '>= 0.10.13'], ['ncurses', '>= 0.9.1'], ['rmail', '>= 0.17'], 'highline', 'net-ssh', ['trollop', '>= 1.7'], 'lockfile', 'mime-types', 'gettext', 'fastthread']
28
28
  end
29
29
 
30
30
  rule 'ss?.png' => 'ss?-small.png' do |t|
@@ -45,11 +45,11 @@ SCREENSHOTS.each do |fn|
45
45
  end
46
46
 
47
47
  task :upload_webpage => WWW_FILES do |t|
48
- sh "scp -C #{t.prerequisites * ' '} wmorgan@rubyforge.org:/var/www/gforge-projects/sup/"
48
+ sh "rsync -essh -cavz #{t.prerequisites * ' '} wmorgan@rubyforge.org:/var/www/gforge-projects/sup/"
49
49
  end
50
50
 
51
51
  task :upload_webpage_images => (SCREENSHOTS + SCREENSHOTS_SMALL) do |t|
52
- sh "scp -C #{t.prerequisites * ' '} wmorgan@rubyforge.org:/var/www/gforge-projects/sup/"
52
+ sh "rsync -essh -cavz #{t.prerequisites * ' '} wmorgan@rubyforge.org:/var/www/gforge-projects/sup/"
53
53
  end
54
54
 
55
55
  # vim: syntax=ruby
data/ReleaseNotes CHANGED
@@ -1,3 +1,9 @@
1
+ Release 0.6:
2
+
3
+ Message attachment searchability automatically takes effect on new messages,
4
+ but if you want it on older ones, you'll have to reindex them. See the
5
+ instructions below, and the help for sup-sync, for how to do this.
6
+
1
7
  Release 0.5:
2
8
 
3
9
  Saving message state (pressing "$") has been sped up. However, this is only
data/bin/sup CHANGED
@@ -5,9 +5,10 @@ require 'ncurses'
5
5
  require 'curses'
6
6
  require 'fileutils'
7
7
  require 'trollop'
8
+ require 'fastthread'
8
9
  require "sup"
9
10
 
10
- BIN_VERSION = "0.5"
11
+ BIN_VERSION = "0.6"
11
12
 
12
13
  unless Redwood::VERSION == BIN_VERSION
13
14
  $stderr.puts <<EOS
@@ -21,7 +22,6 @@ EOS
21
22
  exit(-1)
22
23
  end
23
24
 
24
- $exceptions = []
25
25
  $opts = Trollop::options do
26
26
  version "sup v#{Redwood::VERSION}"
27
27
  banner <<EOS
@@ -55,7 +55,8 @@ Thread.abort_on_exception = true # make debugging possible
55
55
  module Redwood
56
56
 
57
57
  global_keymap = Keymap.new do |k|
58
- k.add :quit, "Quit Redwood", 'q'
58
+ k.add :quit_ask, "Quit Sup, but ask first", 'q'
59
+ k.add :quit_now, "Quit Sup immediately", 'Q'
59
60
  k.add :help, "Show help", 'H', '?'
60
61
  k.add :roll_buffers, "Switch to next buffer", 'b'
61
62
  # k.add :roll_buffers_backwards, "Switch to previous buffer", 'B'
@@ -139,53 +140,8 @@ begin
139
140
  log "starting curses"
140
141
  start_cursing
141
142
 
142
- Colormap.new do |c|
143
- c.add :status_color, Ncurses::COLOR_WHITE, Ncurses::COLOR_BLUE, Ncurses::A_BOLD
144
- c.add :index_old_color, Ncurses::COLOR_WHITE, Ncurses::COLOR_BLACK
145
- c.add :index_new_color, Ncurses::COLOR_WHITE, Ncurses::COLOR_BLACK,
146
- Ncurses::A_BOLD
147
- c.add :index_starred_color, Ncurses::COLOR_YELLOW, Ncurses::COLOR_BLACK,
148
- Ncurses::A_BOLD
149
- c.add :index_draft_color, Ncurses::COLOR_RED, Ncurses::COLOR_BLACK,
150
- Ncurses::A_BOLD
151
- c.add :labellist_old_color, Ncurses::COLOR_WHITE, Ncurses::COLOR_BLACK
152
- c.add :labellist_new_color, Ncurses::COLOR_WHITE, Ncurses::COLOR_BLACK,
153
- Ncurses::A_BOLD
154
- c.add :twiddle_color, Ncurses::COLOR_BLUE, Ncurses::COLOR_BLACK
155
- c.add :label_color, Ncurses::COLOR_YELLOW, Ncurses::COLOR_BLACK
156
- c.add :message_patina_color, Ncurses::COLOR_BLACK, Ncurses::COLOR_GREEN
157
- c.add :alternate_patina_color, Ncurses::COLOR_BLACK, Ncurses::COLOR_BLUE
158
- c.add :missing_message_color, Ncurses::COLOR_BLACK, Ncurses::COLOR_RED
159
- c.add :attachment_color, Ncurses::COLOR_CYAN, Ncurses::COLOR_BLACK
160
- c.add :cryptosig_valid_color, Ncurses::COLOR_YELLOW, Ncurses::COLOR_BLACK, Ncurses::A_BOLD
161
- c.add :cryptosig_unknown_color, Ncurses::COLOR_CYAN, Ncurses::COLOR_BLACK
162
- c.add :cryptosig_invalid_color, Ncurses::COLOR_YELLOW, Ncurses::COLOR_RED, Ncurses::A_BOLD
163
- c.add :generic_notice_patina_color, Ncurses::COLOR_CYAN, Ncurses::COLOR_BLACK
164
- c.add :quote_patina_color, Ncurses::COLOR_YELLOW, Ncurses::COLOR_BLACK
165
- c.add :sig_patina_color, Ncurses::COLOR_YELLOW, Ncurses::COLOR_BLACK
166
- c.add :quote_color, Ncurses::COLOR_YELLOW, Ncurses::COLOR_BLACK
167
- c.add :sig_color, Ncurses::COLOR_YELLOW, Ncurses::COLOR_BLACK
168
- c.add :to_me_color, Ncurses::COLOR_GREEN, Ncurses::COLOR_BLACK
169
- c.add :starred_color, Ncurses::COLOR_YELLOW, Ncurses::COLOR_BLACK,
170
- Ncurses::A_BOLD
171
- c.add :starred_patina_color, Ncurses::COLOR_YELLOW, Ncurses::COLOR_GREEN,
172
- Ncurses::A_BOLD
173
- c.add :alternate_starred_patina_color, Ncurses::COLOR_YELLOW,
174
- Ncurses::COLOR_BLUE, Ncurses::A_BOLD
175
- c.add :snippet_color, Ncurses::COLOR_CYAN, Ncurses::COLOR_BLACK
176
- c.add :option_color, Ncurses::COLOR_WHITE, Ncurses::COLOR_BLACK
177
- c.add :tagged_color, Ncurses::COLOR_YELLOW, Ncurses::COLOR_BLACK,
178
- Ncurses::A_BOLD
179
- c.add :draft_notification_color, Ncurses::COLOR_RED, Ncurses::COLOR_BLACK,
180
- Ncurses::A_BOLD
181
- c.add :completion_character_color, Ncurses::COLOR_WHITE,
182
- Ncurses::COLOR_BLACK, Ncurses::A_BOLD
183
- c.add :horizontal_selector_selected_color, Ncurses::COLOR_YELLOW, Ncurses::COLOR_BLACK, Ncurses::A_BOLD
184
- c.add :horizontal_selector_unselected_color, Ncurses::COLOR_CYAN, Ncurses::COLOR_BLACK
185
- c.add :search_highlight_color, Ncurses::COLOR_BLACK, Ncurses::COLOR_YELLOW, Ncurses::A_BOLD, :highlight => :search_highlight_color
186
- end
187
-
188
143
  bm = BufferManager.new
144
+ Colormap.new.populate_colormap
189
145
 
190
146
  log "initializing mail index buffer"
191
147
  imode = InboxMode.new
@@ -223,7 +179,7 @@ begin
223
179
  SearchResultsMode.spawn_from_query $opts[:search]
224
180
  end
225
181
 
226
- until $exceptions.nonempty? || SuicideManager.die?
182
+ until Redwood::exceptions.nonempty? || SuicideManager.die?
227
183
  c = Ncurses.nonblocking_getch
228
184
  next unless c
229
185
  bm.erase_flash
@@ -240,8 +196,12 @@ begin
240
196
  end
241
197
 
242
198
  case action
243
- when :quit
199
+ when :quit_now
244
200
  break if bm.kill_all_buffers_safely
201
+ when :quit_ask
202
+ if bm.ask_yes_or_no "Really quit?"
203
+ break if bm.kill_all_buffers_safely
204
+ end
245
205
  when :help
246
206
  curmode = bm.focus_buf.mode
247
207
  bm.spawn_unless_exists("<help for #{curmode.name}>") { HelpMode.new curmode, global_keymap }
@@ -300,7 +260,7 @@ begin
300
260
 
301
261
  bm.kill_all_buffers if SuicideManager.die?
302
262
  rescue Exception => e
303
- $exceptions << [e, "main"]
263
+ Redwood::record_exception e, "main"
304
264
  ensure
305
265
  unless $opts[:no_threads]
306
266
  PollManager.stop if PollManager.instantiated?
@@ -316,7 +276,7 @@ ensure
316
276
  Redwood::log "I've been ordered to commit seppuku. I obey!"
317
277
  end
318
278
 
319
- if $exceptions.empty?
279
+ if Redwood::exceptions.empty?
320
280
  Redwood::log "no fatal errors. good job, william."
321
281
  Index.save
322
282
  else
@@ -326,9 +286,9 @@ ensure
326
286
  Index.unlock
327
287
  end
328
288
 
329
- unless $exceptions.empty?
330
- File.open("sup-exception-log.txt", "w") do |f|
331
- $exceptions.each do |e, name|
289
+ unless Redwood::exceptions.empty?
290
+ File.open(File.join(BASE_DIR, "exception-log.txt"), "w") do |f|
291
+ Redwood::exceptions.each do |e, name|
332
292
  f.puts "--- #{e.class.name} from thread: #{name}"
333
293
  f.puts e.message, e.backtrace
334
294
  end
@@ -337,7 +297,7 @@ unless $exceptions.empty?
337
297
  ----------------------------------------------------------------
338
298
  I'm very sorry. It seems that an error occurred in Sup. Please
339
299
  accept my sincere apologies. If you don't mind, please send the
340
- contents of sup-exception-log.txt and a brief report of the
300
+ contents of ~/.sup/exception-log.txt and a brief report of the
341
301
  circumstances to sup-talk at rubyforge dot orgs so that I might
342
302
  address this problem. Thank you!
343
303
 
@@ -345,7 +305,7 @@ Sincerely,
345
305
  William
346
306
  ----------------------------------------------------------------
347
307
  EOS
348
- $exceptions.each do |e, name|
308
+ Redwood::exceptions.each do |e, name|
349
309
  puts "--- #{e.class.name} from thread: #{name}"
350
310
  puts e.message, e.backtrace
351
311
  end
data/lib/sup/colormap.rb CHANGED
@@ -1,3 +1,7 @@
1
+ module Curses
2
+ COLOR_DEFAULT = -1
3
+ end
4
+
1
5
  module Redwood
2
6
 
3
7
  class Colormap
@@ -6,8 +10,44 @@ class Colormap
6
10
  CURSES_COLORS = [Curses::COLOR_BLACK, Curses::COLOR_RED, Curses::COLOR_GREEN,
7
11
  Curses::COLOR_YELLOW, Curses::COLOR_BLUE,
8
12
  Curses::COLOR_MAGENTA, Curses::COLOR_CYAN,
9
- Curses::COLOR_WHITE]
13
+ Curses::COLOR_WHITE, Curses::COLOR_DEFAULT]
10
14
  NUM_COLORS = 15
15
+
16
+ DEFAULT_COLORS = {
17
+ :status => { :fg => "white", :bg => "blue", :attrs => ["bold"] },
18
+ :index_old => { :fg => "white", :bg => "black" },
19
+ :index_new => { :fg => "white", :bg => "black", :attrs => ["bold"] },
20
+ :index_starred => { :fg => "yellow", :bg => "black", :attrs => ["bold"] },
21
+ :index_draft => { :fg => "red", :bg => "black", :attrs => ["bold"] },
22
+ :labellist_old => { :fg => "white", :bg => "black" },
23
+ :labellist_new => { :fg => "white", :bg => "black", :attrs => ["bold"] },
24
+ :twiddle => { :fg => "blue", :bg => "black" },
25
+ :label => { :fg => "yellow", :bg => "black" },
26
+ :message_patina => { :fg => "black", :bg => "green" },
27
+ :alternate_patina => { :fg => "black", :bg => "blue" },
28
+ :missing_message => { :fg => "black", :bg => "red" },
29
+ :attachment => { :fg => "cyan", :bg => "black" },
30
+ :cryptosig_valid => { :fg => "yellow", :bg => "black", :attrs => ["bold"] },
31
+ :cryptosig_unknown => { :fg => "cyan", :bg => "black" },
32
+ :cryptosig_invalid => { :fg => "yellow", :bg => "red", :attrs => ["bold"] },
33
+ :generic_notice_patina => { :fg => "cyan", :bg => "black" },
34
+ :quote_patina => { :fg => "yellow", :bg => "black" },
35
+ :sig_patina => { :fg => "yellow", :bg => "black" },
36
+ :quote => { :fg => "yellow", :bg => "black" },
37
+ :sig => { :fg => "yellow", :bg => "black" },
38
+ :to_me => { :fg => "green", :bg => "black" },
39
+ :starred => { :fg => "yellow", :bg => "black", :attrs => ["bold"] },
40
+ :starred_patina => { :fg => "yellow", :bg => "green", :attrs => ["bold"] },
41
+ :alternate_starred_patina => { :fg => "yellow", :bg => "blue", :attrs => ["bold"] },
42
+ :snippet => { :fg => "cyan", :bg => "black" },
43
+ :option => { :fg => "white", :bg => "black" },
44
+ :tagged => { :fg => "yellow", :bg => "black", :attrs => ["bold"] },
45
+ :draft_notification => { :fg => "red", :bg => "black", :attrs => ["bold"] },
46
+ :completion_character => { :fg => "white", :bg => "black", :attrs => ["bold"] },
47
+ :horizontal_selector_selected => { :fg => "yellow", :bg => "black", :attrs => ["bold"] },
48
+ :horizontal_selector_unselected => { :fg => "cyan", :bg => "black" },
49
+ :search_highlight => { :fg => "black", :bg => "yellow", :attrs => ["bold"] }
50
+ }
11
51
 
12
52
  def initialize
13
53
  raise "only one instance can be created" if @@instance
@@ -108,9 +148,61 @@ class Colormap
108
148
  color
109
149
  end
110
150
 
151
+ ## Try to use the user defined colors, in case of an error fall back
152
+ ## to the default ones.
153
+ def populate_colormap
154
+ user_colors = if File.exists? Redwood::COLOR_FN
155
+ Redwood::log "loading user colors from #{Redwood::COLOR_FN}"
156
+ Redwood::load_yaml_obj Redwood::COLOR_FN
157
+ end
158
+
159
+ error = nil
160
+ Colormap::DEFAULT_COLORS.each_pair do |k, v|
161
+ fg = Curses.const_get "COLOR_#{v[:fg].upcase}"
162
+ bg = Curses.const_get "COLOR_#{v[:bg].upcase}"
163
+ attrs = v[:attrs] ? v[:attrs].map { |a| Curses.const_get "A_#{a.upcase}" } : []
164
+
165
+ if user_colors && (ucolor = user_colors[k])
166
+ if(ufg = ucolor[:fg])
167
+ begin
168
+ fg = Curses.const_get "COLOR_#{ufg.upcase}"
169
+ rescue NameError
170
+ error ||= "Warning: there is no color named \"#{ufg}\", using fallback."
171
+ Redwood::log "Warning: there is no color named \"#{ufg}\""
172
+ end
173
+ end
174
+
175
+ if(ubg = ucolor[:bg])
176
+ begin
177
+ bg = Curses.const_get "COLOR_#{ubg.upcase}"
178
+ rescue NameError
179
+ error ||= "Warning: there is no color named \"#{ubg}\", using fallback."
180
+ Redwood::log "Warning: there is no color named \"#{ubg}\""
181
+ end
182
+ end
183
+
184
+ if(uattrs = ucolor[:attrs])
185
+ attrs = [*uattrs].flatten.map do |a|
186
+ begin
187
+ Curses.const_get "A_#{a.upcase}"
188
+ rescue NameError
189
+ error ||= "Warning: there is no attribute named \"#{a}\", using fallback."
190
+ Redwood::log "Warning: there is no attribute named \"#{a}\", using fallback."
191
+ end
192
+ end
193
+ end
194
+ end
195
+
196
+ symbol = (k.to_s + "_color").to_sym
197
+ add symbol, fg, bg, attrs
198
+ end
199
+
200
+ BufferManager.flash error if error
201
+ end
202
+
111
203
  def self.instance; @@instance; end
112
204
  def self.method_missing meth, *a
113
- Colorcolors.new unless @@instance
205
+ Colormap.new unless @@instance
114
206
  @@instance.send meth, *a
115
207
  end
116
208
  end
data/lib/sup/crypto.rb CHANGED
@@ -53,7 +53,7 @@ class CryptoManager
53
53
  payload_fn.write format_payload(payload)
54
54
  payload_fn.close
55
55
 
56
- recipient_opts = to.map { |r| "--recipient '#{r}'" }.join(" ")
56
+ recipient_opts = to.map { |r| "--recipient '<#{r}>'" }.join(" ")
57
57
  sign_opts = sign ? "--sign --local-user '#{from}'" : ""
58
58
  gpg_output = run_gpg "--output - --armor --encrypt --textmode #{sign_opts} #{recipient_opts} #{payload_fn.path}"
59
59
  raise Error, (gpg_output || "gpg command failed: #{cmd}") unless $?.success?
@@ -150,6 +150,8 @@ private
150
150
  ["Can't find gpg binary in path."]
151
151
  end
152
152
 
153
+ ## here's where we munge rmail output into the format that signed/encrypted
154
+ ## PGP/GPG messages should be
153
155
  def format_payload payload
154
156
  payload.to_s.gsub(/(^|[^\r])\n/, "\\1\r\n").gsub(/^MIME-Version: .*\r\n/, "")
155
157
  end
data/lib/sup/draft.rb CHANGED
@@ -106,7 +106,7 @@ class DraftLoader < Source
106
106
  end
107
107
 
108
108
  def raw_message offset
109
- IO.readlines(fn_for_offset(offset)).join
109
+ IO.read(fn_for_offset(offset))
110
110
  end
111
111
 
112
112
  def start_offset; 0; end
data/lib/sup/hook.rb CHANGED
@@ -120,7 +120,7 @@ private
120
120
  unless @hooks.member? name
121
121
  @hooks[name] =
122
122
  begin
123
- returning IO.readlines(fn_for(name)).join do
123
+ returning IO.read(fn_for(name)) do
124
124
  log "read '#{name}' from #{fn_for(name)}"
125
125
  end
126
126
  rescue SystemCallError => e
data/lib/sup/index.rb CHANGED
@@ -147,6 +147,7 @@ EOS
147
147
  field_infos.add_field :date, :index => :untokenized
148
148
  field_infos.add_field :body
149
149
  field_infos.add_field :label
150
+ field_infos.add_field :attachments
150
151
  field_infos.add_field :subject
151
152
  field_infos.add_field :from
152
153
  field_infos.add_field :to
@@ -223,6 +224,7 @@ EOS
223
224
  :body => (entry[:body] || m.indexable_content),
224
225
  :snippet => snippet, # always override
225
226
  :label => labels.uniq.join(" "),
227
+ :attachments => (entry[:attachments] || m.attachments.uniq.join(" ")),
226
228
  :from => (entry[:from] || (m.from ? m.from.indexable_content : "")),
227
229
  :to => (entry[:to] || (m.to + m.cc + m.bcc).map { |x| x.indexable_content }.join(" ")),
228
230
  :subject => (entry[:subject] || wrap_subj(Message.normalize_subj(m.subj))),
@@ -465,7 +467,7 @@ protected
465
467
  extraopts[:load_deleted] = true if subs =~ /\blabel:deleted\b/
466
468
 
467
469
  ## gmail style "is" operator
468
- subs = subs.gsub(/\b(is):(\S+)\b/) do
470
+ subs = subs.gsub(/\b(is|has):(\S+)\b/) do
469
471
  field, label = $1, $2
470
472
  case label
471
473
  when "read"
@@ -481,6 +483,19 @@ protected
481
483
  end
482
484
  end
483
485
 
486
+ ## gmail style attachments "filename" and "filetype" searches
487
+ subs = subs.gsub(/\b(filename|filetype):(\((.+?)\)\B|(\S+)\b)/) do
488
+ field, name = $1, ($3 || $4)
489
+ case field
490
+ when "filename"
491
+ Redwood::log "filename - translated #{field}:#{name} to attachments:(#{name.downcase})"
492
+ "attachments:(#{name.downcase})"
493
+ when "filetype"
494
+ Redwood::log "filetype - translated #{field}:#{name} to attachments:(*.#{name.downcase})"
495
+ "attachments:(*.#{name.downcase})"
496
+ end
497
+ end
498
+
484
499
  if $have_chronic
485
500
  chronic_failure = false
486
501
  subs = subs.gsub(/\b(before|on|in|during|after):(\((.+?)\)\B|(\S+)\b)/) do
data/lib/sup/keymap.rb CHANGED
@@ -43,16 +43,10 @@ class Keymap
43
43
  when :home: "<home>"
44
44
  when :end: "<end>"
45
45
  when :enter, :return: "<enter>"
46
- when :ctrl_l: "ctrl-l"
47
- when :ctrl_g: "ctrl-g"
48
46
  when :tab: "tab"
49
47
  when " ": "<space>"
50
48
  else
51
- if k.is_a?(String) && k.length == 1
52
- k
53
- else
54
- raise ArgumentError, "unknown key name \"#{k}\""
55
- end
49
+ Curses::keyname(keysym_to_keycode(k))
56
50
  end
57
51
  end
58
52
 
data/lib/sup/label.rb CHANGED
@@ -5,13 +5,13 @@ class LabelManager
5
5
 
6
6
  ## labels that have special semantics. user will be unable to
7
7
  ## add/remove these via normal label mechanisms.
8
- RESERVED_LABELS = [ :starred, :spam, :draft, :unread, :killed, :sent, :deleted, :inbox ]
8
+ RESERVED_LABELS = [ :starred, :spam, :draft, :unread, :killed, :sent, :deleted, :inbox, :attachment ]
9
9
 
10
10
  ## labels which it nonetheless makes sense to search for by
11
- LISTABLE_RESERVED_LABELS = [ :starred, :spam, :draft, :sent, :killed, :deleted, :inbox ]
11
+ LISTABLE_RESERVED_LABELS = [ :starred, :spam, :draft, :sent, :killed, :deleted, :inbox, :attachment ]
12
12
 
13
13
  ## labels that will typically be hidden from the user
14
- HIDDEN_RESERVED_LABELS = [ :starred, :unread ]
14
+ HIDDEN_RESERVED_LABELS = [ :starred, :unread, :attachment ]
15
15
 
16
16
  def initialize fn
17
17
  @fn = fn
data/lib/sup/maildir.rb CHANGED
@@ -12,14 +12,14 @@ class Maildir < Source
12
12
  SCAN_INTERVAL = 30 # seconds
13
13
 
14
14
  ## remind me never to use inheritance again.
15
- yaml_properties :uri, :cur_offset, :usual, :archived, :id, :labels
16
- def initialize uri, last_date=nil, usual=true, archived=false, id=nil, labels=[]
15
+ yaml_properties :uri, :cur_offset, :usual, :archived, :id, :labels, :mtimes
16
+ def initialize uri, last_date=nil, usual=true, archived=false, id=nil, labels=[], mtimes={}
17
17
  super uri, last_date, usual, archived, id
18
18
  uri = URI(Source.expand_filesystem_uri(uri))
19
19
 
20
20
  raise ArgumentError, "not a maildir URI" unless uri.scheme == "maildir"
21
21
  raise ArgumentError, "maildir URI cannot have a host: #{uri.host}" if uri.host
22
- raise ArgumentError, "mbox URI must have a path component" unless uri.path
22
+ raise ArgumentError, "maildir URI must have a path component" unless uri.path
23
23
 
24
24
  @dir = uri.path
25
25
  @labels = (labels || []).freeze
@@ -27,6 +27,11 @@ class Maildir < Source
27
27
  @ids_to_fns = {}
28
28
  @last_scan = nil
29
29
  @mutex = Mutex.new
30
+ #the mtime from the subdirs in the maildir with the unix epoch as default.
31
+ #these are used to determine whether scanning the directory for new mail
32
+ #is a worthwhile effort
33
+ @mtimes = { 'cur' => Time.at(0), 'new' => Time.at(0) }.merge(mtimes || {})
34
+ @dir_ids = { 'cur' => [], 'new' => [] }
30
35
  end
31
36
 
32
37
  def file_path; @dir end
@@ -72,28 +77,38 @@ class Maildir < Source
72
77
 
73
78
  def raw_message id
74
79
  scan_mailbox
75
- with_file_for(id) { |f| f.readlines.join }
80
+ with_file_for(id) { |f| f.read }
76
81
  end
77
82
 
78
83
  def scan_mailbox opts={}
79
84
  return unless @ids.empty? || opts[:rescan]
80
85
  return if @last_scan && (Time.now - @last_scan) < SCAN_INTERVAL
81
86
 
82
- Redwood::log "scanning maildir..."
83
- cdir = File.join(@dir, 'cur')
84
- ndir = File.join(@dir, 'new')
85
-
86
- raise FatalSourceError, "#{cdir} not a directory" unless File.directory? cdir
87
- raise FatalSourceError, "#{ndir} not a directory" unless File.directory? ndir
87
+ initial_poll = @ids.empty?
88
88
 
89
+ Redwood::log "scanning maildir #@dir..."
89
90
  begin
90
- @ids, @ids_to_fns = [], {}
91
- (Dir[File.join(cdir, "*")] + Dir[File.join(ndir, "*")]).map do |fn|
92
- id = make_id fn
93
- @ids << id
94
- @ids_to_fns[id] = fn
91
+ @mtimes.each_key do |d|
92
+ subdir = File.join(@dir, d)
93
+ raise FatalSourceError, "#{subdir} not a directory" unless File.directory? subdir
94
+
95
+ mtime = File.mtime subdir
96
+
97
+ #only scan the dir if the mtime is more recent (or we haven't polled
98
+ #since startup)
99
+ if @mtimes[d] < mtime || initial_poll
100
+ @mtimes[d] = mtime
101
+ @dir_ids[d] = []
102
+ Dir[File.join(subdir, '*')].map do |fn|
103
+ id = make_id fn
104
+ @dir_ids[d] << id
105
+ @ids_to_fns[id] = fn
106
+ end
107
+ else
108
+ Redwood::log "no poll on #{d}. mtime on indicates no new messages."
109
+ end
95
110
  end
96
- @ids.sort!
111
+ @ids = @dir_ids.values.flatten.uniq.sort!
97
112
  rescue SystemCallError, IOError => e
98
113
  raise FatalSourceError, "Problem scanning Maildir directories: #{e.message}."
99
114
  end
@@ -145,8 +160,11 @@ class Maildir < Source
145
160
  private
146
161
 
147
162
  def make_id fn
163
+ #doing this means 1 syscall instead of 2 (File.mtime, File.size).
164
+ #makes a noticeable difference on nfs.
165
+ stat = File.stat(fn)
148
166
  # use 7 digits for the size. why 7? seems nice.
149
- sprintf("%d%07d", File.mtime(fn), File.size(fn) % 10000000).to_i
167
+ sprintf("%d%07d", stat.mtime, stat.size % 10000000).to_i
150
168
  end
151
169
 
152
170
  def with_file_for id
data/lib/sup/mbox.rb CHANGED
@@ -11,6 +11,7 @@ module Redwood
11
11
  ## TODO: move functionality to somewhere better, like message.rb
12
12
  module MBox
13
13
  BREAK_RE = /^From \S+/
14
+ HEADER_RE = /\s*(.*?\S)\s*/
14
15
 
15
16
  def read_header f
16
17
  header = {}
@@ -20,26 +21,26 @@ module MBox
20
21
  ## when scanning over large mbox files.
21
22
  while(line = f.gets)
22
23
  case line
23
- when /^(From):\s*(.*?)\s*$/i,
24
- /^(To):\s*(.*?)\s*$/i,
25
- /^(Cc):\s*(.*?)\s*$/i,
26
- /^(Bcc):\s*(.*?)\s*$/i,
27
- /^(Subject):\s*(.*?)\s*$/i,
28
- /^(Date):\s*(.*?)\s*$/i,
29
- /^(References):\s*(.*?)\s*$/i,
30
- /^(In-Reply-To):\s*(.*?)\s*$/i,
31
- /^(Reply-To):\s*(.*?)\s*$/i,
32
- /^(List-Post):\s*(.*?)\s*$/i,
33
- /^(List-Subscribe):\s*(.*?)\s*$/i,
34
- /^(List-Unsubscribe):\s*(.*?)\s*$/i,
35
- /^(Status):\s*(.*?)\s*$/i: header[last = $1] = $2
36
- when /^(Message-Id):\s*(.*?)\s*$/i: header[mid_field = last = $1] = $2
24
+ when /^(From):#{HEADER_RE}$/i,
25
+ /^(To):#{HEADER_RE}$/i,
26
+ /^(Cc):#{HEADER_RE}$/i,
27
+ /^(Bcc):#{HEADER_RE}$/i,
28
+ /^(Subject):#{HEADER_RE}$/i,
29
+ /^(Date):#{HEADER_RE}$/i,
30
+ /^(References):#{HEADER_RE}$/i,
31
+ /^(In-Reply-To):#{HEADER_RE}$/i,
32
+ /^(Reply-To):#{HEADER_RE}$/i,
33
+ /^(List-Post):#{HEADER_RE}$/i,
34
+ /^(List-Subscribe):#{HEADER_RE}$/i,
35
+ /^(List-Unsubscribe):#{HEADER_RE}$/i,
36
+ /^(Status):#{HEADER_RE}$/i: header[last = $1] = $2
37
+ when /^(Message-Id):#{HEADER_RE}$/i: header[mid_field = last = $1] = $2
37
38
 
38
39
  ## these next three can occur multiple times, and we want the
39
40
  ## first one
40
- when /^(Delivered-To):\s*(.*)$/i,
41
- /^(X-Original-To):\s*(.*)$/i,
42
- /^(Envelope-To):\s*(.*)$/i: header[last = $1] ||= $2
41
+ when /^(Delivered-To):#{HEADER_RE}$/i,
42
+ /^(X-Original-To):#{HEADER_RE}$/i,
43
+ /^(Envelope-To):#{HEADER_RE}$/i: header[last = $1] ||= $2
43
44
 
44
45
  when /^\r*$/: break
45
46
  when /^\S+:/: last = nil # some other header we don't care about