sup 0.11 → 0.12

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.

@@ -54,7 +54,7 @@ private
54
54
  when "error"; "ERROR: "
55
55
  else ""
56
56
  end
57
- "[#{time.to_s}] #{prefix}#{msg}\n"
57
+ "[#{time.to_s}] #{prefix}#{msg.rstrip}\n"
58
58
  end
59
59
 
60
60
  ## actually distribute the message
@@ -1,22 +1,15 @@
1
- require 'rmail'
2
1
  require 'uri'
3
2
 
4
3
  module Redwood
5
4
 
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
5
  class Maildir < Source
12
6
  include SerializeLabelsNicely
13
- SCAN_INTERVAL = 30 # seconds
14
7
  MYHOSTNAME = Socket.gethostname
15
8
 
16
9
  ## remind me never to use inheritance again.
17
- yaml_properties :uri, :cur_offset, :usual, :archived, :id, :labels, :mtimes
18
- def initialize uri, last_date=nil, usual=true, archived=false, id=nil, labels=[], mtimes={}
19
- super uri, last_date, usual, archived, id
10
+ yaml_properties :uri, :usual, :archived, :id, :labels
11
+ def initialize uri, usual=true, archived=false, id=nil, labels=[]
12
+ super uri, usual, archived, id
20
13
  uri = URI(Source.expand_filesystem_uri(uri))
21
14
 
22
15
  raise ArgumentError, "not a maildir URI" unless uri.scheme == "maildir"
@@ -25,28 +18,14 @@ class Maildir < Source
25
18
 
26
19
  @dir = uri.path
27
20
  @labels = Set.new(labels || [])
28
- @ids = []
29
- @ids_to_fns = {}
30
- @last_scan = nil
31
21
  @mutex = Mutex.new
32
- #the mtime from the subdirs in the maildir with the unix epoch as default.
33
- #these are used to determine whether scanning the directory for new mail
34
- #is a worthwhile effort
35
- @mtimes = { 'cur' => Time.at(0), 'new' => Time.at(0) }.merge(mtimes || {})
36
- @dir_ids = { 'cur' => [], 'new' => [] }
22
+ @mtimes = { 'cur' => Time.at(0), 'new' => Time.at(0) }
37
23
  end
38
24
 
39
25
  def file_path; @dir end
40
26
  def self.suggest_labels_for path; [] end
41
27
  def is_source_for? uri; super || (URI(Source.expand_filesystem_uri(uri)) == URI(self.uri)); end
42
28
 
43
- def check
44
- scan_mailbox
45
- return unless start_offset
46
-
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
48
- end
49
-
50
29
  def store_message date, from_email, &block
51
30
  stored = false
52
31
  new_fn = new_maildir_basefn + ':2,S'
@@ -76,7 +55,6 @@ class Maildir < Source
76
55
  end
77
56
 
78
57
  def each_raw_message_line id
79
- scan_mailbox
80
58
  with_file_for(id) do |f|
81
59
  until f.eof?
82
60
  yield f.gets
@@ -85,17 +63,14 @@ class Maildir < Source
85
63
  end
86
64
 
87
65
  def load_header id
88
- scan_mailbox
89
66
  with_file_for(id) { |f| parse_raw_email_header f }
90
67
  end
91
68
 
92
69
  def load_message id
93
- scan_mailbox
94
70
  with_file_for(id) { |f| RMail::Parser.read f }
95
71
  end
96
72
 
97
73
  def raw_header id
98
- scan_mailbox
99
74
  ret = ""
100
75
  with_file_for(id) do |f|
101
76
  until f.eof? || (l = f.gets) =~ /^$/
@@ -106,106 +81,75 @@ class Maildir < Source
106
81
  end
107
82
 
108
83
  def raw_message id
109
- scan_mailbox
110
84
  with_file_for(id) { |f| f.read }
111
85
  end
112
86
 
