Codebase list feed2imap / 6770b06 lib / feed2imap / maildir.rb
6770b06

Tree @6770b06 (Download .tar.gz)

maildir.rb @6770b06raw · history · blame

=begin
Feed2Imap - RSS/Atom Aggregator uploading to an IMAP Server, or local Maildir
Copyright (c) 2009 Andreas Rottmann

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
=end

require 'uri'
require 'fileutils'
require 'fcntl'
require 'rmail'
require 'socket'

class MaildirAccount
  MYHOSTNAME = Socket.gethostname

  @@seq_num = 0

  attr_reader :uri

  def putmail(folder, mail, date = Time::now)
    store_message(folder_dir(folder), date, nil) do |f|
      f.puts(mail)
    end
  end

  def updatemail(folder, mail, idx, date = Time::now, reupload_if_updated = true)
    dir = folder_dir(folder)
    guarantee_maildir(dir)
    mail_files = find_mails(dir, idx)
    flags = nil
    if mail_files.length > 0
      # get the info from the first result and delete everything
      info = maildir_file_info(mail_files[0])
      mail_files.each { |f| File.delete(File.join(dir, f)) }
    elsif not reupload_if_updated
      # mail not present, and we don't want to re-upload it
      return
    end
    store_message(dir, date, info) { |f| f.puts(mail) }
  end

  def to_s
    uri.to_s
  end

  def cleanup(folder, dryrun = false)
    dir = folder_dir(folder)
    puts "-- Considering #{dir}:"
    guarantee_maildir(dir)

    del_count = 0
    recent_time = Time.now() -- (3 * 24 * 60 * 60) # 3 days
    Dir[File.join(dir, 'cur', '*')].each do |fn|
      flags = maildir_file_info_flags(fn)
      # don't consider not-seen, flagged, or recent messages
      mtime = File.mtime(fn)
      next if (not flags.index('S') or
               flags.index('F') or
               mtime > recent_time)
      mail = File.open(fn) do |f|
        RMail::Parser.read(f)
      end
      subject = mail.header['Subject']
      if dryrun
        puts "To remove: #{subject} #{mtime}"
      else
        puts "Removing: #{subject} #{mtime}"
        File.delete(fn)
      end
      del_count += 1
    end
    puts "-- Deleted #{del_count} messages"
    return del_count
  end

  private

  def folder_dir(folder)
    return File.join('/', folder)
  end

  def store_message(dir, date, info, &block)

    guarantee_maildir(dir)

    stored = false
    Dir.chdir(dir) do |d|
      timer = 30
      fd = nil
      while timer >= 0
        new_fn = new_maildir_basefn(date)
        tmp_path = File.join(dir, 'tmp', new_fn)
        new_path = File.join(dir, 'new', new_fn)
        begin
          fd = IO::sysopen(tmp_path,
                           Fcntl::O_WRONLY | Fcntl::O_EXCL | Fcntl::O_CREAT)
          break
        rescue Errno::EEXIST
          sleep 2
          timer -= 2
          next
        end
      end

      if fd
        begin
          f = IO.open(fd)
          # provide a writable interface for the caller
          yield f
          f.fsync
          File.link tmp_path, new_path
          stored = true
        ensure
          File.unlink tmp_path if File.exists? tmp_path
        end
      end

      if stored and info
        cur_path = File.join(dir, 'cur', new_fn + ':' + info)
        File.rename(new_path, cur_path)
      end
    end # Dir.chdir

    return stored
  end

  def find_mails(dir, idx)
    dir_paths = []
    ['cur', 'new'].each do |d|
      subdir = File.join(dir, d)
      raise "#{subdir} not a directory" unless File.directory? subdir
      Dir[File.join(subdir, '*')].each do |fn|
        File.open(fn) do |f|
          mail = RMail::Parser.read(f)
          cache_index = mail.header['Message-ID']
          if cache_index && (cache_index == idx || cache_index == "<#{idx}>")
            dir_paths.push(File.join(d, File.basename(fn)))
          end
        end
      end
    end
    return dir_paths
  end

  def guarantee_maildir(dir)
    # Ensure maildir-folderness
    ['new', 'cur', 'tmp'].each do |d|
      FileUtils.mkdir_p(File.join(dir, d))
    end
  end

  def maildir_file_info(file)
    basename = File.basename(file)
    colon = basename.rindex(':')

    return (colon and basename[colon + 1 .. -1])
  end

  # Re-written and no longer shamelessly taken from
  # http://gitorious.org/sup/mainline/blobs/master/lib/sup/maildir.rb
  def new_maildir_basefn(date)
    fn = "#{date.to_i.to_s}.#{@@seq_num.to_s}.#{MYHOSTNAME}"
    @@seq_num += 1
    fn
  end

  def maildir_file_info_flags(fn)
    parts = fn.split(',')
    if parts.size == 1
      ''
    else
      parts.last
    end
  end

end