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.

@@ -3,7 +3,7 @@ module Redwood
3
3
  class LabelManager
4
4
  include Singleton
5
5
 
6
- ## all labels that have special meaning. user will be unable to
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
@@ -24,8 +24,9 @@ class Logger
24
24
 
25
25
  def log s
26
26
  # $stderr.puts s
27
- @mode << "#{Time.now}: #{s.chomp}\n"
28
27
  make_buf
28
+ @mode << "#{Time.now}: #{s.chomp}\n"
29
+ $stderr.puts "[#{Time.now}] #{s.chomp}" unless @mode.buffer
29
30
  end
30
31
 
31
32
  def self.method_missing m, *a
@@ -1,4 +1,6 @@
1
1
  require "sup/mbox/loader"
2
+ require "sup/mbox/ssh-file"
3
+ require "sup/mbox/ssh-loader"
2
4
 
3
5
  module Redwood
4
6
 
@@ -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
- attr_reader :labels
7
+ attr_reader_cloned :labels
9
8
 
10
- def initialize uri, start_offset=nil, usual=true, archived=false, id=nil
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 << File.basename(@filename).intern unless File.dirname(@filename) =~ /\b(var|usr|spool)\b/
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
@@ -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 index_entry is specified, will fill in values from that,
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
- if opts[:source]
92
- @source = opts[:source]
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
- if opts[:header]
105
- opts[:header]
106
- else
107
- header = @source.load_header @source_info
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 = (Time === date ? date : Time.parse(header["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
- if(@subj = header["subject"])
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"] || "").scan(/<(.*?)>/).flatten
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
- if @body
184
- [Text.new(@body.split("\n"))]
185
- else
186
- message_to_chunks @source.load_message(@source_info)
187
- end
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
- @source.raw_header @source_info
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
- @source.raw_full_message @source_info
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.longname,
201
- to.map { |p| p.longname },
202
- cc.map { |p| p.longname },
203
- bcc.map { |p| p.longname },
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.gsub(/\t/, " ").gsub(/\r/, "").split("\n")
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 = (@snippet ? @snippet + " " : "") + line.gsub(/^\s+/, "").gsub(/[\r\n]/, "").gsub(/\s+/, " ")
301
- @snippet = @snippet[0 ... SNIPPET_LEN]
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