sup 0.0.7 → 0.0.8

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.

@@ -30,6 +30,7 @@ class Index
30
30
  end
31
31
 
32
32
  def save
33
+ Redwood::log "saving index and sources..."
33
34
  FileUtils.mkdir_p @dir unless File.exists? @dir
34
35
  save_sources
35
36
  save_index
@@ -72,25 +73,46 @@ class Index
72
73
  end
73
74
  end
74
75
 
75
- ## Update the message state on disk, by deleting and re-adding it.
76
- ## The message must exist in the index. docid and entry are found
77
- ## unless given.
76
+ ## Syncs the message to the index: deleting if it's already there,
77
+ ## and adding either way. Index state will be determined by m.labels.
78
78
  ##
79
- ## Overwrites the labels on disk with the new labels in 'm', so that
80
- ## we can actually change message state.
81
- def update_message m, docid=nil, entry=nil
82
- unless docid && entry
83
- docid, entry = load_entry_for_id m.id
84
- raise ArgumentError, "cannot find #{m.id} in the index" unless entry
85
- end
79
+ ## docid and entry can be specified if they're already known.
80
+ def sync_message m, docid=nil, entry=nil
81
+ docid, entry = load_entry_for_id m.id unless docid && entry
86
82
 
87
- raise "no entry and no source info for message #{m.id}" unless m.source && m.source_info
83
+ raise "no source info for message #{m.id}" unless m.source && m.source_info
84
+ raise "trying deleting non-corresponding entry #{docid}" if docid && @index[docid][:message_id] != m.id
88
85
 
89
- raise "deleting non-corresponding entry #{docid}" unless @index[docid][:message_id] == m.id
86
+ source_id =
87
+ if m.source.is_a? Integer
88
+ raise "Debugging: integer source set"
89
+ m.source
90
+ else
91
+ m.source.id or raise "unregistered source #{m.source} (id #{m.source.id.inspect})"
92
+ end
90
93
 
91
- @index.delete docid
92
- add_message m
94
+ to = (m.to + m.cc + m.bcc).map { |x| x.email }.join(" ")
95
+ d = {
96
+ :message_id => m.id,
97
+ :source_id => source_id,
98
+ :source_info => m.source_info,
99
+ :date => m.date.to_indexable_s,
100
+ :body => m.content,
101
+ :snippet => m.snippet,
102
+ :label => m.labels.join(" "),
103
+ :from => m.from ? m.from.email : "",
104
+ :to => (m.to + m.cc + m.bcc).map { |x| x.email }.join(" "),
105
+ :subject => wrap_subj(Message.normalize_subj(m.subj)),
106
+ :refs => (m.refs + m.replytos).uniq.join(" "),
107
+ }
108
+
109
+ @index.delete docid if docid
110
+ @index.add_document d
111
+
93
112
  docid, entry = load_entry_for_id m.id
113
+ ## this hasn't been triggered in a long time. TODO: decide whether it's still a problem.
114
+ raise "just added message #{m.id} but couldn't find it in a search" unless docid
115
+ true
94
116
  end
95
117
 
96
118
  def save_index fn=File.join(@dir, "ferret")
@@ -210,41 +232,6 @@ class Index
210
232
  def wrap_subj subj; "__START_SUBJECT__ #{subj} __END_SUBJECT__"; end
211
233
  def unwrap_subj subj; subj =~ /__START_SUBJECT__ (.*?) __END_SUBJECT__/ && $1; end
212
234
 
213
- ## Adds a message to the index. The message cannot already exist in
214
- ## the index.
215
- def add_message m
216
- raise ArgumentError, "index already contains #{m.id}" if contains? m
217
-
218
- source_id =
219
- if m.source.is_a? Integer
220
- m.source
221
- else
222
- m.source.id or raise "unregistered source #{m.source} (id #{m.source.id.inspect})"
223
- end
224
-
225
- to = (m.to + m.cc + m.bcc).map { |x| x.email }.join(" ")
226
- d = {
227
- :message_id => m.id,
228
- :source_id => source_id,
229
- :source_info => m.source_info,
230
- :date => m.date.to_indexable_s,
231
- :body => m.content,
232
- :snippet => m.snippet,
233
- :label => m.labels.join(" "),
234
- :from => m.from ? m.from.email : "",
235
- :to => (m.to + m.cc + m.bcc).map { |x| x.email }.join(" "),
236
- :subject => wrap_subj(Message.normalize_subj(m.subj)),
237
- :refs => (m.refs + m.replytos).uniq.join(" "),
238
- }
239
-
240
- @index.add_document d
241
-
242
- docid, entry = load_entry_for_id m.id
243
- ## this hasn't been triggered in a long time. TODO: decide whether it's still a problem.
244
- raise "just added message #{m.id} but couldn't find it in a search" unless docid
245
- true
246
- end
247
-
248
235
  def drop_entry docno; @index.delete docno; end
