class Redwood::Maildir

Constants

MYHOSTNAME

Public Class Methods

new(uri, usual=true, archived=false, sync_back=true, id=nil, labels=[]) click to toggle source
Calls superclass method
# File lib/sup/maildir.rb, line 12
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)
  parts = @expanded_uri.match /^([a-zA-Z0-9]*:(\/\/)?)(.*)/
  if parts
    prefix = parts[1]
    @path = parts[3]
    uri = URI(prefix + URI.encode(@path, URI_ENCODE_CHARS))
  else
    uri = URI(URI.encode @expanded_uri, URI_ENCODE_CHARS)
    @path = uri.path
  end

  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.decode uri.path
  @labels = Set.new(labels || [])
  @mutex = Mutex.new
  @ctimes = { 'cur' => Time.at(0), 'new' => Time.at(0) }
end
suggest_labels_for(path;) click to toggle source
# File lib/sup/maildir.rb, line 40
def self.suggest_labels_for path; [] end

Public Instance Methods

draft?(id;) click to toggle source
# File lib/sup/maildir.rb, line 196
def draft? id; maildir_data(id)[2].include? "D"; end
each_raw_message_line(id) { |gets| ... } click to toggle source
# File lib/sup/maildir.rb, line 79
def each_raw_message_line id
  with_file_for(id) do |f|
    until f.eof?
      yield f.gets
    end
  end
end
file_path() click to toggle source
# File lib/sup/maildir.rb, line 39
def file_path; @dir end
flagged?(id;) click to toggle source
# File lib/sup/maildir.rb, line 197
def flagged? id; maildir_data(id)[2].include? "F"; end
is_source_for?(uri;) click to toggle source
Calls superclass method
# File lib/sup/maildir.rb, line 41
def is_source_for? uri; super || (uri == @expanded_uri); end
labels?(id) click to toggle source
# File lib/sup/maildir.rb, line 183
def labels? id
  maildir_labels id
end
load_header(id) click to toggle source
# File lib/sup/maildir.rb, line 87
def load_header id
  with_file_for(id) { |f| parse_raw_email_header f }
end
load_message(id) click to toggle source
# File lib/sup/maildir.rb, line 91
def load_message id
  with_file_for(id) { |f| RMail::Parser.read f }
end
maildir_labels(id) click to toggle source
# File lib/sup/maildir.rb, line 187
def maildir_labels id
  (seen?(id) ? [] : [:unread]) +
    (trashed?(id) ?  [:deleted] : []) +
    (flagged?(id) ? [:starred] : []) +
    (passed?(id) ? [:forwarded] : []) +
    (replied?(id) ? [:replied] : []) +
    (draft?(id) ? [:draft] : [])
end
passed?(id;) click to toggle source
# File lib/sup/maildir.rb, line 198
def passed? id; maildir_data(id)[2].include? "P"; end
poll() { |:add, :info => id, :labels => labels + maildir_labels(id) + [:inbox], :progress => to_f/total_size| ... } click to toggle source

XXX use less memory

# File lib/sup/maildir.rb, line 118
def poll
  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
    ctime = File.ctime subdir
    next if prev_ctime >= ctime
    @ctimes[d] = ctime

    old_ids = benchmark(:maildir_read_index) { Index.instance.enum_for(:each_source_info, self.id, "#{d}/").to_a }
    new_ids = benchmark(:maildir_read_dir) {
      Dir.open(subdir).select {
        |f| !File.directory? f}.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"
  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

  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
raw_header(id) click to toggle source
# File lib/sup/maildir.rb, line 103
def raw_header id
  ret = ""
  with_file_for(id) do |f|
    until f.eof? || (l = f.gets) =~ /^$/
      ret += l
    end
  end
  ret
end
raw_message(id) click to toggle source
# File lib/sup/maildir.rb, line 113
def raw_message id
  with_file_for(id) { |f| f.read }
end
replied?(id;) click to toggle source
# File lib/sup/maildir.rb, line 199
def replied? id; maildir_data(id)[2].include? "R"; end
seen?(id;) click to toggle source
# File lib/sup/maildir.rb, line 200
def seen? id; maildir_data(id)[2].include? "S"; end
store_message(date, from_email) { |f| ... } click to toggle source
# File lib/sup/maildir.rb, line 51
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)
    new_path = File.join(@dir, 'new', new_fn)
    begin
      sleep 2 if File.stat(tmp_path)

      File.stat(tmp_path)
    rescue Errno::ENOENT #this is what we want.
      begin
        File.open(tmp_path, 'wb') do |f|
          yield f #provide a writable interface for the caller
          f.fsync
        end

        File.safe_link tmp_path, new_path
        stored = true
      ensure
        File.unlink tmp_path if File.exist? tmp_path
      end
    end #rescue Errno...
  end #Dir.chdir

  stored
end
supported_labels?() click to toggle source
# File lib/sup/maildir.rb, line 43
def supported_labels?
  [:draft, :starred, :forwarded, :replied, :unread, :deleted]
end
sync_back(id, labels) click to toggle source
# File lib/sup/maildir.rb, line 95
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
sync_back_enabled?() click to toggle source
# File lib/sup/maildir.rb, line 47
def sync_back_enabled?
  @sync_back
end
trashed?(id;) click to toggle source
# File lib/sup/maildir.rb, line 201
def trashed? id; maildir_data(id)[2].include? "T"; end
valid?(id) click to toggle source
# File lib/sup/maildir.rb, line 203
def valid? id
  File.exist? File.join(@dir, id)
end

Private Instance Methods

maildir_data(id) click to toggle source
# File lib/sup/maildir.rb, line 223
def maildir_data id
  id = File.basename id
  # Flags we recognize are DFPRST
  id =~ %r{^([^:]+):([12]),([A-Za-z]*)$}
  [($1 || id), ($2 || "2"), ($3 || "")]
end
maildir_mark_file(orig_path, flags) click to toggle source
# File lib/sup/maildir.rb, line 246
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
maildir_reconcile_flags(id, labels) click to toggle source
# File lib/sup/maildir.rb, line 230
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
new_maildir_basefn() click to toggle source
# File lib/sup/maildir.rb, line 209
def new_maildir_basefn
  Kernel::srand()
  "#{Time.now.to_i.to_s}.#{$$}#{Kernel.rand(1000000)}.#{MYHOSTNAME}"
end
with_file_for(id) { |f| ... } click to toggle source
# File lib/sup/maildir.rb, line 214
def with_file_for id
  fn = File.join(@dir, id)
  begin
    File.open(fn, 'rb') { |f| yield f }
  rescue SystemCallError, IOError => e
    raise FatalSourceError, "Problem reading file for id #{id.inspect}: #{fn.inspect}: #{e.message}."
  end
end