]> git.cworth.org Git - sup/blob - lib/sup/maildir.rb
Merge branch 'load-all-threads'
[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   ## remind me never to use inheritance again.
15   yaml_properties :uri, :cur_offset, :usual, :archived, :id, :labels
16   def initialize uri, last_date=nil, usual=true, archived=false, id=nil, labels=[]
17     super uri, last_date, usual, archived, id
18     uri = URI(Source.expand_filesystem_uri(uri))
19
20     raise ArgumentError, "not a maildir URI" unless uri.scheme == "maildir"
21     raise ArgumentError, "maildir URI cannot have a host: #{uri.host}" if uri.host
22     raise ArgumentError, "mbox URI must have a path component" unless uri.path
23
24     @dir = uri.path
25     @labels = (labels || []).freeze
26     @ids = []
27     @ids_to_fns = {}
28     @last_scan = nil
29     @mutex = Mutex.new
30   end
31
32   def file_path; @dir end
33   def self.suggest_labels_for path; [] end
34   def is_source_for? uri; super || (URI(Source.expand_filesystem_uri(uri)) == URI(self.uri)); end
35
36   def check
37     scan_mailbox
38     return unless start_offset
39
40     start = @ids.index(cur_offset || start_offset) or raise OutOfSyncSourceError, "Unknown message id #{cur_offset || start_offset}." # couldn't find the most recent email
41   end
42   
43   def each_raw_message_line id
44     scan_mailbox
45     with_file_for(id) do |f|
46       until f.eof?
47         yield f.gets
48       end
49     end
50   end
51
52   def load_header id
53     scan_mailbox
54     with_file_for(id) { |f| MBox::read_header f }
55   end
56
57   def load_message id
58     scan_mailbox
59     with_file_for(id) { |f| RMail::Parser.read f }
60   end
61
62   def raw_header id
63     scan_mailbox
64     ret = ""
65     with_file_for(id) do |f|
66       until f.eof? || (l = f.gets) =~ /^$/
67         ret += l
68       end
69     end
70     ret
71   end
72
73   def raw_message id
74     scan_mailbox
75     with_file_for(id) { |f| f.readlines.join }
76   end
77
78   def scan_mailbox opts={}
79     return unless @ids.empty? || opts[:rescan]
80     return if @last_scan && (Time.now - @last_scan) < SCAN_INTERVAL
81
82     Redwood::log "scanning maildir..."
83     cdir = File.join(@dir, 'cur')
84     ndir = File.join(@dir, 'new')
85     
86     raise FatalSourceError, "#{cdir} not a directory" unless File.directory? cdir
87     raise FatalSourceError, "#{ndir} not a directory" unless File.directory? ndir
88
89     begin
90       @ids, @ids_to_fns = [], {}
91       (Dir[File.join(cdir, "*")] + Dir[File.join(ndir, "*")]).map do |fn|
92         id = make_id fn
93         @ids << id
94         @ids_to_fns[id] = fn
95       end
96       @ids.sort!
97     rescue SystemCallError, IOError => e
98       raise FatalSourceError, "Problem scanning Maildir directories: #{e.message}."
99     end
100     
101     Redwood::log "done scanning maildir"
102     @last_scan = Time.now
103   end
104   synchronized :scan_mailbox
105
106   def each
107     scan_mailbox
108     return unless start_offset
109
110     start = @ids.index(cur_offset || start_offset) or raise OutOfSyncSourceError, "Unknown message id #{cur_offset || start_offset}." # couldn't find the most recent email
111
112     start.upto(@ids.length - 1) do |i|         
113       id = @ids[i]
114       self.cur_offset = id
115       yield id, @labels + (seen?(id) ? [] : [:unread]) + (trashed?(id) ? [:deleted] : []) + (flagged?(id) ? [:starred] : [])
116     end
117   end
118
119   def start_offset
120     scan_mailbox
121     @ids.first
122   end
123
124   def end_offset
125     scan_mailbox :rescan => true
126     @ids.last
127   end
128
129   def pct_done; 100.0 * (@ids.index(cur_offset) || 0).to_f / (@ids.length - 1).to_f; end
130
131   def draft? msg; maildir_data(msg)[2].include? "D"; end
132   def flagged? msg; maildir_data(msg)[2].include? "F"; end
133   def passed? msg; maildir_data(msg)[2].include? "P"; end
134   def replied? msg; maildir_data(msg)[2].include? "R"; end
135   def seen? msg; maildir_data(msg)[2].include? "S"; end
136   def trashed? msg; maildir_data(msg)[2].include? "T"; end
137
138   def mark_draft msg; maildir_mark_file msg, "D" unless draft? msg; end
139   def mark_flagged msg; maildir_mark_file msg, "F" unless flagged? msg; end
140   def mark_passed msg; maildir_mark_file msg, "P" unless passed? msg; end
141   def mark_replied msg; maildir_mark_file msg, "R" unless replied? msg; end
142   def mark_seen msg; maildir_mark_file msg, "S" unless seen? msg; end
143   def mark_trashed msg; maildir_mark_file msg, "T" unless trashed? msg; end
144
145 private
146
147   def make_id fn
148     # use 7 digits for the size. why 7? seems nice.
149     sprintf("%d%07d", File.mtime(fn), File.size(fn) % 10000000).to_i
150   end
151
152   def with_file_for id
153     fn = @ids_to_fns[id] or raise OutOfSyncSourceError, "No such id: #{id.inspect}."
154     begin
155       File.open(fn) { |f| yield f }
156     rescue SystemCallError, IOError => e
157       raise FatalSourceError, "Problem reading file for id #{id.inspect}: #{fn.inspect}: #{e.message}."
158     end
159   end
160
161   def maildir_data msg
162     fn = File.basename @ids_to_fns[msg]
163     fn =~ %r{^([^:,]+):([12]),([DFPRST]*)$}
164     [($1 || fn), ($2 || "2"), ($3 || "")]
165   end
166
167   ## not thread-safe on msg
168   def maildir_mark_file msg, flag
169     orig_path = @ids_to_fns[msg]
170     orig_base, orig_fn = File.split(orig_path)
171     new_base = orig_base.slice(0..-4) + 'cur'
172     tmp_base = orig_base.slice(0..-4) + 'tmp'
173     md_base, md_ver, md_flags = maildir_data msg
174     md_flags += flag; md_flags = md_flags.split(//).sort.join.squeeze
175     new_path = File.join new_base, "#{md_base}:#{md_ver},#{md_flags}"
176     tmp_path = File.join tmp_base, "#{md_base}:#{md_ver},#{md_flags}"
177     File.link orig_path, tmp_path
178     File.unlink orig_path
179     File.link tmp_path, new_path
180     File.unlink tmp_path
181     @ids_to_fns[msg] = new_path
182   end
183 end
184
185 end