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.
- data/CONTRIBUTORS +13 -6
- data/History.txt +19 -0
- data/ReleaseNotes +35 -0
- data/bin/sup +82 -77
- data/bin/sup-add +7 -7
- data/bin/sup-config +104 -85
- data/bin/sup-dump +4 -5
- data/bin/sup-recover-sources +9 -10
- data/bin/sup-sync +121 -100
- data/bin/sup-sync-back +18 -15
- data/bin/sup-tweak-labels +24 -21
- data/lib/sup.rb +53 -33
- data/lib/sup/account.rb +0 -2
- data/lib/sup/buffer.rb +47 -22
- data/lib/sup/colormap.rb +6 -6
- data/lib/sup/contact.rb +0 -2
- data/lib/sup/crypto.rb +34 -23
- data/lib/sup/draft.rb +6 -14
- data/lib/sup/ferret_index.rb +471 -0
- data/lib/sup/hook.rb +30 -43
- data/lib/sup/hook.rb.BACKUP.8625.rb +158 -0
- data/lib/sup/hook.rb.BACKUP.8681.rb +158 -0
- data/lib/sup/hook.rb.BASE.8625.rb +155 -0
- data/lib/sup/hook.rb.BASE.8681.rb +155 -0
- data/lib/sup/hook.rb.LOCAL.8625.rb +142 -0
- data/lib/sup/hook.rb.LOCAL.8681.rb +142 -0
- data/lib/sup/hook.rb.REMOTE.8625.rb +145 -0
- data/lib/sup/hook.rb.REMOTE.8681.rb +145 -0
- data/lib/sup/imap.rb +18 -8
- data/lib/sup/index.rb +70 -528
- data/lib/sup/interactive-lock.rb +74 -0
- data/lib/sup/keymap.rb +26 -26
- data/lib/sup/label.rb +2 -4
- data/lib/sup/logger.rb +54 -35
- data/lib/sup/maildir.rb +41 -6
- data/lib/sup/mbox.rb +1 -1
- data/lib/sup/mbox/loader.rb +18 -6
- data/lib/sup/mbox/ssh-file.rb +1 -7
- data/lib/sup/message-chunks.rb +36 -23
- data/lib/sup/message.rb +126 -46
- data/lib/sup/mode.rb +3 -2
- data/lib/sup/modes/console-mode.rb +108 -0
- data/lib/sup/modes/edit-message-mode.rb +15 -5
- data/lib/sup/modes/inbox-mode.rb +2 -4
- data/lib/sup/modes/label-list-mode.rb +1 -1
- data/lib/sup/modes/line-cursor-mode.rb +18 -18
- data/lib/sup/modes/log-mode.rb +29 -16
- data/lib/sup/modes/poll-mode.rb +7 -9
- data/lib/sup/modes/reply-mode.rb +5 -3
- data/lib/sup/modes/scroll-mode.rb +2 -2
- data/lib/sup/modes/search-results-mode.rb +9 -11
- data/lib/sup/modes/text-mode.rb +2 -2
- data/lib/sup/modes/thread-index-mode.rb +26 -16
- data/lib/sup/modes/thread-view-mode.rb +84 -39
- data/lib/sup/person.rb +6 -8
- data/lib/sup/poll.rb +46 -47
- data/lib/sup/rfc2047.rb +1 -5
- data/lib/sup/sent.rb +27 -20
- data/lib/sup/source.rb +90 -13
- data/lib/sup/textfield.rb +4 -4
- data/lib/sup/thread.rb +15 -13
- data/lib/sup/undo.rb +0 -1
- data/lib/sup/update.rb +0 -1
- data/lib/sup/util.rb +51 -43
- data/lib/sup/xapian_index.rb +566 -0
- metadata +57 -46
- 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
|
data/lib/sup/keymap.rb
CHANGED
@@ -9,22 +9,22 @@ class Keymap
|
|
9
9
|
|
10
10
|
def self.keysym_to_keycode k
|
11
11
|
case k
|
12
|
-
when :down
|
13
|
-
when :up
|
14
|
-
when :left
|
15
|
-
when :right
|
16
|
-
when :page_down
|
17
|
-
when :page_up
|
18
|
-
when :backspace
|
19
|
-
when :home
|
20
|
-
when :end
|
21
|
-
when :ctrl_l
|
22
|
-
when :ctrl_g
|
23
|
-
when :tab
|
24
|
-
when :enter, :return
|
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
|
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
|
37
|
-
when :up
|
38
|
-
when :left
|
39
|
-
when :right
|
40
|
-
when :page_down
|
41
|
-
when :page_up
|
42
|
-
when :backspace
|
43
|
-
when :home
|
44
|
-
when :end
|
45
|
-
when :enter, :return
|
46
|
-
when :tab
|
47
|
-
when " "
|
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
|
data/lib/sup/label.rb
CHANGED
@@ -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
|
-
|
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
|
data/lib/sup/logger.rb
CHANGED
@@ -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
|
-
|
11
|
+
include Singleton
|
5
12
|
|
6
|
-
|
13
|
+
LEVELS = %w(debug info warn error) # in order!
|
7
14
|
|
8
|
-
def initialize
|
9
|
-
|
10
|
-
|
11
|
-
@
|
12
|
-
@
|
13
|
-
@
|
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
|
-
|
17
|
-
|
18
|
-
def
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
-
|
44
|
-
|
45
|
-
|
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
|
-
|
49
|
-
|
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
|
data/lib/sup/maildir.rb
CHANGED
@@ -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 || [])
|
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
|
-
|
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
|
-
|
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
|
-
|
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{^([
|
216
|
+
fn =~ %r{^([^:]+):([12]),([DFPRST]*)$}
|
182
217
|
[($1 || fn), ($2 || "2"), ($3 || "")]
|
183
218
|
end
|
184
219
|
|
data/lib/sup/mbox.rb
CHANGED
@@ -15,7 +15,7 @@ module MBox
|
|
15
15
|
Time.parse time, 0
|
16
16
|
true
|
17
17
|
rescue NoMethodError
|
18
|
-
|
18
|
+
warn "found invalid date in potential mbox split line, not splitting: #{l.inspect}"
|
19
19
|
false
|
20
20
|
end
|
21
21
|
end
|
data/lib/sup/mbox/loader.rb
CHANGED
@@ -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
|
-
|
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)
|
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)
|
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, (
|
174
|
+
[returned_offset, (labels + [:unread])]
|
163
175
|
end
|
164
176
|
end
|
165
177
|
|