113
- def scan_mailbox opts={}
114
- return unless @ids.empty? || opts[:rescan]
115
- return if @last_scan && (Time.now - @last_scan) < SCAN_INTERVAL
116
-
117
- initial_poll = @ids.empty?
118
-
119
- debug "scanning maildir #@dir..."
120
- begin
121
- @mtimes.each_key do |d|
122
- subdir = File.join(@dir, d)
123
- raise FatalSourceError, "#{subdir} not a directory" unless File.directory? subdir
124
-
125
- mtime = File.mtime subdir
126
-
127
- #only scan the dir if the mtime is more recent (or we haven't polled
128
- #since startup)
129
- if @mtimes[d] < mtime || initial_poll
130
- @mtimes[d] = mtime
131
- @dir_ids[d] = []
132
- Dir[File.join(subdir, '*')].map do |fn|
133
- id = make_id fn
134
- @dir_ids[d] << id
135
- @ids_to_fns[id] = fn
136
- end
137
- else
138
- debug "no poll on #{d}. mtime on indicates no new messages."
139
- end
87
+ ## XXX use less memory
88
+ def poll
89
+ @mtimes.each do |d,prev_mtime|
90
+ subdir = File.join @dir, d
91
+ debug "polling maildir #{subdir}"
92
+ raise FatalSourceError, "#{subdir} not a directory" unless File.directory? subdir
93
+ mtime = File.mtime subdir
94
+ next if prev_mtime >= mtime
95
+ @mtimes[d] = mtime
96
+
97
+ old_ids = benchmark(:maildir_read_index) { Enumerator.new(Index.instance, :each_source_info, self.id, "#{d}/").to_a }
98
+ new_ids = benchmark(:maildir_read_dir) { Dir.glob("#{subdir}/*").map { |x| File.basename x }.sort }
99
+ added = new_ids - old_ids
100
+ deleted = old_ids - new_ids
101
+ debug "#{old_ids.size} in index, #{new_ids.size} in filesystem"
102
+ debug "#{added.size} added, #{deleted.size} deleted"
103
+
104
+ added.each_with_index do |id,i|
105
+ yield :add,
106
+ :info => File.join(d,id),
107
+ :labels => @labels + maildir_labels(id) + [:inbox],
108
+ :progress => i.to_f/(added.size+deleted.size)
140
109
  end
141
- @ids = @dir_ids.values.flatten.uniq.sort!
142
- rescue SystemCallError, IOError => e
143
- raise FatalSourceError, "Problem scanning Maildir directories: #{e.message}."
144
- end
145
-
146
- debug "done scanning maildir"
147
- @last_scan = Time.now
148
- end
149
- synchronized :scan_mailbox
150
110
 
151
- def each
152
- scan_mailbox
153
- return unless start_offset
154
-
155
- start = @ids.index(cur_offset || start_offset) or raise OutOfSyncSourceError, "Unknown message id #{cur_offset || start_offset}." # couldn't find the most recent email
156
-
157
- start.upto(@ids.length - 1) do |i|
158
- id = @ids[i]
159
- self.cur_offset = id
160
- yield id, @labels + (seen?(id) ? [] : [:unread]) + (trashed?(id) ? [:deleted] : []) + (flagged?(id) ? [:starred] : [])
111
+ deleted.each_with_index do |id,i|
112
+ yield :delete,
113
+ :info => File.join(d,id),
114
+ :progress => (i.to_f+added.size)/(added.size+deleted.size)
115
+ end
161
116
  end
117
+ nil
162
118
  end
163
119
 
164
- def start_offset
165
- scan_mailbox
166
- @ids.first
167
- end
168
-
169
- def end_offset
170
- scan_mailbox :rescan => true
171
- @ids.last + 1
120
+ def maildir_labels id
121
+ (seen?(id) ? [] : [:unread]) +
122
+ (trashed?(id) ? [:deleted] : []) +
123
+ (flagged?(id) ? [:starred] : [])
172
124
  end