249
236
 
250
237
  def load_entry_for_id mid
@@ -285,6 +272,12 @@ class Index
285
272
  contacts.keys.compact
286
273
  end
287
274
 
275
+ def load_sources fn=Redwood::SOURCE_FN
276
+ source_array = (Redwood::load_yaml_obj(fn) || []).map { |o| Recoverable.new o }
277
+ @sources = Hash[*(source_array).map { |s| [s.id, s] }.flatten]
278
+ @sources_dirty = false
279
+ end
280
+
288
281
  protected
289
282
 
290
283
  def parse_user_query_string str; @qparser.parse str; end
@@ -308,11 +301,6 @@ protected
308
301
  query
309
302
  end
310
303
 
311
- def load_sources fn=Redwood::SOURCE_FN
312
- @sources = Hash[*(Redwood::load_yaml_obj(fn) || []).map { |s| [s.id, s] }.flatten]
313
- @sources_dirty = false
314
- end
315
-
316
304
  def save_sources fn=Redwood::SOURCE_FN
317
305
  if @sources_dirty || @sources.any? { |id, s| s.dirty? }
318
306
  bakfn = fn + ".bak"
@@ -26,7 +26,7 @@ class Logger
26
26
  # $stderr.puts s
27
27
  make_buf
28
28
  @mode << "#{Time.now}: #{s.chomp}\n"
29
- $stderr.puts "[#{Time.now}] #{s.chomp}" unless @mode.buffer
29
+ $stderr.puts "[#{Time.now}] #{s.chomp}" unless BufferManager.instantiated? && @mode.buffer
30
30
  end
31
31
 
32
32
  def self.method_missing m, *a
