]> git.cworth.org Git - sup/blob - lib/sup/mbox/loader.rb
expand twiddles for mbox and maildir uris (thanks to Magnus Therning)
[sup] / lib / sup / mbox / loader.rb
1 require 'rmail'
2 require 'uri'
3
4 module Redwood
5 module MBox
6
7 class Loader < Source
8   yaml_properties :uri, :cur_offset, :usual, :archived, :id, :labels
9
10   ## uri_or_fp is horrific. need to refactor.
11   def initialize uri_or_fp, start_offset=nil, usual=true, archived=false, id=nil, labels=[]
12     @mutex = Mutex.new
13     @labels = (labels || []).freeze
14
15     case uri_or_fp
16     when String
17       uri_or_fp = Source.expand_filesystem_uri uri_or_fp
18       uri = URI(uri_or_fp)
19       raise ArgumentError, "not an mbox uri" unless uri.scheme == "mbox"
20       raise ArgumentError, "mbox uri ('#{uri}') cannot have a host: #{uri.host}" if uri.host
21       @f = File.open uri.path
22     else
23       @f = uri_or_fp
24     end
25
26     super uri_or_fp, start_offset, usual, archived, id
27   end
28
29   def file_path; URI(uri).path end
30   def is_source_for? uri; super || (URI(Source.expand_filesystem_uri(uri)) == URI(self.uri)); end
31
32   def self.suggest_labels_for path
33     ## heuristic: use the filename as a label, unless the file
34     ## has a path that probably represents an inbox.
35     if File.dirname(path) =~ /\b(var|usr|spool)\b/
36       []
37     else
38       [File.basename(path).intern]
39     end
40   end
41
42   def check
43     if (cur_offset ||= start_offset) > end_offset
44       raise OutOfSyncSourceError, "mbox file is smaller than last recorded message offset. Messages have probably been deleted by another client."
45     end
46   end
47     
48   def start_offset; 0; end
49   def end_offset; File.size @f; end
50
51   def load_header offset
52     header = nil
53     @mutex.synchronize do
54       @f.seek offset
55       l = @f.gets
56       unless l =~ BREAK_RE
57         raise OutOfSyncSourceError, "mismatch in mbox file offset #{offset.inspect}: #{l.inspect}." 
58       end
59       header = MBox::read_header @f
60     end
61     header
62   end
63
64   def load_message offset
65     @mutex.synchronize do
66       @f.seek offset
67       begin
68         RMail::Mailbox::MBoxReader.new(@f).each_message do |input|
69           return RMail::Parser.read(input)
70         end
71       rescue RMail::Parser::Error => e
72         raise FatalSourceError, "error parsing mbox file: #{e.message}"
73       end
74     end
75   end
76
77   def raw_header offset
78     ret = ""
79     @mutex.synchronize do
80       @f.seek offset
81       until @f.eof? || (l = @f.gets) =~ /^$/
82         ret += l
83       end
84     end
85     ret
86   end
87
88   def raw_full_message offset
89     ret = ""
90     each_raw_full_message_line(offset) { |l| ret += l }
91     ret
92   end
93
94   ## apparently it's a million times faster to call this directly if
95   ## we're just moving messages around on disk, than reading things
96   ## into memory with raw_full_message.
97   ##
98   ## i hoped never to have to move shit around on disk but
99   ## sup-sync-back has to do it.
100   def each_raw_full_message_line offset
101     @mutex.synchronize do
102       @f.seek offset
103       yield @f.gets
104       until @f.eof? || (l = @f.gets) =~ BREAK_RE
105         yield l
106       end
107     end
108   end
109
110   def next
111     returned_offset = nil
112     next_offset = cur_offset
113
114     begin
115       @mutex.synchronize do
116         @f.seek cur_offset
117
118         ## cur_offset could be at one of two places here:
119
120         ## 1. before a \n and a mbox separator, if it was previously at
121         ##    EOF and a new message was added; or,
122         ## 2. at the beginning of an mbox separator (in all other
123         ##    cases).
124
125         l = @f.gets or raise "next while at EOF"
126         if l =~ /^\s*$/ # case 1
127           returned_offset = @f.tell
128           @f.gets # now we're at a BREAK_RE, so skip past it
129         else # case 2
130           returned_offset = cur_offset
131           ## we've already skipped past the BREAK_RE, so just go
132         end
133
134         while(line = @f.gets)
135           break if line =~ BREAK_RE
136           next_offset = @f.tell
137         end
138       end
139     rescue SystemCallError, IOError => e
140       raise FatalSourceError, "Error reading #{@f.path}: #{e.message}"
141     end
142
143     self.cur_offset = next_offset
144     [returned_offset, @labels]
145   end
146 end
147
148 end
149 end