From 240e829dfccc4e7d634a48d3e1c6b7c9bdd9b7b5 Mon Sep 17 00:00:00 2001 From: Tom Freudenberg Date: Sat, 17 Nov 2018 01:26:27 +0100 Subject: [PATCH 1/5] Use MidiSmtpServer for incoming SMTP Service - allows optional TLS and AUTH for SMTP Clients - SSL cert will be generated temporarily - all auth credentials will be accepted --- lib/mail_catcher.rb | 12 ++++--- lib/mail_catcher/smtp.rb | 67 ++++++++++++++++------------------------ mailcatcher.gemspec | 1 + 3 files changed, 36 insertions(+), 44 deletions(-) diff --git a/lib/mail_catcher.rb b/lib/mail_catcher.rb index 30e27dfb..90b923c0 100644 --- a/lib/mail_catcher.rb +++ b/lib/mail_catcher.rb @@ -1,6 +1,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" @@ -12,6 +13,8 @@ require "optparse" require "rbconfig" + +require "midi-smtp-server" require "eventmachine" require "thin" @@ -188,9 +191,9 @@ 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 + Smtp.new(options[:smtp_port], options[:smtp_ip], 4, { logger_severity: development? ? 0:9 }).start puts "==> #{smtp_url}" end @@ -241,8 +244,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 00e2209e..a9725435 100644 --- a/lib/mail_catcher/smtp.rb +++ b/lib/mail_catcher/smtp.rb @@ -1,55 +1,42 @@ -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[:tls_mode] = :TLS_OPTIONAL + opts[:auth_mode] = :AUTH_OPTIONAL + opts[:pipelining_extension] = 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 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 eef45ab1..dbdbac17 100644 --- a/mailcatcher.gemspec +++ b/mailcatcher.gemspec @@ -31,6 +31,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" From 94344178dc528eafc94fb53b35a2b68c30a6d4d7 Mon Sep 17 00:00:00 2001 From: Tom Freudenberg Date: Sun, 18 Nov 2018 01:14:23 +0100 Subject: [PATCH 2/5] Change Process.daemon with Process.detach --- lib/mail_catcher.rb | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/mail_catcher.rb b/lib/mail_catcher.rb index 90b923c0..4c328e85 100644 --- a/lib/mail_catcher.rb +++ b/lib/mail_catcher.rb @@ -179,6 +179,14 @@ def run! options=nil # Stash them away for later @@options = options + # daemonize mode, reap the child automatically to prevent zombie + if options[:daemon] && pid = fork + Process.detach(pid) + # wait a second to make sure that all output is send before exit + sleep 2 + exit + end + # If we're running in the foreground sync the output. unless options[:daemon] $stdout.sync = $stderr.sync = true @@ -219,7 +227,10 @@ 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' end end end From f840d4bc392d6e4ba7cd46a4a2ca86722197465c Mon Sep 17 00:00:00 2001 From: Tom Freudenberg Date: Tue, 27 Nov 2018 05:13:24 +0100 Subject: [PATCH 3/5] Remove sleep but replace process.daemon by process.detach --- lib/mail_catcher.rb | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/lib/mail_catcher.rb b/lib/mail_catcher.rb index 4c328e85..82dab5b2 100644 --- a/lib/mail_catcher.rb +++ b/lib/mail_catcher.rb @@ -179,14 +179,6 @@ def run! options=nil # Stash them away for later @@options = options - # daemonize mode, reap the child automatically to prevent zombie - if options[:daemon] && pid = fork - Process.detach(pid) - # wait a second to make sure that all output is send before exit - sleep 2 - exit - end - # If we're running in the foreground sync the output. unless options[:daemon] $stdout.sync = $stderr.sync = true @@ -231,6 +223,11 @@ def run! options=nil $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 From 43fd4f2e6495c8765c3cc5f7138b0a148ba24d2f Mon Sep 17 00:00:00 2001 From: Tom Freudenberg Date: Thu, 29 Nov 2018 12:26:10 +0100 Subject: [PATCH 4/5] Allow optional STARTTLS with option --smtp-tls --- lib/mail_catcher.rb | 9 ++++++++- lib/mail_catcher/smtp.rb | 1 - 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/mail_catcher.rb b/lib/mail_catcher.rb index 82dab5b2..47af2cad 100644 --- a/lib/mail_catcher.rb +++ b/lib/mail_catcher.rb @@ -121,6 +121,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 @@ -193,7 +197,10 @@ def run! options=nil EventMachine.run do # Set up MidiSmtpServer server to run within EventMachine loop rescue_port options[:smtp_port] do - Smtp.new(options[:smtp_port], options[:smtp_ip], 4, { logger_severity: development? ? 0:9 }).start + 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 diff --git a/lib/mail_catcher/smtp.rb b/lib/mail_catcher/smtp.rb index a9725435..34b20ee0 100644 --- a/lib/mail_catcher/smtp.rb +++ b/lib/mail_catcher/smtp.rb @@ -7,7 +7,6 @@ class MailCatcher::Smtp < MidiSmtpServer::Smtpd 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[:tls_mode] = :TLS_OPTIONAL opts[:auth_mode] = :AUTH_OPTIONAL opts[:pipelining_extension] = true # initialize MidiSmtpServer::Smtpd From af09d897ac03fbb55bcbe9b86700a72003fd9b42 Mon Sep 17 00:00:00 2001 From: Tom Freudenberg Date: Fri, 6 Dec 2019 11:40:29 +0100 Subject: [PATCH 5/5] Enable internationalization options from MidiSmtpServer by default --- lib/mail_catcher/smtp.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/mail_catcher/smtp.rb b/lib/mail_catcher/smtp.rb index 34b20ee0..7997718d 100644 --- a/lib/mail_catcher/smtp.rb +++ b/lib/mail_catcher/smtp.rb @@ -9,6 +9,7 @@ def initialize(ports = DEFAULT_SMTPD_PORT, hosts = DEFAULT_SMTPD_HOST, max_proce 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