class Redwood::Maildir

Maildir doesn't provide an ordered unique id, which is what Sup requires to be really useful. So we must maintain, in memory, a mapping between Sup “ids” (timestamps, essentially) and the pathnames on disk.

Constants

MYHOSTNAME
SCAN_INTERVAL

Public Class Methods

new(uri, last_date=nil, usual=true, archived=false, id=nil, labels=[], mtimes={}) click to toggle source
Calls superclass method
# File lib/sup/maildir.rb, line 18
def initialize uri, last_date=nil, usual=true, archived=false, id=nil, labels=[], mtimes={}
  super uri, last_date, usual, archived, id
  uri = URI(Source.expand_filesystem_uri(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

  @dir = uri.path
  @labels = Set.new(labels || [])
  @ids = []
  @ids_to_fns = {}
  @last_scan = nil
  @mutex = Mutex.new
  #the mtime from the subdirs in the maildir with the unix epoch as default.
  #these are used to determine whether scanning the directory for new mail
  #is a worthwhile effort
  @mtimes = { 'cur' => Time.at(0), 'new' => Time.at(0) }.merge(mtimes || {})
  @dir_ids = { 'cur' => [], 'new' => [] }
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

check() click to toggle source
# File lib/sup/maildir.rb, line 43
def check
  scan_mailbox
  return unless start_offset

  start = @ids.index(cur_offset || start_offset) or raise OutOfSyncSourceError, "Unknown message id #{cur_offset || start_offset}." # couldn't find the most recent email
end
draft?(msg;) click to toggle source
# File lib/sup/maildir.rb, line 176
def draft? msg; maildir_data(msg)[2].include? "D"; end
each() { |id, labels + (seen?(id) ? [] : [:unread]) + (trashed?(id) ? [:deleted] : []) + (flagged?(id) ? [:starred] : [])| ... } click to toggle source
# File lib/sup/maildir.rb, line 151
def each
  scan_mailbox
  return unless start_offset

  start = @ids.index(cur_offset || start_offset) or raise OutOfSyncSourceError, "Unknown message id #{cur_offset || start_offset}." # couldn't find the most recent email

  start.upto(@ids.length - 1) do |i|         
    id = @ids[i]
    self.cur_offset = id
    yield id, @labels + (seen?(id) ? [] : [:unread]) + (trashed?(id) ? [:deleted] : []) + (flagged?(id) ? [:starred] : [])
  end
end
each_raw_message_line(id) { |gets| ... } click to toggle source
# File lib/sup/maildir.rb, line 78
def each_raw_message_line id
  scan_mailbox
  with_file_for(id) do |f|
    until f.eof?
      yield f.gets
    end
  end
end
end_offset() click to toggle source
# File lib/sup/maildir.rb, line 169
def end_offset
  scan_mailbox :rescan => true
  @ids.last + 1
end
file_path() click to toggle source
# File lib/sup/maildir.rb, line 39
def file_path; @dir end
filename_for_id(id;) click to toggle source
# File lib/sup/maildir.rb, line 190
def filename_for_id id; @ids_to_fns[id] end
flagged?(msg;) click to toggle source
# File lib/sup/maildir.rb, line 177
def flagged? msg; maildir_data(msg)[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(Source.expand_filesystem_uri(uri)) == URI(self.uri)); end
load_header(id) click to toggle source
# File lib/sup/maildir.rb, line 87
def load_header id
  scan_mailbox
  with_file_for(id) { |f| parse_raw_email_header f }
end
load_message(id) click to toggle source
# File lib/sup/maildir.rb, line 92
def load_message id
  scan_mailbox
  with_file_for(id) { |f| RMail::Parser.read f }
end
mark_draft(msg;) click to toggle source
# File lib/sup/maildir.rb, line 183
def mark_draft msg; maildir_mark_file msg, "D" unless draft? msg; end
mark_flagged(msg;) click to toggle source
# File lib/sup/maildir.rb, line 184
def mark_flagged msg; maildir_mark_file msg, "F" unless flagged? msg; end
mark_passed(msg;) click to toggle source
# File lib/sup/maildir.rb, line 185
def mark_passed msg; maildir_mark_file msg, "P" unless passed? msg; end
mark_replied(msg;) click to toggle source
# File lib/sup/maildir.rb, line 186
def mark_replied msg; maildir_mark_file msg, "R" unless replied? msg; end
mark_seen(msg;) click to toggle source
# File lib/sup/maildir.rb, line 187
def mark_seen msg; maildir_mark_file msg, "S" unless seen? msg; end
mark_trashed(msg;) click to toggle source
# File lib/sup/maildir.rb, line 188
def mark_trashed msg; maildir_mark_file msg, "T" unless trashed? msg; end
passed?(msg;) click to toggle source
# File lib/sup/maildir.rb, line 178
def passed? msg; maildir_data(msg)[2].include? "P"; end
pct_done() click to toggle source
# File lib/sup/maildir.rb, line 174
def pct_done; 100.0 * (@ids.index(cur_offset) || 0).to_f / (@ids.length - 1).to_f; end
raw_header(id) click to toggle source
# File lib/sup/maildir.rb, line 97
def raw_header id
  scan_mailbox
  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 108
def raw_message id
  scan_mailbox
  with_file_for(id) { |f| f.read }
end
replied?(msg;) click to toggle source
# File lib/sup/maildir.rb, line 179
def replied? msg; maildir_data(msg)[2].include? "R"; end
scan_mailbox(opts={}) click to toggle source
# File lib/sup/maildir.rb, line 113
def scan_mailbox opts={}
  return unless @ids.empty? || opts[:rescan]
  return if @last_scan && (Time.now - @last_scan) < SCAN_INTERVAL

  initial_poll = @ids.empty?

  debug "scanning maildir #@dir..."
  begin
    @mtimes.each_key do |d|
      subdir = File.join(@dir, d)
      raise FatalSourceError, "#{subdir} not a directory" unless File.directory? subdir

      mtime = File.mtime subdir

      #only scan the dir if the mtime is more recent (or we haven't polled
      #since startup)
      if @mtimes[d] < mtime || initial_poll
        @mtimes[d] = mtime
        @dir_ids[d] = []
        Dir[File.join(subdir, '*')].map do |fn|
          id = make_id fn
          @dir_ids[d] << id
          @ids_to_fns[id] = fn
        end
      else
        debug "no poll on #{d}.  mtime on indicates no new messages."
      end
    end
    @ids = @dir_ids.values.flatten.uniq.sort!
  rescue SystemCallError, IOError => e
    raise FatalSourceError, "Problem scanning Maildir directories: #{e.message}."
  end
  
  debug "done scanning maildir"
  @last_scan = Time.now
end
seen?(msg;) click to toggle source
# File lib/sup/maildir.rb, line 180
def seen? msg; maildir_data(msg)[2].include? "S"; end
start_offset() click to toggle source
# File lib/sup/maildir.rb, line 164
def start_offset
  scan_mailbox
  @ids.first
end
store_message(date, from_email) { |f| ... } click to toggle source
# File lib/sup/maildir.rb, line 50
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.link tmp_path, new_path
        stored = true
      ensure
        File.unlink tmp_path if File.exists? tmp_path
      end
    end #rescue Errno...
  end #Dir.chdir

  stored
end
trashed?(msg;) click to toggle source
# File lib/sup/maildir.rb, line 181
def trashed? msg; maildir_data(msg)[2].include? "T"; end

Private Instance Methods

maildir_data(msg) click to toggle source
# File lib/sup/maildir.rb, line 216
def maildir_data msg
  fn = File.basename @ids_to_fns[msg]
  fn =~ %r{^([^:]+):([12]),([DFPRST]*)$}
  [($1 || fn), ($2 || "2"), ($3 || "")]
end
maildir_mark_file(msg, flag) click to toggle source

not thread-safe on msg

# File lib/sup/maildir.rb, line 223
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
end
make_id(fn) click to toggle source
# File lib/sup/maildir.rb, line 194
def make_id fn
  #doing this means 1 syscall instead of 2 (File.mtime, File.size).
  #makes a noticeable difference on nfs.
  stat = File.stat(fn)
  # use 7 digits for the size. why 7? seems nice.
  sprintf("%d%07d", stat.mtime, stat.size % 10000000).to_i
end
new_maildir_basefn() click to toggle source
# File lib/sup/maildir.rb, line 202
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 207
def with_file_for id
  fn = @ids_to_fns[id] or raise OutOfSyncSourceError, "No such id: #{id.inspect}."
  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