From 0ec243c2c6e30df430556a57fe1b472f06332b6c Mon Sep 17 00:00:00 2001 From: Ahmed Date: Mon, 28 Jun 2021 11:33:43 +0530 Subject: [PATCH 01/30] REFACTOR: uses async and family --- lib/mail_catcher.rb | 128 ++++---- lib/mail_catcher/bus.rb | 7 - lib/mail_catcher/mail.rb | 15 +- lib/mail_catcher/message.rb | 18 ++ lib/mail_catcher/smtp.rb | 443 +++++++++++++++++++++++++--- lib/mail_catcher/version.rb | 2 +- lib/mail_catcher/web.rb | 3 +- lib/mail_catcher/web/application.rb | 78 +++-- mailcatcher.gemspec | 8 +- 9 files changed, 555 insertions(+), 147 deletions(-) delete mode 100644 lib/mail_catcher/bus.rb create mode 100644 lib/mail_catcher/message.rb diff --git a/lib/mail_catcher.rb b/lib/mail_catcher.rb index b83b6733..0803afa2 100644 --- a/lib/mail_catcher.rb +++ b/lib/mail_catcher.rb @@ -2,36 +2,40 @@ # 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 'async', '~> 1.25' +gem 'async-http', '~> 0.56.3' +gem 'async-io', '~> 1.32.1' +gem 'async-websocket', '~> 0.19.0' +gem 'falcon', '~> 0.39.1' gem "mail", "~> 2.3" gem "rack", "~> 1.5" gem "sinatra", "~> 1.2" gem "sqlite3", "~> 1.3" -gem "thin", "~> 1.5.0" -gem "skinny", "~> 0.2.3" require "open3" require "optparse" require "rbconfig" +require 'socket' +require 'async/io/address_endpoint' +require 'async/http/endpoint' +require 'async/websocket/adapters/rack' +require 'async/io/shared_endpoint' +require 'mail' +require 'falcon' -require "eventmachine" -require "thin" +require '/Users/ahmedgagan/Rails Gem/mailcatcher/lib/mail_catcher/message' +require '/Users/ahmedgagan/Rails Gem/mailcatcher/lib/mail_catcher/version' -module EventMachine - # Monkey patch fix for 10deb4 - # See https://github.com/eventmachine/eventmachine/issues/569 - def self.reactor_running? - (@reactor_running || false) - end -end - -require "mail_catcher/version" +# require 'mail_catcher/message' +# require 'mail_catcher/version' module MailCatcher extend self - autoload :Bus, "mail_catcher/bus" - autoload :Mail, "mail_catcher/mail" - autoload :Smtp, "mail_catcher/smtp" - autoload :Web, "mail_catcher/web" + autoload :Mail, '/Users/ahmedgagan/Rails Gem/mailcatcher/lib/mail_catcher/mail' + autoload :SMTP, '/Users/ahmedgagan/Rails Gem/mailcatcher/lib/mail_catcher/smtp' + autoload :Web, '/Users/ahmedgagan/Rails Gem/mailcatcher/lib/mail_catcher/web' + # autoload :Mail, "mail_catcher/mail" + # autoload :Smtp, "mail_catcher/smtp" + # autoload :Web, "mail_catcher/web" def env ENV.fetch("MAILCATCHER_ENV", "production") @@ -198,73 +202,77 @@ def run! options=nil puts "Starting MailCatcher" - Thin::Logging.debug = development? - Thin::Logging.silent = !development? + Async.logger.level = Logger::DEBUG if options[:verbose] - # One EventMachine loop... - EventMachine.run do - # Set up an SMTP server to run within EventMachine - rescue_port options[:smtp_port] do - EventMachine.start_server options[:smtp_ip], options[:smtp_port], Smtp - puts "==> #{smtp_url}" - end + Async::Reactor.run do |task| + smtp_address = Async::IO::Address.tcp(options[:smtp_ip], options[:smtp_port]) + smtp_endpoint = Async::IO::AddressEndpoint.new(smtp_address) + smtp_socket = rescue_port(options[:smtp_port]) { smtp_endpoint.bind } + puts "==> #{smtp_url}" - # Let Thin set itself up inside our EventMachine loop - # (Skinny/WebSockets just works on the inside) - rescue_port options[:http_port] do - Thin::Server.start(options[:http_ip], options[:http_port], Web) - puts "==> #{http_url}" + smtp_endpoint = MailCatcher::SMTP::URLEndpoint.new(URI.parse(smtp_url), smtp_endpoint) + smtp_server = MailCatcher::SMTP::Server.new(smtp_endpoint) do |envelope| + MailCatcher::Mail.add_message(sender: envelope.sender, recipients: envelope.recipients, + source: envelope.content) end - # Open the web browser before detatching console - if options[:browse] - EventMachine.next_tick do - browse http_url + smtp_task = task.async do |task| + task.annotate "binding to #{smtp_socket.local_address.inspect}" + + begin + smtp_socket.listen(Socket::SOMAXCONN) + smtp_socket.accept_each(task: task, &smtp_server.method(:accept)) + ensure + smtp_socket.close end end + http_address = Async::IO::Address.tcp(options[:http_ip], options[:http_port]) + http_endpoint = Async::IO::AddressEndpoint.new(http_address) + + http_endpoint = Async::HTTP::Endpoint.parse(http_url) + http_app = Falcon::Server.middleware(Web) + http_server = Falcon::Server.new(http_app, http_endpoint) + + http_server.run.each(&:wait) + browse(http_url) if options[:browse] + # Daemonize, if we should, but only after the servers have started. if options[:daemon] - EventMachine.next_tick do - if quittable? - puts "*** MailCatcher runs as a daemon by default. Go to the web interface to quit." - else - puts "*** MailCatcher is now running as a daemon that cannot be quit." - end - Process.daemon + if quittable? + puts '*** MailCatcher runs as a daemon by default. Go to the web interface to quit.' + else + puts '*** MailCatcher is now running as a daemon that cannot be quit.' end + Process.daemon end end + rescue Interrupt + # Cool story end def quit! - EventMachine.next_tick { EventMachine.stop_event_loop } + Async::Task.current.reactor.stop end -protected + def http_url + "http://#{@@options[:http_ip]}:#{@@options[:http_port]}#{@@options[:http_path]}" + end + + protected def smtp_url "smtp://#{@@options[:smtp_ip]}:#{@@options[:smtp_port]}" end - def http_url - "http://#{@@options[:http_ip]}:#{@@options[:http_port]}#{@@options[:http_path]}" - end - def rescue_port port begin yield - - # XXX: EventMachine only spits out RuntimeError with a string description - rescue RuntimeError - if $!.to_s =~ /\bno acceptor\b/ - puts "~~> ERROR: Something's using port #{port}. Are you already running MailCatcher?" - puts "==> #{smtp_url}" - puts "==> #{http_url}" - exit -1 - else - raise - end + rescue Errno::EADDRINUSE + puts "~~> ERROR: Something's using port #{port}. Are you already running MailCatcher?" + puts "==> #{smtp_url}" + puts "==> #{http_url}" + exit(-1) end end end diff --git a/lib/mail_catcher/bus.rb b/lib/mail_catcher/bus.rb deleted file mode 100644 index 0c86fdf1..00000000 --- a/lib/mail_catcher/bus.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -require "eventmachine" - -module MailCatcher - Bus = EventMachine::Channel.new -end diff --git a/lib/mail_catcher/mail.rb b/lib/mail_catcher/mail.rb index 8558d552..73039ed2 100644 --- a/lib/mail_catcher/mail.rb +++ b/lib/mail_catcher/mail.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true -require "eventmachine" require "json" require "mail" require "sqlite3" +require 'async/websocket/client' module MailCatcher::Mail extend self def db @@ -56,10 +56,7 @@ def add_message(message) add_message_part(message_id, cid, part.mime_type || "text/plain", part.attachment? ? 1 : 0, part.filename, part.charset, body, body.length) end - EventMachine.next_tick do - message = MailCatcher::Mail.message message_id - MailCatcher::Bus.push(type: "add", message: message) - end + MailCatcher.send_data(type: 'add', message: message(message_id)) end def add_message_part(*args) @@ -157,18 +154,14 @@ def delete! @delete_all_messages_query ||= db.prepare "DELETE FROM message" @delete_all_messages_query.execute - EventMachine.next_tick do - MailCatcher::Bus.push(type: "clear") - end + MailCatcher.send_data(type: 'clear') end def delete_message!(message_id) @delete_messages_query ||= db.prepare "DELETE FROM message WHERE id = ?" @delete_messages_query.execute(message_id) - EventMachine.next_tick do - MailCatcher::Bus.push(type: "remove", id: message_id) - end + MailCatcher.send_data(type: 'remove', id: message_id) end def delete_older_messages!(count = MailCatcher.options[:messages_limit]) diff --git a/lib/mail_catcher/message.rb b/lib/mail_catcher/message.rb new file mode 100644 index 00000000..8982eef3 --- /dev/null +++ b/lib/mail_catcher/message.rb @@ -0,0 +1,18 @@ +module MailCatcher + def send_data(data) + Async do + endpoint = Async::HTTP::Endpoint.parse("#{MailCatcher.http_url}/messages") + + begin + Async::WebSocket::Client.connect(endpoint) do |connection| + puts 'Connected...' + + connection.write data + connection.flush + end + rescue Errno::ECONNREFUSED + puts 'Cannot connect to the host. Please try again...' + end + end + end +end diff --git a/lib/mail_catcher/smtp.rb b/lib/mail_catcher/smtp.rb index 6fc8b42c..bcdf2aa2 100644 --- a/lib/mail_catcher/smtp.rb +++ b/lib/mail_catcher/smtp.rb @@ -1,58 +1,415 @@ # frozen_string_literal: true -require "eventmachine" +module MailCatcher + module SMTP + require 'uri' -require "mail_catcher/mail" + require 'async/io/endpoint' + require 'async/io/host_endpoint' + require 'async/io/ssl_endpoint' -class MailCatcher::Smtp < EventMachine::Protocols::SmtpServer - # We override EM's mail from processing to allow multiple mail-from commands - # per [RFC 2821](https://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 + class URLEndpoint < Async::IO::Endpoint + def self.parse(string, **options) + url = URI.parse(string).normalize + + new(url, **options) + end + + def initialize(url, endpoint = nil, **options) + super(**options) + + raise ArgumentError, "URL must be absolute (include scheme, host): #{url}" unless url.absolute? + + @url = url + @endpoint = endpoint + end + + def to_s + "\#<#{self.class} #{@url} #{@options.inspect}>" + end + + attr :url, :options + + def address + endpoint.address + end + + def secure? + ['smtps'].include?(@url.scheme) + end + + def protocol + Protocol::SMTP + end + + def default_port + secure? ? 465 : 25 + end + + def default_port? + port == default_port + end + + def port + @options[:port] || @url.port || default_port + end + + def hostname + @options[:hostname] || @url.hostname + end + + def scheme + @options[:scheme] || @url.scheme + end + + def authority + if default_port? + hostname + else + "#{hostname}:#{port}" + end + end + + def path + @url.request_uri + end + + LOCALHOST = 'localhost' + + # We don't try to validate peer certificates when talking to localhost because they would always be self-signed. + def ssl_verify_mode + case hostname + when LOCALHOST + OpenSSL::SSL::VERIFY_NONE + else + OpenSSL::SSL::VERIFY_PEER + end + end + + def ssl_context + @options[:ssl_context] || ::OpenSSL::SSL::SSLContext.new.tap do |context| + context.set_params( + verify_mode: ssl_verify_mode + ) + end + end + + def tcp_options + { reuse_port: @options[:reuse_port] ? true : false } + end + + def build_endpoint(endpoint = nil) + endpoint ||= Async::IO::Endpoint.tcp(hostname, port, tcp_options) + + if secure? + # Wrap it in SSL: + return Async::IO::SSLEndpoint.new(endpoint, + ssl_context: ssl_context, + hostname: hostname) + end + + endpoint + end + + def endpoint + @endpoint ||= build_endpoint + end + + def bind(*args, &block) + endpoint.bind(*args, &block) + end + + def connect(*args, &block) + endpoint.connect(*args, &block) + end + + def each + return to_enum unless block_given? + + endpoint.each do |endpoint| + yield self.class.new(@url, endpoint, @options) + end + end + + def key + [@url.scheme, @url.userinfo, @url.host, @url.port, @options] + end + + def eql?(other) + key.eql? other.key + end + + def hash + key.hash + end end - super - end + Envelope = Struct.new(:sender, :recipients, :content) - def current_message - @current_message ||= {} - end + require 'async/io/protocol/line' - def receive_reset - @current_message = nil - true - end + module Protocol + module SMTP + def self.server(stream, *args) + Server.new(stream, *args) + end - def receive_sender(sender) - current_message[:sender] = sender - true - end + class Server < Async::IO::Protocol::Line + CR = "\r" + LF = "\n" + CRLF = "\r\n" + SP = ' ' + COLON = ':' + DOT = '.' - def receive_recipient(recipient) - current_message[:recipients] ||= [] - current_message[:recipients] << recipient - true - end + def initialize(stream, hostname: nil) + super(stream, CRLF) + + @hostname = hostname + @state = {} + end + + attr :hostname + + def read_line + if line = @stream.read_until(LF) + line.chomp(CR) + else + @stream.eof! + end + end + + alias write_line write_lines + + def write_response(code, *lines, last_line) + write_lines(*lines.map { |line| "#{code}-#{line}" }, + "#{code} #{last_line}") + end + + def each(task: Async::Task.current) + write_response 220, 'MailCatcher ready' - def receive_data_chunk(lines) - current_message[:source] ||= +"" - lines.each do |line| - current_message[:source] << line << "\r\n" + loop do + line = read_line + command, line = line.split(SP, 2) + case command + when 'HELO' + write_response 250, hostname + when 'EHLO' + write_response 250, hostname, 'CHUNKING', '8BITMIME', 'BINARYMIME', 'SMTPUTF8' + when 'SEND' + write_response 502, 'Command not implemented' + when 'SOML' + write_response 502, 'Command not implemented' + when 'SAML' + write_response 502, 'Command not implemented' + when 'MAIL' + from, line = line.split(COLON, 2) + unless from == 'FROM' + write_response 500, 'Syntax error, command unrecognized' + next + end + sender, line = line.split(SP, 2) + encoding = nil + if line && !line.empty? + line.split(SP).each do |param| + case param + when 'BODY=7BIT' + encoding = :ascii + when 'BODY=8BITMIME' + encoding = :binary + when 'BODY=BINARYMIME' + encoding = :binary + when 'SMTPUTF8' + encoding = :utf8 + else + write_response 501, 'Unexpected parameters or arguments' + end + end + end + @state[:sender] = sender + @state[:encoding] = encoding if encoding + write_response 250, "New message from: #{sender}" + when 'RCPT' + unless @state[:sender] + write_response 503, 'Bad sequence of commands' + next + end + to, line = line.split(COLON, 2) + unless to == 'TO' + write_response 501, 'Syntax error in parameters or arguments' + next + end + recipient, line = line.split(SP, 2) + if line && !line.empty? + write_response 501, 'Unexpected parameters or arguments' + next + end + @state[:recipients] ||= [] + @state[:recipients] << recipient + write_response 250, "Recipient added: #{recipient}" + when 'DATA' + unless @state[:sender] && @state[:recipients] + write_response 503, 'Bad sequence of commands' + next + end + if @state.key? :buffer # BDAT + write_response 503, 'Bad sequence of commands' + next + end + if line && !line.empty? + write_response 501, 'Unexpected parameters or arguments' + next + end + write_response 354, 'Start mail input; end with .' + buffer = ''.b + loop do + line = read_line + break if line == DOT + + line.delete_prefix!(DOT) + buffer << line << CRLF + task.yield + end + encoding = @state[:encoding] + begin + case encoding + when :ascii + buffer.force_encoding(Encoding::ASCII) + when :binary + buffer.force_encoding(Encoding::BINARY) + when :utf8, nil + buffer.force_encoding(Encoding::UTF_8) + end + rescue ArgumentError => e + write_response 501, "Incorrect encoding (#{encoding}): #{e}" + @state.clear + next + end + yield Envelope.new(@state[:sender], @state[:recipients], buffer) + @state.clear + write_response 250, 'Message sent' + when 'BDAT' + unless @state[:sender] && @state[:recipients] + write_response 503, 'Bad sequence of commands' + next + end + size, line = line.split(SP, 2) + unless size.to_i.to_s == size + write_response 501, 'Syntax error in parameters or arguments' + next + end + size = size.to_i + last = false + if line == 'LAST' + last = true + elsif line && !line.empty? + write_response 501, 'Unexpected parameters or arguments' + next + end + buffer = @state[:buffer] ||= ''.b + buffer << read(size) + if last + begin + case encoding + when :ascii + buffer.force_encoding(Encoding::ASCII) + when :binary + buffer.force_encoding(Encoding::BINARY) + when :utf8, nil + buffer.force_encoding(Encoding::UTF_8) + end + rescue ArgumentError => e + write_response 500, "Bad encoding: #{e}" + @state.clear + next + end + yield Envelope.new(@state[:sender], @state[:recipients], buffer) + @state.clear + write_response 250, 'Message sent' + else + write_response 250, 'Data received' + end + when 'RSET' + if line && !line.empty? + write_response 501, 'Unexpected parameters or arguments' + next + end + envelope = nil + write_response 250, 'OK' + when 'NOOP' + if line && !line.empty? + write_response 501, 'Unexpected parameters or arguments' + next + end + write_response 250, 'OK' + when 'EXPN' + write_response 502, 'Command not implemented' + when 'VRFY' + write_response 502, 'Command not implemented' + when 'HELP' + write_response 502, 'Command not implemented' + when 'STARTTLS' + write_response 502, 'Command not implemented' + when 'QUIT' + if line && !line.empty? + write_response 501, 'Unexpected parameters or arguments' + next + end + write_response 221, 'Bye!' + close unless closed? + return + else + write_response 500, 'Syntax error, command unrecognized' + end + + task.yield + end + + close unless closed? + end + end + end end - true - end - def receive_message - 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 + require 'async' + require 'async/task' + require 'async/io/stream' + + class Server + def initialize(endpoint, protocol = endpoint.protocol) + @endpoint = endpoint + @protocol = protocol + + define_singleton_method(:call, &proc) if block_given? + end + + def accept(peer, address, task: Async::Task.current) + Async.logger.debug(self) { "Incoming connnection from #{address.inspect}" } + + stream = Async::IO::Stream.new(peer) + protocol = @protocol.server(stream, hostname: @endpoint.hostname) + + protocol.each do |envelope| + Async.logger.debug(self) { "Incoming message from #{address.inspect}: #{envelope.inspect}" } + + call(envelope) + end + + Async.logger.debug(self) { "Connection from #{address.inspect} closed cleanly" } + rescue EOFError, Errno::ECONNRESET, Errno::EPIPE, Errno::EPROTOTYPE + # Sometimes client will disconnect without completing a result or reading the entire buffer. That means we are done. + # Errno::EPROTOTYPE is a bug with Darwin. It happens because the socket is lazily created (in Darwin). + Async.logger.debug(self) { "Connection from #{address.inspect} closed: #{$ERROR_INFO}" } + end + + def call + raise NotImplementedError, 'Supply a block to MailCatcher::SMTP::Server.new or subclass and implement #call' + end + + def run + @endpoint.accept(&method(:accept)) + end + end end end diff --git a/lib/mail_catcher/version.rb b/lib/mail_catcher/version.rb index 969eb100..1e7b3109 100644 --- a/lib/mail_catcher/version.rb +++ b/lib/mail_catcher/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module MailCatcher - VERSION = "0.8.0.beta3" + VERSION = "1.0.0.beta1" end diff --git a/lib/mail_catcher/web.rb b/lib/mail_catcher/web.rb index e688132f..0fd45032 100644 --- a/lib/mail_catcher/web.rb +++ b/lib/mail_catcher/web.rb @@ -2,7 +2,8 @@ require "rack/builder" -require "mail_catcher/web/application" +require '/Users/ahmedgagan/Rails Gem/mailcatcher/lib/mail_catcher/web/application' +# require "mail_catcher/web/application" module MailCatcher module Web extend self diff --git a/lib/mail_catcher/web/application.rb b/lib/mail_catcher/web/application.rb index 3a2246ff..1d133bf7 100644 --- a/lib/mail_catcher/web/application.rb +++ b/lib/mail_catcher/web/application.rb @@ -5,18 +5,55 @@ require "uri" require "sinatra" -require "skinny" +require 'async/websocket/adapters/rack' -require "mail_catcher/bus" -require "mail_catcher/mail" - -class Sinatra::Request - include Skinny::Helpers -end +require '/Users/ahmedgagan/Rails Gem/mailcatcher/lib/mail_catcher/mail' +# require "mail_catcher/mail" module MailCatcher module Web class Application < Sinatra::Base + def initialize + super + @connections = Set.new + @semaphore = Async::Semaphore.new(512) + + @count = 0 + end + + def connect(connection) + @connections << connection + + @count += 1 + end + + def each(&block) + @connections.each(&block) + end + + def broadcast(message) + Console.logger.info "Broadcast: #{message.inspect}" + start_time = Async::Clock.now + + @connections.each do |connection| + @semaphore.async do + begin + connection.write(message) + connection.flush + rescue IOError + self.disconnect(connection) + end + end + end + + end_time = Async::Clock.now + Console.logger.info "Duration: #{(end_time - start_time).round(3)}s for #{@connections.count} connected clients." + end + + def disconnect connection + @connections.delete(connection) + end + set :environment, MailCatcher.env set :prefix, MailCatcher.options[:http_path] set :asset_prefix, File.join(prefix, "assets") @@ -61,21 +98,20 @@ def asset_path(filename) end get "/messages" do - if request.websocket? - request.websocket!( - :on_start => proc do |websocket| - bus_subscription = MailCatcher::Bus.subscribe do |message| - begin - websocket.send_message(JSON.generate(message)) - rescue => exception - MailCatcher.log_exception("Error sending message through websocket", message, exception) - end + if Async::WebSocket::Adapters::Rack.websocket?(env) + puts 'WebSockets connection opened...' + Async::WebSocket::Adapters::Rack.open(env, protocols: %w[ws]) do |connection| + self.connect(connection) + + begin + while message = connection.read + self.broadcast(message) end - - websocket.on_close do |*| - MailCatcher::Bus.unsubscribe bus_subscription - end - end) + rescue Protocol::WebSocket::ClosedError, IOError + self.disconnect(connection) + connection.close + end + end else content_type :json JSON.generate(Mail.messages) diff --git a/mailcatcher.gemspec b/mailcatcher.gemspec index 2a04c9a6..fd74f181 100644 --- a/mailcatcher.gemspec +++ b/mailcatcher.gemspec @@ -32,13 +32,15 @@ Gem::Specification.new do |s| s.required_ruby_version = ">= 2.0.0" - s.add_dependency "eventmachine", "1.0.9.1" + s.add_dependency 'async', '~> 1.25' + s.add_dependency 'async-http', '~> 0.56.3' + s.add_dependency 'async-io', '~> 1.32.1' + s.add_dependency 'async-websocket', '~> 0.19.0' + s.add_dependency 'falcon', '~> 0.39.1' s.add_dependency "mail", "~> 2.3" s.add_dependency "rack", "~> 1.5" s.add_dependency "sinatra", "~> 1.2" s.add_dependency "sqlite3", "~> 1.3" - s.add_dependency "thin", "~> 1.5.0" - s.add_dependency "skinny", "~> 0.2.3" s.add_development_dependency "coffee-script" s.add_development_dependency "compass", "~> 1.0.3" From 4b65ae4e3a8014d67b2f7bed6b4f419345f61f32 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Mon, 28 Jun 2021 11:50:18 +0530 Subject: [PATCH 02/30] DEV: uses relative require paths --- lib/mail_catcher.rb | 20 ++++++++++---------- lib/mail_catcher/web.rb | 4 ++-- lib/mail_catcher/web/application.rb | 4 ++-- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/mail_catcher.rb b/lib/mail_catcher.rb index 0803afa2..5d269bef 100644 --- a/lib/mail_catcher.rb +++ b/lib/mail_catcher.rb @@ -23,19 +23,19 @@ require 'mail' require 'falcon' -require '/Users/ahmedgagan/Rails Gem/mailcatcher/lib/mail_catcher/message' -require '/Users/ahmedgagan/Rails Gem/mailcatcher/lib/mail_catcher/version' +# require '/Users/ahmedgagan/Rails Gem/mailcatcher/lib/mail_catcher/message' +# require '/Users/ahmedgagan/Rails Gem/mailcatcher/lib/mail_catcher/version' -# require 'mail_catcher/message' -# require 'mail_catcher/version' +require 'mail_catcher/message' +require 'mail_catcher/version' module MailCatcher extend self - autoload :Mail, '/Users/ahmedgagan/Rails Gem/mailcatcher/lib/mail_catcher/mail' - autoload :SMTP, '/Users/ahmedgagan/Rails Gem/mailcatcher/lib/mail_catcher/smtp' - autoload :Web, '/Users/ahmedgagan/Rails Gem/mailcatcher/lib/mail_catcher/web' - # autoload :Mail, "mail_catcher/mail" - # autoload :Smtp, "mail_catcher/smtp" - # autoload :Web, "mail_catcher/web" + # autoload :Mail, '/Users/ahmedgagan/Rails Gem/mailcatcher/lib/mail_catcher/mail' + # autoload :SMTP, '/Users/ahmedgagan/Rails Gem/mailcatcher/lib/mail_catcher/smtp' + # autoload :Web, '/Users/ahmedgagan/Rails Gem/mailcatcher/lib/mail_catcher/web' + autoload :Mail, "mail_catcher/mail" + autoload :SMTP, "mail_catcher/smtp" + autoload :Web, "mail_catcher/web" def env ENV.fetch("MAILCATCHER_ENV", "production") diff --git a/lib/mail_catcher/web.rb b/lib/mail_catcher/web.rb index 0fd45032..58e2ae4f 100644 --- a/lib/mail_catcher/web.rb +++ b/lib/mail_catcher/web.rb @@ -2,8 +2,8 @@ require "rack/builder" -require '/Users/ahmedgagan/Rails Gem/mailcatcher/lib/mail_catcher/web/application' -# require "mail_catcher/web/application" +# require '/Users/ahmedgagan/Rails Gem/mailcatcher/lib/mail_catcher/web/application' +require "mail_catcher/web/application" module MailCatcher module Web extend self diff --git a/lib/mail_catcher/web/application.rb b/lib/mail_catcher/web/application.rb index 1d133bf7..75798b75 100644 --- a/lib/mail_catcher/web/application.rb +++ b/lib/mail_catcher/web/application.rb @@ -7,8 +7,8 @@ require "sinatra" require 'async/websocket/adapters/rack' -require '/Users/ahmedgagan/Rails Gem/mailcatcher/lib/mail_catcher/mail' -# require "mail_catcher/mail" +# require '/Users/ahmedgagan/Rails Gem/mailcatcher/lib/mail_catcher/mail' +require "mail_catcher/mail" module MailCatcher module Web From 236bdc6cbedd4b3df4fbf484e4156628d2329d22 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Mon, 28 Jun 2021 15:36:08 +0530 Subject: [PATCH 03/30] DEV: does not daemonize server --- lib/mail_catcher.rb | 49 +++++++++++------------------ lib/mail_catcher/web.rb | 3 +- lib/mail_catcher/web/application.rb | 2 -- 3 files changed, 19 insertions(+), 35 deletions(-) diff --git a/lib/mail_catcher.rb b/lib/mail_catcher.rb index 5d269bef..d045f21e 100644 --- a/lib/mail_catcher.rb +++ b/lib/mail_catcher.rb @@ -12,27 +12,20 @@ gem "sinatra", "~> 1.2" gem "sqlite3", "~> 1.3" -require "open3" -require "optparse" -require "rbconfig" -require 'socket' require 'async/io/address_endpoint' require 'async/http/endpoint' require 'async/websocket/adapters/rack' require 'async/io/shared_endpoint' -require 'mail' require 'falcon' - -# require '/Users/ahmedgagan/Rails Gem/mailcatcher/lib/mail_catcher/message' -# require '/Users/ahmedgagan/Rails Gem/mailcatcher/lib/mail_catcher/version' - +require "open3" +require "optparse" +require "rbconfig" +require 'socket' +require 'mail' require 'mail_catcher/message' require 'mail_catcher/version' module MailCatcher extend self - # autoload :Mail, '/Users/ahmedgagan/Rails Gem/mailcatcher/lib/mail_catcher/mail' - # autoload :SMTP, '/Users/ahmedgagan/Rails Gem/mailcatcher/lib/mail_catcher/smtp' - # autoload :Web, '/Users/ahmedgagan/Rails Gem/mailcatcher/lib/mail_catcher/web' autoload :Mail, "mail_catcher/mail" autoload :SMTP, "mail_catcher/smtp" autoload :Web, "mail_catcher/web" @@ -51,18 +44,10 @@ def which?(command) end end - def mac? - RbConfig::CONFIG["host_os"] =~ /darwin/ - end - def windows? RbConfig::CONFIG["host_os"] =~ /mswin|mingw/ end - def macruby? - mac? and const_defined? :MACRUBY_VERSION - end - def browseable? windows? or which? "open" end @@ -229,23 +214,25 @@ def run! options=nil http_address = Async::IO::Address.tcp(options[:http_ip], options[:http_port]) http_endpoint = Async::IO::AddressEndpoint.new(http_address) + http_socket = rescue_port(options[:http_port]) { http_endpoint.bind } + puts "==> #{http_url}" - http_endpoint = Async::HTTP::Endpoint.parse(http_url) - http_app = Falcon::Server.middleware(Web) + http_endpoint = Async::HTTP::Endpoint.new(URI.parse(http_url), http_endpoint) + http_app = Falcon::Adapters::Rack.new(Web.app) http_server = Falcon::Server.new(http_app, http_endpoint) - http_server.run.each(&:wait) - browse(http_url) if options[:browse] + http_task = task.async do |task| + task.annotate "binding to #{http_socket.local_address.inspect}" - # Daemonize, if we should, but only after the servers have started. - if options[:daemon] - if quittable? - puts '*** MailCatcher runs as a daemon by default. Go to the web interface to quit.' - else - puts '*** MailCatcher is now running as a daemon that cannot be quit.' + begin + http_socket.listen(Socket::SOMAXCONN) + http_socket.accept_each(task: task, &http_server.method(:accept)) + ensure + http_socket.close end - Process.daemon end + + browse(http_url) if options[:browse] end rescue Interrupt # Cool story diff --git a/lib/mail_catcher/web.rb b/lib/mail_catcher/web.rb index 58e2ae4f..88e3dad4 100644 --- a/lib/mail_catcher/web.rb +++ b/lib/mail_catcher/web.rb @@ -2,8 +2,7 @@ require "rack/builder" -# require '/Users/ahmedgagan/Rails Gem/mailcatcher/lib/mail_catcher/web/application' -require "mail_catcher/web/application" +# require "mail_catcher/web/application" module MailCatcher module Web extend self diff --git a/lib/mail_catcher/web/application.rb b/lib/mail_catcher/web/application.rb index 75798b75..c7cbf706 100644 --- a/lib/mail_catcher/web/application.rb +++ b/lib/mail_catcher/web/application.rb @@ -6,8 +6,6 @@ require "sinatra" require 'async/websocket/adapters/rack' - -# require '/Users/ahmedgagan/Rails Gem/mailcatcher/lib/mail_catcher/mail' require "mail_catcher/mail" module MailCatcher From 49ff9f029f5e573aa5096010a993ebc6992ccda6 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Mon, 28 Jun 2021 15:37:22 +0530 Subject: [PATCH 04/30] DEV: removes unused variable --- lib/mail_catcher.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mail_catcher.rb b/lib/mail_catcher.rb index d045f21e..a14b7d05 100644 --- a/lib/mail_catcher.rb +++ b/lib/mail_catcher.rb @@ -221,7 +221,7 @@ def run! options=nil http_app = Falcon::Adapters::Rack.new(Web.app) http_server = Falcon::Server.new(http_app, http_endpoint) - http_task = task.async do |task| + task.async do |task| task.annotate "binding to #{http_socket.local_address.inspect}" begin From 2a8f1976ddca7f0284355f95d1df21a9983e0438 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Mon, 28 Jun 2021 15:43:40 +0530 Subject: [PATCH 05/30] DEV: adds method to check OS --- lib/mail_catcher.rb | 4 ++++ lib/mail_catcher/web.rb | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/mail_catcher.rb b/lib/mail_catcher.rb index a14b7d05..8531fb3b 100644 --- a/lib/mail_catcher.rb +++ b/lib/mail_catcher.rb @@ -44,6 +44,10 @@ def which?(command) end end + def mac? + RbConfig::CONFIG["host_os"] =~ /darwin/ + end + def windows? RbConfig::CONFIG["host_os"] =~ /mswin|mingw/ end diff --git a/lib/mail_catcher/web.rb b/lib/mail_catcher/web.rb index 88e3dad4..e688132f 100644 --- a/lib/mail_catcher/web.rb +++ b/lib/mail_catcher/web.rb @@ -2,7 +2,7 @@ require "rack/builder" -# require "mail_catcher/web/application" +require "mail_catcher/web/application" module MailCatcher module Web extend self From 94c1824b8dacebf23cecd08cd876be6a3681d256 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Mon, 28 Jun 2021 15:50:14 +0530 Subject: [PATCH 06/30] DEV: Always sync the output --- lib/mail_catcher.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/mail_catcher.rb b/lib/mail_catcher.rb index 8531fb3b..99b51b30 100644 --- a/lib/mail_catcher.rb +++ b/lib/mail_catcher.rb @@ -184,10 +184,8 @@ def run! options=nil # Stash them away for later @@options = options - # If we're running in the foreground sync the output. - unless options[:daemon] - $stdout.sync = $stderr.sync = true - end + # sync the output. + $stdout.sync = $stderr.sync = true puts "Starting MailCatcher" From bbf9327f664394832044b7a4f33036c17a5db8fb Mon Sep 17 00:00:00 2001 From: Ahmed Date: Tue, 6 Jul 2021 13:49:36 +0530 Subject: [PATCH 07/30] DEV: Daemonises server & changes as suggested --- lib/mail_catcher.rb | 31 ++++++++++++++--------------- lib/mail_catcher/web/application.rb | 25 ++++++++++------------- 2 files changed, 26 insertions(+), 30 deletions(-) diff --git a/lib/mail_catcher.rb b/lib/mail_catcher.rb index 99b51b30..887fb8ab 100644 --- a/lib/mail_catcher.rb +++ b/lib/mail_catcher.rb @@ -1,17 +1,5 @@ # frozen_string_literal: true -# Apparently rubygems won't activate these on its own, so here we go. Let's -# repeat the invention of Bundler all over again. -gem 'async', '~> 1.25' -gem 'async-http', '~> 0.56.3' -gem 'async-io', '~> 1.32.1' -gem 'async-websocket', '~> 0.19.0' -gem 'falcon', '~> 0.39.1' -gem "mail", "~> 2.3" -gem "rack", "~> 1.5" -gem "sinatra", "~> 1.2" -gem "sqlite3", "~> 1.3" - require 'async/io/address_endpoint' require 'async/http/endpoint' require 'async/websocket/adapters/rack' @@ -184,18 +172,30 @@ def run! options=nil # Stash them away for later @@options = options - # sync the output. - $stdout.sync = $stderr.sync = true + # If we're running in the foreground sync the output. + unless options[:daemon] + $stdout.sync = $stderr.sync = true + end puts "Starting MailCatcher" + puts "==> #{smtp_url}" + puts "==> #{http_url}" Async.logger.level = Logger::DEBUG if options[:verbose] + if options[:daemon] + if quittable? + puts "*** MailCatcher runs as a daemon by default. Go to the web interface to quit." + else + puts "*** MailCatcher is now running as a daemon that cannot be quit." + end + Process.daemon + end + Async::Reactor.run do |task| smtp_address = Async::IO::Address.tcp(options[:smtp_ip], options[:smtp_port]) smtp_endpoint = Async::IO::AddressEndpoint.new(smtp_address) smtp_socket = rescue_port(options[:smtp_port]) { smtp_endpoint.bind } - puts "==> #{smtp_url}" smtp_endpoint = MailCatcher::SMTP::URLEndpoint.new(URI.parse(smtp_url), smtp_endpoint) smtp_server = MailCatcher::SMTP::Server.new(smtp_endpoint) do |envelope| @@ -217,7 +217,6 @@ def run! options=nil http_address = Async::IO::Address.tcp(options[:http_ip], options[:http_port]) http_endpoint = Async::IO::AddressEndpoint.new(http_address) http_socket = rescue_port(options[:http_port]) { http_endpoint.bind } - puts "==> #{http_url}" http_endpoint = Async::HTTP::Endpoint.new(URI.parse(http_url), http_endpoint) http_app = Falcon::Adapters::Rack.new(Web.app) diff --git a/lib/mail_catcher/web/application.rb b/lib/mail_catcher/web/application.rb index c7cbf706..11b6d015 100644 --- a/lib/mail_catcher/web/application.rb +++ b/lib/mail_catcher/web/application.rb @@ -14,7 +14,6 @@ class Application < Sinatra::Base def initialize super @connections = Set.new - @semaphore = Async::Semaphore.new(512) @count = 0 end @@ -30,25 +29,23 @@ def each(&block) end def broadcast(message) - Console.logger.info "Broadcast: #{message.inspect}" + Async.logger.debug(self) { "Broadcast: #{message.inspect}" } start_time = Async::Clock.now @connections.each do |connection| - @semaphore.async do - begin - connection.write(message) - connection.flush - rescue IOError - self.disconnect(connection) - end + begin + connection.write(message) + connection.flush + rescue IOError + disconnect(connection) end end end_time = Async::Clock.now - Console.logger.info "Duration: #{(end_time - start_time).round(3)}s for #{@connections.count} connected clients." + Async.logger.debug(self) { "Duration: #{(end_time - start_time).round(3)}s for #{@connections.count} connected clients." } end - def disconnect connection + def disconnect(connection) @connections.delete(connection) end @@ -99,14 +96,14 @@ def asset_path(filename) if Async::WebSocket::Adapters::Rack.websocket?(env) puts 'WebSockets connection opened...' Async::WebSocket::Adapters::Rack.open(env, protocols: %w[ws]) do |connection| - self.connect(connection) + connect(connection) begin while message = connection.read - self.broadcast(message) + broadcast(message) end rescue Protocol::WebSocket::ClosedError, IOError - self.disconnect(connection) + disconnect(connection) connection.close end end From 480ff4473aa561604a5a14eab62ac10820313327 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Tue, 6 Jul 2021 18:25:34 +0530 Subject: [PATCH 08/30] DEV: better handle websocket connections --- lib/mail_catcher/message.rb | 26 +++++++++++++------------- lib/mail_catcher/web/application.rb | 2 ++ 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/lib/mail_catcher/message.rb b/lib/mail_catcher/message.rb index 8982eef3..33ef2131 100644 --- a/lib/mail_catcher/message.rb +++ b/lib/mail_catcher/message.rb @@ -1,18 +1,18 @@ module MailCatcher - def send_data(data) - Async do - endpoint = Async::HTTP::Endpoint.parse("#{MailCatcher.http_url}/messages") - - begin - Async::WebSocket::Client.connect(endpoint) do |connection| - puts 'Connected...' + def endpoint + Async::HTTP::Endpoint.parse("#{MailCatcher.http_url}/messages") + end - connection.write data - connection.flush - end - rescue Errno::ECONNREFUSED - puts 'Cannot connect to the host. Please try again...' - end + def self.connection + @connection ||= begin + Async::WebSocket::Client.connect(endpoint) + rescue Errno::ECONNREFUSED + puts 'Cannot connect to the host. Please try again...' end end + + def send_data(data) + connection.write data + connection.flush + end end diff --git a/lib/mail_catcher/web/application.rb b/lib/mail_catcher/web/application.rb index 11b6d015..a35f7b6b 100644 --- a/lib/mail_catcher/web/application.rb +++ b/lib/mail_catcher/web/application.rb @@ -106,6 +106,8 @@ def asset_path(filename) disconnect(connection) connection.close end + ensure + disconnect(connection) end else content_type :json From 7f370022522dac48d375cd16966738b73c43c5c3 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Tue, 6 Jul 2021 19:26:48 +0530 Subject: [PATCH 09/30] DEV: removes unwanted code --- lib/mail_catcher.rb | 11 ----------- lib/mail_catcher/web/application.rb | 4 ---- 2 files changed, 15 deletions(-) diff --git a/lib/mail_catcher.rb b/lib/mail_catcher.rb index 887fb8ab..3a87dec4 100644 --- a/lib/mail_catcher.rb +++ b/lib/mail_catcher.rb @@ -32,10 +32,6 @@ def which?(command) end end - def mac? - RbConfig::CONFIG["host_os"] =~ /darwin/ - end - def windows? RbConfig::CONFIG["host_os"] =~ /mswin|mingw/ end @@ -127,13 +123,6 @@ def parse! arguments=ARGV, defaults=@defaults options[:quit] = false end - if mac? - parser.on("--[no-]growl") do |growl| - puts "Growl is no longer supported" - exit -2 - end - end - unless windows? parser.on("-f", "--foreground", "Run in the foreground") do options[:daemon] = false diff --git a/lib/mail_catcher/web/application.rb b/lib/mail_catcher/web/application.rb index a35f7b6b..0209d79f 100644 --- a/lib/mail_catcher/web/application.rb +++ b/lib/mail_catcher/web/application.rb @@ -30,7 +30,6 @@ def each(&block) def broadcast(message) Async.logger.debug(self) { "Broadcast: #{message.inspect}" } - start_time = Async::Clock.now @connections.each do |connection| begin @@ -40,9 +39,6 @@ def broadcast(message) disconnect(connection) end end - - end_time = Async::Clock.now - Async.logger.debug(self) { "Duration: #{(end_time - start_time).round(3)}s for #{@connections.count} connected clients." } end def disconnect(connection) From 90987757012541674cec048b4018b7e8fea20438 Mon Sep 17 00:00:00 2001 From: Samuel Cochran Date: Tue, 13 Jul 2021 11:33:43 +1000 Subject: [PATCH 10/30] Fix verbose logging mailcatcher --verbose failed with: NameError: uninitialized constant MailCatcher::Logger Async.logger is a Console::Logger, not a Logger. But it accepts symbols as log levels. --- lib/mail_catcher.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mail_catcher.rb b/lib/mail_catcher.rb index 3a87dec4..3c0c2aa1 100644 --- a/lib/mail_catcher.rb +++ b/lib/mail_catcher.rb @@ -170,7 +170,7 @@ def run! options=nil puts "==> #{smtp_url}" puts "==> #{http_url}" - Async.logger.level = Logger::DEBUG if options[:verbose] + Async.logger.level = :debug if options[:verbose] if options[:daemon] if quittable? From 0ed0306b56aefebeaf6ad2af3aee641e282b96e1 Mon Sep 17 00:00:00 2001 From: Samuel Cochran Date: Tue, 13 Jul 2021 11:51:43 +1000 Subject: [PATCH 11/30] Use a message bus Add a Bus which is a Channel modelled roughly after eventmachine channels which can broadcast a message to multiple consumers as async tasks. Then modify each websocket to use a Queue to copy broadcast messages to clients. --- lib/mail_catcher.rb | 5 ++- lib/mail_catcher/bus.rb | 48 ++++++++++++++++++++++ lib/mail_catcher/mail.rb | 6 +-- lib/mail_catcher/message.rb | 18 -------- lib/mail_catcher/smtp.rb | 12 +++--- lib/mail_catcher/web/application.rb | 64 +++++++++-------------------- 6 files changed, 80 insertions(+), 73 deletions(-) create mode 100644 lib/mail_catcher/bus.rb delete mode 100644 lib/mail_catcher/message.rb diff --git a/lib/mail_catcher.rb b/lib/mail_catcher.rb index 3c0c2aa1..4f4fe2f6 100644 --- a/lib/mail_catcher.rb +++ b/lib/mail_catcher.rb @@ -10,10 +10,11 @@ require "rbconfig" require 'socket' require 'mail' -require 'mail_catcher/message' -require 'mail_catcher/version' + +require "mail_catcher/version" module MailCatcher extend self + autoload :Bus, "mail_catcher/bus" autoload :Mail, "mail_catcher/mail" autoload :SMTP, "mail_catcher/smtp" autoload :Web, "mail_catcher/web" diff --git a/lib/mail_catcher/bus.rb b/lib/mail_catcher/bus.rb new file mode 100644 index 00000000..e3af681f --- /dev/null +++ b/lib/mail_catcher/bus.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module MailCatcher + # Async-friendly broadcast channel + class Channel + def initialize + @subscription_id = 0 + @subscriptions = {} + end + + def subscriber_count + @subscriptions.size + end + + def push(*values) + Async.run do + values.each do |value| + @subscriptions.each_value do |subscription| + Async do + subscription.call(value) + end + end + end + end + end + + def subscribe(&block) + subscription_id = next_subscription_id + + @subscriptions[subscription_id] = block + + subscription_id + end + + def unsubscribe(subscription_id) + @subscriptions.delete(subscription_id) + end + + private + + def next_subscription_id + @subscription_id += 1 + end + end + + # Then we instantiate a global one + Bus = Channel.new +end diff --git a/lib/mail_catcher/mail.rb b/lib/mail_catcher/mail.rb index 73039ed2..a0febaf9 100644 --- a/lib/mail_catcher/mail.rb +++ b/lib/mail_catcher/mail.rb @@ -56,7 +56,7 @@ def add_message(message) add_message_part(message_id, cid, part.mime_type || "text/plain", part.attachment? ? 1 : 0, part.filename, part.charset, body, body.length) end - MailCatcher.send_data(type: 'add', message: message(message_id)) + MailCatcher::Bus.push(type: "add", message: message(message_id)) end def add_message_part(*args) @@ -154,14 +154,14 @@ def delete! @delete_all_messages_query ||= db.prepare "DELETE FROM message" @delete_all_messages_query.execute - MailCatcher.send_data(type: 'clear') + MailCatcher::Bus.push(type: "clear") end def delete_message!(message_id) @delete_messages_query ||= db.prepare "DELETE FROM message WHERE id = ?" @delete_messages_query.execute(message_id) - MailCatcher.send_data(type: 'remove', id: message_id) + MailCatcher::Bus.push(type: "remove", id: message_id) end def delete_older_messages!(count = MailCatcher.options[:messages_limit]) diff --git a/lib/mail_catcher/message.rb b/lib/mail_catcher/message.rb deleted file mode 100644 index 33ef2131..00000000 --- a/lib/mail_catcher/message.rb +++ /dev/null @@ -1,18 +0,0 @@ -module MailCatcher - def endpoint - Async::HTTP::Endpoint.parse("#{MailCatcher.http_url}/messages") - end - - def self.connection - @connection ||= begin - Async::WebSocket::Client.connect(endpoint) - rescue Errno::ECONNREFUSED - puts 'Cannot connect to the host. Please try again...' - end - end - - def send_data(data) - connection.write data - connection.flush - end -end diff --git a/lib/mail_catcher/smtp.rb b/lib/mail_catcher/smtp.rb index bcdf2aa2..050a568b 100644 --- a/lib/mail_catcher/smtp.rb +++ b/lib/mail_catcher/smtp.rb @@ -1,13 +1,13 @@ # frozen_string_literal: true -module MailCatcher - module SMTP - require 'uri' +require "uri" - require 'async/io/endpoint' - require 'async/io/host_endpoint' - require 'async/io/ssl_endpoint' +require "async/io/endpoint" +require "async/io/host_endpoint" +require "async/io/ssl_endpoint" +module MailCatcher + module SMTP class URLEndpoint < Async::IO::Endpoint def self.parse(string, **options) url = URI.parse(string).normalize diff --git a/lib/mail_catcher/web/application.rb b/lib/mail_catcher/web/application.rb index 0209d79f..052e5460 100644 --- a/lib/mail_catcher/web/application.rb +++ b/lib/mail_catcher/web/application.rb @@ -11,40 +11,6 @@ module MailCatcher module Web class Application < Sinatra::Base - def initialize - super - @connections = Set.new - - @count = 0 - end - - def connect(connection) - @connections << connection - - @count += 1 - end - - def each(&block) - @connections.each(&block) - end - - def broadcast(message) - Async.logger.debug(self) { "Broadcast: #{message.inspect}" } - - @connections.each do |connection| - begin - connection.write(message) - connection.flush - rescue IOError - disconnect(connection) - end - end - end - - def disconnect(connection) - @connections.delete(connection) - end - set :environment, MailCatcher.env set :prefix, MailCatcher.options[:http_path] set :asset_prefix, File.join(prefix, "assets") @@ -90,20 +56,30 @@ def asset_path(filename) get "/messages" do if Async::WebSocket::Adapters::Rack.websocket?(env) - puts 'WebSockets connection opened...' Async::WebSocket::Adapters::Rack.open(env, protocols: %w[ws]) do |connection| - connect(connection) - + Async.logger.debug(connection, "Websocket connection opened") begin - while message = connection.read - broadcast(message) + queue = Async::Queue.new + + subscription_id = MailCatcher::Bus.subscribe { |message| queue << message } + Async.logger.debug(connection, "Websocket connection subscribed to bus: #{subscription_id}") + + queue.each do |message| + begin + Async.logger.debug(connection, "Sending #{message}") + connection.write(message) + connection.flush + rescue IOError + Async.logger.error(connection, "Failed sending #{message}", $!) + raise + end end - rescue Protocol::WebSocket::ClosedError, IOError - disconnect(connection) - connection.close + rescue + Async.logger.error(connection, "Connection error", $!) + ensure + Async.logger.debug(connection, "Unsubscribing from bus, subscription #{subscription_id}") + MailCatcher::Bus.unsubscribe(subscription_id) end - ensure - disconnect(connection) end else content_type :json From d08ba9f35a4d03bebc489141d53a4ae67e1900ce Mon Sep 17 00:00:00 2001 From: Samuel Cochran Date: Tue, 13 Jul 2021 11:55:18 +1000 Subject: [PATCH 12/30] Why does the data store need websockets? --- lib/mail_catcher/mail.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/mail_catcher/mail.rb b/lib/mail_catcher/mail.rb index a0febaf9..8c033745 100644 --- a/lib/mail_catcher/mail.rb +++ b/lib/mail_catcher/mail.rb @@ -3,7 +3,6 @@ require "json" require "mail" require "sqlite3" -require 'async/websocket/client' module MailCatcher::Mail extend self def db From 1fe62ca8e6a753f2e3279dcf6e994c3c46750f82 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Tue, 13 Jul 2021 08:11:26 +0530 Subject: [PATCH 13/30] DEV: Opens port before daemonizing server --- lib/mail_catcher.rb | 41 ++++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/lib/mail_catcher.rb b/lib/mail_catcher.rb index 4f4fe2f6..bab42dd4 100644 --- a/lib/mail_catcher.rb +++ b/lib/mail_catcher.rb @@ -168,8 +168,18 @@ def run! options=nil end puts "Starting MailCatcher" - puts "==> #{smtp_url}" - puts "==> #{http_url}" + + Async.run do + @smtp_address = Async::IO::Address.tcp(options[:smtp_ip], options[:smtp_port]) + @smtp_endpoint = Async::IO::AddressEndpoint.new(@smtp_address) + @smtp_socket = rescue_port(options[:smtp_port]) { @smtp_endpoint.bind } + puts "==> #{smtp_url}" + + @http_address = Async::IO::Address.tcp(options[:http_ip], options[:http_port]) + @http_endpoint = Async::IO::AddressEndpoint.new(@http_address) + @http_socket = rescue_port(options[:http_port]) { @http_endpoint.bind } + puts "==> #{http_url}" + end Async.logger.level = :debug if options[:verbose] @@ -183,43 +193,36 @@ def run! options=nil end Async::Reactor.run do |task| - smtp_address = Async::IO::Address.tcp(options[:smtp_ip], options[:smtp_port]) - smtp_endpoint = Async::IO::AddressEndpoint.new(smtp_address) - smtp_socket = rescue_port(options[:smtp_port]) { smtp_endpoint.bind } - smtp_endpoint = MailCatcher::SMTP::URLEndpoint.new(URI.parse(smtp_url), smtp_endpoint) + smtp_endpoint = MailCatcher::SMTP::URLEndpoint.new(URI.parse(smtp_url), @smtp_endpoint) smtp_server = MailCatcher::SMTP::Server.new(smtp_endpoint) do |envelope| MailCatcher::Mail.add_message(sender: envelope.sender, recipients: envelope.recipients, source: envelope.content) end smtp_task = task.async do |task| - task.annotate "binding to #{smtp_socket.local_address.inspect}" + task.annotate "binding to #{@smtp_socket.local_address.inspect}" begin - smtp_socket.listen(Socket::SOMAXCONN) - smtp_socket.accept_each(task: task, &smtp_server.method(:accept)) + @smtp_socket.listen(Socket::SOMAXCONN) + @smtp_socket.accept_each(task: task, &smtp_server.method(:accept)) ensure - smtp_socket.close + @smtp_socket.close end end - http_address = Async::IO::Address.tcp(options[:http_ip], options[:http_port]) - http_endpoint = Async::IO::AddressEndpoint.new(http_address) - http_socket = rescue_port(options[:http_port]) { http_endpoint.bind } - - http_endpoint = Async::HTTP::Endpoint.new(URI.parse(http_url), http_endpoint) + http_endpoint = Async::HTTP::Endpoint.new(URI.parse(http_url), @http_endpoint) http_app = Falcon::Adapters::Rack.new(Web.app) http_server = Falcon::Server.new(http_app, http_endpoint) task.async do |task| - task.annotate "binding to #{http_socket.local_address.inspect}" + task.annotate "binding to #{@http_socket.local_address.inspect}" begin - http_socket.listen(Socket::SOMAXCONN) - http_socket.accept_each(task: task, &http_server.method(:accept)) + @http_socket.listen(Socket::SOMAXCONN) + @http_socket.accept_each(task: task, &http_server.method(:accept)) ensure - http_socket.close + @http_socket.close end end From a64a9144dcca7898e21257eeb269805cfe09d0aa Mon Sep 17 00:00:00 2001 From: Ahmed Date: Thu, 15 Jul 2021 00:43:40 +0530 Subject: [PATCH 14/30] DEV: Convers Mintest to RSpec --- Rakefile | 7 +-- mailcatcher.gemspec | 2 +- spec/acceptance_spec.rb | 104 ++++++++++++++++++++-------------------- 3 files changed, 54 insertions(+), 59 deletions(-) diff --git a/Rakefile b/Rakefile index 5a4bf6b7..3c57c4e6 100644 --- a/Rakefile +++ b/Rakefile @@ -2,6 +2,7 @@ require "fileutils" require "rubygems" +require 'rspec/core/rake_task' require "mail_catcher/version" @@ -52,11 +53,7 @@ RDoc::Task.new(:rdoc => "doc",:clobber_rdoc => "doc:clean", :rerdoc => "doc:forc rdoc.rdoc_files.include "lib/**/*.rb" end -require "rake/testtask" - -Rake::TestTask.new do |task| - task.pattern = "spec/*_spec.rb" -end +RSpec::Core::RakeTask.new(:test) task :test => :assets diff --git a/mailcatcher.gemspec b/mailcatcher.gemspec index fd74f181..32a08bef 100644 --- a/mailcatcher.gemspec +++ b/mailcatcher.gemspec @@ -44,7 +44,7 @@ Gem::Specification.new do |s| s.add_development_dependency "coffee-script" s.add_development_dependency "compass", "~> 1.0.3" - s.add_development_dependency "minitest", "~> 5.0" + s.add_development_dependency "rspec" s.add_development_dependency "rake" s.add_development_dependency "rdoc" s.add_development_dependency "sass" diff --git a/spec/acceptance_spec.rb b/spec/acceptance_spec.rb index 74e233a9..2af318cc 100644 --- a/spec/acceptance_spec.rb +++ b/spec/acceptance_spec.rb @@ -2,7 +2,6 @@ ENV["MAILCATCHER_ENV"] ||= "test" -require "minitest/autorun" require "mail_catcher" require "socket" require "net/smtp" @@ -14,11 +13,6 @@ # Start MailCatcher MAILCATCHER_PID = spawn "bundle", "exec", "mailcatcher", "--foreground", "--smtp-port", SMTP_PORT.to_s, "--http-port", HTTP_PORT.to_s -# Make sure it will be stopped -MiniTest.after_run do - Process.kill("TERM", MAILCATCHER_PID) and Process.wait -end - # Wait for it to boot begin TCPSocket.new("127.0.0.1", SMTP_PORT).close @@ -27,10 +21,14 @@ retry end -describe MailCatcher do +RSpec.describe MailCatcher do DEFAULT_FROM = "from@example.com" DEFAULT_TO = "to@example.com" + after(:all) do + Process.kill("TERM", MAILCATCHER_PID) and Process.wait + end + def deliver(message, options={}) options = {:from => DEFAULT_FROM, :to => DEFAULT_TO}.merge(options) Net::SMTP.start('127.0.0.1', SMTP_PORT) do |smtp| @@ -117,10 +115,10 @@ def body_element it "catches and displays a plain text message as plain text and source" do deliver_example("plainmail") - _(message_from_element.text).must_include DEFAULT_FROM - _(message_to_element.text).must_include DEFAULT_TO - _(message_subject_element.text).must_equal "Plain mail" - _(Time.parse(message_received_element.text)).must_be_close_to Time.now, 5 + expect(message_from_element.text).to include(DEFAULT_FROM) + expect(message_to_element.text).to include(DEFAULT_TO) + expect(message_subject_element.text).to include("Plain mail") + expect(Time.parse(message_received_element.text)).to be <= Time.now + 5 message_row_element.click @@ -131,28 +129,28 @@ def body_element plain_tab_element.click wait.until { iframe_element.displayed? } - _(iframe_element.attribute(:src)).must_match(/\.plain\Z/) + expect(iframe_element.attribute(:src)).to match(/\.plain\Z/) selenium.switch_to.frame(iframe_element) - _(body_element.text).wont_include "Subject: Plain mail" - _(body_element.text).must_include "Here's some text" + expect(body_element.text).not_to include("Subject: Plain mail") + expect(body_element.text).to include("Here's some text") selenium.switch_to.default_content source_tab_element.click selenium.switch_to.frame(iframe_element) - _(body_element.text).must_include "Subject: Plain mail" - _(body_element.text).must_include "Here's some text" + expect(body_element.text).to include("Subject: Plain mail") + expect(body_element.text).to include("Here's some text") end it "catches and displays an html message as html and source" do deliver_example("htmlmail") - _(message_from_element.text).must_include DEFAULT_FROM - _(message_to_element.text).must_include DEFAULT_TO - _(message_subject_element.text).must_equal "Test HTML Mail" - _(Time.parse(message_received_element.text)).must_be_close_to Time.now, 5 + expect(message_from_element.text).to include(DEFAULT_FROM) + expect(message_to_element.text).to include(DEFAULT_TO) + expect(message_subject_element.text).to eql("Test HTML Mail") + expect(Time.parse(message_received_element.text)).to be <= Time.now + 5 message_row_element.click @@ -163,30 +161,30 @@ def body_element html_tab_element.click wait.until { iframe_element.displayed? } - _(iframe_element.attribute(:src)).must_match /\.html\Z/ + expect(iframe_element.attribute(:src)).to match(/\.html\Z/) selenium.switch_to.frame(iframe_element) - _(body_element.text).must_include "Yo, you slimey scoundrel." - _(body_element.text).wont_include "Content-Type: text/html" - _(body_element.text).wont_include "Yo, you slimey scoundrel." + expect(body_element.text).to include("Yo, you slimey scoundrel.") + expect(body_element.text).not_to include("Content-Type: text/html") + expect(body_element.text).not_to include("Yo, you slimey scoundrel.") selenium.switch_to.default_content source_tab_element.click selenium.switch_to.frame(iframe_element) - _(body_element.text).must_include "Content-Type: text/html" - _(body_element.text).must_include "Yo, you slimey scoundrel." - _(body_element.text).wont_include "Yo, you slimey scoundrel." + expect(body_element.text).to include "Content-Type: text/html" + expect(body_element.text).to include "Yo, you slimey scoundrel." + expect(body_element.text).not_to include "Yo, you slimey scoundrel." end it "catches and displays a multipart message as text, html and source" do deliver_example("multipartmail") - _(message_from_element.text).must_include DEFAULT_FROM - _(message_to_element.text).must_include DEFAULT_TO - _(message_subject_element.text).must_equal "Test Multipart Mail" - _(Time.parse(message_received_element.text)).must_be_close_to Time.now, 5 + expect(message_from_element.text).to include DEFAULT_FROM + expect(message_to_element.text).to include DEFAULT_TO + expect(message_subject_element.text).to eql "Test Multipart Mail" + expect(Time.parse(message_received_element.text)).to be <= Time.now + 5 message_row_element.click @@ -197,37 +195,37 @@ def body_element plain_tab_element.click wait.until { iframe_element.displayed? } - _(iframe_element.attribute(:src)).must_match /\.plain\Z/ + expect(iframe_element.attribute(:src)).to match /\.plain\Z/ selenium.switch_to.frame(iframe_element) - _(body_element.text).must_include "Plain text mail" - _(body_element.text).wont_include "HTML mail" - _(body_element.text).wont_include "Content-Type: multipart/alternative; boundary=BOUNDARY--198849662" + expect(body_element.text).to include "Plain text mail" + expect(body_element.text).not_to include "HTML mail" + expect(body_element.text).not_to include "Content-Type: multipart/alternative; boundary=BOUNDARY--198849662" selenium.switch_to.default_content html_tab_element.click selenium.switch_to.frame(iframe_element) - _(body_element.text).must_include "HTML mail" - _(body_element.text).wont_include "Content-Type: multipart/alternative; boundary=BOUNDARY--198849662" + expect(body_element.text).to include "HTML mail" + expect(body_element.text).not_to include "Content-Type: multipart/alternative; boundary=BOUNDARY--198849662" selenium.switch_to.default_content source_tab_element.click selenium.switch_to.frame(iframe_element) - _(body_element.text).must_include "Content-Type: multipart/alternative; boundary=BOUNDARY--198849662" - _(body_element.text).must_include "Plain text mail" - _(body_element.text).must_include "HTML mail" + expect(body_element.text).to include "Content-Type: multipart/alternative; boundary=BOUNDARY--198849662" + expect(body_element.text).to include "Plain text mail" + expect(body_element.text).to include "HTML mail" end it "catches and displays a multipart UTF8 message as text, html and source" do deliver_example("multipartmail-with-utf8") - _(message_from_element.text).must_include DEFAULT_FROM - _(message_to_element.text).must_include DEFAULT_TO - _(message_subject_element.text).must_equal "Test Multipart UTF8 Mail" - _(Time.parse(message_received_element.text)).must_be_close_to Time.now, 5 + expect(message_from_element.text).to include DEFAULT_FROM + expect(message_to_element.text).to include DEFAULT_TO + expect(message_subject_element.text).to eql "Test Multipart UTF8 Mail" + expect(Time.parse(message_received_element.text)).to be <= Time.now + 5 message_row_element.click @@ -238,28 +236,28 @@ def body_element plain_tab_element.click wait.until { iframe_element.displayed? } - _(iframe_element.attribute(:src)).must_match /\.plain\Z/ + expect(iframe_element.attribute(:src)).to match /\.plain\Z/ selenium.switch_to.frame(iframe_element) - _(body_element.text).must_include "Plain text mail" - _(body_element.text).wont_include "HTML mail" - _(body_element.text).wont_include "Content-Type: multipart/alternative; boundary=BOUNDARY--198849662" + expect(body_element.text).to include "Plain text mail" + expect(body_element.text).not_to include "HTML mail" + expect(body_element.text).not_to include "Content-Type: multipart/alternative; boundary=BOUNDARY--198849662" selenium.switch_to.default_content html_tab_element.click selenium.switch_to.frame(iframe_element) - _(body_element.text).must_include "HTML mail" - _(body_element.text).wont_include "Content-Type: multipart/alternative; boundary=BOUNDARY--198849662" + expect(body_element.text).to include "HTML mail" + expect(body_element.text).not_to include "Content-Type: multipart/alternative; boundary=BOUNDARY--198849662" selenium.switch_to.default_content source_tab_element.click selenium.switch_to.frame(iframe_element) - _(body_element.text).must_include "Content-Type: multipart/alternative; boundary=BOUNDARY--198849662" - _(body_element.text).must_include "Plain text mail" - _(body_element.text).must_include "© HTML mail" + expect(body_element.text).to include "Content-Type: multipart/alternative; boundary=BOUNDARY--198849662" + expect(body_element.text).to include "Plain text mail" + expect(body_element.text).to include "© HTML mail" end it "catches and displays an unknown message as source" do From 811a3b54c2839abb4e53d3729e6e6fbb611e4699 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Thu, 15 Jul 2021 11:07:01 +0530 Subject: [PATCH 15/30] CI: setup chrome & chromedriver in github CI --- .../workflows/actions/setup-chrome/action.yml | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/workflows/actions/setup-chrome/action.yml diff --git a/.github/workflows/actions/setup-chrome/action.yml b/.github/workflows/actions/setup-chrome/action.yml new file mode 100644 index 00000000..2d3b5e4d --- /dev/null +++ b/.github/workflows/actions/setup-chrome/action.yml @@ -0,0 +1,21 @@ +name: 'Setup Chrome and chromedriver' +runs: + using: "composite" + steps: + - run: | + wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add - + echo "deb http://dl.google.com/linux/chrome/deb/ stable main" | sudo tee -a /etc/apt/sources.list.d/google-chrome.list + sudo apt-get update -qqy + sudo apt-get -qqy install google-chrome-stable + CHROME_VERSION=$(google-chrome-stable --version) + CHROME_FULL_VERSION=${CHROME_VERSION%%.*} + CHROME_MAJOR_VERSION=${CHROME_FULL_VERSION//[!0-9]} + sudo rm /etc/apt/sources.list.d/google-chrome.list + export CHROMEDRIVER_VERSION=`curl -s https://chromedriver.storage.googleapis.com/LATEST_RELEASE_${CHROME_MAJOR_VERSION%%.*}` + curl -L -O "https://chromedriver.storage.googleapis.com/${CHROMEDRIVER_VERSION}/chromedriver_linux64.zip" + unzip chromedriver_linux64.zip && chmod +x chromedriver && sudo mv chromedriver /usr/local/bin + export CHROMEDRIVER_VERSION=`curl -s https://chromedriver.storage.googleapis.com/LATEST_RELEASE_${CHROME_MAJOR_VERSION%%.*}` + curl -L -O "https://chromedriver.storage.googleapis.com/${CHROMEDRIVER_VERSION}/chromedriver_linux64.zip" + unzip chromedriver_linux64.zip && chmod +x chromedriver && sudo mv chromedriver /usr/local/bin + chromedriver -version + shell: bash \ No newline at end of file From f28a10200a2c73bb5b81388deacdc73398b4e610 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Thu, 15 Jul 2021 11:16:43 +0530 Subject: [PATCH 16/30] CI: changes chrome & chromedriver setup --- .github/workflows/ci.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 34cb0ac3..c2ab620e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,8 +24,10 @@ jobs: - uses: actions/setup-node@v2 - - uses: browser-actions/setup-chrome@latest - - uses: nanasess/setup-chromedriver@master + # - uses: browser-actions/setup-chrome@latest + # - uses: nanasess/setup-chromedriver@master + - name: Setup Chrome and chromedriver + uses: ./.github/actions/setup-chrome - name: Run tests run: bundle exec rake test From 84275721e01395e153a1cd20cc1f47a33917e903 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Thu, 15 Jul 2021 11:19:31 +0530 Subject: [PATCH 17/30] CI: changes actions folder path --- .github/{workflows => }/actions/setup-chrome/action.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/{workflows => }/actions/setup-chrome/action.yml (100%) diff --git a/.github/workflows/actions/setup-chrome/action.yml b/.github/actions/setup-chrome/action.yml similarity index 100% rename from .github/workflows/actions/setup-chrome/action.yml rename to .github/actions/setup-chrome/action.yml From 45393ce099381c6eed34e671cd604fb89bf5df8e Mon Sep 17 00:00:00 2001 From: Ahmed Date: Thu, 15 Jul 2021 11:28:44 +0530 Subject: [PATCH 18/30] CI: Removes custom chrome & chromedriver setup --- .github/actions/setup-chrome/action.yml | 21 --------------------- .github/workflows/ci.yml | 6 ++---- 2 files changed, 2 insertions(+), 25 deletions(-) delete mode 100644 .github/actions/setup-chrome/action.yml diff --git a/.github/actions/setup-chrome/action.yml b/.github/actions/setup-chrome/action.yml deleted file mode 100644 index 2d3b5e4d..00000000 --- a/.github/actions/setup-chrome/action.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: 'Setup Chrome and chromedriver' -runs: - using: "composite" - steps: - - run: | - wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add - - echo "deb http://dl.google.com/linux/chrome/deb/ stable main" | sudo tee -a /etc/apt/sources.list.d/google-chrome.list - sudo apt-get update -qqy - sudo apt-get -qqy install google-chrome-stable - CHROME_VERSION=$(google-chrome-stable --version) - CHROME_FULL_VERSION=${CHROME_VERSION%%.*} - CHROME_MAJOR_VERSION=${CHROME_FULL_VERSION//[!0-9]} - sudo rm /etc/apt/sources.list.d/google-chrome.list - export CHROMEDRIVER_VERSION=`curl -s https://chromedriver.storage.googleapis.com/LATEST_RELEASE_${CHROME_MAJOR_VERSION%%.*}` - curl -L -O "https://chromedriver.storage.googleapis.com/${CHROMEDRIVER_VERSION}/chromedriver_linux64.zip" - unzip chromedriver_linux64.zip && chmod +x chromedriver && sudo mv chromedriver /usr/local/bin - export CHROMEDRIVER_VERSION=`curl -s https://chromedriver.storage.googleapis.com/LATEST_RELEASE_${CHROME_MAJOR_VERSION%%.*}` - curl -L -O "https://chromedriver.storage.googleapis.com/${CHROMEDRIVER_VERSION}/chromedriver_linux64.zip" - unzip chromedriver_linux64.zip && chmod +x chromedriver && sudo mv chromedriver /usr/local/bin - chromedriver -version - shell: bash \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c2ab620e..34cb0ac3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,10 +24,8 @@ jobs: - uses: actions/setup-node@v2 - # - uses: browser-actions/setup-chrome@latest - # - uses: nanasess/setup-chromedriver@master - - name: Setup Chrome and chromedriver - uses: ./.github/actions/setup-chrome + - uses: browser-actions/setup-chrome@latest + - uses: nanasess/setup-chromedriver@master - name: Run tests run: bundle exec rake test From cf4df418809620f449c492bdfd7c3e329bfd4cd5 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Thu, 15 Jul 2021 11:51:29 +0530 Subject: [PATCH 19/30] DEV: update smtp.rb as suggested & change dependency order --- lib/mail_catcher/smtp.rb | 2 +- mailcatcher.gemspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/mail_catcher/smtp.rb b/lib/mail_catcher/smtp.rb index 050a568b..6eaaabbf 100644 --- a/lib/mail_catcher/smtp.rb +++ b/lib/mail_catcher/smtp.rb @@ -335,7 +335,7 @@ def each(task: Async::Task.current) write_response 501, 'Unexpected parameters or arguments' next end - envelope = nil + @state.clear write_response 250, 'OK' when 'NOOP' if line && !line.empty? diff --git a/mailcatcher.gemspec b/mailcatcher.gemspec index 32a08bef..8f525222 100644 --- a/mailcatcher.gemspec +++ b/mailcatcher.gemspec @@ -44,9 +44,9 @@ Gem::Specification.new do |s| s.add_development_dependency "coffee-script" s.add_development_dependency "compass", "~> 1.0.3" - s.add_development_dependency "rspec" s.add_development_dependency "rake" s.add_development_dependency "rdoc" + s.add_development_dependency "rspec" s.add_development_dependency "sass" s.add_development_dependency "selenium-webdriver", "~> 3.7" s.add_development_dependency "sprockets" From c35e63aa2800001d6014bc7c5b75b9417aac1d5b Mon Sep 17 00:00:00 2001 From: Ahmed Date: Thu, 15 Jul 2021 15:50:48 +0530 Subject: [PATCH 20/30] DEV: Adds SMTP::URLEndpoint spec --- lib/mail_catcher/smtp.rb | 4 +- spec/smtp_spec.rb | 88 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 spec/smtp_spec.rb diff --git a/lib/mail_catcher/smtp.rb b/lib/mail_catcher/smtp.rb index 6eaaabbf..1fc4adf7 100644 --- a/lib/mail_catcher/smtp.rb +++ b/lib/mail_catcher/smtp.rb @@ -25,7 +25,7 @@ def initialize(url, endpoint = nil, **options) end def to_s - "\#<#{self.class} #{@url} #{@options.inspect}>" + @url.to_s end attr :url, :options @@ -71,7 +71,7 @@ def authority end def path - @url.request_uri + @url.path end LOCALHOST = 'localhost' diff --git a/spec/smtp_spec.rb b/spec/smtp_spec.rb new file mode 100644 index 00000000..6c5e0a89 --- /dev/null +++ b/spec/smtp_spec.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +ENV["MAILCATCHER_ENV"] ||= "test" + +require "mail_catcher" + +SMTP_PORT = 20025 + +def smtp_url_1 + "#{scheme}://#{ip}:#{SMTP_PORT}" +end + +def smtp_url_2 + "#{scheme}s://#{ip}#{path}" +end + +def path + '/path' +end + +def scheme + 'smtp' +end + +def ip + '127.0.0.1' +end + +def url_endpoint_1 + MailCatcher::SMTP::URLEndpoint.new(URI.parse(smtp_url_1), SMTP_PORT) +end + +def url_endpoint_2 + MailCatcher::SMTP::URLEndpoint.new(URI.parse(smtp_url_2), SMTP_PORT) +end + +RSpec.describe MailCatcher::SMTP do + describe MailCatcher::SMTP::URLEndpoint do + it 'returns string url' do + expect(url_endpoint_1.to_s).to eql(smtp_url_1) + expect(url_endpoint_2.to_s).to eql(smtp_url_2) + end + + it 'returns if endpoint is secure or not' do + expect(url_endpoint_1.secure?).to be(false) + expect(url_endpoint_2.secure?).to be(true) + end + + it 'returns endpoint protocol' do + expect(url_endpoint_1.protocol).to be(MailCatcher::SMTP::Protocol::SMTP) + expect(url_endpoint_2.protocol).to be(MailCatcher::SMTP::Protocol::SMTP) + end + + it 'returns default_port' do + expect(url_endpoint_1.default_port).to be(25) + end + + it 'returns if endpoint used default_port' do + expect(url_endpoint_1.default_port?).to be(false) + expect(url_endpoint_2.default_port?).to be(true) + end + + it 'returns endpoint port' do + expect(url_endpoint_1.port).to be(SMTP_PORT) + expect(url_endpoint_2.port).to be(url_endpoint_2.default_port) + end + + it 'returns hostname of endpoint' do + expect(url_endpoint_1.hostname).to eql(ip) + expect(url_endpoint_2.hostname).to eql(ip) + end + + it 'returns scheme of endpoint' do + expect(url_endpoint_1.scheme).to eql(scheme) + expect(url_endpoint_2.scheme).to eql("#{scheme}s") + end + + it 'checks authority of endpoint' do + expect(url_endpoint_1.authority).to eql("#{ip}:#{SMTP_PORT}") + expect(url_endpoint_2.authority).to eql(ip) + end + + it 'returns path of endpoint' do + expect(url_endpoint_1.path).to eql('') + expect(url_endpoint_2.path).to eql(path) + end + end +end From fad3fa2915a40a3d1f1796a6aed083b70f0258d2 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Fri, 16 Jul 2021 12:00:31 +0530 Subject: [PATCH 21/30] DEV: adds more test for SMTP::URLEndpoint class --- spec/smtp_spec.rb | 118 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 87 insertions(+), 31 deletions(-) diff --git a/spec/smtp_spec.rb b/spec/smtp_spec.rb index 6c5e0a89..04c86493 100644 --- a/spec/smtp_spec.rb +++ b/spec/smtp_spec.rb @@ -26,63 +26,119 @@ def ip '127.0.0.1' end +def http_address + Async::IO::Address.tcp(ip, SMTP_PORT) +end + +def http_endpoint + Async::IO::AddressEndpoint.new(http_address) +end + def url_endpoint_1 - MailCatcher::SMTP::URLEndpoint.new(URI.parse(smtp_url_1), SMTP_PORT) + MailCatcher::SMTP::URLEndpoint.new(URI.parse(smtp_url_1), http_endpoint) end def url_endpoint_2 - MailCatcher::SMTP::URLEndpoint.new(URI.parse(smtp_url_2), SMTP_PORT) + MailCatcher::SMTP::URLEndpoint.new(URI.parse(smtp_url_2), http_endpoint, reuse_port: true) end RSpec.describe MailCatcher::SMTP do describe MailCatcher::SMTP::URLEndpoint do - it 'returns string url' do - expect(url_endpoint_1.to_s).to eql(smtp_url_1) - expect(url_endpoint_2.to_s).to eql(smtp_url_2) + context '#to_s' do + it 'returns string url' do + expect(url_endpoint_1.to_s).to eql(smtp_url_1) + expect(url_endpoint_2.to_s).to eql(smtp_url_2) + end + end + + context '#secure?' do + it 'returns if endpoint is secure or not' do + expect(url_endpoint_1.secure?).to be(false) + expect(url_endpoint_2.secure?).to be(true) + end + end + + context '#protocol' do + it 'returns endpoint protocol' do + expect(url_endpoint_1.protocol).to be(MailCatcher::SMTP::Protocol::SMTP) + expect(url_endpoint_2.protocol).to be(MailCatcher::SMTP::Protocol::SMTP) + end + end + + context '#default_port' do + it 'returns default_port' do + expect(url_endpoint_1.default_port).to be(25) + end + end + + context '#default_port?' do + it 'returns if endpoint used default_port' do + expect(url_endpoint_1.default_port?).to be(false) + expect(url_endpoint_2.default_port?).to be(true) + end end - it 'returns if endpoint is secure or not' do - expect(url_endpoint_1.secure?).to be(false) - expect(url_endpoint_2.secure?).to be(true) + context '#port' do + it 'returns endpoint port' do + expect(url_endpoint_1.port).to be(SMTP_PORT) + expect(url_endpoint_2.port).to be(url_endpoint_2.default_port) + end end - it 'returns endpoint protocol' do - expect(url_endpoint_1.protocol).to be(MailCatcher::SMTP::Protocol::SMTP) - expect(url_endpoint_2.protocol).to be(MailCatcher::SMTP::Protocol::SMTP) + context '#hostname' do + it 'returns hostname of endpoint' do + expect(url_endpoint_1.hostname).to eql(ip) + expect(url_endpoint_2.hostname).to eql(ip) + end end - it 'returns default_port' do - expect(url_endpoint_1.default_port).to be(25) + context '#scheme' do + it 'returns scheme of endpoint' do + expect(url_endpoint_1.scheme).to eql(scheme) + expect(url_endpoint_2.scheme).to eql("#{scheme}s") + end end - it 'returns if endpoint used default_port' do - expect(url_endpoint_1.default_port?).to be(false) - expect(url_endpoint_2.default_port?).to be(true) + context '#authority' do + it 'checks authority of endpoint' do + expect(url_endpoint_1.authority).to eql("#{ip}:#{SMTP_PORT}") + expect(url_endpoint_2.authority).to eql(ip) + end end - it 'returns endpoint port' do - expect(url_endpoint_1.port).to be(SMTP_PORT) - expect(url_endpoint_2.port).to be(url_endpoint_2.default_port) + context '#path' do + it 'returns path of endpoint' do + expect(url_endpoint_1.path).to eql('') + expect(url_endpoint_2.path).to eql(path) + end end - it 'returns hostname of endpoint' do - expect(url_endpoint_1.hostname).to eql(ip) - expect(url_endpoint_2.hostname).to eql(ip) + context '#ssl_verify_mode' do + it 'checks ssl is in verify mode for the endpoint' do + expect(url_endpoint_1.ssl_verify_mode).to be(OpenSSL::SSL::VERIFY_PEER) + expect(url_endpoint_2.ssl_verify_mode).to be(OpenSSL::SSL::VERIFY_PEER) + end end - it 'returns scheme of endpoint' do - expect(url_endpoint_1.scheme).to eql(scheme) - expect(url_endpoint_2.scheme).to eql("#{scheme}s") + context '#ssl_context' do + it 'returns ssl context of the endpoint' do + expect(url_endpoint_1.ssl_context.class).to be(OpenSSL::SSL::SSLContext) + expect(url_endpoint_2.ssl_context.class).to be(OpenSSL::SSL::SSLContext) + end end - it 'checks authority of endpoint' do - expect(url_endpoint_1.authority).to eql("#{ip}:#{SMTP_PORT}") - expect(url_endpoint_2.authority).to eql(ip) + context '#tcp_options' do + it 'returns tcp_options of the endpoint' do + expect(url_endpoint_1.tcp_options).to eql({ :reuse_port => false }) + expect(url_endpoint_2.tcp_options).to eql({ :reuse_port => true }) + end end - it 'returns path of endpoint' do - expect(url_endpoint_1.path).to eql('') - expect(url_endpoint_2.path).to eql(path) + context '#build_endpoint' do + it 'builds smtp endpoint for the endpoint' do + expect(url_endpoint_1.build_endpoint.class).to be(Async::IO::HostEndpoint) + expect(url_endpoint_2.build_endpoint.class).to be(Async::IO::SSLEndpoint) + end end end end From ae2e7f89f2c7ca93c9afcfeb3bfb2bb976d3f8de Mon Sep 17 00:00:00 2001 From: Ahmed Date: Sat, 17 Jul 2021 12:27:41 +0530 Subject: [PATCH 22/30] RSPEC: Removes method and uses let --- spec/smtp_spec.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/spec/smtp_spec.rb b/spec/smtp_spec.rb index 04c86493..7549f0a6 100644 --- a/spec/smtp_spec.rb +++ b/spec/smtp_spec.rb @@ -44,6 +44,16 @@ def url_endpoint_2 RSpec.describe MailCatcher::SMTP do describe MailCatcher::SMTP::URLEndpoint do + let(:scheme) { 'smtp' } + let(:path) { '/path' } + let(:ip) { '127.0.0.1' } + let(:smtp_url_1) { "#{scheme}://#{ip}:#{SMTP_PORT}" } + let(:smtp_url_2) { "#{scheme}s://#{ip}#{path}" } + let(:http_address) { Async::IO::Address.tcp(ip, SMTP_PORT) } + let(:http_endpoint) { Async::IO::AddressEndpoint.new(http_address) } + let(:url_endpoint_1) { MailCatcher::SMTP::URLEndpoint.new(URI.parse(smtp_url_1), http_endpoint) } + let(:url_endpoint_2) { MailCatcher::SMTP::URLEndpoint.new(URI.parse(smtp_url_2), http_endpoint, reuse_port: true) } + context '#to_s' do it 'returns string url' do expect(url_endpoint_1.to_s).to eql(smtp_url_1) From 56e6bfa133808c9b35187431c533aa8e8893c5d9 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Sat, 17 Jul 2021 12:28:34 +0530 Subject: [PATCH 23/30] DEV: Removes methods from smtp_spec file --- spec/smtp_spec.rb | 36 ------------------------------------ 1 file changed, 36 deletions(-) diff --git a/spec/smtp_spec.rb b/spec/smtp_spec.rb index 7549f0a6..59ebcc90 100644 --- a/spec/smtp_spec.rb +++ b/spec/smtp_spec.rb @@ -6,42 +6,6 @@ SMTP_PORT = 20025 -def smtp_url_1 - "#{scheme}://#{ip}:#{SMTP_PORT}" -end - -def smtp_url_2 - "#{scheme}s://#{ip}#{path}" -end - -def path - '/path' -end - -def scheme - 'smtp' -end - -def ip - '127.0.0.1' -end - -def http_address - Async::IO::Address.tcp(ip, SMTP_PORT) -end - -def http_endpoint - Async::IO::AddressEndpoint.new(http_address) -end - -def url_endpoint_1 - MailCatcher::SMTP::URLEndpoint.new(URI.parse(smtp_url_1), http_endpoint) -end - -def url_endpoint_2 - MailCatcher::SMTP::URLEndpoint.new(URI.parse(smtp_url_2), http_endpoint, reuse_port: true) -end - RSpec.describe MailCatcher::SMTP do describe MailCatcher::SMTP::URLEndpoint do let(:scheme) { 'smtp' } From 865dbb1b3d4cd3aae7015fcf96c6e7e0262a04be Mon Sep 17 00:00:00 2001 From: Ahmed Date: Sat, 17 Jul 2021 13:31:18 +0530 Subject: [PATCH 24/30] DEV: Adds rspec test for SMTP::Protocol::SMTP::Server class --- spec/smtp_protocol_spec.rb | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 spec/smtp_protocol_spec.rb diff --git a/spec/smtp_protocol_spec.rb b/spec/smtp_protocol_spec.rb new file mode 100644 index 00000000..31ce596b --- /dev/null +++ b/spec/smtp_protocol_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'async/io/protocol/line' +require 'async/io/socket' +require 'mail_catcher' + +RSpec.describe MailCatcher::SMTP::Protocol::SMTP::Server do + let(:pipe) { @pipe = Async::IO::Socket.pair(Socket::AF_UNIX, Socket::SOCK_STREAM) } + let(:remote) { pipe.first } + let(:code) { 220 } + subject { described_class.new(Async::IO::Stream.new(pipe.last, deferred: true)) } + + after(:each) { defined?(@pipe) && @pipe&.each(&:close) } + + describe '#write_response' do + it "should write response" do + subject.write_response(code, 'Hello World') + subject.close + + expect(remote.read.strip).to eql("#{code} Hello World") + end + end + + describe '#read_line' do + before(:each) do + remote.write("Hello World\n") + remote.close + end + + it "should read one line" do + expect(subject.read_line).to be == "Hello World" + end + + it "should be binary encoding" do + expect(subject.read_line.encoding).to be == Encoding::BINARY + end + end +end From c1d19e0ad19bc614d8328b9bbc49cb40eb0dc3c4 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Tue, 20 Jul 2021 16:09:01 +0530 Subject: [PATCH 25/30] DEV: Adds rspec for SMTP::Protocol --- spec/smtp_protocol_spec.rb | 91 +++++++++++++++++++++++++++++++++++++- 1 file changed, 89 insertions(+), 2 deletions(-) diff --git a/spec/smtp_protocol_spec.rb b/spec/smtp_protocol_spec.rb index 31ce596b..b9a47222 100644 --- a/spec/smtp_protocol_spec.rb +++ b/spec/smtp_protocol_spec.rb @@ -3,16 +3,49 @@ require 'async/io/protocol/line' require 'async/io/socket' require 'mail_catcher' +require 'net/smtp' + +message = < +To: Destination Address +Subject: test message +Date: Sat, 23 Jun 2001 16:26:43 +0900 +Message-Id: + +This is a test message. +END_OF_MESSAGE RSpec.describe MailCatcher::SMTP::Protocol::SMTP::Server do let(:pipe) { @pipe = Async::IO::Socket.pair(Socket::AF_UNIX, Socket::SOCK_STREAM) } let(:remote) { pipe.first } let(:code) { 220 } subject { described_class.new(Async::IO::Stream.new(pipe.last, deferred: true)) } + let(:domain) { 'test@mail.com' } after(:each) { defined?(@pipe) && @pipe&.each(&:close) } - describe '#write_response' do + SMTP_PORT = 20025 + HTTP_PORT = 20080 + + before :all do + # Start MailCatcher + @pid = spawn "bundle", "exec", "mailcatcher", "--foreground", "--smtp-port", SMTP_PORT.to_s, "--http-port", HTTP_PORT.to_s + + # Wait for it to boot + begin + TCPSocket.new("127.0.0.1", SMTP_PORT).close + TCPSocket.new("127.0.0.1", HTTP_PORT).close + rescue Errno::ECONNREFUSED + retry + end + end + + after :all do + # Quit MailCatcher at the end + Process.kill("TERM", @pid) and Process.wait + end + + context '#write_response' do it "should write response" do subject.write_response(code, 'Hello World') subject.close @@ -21,7 +54,7 @@ end end - describe '#read_line' do + context '#read_line' do before(:each) do remote.write("Hello World\n") remote.close @@ -35,4 +68,58 @@ expect(subject.read_line.encoding).to be == Encoding::BINARY end end + + context "#each" do + it 'returns 250 code when HELO command' do + Net::SMTP.start('127.0.0.1', SMTP_PORT) do |smtp| + expect(smtp.helo(domain).status.to_i).to eql(250) + end + end + + it 'returns 250 code when EHLO command' do + Net::SMTP.start('127.0.0.1', SMTP_PORT) do |smtp| + expect(smtp.ehlo(domain).status.to_i).to eql(250) + end + end + + it 'raise Net::SMTPSyntaxError error when RCPT command' do + Net::SMTP.start('127.0.0.1', SMTP_PORT) do |smtp| + expect{ smtp.rcptto(domain).status.to_i }.to raise_error(Net::SMTPSyntaxError) + end + end + + it 'raise Net::SMTPUnknownError when DATA command used' do + Net::SMTP.start('127.0.0.1', SMTP_PORT) do |smtp| + expect{ smtp.data("From: john@example.com + To: betty@example.com + Subject: I found a bug + + Check vm.c:58879. + ") }.to raise_error(Net::SMTPUnknownError) + end + end + + it 'returns 250 code when RSET command' do + Net::SMTP.start('127.0.0.1', SMTP_PORT) do |smtp| + expect(smtp.rset.status.to_i).to eql(250) + end + end + + it 'returns 250 code MAIL command' do + Net::SMTP.start('127.0.0.1', SMTP_PORT) do |smtp| + expect(smtp.send_message(message, 'your@example.com', 'to@example.com').status.to_i).to eql(250) + end + end + + it 'returns 502 code STARTTLS command' do + Net::SMTP.start('127.0.0.1', SMTP_PORT) do |smtp| + expect{ smtp.starttls }.to raise_error(Net::SMTPSyntaxError) + end + end + + it 'returns 221 code QUIT command' do + smtp = Net::SMTP.start('127.0.0.1', SMTP_PORT) + expect(smtp.quit.status.to_i).to eql(221) + end + end end From 6fb8c25c71a6e8960ff04070cd49b5e99ff6f23a Mon Sep 17 00:00:00 2001 From: Ahmed Date: Tue, 20 Jul 2021 16:16:27 +0530 Subject: [PATCH 26/30] DEV: Removes CHUNKING --- lib/mail_catcher/smtp.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mail_catcher/smtp.rb b/lib/mail_catcher/smtp.rb index 1fc4adf7..0f9338a5 100644 --- a/lib/mail_catcher/smtp.rb +++ b/lib/mail_catcher/smtp.rb @@ -196,7 +196,7 @@ def each(task: Async::Task.current) when 'HELO' write_response 250, hostname when 'EHLO' - write_response 250, hostname, 'CHUNKING', '8BITMIME', 'BINARYMIME', 'SMTPUTF8' + write_response 250, hostname, '8BITMIME', 'BINARYMIME', 'SMTPUTF8' when 'SEND' write_response 502, 'Command not implemented' when 'SOML' From a06b7e230afeb5dd3a3779ad9e292a0fa122cdf0 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Tue, 20 Jul 2021 17:17:40 +0530 Subject: [PATCH 27/30] DEV: Linting --- spec/smtp_protocol_spec.rb | 191 +++++++++++++++++++------------------ 1 file changed, 97 insertions(+), 94 deletions(-) diff --git a/spec/smtp_protocol_spec.rb b/spec/smtp_protocol_spec.rb index b9a47222..4117afe8 100644 --- a/spec/smtp_protocol_spec.rb +++ b/spec/smtp_protocol_spec.rb @@ -5,121 +5,124 @@ require 'mail_catcher' require 'net/smtp' -message = < -To: Destination Address -Subject: test message -Date: Sat, 23 Jun 2001 16:26:43 +0900 -Message-Id: - -This is a test message. +message = <<~END_OF_MESSAGE + From: Your Name + To: Destination Address + Subject: test message + Date: Sat, 23 Jun 2001 16:26:43 +0900 + Message-Id: + #{' '} + This is a test message. END_OF_MESSAGE +SMTP_PORT = 20_025 +HTTP_PORT = 20_080 + RSpec.describe MailCatcher::SMTP::Protocol::SMTP::Server do let(:pipe) { @pipe = Async::IO::Socket.pair(Socket::AF_UNIX, Socket::SOCK_STREAM) } - let(:remote) { pipe.first } + let(:remote) { pipe.first } let(:code) { 220 } - subject { described_class.new(Async::IO::Stream.new(pipe.last, deferred: true)) } - let(:domain) { 'test@mail.com' } - - after(:each) { defined?(@pipe) && @pipe&.each(&:close) } + subject { described_class.new(Async::IO::Stream.new(pipe.last, deferred: true)) } + let(:domain) { 'test@mail.com' } - SMTP_PORT = 20025 - HTTP_PORT = 20080 + after(:each) { defined?(@pipe) && @pipe&.each(&:close) } - before :all do + before :all do # Start MailCatcher - @pid = spawn "bundle", "exec", "mailcatcher", "--foreground", "--smtp-port", SMTP_PORT.to_s, "--http-port", HTTP_PORT.to_s + @pid = spawn 'bundle', 'exec', 'mailcatcher', '--foreground', '--smtp-port', SMTP_PORT.to_s, '--http-port', + HTTP_PORT.to_s # Wait for it to boot begin - TCPSocket.new("127.0.0.1", SMTP_PORT).close - TCPSocket.new("127.0.0.1", HTTP_PORT).close + TCPSocket.new('127.0.0.1', SMTP_PORT).close + TCPSocket.new('127.0.0.1', HTTP_PORT).close rescue Errno::ECONNREFUSED retry end end - after :all do + after :all do # Quit MailCatcher at the end - Process.kill("TERM", @pid) and Process.wait + Process.kill('TERM', @pid) and Process.wait end context '#write_response' do - it "should write response" do - subject.write_response(code, 'Hello World') - subject.close + it 'should write response' do + subject.write_response(code, 'Hello World') + subject.close - expect(remote.read.strip).to eql("#{code} Hello World") - end - end + expect(remote.read.strip).to eql("#{code} Hello World") + end + end context '#read_line' do - before(:each) do - remote.write("Hello World\n") - remote.close - end - - it "should read one line" do - expect(subject.read_line).to be == "Hello World" - end - - it "should be binary encoding" do - expect(subject.read_line.encoding).to be == Encoding::BINARY - end - end - - context "#each" do - it 'returns 250 code when HELO command' do - Net::SMTP.start('127.0.0.1', SMTP_PORT) do |smtp| - expect(smtp.helo(domain).status.to_i).to eql(250) - end - end - - it 'returns 250 code when EHLO command' do - Net::SMTP.start('127.0.0.1', SMTP_PORT) do |smtp| - expect(smtp.ehlo(domain).status.to_i).to eql(250) - end - end - - it 'raise Net::SMTPSyntaxError error when RCPT command' do - Net::SMTP.start('127.0.0.1', SMTP_PORT) do |smtp| - expect{ smtp.rcptto(domain).status.to_i }.to raise_error(Net::SMTPSyntaxError) - end - end - - it 'raise Net::SMTPUnknownError when DATA command used' do - Net::SMTP.start('127.0.0.1', SMTP_PORT) do |smtp| - expect{ smtp.data("From: john@example.com - To: betty@example.com - Subject: I found a bug - - Check vm.c:58879. - ") }.to raise_error(Net::SMTPUnknownError) - end - end - - it 'returns 250 code when RSET command' do - Net::SMTP.start('127.0.0.1', SMTP_PORT) do |smtp| - expect(smtp.rset.status.to_i).to eql(250) - end - end - - it 'returns 250 code MAIL command' do - Net::SMTP.start('127.0.0.1', SMTP_PORT) do |smtp| - expect(smtp.send_message(message, 'your@example.com', 'to@example.com').status.to_i).to eql(250) - end - end - - it 'returns 502 code STARTTLS command' do - Net::SMTP.start('127.0.0.1', SMTP_PORT) do |smtp| - expect{ smtp.starttls }.to raise_error(Net::SMTPSyntaxError) - end - end - - it 'returns 221 code QUIT command' do - smtp = Net::SMTP.start('127.0.0.1', SMTP_PORT) - expect(smtp.quit.status.to_i).to eql(221) - end - end + before(:each) do + remote.write("Hello World\n") + remote.close + end + + it 'should read one line' do + expect(subject.read_line).to be == 'Hello World' + end + + it 'should be binary encoding' do + expect(subject.read_line.encoding).to be == Encoding::BINARY + end + end + + context '#each' do + it 'returns 250 code when HELO command' do + Net::SMTP.start('127.0.0.1', SMTP_PORT) do |smtp| + expect(smtp.helo(domain).status.to_i).to eql(250) + end + end + + it 'returns 250 code when EHLO command' do + Net::SMTP.start('127.0.0.1', SMTP_PORT) do |smtp| + expect(smtp.ehlo(domain).status.to_i).to eql(250) + end + end + + it 'raise Net::SMTPSyntaxError error when RCPT command' do + Net::SMTP.start('127.0.0.1', SMTP_PORT) do |smtp| + expect { smtp.rcptto(domain).status.to_i }.to raise_error(Net::SMTPSyntaxError) + end + end + + it 'raise Net::SMTPUnknownError when DATA command used' do + Net::SMTP.start('127.0.0.1', SMTP_PORT) do |smtp| + expect do + smtp.data("From: john@example.com + To: betty@example.com + Subject: I found a bug + + Check vm.c:58879. + ") + end.to raise_error(Net::SMTPUnknownError) + end + end + + it 'returns 250 code when RSET command' do + Net::SMTP.start('127.0.0.1', SMTP_PORT) do |smtp| + expect(smtp.rset.status.to_i).to eql(250) + end + end + + it 'returns 250 code MAIL command' do + Net::SMTP.start('127.0.0.1', SMTP_PORT) do |smtp| + expect(smtp.send_message(message, 'your@example.com', 'to@example.com').status.to_i).to eql(250) + end + end + + it 'returns 502 code STARTTLS command' do + Net::SMTP.start('127.0.0.1', SMTP_PORT) do |smtp| + expect { smtp.starttls }.to raise_error(Net::SMTPSyntaxError) + end + end + + it 'returns 221 code QUIT command' do + smtp = Net::SMTP.start('127.0.0.1', SMTP_PORT) + expect(smtp.quit.status.to_i).to eql(221) + end + end end From adbc161874053282c9ebb83018c618b3f8eb813e Mon Sep 17 00:00:00 2001 From: Ahmed Date: Thu, 22 Jul 2021 17:18:18 +0530 Subject: [PATCH 28/30] DEV: fix ruby 3.0 working --- lib/mail_catcher/smtp.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/mail_catcher/smtp.rb b/lib/mail_catcher/smtp.rb index 0f9338a5..44fea92c 100644 --- a/lib/mail_catcher/smtp.rb +++ b/lib/mail_catcher/smtp.rb @@ -162,7 +162,7 @@ class Server < Async::IO::Protocol::Line COLON = ':' DOT = '.' - def initialize(stream, hostname: nil) + def initialize(stream, *args) super(stream, CRLF) @hostname = hostname @@ -377,11 +377,11 @@ def each(task: Async::Task.current) require 'async/io/stream' class Server - def initialize(endpoint, protocol = endpoint.protocol) + def initialize(endpoint, protocol = endpoint.protocol, &block) @endpoint = endpoint @protocol = protocol - define_singleton_method(:call, &proc) if block_given? + define_singleton_method(:call, block) if block end def accept(peer, address, task: Async::Task.current) From ae93635cb88275cc3a817b6efa9cc82b19d81d59 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Mon, 26 Jul 2021 11:53:01 +0530 Subject: [PATCH 29/30] DEV: always convert command to upcase --- lib/mail_catcher/smtp.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mail_catcher/smtp.rb b/lib/mail_catcher/smtp.rb index 44fea92c..328079d3 100644 --- a/lib/mail_catcher/smtp.rb +++ b/lib/mail_catcher/smtp.rb @@ -192,7 +192,7 @@ def each(task: Async::Task.current) loop do line = read_line command, line = line.split(SP, 2) - case command + case command.upcase when 'HELO' write_response 250, hostname when 'EHLO' From 6815f296fda555e5263720e41469c4637a872220 Mon Sep 17 00:00:00 2001 From: Ahmed Gagan Date: Thu, 29 Jul 2021 11:49:48 +0530 Subject: [PATCH 30/30] Update spec/smtp_protocol_spec.rb Co-authored-by: Samuel Cochran --- spec/smtp_protocol_spec.rb | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/spec/smtp_protocol_spec.rb b/spec/smtp_protocol_spec.rb index 4117afe8..f2ea6b22 100644 --- a/spec/smtp_protocol_spec.rb +++ b/spec/smtp_protocol_spec.rb @@ -5,15 +5,15 @@ require 'mail_catcher' require 'net/smtp' -message = <<~END_OF_MESSAGE - From: Your Name - To: Destination Address - Subject: test message - Date: Sat, 23 Jun 2001 16:26:43 +0900 - Message-Id: - #{' '} - This is a test message. -END_OF_MESSAGE +message = <<~MESSAGE + From: Your Name + To: Destination Address + Subject: test message + Date: Sat, 23 Jun 2001 16:26:43 +0900 + Message-Id: + + This is a test message. +MESSAGE SMTP_PORT = 20_025 HTTP_PORT = 20_080