diff --git a/lib/mail_catcher.rb b/lib/mail_catcher.rb index fb289732..16c58ca2 100644 --- a/lib/mail_catcher.rb +++ b/lib/mail_catcher.rb @@ -3,6 +3,7 @@ # Apparently rubygems won't activate these on its own, so here we go. Let's # repeat the invention of Bundler all over again. gem "eventmachine", "1.0.9.1" +gem "midi-smtp-server", "~> 2.3.1" gem "mail", "~> 2.3" gem "rack", "~> 1.5" gem "sinatra", "~> 1.2" @@ -14,6 +15,8 @@ require "optparse" require "rbconfig" + +require "midi-smtp-server" require "eventmachine" require "thin" @@ -121,6 +124,10 @@ def parse! arguments=ARGV, defaults=@defaults options[:smtp_port] = port end + parser.on("--smtp-tls", "Enable tls for the smtp server") do + options[:smtp_tls] = true + end + parser.on("--http-ip IP", "Set the ip address of the http server") do |ip| options[:http_ip] = ip end @@ -195,9 +202,12 @@ def run! options=nil # One EventMachine loop... EventMachine.run do - # Set up an SMTP server to run within EventMachine + # Set up MidiSmtpServer server to run within EventMachine loop rescue_port options[:smtp_port] do - EventMachine.start_server options[:smtp_ip], options[:smtp_port], Smtp + midi_smtp_server_opts = {} + midi_smtp_server_opts[:tls_mode] = :TLS_OPTIONAL if options[:smtp_tls] + midi_smtp_server_opts[:logger_severity] = development? ? 0:9 + Smtp.new(options[:smtp_port], options[:smtp_ip], 4, midi_smtp_server_opts).start puts "==> #{smtp_url}" end @@ -223,7 +233,15 @@ def run! options=nil else puts "*** MailCatcher is now running as a daemon that cannot be quit." end - Process.daemon + # when daemonize redirect standard input, standard output and standard error to /dev/null + $stdin.reopen "/dev/null" + $stdout.reopen "/dev/null", "a" + $stderr.reopen '/dev/null', 'a' + # daemonize mode, reap the child automatically to prevent zombie + if pid = fork + Process.detach(pid) + exit + end end end end @@ -248,8 +266,9 @@ def rescue_port port yield # XXX: EventMachine only spits out RuntimeError with a string description - rescue RuntimeError - if $!.to_s =~ /\bno acceptor\b/ + # XXX: MidiSmtpServer uses seperate ErrorClass + rescue RuntimeError, Errno::EADDRINUSE + if $!.to_s =~ /\bno acceptor\b/ || $!.class == Errno::EADDRINUSE puts "~~> ERROR: Something's using port #{port}. Are you already running MailCatcher?" puts "==> #{smtp_url}" puts "==> #{http_url}" diff --git a/lib/mail_catcher/smtp.rb b/lib/mail_catcher/smtp.rb index cbaf8bc5..b74a2f0a 100644 --- a/lib/mail_catcher/smtp.rb +++ b/lib/mail_catcher/smtp.rb @@ -1,58 +1,45 @@ # frozen_string_literal: true -require "eventmachine" +require "midi-smtp-server" require "mail_catcher/mail" -class MailCatcher::Smtp < EventMachine::Protocols::SmtpServer - # We override EM's mail from processing to allow multiple mail-from commands - # per [RFC 2821](http://tools.ietf.org/html/rfc2821#section-4.1.1.2) - def process_mail_from sender - if @state.include? :mail_from - @state -= [:mail_from, :rcpt, :data] - receive_reset - end - - super - end - - def current_message - @current_message ||= {} - end +class MailCatcher::Smtp < MidiSmtpServer::Smtpd - def receive_reset - @current_message = nil - true + def initialize(ports = DEFAULT_SMTPD_PORT, hosts = DEFAULT_SMTPD_HOST, max_processings = DEFAULT_SMTPD_MAX_PROCESSINGS, opts = {}) + # set compatible modes and enable optional TLS and AUTH per default + opts[:io_cmd_timeout] = nil + opts[:auth_mode] = :AUTH_OPTIONAL + opts[:pipelining_extension] = true + opts[:internationalization_extensions] = true + # initialize MidiSmtpServer::Smtpd + super ports, hosts, max_processings, opts end - def receive_sender(sender) - current_message[:sender] = sender - true - end - - def receive_recipient(recipient) - current_message[:recipients] ||= [] - current_message[:recipients] << recipient - true - end - - def receive_data_chunk(lines) - current_message[:source] ||= +"" - lines.each do |line| - current_message[:source] << line << "\r\n" + def on_auth_event(ctx, authorization_id, authentication_id, authentication) + # simply allow any combination of user and password + if authentication_id != '' && authentication != '' + # simulate successful authentification + return 'mailcatcher' end - true + # otherwise exit with authentification exception + raise MidiSmtpServer::Smtpd535Exception end - def receive_message + def on_message_data_event(ctx) + # build current_message from ctx values + current_message = {} + current_message[:sender] = ctx[:envelope][:from] + current_message[:recipients] = ctx[:envelope][:to] + current_message[:source] = ctx[:message][:data] + # append to MailCatcher MailCatcher::Mail.add_message current_message MailCatcher::Mail.delete_older_messages! puts "==> SMTP: Received message from '#{current_message[:sender]}' (#{current_message[:source].length} bytes)" - true rescue => exception MailCatcher.log_exception("Error receiving message", @current_message, exception) - false - ensure - @current_message = nil + # re-raise to signal error to smtp client + raise end + end diff --git a/mailcatcher.gemspec b/mailcatcher.gemspec index 0e896941..e9452f28 100644 --- a/mailcatcher.gemspec +++ b/mailcatcher.gemspec @@ -33,6 +33,7 @@ Gem::Specification.new do |s| s.required_ruby_version = ">= 2.0.0" s.add_dependency "eventmachine", "1.0.9.1" + s.add_dependency "midi-smtp-server", "~> 2.3.1" s.add_dependency "mail", "~> 2.3" s.add_dependency "rack", "~> 1.5" s.add_dependency "sinatra", "~> 1.2"