]> git.cworth.org Git - sup/blob - lib/sup/maildir.rb
labels now fully determined by sources.yaml, and lots of improvements to sup-config
[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   yaml_properties :uri, :cur_offset, :usual, :archived, :id, :labels
15   def initialize uri, last_date=nil, usual=true, archived=false, id=nil, labels=[]
16     super uri, last_date, usual, archived, id
17     uri = URI(uri)
18
19     raise ArgumentError, "not a maildir URI" unless uri.scheme == "maildir"
20     raise ArgumentError, "maildir URI cannot have a host: #{uri.host}" if uri.host
21
22     @dir = uri.path
23     @labels = (labels || []).freeze
24     @ids = []
25     @ids_to_fns = {}
26     @last_scan = nil
27     @mutex = Mutex.new
28   end
29
30   def self.suggest_labels_for path; [] end
31
32   def check
33     scan_mailbox
34     start = @ids.index(cur_offset || start_offset) or raise OutOfSyncSourceError, "Unknown message id #{cur_offset || start_offset}." # couldn't find the most recent email
35   end
36
37   def load_header id
38     scan_mailbox
39     with_file_for(id) { |f| MBox::read_header f }
40   end
41
42   def load_message id
43     scan_mailbox
44     with_file_for(id) { |f| RMail::Parser.read f }
45   end
46
47   def raw_header id
48     scan_mailbox
49     ret = ""
50     with_file_for(id) do |f|
51       until f.eof? || (l = f.gets) =~ /^$/
52         ret += l
53       end
54     end
55     ret
56   end
57
58   def raw_full_message id
59     scan_mailbox
60     with_file_for(id) { |f| f.readlines.join }
61   end
62
63   def scan_mailbox
64     return if @last_scan && (Time.now - @last_scan) < SCAN_INTERVAL
65
66     cdir = File.join(@dir, 'cur')
67     ndir = File.join(@dir, 'new')
68     
69     raise FatalSourceError, "#{cdir} not a directory" unless File.directory? cdir
70     raise FatalSourceError, "#{ndir} not a directory" unless File.directory? ndir
71
72     begin
73       @ids, @ids_to_fns = @mutex.synchronize do
74         ids, ids_to_fns = [], {}
75         (Dir[File.join(cdir, "*")] + Dir[File.join(ndir, "*")]).map do |fn|
76           id = make_id fn
77           ids << id
78           ids_to_fns[id] = fn
79         end
80         [ids.sort, ids_to_fns]
81       end
82     rescue SystemCallError, IOError => e
83       raise FatalSourceError, "Problem scanning Maildir directories: #{e.message}."
84     end
85     
86     @last_scan = Time.now
87   end
88
89   def each
90     scan_mailbox
91     start = @ids.index(cur_offset || start_offset) or raise OutOfSyncSourceError, "Unknown message id #{cur_offset || start_offset}." # couldn't find the most recent email
92
93     start.upto(@ids.length - 1) do |i|         
94       id = @ids[i]
95       self.cur_offset = id
96       yield id, @labels + (@ids_to_fns[id] =~ /,.*R.*$/ ? [] : [:unread])
97     end
98   end
99
100   def start_offset
101     scan_mailbox
102     @ids.first
103   end
104
105   def end_offset
106     scan_mailbox
107     @ids.last
108   end
109
110   def pct_done; 100.0 * (@ids.index(cur_offset) || 0).to_f / (@ids.length - 1).to_f; end
111
112 private
113
114   def make_id fn
115     # use 7 digits for the size. why 7? seems nice.
116     sprintf("%d%07d", File.mtime(fn), File.size(fn)).to_i
117   end
118
119   def with_file_for id
120     fn = @ids_to_fns[id] or raise OutOfSyncSourceError, "No such id: #{id.inspect}."
121     begin
122       File.open(fn) { |f| yield f }
123     rescue SystemCallError, IOError => e
124       raise FatalSourceError, "Problem reading file for id #{id.inspect}: #{fn.inspect}: #{e.message}."
125     end
126   end
127 end
128
129 end