1
0
مراية لـ https://github.com/postalserver/postal.git تم المزامنة 2026-04-22 06:35:43 +00:00

initial commit from appmail

هذا الالتزام موجود في:
Adam Cooke
2017-04-19 13:07:25 +01:00
الأصل a3eff53792
التزام 2fdba0ceb5
474 ملفات معدلة مع 51228 إضافات و0 حذوفات

عرض الملف

@@ -0,0 +1,435 @@
require 'nifty/utils/random_string'
module Postal
module SMTPServer
class Client
CRAM_MD5_DIGEST = OpenSSL::Digest.new('md5')
attr_reader :logging_enabled
def initialize(ip_address)
@logging_enabled = true
@ip_address = ip_address
if @ip_address
check_ip_address
@state = :welcome
else
@state = :preauth
end
reset
end
def check_ip_address
if @ip_address && Postal.config.smtp_server.log_exclude_ips && @ip_address =~ Regexp.new(Postal.config.smtp_server.log_exclude_ips)
@logging_enabled = false
end
end
def transaction_reset
@recipients = []
@mail_from = nil
@data = nil
@headers = nil
end
def reset
@credential = nil
transaction_reset
end
def id
@id ||= Nifty::Utils::RandomString.generate(:length => 6).upcase
end
def handle(data)
if @state == :preauth
proxy(data)
else
if @proc
log "\e[32m<= #{data.strip}\e[0m"
@proc.call(data)
else
log "\e[32m<= #{data.strip}\e[0m"
handle_command(data)
end
end
end
def finished?
@finished || false
end
def start_tls?
@start_tls || false
end
def start_tls=(value)
@start_tls = value
end
def handle_command(data)
case data
when /^QUIT/i then quit
when /^STARTTLS/i then starttls
when /^EHLO/i then ehlo(data)
when /^HELO/i then helo(data)
when /^RSET/i then rset
when /^NOOP/i then noop
when /^AUTH PLAIN/i then auth_plain(data)
when /^AUTH LOGIN/i then auth_login(data)
when /^AUTH CRAM-MD5/i then auth_cram_md5(data)
when /^MAIL FROM/i then mail_from(data)
when /^RCPT TO/i then rcpt_to(data)
when /^DATA/i then data(data)
else
'502 Invalid/unsupported command'
end
end
def log(text)
return false unless @logging_enabled
Postal.logger_for(:smtp_server).debug "[#{id}] #{text}"
end
private
def resolve_hostname
@hostname = Resolv.new.getname(@ip_address) rescue @ip_address
end
def proxy(data)
if m = data.match(/\APROXY (.+) (.+) (.+) (.+) (.+)\z/)
@ip_address = m[2]
check_ip_address
@state = :welcome
log "\e[35m Client identified as #{@ip_address}\e[0m"
"220 #{Postal.config.dns.smtp_server_hostname} ESMTP Postal/#{id}"
else
@finished = true
'502 Proxy Error'
end
end
def quit
@finished = true
"221 Closing Connection"
end
def starttls
@start_tls = true
@tls = true
"220 Ready to start TLS"
end
def ehlo(data)
resolve_hostname
@helo_name = data.strip.split(' ', 2)[1]
reset
@state = :welcomed
["250-My capabilities are", @tls ? nil : "250-STARTTLS", "250 AUTH CRAM-MD5 PLAIN LOGIN", ]
end
def helo(data)
resolve_hostname
@helo_name = data.strip.split(' ', 2)[1]
reset
@state = :welcomed
"250 #{Postal.config.dns.smtp_server_hostname}"
end
def rset
reset
@state = :welcomed
'250 OK'
end
def noop
'250 OK'
end
def auth_plain(data)
handler = Proc.new do |data|
@proc = nil
data = Base64.decode64(data)
parts = data.split("\0")
username, password = parts[-2], parts[-1]
unless username && password
next '535 Authenticated failed - protocol error'
end
authenticate(password)
end
data = data.gsub(/AUTH PLAIN ?/i, '')
if data.strip == ''
@proc = handler
'334'
else
handler.call(data)
end
end
def auth_login(data)
password_handler = Proc.new do |data|
@proc = nil
password = Base64.decode64(data)
authenticate(password)
end
username_handler = Proc.new do |data|
@proc = password_handler
'334 UGFzc3dvcmQ6'
end
data = data.gsub!(/AUTH LOGIN ?/i, '')
if data.strip == ''
@proc = username_handler
'334 VXNlcm5hbWU6'
end
end
def authenticate(password)
if @credential = Credential.where(:type => 'SMTP', :key => password).first
@credential.use
"235 Granted for #{@credential.server.organization.permalink}/#{@credential.server.permalink}"
else
"535 Invalid credential"
end
end
def auth_cram_md5(data)
challenge = Digest::SHA1.hexdigest(Time.now.to_i.to_s + rand(100000).to_s)
challenge = "<#{challenge[0,20]}@#{Postal.config.dns.smtp_server_hostname}>"
handler = Proc.new do |data|
@proc = nil
username, password = Base64.decode64(data).split(' ', 2).map{ |a| a.chomp }
org_permlink, server_permalink = username.split(/[\/\_]/, 2)
server = ::Server.includes(:organization).where(:organizations => {:permalink => org_permlink}, :permalink => server_permalink).first
next '535 Denied' if server.nil?
grant = nil
server.credentials.where(:type => 'SMTP').each do |credential|
correct_response = OpenSSL::HMAC.hexdigest(CRAM_MD5_DIGEST, credential.key, challenge)
if password == correct_response
@credential = credential
@credential.use
grant = "235 Granted for #{credential.server.organization.permalink}/#{credential.server.permalink}"
break
end
end
grant || '535 Denied'
end
@proc = handler
"334 " + Base64.encode64(challenge).gsub(/[\r\n]/, '')
end
def mail_from(data)
unless in_state(:welcomed, :mail_from_received)
return '503 EHLO/HELO first please'
end
@state = :mail_from_received
transaction_reset
@mail_from = data.gsub(/MAIL FROM\s*:\s*/i, '').gsub(/.*</, '').gsub(/>.*/, '').strip
'250 OK'
end
def rcpt_to(data)
unless in_state(:mail_from_received, :rcpt_to_received)
return '503 EHLO/HELO and MAIL FROM first please'
end
rcpt_to = data.gsub(/RCPT TO\s*:\s*/i, '').gsub(/.*</, '').gsub(/>.*/, '').strip
uname, domain = rcpt_to.split('@', 2)
uname, tag = uname.split('+', 2)
if domain =~ /\A#{Regexp.escape(Postal.config.dns.custom_return_path_prefix)}\./
# This is a return path
@state = :rcpt_to_received
if server = ::Server.where(:token => uname).first
if server.suspended?
'535 Mail server has been suspended'
else
log "Added bounce on server #{server.id}"
@recipients << [:bounce, rcpt_to, server]
'250 OK'
end
else
'550 Invalid server token'
end
elsif domain == Postal.config.dns.route_domain
# This is an email direct to a route. This isn't actually supported yet.
@state = :rcpt_to_received
if route = Route.where(:token => uname).first
if route.server.suspended?
'535 Mail server has been suspended'
elsif route.mode == 'Reject'
'550 Route does not accept incoming messages'
else
log "Added route #{route.id} to recipients (tag: #{tag.inspect})"
actual_rcpt_to = "#{route.name}" + (tag ? "+#{tag}" : "") + "@#{route.domain.name}"
@recipients << [:route, actual_rcpt_to, route.server, :route => route]
'250 OK'
end
else
'550 Invalid route token'
end
elsif @credential
# This is outgoing mail for an authenticated user
@state = :rcpt_to_received
if @credential.server.suspended?
'535 Mail server has been suspended'
else
log "Added external address '#{rcpt_to}'"
@recipients << [:credential, rcpt_to, @credential.server]
'250 OK'
end
elsif uname && domain && route = Route.find_by_name_and_domain(uname, domain)
# This is incoming mail for a route
@state = :rcpt_to_received
if route.server.suspended?
'535 Mail server has been suspended'
elsif route.mode == 'Reject'
'550 Route does not accept incoming messages'
else
log "Added route #{route.id} to recipients (tag: #{tag.inspect})"
@recipients << [:route, rcpt_to, route.server, :route => route]
'250 OK'
end
else
# This is unaccepted mail
'530 Authentication required'
end
end
def data(data)
unless in_state(:rcpt_to_received)
return '503 HELO/EHLO, MAIL FROM and RCPT TO before sending data'
end
@data = "".force_encoding("BINARY")
@headers = {}
@receiving_headers = true
received_header_content = "from #{@helo_name} (#{@hostname} [#{@ip_address}]) by #{Postal.config.dns.smtp_server_hostname} with SMTP; #{Time.now.rfc2822.to_s}".force_encoding('BINARY')
@data << "Received: #{received_header_content}\r\n"
@headers['received'] = [received_header_content]
handler = Proc.new do |data|
if data == '.'
@logging_enabled = true
@proc = nil
finished
else
data = data.to_s.sub(/\A\.\./, '.')
if @credential && @credential.server.log_smtp_data?
# We want to log if enabled
else
log "Not logging further message data."
@logging_enabled = false
end
if @receiving_headers
if data.blank?
@receiving_headers = false
elsif data.to_s =~ /^\s/
# This is a continuation of a header
if @header_key && @headers[@header_key.downcase] && @headers[@header_key.downcase].last
@headers[@header_key.downcase].last << data.to_s
end
else
@header_key, value = data.split(/\:\s*/, 2)
@headers[@header_key.downcase] ||= []
@headers[@header_key.downcase] << value
end
end
@data << data
@data << "\r\n"
nil
end
end
@proc = handler
'354 Go ahead'
end
def finished
if @data.bytesize > 14.megabytes.to_i
return "552 Message too large (maximum size 14MB)"
end
if @headers['received'].select { |r| r =~ /by #{Postal.config.dns.smtp_server_hostname}/ }.count > 4
return '550 Loop detected'
end
authenticated_domain = nil
if @credential
authenticated_domain = @credential.server.find_authenticated_domain_from_headers(@headers)
if authenticated_domain.nil?
return '530 From/Sender name is not valid'
end
end
@recipients.each do |recipient|
type, rcpt_to, server, options = recipient
case type
when :credential
# Outgoing messages are just inserted
message = server.message_db.new_message
message.rcpt_to = rcpt_to
message.mail_from = @mail_from
message.raw_message = @data
message.received_with_ssl = @tls
message.scope = 'outgoing'
message.domain_id = authenticated_domain&.id
message.credential_id = @credential.id
message.save
when :bounce
if rp_route = server.routes.where(:name => "__returnpath__").first
# If there's a return path route, we can use this to create the message
rp_route.create_messages do |message|
message.rcpt_to = rcpt_to
message.mail_from = @mail_from
message.raw_message = @data
message.received_with_ssl = @tls
end
else
# There's no return path route, we just need to insert the mesage
# without going through the route.
message = server.message_db.new_message
message.rcpt_to = rcpt_to
message.mail_from = @mail_from
message.raw_message = @data
message.received_with_ssl = @tls
message.scope = 'incoming'
message.bounce = 1
message.save
end
when :route
options[:route].create_messages do |message|
message.rcpt_to = rcpt_to
message.mail_from = @mail_from
message.raw_message = @data
message.received_with_ssl = @tls
end
end
end
transaction_reset
'250 OK'
end
def in_state(*states)
states.include?(@state)
end
end
end
end

عرض الملف

@@ -0,0 +1,322 @@
require 'ipaddr'
require 'epoll' if RUBY_PLATFORM.include?('linux')
module Postal
module SMTPServer
class Server
def initialize(options = {})
@options = options
@options[:ports] ||= Postal.config.smtp_server.ports
@options[:debug] ||= false
prepare_environment
end
def prepare_environment
$\ = "\r\n"
BasicSocket.do_not_reverse_lookup = true
trap("USR1") do
STDOUT.puts "Received USR1 signal, respawning."
fork do
if ENV['APP_ROOT']
Dir.chdir(ENV['APP_ROOT'])
end
ENV.delete('BUNDLE_GEMFILE')
exec("bundle exec --keep-file-descriptors rake postal:smtp_server", :close_others => false)
end
end
trap("TERM") do
STDOUT.puts "Received TERM signal, shutting down."
unlisten
end
end
def ssl_context
@ssl_context ||= begin
ssl_context = OpenSSL::SSL::SSLContext.new
certs = Postal.ssl_certificates
ssl_context.cert = certs.shift
ssl_context.extra_chain_cert = certs
ssl_context.key = Postal.signing_key
ssl_context.ssl_version = "SSLv23"
ssl_context
end
end
def listen
if ENV['SERVER_FD']
@server = TCPServer.for_fd(ENV['SERVER_FD'].to_i)
else
@server = TCPServer.open('::', @options[:ports].first)
end
@server.autoclose = false
@server.close_on_exec = false
if defined?(Socket::SOL_SOCKET) && defined?(Socket::SO_KEEPALIVE)
@server.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
end
if defined?(Socket::SOL_TCP) && defined?(Socket::TCP_KEEPIDLE) && defined?(Socket::TCP_KEEPINTVL) && defined?(Socket::TCP_KEEPCNT)
@server.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPIDLE, 50)
@server.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPINTVL, 10)
@server.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPCNT, 5)
end
ENV['SERVER_FD'] = @server.to_i.to_s
end
def unlisten
if @epoll
@epoll.del(@server)
if @epoll.size == 0
Process.exit(0)
end
end
@server.close
end
def kill_parent
Process.kill('TERM', Process.ppid)
end
def run_linux
if ENV['SERVER_FD']
listen
kill_parent
else
listen
end
@epoll = Epoll.create
logger.info "Listening"
@epoll.add(@server, Epoll::IN)
buffers = Hash.new { |h, k| h[k] = String.new.force_encoding('BINARY') }
clients = {}
loop do
evlist = @epoll.wait
evlist.each do |ev|
io = ev.data
if io.is_a?(TCPServer)
begin
new_io = io.accept
if Postal.config.smtp_server.proxy_protocol
client = Client.new(nil)
if Postal.config.smtp_server.log_connect
logger.debug "[#{client.id}] \e[35m Connection opened from #{new_io.remote_address.ip_address}\e[0m"
end
else
client = Client.new(new_io.remote_address.ip_address)
if Postal.config.smtp_server.log_connect
logger.debug "[#{client.id}] \e[35m Connection opened from #{new_io.remote_address.ip_address}\e[0m"
end
client.log "\e[35m Client identified as #{new_io.remote_address.ip_address}\e[0m"
new_io.print("220 #{Postal.config.dns.smtp_server_hostname} ESMTP Postal/#{client.id}")
end
clients[new_io] = client
@epoll.add(new_io, Epoll::IN|Epoll::PRI|Epoll::HUP)
rescue => e
Raven.capture_exception(e, :extra => {:log_id => (client.id rescue nil)})
logger.error "An error occurred while accepting a new client."
logger.error "#{e.class}: #{e.message}"
e.backtrace.each do |line|
logger.error line
end
new_io.close rescue nil
end
else
begin
client = clients[io]
eof = false
begin
case io
when OpenSSL::SSL::SSLSocket
buffers[io] << io.readpartial(10240)
while(io.pending > 0)
buffers[io] << io.readpartial(10240)
end
else
buffers[io] << io.readpartial(10240)
end
rescue EOFError, Errno::ECONNRESET
# Client went away
eof = true
end
while buffers[io].index("\n")
if buffers[io].index("\r\n")
line, buffers[io] = buffers[io].split("\r\n", 2)
else
line, buffers[io] = buffers[io].split("\n", 2)
end
result = client.handle(line)
unless result.nil?
result = [result] unless result.is_a?(Array)
result.compact.each do |line|
client.log "\e[34m=> #{line.strip}\e[0m"
begin
io.write(line.to_s + "\r\n")
io.flush
rescue Errno::ECONNRESET
# Client disconnected before we could write response
eof = true
end
end
end
end
if !eof && client.start_tls?
client.start_tls = false
@epoll.del(io)
clients.delete(io)
buffers.delete(io)
tcp_io = io
io = OpenSSL::SSL::SSLSocket.new(io, ssl_context)
@epoll.add(io, Epoll::IN)
clients[io] = client
io.sync_close = true
begin
io.accept
rescue OpenSSL::SSL::SSLError => e
client.log "SSL Negotiation Failed: #{e.message}"
eof = true
end
end
if client.finished? || eof
client.log "\e[35m Connection closed\e[0m"
@epoll.del(io)
clients.delete(io)
buffers.delete(io)
io.close
if @epoll.size == 0
Process.exit(0)
end
end
rescue => e
client_id = client ? client.id : '------'
Raven.capture_exception(e, :extra => {:log_id => (client.id rescue nil)})
logger.error "[#{client_id}] An error occurred while processing data from a client."
logger.error "[#{client_id}] #{e.class}: #{e.message}"
e.backtrace.each do |line|
logger.error "[#{client_id}] #{line}"
end
# Close all IO and forget this client
@epoll.del(io) rescue nil
clients.delete(io)
buffers.delete(io)
io.close rescue nil
if @epoll.size == 0
Process.exit(0)
end
end
end
end
end
end
def run_non_linux
if ENV['SERVER_FD']
listen
kill_parent
else
listen
end
logger.info "Listening"
Thread.abort_on_exception = true
client_threads = []
loop do
s = nil
begin
until s
l = select([@server], [@server], [@server], 0.5)
s = @server.accept if l
end
rescue IOError
STDERR.puts "Server socket was closed."
break
end
client_threads << Thread.new(s) do |io|
begin
if Postal.config.smtp_server.proxy_protocol
client = Client.new(nil)
if Postal.config.smtp_server.log_connect
logger.debug "[#{client.id}] \e[35m Connection opened from #{io.remote_address.ip_address}\e[0m"
end
else
client = Client.new(io.remote_address.ip_address)
if Postal.config.smtp_server.log_connect
logger.debug "[#{client.id}] \e[35m Connection opened from #{io.remote_address.ip_address}\e[0m"
end
client.log "\e[35m Client identified as #{io.remote_address.ip_address}\e[0m"
io.print("220 #{Postal.config.dns.smtp_server_hostname} ESMTP Postal/#{client.id}")
end
loop do
if received_data = io.gets
if result = client.handle(received_data.chomp)
result = [result] unless result.is_a?(Array)
result.compact.each do |line|
client.log "\e[34m=> #{line.strip}\e[0m"
io.write(line.to_s + "\r\n")
io.flush
end
end
end
if client.start_tls?
client.start_tls = false
tcp_io = io
io = OpenSSL::SSL::SSLSocket.new(io, ssl_context)
io.sync_close = true
begin
io.accept
rescue OpenSSL::SSL::SSLError => e
logger.error "SSL Negotiation Failed: #{e.message}"
io.close rescue nil
tcp_io.close rescue nil
eof = true
end
end
if received_data.nil? || client.finished?
client.log "\e[35m Connection closed\e[0m"
io.close
break
end
end
rescue => e
Raven.capture_exception(e, :extra => {:log_id => (client.id rescue nil)})
logger.error "An error occurred while handling a client."
logger.error "#{e.class}: #{e.message}"
e.backtrace.each do |line|
logger.error line
end
# Close all IO
io.close rescue nil
ensure
client_threads.delete(Thread.current)
end
end
end
client_threads.each{ |t| t.join unless t == Thread.current }
end
def run
if ENV['PID_FILE']
File.open(ENV['PID_FILE'], 'w') { |f| f.write(Process.pid.to_s + "\n") }
end
if Postal.config.smtp_server&.evented
logger.info "Running epoll driven server for Linux host.."
run_linux
else
logger.info "Running thread based compatibility server for non-Linux host."
run_non_linux
end
end
private
def logger
Postal.logger_for(:smtp_server)
end
end
end
end