lib/sup/maildir.rb in sup-0.14.1.1 vs lib/sup/maildir.rb in sup-0.15.0
- old
+ new
@@ -1,34 +1,47 @@
require 'uri'
+require 'set'
module Redwood
class Maildir < Source
include SerializeLabelsNicely
MYHOSTNAME = Socket.gethostname
## remind me never to use inheritance again.
- yaml_properties :uri, :usual, :archived, :id, :labels
- def initialize uri, usual=true, archived=false, id=nil, labels=[]
+ yaml_properties :uri, :usual, :archived, :sync_back, :id, :labels
+ def initialize uri, usual=true, archived=false, sync_back=true, id=nil, labels=[]
super uri, usual, archived, id
@expanded_uri = Source.expand_filesystem_uri(uri)
uri = URI(@expanded_uri)
raise ArgumentError, "not a maildir URI" unless uri.scheme == "maildir"
raise ArgumentError, "maildir URI cannot have a host: #{uri.host}" if uri.host
raise ArgumentError, "maildir URI must have a path component" unless uri.path
+ @sync_back = sync_back
+ # sync by default if not specified
+ @sync_back = true if @sync_back.nil?
+
@dir = uri.path
@labels = Set.new(labels || [])
@mutex = Mutex.new
- @mtimes = { 'cur' => Time.at(0), 'new' => Time.at(0) }
+ @ctimes = { 'cur' => Time.at(0), 'new' => Time.at(0) }
end
def file_path; @dir end
def self.suggest_labels_for path; [] end
def is_source_for? uri; super || (uri == @expanded_uri); end
+ def supported_labels?
+ [:draft, :starred, :forwarded, :replied, :unread, :deleted]
+ end
+
+ def sync_back_enabled?
+ @sync_back
+ end
+
def store_message date, from_email, &block
stored = false
new_fn = new_maildir_basefn + ':2,S'
Dir.chdir(@dir) do |d|
tmp_path = File.join(@dir, 'tmp', new_fn)
@@ -42,11 +55,11 @@
File.open(tmp_path, 'wb') do |f|
yield f #provide a writable interface for the caller
f.fsync
end
- File.link tmp_path, new_path
+ File.safe_link tmp_path, new_path
stored = true
ensure
File.unlink tmp_path if File.exists? tmp_path
end
end #rescue Errno...
@@ -69,10 +82,18 @@
def load_message id
with_file_for(id) { |f| RMail::Parser.read f }
end
+ def sync_back id, labels
+ synchronize do
+ debug "syncing back maildir message #{id} with flags #{labels.to_a}"
+ flags = maildir_reconcile_flags id, labels
+ maildir_mark_file id, flags
+ end
+ end
+
def raw_header id
ret = ""
with_file_for(id) do |f|
until f.eof? || (l = f.gets) =~ /^$/
ret += l
@@ -85,61 +106,91 @@
with_file_for(id) { |f| f.read }
end
## XXX use less memory
def poll
- @mtimes.each do |d,prev_mtime|
+ added = []
+ deleted = []
+ updated = []
+ @ctimes.each do |d,prev_ctime|
subdir = File.join @dir, d
debug "polling maildir #{subdir}"
raise FatalSourceError, "#{subdir} not a directory" unless File.directory? subdir
- mtime = File.mtime subdir
- next if prev_mtime >= mtime
- @mtimes[d] = mtime
+ ctime = File.ctime subdir
+ next if prev_ctime >= ctime
+ @ctimes[d] = ctime
old_ids = benchmark(:maildir_read_index) { Enumerator.new(Index.instance, :each_source_info, self.id, "#{d}/").to_a }
- new_ids = benchmark(:maildir_read_dir) { Dir.glob("#{subdir}/*").map { |x| File.basename x }.sort }
- added = new_ids - old_ids
- deleted = old_ids - new_ids
+ new_ids = benchmark(:maildir_read_dir) { Dir.glob("#{subdir}/*").map { |x| File.join(d,File.basename(x)) }.sort }
+ added += new_ids - old_ids
+ deleted += old_ids - new_ids
debug "#{old_ids.size} in index, #{new_ids.size} in filesystem"
- debug "#{added.size} added, #{deleted.size} deleted"
+ end
- added.each_with_index do |id,i|
- yield :add,
- :info => File.join(d,id),
- :labels => @labels + maildir_labels(id) + [:inbox],
- :progress => i.to_f/(added.size+deleted.size)
- end
+ ## find updated mails by checking if an id is in both added and
+ ## deleted arrays, meaning that its flags changed or that it has
+ ## been moved, these ids need to be removed from added and deleted
+ add_to_delete = del_to_delete = []
+ map = Hash.new { |hash, key| hash[key] = [] }
+ deleted.each do |id_del|
+ map[maildir_data(id_del)[0]].push id_del
+ end
+ added.each do |id_add|
+ map[maildir_data(id_add)[0]].each do |id_del|
+ updated.push [ id_del, id_add ]
+ add_to_delete.push id_add
+ del_to_delete.push id_del
+ end
+ end
+ added -= add_to_delete
+ deleted -= del_to_delete
+ debug "#{added.size} added, #{deleted.size} deleted, #{updated.size} updated"
+ total_size = added.size+deleted.size+updated.size
- deleted.each_with_index do |id,i|
- yield :delete,
- :info => File.join(d,id),
- :progress => (i.to_f+added.size)/(added.size+deleted.size)
- end
+ added.each_with_index do |id,i|
+ yield :add,
+ :info => id,
+ :labels => @labels + maildir_labels(id) + [:inbox],
+ :progress => i.to_f/total_size
end
+
+ deleted.each_with_index do |id,i|
+ yield :delete,
+ :info => id,
+ :progress => (i.to_f+added.size)/total_size
+ end
+
+ updated.each_with_index do |id,i|
+ yield :update,
+ :old_info => id[0],
+ :new_info => id[1],
+ :labels => @labels + maildir_labels(id[1]),
+ :progress => (i.to_f+added.size+deleted.size)/total_size
+ end
nil
end
+ def labels? id
+ maildir_labels id
+ end
+
def maildir_labels id
(seen?(id) ? [] : [:unread]) +
(trashed?(id) ? [:deleted] : []) +
- (flagged?(id) ? [:starred] : [])
+ (flagged?(id) ? [:starred] : []) +
+ (passed?(id) ? [:forwarded] : []) +
+ (replied?(id) ? [:replied] : []) +
+ (draft?(id) ? [:draft] : [])
end
def draft? id; maildir_data(id)[2].include? "D"; end
def flagged? id; maildir_data(id)[2].include? "F"; end
def passed? id; maildir_data(id)[2].include? "P"; end
def replied? id; maildir_data(id)[2].include? "R"; end
def seen? id; maildir_data(id)[2].include? "S"; end
def trashed? id; maildir_data(id)[2].include? "T"; end
- def mark_draft id; maildir_mark_file id, "D" unless draft? id; end
- def mark_flagged id; maildir_mark_file id, "F" unless flagged? id; end
- def mark_passed id; maildir_mark_file id, "P" unless passed? id; end
- def mark_replied id; maildir_mark_file id, "R" unless replied? id; end
- def mark_seen id; maildir_mark_file id, "S" unless seen? id; end
- def mark_trashed id; maildir_mark_file id, "T" unless trashed? id; end
-
def valid? id
File.exists? File.join(@dir, id)
end
private
@@ -157,28 +208,50 @@
raise FatalSourceError, "Problem reading file for id #{id.inspect}: #{fn.inspect}: #{e.message}."
end
end
def maildir_data id
- id =~ %r{^([^:]+):([12]),([DFPRST]*)$}
+ id = File.basename id
+ # Flags we recognize are DFPRST
+ id =~ %r{^([^:]+):([12]),([A-Za-z]*)$}
[($1 || id), ($2 || "2"), ($3 || "")]
end
- ## not thread-safe on msg
- def maildir_mark_file msg, flag
- orig_path = @ids_to_fns[msg]
- orig_base, orig_fn = File.split(orig_path)
- new_base = orig_base.slice(0..-4) + 'cur'
- tmp_base = orig_base.slice(0..-4) + 'tmp'
- md_base, md_ver, md_flags = maildir_data msg
- md_flags += flag; md_flags = md_flags.split(//).sort.join.squeeze
- new_path = File.join new_base, "#{md_base}:#{md_ver},#{md_flags}"
- tmp_path = File.join tmp_base, "#{md_base}:#{md_ver},#{md_flags}"
- File.link orig_path, tmp_path
- File.unlink orig_path
- File.link tmp_path, new_path
- File.unlink tmp_path
- @ids_to_fns[msg] = new_path
+ def maildir_reconcile_flags id, labels
+ new_flags = Set.new( maildir_data(id)[2].each_char )
+
+ # Set flags based on labels for the six flags we recognize
+ if labels.member? :draft then new_flags.add?( "D" ) else new_flags.delete?( "D" ) end
+ if labels.member? :starred then new_flags.add?( "F" ) else new_flags.delete?( "F" ) end
+ if labels.member? :forwarded then new_flags.add?( "P" ) else new_flags.delete?( "P" ) end
+ if labels.member? :replied then new_flags.add?( "R" ) else new_flags.delete?( "R" ) end
+ if not labels.member? :unread then new_flags.add?( "S" ) else new_flags.delete?( "S" ) end
+ if labels.member? :deleted or labels.member? :killed then new_flags.add?( "T" ) else new_flags.delete?( "T" ) end
+
+ ## Flags must be stored in ASCII order according to Maildir
+ ## documentation
+ new_flags.to_a.sort.join
+ end
+
+ def maildir_mark_file orig_path, flags
+ @mutex.synchronize do
+ new_base = (flags.include?("S")) ? "cur" : "new"
+ md_base, md_ver, md_flags = maildir_data orig_path
+
+ return if md_flags == flags
+
+ new_loc = File.join new_base, "#{md_base}:#{md_ver},#{flags}"
+ orig_path = File.join @dir, orig_path
+ new_path = File.join @dir, new_loc
+ tmp_path = File.join @dir, "tmp", "#{md_base}:#{md_ver},#{flags}"
+
+ File.safe_link orig_path, tmp_path
+ File.unlink orig_path
+ File.safe_link tmp_path, new_path
+ File.unlink tmp_path
+
+ new_loc
+ end
end
end
end