173
125
 
174
- def pct_done; 100.0 * (@ids.index(cur_offset) || 0).to_f / (@ids.length - 1).to_f; end
126
+ def draft? id; maildir_data(id)[2].include? "D"; end
127
+ def flagged? id; maildir_data(id)[2].include? "F"; end
128
+ def passed? id; maildir_data(id)[2].include? "P"; end
129
+ def replied? id; maildir_data(id)[2].include? "R"; end
130
+ def seen? id; maildir_data(id)[2].include? "S"; end
131
+ def trashed? id; maildir_data(id)[2].include? "T"; end
175
132
 
176
- def draft? msg; maildir_data(msg)[2].include? "D"; end
177
- def flagged? msg; maildir_data(msg)[2].include? "F"; end
178
- def passed? msg; maildir_data(msg)[2].include? "P"; end
179
- def replied? msg; maildir_data(msg)[2].include? "R"; end
180
- def seen? msg; maildir_data(msg)[2].include? "S"; end
181
- def trashed? msg; maildir_data(msg)[2].include? "T"; end
133
+ def mark_draft id; maildir_mark_file id, "D" unless draft? id; end
134
+ def mark_flagged id; maildir_mark_file id, "F" unless flagged? id; end
135
+ def mark_passed id; maildir_mark_file id, "P" unless passed? id; end
136
+ def mark_replied id; maildir_mark_file id, "R" unless replied? id; end
137
+ def mark_seen id; maildir_mark_file id, "S" unless seen? id; end
138
+ def mark_trashed id; maildir_mark_file id, "T" unless trashed? id; end
182
139
 
183
- def mark_draft msg; maildir_mark_file msg, "D" unless draft? msg; end
184
- def mark_flagged msg; maildir_mark_file msg, "F" unless flagged? msg; end
185
- def mark_passed msg; maildir_mark_file msg, "P" unless passed? msg; end
186
- def mark_replied msg; maildir_mark_file msg, "R" unless replied? msg; end
187
- def mark_seen msg; maildir_mark_file msg, "S" unless seen? msg; end
188
- def mark_trashed msg; maildir_mark_file msg, "T" unless trashed? msg; end
189
-
190
- def filename_for_id id; @ids_to_fns[id] end
140
+ def valid? id
141
+ File.exists? File.join(@dir, id)
142
+ end
191
143
 
192
144
  private
193
145
 
194
- def make_id fn
195
- #doing this means 1 syscall instead of 2 (File.mtime, File.size).
196
- #makes a noticeable difference on nfs.
197
- stat = File.stat(fn)
198
- # use 7 digits for the size. why 7? seems nice.
199
- sprintf("%d%07d", stat.mtime, stat.size % 10000000).to_i
200
- end
201
-
202
146
  def new_maildir_basefn
203
147
  Kernel::srand()
204
148
  "#{Time.now.to_i.to_s}.#{$$}#{Kernel.rand(1000000)}.#{MYHOSTNAME}"
205
149
  end
206
150
 
207
151
  def with_file_for id
208
- fn = @ids_to_fns[id] or raise OutOfSyncSourceError, "No such id: #{id.inspect}."
152
+ fn = File.join(@dir, id)
209
153
  begin
210
154
  File.open(fn, 'rb') { |f| yield f }
211
155
  rescue SystemCallError, IOError => e
@@ -213,10 +157,9 @@ private
213
157
  end
214
158
  end
215
159
 
216
- def maildir_data msg
217
- fn = File.basename @ids_to_fns[msg]
218
- fn =~ %r{^([^:]+):([12]),([DFPRST]*)$}
219
- [($1 || fn), ($2 || "2"), ($3 || "")]
160
+ def maildir_data id
161
+ id =~ %r{^([^:]+):([12]),([DFPRST]*)$}
162
+ [($1 || id), ($2 || "2"), ($3 || "")]
220
163
  end
