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
11 class Maildir < Source
12 SCAN_INTERVAL = 30 # seconds
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))
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
25 @labels = (labels || []).freeze
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
38 return unless start_offset
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
45 with_file_for(id) { |f| MBox::read_header f }
50 with_file_for(id) { |f| RMail::Parser.read f }
56 with_file_for(id) do |f|
57 until f.eof? || (l = f.gets) =~ /^$/
66 with_file_for(id) { |f| f.readlines.join }
70 return if @last_scan && (Time.now - @last_scan) < SCAN_INTERVAL
72 cdir = File.join(@dir, 'cur')
73 ndir = File.join(@dir, 'new')
75 raise FatalSourceError, "#{cdir} not a directory" unless File.directory? cdir
76 raise FatalSourceError, "#{ndir} not a directory" unless File.directory? ndir
79 @ids, @ids_to_fns = @mutex.synchronize do
80 ids, ids_to_fns = [], {}
81 (Dir[File.join(cdir, "*")] + Dir[File.join(ndir, "*")]).map do |fn|
86 [ids.sort, ids_to_fns]
88 rescue SystemCallError, IOError => e
89 raise FatalSourceError, "Problem scanning Maildir directories: #{e.message}."
97 return unless start_offset
99 start = @ids.index(cur_offset || start_offset) or raise OutOfSyncSourceError, "Unknown message id #{cur_offset || start_offset}." # couldn't find the most recent email
101 start.upto(@ids.length - 1) do |i|
104 yield id, @labels + (seen?(id) ? [] : [:unread]) + (trashed?(id) ? [:deleted] : []) + (flagged?(id) ? [:starred] : [])
118 def pct_done; 100.0 * (@ids.index(cur_offset) || 0).to_f / (@ids.length - 1).to_f; end
120 def draft? msg; maildir_data(msg)[2].include? "D"; end
121 def flagged? msg; maildir_data(msg)[2].include? "F"; end
122 def passed? msg; maildir_data(msg)[2].include? "P"; end
123 def replied? msg; maildir_data(msg)[2].include? "R"; end
124 def seen? msg; maildir_data(msg)[2].include? "S"; end
125 def trashed? msg; maildir_data(msg)[2].include? "T"; end
127 def mark_draft msg; maildir_mark_file msg, "D" unless draft? msg; end
128 def mark_flagged msg; maildir_mark_file msg, "F" unless flagged? msg; end
129 def mark_passed msg; maildir_mark_file msg, "P" unless passed? msg; end
130 def mark_replied msg; maildir_mark_file msg, "R" unless replied? msg; end
131 def mark_seen msg; maildir_mark_file msg, "S" unless seen? msg; end
132 def mark_trashed msg; maildir_mark_file msg, "T" unless trashed? msg; end
137 # use 7 digits for the size. why 7? seems nice.
138 sprintf("%d%07d", File.mtime(fn), File.size(fn)).to_i
142 fn = @ids_to_fns[id] or raise OutOfSyncSourceError, "No such id: #{id.inspect}."
144 File.open(fn) { |f| yield f }
145 rescue SystemCallError, IOError => e
146 raise FatalSourceError, "Problem reading file for id #{id.inspect}: #{fn.inspect}: #{e.message}."
151 fn = File.basename @ids_to_fns[msg]
152 fn =~ %r{^([^:,]+):([12]),([DFPRST]*)$}
153 [($1 || fn), ($2 || "2"), ($3 || "")]
156 ## not thread-safe on msg
157 def maildir_mark_file msg, flag
158 orig_path = @ids_to_fns[msg]
159 orig_base, orig_fn = File.split(orig_path)
160 new_base = orig_base.slice(0..-4) + 'cur'
161 tmp_base = orig_base.slice(0..-4) + 'tmp'
162 md_base, md_ver, md_flags = maildir_data msg
163 md_flags += flag; md_flags = md_flags.split(//).sort.join.squeeze
164 new_path = File.join new_base, "#{md_base}:#{md_ver},#{md_flags}"
165 tmp_path = File.join tmp_base, "#{md_base}:#{md_ver},#{md_flags}"
166 File.link orig_path, tmp_path
167 File.unlink orig_path
168 File.link tmp_path, new_path
170 @ids_to_fns[msg] = new_path