Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
hermes / bin / hermesmail
Size: Mime:
#!/usr/bin/env ruby

#
#  hermesmail  --  Mail filtering and delivery
#

begin
  require "appl"
rescue LoadError
  raise "This requires the Gem 'appl'."
end

require "hermes/version"
require "hermes/transports"
require "hermes/cli/pop"


module Hermes

  class Processed < Mail

    attr_accessor :debug

    class Done < Exception ; end

    # Do nothing, just finish.
    def done
      raise Done
    end

    alias delete done

    # Save in a local mailbox
    def deposit mailbox = nil
      save mailbox
      done
    end

    # Forward by SMTP
    def forward_smtp to
      send nil, to
      done
    end

    # Forward by sendmail
    def forward_sendmail to
      sendmail to
      done
    end
    alias forward forward_smtp


    self.logfile  = "hermesmail.log"
    self.loglevel = :ERR

    @failed_process = "=failed-process"

    class <<self
      attr_accessor :failed_process
      def process input, debug = false
        i = parse input
        i.debug = debug
        i.execute
      rescue
        raise if debug
        open_failed { |f|
          log_exception "Error while parsing mail", f.path
          f.write input
        }
      end
      def log_exception msg, *args
        log :ERR, "#{msg}: #$! (#{$!.class})", *args
        $!.backtrace.each { |b| log :INF, "    #{b}" }
      end
      private
      def open_failed
        i = 0
        d = expand_sysdir
        w = Time.now.strftime "%Y%m%d%H%M%S"
        begin
          p = File.join d, "failed-#{w}-%05d" % i
          File.open p, File::CREAT|File::EXCL|File::WRONLY do |f|
            yield f
          end
        rescue Errno::ENOENT
          Dir.mkdir! d and retry
        rescue Errno::EEXIST
          i +=1
          retry
        end
      end
    end

    def execute
      process
      save
    rescue Done
    rescue
      raise if @debug
      log_exception "Error while processing mail"
      b = cls.box cls.failed_process
      save b
    end

    def log_exception msg, *args
      cls.log_exception msg, *args
    end

  end


  class Fetch

    class <<self
      private :new
      def create *args, &block
        i = new
        i.instance_eval *args, &block
        def i.each
          @list.each { |a|
            c = a[ :type].new *a[ :args]
            a[ :logins].each { |l|
              c.login *l do yield c end
            }
          }
        end
        i
      end
    end

    def initialize
      @list = []
    end

    def pop  *args ; access Cli::Pop,  *args do yield end ; end
    def pops *args ; access Cli::Pops, *args do yield end ; end

    def login *args
      @access[ :logins].push args
      nil
    end

    private

    def access type, *args
      @access and raise "Access methods must not be nested."
      @access = { type: type, args: args, logins: [] }
      yield
      @list.push @access
      nil
    ensure
      @access = nil
    end

  end


  class MailApp < Application

    NAME      = "hermesmail"
    VERSION   = Hermes::VERSION
    SUMMARY   = "A mail delivery agent written in Ruby"
    COPYRIGHT = Hermes::COPYRIGHT
    LICENSE   = Hermes::LICENSE
    AUTHORS   = Hermes::AUTHORS

    DESCRIPTION = <<-EOT
This mail delivery agent (MDA) reads a configuration file
that is plain Ruby code. See the examples section for how
to write one.
    EOT

    attr_accessor :rulesfile, :mbox, :fetchfile
    attr_bang :debug, :fetch, :keep
    def quiet! ; @quiet += 1 ; end

    def initialize *args
      @quiet = 0
      super
      $*.replace @args
    end

    RULESFILE = "~/.hermesmail-rules"
    FETCHFILE = "~/.hermesmail-fetch"

    define_option "r", :rulesfile=, "NAME", RULESFILE, "filtering rules"
    alias_option  "r", "rulesfile"

    define_option "M", :mbox=, "MBOX",
                         "process all in MBOX instead of one from stdin"
    alias_option  "M", "mbox"

    define_option "f", :fetch!,                "fetch from a POP server"
    alias_option  "f", "fetch"

    define_option "F", :fetchfile=, "FILE", FETCHFILE,
                         "a PGP-encrypted file containing fetch methods"
    alias_option  "F", "fetchfile"
    alias_option  "F", "fetch-file"

    define_option "k", :keep!,    "don't delete the mails on the server"
    alias_option  "k", "keep"

    define_option "q", :quiet!,
                     "less output (once = no progress, twice = nothing)"
    alias_option  "q", "quiet"

    define_option "g", :debug!,               "full Ruby error messages"
    alias_option  "g", "debug"

    define_option "h", :help,                             "show options"
    alias_option  "h", "help"
    define_option "V", :version,                          "show version"
    alias_option  "V", "version"

    def run
      Processed.class_eval read_rules
      if @mbox and @fetch then
        raise "Specify either mbox or fetch but not both."
      end
      if @mbox then
        b = Box.find @mbox
        b.each { |m| Processed.process m }
      elsif @fetch then
        read_fetches.each { |s|
          c = s.count
          puts "#{c} Mails in #{s.name}." if @quiet < 2
          i = 0
          s.each { |m|
            print "\r#{i}/#{c}  " if @quiet < 1
            i += 1
            Processed.process m
            raise Cli::Pop::Keep if @keep
          }
          puts "\rDone.     " if @quiet < 1
        }
      else
        msg = $<.read
        msg.force_encoding Encoding::ASCII_8BIT
        Processed.process msg, @debug
      end
    end

    private

    def read_rules
      r = File.expand_path @rulesfile
      File.read r
    end

    def read_fetches
      p = File.expand_path @fetchfile
      Fetch.create `gpg -d #{p}`
    end

  end

  MailApp.run

end