sup 0.8.1 → 0.9

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.

Files changed (67) hide show
  1. data/CONTRIBUTORS +13 -6
  2. data/History.txt +19 -0
  3. data/ReleaseNotes +35 -0
  4. data/bin/sup +82 -77
  5. data/bin/sup-add +7 -7
  6. data/bin/sup-config +104 -85
  7. data/bin/sup-dump +4 -5
  8. data/bin/sup-recover-sources +9 -10
  9. data/bin/sup-sync +121 -100
  10. data/bin/sup-sync-back +18 -15
  11. data/bin/sup-tweak-labels +24 -21
  12. data/lib/sup.rb +53 -33
  13. data/lib/sup/account.rb +0 -2
  14. data/lib/sup/buffer.rb +47 -22
  15. data/lib/sup/colormap.rb +6 -6
  16. data/lib/sup/contact.rb +0 -2
  17. data/lib/sup/crypto.rb +34 -23
  18. data/lib/sup/draft.rb +6 -14
  19. data/lib/sup/ferret_index.rb +471 -0
  20. data/lib/sup/hook.rb +30 -43
  21. data/lib/sup/hook.rb.BACKUP.8625.rb +158 -0
  22. data/lib/sup/hook.rb.BACKUP.8681.rb +158 -0
  23. data/lib/sup/hook.rb.BASE.8625.rb +155 -0
  24. data/lib/sup/hook.rb.BASE.8681.rb +155 -0
  25. data/lib/sup/hook.rb.LOCAL.8625.rb +142 -0
  26. data/lib/sup/hook.rb.LOCAL.8681.rb +142 -0
  27. data/lib/sup/hook.rb.REMOTE.8625.rb +145 -0
  28. data/lib/sup/hook.rb.REMOTE.8681.rb +145 -0
  29. data/lib/sup/imap.rb +18 -8
  30. data/lib/sup/index.rb +70 -528
  31. data/lib/sup/interactive-lock.rb +74 -0
  32. data/lib/sup/keymap.rb +26 -26
  33. data/lib/sup/label.rb +2 -4
  34. data/lib/sup/logger.rb +54 -35
  35. data/lib/sup/maildir.rb +41 -6
  36. data/lib/sup/mbox.rb +1 -1
  37. data/lib/sup/mbox/loader.rb +18 -6
  38. data/lib/sup/mbox/ssh-file.rb +1 -7
  39. data/lib/sup/message-chunks.rb +36 -23
  40. data/lib/sup/message.rb +126 -46
  41. data/lib/sup/mode.rb +3 -2
  42. data/lib/sup/modes/console-mode.rb +108 -0
  43. data/lib/sup/modes/edit-message-mode.rb +15 -5
  44. data/lib/sup/modes/inbox-mode.rb +2 -4
  45. data/lib/sup/modes/label-list-mode.rb +1 -1
  46. data/lib/sup/modes/line-cursor-mode.rb +18 -18
  47. data/lib/sup/modes/log-mode.rb +29 -16
  48. data/lib/sup/modes/poll-mode.rb +7 -9
  49. data/lib/sup/modes/reply-mode.rb +5 -3
  50. data/lib/sup/modes/scroll-mode.rb +2 -2
  51. data/lib/sup/modes/search-results-mode.rb +9 -11
  52. data/lib/sup/modes/text-mode.rb +2 -2
  53. data/lib/sup/modes/thread-index-mode.rb +26 -16
  54. data/lib/sup/modes/thread-view-mode.rb +84 -39
  55. data/lib/sup/person.rb +6 -8
  56. data/lib/sup/poll.rb +46 -47
  57. data/lib/sup/rfc2047.rb +1 -5
  58. data/lib/sup/sent.rb +27 -20
  59. data/lib/sup/source.rb +90 -13
  60. data/lib/sup/textfield.rb +4 -4
  61. data/lib/sup/thread.rb +15 -13
  62. data/lib/sup/undo.rb +0 -1
  63. data/lib/sup/update.rb +0 -1
  64. data/lib/sup/util.rb +51 -43
  65. data/lib/sup/xapian_index.rb +566 -0
  66. metadata +57 -46
  67. data/lib/sup/suicide.rb +0 -36
