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/label.rb
CHANGED
@@ -3,7 +3,7 @@ module Redwood
|
|
3
3
|
class LabelManager
|
4
4
|
include Singleton
|
5
5
|
|
6
|
-
##
|
6
|
+
## labels that have special semantics. user will be unable to
|
7
7
|
## add/remove these via normal label mechanisms.
|
8
8
|
RESERVED_LABELS = [ :starred, :spam, :draft, :unread, :killed, :sent ]
|
9
9
|
|
@@ -28,11 +28,8 @@ class LabelManager
|
|
28
28
|
end
|
29
29
|
|
30
30
|
def user_labels; @labels.keys; end
|
31
|
-
|
32
31
|
def << t; @labels[t] = true unless @labels.member?(t) || RESERVED_LABELS.member?(t); end
|
33
|
-
|
34
32
|
def delete t; @labels.delete t; end
|
35
|
-
|
36
33
|
def save
|
37
34
|
File.open(@fn, "w") { |f| f.puts @labels.keys }
|
38
35
|
end
|
data/lib/sup/logger.rb
CHANGED
data/lib/sup/mbox.rb
CHANGED
data/lib/sup/mbox/loader.rb
CHANGED
@@ -1,27 +1,35 @@
|
|
1
|
-
require 'thread'
|
2
1
|
require 'rmail'
|
3
2
|
|
4
3
|
module Redwood
|
5
4
|
module MBox
|
6
5
|
|
7
6
|
class Loader < Source
|
8
|
-
|
7
|
+
attr_reader_cloned :labels
|
9
8
|
|
10
|
-
def initialize
|
11
|
-
raise ArgumentError, "not an mbox uri" unless uri =~ %r!mbox://!
|
9
|
+
def initialize uri_or_fp, start_offset=nil, usual=true, archived=false, id=nil
|
12
10
|
super
|
13
11
|
|
14
12
|
@mutex = Mutex.new
|
15
|
-
@filename = uri.sub(%r!^mbox://!, "")
|
16
|
-
@f = File.open @filename
|
17
|
-
## heuristic: use the filename as a label, unless the file
|
18
|
-
## has a path that probably represents an inbox.
|
19
13
|
@labels = [:unread]
|
20
|
-
@labels <<
|
14
|
+
@labels << :inbox unless archived?
|
15
|
+
|
16
|
+
case uri_or_fp
|
17
|
+
when String
|
18
|
+
raise ArgumentError, "not an mbox uri" unless uri_or_fp =~ %r!mbox://!
|
19
|
+
|
20
|
+
fn = uri_or_fp.sub(%r!^mbox://!, "")
|
21
|
+
## heuristic: use the filename as a label, unless the file
|
22
|
+
## has a path that probably represents an inbox.
|
23
|
+
@labels << File.basename(fn).intern unless File.dirname(fn) =~ /\b(var|usr|spool)\b/
|
24
|
+
@f = File.open fn
|
25
|
+
else
|
26
|
+
@f = uri_or_fp
|
27
|
+
end
|
21
28
|
end
|
22
29
|
|
23
30
|
def start_offset; 0; end
|
24
31
|
def end_offset; File.size @f; end
|
32
|
+
def total; end_offset; end
|
25
33
|
|
26
34
|
def load_header offset
|
27
35
|
header = nil
|
@@ -38,6 +46,7 @@ class Loader < Source
|
|
38
46
|
end
|
39
47
|
|
40
48
|
def load_message offset
|
49
|
+
raise SourceError, self.broken_msg if broken?
|
41
50
|
@mutex.synchronize do
|
42
51
|
@f.seek offset
|
43
52
|
begin
|
@@ -51,6 +60,7 @@ class Loader < Source
|
|
51
60
|
end
|
52
61
|
|
53
62
|
def raw_header offset
|
63
|
+
raise SourceError, self.broken_msg if broken?
|
54
64
|
ret = ""
|
55
65
|
@mutex.synchronize do
|
56
66
|
@f.seek offset
|
@@ -62,6 +72,7 @@ class Loader < Source
|
|
62
72
|
end
|
63
73
|
|
64
74
|
def raw_full_message offset
|
75
|
+
raise SourceError, self.broken_msg if broken?
|
65
76
|
ret = ""
|
66
77
|
@mutex.synchronize do
|
67
78
|
@f.seek offset
|
@@ -74,6 +85,7 @@ class Loader < Source
|
|
74
85
|
end
|
75
86
|
|
76
87
|
def next
|
88
|
+
raise SourceError, self.broken_msg if broken?
|
77
89
|
returned_offset = nil
|
78
90
|
next_offset = cur_offset
|
79
91
|
|
@@ -0,0 +1,239 @@
|
|
1
|
+
require 'net/ssh'
|
2
|
+
|
3
|
+
module Redwood
|
4
|
+
module MBox
|
5
|
+
|
6
|
+
class SSHFileError < StandardError; end
|
7
|
+
|
8
|
+
## this is a file-like interface to a file that actually lives on the
|
9
|
+
## other end of an ssh connection. it works by using wc, head and tail
|
10
|
+
## to simulate (buffered) random access. on a fast connection, this
|
11
|
+
## can have a good bandwidth, but the latency is pretty terrible:
|
12
|
+
## about 1 second (!) per request. luckily, we're either just reading
|
13
|
+
## straight through the mbox (an import) or we're reading a few
|
14
|
+
## messages at a time (viewing messages) so the latency is not a problem.
|
15
|
+
|
16
|
+
## all of the methods here can throw SSHFileErrors, SocketErrors,
|
17
|
+
## Net::SSH::Exceptions and Errno::ENOENTs.
|
18
|
+
|
19
|
+
## debugging TODO: remove me
|
20
|
+
def debug s
|
21
|
+
Redwood::log s
|
22
|
+
end
|
23
|
+
module_function :debug
|
24
|
+
|
25
|
+
## a simple buffer of contiguous data
|
26
|
+
class Buffer
|
27
|
+
def initialize
|
28
|
+
clear!
|
29
|
+
end
|
30
|
+
|
31
|
+
def clear!
|
32
|
+
@start = nil
|
33
|
+
@buf = ""
|
34
|
+
end
|
35
|
+
|
36
|
+
def empty?; @start.nil?; end
|
37
|
+
def start; @start; end
|
38
|
+
def endd; @start + @buf.length; end
|
39
|
+
|
40
|
+
def add data, offset=endd
|
41
|
+
#MBox::debug "+ adding #{data.length} bytes; size will be #{size + data.length}; limit #{SSHFile::MAX_BUF_SIZE}"
|
42
|
+
|
43
|
+
if start.nil?
|
44
|
+
@buf = data
|
45
|
+
@start = offset
|
46
|
+
return
|
47
|
+
end
|
48
|
+
|
49
|
+
raise "non-continguous data added to buffer (data #{offset}:#{offset + data.length}, buf range #{start}:#{endd})" if offset + data.length < start || offset > endd
|
50
|
+
|
51
|
+
if offset < start
|
52
|
+
@buf = data[0 ... (start - offset)] + @buf
|
53
|
+
@start = offset
|
54
|
+
else
|
55
|
+
return if offset + data.length < endd
|
56
|
+
@buf += data[(endd - offset) .. -1]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def [](o)
|
61
|
+
raise "only ranges supported due to programmer's laziness" unless o.is_a? Range
|
62
|
+
@buf[Range.new(o.first - @start, o.last - @start, o.exclude_end?)]
|
63
|
+
end
|
64
|
+
|
65
|
+
def index what, start=0
|
66
|
+
x = @buf.index(what, start - @start)
|
67
|
+
x.nil? ? nil : x + @start
|
68
|
+
end
|
69
|
+
def rindex what, start=0
|
70
|
+
x = @buf.rindex(what, start - @start)
|
71
|
+
x.nil? ? nil : x + @start
|
72
|
+
end
|
73
|
+
|
74
|
+
def size; empty? ? 0 : @buf.size; end
|
75
|
+
def to_s; empty? ? "<empty>" : "[#{start}, #{endd})"; end # for debugging
|
76
|
+
end
|
77
|
+
|
78
|
+
## the file-like interface to a remote file
|
79
|
+
class SSHFile
|
80
|
+
MAX_BUF_SIZE = 1024 * 1024 # bytes
|
81
|
+
MAX_TRANSFER_SIZE = 1024 * 128
|
82
|
+
REASONABLE_TRANSFER_SIZE = 1024 * 32
|
83
|
+
SIZE_CHECK_INTERVAL = 60 * 1 # seconds
|
84
|
+
|
85
|
+
@@shells = {}
|
86
|
+
@@shells_mutex = Mutex.new
|
87
|
+
|
88
|
+
def initialize host, fn, ssh_opts={}
|
89
|
+
@buf = Buffer.new
|
90
|
+
@host = host
|
91
|
+
@fn = fn
|
92
|
+
@ssh_opts = ssh_opts
|
93
|
+
@file_size = nil
|
94
|
+
@offset = 0
|
95
|
+
@say_id = nil
|
96
|
+
@broken_msg = nil
|
97
|
+
end
|
98
|
+
|
99
|
+
def broken?; !@broken_msg.nil?; end
|
100
|
+
|
101
|
+
## TODO: share this code with imap
|
102
|
+
def say s
|
103
|
+
@say_id = BufferManager.say s, @say_id if BufferManager.instantiated?
|
104
|
+
Redwood::log s
|
105
|
+
end
|
106
|
+
def shutup
|
107
|
+
BufferManager.clear @say_id if BufferManager.instantiated?
|
108
|
+
@say_id = nil
|
109
|
+
end
|
110
|
+
private :say, :shutup
|
111
|
+
|
112
|
+
def connect
|
113
|
+
raise SSHFileError, @broken_msg if broken?
|
114
|
+
return if @shell
|
115
|
+
|
116
|
+
key = [@host, @ssh_opts[:username]]
|
117
|
+
@shell =
|
118
|
+
@@shells_mutex.synchronize do
|
119
|
+
if @@shells.member? key
|
120
|
+
returning(@@shells[key]) do |shell|
|
121
|
+
say "Checking for #@fn..."
|
122
|
+
begin
|
123
|
+
raise Errno::ENOENT, @fn unless shell.test("-e #@fn").status == 0
|
124
|
+
ensure
|
125
|
+
shutup
|
126
|
+
end
|
127
|
+
end
|
128
|
+
else
|
129
|
+
say "Opening SSH connection to #{@host}..."
|
130
|
+
begin
|
131
|
+
#raise SSHFileError, "simulated SSH file error"
|
132
|
+
session = Net::SSH.start @host, @ssh_opts
|
133
|
+
say "Starting SSH shell..."
|
134
|
+
shell = session.shell.sync
|
135
|
+
say "Checking for #@fn..."
|
136
|
+
raise Errno::ENOENT, @fn unless shell.test("-e #@fn").status == 0
|
137
|
+
@@shells[key] = shell
|
138
|
+
ensure
|
139
|
+
shutup
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def eof?; @offset >= size; end
|
146
|
+
def eof; eof?; end # lame but IO's method is named this and rmail calls that
|
147
|
+
def seek loc; @offset = loc; end
|
148
|
+
def tell; @offset; end
|
149
|
+
def total; size; end
|
150
|
+
|
151
|
+
def size
|
152
|
+
if @file_size.nil? || (Time.now - @last_size_check) > SIZE_CHECK_INTERVAL
|
153
|
+
@last_size_check = Time.now
|
154
|
+
@file_size = do_remote("wc -c #@fn").split.first.to_i
|
155
|
+
end
|
156
|
+
@file_size
|
157
|
+
end
|
158
|
+
|
159
|
+
def gets
|
160
|
+
return nil if eof?
|
161
|
+
make_buf_include @offset
|
162
|
+
expand_buf_forward while @buf.index("\n", @offset).nil? && @buf.endd < size
|
163
|
+
returning(@buf[@offset .. (@buf.index("\n", @offset) || -1)]) { |line| @offset += line.length }
|
164
|
+
end
|
165
|
+
|
166
|
+
def read n
|
167
|
+
return nil if eof?
|
168
|
+
make_buf_include @offset, n
|
169
|
+
@buf[@offset ... (@offset += n)]
|
170
|
+
end
|
171
|
+
|
172
|
+
private
|
173
|
+
|
174
|
+
def do_remote cmd, expected_size=0
|
175
|
+
begin
|
176
|
+
retries = 0
|
177
|
+
connect
|
178
|
+
# MBox::debug "sending command: #{cmd.inspect}"
|
179
|
+
begin
|
180
|
+
result = @shell.send_command cmd
|
181
|
+
raise SSHFileError, "Failure during remote command #{cmd.inspect}: #{result.stderr[0 .. 100]}" unless result.status == 0
|
182
|
+
rescue Net::SSH::Exception # these happen occasionally for no apparent reason. gotta love that nondeterminism!
|
183
|
+
retry if (retries += 1) < 3
|
184
|
+
raise
|
185
|
+
end
|
186
|
+
rescue Net::SSH::Exception, SSHFileError, Errno::ENOENT => e
|
187
|
+
@broken_msg = e.message
|
188
|
+
raise
|
189
|
+
end
|
190
|
+
result.stdout
|
191
|
+
end
|
192
|
+
|
193
|
+
def get_bytes offset, size
|
194
|
+
do_remote "tail -c +#{offset + 1} #@fn | head -c #{size}", size
|
195
|
+
end
|
196
|
+
|
197
|
+
def expand_buf_forward n=REASONABLE_TRANSFER_SIZE
|
198
|
+
@buf.add get_bytes(@buf.endd, n)
|
199
|
+
end
|
200
|
+
|
201
|
+
## try our best to transfer somewhere between
|
202
|
+
## REASONABLE_TRANSFER_SIZE and MAX_TRANSFER_SIZE bytes
|
203
|
+
def make_buf_include offset, size=0
|
204
|
+
good_size = [size, REASONABLE_TRANSFER_SIZE].max
|
205
|
+
|
206
|
+
trans_start, trans_size =
|
207
|
+
if @buf.empty?
|
208
|
+
[offset, good_size]
|
209
|
+
elsif offset < @buf.start
|
210
|
+
if @buf.start - offset <= good_size
|
211
|
+
start = [@buf.start - good_size, 0].max
|
212
|
+
[start, @buf.start - start]
|
213
|
+
elsif @buf.start - offset < MAX_TRANSFER_SIZE
|
214
|
+
[offset, @buf.start - offset]
|
215
|
+
else
|
216
|
+
MBox::debug "clearing SSH buffer because buf.start #{@buf.start} - offset #{offset} >= #{MAX_TRANSFER_SIZE}"
|
217
|
+
@buf.clear!
|
218
|
+
[offset, good_size]
|
219
|
+
end
|
220
|
+
else
|
221
|
+
return if [offset + size, self.size].min <= @buf.endd # whoohoo!
|
222
|
+
if offset - @buf.endd <= good_size
|
223
|
+
[@buf.endd, good_size]
|
224
|
+
elsif offset - @buf.endd < MAX_TRANSFER_SIZE
|
225
|
+
[@buf.endd, offset - @buf.endd]
|
226
|
+
else
|
227
|
+
MBox::debug "clearing SSH buffer because offset #{offset} - buf.end #{@buf.endd} >= #{MAX_TRANSFER_SIZE}"
|
228
|
+
@buf.clear!
|
229
|
+
[offset, good_size]
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
@buf.clear! if @buf.size > MAX_BUF_SIZE
|
234
|
+
@buf.add get_bytes(trans_start, trans_size), trans_start
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
end
|
239
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'net/ssh'
|
2
|
+
|
3
|
+
module Redwood
|
4
|
+
module MBox
|
5
|
+
|
6
|
+
## this is slightly complicated because SSHFile (and thus @f or
|
7
|
+
## @loader) can throw a variety of exceptions, and we need to catch
|
8
|
+
## those, reraise them as SourceErrors, and set ourselves as broken.
|
9
|
+
|
10
|
+
class SSHLoader < Source
|
11
|
+
attr_reader_cloned :labels
|
12
|
+
attr_accessor :username, :password
|
13
|
+
|
14
|
+
def initialize uri, username=nil, password=nil, start_offset=nil, usual=true, archived=false, id=nil
|
15
|
+
raise ArgumentError, "not an mbox+ssh uri: #{uri.inspect}" unless uri =~ %r!^mbox\+ssh://!
|
16
|
+
|
17
|
+
super uri, start_offset, usual, archived, id
|
18
|
+
|
19
|
+
@parsed_uri = URI(uri)
|
20
|
+
@username = username
|
21
|
+
@password = password
|
22
|
+
@uri = uri
|
23
|
+
@cur_offset = start_offset
|
24
|
+
|
25
|
+
opts = {}
|
26
|
+
opts[:username] = @username if @username
|
27
|
+
opts[:password] = @password if @password
|
28
|
+
|
29
|
+
@f = SSHFile.new host, filename, opts
|
30
|
+
@loader = Loader.new @f, start_offset, usual, archived, id
|
31
|
+
|
32
|
+
## heuristic: use the filename as a label, unless the file
|
33
|
+
## has a path that probably represents an inbox.
|
34
|
+
@labels = [:unread]
|
35
|
+
@labels << :inbox unless archived?
|
36
|
+
@labels << File.basename(filename).intern unless File.dirname(filename) =~ /\b(var|usr|spool)\b/
|
37
|
+
end
|
38
|
+
|
39
|
+
def host; @parsed_uri.host; end
|
40
|
+
def filename; @parsed_uri.path[1..-1] end
|
41
|
+
|
42
|
+
def next
|
43
|
+
return if broken?
|
44
|
+
begin
|
45
|
+
offset, labels = @loader.next
|
46
|
+
self.cur_offset = @loader.cur_offset # superclass keeps @cur_offset which is used by yaml
|
47
|
+
[offset, (labels + @labels).uniq] # add our labels
|
48
|
+
rescue Net::SSH::Exception, SocketError, SSHFileError, Errno::ENOENT => e
|
49
|
+
recover_from e
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def end_offset
|
54
|
+
begin
|
55
|
+
@f.size
|
56
|
+
rescue Net::SSH::Exception, SocketError, SSHFileError, Errno::ENOENT => e
|
57
|
+
recover_from e
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def cur_offset= o; @cur_offset = @loader.cur_offset = o; @dirty = true; end
|
62
|
+
def id; @loader.id; end
|
63
|
+
def id= o; @id = @loader.id = o; end
|
64
|
+
# def cur_offset; @loader.cur_offset; end # think we'll be ok without this
|
65
|
+
def to_s; @parsed_uri.to_s; end
|
66
|
+
|
67
|
+
def recover_from e
|
68
|
+
m = "error communicating with SSH server #{host} (#{e.class.name}): #{e.message}"
|
69
|
+
Redwood::log m
|
70
|
+
self.broken_msg = @loader.broken_msg = m
|
71
|
+
raise SourceError, m
|
72
|
+
end
|
73
|
+
|
74
|
+
[:start_offset, :load_header, :load_message, :raw_header, :raw_full_message].each do |meth|
|
75
|
+
define_method meth do |*a|
|
76
|
+
begin
|
77
|
+
@loader.send meth, *a
|
78
|
+
rescue Net::SSH::Exception, SocketError, SSHFileError, Errno::ENOENT => e
|
79
|
+
recover_from e
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
Redwood::register_yaml(SSHLoader, %w(uri username password cur_offset usual archived id))
|
86
|
+
|
87
|
+
end
|
88
|
+
end
|
data/lib/sup/message.rb
CHANGED
@@ -54,7 +54,7 @@ class Message
|
|
54
54
|
attr_reader :lines
|
55
55
|
def initialize lines
|
56
56
|
## do some wrapping
|
57
|
-
@lines = lines.map { |l| l.wrap 80 }.flatten
|
57
|
+
@lines = lines.map { |l| l.chomp.wrap 80 }.flatten
|
58
58
|
end
|
59
59
|
end
|
60
60
|
|
@@ -86,28 +86,20 @@ class Message
|
|
86
86
|
|
87
87
|
bool_reader :dirty
|
88
88
|
|
89
|
-
## if
|
89
|
+
## if you specify a :header, will use values from that. otherwise, will try and
|
90
|
+
## load the header from the source.
|
90
91
|
def initialize opts
|
91
|
-
|
92
|
-
|
93
|
-
@source_info = opts[:source_info] or raise ArgumentError, ":source but no :source_info"
|
94
|
-
@body = nil
|
95
|
-
else
|
96
|
-
@source = @source_info = nil
|
97
|
-
@body = opts[:body] or raise ArgumentError, "one of :body or :source must be specified"
|
98
|
-
end
|
92
|
+
@source = opts[:source] or raise ArgumentError, "source can't be nil"
|
93
|
+
@source_info = opts[:source_info] or raise ArgumentError, "source_info can't be nil"
|
99
94
|
@snippet = opts[:snippet] || ""
|
100
95
|
@labels = opts[:labels] || []
|
101
96
|
@dirty = false
|
102
97
|
|
103
|
-
header
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
header.each { |k, v| header[k.downcase] = v }
|
109
|
-
header
|
110
|
-
end
|
98
|
+
read_header(opts[:header] || @source.load_header(@source_info))
|
99
|
+
end
|
100
|
+
|
101
|
+
def read_header header
|
102
|
+
header.each { |k, v| header[k.downcase] = v }
|
111
103
|
|
112
104
|
%w(message-id date).each do |f|
|
113
105
|
raise MessageFormatError, "no #{f} field in header #{header.inspect} (source #@source offset #@source_info)" unless header.include? f
|
@@ -116,22 +108,18 @@ class Message
|
|
116
108
|
|
117
109
|
begin
|
118
110
|
date = header["date"]
|
119
|
-
@date =
|
111
|
+
@date = Time === date ? date : Time.parse(header["date"])
|
120
112
|
rescue ArgumentError => e
|
121
113
|
raise MessageFormatError, "unparsable date #{header['date']}: #{e.message}"
|
122
114
|
end
|
123
115
|
|
124
|
-
|
125
|
-
@subj = @subj.gsub(/\s+/, " ").gsub(/\s+$/, "")
|
126
|
-
else
|
127
|
-
@subj = DEFAULT_SUBJECT
|
128
|
-
end
|
116
|
+
@subj = header.member?("subject") ? header["subject"].gsub(/\s+/, " ").gsub(/\s+$/, "") : DEFAULT_SUBJECT
|
129
117
|
@from = Person.for header["from"]
|
130
118
|
@to = Person.for_several header["to"]
|
131
119
|
@cc = Person.for_several header["cc"]
|
132
120
|
@bcc = Person.for_several header["bcc"]
|
133
121
|
@id = header["message-id"]
|
134
|
-
@refs = (header["references"] || "").
|
122
|
+
@refs = (header["references"] || "").gsub(/[<>]/, "").split(/\s+/).flatten
|
135
123
|
@replytos = (header["in-reply-to"] || "").scan(/<(.*?)>/).flatten
|
136
124
|
@replyto = Person.for header["reply-to"]
|
137
125
|
@list_address =
|
@@ -144,7 +132,9 @@ class Message
|
|
144
132
|
@recipient_email = header["delivered-to"]
|
145
133
|
@status = header["status"]
|
146
134
|
end
|
135
|
+
private :read_header
|
147
136
|
|
137
|
+
def broken?; @source.broken?; end
|
148
138
|
def snippet; @snippet || to_chunks && @snippet; end
|
149
139
|
def is_list_message?; !@list_address.nil?; end
|
150
140
|
def is_draft?; DraftLoader === @source; end
|
@@ -154,6 +144,7 @@ class Message
|
|
154
144
|
end
|
155
145
|
|
156
146
|
def save index
|
147
|
+
return if broken?
|
157
148
|
index.update_message self if @dirty
|
158
149
|
@dirty = false
|
159
150
|
end
|
@@ -179,30 +170,59 @@ class Message
|
|
179
170
|
@dirty = true
|
180
171
|
end
|
181
172
|
|
173
|
+
## this is called when the message body needs to actually be loaded.
|
182
174
|
def to_chunks
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
175
|
+
@chunks ||=
|
176
|
+
if @source.broken?
|
177
|
+
[Text.new(error_message(@source.broken_msg.split("\n")))]
|
178
|
+
else
|
179
|
+
begin
|
180
|
+
read_header @source.load_header(@source_info)
|
181
|
+
message_to_chunks @source.load_message(@source_info)
|
182
|
+
rescue SourceError, SocketError => e
|
183
|
+
[Text.new(error_message(e.message))]
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def error_message msg
|
189
|
+
<<EOS
|
190
|
+
#@snippet...
|
191
|
+
|
192
|
+
***********************************************************************
|
193
|
+
* An error occurred while loading this message. It is possible that *
|
194
|
+
* the source has changed, or (in the case of remote sources) is down. *
|
195
|
+
***********************************************************************
|
196
|
+
|
197
|
+
The error message was:
|
198
|
+
#{msg}
|
199
|
+
EOS
|
188
200
|
end
|
189
201
|
|
190
202
|
def raw_header
|
191
|
-
|
203
|
+
begin
|
204
|
+
@source.raw_header @source_info
|
205
|
+
rescue SourceError => e
|
206
|
+
error_message e.message
|
207
|
+
end
|
192
208
|
end
|
193
209
|
|
194
210
|
def raw_full_message
|
195
|
-
|
211
|
+
begin
|
212
|
+
@source.raw_full_message @source_info
|
213
|
+
rescue SourceError => e
|
214
|
+
error_message(e.message)
|
215
|
+
end
|
196
216
|
end
|
197
217
|
|
198
218
|
def content
|
199
219
|
[
|
200
|
-
from && from.
|
201
|
-
to.map { |p| p.
|
202
|
-
cc.map { |p| p.
|
203
|
-
bcc.map { |p| p.
|
220
|
+
from && "#{from.name} #{from.email}",
|
221
|
+
to.map { |p| "#{p.name} #{p.email}" },
|
222
|
+
cc.map { |p| "#{p.name} #{p.email}" },
|
223
|
+
bcc.map { |p| "#{p.name} #{p.email}" },
|
204
224
|
to_chunks.select { |c| c.is_a? Text }.map { |c| c.lines },
|
205
|
-
subj,
|
225
|
+
Message.normalize_subj(subj),
|
206
226
|
].flatten.compact.join " "
|
207
227
|
end
|
208
228
|
|
@@ -226,10 +246,10 @@ private
|
|
226
246
|
ret = [] <<
|
227
247
|
case m.header.content_type
|
228
248
|
when "text/plain", nil
|
229
|
-
raise MessageFormatError, "no message body before decode" unless
|
249
|
+
raise MessageFormatError, "no message body before decode (source #@source info #@source_info)" unless
|
230
250
|
m.body
|
231
251
|
body = m.decode or raise MessageFormatError, "no message body"
|
232
|
-
text_to_chunks body.
|
252
|
+
text_to_chunks body.normalize_whitespace.split("\n")
|
233
253
|
when /^multipart\//
|
234
254
|
nil
|
235
255
|
else
|
@@ -244,7 +264,6 @@ private
|
|
244
264
|
## parse the lines of text into chunk objects. the heuristics here
|
245
265
|
## need tweaking in some nice manner. TODO: move these heuristics
|
246
266
|
## into the classes themselves.
|
247
|
-
|
248
267
|
def text_to_chunks lines
|
249
268
|
state = :text # one of :text, :quote, or :sig
|
250
269
|
chunks = []
|
@@ -252,9 +271,11 @@ private
|
|
252
271
|
|
253
272
|
lines.each_with_index do |line, i|
|
254
273
|
nextline = lines[(i + 1) ... lines.length].find { |l| l !~ /^\s*$/ } # skip blank lines
|
274
|
+
|
255
275
|
case state
|
256
276
|
when :text
|
257
277
|
newstate = nil
|
278
|
+
|
258
279
|
if line =~ QUOTE_PATTERN || (line =~ QUOTE_START_PATTERN && (nextline =~ QUOTE_PATTERN || nextline =~ QUOTE_START_PATTERN))
|
259
280
|
newstate = :quote
|
260
281
|
elsif line =~ SIG_PATTERN && (lines.length - i) < MAX_SIG_DISTANCE
|
@@ -262,6 +283,7 @@ private
|
|
262
283
|
elsif line =~ BLOCK_QUOTE_PATTERN
|
263
284
|
newstate = :block_quote
|
264
285
|
end
|
286
|
+
|
265
287
|
if newstate
|
266
288
|
chunks << Text.new(chunk_lines) unless chunk_lines.empty?
|
267
289
|
chunk_lines = [line]
|
@@ -269,8 +291,10 @@ private
|
|
269
291
|
else
|
270
292
|
chunk_lines << line
|
271
293
|
end
|
294
|
+
|
272
295
|
when :quote
|
273
296
|
newstate = nil
|
297
|
+
|
274
298
|
if line =~ QUOTE_PATTERN || line =~ QUOTE_START_PATTERN || line =~ /^\s*$/
|
275
299
|
chunk_lines << line
|
276
300
|
elsif line =~ SIG_PATTERN && (lines.length - i) < MAX_SIG_DISTANCE
|
@@ -278,6 +302,7 @@ private
|
|
278
302
|
else
|
279
303
|
newstate = :text
|
280
304
|
end
|
305
|
+
|
281
306
|
if newstate
|
282
307
|
if chunk_lines.empty?
|
283
308
|
# nothing
|
@@ -289,21 +314,20 @@ private
|
|
289
314
|
chunk_lines = [line]
|
290
315
|
state = newstate
|
291
316
|
end
|
317
|
+
|
292
318
|
when :block_quote
|
293
319
|
chunk_lines << line
|
320
|
+
|
294
321
|
when :sig
|
295
322
|
chunk_lines << line
|
296
323
|
end
|
297
324
|
|
298
325
|
if state == :text && (@snippet.nil? || @snippet.length < SNIPPET_LEN) &&
|
299
326
|
line !~ /[=\*#_-]{3,}/ && line !~ /^\s*$/
|
300
|
-
@snippet
|
301
|
-
@snippet
|
327
|
+
@snippet += " " unless @snippet.empty?
|
328
|
+
@snippet += line.gsub(/^\s+/, "").gsub(/[\r\n]/, "").gsub(/\s+/, " ")
|
329
|
+
@snippet = @snippet[0 ... SNIPPET_LEN].chomp
|
302
330
|
end
|
303
|
-
# if @snippet.nil? && state == :text && (line.length > 40 ||
|
304
|
-
# line =~ /\S+.*[^,!:]\s*$/)
|
305
|
-
# @snippet = line.gsub(/^\s+/, "").gsub(/[\r\n]/, "")[0 .. 80]
|
306
|
-
# end
|
307
331
|
end
|
308
332
|
|
309
333
|
## final object
|