@@ -0,0 +1,127 @@
1
+ require 'rmail'
2
+ require 'uri'
3
+
4
+ module Redwood
5
+
6
+ ## Maildir doesn't provide an ordered unique id, which is what Sup
7
+ ## requires to be really useful. So we must maintain, in memory, a
8
+ ## mapping between Sup "ids" (timestamps, essentially) and the
9
+ ## pathnames on disk.
10
+
11
+ class Maildir < Source
12
+ SCAN_INTERVAL = 30 # seconds
13
+
14
+ def initialize uri, last_date=nil, usual=true, archived=false, id=nil
15
+ super
16
+ uri = URI(uri)
17
+
18
+ raise ArgumentError, "not a maildir URI" unless uri.scheme == "maildir"
19
+ raise ArgumentError, "maildir URI cannot have a host: #{uri.host}" if uri.host
20
+
21
+ @dir = uri.path
22
+ @ids = []
23
+ @ids_to_fns = {}
24
+ @last_scan = nil
25
+ @mutex = Mutex.new
26
+ end
27
+
28
+ def check
29
+ scan_mailbox
30
+ start = @ids.index(cur_offset || start_offset) or raise OutOfSyncSourceError, "Unknown message id #{cur_offset || start_offset}." # couldn't find the most recent email
31
+ end
32
+
33
+ def load_header id
34
+ scan_mailbox
35
+ with_file_for(id) { |f| MBox::read_header f }
36
+ end
37
+
38
+ def load_message id
39
+ scan_mailbox
40
+ with_file_for(id) { |f| RMail::Parser.read f }
41
+ end
42
+
43
+ def raw_header id
44
+ scan_mailbox
45
+ ret = ""
46
+ with_file_for(id) do |f|
47
+ until f.eof? || (l = f.gets) =~ /^$/
48
+ ret += l
49
+ end
50
+ end
51
+ ret
52
+ end
53
+
54
+ def raw_full_message id
55
+ scan_mailbox
56
+ with_file_for(id) { |f| f.readlines.join }
57
+ end
58
+
59
+ def scan_mailbox
60
+ return if @last_scan && (Time.now - @last_scan) < SCAN_INTERVAL
61
+
62
+ cdir = File.join(@dir, 'cur')
63
+ ndir = File.join(@dir, 'new')
64
+
65
+ raise FatalSourceError, "#{cdir} not a directory" unless File.directory? cdir
66
+ raise FatalSourceError, "#{ndir} not a directory" unless File.directory? ndir
67
+
68
+ begin
69
+ @ids, @ids_to_fns = @mutex.synchronize do
70
+ ids, ids_to_fns = [], {}
71
+ (Dir[File.join(cdir, "*")] + Dir[File.join(ndir, "*")]).map do |fn|
72
+ id = make_id fn
73
+ ids << id
74
+ ids_to_fns[id] = fn
75
+ end
76
+ [ids.sort, ids_to_fns]
77
+ end
78
+ rescue SystemCallError, IOError => e
79
+ raise FatalSourceError, "Problem scanning Maildir directories: #{e.message}."
80
+ end
81
+
82
+ @last_scan = Time.now
83
+ end
84
+
85
+ def each
86
+ scan_mailbox
87
+ start = @ids.index(cur_offset || start_offset) or raise OutOfSyncSourceError, "Unknown message id #{cur_offset || start_offset}." # couldn't find the most recent email
88
+
89
+ start.upto(@ids.length - 1) do |i|
90
+ id = @ids[i]
91
+ self.cur_offset = id
92
+ yield id, (@ids_to_fns[id] =~ /,.*R.*$/ ? [] : [:unread])
93
+ end
94
+ end
95
+
96
+ def start_offset
97
+ scan_mailbox
98
+ @ids.first
99
+ end
100
+
101
+ def end_offset
102
+ scan_mailbox
103
+ @ids.last
104
+ end
105
+
106
+ def pct_done; 100.0 * (@ids.index(cur_offset) || 0).to_f / (@ids.length - 1).to_f; end
107
+
108
+ private
109
+
110
+ def make_id fn
111
+ # use 7 digits for the size. why 7? seems nice.
112
+ sprintf("%d%07d", File.mtime(fn), File.size(fn)).to_i
113
+ end
114
+
115
+ def with_file_for id
116
+ fn = @ids_to_fns[id] or raise OutOfSyncSourceError, "No such id: #{id.inspect}."
117
+ begin
118
+ File.open(fn) { |f| yield f }
119
+ rescue SystemCallError, IOError => e
120
+ raise FatalSourceError, "Problem reading file for id #{id.inspect}: #{fn.inspect}: #{e.message}."
121
+ end
122
+ end
123
+ end
124
+
125
+ Redwood::register_yaml(Maildir, %w(uri cur_offset usual archived id))
126
+
127
+ end
@@ -1,32 +1,36 @@
1
1
  require 'rmail'
2
+ require 'uri'
2
3
 
3
4
  module Redwood
4
5
  module MBox
5
6
 
6
7
  class Loader < Source
7
- attr_reader_cloned :labels
8
-
9
8
  def initialize uri_or_fp, start_offset=nil, usual=true, archived=false, id=nil
10
9
  super
11
10
 
12
11
  @mutex = Mutex.new
13
12
  @labels = [:unread]
14
- @labels << :inbox unless archived?
15
13
 
16
14
  case uri_or_fp
17
15
  when String