@@ -0,0 +1,74 @@
1
+ require 'fileutils'
2
+
3
+ module Redwood
4
+
5
+ ## wrap a nice interactive layer on top of anything that has a #lock method
6
+ ## which throws a LockError which responds to #user, #host, #mtim, #pname, and
7
+ ## #pid.
8
+
9
+ module InteractiveLock
10
+ def pluralize number_of, kind; "#{number_of} #{kind}" + (number_of == 1 ? "" : "s") end
11
+
12
+ def time_ago_in_words time
13
+ secs = (Time.now - time).to_i
14
+ mins = secs / 60
15
+ time = if mins == 0
16
+ pluralize secs, "second"
17
+ else
18
+ pluralize mins, "minute"
19
+ end
20
+ end
21
+
22
+ DELAY = 5 # seconds
23
+
24
+ def lock_interactively stream=$stderr
25
+ begin
26
+ Index.lock
27
+ rescue Index::LockError => e
28
+ stream.puts <<EOS
29
+ Error: the index is locked by another process! User '#{e.user}' on
30
+ host '#{e.host}' is running #{e.pname} with pid #{e.pid}.
31
+ The process was alive as of at least #{time_ago_in_words e.mtime} ago.
32
+
33
+ EOS
34
+ stream.print "Should I ask that process to kill itself (y/n)? "
35
+ stream.flush
36
+
37
+ success = if $stdin.gets =~ /^\s*y(es)?\s*$/i
38
+ stream.puts "Ok, trying to kill process..."
39
+
40
+ begin
41
+ Process.kill "TERM", e.pid.to_i
42
+ sleep DELAY
43
+ rescue Errno::ESRCH # no such process
44
+ stream.puts "Hm, I couldn't kill it."
45
+ end
46
+
47
+ stream.puts "Let's try that again."
48
+ begin
49
+ Index.lock
50
+ rescue Index::LockError => e
51
+ stream.puts "I couldn't lock the index. The lockfile might just be stale."
52
+ stream.print "Should I just remove it and continue? (y/n) "
53
+ stream.flush
54
+
55
+ if $stdin.gets =~ /^\s*y(es)?\s*$/i
56
+ FileUtils.rm e.path
57
+
58
+ stream.puts "Let's try that one more time."
59
+ begin
60
+ Index.lock
61
+ true
62
+ rescue Index::LockError => e
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ stream.puts "Sorry, couldn't unlock the index." unless success
69
+ success
70
+ end
71
+ end
72
+ end
73
+
74
+ end
@@ -9,22 +9,22 @@ class Keymap
9
9
 
10
10
  def self.keysym_to_keycode k
11
11
  case k
12
- when :down: Curses::KEY_DOWN
13
- when :up: Curses::KEY_UP
14
- when :left: Curses::KEY_LEFT
15
- when :right: Curses::KEY_RIGHT
16
- when :page_down: Curses::KEY_NPAGE
17
- when :page_up: Curses::KEY_PPAGE
18
- when :backspace: Curses::KEY_BACKSPACE
19
- when :home: Curses::KEY_HOME
20
- when :end: Curses::KEY_END
21
- when :ctrl_l: "\f"[0]
22
- when :ctrl_g: "\a"[0]
23
- when :tab: "\t"[0]
24
- when :enter, :return: 10 #Curses::KEY_ENTER
12
+ when :down then Curses::KEY_DOWN
13
+ when :up then Curses::KEY_UP
14
+ when :left then Curses::KEY_LEFT
15
+ when :right then Curses::KEY_RIGHT
16
+ when :page_down then Curses::KEY_NPAGE
17
+ when :page_up then Curses::KEY_PPAGE
18
+ when :backspace then Curses::KEY_BACKSPACE
19
+ when :home then Curses::KEY_HOME
20
+ when :end then Curses::KEY_END
21
+ when :ctrl_l then "\f".ord
22
+ when :ctrl_g then "\a".ord
23
+ when :tab then "\t".ord
24
+ when :enter, :return then 10 #Curses::KEY_ENTER
25
25
  else
26
26
  if k.is_a?(String) && k.length == 1
27
- k[0]
27
+ k.ord
28
28
  else
29
29
  raise ArgumentError, "unknown key name '#{k}'"
30
30
  end
@@ -33,18 +33,18 @@ class Keymap
33
33
 
34
34
  def self.keysym_to_string k
35
35
  case k