221
164
 
222
165
  ## not thread-safe on msg
@@ -1,13 +1,155 @@
1
- require "sup/mbox/loader"
2
- require "sup/mbox/ssh-file"
3
- require "sup/mbox/ssh-loader"
1
+ require 'uri'
2
+ require 'set'
4
3
 
5
4
  module Redwood
6
5
 
7
- module MBox
6
+ class MBox < Source
8
7
  BREAK_RE = /^From \S+ (.+)$/
9
8
 
10
- def is_break_line? l
9
+ include SerializeLabelsNicely
10
+ yaml_properties :uri, :usual, :archived, :id, :labels
11
+
12
+ attr_reader :labels
13
+
14
+ ## uri_or_fp is horrific. need to refactor.
15
+ def initialize uri_or_fp, usual=true, archived=false, id=nil, labels=nil
16
+ @mutex = Mutex.new
17
+ @labels = Set.new((labels || []) - LabelManager::RESERVED_LABELS)
18
+
19
+ case uri_or_fp
20
+ when String
21
+ uri = URI(Source.expand_filesystem_uri(uri_or_fp))
22
+ raise ArgumentError, "not an mbox uri" unless uri.scheme == "mbox"
23
+ raise ArgumentError, "mbox URI ('#{uri}') cannot have a host: #{uri.host}" if uri.host
24
+ raise ArgumentError, "mbox URI must have a path component" unless uri.path
25
+ @f = File.open uri.path, 'rb'
26
+ @path = uri.path
27
+ else
28
+ @f = uri_or_fp
29
+ @path = uri_or_fp.path
30
+ end
31
+
32
+ super uri_or_fp, usual, archived, id
33
+ end
34
+
35
+ def file_path; @path end
36
+ def is_source_for? uri; super || (self.uri.is_a?(String) && (URI(Source.expand_filesystem_uri(uri)) == URI(Source.expand_filesystem_uri(self.uri)))) end
37
+
38
+ def self.suggest_labels_for path
39
+ ## heuristic: use the filename as a label, unless the file
40
+ ## has a path that probably represents an inbox.
41
+ if File.dirname(path) =~ /\b(var|usr|spool)\b/
42
+ []
43
+ else
44
+ [File.basename(path).downcase.intern]
45
+ end
46
+ end
47
+
48
+ def load_header offset
49
+ header = nil
50
+ @mutex.synchronize do
51
+ @f.seek offset
52
+ header = parse_raw_email_header @f
53
+ end
54
+ header
55
+ end
56
+
57
+ def load_message offset
58
+ @mutex.synchronize do
59
+ @f.seek offset
60
+ begin
61
+ ## don't use RMail::Mailbox::MBoxReader because it doesn't properly ignore
62
+ ## "From" at the start of a message body line.
63
+ string = ""
64
+ until @f.eof? || MBox::is_break_line?(l = @f.gets)
65
+ string << l
66
+ end
67
+ RMail::Parser.read string
68
+ rescue RMail::Parser::Error => e
69
+ raise FatalSourceError, "error parsing mbox file: #{e.message}"
70
+ end
71
+ end
72
+ end
73
+
74
+ def raw_header offset
75
+ ret = ""
76
+ @mutex.synchronize do
77
+ @f.seek offset
78
+ until @f.eof? || (l = @f.gets) =~ /^\r*$/
79
+ ret << l
80
+ end
81
+ end
82
+ ret
83
+ end
84
+
85
+ def raw_message offset
86
+ ret = ""
87
+ each_raw_message_line(offset) { |l| ret << l }
88
+ ret
89
+ end
90
+
91
+ def store_message date, from_email, &block
92
+ need_blank = File.exists?(@path) && !File.zero?(@path)
93
+ File.open(@path, "ab") do |f|
94
+ f.puts if need_blank
95
+ f.puts "From #{from_email} #{date.asctime}"
96
+ yield f
97
+ end
98
+ end
99
+
100
+ ## apparently it's a million times faster to call this directly if
101
+ ## we're just moving messages around on disk, than reading things
102
+ ## into memory with raw_message.
103
+ ##
104
+ ## i hoped never to have to move shit around on disk but
105
+ ## sup-sync-back has to do it.
106
+ def each_raw_message_line offset
107
+ @mutex.synchronize do
108
+ @f.seek offset
109
+ until @f.eof? || MBox::is_break_line?(l = @f.gets)
110
+ yield l
111
+ end
112
+ end
113
+ end
114
+
115
+ def default_labels
116
+ [:inbox, :unread]
117
+ end
118
+
119
+ def poll
120
+ first_offset = first_new_message
121
+ offset = first_offset
122
+ end_offset = File.size @f
123
+ while offset and offset < end_offset
124
+ yield :add,
125
+ :info => offset,
126
+ :labels => (labels + default_labels),
127
+ :progress => (offset - first_offset).to_f/end_offset
128
+ offset = next_offset offset
129
+ end
130
+ end
131
+
132
+ def next_offset offset
133
+ @mutex.synchronize do
134
+ @f.seek offset
135
+ nil while line = @f.gets and not MBox::is_break_line? line
136
+ offset = @f.tell
137
+ offset != File.size(@f) ? offset : nil
138
+ end
139
+ end
140
+
141
+ ## TODO optimize this by iterating over allterms list backwards or
142
+ ## storing source_info negated
143
+ def last_indexed_message
144
+ benchmark(:mbox_read_index) { Enumerator.new(Index.instance, :each_source_info, self.id).map(&:to_i).max }
145
+ end
146
+
147
+ ## offset of first new message or nil
148
+ def first_new_message
149
+ next_offset(last_indexed_message || 0)
150
+ end
151
+
152
+ def self.is_break_line? l
11
153
  l =~ BREAK_RE or return false
