]> git.cworth.org Git - sup/blob - lib/sup/maildir.rb
012663d85eb7739173184c09009de42e9f08e106
[sup] / lib / sup / maildir.rb
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