]> git.cworth.org Git - sup/blob - lib/sup/maildir.rb
many maildir changes from magnus therning
[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 file_path; @dir end
31   def self.suggest_labels_for path; [] end
32
33   def check
34     scan_mailbox
35     start = @ids.index(cur_offset || start_offset) or raise OutOfSyncSourceError, "Unknown message id #{cur_offset || start_offset}." # couldn't find the most recent email
36   end
37
38   def load_header id
39     scan_mailbox
40     with_file_for(id) { |f| MBox::read_header f }
41   end
42
43   def load_message id
44     scan_mailbox
45     with_file_for(id) { |f| RMail::Parser.read f }
46   end
47
48   def raw_header id
49     scan_mailbox
50     ret = ""
51     with_file_for(id) do |f|
52       until f.eof? || (l = f.gets) =~ /^$/
53         ret += l
54       end
55     end
56     ret
57   end
58
59   def raw_full_message id
60     scan_mailbox
61     with_file_for(id) { |f| f.readlines.join }
62   end
63
64   def scan_mailbox
65     return if @last_scan && (Time.now - @last_scan) < SCAN_INTERVAL
66
67     cdir = File.join(@dir, 'cur')
68     ndir = File.join(@dir, 'new')
69     
70     raise FatalSourceError, "#{cdir} not a directory" unless File.directory? cdir
71     raise FatalSourceError, "#{ndir} not a directory" unless File.directory? ndir
72
73     begin
74       @ids, @ids_to_fns = @mutex.synchronize do
75         ids, ids_to_fns = [], {}
76         (Dir[File.join(cdir, "*")] + Dir[File.join(ndir, "*")]).map do |fn|
77           id = make_id fn
78           ids << id
79           ids_to_fns[id] = fn
80         end
81         [ids.sort, ids_to_fns]
82       end
83     rescue SystemCallError, IOError => e
84       raise FatalSourceError, "Problem scanning Maildir directories: #{e.message}."
85     end
86     
87     @last_scan = Time.now
88   end
89
90   def each
91     scan_mailbox
92     start = @ids.index(cur_offset || start_offset) or raise OutOfSyncSourceError, "Unknown message id #{cur_offset || start_offset}." # couldn't find the most recent email
93
94     start.upto(@ids.length - 1) do |i|         
95       id = @ids[i]
96       self.cur_offset = id
97       yield id, @labels + (seen?(id) ? [] : [:unread]) + (trashed?(id) ? [:deleted] : [])
98     end
99   end
100
101   def start_offset
102     scan_mailbox
103     @ids.first
104   end
105
106   def end_offset
107     scan_mailbox
108     @ids.last
109   end
110
111   def pct_done; 100.0 * (@ids.index(cur_offset) || 0).to_f / (@ids.length - 1).to_f; end
112
113   def draft? msg; maildir_data(msg)[2].include? "D"; end
114   def flagged? msg; maildir_data(msg)[2].include? "F"; end
115   def passed? msg; maildir_data(msg)[2].include? "P"; end
116   def replied? msg; maildir_data(msg)[2].include? "R"; end
117   def seen? msg; maildir_data(msg)[2].include? "S"; end
118   def trashed? msg; maildir_data(msg)[2].include? "T"; end
119
120   def mark_draft msg; maildir_mark_file msg, "D" unless draft? msg; end
121   def mark_flagged msg; maildir_mark_file msg, "F" unless flagged? msg; end
122   def mark_passed msg; maildir_mark_file msg, "P" unless passed? msg; end
123   def mark_replied msg; maildir_mark_file msg, "R" unless replied? msg; end
124   def mark_seen msg; maildir_mark_file msg, "S" unless seen? msg; end
125   def mark_trashed msg; maildir_mark_file msg, "T" unless trashed? msg; end
126
127 private
128
129   def make_id fn
130     # use 7 digits for the size. why 7? seems nice.
131     sprintf("%d%07d", File.mtime(fn), File.size(fn)).to_i
132   end
133
134   def with_file_for id
135     fn = @ids_to_fns[id] or raise OutOfSyncSourceError, "No such id: #{id.inspect}."
136     begin
137       File.open(fn) { |f| yield f }
138     rescue SystemCallError, IOError => e
139       raise FatalSourceError, "Problem reading file for id #{id.inspect}: #{fn.inspect}: #{e.message}."
140     end
141   end
142
143   def maildir_data msg
144     fn = File.basename @ids_to_fns[msg]
145     fn =~ %r{^([^:,]+):([12]),([DFPRST]*)$}
146     [($1 || fn), ($2 || "2"), ($3 || "")]
147   end
148
149   ## not thread-safe on msg
150   def maildir_mark_file msg, flag
151     orig_path = @ids_to_fns[msg]
152     orig_base, orig_fn = File.split(orig_path)
153     new_base = orig_base.slice(0..-4) + 'cur'
154     tmp_base = orig_base.slice(0..-4) + 'tmp'
155     md_base, md_ver, md_flags = maildir_data msg
156     md_flags += flag; md_flags = md_flags.split(//).sort.join.squeeze
157     new_path = File.join new_base, "#{md_base}:#{md_ver},#{md_flags}"
158     tmp_path = File.join tmp_base, "#{md_base}:#{md_ver},#{md_flags}"
159     File.link orig_path, tmp_path
160     File.unlink orig_path
161     File.link tmp_path, new_path
162     File.unlink tmp_path
163     @ids_to_fns[msg] = new_path
164   end
165 end
166
167 end