12
154
  time = $1
13
155
  begin
@@ -19,6 +161,9 @@ module MBox
19
161
  false
20
162
  end
21
163
  end
22
- module_function :is_break_line?
164
+
165
+ class Loader < self
166
+ yaml_properties :uri, :usual, :archived, :id, :labels
167
+ end
23
168
  end
24
169
  end
@@ -32,9 +32,25 @@ require 'tempfile'
32
32
  ## attachments are quotable; Signatures are not.
33
33
 
34
34
  ## monkey-patch time: make temp files have the right extension
35
- class Tempfile
36
- def make_tmpname basename, n
37
- sprintf '%d-%d-%s', $$, n, basename
35
+ ## Backport from Ruby 1.9.2 for versions lower than 1.8.7
36
+ if RUBY_VERSION < '1.8.7'
37
+ class Tempfile
38
+ def make_tmpname(prefix_suffix, n)
39
+ case prefix_suffix
40
+ when String
41
+ prefix = prefix_suffix
42
+ suffix = ""
43
+ when Array
44
+ prefix = prefix_suffix[0]
45
+ suffix = prefix_suffix[1]
46
+ else
47
+ raise ArgumentError, "unexpected prefix_suffix: #{prefix_suffix.inspect}"
48
+ end
49
+ t = Time.now.strftime("%Y%m%d")
50
+ path = "#{prefix}#{t}-#{$$}-#{rand(0x100000000).to_s(36)}"
51
+ path << "-#{n}" if n
52
+ path << suffix
53
+ end
38
54
  end
39
55
  end
40
56
 
@@ -149,7 +165,7 @@ EOS
149
165
  end
150
166
 
151
167
  def write_to_disk
152
- file = Tempfile.new(@filename || "sup-attachment")
168
+ file = Tempfile.new(["sup", @filename.gsub("/", "_") || "sup-attachment"])
153
169
  file.print @raw_content
154
170
  file.close
155
171
  file.path