36
- when :down: "<down arrow>"
37
- when :up: "<up arrow>"
38
- when :left: "<left arrow>"
39
- when :right: "<right arrow>"
40
- when :page_down: "<page down>"
41
- when :page_up: "<page up>"
42
- when :backspace: "<backspace>"
43
- when :home: "<home>"
44
- when :end: "<end>"
45
- when :enter, :return: "<enter>"
46
- when :tab: "tab"
47
- when " ": "<space>"
36
+ when :down then "<down arrow>"
37
+ when :up then "<up arrow>"
38
+ when :left then "<left arrow>"
39
+ when :right then "<right arrow>"
40
+ when :page_down then "<page down>"
41
+ when :page_up then "<page up>"
42
+ when :backspace then "<backspace>"
43
+ when :home then "<home>"
44
+ when :end then "<end>"
45
+ when :enter, :return then "<enter>"
46
+ when :tab then "tab"
47
+ when " " then "<space>"
48
48
  else
49
49
  Curses::keyname(keysym_to_keycode(k))
50
50
  end
@@ -22,8 +22,6 @@ class LabelManager
22
22
  @new_labels = {}
23
23
  @modified = false
24
24
  labels.each { |t| @labels[t] = true }
25
-
26
- self.class.i_am_the_instance self
27
25
  end
28
26
 
29
27
  def new_label? l; @new_labels.include?(l) end
@@ -61,9 +59,9 @@ class LabelManager
61
59
  l
62
60
  end
63
61
  end
64
-
62
+
65
63
  def << t
66
- t = t.intern unless t.is_a? Symbol
64
+ raise ArgumentError, "expecting a symbol" unless t.is_a? Symbol
67
65
  unless @labels.member?(t) || RESERVED_LABELS.member?(t)
68
66
  @labels[t] = true
69
67
  @new_labels[t] = true
@@ -1,54 +1,73 @@
1
+ require "sup"
2
+ require 'stringio'
3
+ require 'thread'
4
+
1
5
  module Redwood
2
6
 
7
+ ## simple centralized logger. outputs to multiple sinks by calling << on them.
8
+ ## also keeps a record of all messages, so that adding a new sink will send all
9
+ ## previous messages to it by default.
3
10
  class Logger
4
- @@instance = nil
11
+ include Singleton
5
12
 
6
- attr_reader :buf
13
+ LEVELS = %w(debug info warn error) # in order!
7
14
 
8
- def initialize
9
- raise "only one Log can be defined" if @@instance
10
- @@instance = self
11
- @mode = LogMode.new
12
- @respawn = true
13
- @spawning = false # to prevent infinite loops!
15
+ def initialize level=nil
16
+ level ||= ENV["SUP_LOG_LEVEL"] || "info"
17
+ @level = LEVELS.index(level) or raise ArgumentError, "invalid log level #{level.inspect}: should be one of #{LEVELS * ', '}"
18
+ @mutex = Mutex.new
19
+ @buf = StringIO.new
20
+ @sinks = []
14
21
  end
15
22
 
16
- ## must be called if you want to see anything!
17
- ## once called, will respawn if killed...
18
- def make_buf
19
- return if @mode.buffer || !BufferManager.instantiated? || !@respawn || @spawning
20
- @spawning = true
21
- @mode.buffer = BufferManager.instance.spawn "log", @mode, :hidden => true, :system => true
22
- @spawning = false
23
+ def level; LEVELS[@level] end
24
+
25
+ def add_sink s, copy_current=true
26
+ @mutex.synchronize do
27
+ @sinks << s
28
+ s << @buf.string if copy_current
29
+ end
23
30
  end
24
31
 
25
- def log s
26
- # $stderr.puts s
27
- make_buf
28
- prefix = "#{Time.now}: "
29
- padding = " " * prefix.length
30
- first = true
31
- s.split(/[\r\n]/).each do |l|
32
- l = l.chomp
33
- if first
34
- first = false
35
- @mode << "#{prefix}#{l}\n"
36
- else
37
- @mode << "#{padding}#{l}\n"
32
+ def remove_sink s; @mutex.synchronize { @sinks.delete s } end
33
+ def remove_all_sinks!; @mutex.synchronize { @sinks.clear } end
34
+ def clear!; @mutex.synchronize { @buf = StringIO.new } end
35
+
36
+ LEVELS.each_with_index do |l, method_level|
37
+ define_method(l) do |s|
38
+ if method_level >= @level
39
+ send_message format_message(l, Time.now, s)
38
40
  end
39
41
  end
40
- $stderr.puts "[#{Time.now}] #{s.chomp}" unless BufferManager.instantiated? && @mode.buffer
41
42
  end
