#!/usr/bin/env ruby ########################################################### # License ########################################################### # Copyright (c) 2007 Ari Johnson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ########################################################### ########################################################### # Configuration ########################################################### $DEFAULT_POSTCONF = '/usr/sbin/postconf' $DEFAULT_POSTMAP = '/usr/sbin/postmap' $DEFAULT_LOGFILE = 'delivery.log' require 'optparse' require 'rfilter/delivery_agent' ########################################################### # Utility functions ########################################################### def parse_command_line(args) options = Hash.new options['postconf'] = $DEFAULT_POSTCONF options['postmap'] = $DEFAULT_POSTMAP options['logfile'] = $DEFAULT_LOGFILE options['address'] = nil options['script'] = nil parser = OptionParser.new parser.banner = "Usage: #{File.basename($0)} [options]" parser.separator "" parser.separator "Specific options:" parser.on("-l", "--logfile [FILENAME]", "Save the delivery log to FILENAME") do |logfile| options['logfile'] = logfile end parser.on("-p", "--postconf [PATH]", "Use the given 'postconf' utility") do |postconf| options['postconf'] = postconf end parser.on("-m", "--postmap [PATH]", "Use the given 'postmap' utility") do |postmap| options['postmap'] = postmap end parser.on("-d", "--deliver ADDRESS", "Deliver the message to ADDRESS") do |address| options['address'] = address end parser.on("-s", "--script [SCRIPT]", "Deliver the message using SCRIPT") do |script| options['script'] = script end parser.separator "" parser.separator "Common options:" parser.on_tail("-h", "--help", "Show this message") do puts parser exit end parser.parse!(args) return options end def postconf(parameter) return `#{$postconf} '#{parameter}'`.sub(/^[^=]*\s*=\s*(.*)$/, '\1').strip.untaint end def postmap(path, key) return `#{$postmap} -q '#{key.untaint}' '#{path}'`.strip.untaint end def find_map(path) method, path = path.split(':', 2) if method == 'proxy' method, path = path.split(':', 2) end if ! path path = method method = postconf('default_database_type') end return method, path end def read_map(key, paths) mps = Array.new if paths.kind_of? Array paths.each do |path| mps << find_map(path) end else mps << find_map(paths) end maps = Array.new primary_method = nil primary_parameters = Hash.new dbh = nil mps.each do |method, path| if primary_method and method != primary_method maps << read_map(key, "#{method}:#{path}").shift else if ! primary_method primary_method = method case primary_method when 'static' # Do nothing when 'hash' # Do nothing when 'pgsql' require 'dbi' parameters = read_map_file(path) dbh = DBI.connect("DBI:Pg:#{parameters['dbname']}:#{parameters['hosts']}", parameters['user'], parameters['password']) primary_parameters['dbname'] = parameters['dbname'] primary_parameters['hosts'] = parameters['hosts'] primary_parameters['user'] = parameters['user'] primary_parameters['password'] = parameters['password'] else $stderr.puts("method #{primary_method} not supported") exit 1 end else case primary_method when 'static' # Do nothing when 'hash' # Do nothing when 'pgsql' parameters = read_map_file(path) end end case primary_method when 'static' maps << path when 'hash' maps << postmap(path, key) when 'pgsql' if parameters['dbname'] == primary_parameters['dbname'] and parameters['hosts'] == primary_parameters['hosts'] and parameters['user'] == primary_parameters['user'] and parameters['password'] == primary_parameters['password'] if parameters['query'] query = parameters['query'].sub(/%s/, key) else query = "SELECT \"#{parameters['select_field']}\" FROM \"#{parameters['table']}\" WHERE \"#{parameters['where_field']}\" = '#{key}'" end dbh.select_all(query) do |row| if parameters['query'] maps << row.shift else maps << row[parameters['select_field']] end end else maps << read_map(key, "#{method}:#{path}").shift end end end end return maps end def read_map_file(path) lines = IO.readlines(path) if ! lines $stderr.puts("could not read file #{path}") exit 1 end retval = Hash.new lines.each do |line| line.scan(/^([^=]*)\s*=\s*(.*)$/) do |parameter, value| if parameter and value retval[parameter.strip.untaint] = value.strip.untaint end end end return retval end def lookup_account(address) mailbox, uid, gid = read_map(address, [$mailbox_maps, $uid_maps, $gid_maps]) if mailbox and uid and gid return mailbox.untaint, uid.untaint.to_i, gid.untaint.to_i end return nil end def lookup_mailbox(address) localpart, domain = address.split('@', 2) name, ext = localpart.split('+', 2) res = lookup_account(address) if res return res end res = lookup_account("#{name}@#{domain}") if res return res end res = lookup_account("#{name}+#{ext}") if res return res end res = lookup_account("#{name}") if res return res end res = lookup_account("@#{domain}") if res return res end return nil end ########################################################### # Deliver class - handle actual delivery # The default is to deliver to "./", which is where it # would go if Postfix were doing the delivery itself. ########################################################### class Deliver def initialize(agent) @agent = agent end def agent return @agent end def main agent.save("./") end end ########################################################### # Main delivery code ########################################################### # Parse the command line argumnents options = parse_command_line(ARGV) $logfile = options['logfile'] $postconf = options['postconf'] $postmap = options['postmap'] address = options['address'] script = options['script'] # Make sure we were given a delivery address if ! address $stderr.puts("no address supplied - #{File.basename($0)} -h for help") exit 1 end # Load in the necessary postfix configuration mailbox_base = postconf('virtual_mailbox_base') $mailbox_maps = postconf('virtual_mailbox_maps') $uid_maps = postconf('virtual_uid_maps') $gid_maps = postconf('virtual_gid_maps') # Check if the mailbox is in the database res = lookup_mailbox(address.untaint) if ! res $stderr.puts "could not find mailbox" exit 1 end # Split out the mailbox path, uid, and gid from the database mailbox, uid, gid = res mailbox.strip! # Set our effective gid and uid Process.egid = gid Process.euid = uid # Change to the mailbox location Dir.chdir(mailbox_base) Dir.chdir(mailbox) # Read in script if script script_file = File.new(script.untaint) if script_file code = script_file.read.untaint eval(code, Deliver.module_eval("binding"), script) end end # Process and deliver the message begin RFilter::DeliveryAgent.process($stdin, $logfile) do |agent| deliver = Deliver.new(agent) deliver.main end rescue RFilter::DeliveryAgent::DeliveryComplete => exception exit(RFilter::DeliveryAgent.exitcode(exception)) rescue Exception exit(RFilter::MTA::EX_TEMPFAIL) end