18
- raise ArgumentError, "not an mbox uri" unless uri_or_fp =~ %r!mbox://!
19
-
20
- fn = uri_or_fp.sub(%r!^mbox://!, "")
16
+ uri = URI(uri_or_fp)
17
+ raise ArgumentError, "not an mbox uri" unless uri.scheme == "mbox"
18
+ raise ArgumentError, "mbox uri ('#{uri}') cannot have a host: #{uri.host}" if uri.host
21
19
  ## heuristic: use the filename as a label, unless the file
22
20
  ## 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
21
+ @labels << File.basename(uri.path).intern unless File.dirname(uri.path) =~ /\b(var|usr|spool)\b/
22
+ @f = File.open uri.path
25
23
  else
26
24
  @f = uri_or_fp
27
25
  end
28
26
  end
29
27
 
28
+ def check
29
+ if (cur_offset ||= start_offset) > end_offset
30
+ raise OutOfSyncSourceError, "mbox file is smaller than last recorded message offset. Messages have probably been deleted by another client."
31
+ end
32
+ end
33
+
30
34
  def start_offset; 0; end
31
35
  def end_offset; File.size @f; end
32
36
 
@@ -36,9 +40,7 @@ class Loader < Source
36
40
  @f.seek offset
37
41
  l = @f.gets
38
42
  unless l =~ BREAK_RE
39
- Redwood::log "#{to_s}: offset mismatch in mbox file offset #{offset.inspect}: #{l.inspect}"
40
- self.broken_msg = "offset mismatch in mbox file offset #{offset.inspect}: #{l.inspect}. Run 'sup-import --rebuild #{to_s}' to correct this."
41
- raise SourceError, self.broken_msg
43
+ raise OutOfSyncSourceError, "mismatch in mbox file offset #{offset.inspect}: #{l.inspect}."
42
44
  end
43
45
  header = MBox::read_header @f
44
46
  end
@@ -46,7 +48,6 @@ class Loader < Source
46
48
  end
47
49
 
48
50
  def load_message offset
49
- raise SourceError, self.broken_msg if broken?
50
51
  @mutex.synchronize do
51
52
  @f.seek offset
52
53
  begin
@@ -54,13 +55,12 @@ class Loader < Source
54
55
  return RMail::Parser.read(input)
55
56
  end
56
57
  rescue RMail::Parser::Error => e
57
- raise SourceError, "error parsing message with rmail: #{e.message}"
58
+ raise FatalSourceError, "error parsing mbox file: #{e.message}"
58
59
  end
59
60
  end
60
61
  end
61
62
 
62
63
  def raw_header offset
63
- raise SourceError, self.broken_msg if broken?
64
64
  ret = ""
65
65
  @mutex.synchronize do
66
66
  @f.seek offset
@@ -72,7 +72,6 @@ class Loader < Source
72
72
  end
73
73
 
74
74
  def raw_full_message offset
75
- raise SourceError, self.broken_msg if broken?
76
75
  ret = ""
77
76
  @mutex.synchronize do
78
77
  @f.seek offset
@@ -85,37 +84,40 @@ class Loader < Source
85
84
  end
86
85
 
87
86
  def next
88
- raise SourceError, self.broken_msg if broken?
89
87
  returned_offset = nil
90
88
  next_offset = cur_offset
91
89
 
92
- @mutex.synchronize do
93
- @f.seek cur_offset
94
-
95
- ## cur_offset could be at one of two places here:
96
-
97
- ## 1. before a \n and a mbox separator, if it was previously at
98
- ## EOF and a new message was added; or,
99
- ## 2. at the beginning of an mbox separator (in all other
100
- ## cases).
101
-
102
- l = @f.gets or raise "next while at EOF"
103
- if l =~ /^\s*$/ # case 1
104
- returned_offset = @f.tell
105
- @f.gets # now we're at a BREAK_RE, so skip past it
106
- else # case 2
107
- returned_offset = cur_offset
108
- ## we've already skipped past the BREAK_RE, so just go
109
- end
90
+ begin
91
+ @mutex.synchronize do
92
+ @f.seek cur_offset
110
93
 
111
- while(line = @f.gets)
112
- break if line =~ BREAK_RE
113
- next_offset = @f.tell
94
+ ## cur_offset could be at one of two places here:
95
+
96
+ ## 1. before a \n and a mbox separator, if it was previously at
97
+ ## EOF and a new message was added; or,
98
+ ## 2. at the beginning of an mbox separator (in all other
99
+ ## cases).
100
+
101
+ l = @f.gets or raise "next while at EOF"
102
+ if l =~ /^\s*$/ # case 1
103
+ returned_offset = @f.tell
104
+ @f.gets # now we're at a BREAK_RE, so skip past it
105
+ else # case 2
106
+ returned_offset = cur_offset
107
+ ## we've already skipped past the BREAK_RE, so just go
108
+ end
109
+
110
+ while(line = @f.gets)
111
+ break if line =~ BREAK_RE
112
+ next_offset = @f.tell
113
+ end
114
114
  end
115
+ rescue SystemCallError, IOError => e
116
+ raise FatalSourceError, "Error reading #{@f.path}: #{e.message}"
115
117
  end
116
118
 
117
119
  self.cur_offset = next_offset
118
- [returned_offset, labels]
120
+ [returned_offset, @labels.clone]
119
121
  end
120
122
  end
121
123
 
@@ -106,14 +106,12 @@ class SSHFile
106
106
  @file_size = nil
107
107
  @offset = 0
108
108
  @say_id = nil
109
- @broken_msg = nil
110
109
  @shell = nil
111
110
  @shell_mutex = nil
112
111
  @buf_mutex = Mutex.new
113
112
  end
114
113
 
115
114
  def to_s; "mbox+ssh://#@host/#@fn"; end ## TODO: remove this EVILness
116
- def broken?; !@broken_msg.nil?; end
117
115
 
118
116
  def connect
119
117
  do_remote nil
@@ -164,7 +162,6 @@ private
164
162
  end
165
163
 
166
164
  def unsafe_connect
167
- raise SSHFileError, @broken_msg if broken?
168
165
  return if @shell
169
166
 
170
167
  @key = [@host, @ssh_opts[:username]]
@@ -172,14 +169,13 @@ private
172
169
  @shell, @shell_mutex = @@shells_mutex.synchronize do
173
170
  unless @@shells.member? @key
174
171
  say "Opening SSH connection to #{@host} for #@fn..."
175
- #raise SSHFileError, "simulated SSH file error"
176
172
  session = Net::SSH.start @host, @ssh_opts
177
173
  say "Starting SSH shell..."
178
174
  @@shells[@key] = [session.shell.sync, Mutex.new]
179
175
  end
180
176
  @@shells[@key]
181
177
  end
182
-
178
+
183
179
  say "Checking for #@fn..."
184
180
  @shell_mutex.synchronize { raise Errno::ENOENT, @fn unless @shell.test("-e #@fn").status == 0 }
185
181
  ensure
@@ -190,29 +186,24 @@ private
190
186
  def do_remote cmd, expected_size=0
191
187
  retries = 0
192
188
  result = nil
193
- begin
194
- begin
195
- unsafe_connect
196
- if cmd
197
- # MBox::debug "sending command: #{cmd.inspect}"
198
- result = @shell_mutex.synchronize { x = @shell.send_command cmd; sleep 0.25; x }
199
- raise SSHFileError, "Failure during remote command #{cmd.inspect}: #{(result.stderr || result.stdout || "")[0 .. 100]}" unless result.status == 0
200
- end
201
189
 
202
- ## Net::SSH::Exceptions seem to happen every once in a while for
203
- ## no good reason.
204
- rescue Net::SSH::Exception, *RECOVERABLE_ERRORS
205
- if (retries += 1) <= 3
206
- @@shells_mutex.synchronize do
207
- @shell = nil
208
- @@shells[@key] = nil
209
- end
210
- retry
190
+ begin
191
+ unsafe_connect
192
+ if cmd
193
+ # MBox::debug "sending command: #{cmd.inspect}"
194
+ result = @shell_mutex.synchronize { x = @shell.send_command cmd; sleep 0.25; x }
195
+ raise SSHFileError, "Failure during remote command #{cmd.inspect}: #{(result.stderr || result.stdout || "")[0 .. 100]}" unless result.status == 0
196
+ end
197
+ ## Net::SSH::Exceptions seem to happen every once in a while for
198
+ ## no good reason.
199
+ rescue Net::SSH::Exception, *RECOVERABLE_ERRORS
200
+ if (retries += 1) <= 3
201
+ @@shells_mutex.synchronize do
202
+ @shell = nil
203
+ @@shells[@key] = nil
211
204
  end
212
- raise
205
+ retry
213
206
  end
214
- rescue Net::SSH::Exception, SSHFileError, SystemCallError => e
215
- @broken_msg = e.message
216
207
  raise
217
208
  end
218
209