42
-
43
- def self.method_missing m, *a
44
- @@instance = Logger.new unless @@instance
45
- @@instance.send m, *a
43
+
44
+ ## send a message regardless of the current logging level
45
+ def force_message m; send_message format_message(nil, Time.now, m) end
46
+
47
+ private
48
+
49
+ ## level can be nil!
50
+ def format_message level, time, msg
51
+ prefix = case level
52
+ when "warn"; "WARNING: "
53
+ when "error"; "ERROR: "
54
+ else ""
55
+ end
56
+ "[#{time.to_s}] #{prefix}#{msg}\n"
46
57
  end
47
58
 
48
- def self.buffer
49
- @@instance.buf
59
+ ## actually distribute the message
60
+ def send_message m
61
+ @mutex.synchronize do
62
+ @sinks.each { |sink| sink << m }
63
+ @buf << m
64
+ end
50
65
  end
51
66
  end
52
67
 
68
+ ## include me to have top-level #debug, #info, etc. methods.
69
+ module LogsStuff
70
+ Logger::LEVELS.each { |l| define_method(l) { |s| Logger.instance.send(l, s) } }
53
71
  end
54
72
 
73
+ end
@@ -9,7 +9,9 @@ module Redwood
9
9
  ## pathnames on disk.
10
10
 
11
11
  class Maildir < Source
12
+ include SerializeLabelsNicely
12
13
  SCAN_INTERVAL = 30 # seconds
14
+ MYHOSTNAME = Socket.gethostname
13
15
 
14
16
  ## remind me never to use inheritance again.
15
17
  yaml_properties :uri, :cur_offset, :usual, :archived, :id, :labels, :mtimes
@@ -22,7 +24,7 @@ class Maildir < Source
22
24
  raise ArgumentError, "maildir URI must have a path component" unless uri.path
23
25
 
24
26
  @dir = uri.path
25
- @labels = (labels || []).freeze
27
+ @labels = Set.new(labels || [])
26
28
  @ids = []
27
29
  @ids_to_fns = {}
28
30
  @last_scan = nil
@@ -44,7 +46,35 @@ class Maildir < Source
44
46
 
45
47
  start = @ids.index(cur_offset || start_offset) or raise OutOfSyncSourceError, "Unknown message id #{cur_offset || start_offset}." # couldn't find the most recent email
46
48
  end
47
-
49
+
50
+ def store_message date, from_email, &block
51
+ stored = false
52
+ new_fn = new_maildir_basefn + ':2,S'
53
+ Dir.chdir(@dir) do |d|
54
+ tmp_path = File.join(@dir, 'tmp', new_fn)
55
+ new_path = File.join(@dir, 'new', new_fn)
56
+ begin
57
+ sleep 2 if File.stat(tmp_path)
58
+
59
+ File.stat(tmp_path)
60
+ rescue Errno::ENOENT #this is what we want.
61
+ begin
62
+ File.open(tmp_path, 'w') do |f|
63
+ yield f #provide a writable interface for the caller
64
+ f.fsync
65
+ end
66
+
67
+ File.link tmp_path, new_path
68
+ stored = true
69
+ ensure
70
+ File.unlink tmp_path if File.exists? tmp_path
71
+ end
72
+ end #rescue Errno...
73
+ end #Dir.chdir
74
+
75
+ stored
76
+ end
77
+
48
78
  def each_raw_message_line id
49
79
  scan_mailbox
50
80
  with_file_for(id) do |f|
@@ -86,7 +116,7 @@ class Maildir < Source
86
116
 
87
117
  initial_poll = @ids.empty?
88
118
 
89
- Redwood::log "scanning maildir #@dir..."
119
+ debug "scanning maildir #@dir..."
90
120
  begin
91
121
  @mtimes.each_key do |d|
92
122
  subdir = File.join(@dir, d)
@@ -105,7 +135,7 @@ class Maildir < Source
105
135
  @ids_to_fns[id] = fn
106
136
  end
107
137
  else
108
- Redwood::log "no poll on #{d}. mtime on indicates no new messages."
138
+ debug "no poll on #{d}. mtime on indicates no new messages."
109
139
  end
110
140
  end
111
141
  @ids = @dir_ids.values.flatten.uniq.sort!
@@ -113,7 +143,7 @@ class Maildir < Source
113
143
  raise FatalSourceError, "Problem scanning Maildir directories: #{e.message}."
