sup 0.0.2 → 0.0.3
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/History.txt +11 -0
- data/Manifest.txt +40 -37
- data/README.txt +56 -32
- data/Rakefile +1 -1
- data/bin/sup +43 -65
- data/bin/sup-import +58 -14
- data/bin/sup-recover-sources +100 -0
- data/doc/FAQ.txt +18 -17
- data/doc/Philosophy.txt +33 -26
- data/doc/TODO +7 -0
- data/lib/sup.rb +36 -6
- data/lib/sup/buffer.rb +101 -66
- data/lib/sup/draft.rb +3 -8
- data/lib/sup/imap.rb +120 -36
- data/lib/sup/index.rb +52 -78
- data/lib/sup/label.rb +1 -4
- data/lib/sup/logger.rb +2 -1
- data/lib/sup/mbox.rb +2 -0
- data/lib/sup/mbox/loader.rb +21 -9
- data/lib/sup/mbox/ssh-file.rb +239 -0
- data/lib/sup/mbox/ssh-loader.rb +88 -0
- data/lib/sup/message.rb +70 -46
- data/lib/sup/modes/inbox-mode.rb +1 -1
- data/lib/sup/modes/label-list-mode.rb +1 -1
- data/lib/sup/modes/line-cursor-mode.rb +0 -1
- data/lib/sup/modes/scroll-mode.rb +12 -2
- data/lib/sup/modes/search-results-mode.rb +3 -4
- data/lib/sup/modes/text-mode.rb +1 -1
- data/lib/sup/modes/thread-index-mode.rb +10 -8
- data/lib/sup/modes/thread-view-mode.rb +13 -12
- data/lib/sup/person.rb +43 -40
- data/lib/sup/poll.rb +11 -9
- data/lib/sup/source.rb +44 -18
- data/lib/sup/util.rb +31 -3
- metadata +53 -40
data/lib/sup/modes/inbox-mode.rb
CHANGED
@@ -7,7 +7,7 @@ class InboxMode < ThreadIndexMode
|
|
7
7
|
## overwrite toggle_archived with archive
|
8
8
|
k.add :archive, "Archive thread (remove from inbox)", 'a'
|
9
9
|
k.add :load_more_threads, "Load #{LOAD_MORE_THREAD_NUM} more threads", 'M'
|
10
|
-
k.add :reload, "Discard threads and reload", '
|
10
|
+
k.add :reload, "Discard threads and reload", 'D'
|
11
11
|
end
|
12
12
|
|
13
13
|
def initialize
|
@@ -77,6 +77,11 @@ class ScrollMode < Mode
|
|
77
77
|
@botline = [@topline + buffer.content_height, lines].min
|
78
78
|
end
|
79
79
|
|
80
|
+
def resize *a
|
81
|
+
super *a
|
82
|
+
ensure_mode_validity
|
83
|
+
end
|
84
|
+
|
80
85
|
protected
|
81
86
|
|
82
87
|
def draw_line ln, opts={}
|
@@ -86,13 +91,18 @@ protected
|
|
86
91
|
:highlight => opts[:highlight]
|
87
92
|
when Array
|
88
93
|
xpos = 0
|
94
|
+
|
95
|
+
## speed test
|
96
|
+
# str = s.map { |color, text| text }.join
|
97
|
+
# buffer.write ln - @topline, 0, str, :color => :none, :highlight => opts[:highlight]
|
98
|
+
# return
|
99
|
+
|
89
100
|
s.each do |color, text|
|
90
|
-
raise "nil text for color '#{color}'" if text.nil?
|
101
|
+
raise "nil text for color '#{color}'" if text.nil? # good for debugging
|
91
102
|
if xpos + text.length < @leftcol
|
92
103
|
buffer.write ln - @topline, 0, "", :color => color,
|
93
104
|
:highlight => opts[:highlight]
|
94
105
|
xpos += text.length
|
95
|
-
## nothing
|
96
106
|
elsif xpos < @leftcol
|
97
107
|
## partial
|
98
108
|
buffer.write ln - @topline, 0, text[(@leftcol - xpos) .. -1],
|
@@ -5,9 +5,8 @@ class SearchResultsMode < ThreadIndexMode
|
|
5
5
|
k.add :load_more_threads, "Load #{LOAD_MORE_THREAD_NUM} more threads", 'M'
|
6
6
|
end
|
7
7
|
|
8
|
-
def initialize
|
9
|
-
|
10
|
-
@content = content.gsub(/[\(\)]/) { |x| "\\" + x }
|
8
|
+
def initialize qobj
|
9
|
+
@qobj = qobj
|
11
10
|
super
|
12
11
|
end
|
13
12
|
|
@@ -15,7 +14,7 @@ class SearchResultsMode < ThreadIndexMode
|
|
15
14
|
def is_relevant? m; super; end
|
16
15
|
|
17
16
|
def load_more_threads n=ThreadIndexMode::LOAD_MORE_THREAD_NUM
|
18
|
-
load_n_threads_background n, :
|
17
|
+
load_n_threads_background n, :qobj => @qobj,
|
19
18
|
:load_killed => true,
|
20
19
|
:load_spam => false,
|
21
20
|
:when_done =>(lambda do |num|
|
data/lib/sup/modes/text-mode.rb
CHANGED
@@ -47,8 +47,11 @@ class ThreadIndexMode < LineCursorMode
|
|
47
47
|
t = @threads[this_curpos]
|
48
48
|
|
49
49
|
## TODO: don't regen text completely
|
50
|
-
|
51
|
-
|
50
|
+
Redwood::reporting_thread do
|
51
|
+
mode = ThreadViewMode.new t, @hidden_labels
|
52
|
+
BufferManager.spawn t.subj, mode
|
53
|
+
BufferManager.draw_screen
|
54
|
+
end
|
52
55
|
end
|
53
56
|
|
54
57
|
def handle_starred_update m
|
@@ -287,9 +290,7 @@ class ThreadIndexMode < LineCursorMode
|
|
287
290
|
update
|
288
291
|
BufferManager.clear @mbid
|
289
292
|
@mbid = nil
|
290
|
-
|
291
293
|
BufferManager.draw_screen
|
292
|
-
|
293
294
|
@ts.size - orig_size
|
294
295
|
end
|
295
296
|
|
@@ -320,6 +321,7 @@ protected
|
|
320
321
|
end
|
321
322
|
|
322
323
|
def update_text_for_line l
|
324
|
+
return unless l # not sure why this happens, but it does, occasionally
|
323
325
|
@text[l] = text_for_thread @threads[l]
|
324
326
|
buffer.mark_dirty if buffer
|
325
327
|
end
|
@@ -359,11 +361,11 @@ protected
|
|
359
361
|
[base_color, sprintf("%-#{@from_width}s", from)],
|
360
362
|
[:starred_color, starred ? "*" : " "],
|
361
363
|
[:none, t.size == 1 ? " " * (@size_width + 2) : sprintf("(%#{@size_width}d)", t.size)],
|
362
|
-
[:to_me_color, dp ? " >" : (p ? '
|
363
|
-
[base_color, t.subj]
|
364
|
+
[:to_me_color, dp ? " >" : (p ? ' +' : " ")],
|
365
|
+
[base_color, t.subj + (t.subj.empty? ? "" : " ")],
|
364
366
|
] +
|
365
|
-
(t.labels - @hidden_labels).map { |label| [:label_color, "
|
366
|
-
[[:snippet_color,
|
367
|
+
(t.labels - @hidden_labels).map { |label| [:label_color, "+#{label} "] } +
|
368
|
+
[[:snippet_color, t.snippet]
|
367
369
|
]
|
368
370
|
end
|
369
371
|
|
@@ -46,8 +46,10 @@ class ThreadViewMode < LineCursorMode
|
|
46
46
|
end
|
47
47
|
@state[latest] = :open if @state[latest] == :closed
|
48
48
|
|
49
|
-
|
50
|
-
|
49
|
+
BufferManager.say "Loading message bodies..." do
|
50
|
+
regen_chunks
|
51
|
+
regen_text
|
52
|
+
end
|
51
53
|
end
|
52
54
|
|
53
55
|
def draw_line ln, opts={}
|
@@ -141,6 +143,7 @@ class ThreadViewMode < LineCursorMode
|
|
141
143
|
if m.is_draft?
|
142
144
|
mode = ResumeMode.new m
|
143
145
|
BufferManager.spawn "Edit message", mode
|
146
|
+
mode.edit
|
144
147
|
else
|
145
148
|
BufferManager.flash "Not a draft message!"
|
146
149
|
end
|
@@ -210,8 +213,6 @@ class ThreadViewMode < LineCursorMode
|
|
210
213
|
UpdateManager.relay :read, m
|
211
214
|
end
|
212
215
|
end
|
213
|
-
|
214
|
-
Redwood::log "releasing chunks and text from \"#{buffer.title}\""
|
215
216
|
@messages = @chunks = @text = nil
|
216
217
|
end
|
217
218
|
|
@@ -281,7 +282,7 @@ private
|
|
281
282
|
when :open
|
282
283
|
[[prefix_widget, widget, imp_widget,
|
283
284
|
[:message_patina_color,
|
284
|
-
"#{m.from ? m.from.mediumname : '?'} to #{m.
|
285
|
+
"#{m.from ? m.from.mediumname : '?'} to #{m.recipients.map { |l| l.shortname }.join(', ')} #{m.date.to_nice_s} (#{m.date.to_nice_distance_s})"]]]
|
285
286
|
# (m.to.empty? ? [] : [[[:message_patina_color, prefix + " To: " + m.recipients.map { |x| x.mediumname }.join(", ")]]]) +
|
286
287
|
when :closed
|
287
288
|
[[prefix_widget, widget, imp_widget,
|
@@ -291,13 +292,13 @@ private
|
|
291
292
|
labels = m.labels# - @hidden_labels
|
292
293
|
x = [[prefix_widget, widget, imp_widget, [:message_patina_color, "From: #{m.from ? m.from.longname : '?'}"]]] +
|
293
294
|
((m.to.empty? ? [] : break_into_lines(" To: ", m.to.map { |x| x.longname })) +
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
295
|
+
(m.cc.empty? ? [] : break_into_lines(" Cc: ", m.cc.map { |x| x.longname })) +
|
296
|
+
(m.bcc.empty? ? [] : break_into_lines(" Bcc: ", m.bcc.map { |x| x.longname })) +
|
297
|
+
[" Date: #{m.date.strftime DATE_FORMAT} (#{m.date.to_nice_distance_s})"] +
|
298
|
+
[" Subject: #{m.subj}"] +
|
299
|
+
[(parent ? " In reply to: #{parent.from.mediumname}'s message of #{parent.date.strftime DATE_FORMAT}" : nil)] +
|
300
|
+
[labels.empty? ? nil : " Labels: #{labels.join(', ')}"]
|
301
|
+
).flatten.compact.map { |l| [[:message_patina_color, prefix + " " + l]] }
|
301
302
|
#raise x.inspect
|
302
303
|
x
|
303
304
|
end
|
data/lib/sup/person.rb
CHANGED
@@ -1,5 +1,31 @@
|
|
1
1
|
module Redwood
|
2
2
|
|
3
|
+
class PersonManager
|
4
|
+
include Singleton
|
5
|
+
|
6
|
+
def initialize fn
|
7
|
+
@fn = fn
|
8
|
+
@names = {}
|
9
|
+
IO.readlines(fn).map { |l| l =~ /^(.*)?:\s+(\d+)\s+(.*)$/ && @names[$1] = [$2.to_i, $3] } if File.exists? fn
|
10
|
+
self.class.i_am_the_instance self
|
11
|
+
end
|
12
|
+
|
13
|
+
def name_for email; @names.member?(email) && @names[email][1]; end
|
14
|
+
def register email, name
|
15
|
+
return unless name
|
16
|
+
|
17
|
+
name = name.gsub(/^\s+|\s+$/, "").gsub(/\s+/, " ")
|
18
|
+
|
19
|
+
## all else being equal, prefer longer names, unless the prior name
|
20
|
+
## doesn't contain any capitalization
|
21
|
+
oldcount, oldname = @names[email]
|
22
|
+
@names[email] = [0, name] if oldname.nil? || oldname.length < name.length || (oldname !~ /[A-Z]/ && name =~ /[A-Z]/)
|
23
|
+
@names[email][0] = Time.now.to_i
|
24
|
+
end
|
25
|
+
|
26
|
+
def save; File.open(@fn, "w") { |f| @names.each { |email, (time, name)| f.puts "#{email}: #{time} #{name}" } }; end
|
27
|
+
end
|
28
|
+
|
3
29
|
class Person
|
4
30
|
@@email_map = {}
|
5
31
|
|
@@ -7,22 +33,14 @@ class Person
|
|
7
33
|
|
8
34
|
def initialize name, email
|
9
35
|
raise ArgumentError, "email can't be nil" unless email
|
10
|
-
@name =
|
11
|
-
if name
|
12
|
-
name.gsub(/^\s+|\s+$/, "").gsub(/\s+/, " ")
|
13
|
-
else
|
14
|
-
nil
|
15
|
-
end
|
16
36
|
@email = email.gsub(/^\s+|\s+$/, "").gsub(/\s+/, " ").downcase
|
17
|
-
|
37
|
+
PersonManager.register @email, name
|
38
|
+
@name = PersonManager.name_for @email
|
18
39
|
end
|
19
40
|
|
20
41
|
def == o; o && o.email == email; end
|
21
42
|
alias :eql? :==
|
22
|
-
|
23
|
-
def hash
|
24
|
-
[name, email].hash
|
25
|
-
end
|
43
|
+
def hash; [name, email].hash; end
|
26
44
|
|
27
45
|
def shortname
|
28
46
|
case @name
|
@@ -31,9 +49,9 @@ class Person
|
|
31
49
|
when /(\S+) \S+/
|
32
50
|
$1
|
33
51
|
when nil
|
34
|
-
@email
|
52
|
+
@email
|
35
53
|
else
|
36
|
-
@name
|
54
|
+
@name
|
37
55
|
end
|
38
56
|
end
|
39
57
|
|
@@ -45,18 +63,12 @@ class Person
|
|
45
63
|
end
|
46
64
|
end
|
47
65
|
|
48
|
-
def mediumname
|
49
|
-
if @name
|
50
|
-
name
|
51
|
-
else
|
52
|
-
@email
|
53
|
-
end
|
54
|
-
end
|
66
|
+
def mediumname; @name || @email; end
|
55
67
|
|
56
68
|
def full_address
|
57
69
|
if @name && @email
|
58
70
|
if @name =~ /"/
|
59
|
-
"#{@name.inspect} <#@email>"
|
71
|
+
"#{@name.inspect} <#@email>" # escape quotes
|
60
72
|
else
|
61
73
|
"#@name <#@email>"
|
62
74
|
end
|
@@ -65,6 +77,7 @@ class Person
|
|
65
77
|
end
|
66
78
|
end
|
67
79
|
|
80
|
+
## when sorting addresses, sort by this
|
68
81
|
def sort_by_me
|
69
82
|
case @name
|
70
83
|
when /^(\S+), \S+/
|
@@ -80,18 +93,10 @@ class Person
|
|
80
93
|
end.downcase
|
81
94
|
end
|
82
95
|
|
83
|
-
def self.for_several s
|
84
|
-
return [] if s.nil?
|
85
|
-
|
86
|
-
begin
|
87
|
-
s.split_on_commas.map { |ss| self.for ss }
|
88
|
-
rescue StandardError => e
|
89
|
-
raise "#{e.message}: for #{s.inspect}"
|
90
|
-
end
|
91
|
-
end
|
92
|
-
|
93
96
|
def self.for s
|
94
97
|
return nil if s.nil?
|
98
|
+
|
99
|
+
## try and parse an email address and name
|
95
100
|
name, email =
|
96
101
|
case s
|
97
102
|
when /["'](.*?)["'] <(.*?)>/, /([^,]+) <(.*?)>/
|
@@ -105,15 +110,13 @@ class Person
|
|
105
110
|
[nil, s]
|
106
111
|
end
|
107
112
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
Person.new name, email
|
116
|
-
end
|
113
|
+
@@email_map[email] ||= Person.new name, email
|
114
|
+
end
|
115
|
+
|
116
|
+
def self.for_several s
|
117
|
+
return [] if s.nil?
|
118
|
+
|
119
|
+
s.split_on_commas.map { |ss| self.for ss }
|
117
120
|
end
|
118
121
|
end
|
119
122
|
|
data/lib/sup/poll.rb
CHANGED
@@ -12,13 +12,6 @@ class PollManager
|
|
12
12
|
@last_poll = nil
|
13
13
|
|
14
14
|
self.class.i_am_the_instance self
|
15
|
-
|
16
|
-
Redwood::reporting_thread do
|
17
|
-
while true
|
18
|
-
sleep DELAY / 2
|
19
|
-
poll if @last_poll.nil? || (Time.now - @last_poll) >= DELAY
|
20
|
-
end
|
21
|
-
end
|
22
15
|
end
|
23
16
|
|
24
17
|
def buffer
|
@@ -38,6 +31,15 @@ class PollManager
|
|
38
31
|
[num, numi]
|
39
32
|
end
|
40
33
|
|
34
|
+
def start_thread
|
35
|
+
Redwood::reporting_thread do
|
36
|
+
while true
|
37
|
+
sleep DELAY / 2
|
38
|
+
poll if @last_poll.nil? || (Time.now - @last_poll) >= DELAY
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
41
43
|
def do_poll
|
42
44
|
return [0, 0] if @polling
|
43
45
|
@polling = true
|
@@ -54,7 +56,7 @@ class PollManager
|
|
54
56
|
num_inbox = 0
|
55
57
|
source.each do |offset, labels|
|
56
58
|
start_offset ||= offset
|
57
|
-
yield "
|
59
|
+
yield "Found message at #{offset} with labels #{labels * ', '}"
|
58
60
|
begin
|
59
61
|
m = Redwood::Message.new :source => source, :source_info => offset,
|
60
62
|
:labels => labels
|
@@ -71,7 +73,7 @@ class PollManager
|
|
71
73
|
total_num += 1
|
72
74
|
total_numi += 1 if m.labels.include? :inbox
|
73
75
|
end
|
74
|
-
rescue
|
76
|
+
rescue SourceError, MessageFormatError => e
|
75
77
|
yield "Ignoring erroneous message at #{source}##{offset}: #{e.message}"
|
76
78
|
end
|
77
79
|
|
data/lib/sup/source.rb
CHANGED
@@ -6,13 +6,10 @@ class Source
|
|
6
6
|
## dirty? described whether cur_offset has changed, which means the
|
7
7
|
## source needs to be re-saved to disk.
|
8
8
|
##
|
9
|
-
## broken? means no message can be loaded
|
10
|
-
## down
|
11
|
-
bool_reader :usual, :archived, :dirty
|
12
|
-
attr_reader :cur_offset, :broken_msg
|
13
|
-
attr_accessor :id
|
9
|
+
## broken? means no message can be loaded, e.g. IMAP server is
|
10
|
+
## down, mbox file is corrupt and needs to be rescanned.
|
14
11
|
|
15
|
-
##
|
12
|
+
## When writing a new source, you should implement:
|
16
13
|
##
|
17
14
|
## start_offset
|
18
15
|
## end_offset
|
@@ -20,11 +17,19 @@ class Source
|
|
20
17
|
## load_message(offset)
|
21
18
|
## raw_header(offset)
|
22
19
|
## raw_full_message(offset)
|
23
|
-
## next
|
20
|
+
## next (or each, if you prefer)
|
21
|
+
|
22
|
+
## you can throw SourceErrors from any of those, but we don't catch
|
23
|
+
## anything else, so make sure you catch all non-fatal errors and
|
24
|
+
## reraise them as source errors.
|
25
|
+
|
26
|
+
bool_reader :usual, :archived, :dirty
|
27
|
+
attr_reader :uri, :cur_offset, :broken_msg
|
28
|
+
attr_accessor :id
|
24
29
|
|
25
30
|
def initialize uri, initial_offset=nil, usual=true, archived=false, id=nil
|
26
31
|
@uri = uri
|
27
|
-
@cur_offset = initial_offset
|
32
|
+
@cur_offset = initial_offset
|
28
33
|
@usual = usual
|
29
34
|
@archived = archived
|
30
35
|
@id = id
|
@@ -35,28 +40,49 @@ class Source
|
|
35
40
|
def broken?; !@broken_msg.nil?; end
|
36
41
|
def to_s; @uri; end
|
37
42
|
def seek_to! o; self.cur_offset = o; end
|
38
|
-
def reset
|
43
|
+
def reset!
|
44
|
+
return if broken?
|
45
|
+
begin
|
46
|
+
seek_to! start_offset
|
47
|
+
rescue SourceError
|
48
|
+
end
|
49
|
+
end
|
39
50
|
def == o; o.to_s == to_s; end
|
40
|
-
def done?;
|
51
|
+
def done?;
|
52
|
+
return true if broken?
|
53
|
+
begin
|
54
|
+
(self.cur_offset ||= start_offset) >= end_offset
|
55
|
+
rescue SourceError => e
|
56
|
+
true
|
57
|
+
end
|
58
|
+
end
|
41
59
|
def is_source_for? s; to_s == s; end
|
42
60
|
|
43
61
|
def each
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
62
|
+
return if broken?
|
63
|
+
begin
|
64
|
+
self.cur_offset ||= start_offset
|
65
|
+
until done? || broken? # just like life!
|
66
|
+
n, labels = self.next
|
67
|
+
raise "no message" unless n
|
68
|
+
yield n, labels
|
69
|
+
end
|
70
|
+
rescue SourceError => e
|
71
|
+
self.broken_msg = e.message
|
49
72
|
end
|
50
73
|
end
|
51
74
|
|
52
75
|
protected
|
53
|
-
|
76
|
+
|
54
77
|
def cur_offset= o
|
55
78
|
@cur_offset = o
|
56
79
|
@dirty = true
|
57
80
|
end
|
58
|
-
|
59
|
-
|
81
|
+
|
82
|
+
def broken_msg= m
|
83
|
+
@broken_msg = m
|
84
|
+
# Redwood::log "#{to_s}: #{m}"
|
85
|
+
end
|
60
86
|
end
|
61
87
|
|
62
88
|
Redwood::register_yaml(Source, %w(uri cur_offset usual archived id))
|