114
144
  end
115
145
 
116
- Redwood::log "done scanning maildir"
146
+ debug "done scanning maildir"
117
147
  @last_scan = Time.now
118
148
  end
119
149
  synchronized :scan_mailbox
@@ -167,6 +197,11 @@ private
167
197
  sprintf("%d%07d", stat.mtime, stat.size % 10000000).to_i
168
198
  end
169
199
 
200
+ def new_maildir_basefn
201
+ Kernel::srand()
202
+ "#{Time.now.to_i.to_s}.#{$$}#{Kernel.rand(1000000)}.#{MYHOSTNAME}"
203
+ end
204
+
170
205
  def with_file_for id
171
206
  fn = @ids_to_fns[id] or raise OutOfSyncSourceError, "No such id: #{id.inspect}."
172
207
  begin
@@ -178,7 +213,7 @@ private
178
213
 
179
214
  def maildir_data msg
180
215
  fn = File.basename @ids_to_fns[msg]
181
- fn =~ %r{^([^:,]+):([12]),([DFPRST]*)$}
216
+ fn =~ %r{^([^:]+):([12]),([DFPRST]*)$}
182
217
  [($1 || fn), ($2 || "2"), ($3 || "")]
183
218
  end
184
219
 
@@ -15,7 +15,7 @@ module MBox
15
15
  Time.parse time, 0
16
16
  true
17
17
  rescue NoMethodError
18
- Redwood::log "found invalid date in potential mbox split line, not splitting: #{l.inspect}"
18
+ warn "found invalid date in potential mbox split line, not splitting: #{l.inspect}"
19
19
  false
20
20
  end
21
21
  end
@@ -1,17 +1,20 @@
1
1
  require 'rmail'
2
2
  require 'uri'
3
+ require 'set'
3
4
 
4
5
  module Redwood
5
6
  module MBox
6
7
 
7
8
  class Loader < Source
9
+ include SerializeLabelsNicely
8
10
  yaml_properties :uri, :cur_offset, :usual, :archived, :id, :labels
9
- attr_accessor :labels
11
+
12
+ attr_reader :labels
10
13
 
11
14
  ## uri_or_fp is horrific. need to refactor.
12
- def initialize uri_or_fp, start_offset=0, usual=true, archived=false, id=nil, labels=[]
15
+ def initialize uri_or_fp, start_offset=0, usual=true, archived=false, id=nil, labels=nil
13
16
  @mutex = Mutex.new
14
- @labels = ((labels || []) - LabelManager::RESERVED_LABELS).uniq.freeze
17
+ @labels = Set.new((labels || []) - LabelManager::RESERVED_LABELS)
15
18
 
16
19
  case uri_or_fp
17
20
  when String
@@ -47,7 +50,7 @@ class Loader < Source
47
50
  raise OutOfSyncSourceError, "mbox file is smaller than last recorded message offset. Messages have probably been deleted by another client."
48
51
  end
49
52
  end
50
-
53
+
51
54
  def start_offset; 0; end
52
55
  def end_offset; File.size @f; end
53
56
 
@@ -85,7 +88,7 @@ class Loader < Source
85
88
  @mutex.synchronize do
86
89
  @f.seek cur_offset
87
90
  string = ""
88
- until @f.eof? || (l = @f.gets) =~ BREAK_RE
91
+ until @f.eof? || MBox::is_break_line?(l = @f.gets)
89
92
  string << l
90
93
  end
91
94
  self.cur_offset += string.length
@@ -109,6 +112,15 @@ class Loader < Source
109
112
  ret
110
113
  end
111
114
 
115
+ def store_message date, from_email, &block
116
+ need_blank = File.exists?(@filename) && !File.zero?(@filename)
117
+ File.open(@filename, "a") do |f|
118
+ f.puts if need_blank
119
+ f.puts "From #{from_email} #{date.rfc2822}"
120
+ yield f
121
+ end
122
+ end
123
+
112
124
  ## apparently it's a million times faster to call this directly if
113
125
  ## we're just moving messages around on disk, than reading things
114
126
  ## into memory with raw_message.
@@ -159,7 +171,7 @@ class Loader < Source
159
171
  end
160
172
 
161
173
  self.cur_offset = next_offset
162
- [returned_offset, (self.labels + [:unread]).uniq]
174
+ [returned_offset, (labels + [:unread])]
163
175
  end
164
176
  end
165
177