class MidiSmtpServer::Smtpd

class for SmtpServer

Attributes

auth_mode[R]

Authentification mode

crlf_mode[R]

CRLF handling based on conformity to RFC(2)822

do_dns_reverse_lookup[R]

Flag if should do reverse DNS lookups on incoming connections

encrypt_mode[R]

Encryption mode

internationalization_extensions[R]

handle SMTP 8BITMIME and SMTPUTF8 extension

io_buffer_chunk_size[R]

Bytes to read non-blocking from socket into buffer, as a FixNum

io_buffer_max_size[R]

Maximum bytes to read as buffer before expecting completet incoming data line, as a FixNum

io_cmd_timeout[R]

Maximum time in seconds to wait for a complete incoming data line, as a FixNum

logger[R]

logging object, may be overrriden by special loggers like YELL or others

max_connections[R]

Maximum number of allowed connections, this does limit the TCP connections, as a FixNum

max_processings[R]

Maximum number of simultaneous processed connections, this does not limit the TCP connections itself, as a FixNum

pipelining_extension[R]

handle SMTP PIPELINING extension

Public Class Methods

new( ports: DEFAULT_SMTPD_PORT, hosts: DEFAULT_SMTPD_HOST, max_processings: DEFAULT_SMTPD_MAX_PROCESSINGS, max_connections: nil, crlf_mode: nil, do_dns_reverse_lookup: nil, io_cmd_timeout: nil, io_buffer_chunk_size: nil, io_buffer_max_size: nil, pipelining_extension: nil, internationalization_extensions: nil, auth_mode: nil, tls_mode: nil, tls_cert_path: nil, tls_key_path: nil, tls_ciphers: nil, tls_methods: nil, tls_cert_cn: nil, tls_cert_san: nil, logger: nil, logger_severity: nil ) click to toggle source

Initialize SMTP Server class

ports

ports to listen on. Allows multiple ports like “2525, 3535” or “2525:3535, 2525”. Default value = DEFAULT_SMTPD_PORT

hosts

interface ip or hostname to listen on or “*” to listen on all interfaces, allows multiple hostnames and ip_addresses like “name.domain.com, 127.0.0.1, ::1”. Default value = DEFAULT_SMTPD_HOST

max_processings

maximum number of simultaneous processed connections, this does not limit the number of concurrent TCP connections. Default value = DEFAULT_SMTPD_MAX_PROCESSINGS

max_connections

maximum number of connections, this does limit the number of concurrent TCP connections (not set or nil => unlimited)

crlf_mode

CRLF handling support (:CRLF_ENSURE [default], :CRLF_LEAVE, :CRLF_STRICT)

do_dns_reverse_lookup

flag if this smtp server should do reverse DNS lookups on incoming connections

io_cmd_timeout

time in seconds to wait until complete line of data is expected (DEFAULT_IO_CMD_TIMEOUT, nil => disabled test)

io_buffer_chunk_size

size of chunks (bytes) to read non-blocking from socket (DEFAULT_IO_BUFFER_CHUNK_SIZE)

io_buffer_max_size

max size of buffer (max line length) until lf ist expected (DEFAULT_IO_BUFFER_MAX_SIZE, nil => disabled test)

pipelining_extension

set to true for support of SMTP PIPELINING extension (DEFAULT_PIPELINING_EXTENSION_ENABLED)

internationalization_extensions

set to true for support of SMTP 8BITMIME and SMTPUTF8 extensions (DEFAULT_INTERNATIONALIZATION_EXTENSIONS_ENABLED)

auth_mode

enable builtin authentication support (:AUTH_FORBIDDEN [default], :AUTH_OPTIONAL, :AUTH_REQUIRED)

tls_mode

enable builtin TLS support (:TLS_FORBIDDEN [default], :TLS_OPTIONAL, :TLS_REQUIRED)

tls_cert_path

path to tls cerificate chain file

tls_key_path

path to tls key file

tls_ciphers

allowed ciphers for connection

tls_methods

allowed methods for protocol

tls_cert_cn

set subject (CN) for self signed certificate “cn.domain.com”

tls_cert_san

set subject alternative (SAN) for self signed certificate, allows multiple names like “alt1.domain.com, alt2.domain.com”

logger

own logger class, otherwise default logger is created (DEPRECATED: use on_logging_event for special needs on logging)

logger_severity

set or override the logger level if given

# File lib/midi-smtp-server.rb, line 196
def initialize(
  ports: DEFAULT_SMTPD_PORT,
  hosts: DEFAULT_SMTPD_HOST,
  max_processings: DEFAULT_SMTPD_MAX_PROCESSINGS,
  max_connections: nil,
  crlf_mode: nil,
  do_dns_reverse_lookup: nil,
  io_cmd_timeout: nil,
  io_buffer_chunk_size: nil,
  io_buffer_max_size: nil,
  pipelining_extension: nil,
  internationalization_extensions: nil,
  auth_mode: nil,
  tls_mode: nil,
  tls_cert_path: nil,
  tls_key_path: nil,
  tls_ciphers: nil,
  tls_methods: nil,
  tls_cert_cn: nil,
  tls_cert_san: nil,
  logger: nil,
  logger_severity: nil
)
  # create an exposed logger to forward loggings to the on_logging_event
  @logger = MidiSmtpServer::ForwardingLogger.new(method(:on_logging_event))

  # external logging
  if logger.nil?
    @logger_protected = Logger.new($stdout)
    @logger_protected.datetime_format = '%Y-%m-%d %H:%M:%S'
    @logger_protected.formatter = proc { |severity, datetime, _progname, msg| "#{datetime}: [#{severity}] #{msg.chomp}\n" }
    @logger_protected.level = logger_severity.nil? ? Logger::DEBUG : logger_severity
  else
    @logger_protected = logger
    @logger_protected.level = logger_severity unless logger_severity.nil?
    logger.warn('Deprecated: "logger" was set on new! Please use "on_logging_event" instead.')
  end

  # list of TCPServers
  @tcp_servers = []
  # list of running threads
  @tcp_server_threads = []

  # lists for connections and thread management
  @connections = []
  @processings = []
  @connections_mutex = Mutex.new
  @connections_cv = ConditionVariable.new

  # settings

  # build array of ports
  # split string into array to instantiate multiple servers
  @ports = ports.to_s.delete(' ').split(',')
  # check for at least one port specification
  raise 'Missing port(s) to bind service(s) to!' if @ports.empty?
  # check that not also a '' empty item for port is added to the list
  raise 'Do not use empty value "" for port(s). Please use specific port(s)!' if @ports.include?('')

  # build array of hosts
  # split string into array to instantiate multiple servers
  @hosts = hosts.to_s.delete(' ').split(',')
  #
  # Check source of TCPServer.c at https://github.com/ruby/ruby/blob/trunk/ext/socket/tcpserver.c#L25-L31
  # * Internally, TCPServer.new calls getaddrinfo() function to obtain ip_addresses.
  # * If getaddrinfo() returns multiple ip_addresses,
  # * TCPServer.new TRIES to create a server socket for EACH address and RETURNS FIRST one that is SUCCESSFUL.
  #
  # So for that it was a small portion of luck which address had been used then.
  # We won't support that magic anymore. If wish to bind on all local ip_addresses
  # and interfaces, use new "*" wildcard, otherwise specify ip_addresses and / or hostnames
  #
  # raise exception when found empty or inner empty hosts specification like "" or "a.b.c.d,,e.f.g.h", guess miss-coding
  raise 'No hosts defined! Please use specific hostnames and / or ip_addresses or "*" for wildcard!' if @hosts.empty?
  raise 'Detected an empty identifier in given hosts! Please use specific hostnames and / or ip_addresses or "*" for wildcard!' if @hosts.include?('')

  # build array of addresses for ip_addresses and ports to use
  @addresses = []
  @hosts.each_with_index do |host, index|
    # resolv ip_addresses for host if not wildcard / all hosts
    # if host is "*" wildcard (all) interfaces are used
    # otherwise it will be bind to the found host ip_addresses
    if host == '*'
      ip_addresses_for_host = []
      Socket.ip_address_list.each do |a|
        # test for all local valid ipv4 and ipv6 ip_addresses
        # check question on stackoverflow for details
        # https://stackoverflow.com/questions/59770803/identify-all-relevant-ip-addresses-from-ruby-socket-ip-address-list
        ip_addresses_for_host << a.ip_address if \
          (a.ipv4? &&
            (a.ipv4_loopback? || a.ipv4_private? ||
             !(a.ipv4_loopback? || a.ipv4_private? || a.ipv4_multicast?)
            )
          ) ||
          (a.ipv6? &&
            (a.ipv6_loopback? || a.ipv6_unique_local? ||
             !(a.ipv6_loopback? || a.ipv6_unique_local? || a.ipv6_linklocal? || a.ipv6_multicast? || a.ipv6_sitelocal? ||
               a.ipv6_mc_global? || a.ipv6_mc_linklocal? || a.ipv6_mc_nodelocal? || a.ipv6_mc_orglocal? || a.ipv6_mc_sitelocal? ||
               a.ipv6_v4compat? || a.ipv6_v4mapped? || a.ipv6_unspecified?)
            )
          )
      end
    else
      ip_addresses_for_host = Resolv.new.getaddresses(host).uniq
    end
    # get ports for that host entry
    # if ports at index are not specified, use last item
    # of ports array. if multiple ports specified by
    # item like 2525:3535:4545, then all ports will be instantiated
    ports_for_host = (index < @ports.length ? @ports[index] : @ports.last).to_s.split(':')
    # append combination of ip_address and ports to the list of serving addresses
    ip_addresses_for_host.each do |ip_address|
      ports_for_host.each do |port|
        @addresses << "#{ip_address}:#{port}"
      end
    end
  end

  # read max_processings
  @max_processings = max_processings
  raise 'Number of simultaneous processings (max_processings) must be a positive integer!' unless @max_processings.is_a?(Integer) && @max_processings.positive?
  # check max_connections
  @max_connections = max_connections
  raise 'Number of concurrent connections is lower than number of simultaneous processings!' if @max_connections && @max_connections < @max_processings

  # check for crlf mode
  @crlf_mode = crlf_mode.nil? ? DEFAULT_CRLF_MODE : crlf_mode
  raise "Unknown CRLF mode #{@crlf_mode} was given!" unless CRLF_MODES.include?(@crlf_mode)

  # always prevent auto resolving hostnames to prevent a delay on socket connect
  BasicSocket.do_not_reverse_lookup = true
  # do reverse lookups manually if enabled by io.addr and io.peeraddr
  @do_dns_reverse_lookup = do_dns_reverse_lookup.nil? ? true : do_dns_reverse_lookup

  # io and buffer settings
  @io_cmd_timeout = io_cmd_timeout.nil? ? DEFAULT_IO_CMD_TIMEOUT : io_cmd_timeout
  @io_buffer_chunk_size = io_buffer_chunk_size.nil? ? DEFAULT_IO_BUFFER_CHUNK_SIZE : io_buffer_chunk_size
  @io_buffer_max_size = io_buffer_max_size.nil? ? DEFAULT_IO_BUFFER_MAX_SIZE : io_buffer_max_size

  # smtp extensions
  @pipelining_extension = pipelining_extension.nil? ? DEFAULT_PIPELINING_EXTENSION_ENABLED : pipelining_extension
  @internationalization_extensions = internationalization_extensions.nil? ? DEFAULT_INTERNATIONALIZATION_EXTENSIONS_ENABLED : internationalization_extensions

  # check for authentification
  @auth_mode = auth_mode.nil? ? DEFAULT_AUTH_MODE : auth_mode
  raise "Unknown authentification mode #{@auth_mode} was given!" unless AUTH_MODES.include?(@auth_mode)

  # check for encryption
  @encrypt_mode = tls_mode.nil? ? DEFAULT_ENCRYPT_MODE : tls_mode
  raise "Unknown encryption mode #{@encrypt_mode} was given!" unless ENCRYPT_MODES.include?(@encrypt_mode)
  # SSL transport layer for STARTTLS
  if @encrypt_mode == :TLS_FORBIDDEN
    @tls = nil
  else
    # try to load openssl gem now
    begin
      require 'openssl'
    rescue LoadError
    end
    # check for openssl gem when enabling  TLS / SSL
    raise 'The openssl library gem is not installed!' unless defined?(OpenSSL)
    # check for given CN and SAN
    if tls_cert_cn.nil?
      # build generic set of "valid" self signed certificate CN and SAN
      # using all given hosts and detected ip_addresses but not "*" wildcard
      tls_cert_san = ([] + @hosts + @addresses.map { |address| address.rpartition(':').first }).uniq
      tls_cert_san.delete('*')
      # build generic CN based on first SAN
      if tls_cert_san.first.match?(/^(127\.0?0?0\.0?0?0\.0?0?1|::1|localhost)$/i)
        # used generic localhost.local
        tls_cert_cn = 'localhost.local'
      else
        # use first element from detected hosts and ip_addresses
        # drop that element from SAN
        tls_cert_cn = tls_cert_san.first
        tls_cert_san.slice!(0)
      end
    else
      tls_cert_cn = tls_cert_cn.to_s.strip
      tls_cert_san = tls_cert_san.to_s.delete(' ').split(',')
    end
    # create ssl transport service
    @tls = TlsTransport.new(tls_cert_path, tls_key_path, tls_ciphers, tls_methods, tls_cert_cn, tls_cert_san, @logger)
  end
end

Public Instance Methods

addresses() click to toggle source

Array of ip_address:port which get bound and build up from given hosts and ports

# File lib/midi-smtp-server.rb, line 137
def addresses
  # prevent original array from being changed
  @addresses.dup
end
authenticated?(ctx) click to toggle source

check the status of authentication for a given context

# File lib/midi-smtp-server.rb, line 435
def authenticated?(ctx)
  ctx[:server][:authenticated] && !ctx[:server][:authenticated].to_s.empty?
end
connections() click to toggle source

Return the current number of connected clients

# File lib/midi-smtp-server.rb, line 84
def connections
  @connections.size
end
connections?() click to toggle source

Return if has active connected clients

# File lib/midi-smtp-server.rb, line 89
def connections?
  @connections.any?
end
encrypted?(ctx) click to toggle source

check the status of encryption for a given context

# File lib/midi-smtp-server.rb, line 440
def encrypted?(ctx)
  ctx[:server][:encrypted] && !ctx[:server][:encrypted].to_s.empty?
end
hosts() click to toggle source

Array of hosts / ip_addresses on which to bind, set as string seperated by commata like 'name.domain.com, 127.0.0.1, ::1'

# File lib/midi-smtp-server.rb, line 131
def hosts
  # prevent original array from being changed
  @hosts.dup
end
join(sleep_seconds_before_join: 1) click to toggle source

Join with the server thread(s) before joining the server threads, check and wait optionally a few seconds to let the service(s) come up

# File lib/midi-smtp-server.rb, line 106
def join(sleep_seconds_before_join: 1)
  # check already existing TCPServers
  return if @tcp_servers.empty?
  # wait some seconds before joininig the upcoming threads
  # and check that all TCPServers gots one thread
  while (@tcp_server_threads.length < @tcp_servers.length) && sleep_seconds_before_join.positive?
    sleep_seconds_before_join -= 1
    sleep 1
  end
  # try to join any thread
  begin
    @tcp_server_threads.each(&:join)

  # catch ctrl-c to stop service
  rescue Interrupt
  end
end
on_auth_event(ctx, authorization_id, authentication_id, authentication) click to toggle source

check the authentification on AUTH if any value returned, that will be used for ongoing processing otherwise the original value will be used for authorization_id

# File lib/midi-smtp-server.rb, line 426
def on_auth_event(ctx, authorization_id, authentication_id, authentication)
  # if authentification is used, override this event
  # and implement your own user management.
  # otherwise all authentifications are blocked per default
  on_logging_event(ctx, Logger::DEBUG, "Deny access from #{ctx[:server][:remote_ip]}:#{ctx[:server][:remote_port]} for #{authentication_id}" + (authorization_id == '' ? '' : "/#{authorization_id}") + " with #{authentication}")
  raise Smtpd535Exception
end
on_connect_event(ctx) click to toggle source

event on CONNECTION you may change the ctx[:local_response] and you may change the ctx[:helo_response] in here so that these will be used as local welcome and greeting strings the values are not allowed to return CR nor LF chars and will be stripped

# File lib/midi-smtp-server.rb, line 408
def on_connect_event(ctx)
  on_logging_event(ctx, Logger::DEBUG, "Client connect from #{ctx[:server][:remote_ip]}:#{ctx[:server][:remote_port]} to #{ctx[:server][:local_ip]}:#{ctx[:server][:local_port]}")
end
on_disconnect_event(ctx) click to toggle source

event before DISONNECT

# File lib/midi-smtp-server.rb, line 413
def on_disconnect_event(ctx)
  on_logging_event(ctx, Logger::DEBUG, "Client disconnect from #{ctx[:server][:remote_ip]}:#{ctx[:server][:remote_port]} on #{ctx[:server][:local_ip]}:#{ctx[:server][:local_port]}")
end
on_helo_event(ctx, helo_data) click to toggle source

event on HELO/EHLO you may change the ctx[:helo_response] in here so that this will be used as greeting string the value is not allowed to return CR nor LF chars and will be stripped

# File lib/midi-smtp-server.rb, line 421
def on_helo_event(ctx, helo_data) end
on_logging_event(_ctx, severity, msg, err: nil) click to toggle source

event on LOGGING the exposed logger property is from class MidiSmtpServer::ForwardingLogger and pushes any logging message to this on_logging_event. if logging occurs from inside session, the _ctx should be not nil if logging occurs from an error, the err object should be filled

# File lib/midi-smtp-server.rb, line 387
def on_logging_event(_ctx, severity, msg, err: nil)
  case severity
    when Logger::INFO
      @logger_protected.info(msg)
    when Logger::WARN
      @logger_protected.warn(msg)
    when Logger::ERROR
      @logger_protected.error(msg)
    when Logger::FATAL
      @logger_protected.fatal(msg)
      err.backtrace.each { |log| @logger_protected.fatal("#{log}") }
    else
      @logger_protected.debug(msg)
  end
end
on_mail_from_event(ctx, mail_from_data) click to toggle source

get address send in MAIL FROM if any value returned, that will be used for ongoing processing otherwise the original value will be used

# File lib/midi-smtp-server.rb, line 447
def on_mail_from_event(ctx, mail_from_data) end
on_message_data_event(ctx) click to toggle source

get each message after DATA <message>

# File lib/midi-smtp-server.rb, line 464
def on_message_data_event(ctx) end
on_message_data_headers_event(ctx) click to toggle source

event when headers are received while receiving message DATA

# File lib/midi-smtp-server.rb, line 461
def on_message_data_headers_event(ctx) end
on_message_data_receiving_event(ctx) click to toggle source

event while receiving message DATA

# File lib/midi-smtp-server.rb, line 458
def on_message_data_receiving_event(ctx) end
on_message_data_start_event(ctx) click to toggle source

event when beginning with message DATA

# File lib/midi-smtp-server.rb, line 455
def on_message_data_start_event(ctx) end
on_process_line_unknown_event(_ctx, _line) click to toggle source

event when process_line identifies an unknown command line allows to abort sessions for a series of unknown activities to prevent denial of service attacks etc.

# File lib/midi-smtp-server.rb, line 469
def on_process_line_unknown_event(_ctx, _line)
  # per default we encounter an error
  raise Smtpd500Exception
end
on_rcpt_to_event(ctx, rcpt_to_data) click to toggle source

get each address send in RCPT TO if any value returned, that will be used for ongoing processing otherwise the original value will be used

# File lib/midi-smtp-server.rb, line 452
def on_rcpt_to_event(ctx, rcpt_to_data) end
ports() click to toggle source

Array of ports on which to bind, set as string seperated by commata like '2525, 3535' or '2525:3535, 2525'

# File lib/midi-smtp-server.rb, line 125
def ports
  # prevent original array from being changed
  @ports.dup
end
processings() click to toggle source

Return the current number of processed clients

# File lib/midi-smtp-server.rb, line 94
def processings
  @processings.size
end
processings?() click to toggle source

Return if has active processed clients

# File lib/midi-smtp-server.rb, line 99
def processings?
  @processings.any?
end
shutdown() click to toggle source

Schedule a shutdown for the server

# File lib/midi-smtp-server.rb, line 74
def shutdown
  @shutdown = true
end
shutdown?() click to toggle source

test for shutdown state

# File lib/midi-smtp-server.rb, line 79
def shutdown?
  @shutdown
end
ssl_context() click to toggle source

Current TLS OpenSSL::SSL::SSLContext when initalized by :TLS_OPTIONAL, :TLS_REQUIRED

# File lib/midi-smtp-server.rb, line 143
def ssl_context
  @tls&.ssl_context
end
start() click to toggle source

Start the server

# File lib/midi-smtp-server.rb, line 47
def start
  serve_service
end
stop(wait_seconds_before_close: 2, gracefully: true) click to toggle source

Stop the server

# File lib/midi-smtp-server.rb, line 52
def stop(wait_seconds_before_close: 2, gracefully: true)
  # always signal shutdown
  shutdown if gracefully
  # wait if some connection(s) need(s) more time to handle shutdown
  sleep wait_seconds_before_close if connections?
  # drop tcp_servers while raising SmtpdStopServiceException
  @connections_mutex.synchronize do
    @tcp_server_threads.each do |tcp_server_thread|
      # use safe navigation (&.) to make sure that obj exists like ... if tcp_server_thread
      tcp_server_thread&.raise SmtpdStopServiceException
    end
  end
  # wait if some connection(s) still need(s) more time to come down
  sleep wait_seconds_before_close if connections? || !stopped?
end
stopped?() click to toggle source

Returns true if the server has stopped.

# File lib/midi-smtp-server.rb, line 69
def stopped?
  @tcp_server_threads.empty? && @tcp_servers.empty?
end

Protected Instance Methods

process_auth_login_pass(session, encoded_auth_response) click to toggle source
# File lib/midi-smtp-server.rb, line 1274
def process_auth_login_pass(session, encoded_auth_response)
  begin
    # extract auth id (and password)
    @auth_values = [
      session[:auth_challenge][:authorization_id],
      session[:auth_challenge][:authentication_id],
      Base64.decode64(encoded_auth_response)
    ]
    # check for valid credentials
    return_value = on_auth_event(session[:ctx], @auth_values[0], @auth_values[1], @auth_values[2])
    if return_value
      # overwrite data with returned value as authorization id
      @auth_values[0] = return_value
    end
    # save authentication information to ctx
    session[:ctx][:server][:authorization_id] = @auth_values[0].to_s.empty? ? @auth_values[1] : @auth_values[0]
    session[:ctx][:server][:authentication_id] = @auth_values[1]
    session[:ctx][:server][:authenticated] = Time.now.utc
    # response code
    return '235 OK'

  ensure
    # whatever happens in this check, reset next sequence
    session[:cmd_sequence] = :CMD_RSET
    # and reset auth_challenge
    session[:auth_challenge] = {}
  end
end
process_auth_login_user(session, encoded_auth_response) click to toggle source
# File lib/midi-smtp-server.rb, line 1264
def process_auth_login_user(session, encoded_auth_response)
  # save challenged auth_id
  session[:auth_challenge][:authorization_id] = ''
  session[:auth_challenge][:authentication_id] = Base64.decode64(encoded_auth_response)
  # set sequence for next command input
  session[:cmd_sequence] = :CMD_AUTH_LOGIN_PASS
  # response code with request for Password
  return +'' << '334 ' << Base64.strict_encode64('Password:')
end
process_auth_plain(session, encoded_auth_response) click to toggle source

handle plain authentification

# File lib/midi-smtp-server.rb, line 1239
def process_auth_plain(session, encoded_auth_response)
  begin
    # extract auth id (and password)
    @auth_values = Base64.decode64(encoded_auth_response).split("\x00")
    # check for valid credentials parameters
    raise Smtpd500Exception unless @auth_values.length == 3
    # call event function to test credentials
    return_value = on_auth_event(session[:ctx], @auth_values[0], @auth_values[1], @auth_values[2])
    if return_value
      # overwrite data with returned value as authorization id
      @auth_values[0] = return_value
    end
    # save authentication information to ctx
    session[:ctx][:server][:authorization_id] = @auth_values[0].to_s.empty? ? @auth_values[1] : @auth_values[0]
    session[:ctx][:server][:authentication_id] = @auth_values[1]
    session[:ctx][:server][:authenticated] = Time.now.utc
    # response code
    return '235 OK'

  ensure
    # whatever happens in this check, reset next sequence
    session[:cmd_sequence] = :CMD_RSET
  end
end
process_line(session, line, line_break) click to toggle source
# File lib/midi-smtp-server.rb, line 798
def process_line(session, line, line_break)
  # check whether in auth challenge modes
  if session[:cmd_sequence] == :CMD_AUTH_PLAIN_VALUES
    # handle authentication
    process_auth_plain(session, line)

  # check whether in auth challenge modes
  elsif session[:cmd_sequence] == :CMD_AUTH_LOGIN_USER
    # handle authentication
    process_auth_login_user(session, line)

  # check whether in auth challenge modes
  elsif session[:cmd_sequence] == :CMD_AUTH_LOGIN_PASS
    # handle authentication
    process_auth_login_pass(session, line)

  # check whether in data or command mode
  elsif session[:cmd_sequence] != :CMD_DATA

    # Handle specific messages from the client
    case line

      when (/^(HELO|EHLO)(\s+.*)?$/i)
        # HELO/EHLO
        # 250 Requested mail action okay, completed
        # 421 <domain> Service not available, closing transmission channel
        # 500 Syntax error, command unrecognised
        # 501 Syntax error in parameters or arguments
        # 504 Command parameter not implemented
        # 521 <domain> does not accept mail [rfc1846]
        # ---------
        # check valid command sequence
        raise Smtpd503Exception if session[:cmd_sequence] != :CMD_HELO
        # handle command
        @cmd_data = line.gsub(/^(HELO|EHLO)\ /i, '').strip
        # call event to handle data
        on_helo_event(session[:ctx], @cmd_data)
        # if no error raised, append to message hash
        session[:ctx][:server][:helo] = @cmd_data
        # set sequence state as RSET
        session[:cmd_sequence] = :CMD_RSET
        # check whether to answer as HELO or EHLO
        case line
          when (/^EHLO/i)
            # rubocop:disable Style/StringConcatenation
            # reply supported extensions
            return "250-#{session[:ctx][:server][:helo_response].to_s.strip}\r\n" +
                   # respond with 8BITMIME extension
                   (@internationalization_extensions ? "250-8BITMIME\r\n" : '') +
                   # respond with SMTPUTF8 extension
                   (@internationalization_extensions ? "250-SMTPUTF8\r\n" : '') +
                   # respond with PIPELINING if enabled
                   (@pipelining_extension ? "250-PIPELINING\r\n" : '') +
                   # respond with AUTH extensions if enabled
                   (@auth_mode == :AUTH_FORBIDDEN ? '' : "250-AUTH LOGIN PLAIN\r\n") +
                   # respond with STARTTLS if available and not already enabled
                   (@encrypt_mode == :TLS_FORBIDDEN || encrypted?(session[:ctx]) ? '' : "250-STARTTLS\r\n") +
                   '250 OK'
            # rubocop:enable all
          else
            # reply ok only
            return "250 OK #{session[:ctx][:server][:helo_response].to_s.strip}".strip
        end

      when /^STARTTLS\s*$/i
        # STARTTLS
        # 220 Ready to start TLS
        # 454 TLS not available
        # 501 Syntax error (no parameters allowed)
        # ---------
        # check that encryption is allowed
        raise Smtpd500Exception if @encrypt_mode == :TLS_FORBIDDEN
        # check valid command sequence
        raise Smtpd503Exception if session[:cmd_sequence] == :CMD_HELO
        # check initialized TlsTransport object
        raise Tls454Exception unless @tls
        # check valid command sequence
        raise Smtpd503Exception if encrypted?(session[:ctx])
        # set sequence for next command input
        session[:cmd_sequence] = :CMD_STARTTLS
        # return with new service ready message
        return '220 Ready to start TLS'

      when (/^AUTH(\s+)((LOGIN|PLAIN)(\s+[A-Z0-9=]+)?|CRAM-MD5)\s*$/i)
        # AUTH
        # 235 Authentication Succeeded
        # 432 A password transition is needed
        # 454 Temporary authentication failure
        # 500 Authentication Exchange line is too long
        # 530 Authentication required
        # 534 Authentication mechanism is too weak
        # 535 Authentication credentials invalid
        # 538 Encryption required for requested authentication mechanism
        # ---------
        # check that authentication is allowed
        raise Smtpd500Exception if @auth_mode == :AUTH_FORBIDDEN
        # check valid command sequence
        raise Smtpd503Exception if session[:cmd_sequence] != :CMD_RSET
        # check that encryption is enabled if necessary
        raise Tls530Exception if @encrypt_mode == :TLS_REQUIRED && !encrypted?(session[:ctx])
        # check that not already authenticated
        raise Smtpd503Exception if authenticated?(session[:ctx])
        # handle command line
        @auth_data = line.gsub(/^AUTH\ /i, '').strip.gsub(/\s+/, ' ').split
        # handle auth command
        case @auth_data[0]

          when (/PLAIN/i)
            # check if only command was given
            if @auth_data.length == 1
              # set sequence for next command input
              session[:cmd_sequence] = :CMD_AUTH_PLAIN_VALUES
              # response code include post ending with a space
              return '334 '
            else
              # handle authentication with given auth_id and password
              process_auth_plain(session, @auth_data.length == 2 ? @auth_data[1] : [])
            end

          when (/LOGIN/i)
            # check if auth_id was sent too
            case @auth_data.length

              when 1
                # reset auth_challenge
                session[:auth_challenge] = {}
                # set sequence for next command input
                session[:cmd_sequence] = :CMD_AUTH_LOGIN_USER
                # response code with request for Username
                return +'' << '334 ' << Base64.strict_encode64('Username:')

              when 2
                # handle next sequence
                process_auth_login_user(session, @auth_data[1])

              else
                raise Smtpd500Exception
            end

          # not supported in case of also unencrypted data delivery
          # instead of supporting password encryption only, we will
          # provide optional SMTPS service instead
          # read discussion on https://github.com/4commerce-technologies-AG/midi-smtp-server/issues/3#issuecomment-126898711
          #
          # when (/CRAM-MD5/i)
          #   raise Smtpd500Exception

          else
            # unknown auth method
            raise Smtpd500Exception

        end

      when (/^NOOP\s*$/i)
        # NOOP
        # 250 Requested mail action okay, completed
        # 421 <domain> Service not available, closing transmission channel
        # 500 Syntax error, command unrecognised
        return '250 OK'

      when (/^RSET\s*$/i)
        # RSET
        # 250 Requested mail action okay, completed
        # 421 <domain> Service not available, closing transmission channel
        # 500 Syntax error, command unrecognised
        # 501 Syntax error in parameters or arguments
        # ---------
        # check valid command sequence
        raise Smtpd503Exception if session[:cmd_sequence] == :CMD_HELO
        # check that encryption is enabled if necessary
        raise Tls530Exception if @encrypt_mode == :TLS_REQUIRED && !encrypted?(session[:ctx])
        # handle command
        process_reset_session(session)
        return '250 OK'

      when (/^QUIT\s*$/i)
        # QUIT
        # 221 <domain> Service closing transmission channel
        # 500 Syntax error, command unrecognised
        session[:cmd_sequence] = :CMD_QUIT
        return ''

      when (/^MAIL FROM:/i)
        # MAIL
        # 250 Requested mail action okay, completed
        # 421 <domain> Service not available, closing transmission channel
        # 451 Requested action aborted: local error in processing
        # 452 Requested action not taken: insufficient system storage
        # 500 Syntax error, command unrecognised
        # 501 Syntax error in parameters or arguments
        # 552 Requested mail action aborted: exceeded storage allocation
        # ---------
        # check valid command sequence
        raise Smtpd503Exception if session[:cmd_sequence] != :CMD_RSET
        # check that encryption is enabled if necessary
        raise Tls530Exception if @encrypt_mode == :TLS_REQUIRED && !encrypted?(session[:ctx])
        # check that authentication is enabled if necessary
        raise Smtpd530Exception if @auth_mode == :AUTH_REQUIRED && !authenticated?(session[:ctx])
        # handle command
        @cmd_data = line.gsub(/^MAIL FROM:/i, '').strip
        # check for BODY= parameter
        case @cmd_data
          # test for explicit 7bit
          when (/\sBODY=7BIT(\s|$)/i)
            # raise exception if not supported
            raise Smtpd501Exception unless @internationalization_extensions
            # save info about encoding
            session[:ctx][:envelope][:encoding_body] = '7bit'
          # test for 8bit
          when (/\sBODY=8BITMIME(\s|$)/i)
            # raise exception if not supported
            raise Smtpd501Exception unless @internationalization_extensions
            # save info about encoding
            session[:ctx][:envelope][:encoding_body] = '8bitmime'
          # test for unknown encoding
          when (/\sBODY=.*$/i)
            # unknown BODY encoding
            raise Smtpd501Exception
        end
        # check for SMTPUTF8 parameter
        case @cmd_data
          # test for explicit 7bit
          when (/\sSMTPUTF8(\s|$)/i)
            # raise exception if not supported
            raise Smtpd501Exception unless @internationalization_extensions
            # save info about encoding
            session[:ctx][:envelope][:encoding_utf8] = 'utf8'
        end
        # drop any BODY= and SMTPUTF8 content
        @cmd_data = @cmd_data.gsub(/\sBODY=(7BIT|8BITMIME)/i, '').gsub(/\sSMTPUTF8/i, '').strip if @internationalization_extensions
        # call event to handle data
        return_value = on_mail_from_event(session[:ctx], @cmd_data)
        if return_value
          # overwrite data with returned value
          @cmd_data = return_value
        end
        # if no error raised, append to message hash
        session[:ctx][:envelope][:from] = @cmd_data
        # set sequence state
        session[:cmd_sequence] = :CMD_MAIL
        # reply ok
        return '250 OK'

      when (/^RCPT TO:/i)
        # RCPT
        # 250 Requested mail action okay, completed
        # 251 User not local; will forward to <forward-path>
        # 421 <domain> Service not available, closing transmission channel
        # 450 Requested mail action not taken: mailbox unavailable
        # 451 Requested action aborted: local error in processing
        # 452 Requested action not taken: insufficient system storage
        # 500 Syntax error, command unrecognised
        # 501 Syntax error in parameters or arguments
        # 503 Bad sequence of commands
        # 521 <domain> does not accept mail [rfc1846]
        # 550 Requested action not taken: mailbox unavailable
        # 551 User not local; please try <forward-path>
        # 552 Requested mail action aborted: exceeded storage allocation
        # 553 Requested action not taken: mailbox name not allowed
        # ---------
        # check valid command sequence
        raise Smtpd503Exception unless [:CMD_MAIL, :CMD_RCPT].include?(session[:cmd_sequence])
        # check that encryption is enabled if necessary
        raise Tls530Exception if @encrypt_mode == :TLS_REQUIRED && !encrypted?(session[:ctx])
        # check that authentication is enabled if necessary
        raise Smtpd530Exception if @auth_mode == :AUTH_REQUIRED && !authenticated?(session[:ctx])
        # handle command
        @cmd_data = line.gsub(/^RCPT TO:/i, '').strip
        # call event to handle data
        return_value = on_rcpt_to_event(session[:ctx], @cmd_data)
        if return_value
          # overwrite data with returned value
          @cmd_data = return_value
        end
        # if no error raised, append to message hash
        session[:ctx][:envelope][:to] << @cmd_data
        # set sequence state
        session[:cmd_sequence] = :CMD_RCPT
        # reply ok
        return '250 OK'

      when (/^DATA\s*$/i)
        # DATA
        # 354 Start mail input; end with <CRLF>.<CRLF>
        # 250 Requested mail action okay, completed
        # 421 <domain> Service not available, closing transmission channel received data
        # 451 Requested action aborted: local error in processing
        # 452 Requested action not taken: insufficient system storage
        # 500 Syntax error, command unrecognised
        # 501 Syntax error in parameters or arguments
        # 503 Bad sequence of commands
        # 552 Requested mail action aborted: exceeded storage allocation
        # 554 Transaction failed
        # ---------
        # check valid command sequence
        raise Smtpd503Exception if session[:cmd_sequence] != :CMD_RCPT
        # check that encryption is enabled if necessary
        raise Tls530Exception if @encrypt_mode == :TLS_REQUIRED && !encrypted?(session[:ctx])
        # check that authentication is enabled if necessary
        raise Smtpd530Exception if @auth_mode == :AUTH_REQUIRED && !authenticated?(session[:ctx])
        # handle command
        # set sequence state
        session[:cmd_sequence] = :CMD_DATA
        # save incoming UTC time
        session[:ctx][:message][:received] = Time.now.utc
        # reply ok / proceed with message data
        return '354 Enter message, ending with "." on a line by itself'

      else
        # If we somehow get to this point then
        # allow handling of unknown command line
        on_process_line_unknown_event(session[:ctx], line)
    end

  else
    # If we are in date mode then ...

    # call event to signal beginning of message data transfer
    on_message_data_start_event(session[:ctx]) unless session[:ctx][:message][:data][0]

    # ... and the entire new message data (line) does NOT consists
    # solely of a period (.) on a line by itself then we are being
    # told to continue data mode and the command sequence state
    # will stay on :CMD_DATA
    unless line == '.'
      # remove a preceding first dot as defined by RFC 5321 (section-4.5.2)
      line.slice!(0) if line[0] == '.'

      # if received an empty line the first time, that identifies
      # end of headers.
      unless session[:ctx][:message][:headers][0] || line[0]
        # change flag to do not signal this again for the
        # active message data transmission
        session[:ctx][:message][:headers] = true.to_s
        # call event to process received headers
        on_message_data_headers_event(session[:ctx])
      end

      # we need to add the new message data (line) to the message
      # and make sure to add CR LF as defined by RFC
      session[:ctx][:message][:data] << line << line_break

      # call event to inspect message data while recording line by line
      # e.g. abort while receiving too big incoming mail or
      # create a teergrube for spammers etc.
      on_message_data_receiving_event(session[:ctx])

      # just return and stay on :CMD_DATA
      return ''
    end

    # otherwise the entire new message data (line) consists
    # solely of a period on a line by itself then we are being
    # told to finish data mode

    # remove last CR LF pair or single LF in buffer
    session[:ctx][:message][:data].chomp!
    # save delivered UTC time
    session[:ctx][:message][:delivered] = Time.now.utc
    # save bytesize of message data
    session[:ctx][:message][:bytesize] = session[:ctx][:message][:data].bytesize
    # call event to process message
    begin
      on_message_data_event(session[:ctx])
      return '250 Requested mail action okay, completed'

    # test for SmtpdException
    rescue SmtpdException
      # just re-raise exception set by app
      raise

    # test all other Exceptions
    rescue StandardError => e
      # send correct aborted message to smtp dialog
      raise Smtpd451Exception, e

    ensure
      # always start with empty values after finishing incoming message
      # and rset command sequence
      process_reset_session(session)
    end

  end
end
process_reset_session(session, connection_initialize: false) click to toggle source

reset the context of current smtpd dialog

# File lib/midi-smtp-server.rb, line 1184
def process_reset_session(session, connection_initialize: false)
  # set active command sequence info
  session[:cmd_sequence] = connection_initialize ? :CMD_HELO : :CMD_RSET
  # drop any auth challenge
  session[:auth_challenge] = {}
  # test existing of :ctx hash
  session[:ctx] || session[:ctx] = {}
  # reset server values (only on connection start)
  if connection_initialize
    # create or rebuild :ctx hash
    # and mark strings as mutable
    session[:ctx].merge!(
      server: {
        local_host: +'',
        local_ip: +'',
        local_port: +'',
        local_response: +'',
        remote_host: +'',
        remote_ip: +'',
        remote_port: +'',
        helo: +'',
        helo_response: +'',
        connected: +'',
        exceptions: 0,
        errors: [],
        authorization_id: +'',
        authentication_id: +'',
        authenticated: +'',
        encrypted: +''
      }
    )
  end
  # reset envelope values
  session[:ctx].merge!(
    envelope: {
      from: +'',
      to: [],
      encoding_body: +'',
      encoding_utf8: +''
    }
  )
  # reset message data
  session[:ctx].merge!(
    message: {
      received: -1,
      delivered: -1,
      bytesize: -1,
      headers: +'',
      crlf: +"\r\n",
      data: +''
    }
  )
end
serve_client(session, io) click to toggle source

handle connection

# File lib/midi-smtp-server.rb, line 581
def serve_client(session, io)
  # handle connection
  begin
    begin
      # ON CONNECTION
      # 220 <domain> Service ready
      # 421 <domain> Service not available, closing transmission channel
      # Reset and initialize message
      process_reset_session(session, connection_initialize: true)

      # get local address info
      _, session[:ctx][:server][:local_port], session[:ctx][:server][:local_host], session[:ctx][:server][:local_ip] = @do_dns_reverse_lookup ? io.addr(:hostname) : io.addr(:numeric)
      # get remote partner hostname and address
      _, session[:ctx][:server][:remote_port], session[:ctx][:server][:remote_host], session[:ctx][:server][:remote_ip] = @do_dns_reverse_lookup ? io.peeraddr(:hostname) : io.peeraddr(:numeric)

      # save connection date/time
      session[:ctx][:server][:connected] = Time.now.utc

      # build and save the local welcome and greeting response strings
      session[:ctx][:server][:local_response] = "#{session[:ctx][:server][:local_host]} says welcome!"
      session[:ctx][:server][:helo_response] = "#{session[:ctx][:server][:local_host]} at your service!"

      # check if we want to let this remote station connect us
      on_connect_event(session[:ctx])

      # drop connection (respond 421) if too busy
      raise 'Abort connection while too busy, exceeding max_connections!' if max_connections && connections > max_connections

      # check active processings for new client
      @connections_mutex.synchronize do
        # when processings exceed maximum number of simultaneous allowed processings, then wait for next free slot
        @connections_cv.wait(@connections_mutex) until processings < max_processings
      end

      # append this to list of processings
      @processings << Thread.current

      # reply local welcome message
      output = +"220 #{session[:ctx][:server][:local_response].to_s.strip}\r\n"

      # log and show to client
      on_logging_event(session[:ctx], Logger::DEBUG, +'>>> ' << output)
      io.print output unless io.closed?

      # initialize \r\n for line_break, this is used for CRLF_ENSURE and CRLF_STRICT and mark as mutable
      line_break = +"\r\n"

      # initialize io_buffer for input data and mark as mutable
      io_buffer = +''

      # initialize io_buffer_line_lf index
      io_buffer_line_lf = nil

      # initialize timeout timestamp
      timestamp_timeout = Time.now.to_i

      # while input data handle communication
      loop do
        # test if STARTTLS sequence
        if session[:cmd_sequence] == :CMD_STARTTLS
          # start ssl tunnel
          io = @tls.start(io)
          # save enabled tls
          session[:ctx][:server][:encrypted] = Time.now.utc
          # set sequence back to HELO/EHLO
          session[:cmd_sequence] = :CMD_HELO
          # reset timeout timestamp
          timestamp_timeout = Time.now.to_i
        end

        # read input data from Socket / SSLSocket into io_buffer
        # by non-blocking action until \n is found
        begin
          unless io_buffer_line_lf
            # check for timeout on IO
            raise SmtpdIOTimeoutException if @io_cmd_timeout && Time.now.to_i - timestamp_timeout > @io_cmd_timeout
            # read chunks of input data until line-feed
            io_buffer << io.read_nonblock(@io_buffer_chunk_size)
            # check for buffersize
            raise SmtpdIOBufferOverrunException if @io_buffer_max_size && io_buffer.length > @io_buffer_max_size
            # check for lf in current io_buffer
            io_buffer_line_lf = io_buffer.index("\n")
          end

        # ignore exception when no input data is available yet
        rescue IO::WaitReadable
          # but wait a few moment to slow down system utilization
          sleep 0.1
        end

        # check if io_buffer is filled and contains already a line-feed
        while io_buffer_line_lf
          # extract line (containing \n) from io_buffer and slice io_buffer
          line = io_buffer.slice!(0, io_buffer_line_lf + 1)

          # check for next line-feed already in io_buffer
          io_buffer_line_lf = io_buffer.index("\n")

          # process commands and handle special SmtpdExceptions
          begin
            # check for pipelining extension or violation
            raise Smtpd500PipeliningException unless @pipelining_extension || !io_buffer_line_lf || (session[:cmd_sequence] == :CMD_DATA)

            # handle input line based on @crlf_mode
            case crlf_mode
              when :CRLF_ENSURE
                # remove any \r or \n occurence from line
                line.delete!("\r\n")
                # log line, verbosity based on log severity and command sequence
                on_logging_event(session[:ctx], Logger::DEBUG, +'<<< ' << line << "\n") if session[:cmd_sequence] != :CMD_DATA

              when :CRLF_LEAVE
                # use input line_break for line_break
                line_break = line[-2..] == "\r\n" ? "\r\n" : "\n"
                # check to override session crlf info, only when CRLF_LEAVE is used and in DATA mode
                session[:ctx][:message][:crlf] = line_break if session[:cmd_sequence] == :CMD_DATA
                # remove any line_break from line
                line.chomp!
                # log line, verbosity based on log severity and command sequence
                on_logging_event(session[:ctx], Logger::DEBUG, +'<<< ' << line.gsub("\r", '[\r]') << "\n") if session[:cmd_sequence] != :CMD_DATA

              when :CRLF_STRICT
                # check line ends up by \r\n
                raise Smtpd500CrLfSequenceException unless line[-2..] == "\r\n"
                # remove any line_break from line
                line.chomp!
                # check line for additional \r
                raise Smtpd500Exception, 'Line contains additional CR chars!' if line.index("\r")
                # log line, verbosity based on log severity and command sequence
                on_logging_event(session[:ctx], Logger::DEBUG, +'<<< ' << line << "\n") if session[:cmd_sequence] != :CMD_DATA
            end

            # process line and mark output as mutable
            output = +process_line(session, line, line_break)

          # defined abort channel exception
          rescue Smtpd421Exception => e
            # just re-raise this exception and exit loop and communication
            raise

          # defined SmtpdException
          rescue SmtpdException => e
            # inc number of detected exceptions during this session
            session[:ctx][:server][:exceptions] += 1
            # save clone of error object to context
            session[:ctx][:server][:errors].push(e.clone)
            # log error info if logging
            on_logging_event(session[:ctx], Logger::ERROR, "#{e} (#{e.class})".strip, err: e)
            # get the given smtp dialog result
            output = +"#{e.smtpd_result}"

          # Unknown general Exception during processing
          rescue StandardError => e
            # inc number of detected exceptions during this session
            session[:ctx][:server][:exceptions] += 1
            # save clone of error object to context
            session[:ctx][:server][:errors].push(e.clone)
            # log error info if logging
            on_logging_event(session[:ctx], Logger::ERROR, "#{e} (#{e.class})".strip, err: e)
            # set default smtp server dialog error
            output = +"#{Smtpd500Exception.new.smtpd_result}"
          end

          # check result
          unless output.empty?
            # log smtp dialog // message data is stored separate
            on_logging_event(session[:ctx], Logger::DEBUG, +'>>> ' << output)
            # append line feed
            output << "\r\n"
            # smtp dialog response
            io.print(output) unless io.closed? || shutdown?
          end

          # reset timeout timestamp
          timestamp_timeout = Time.now.to_i
        end

        # check for valid quit or broken communication
        break if (session[:cmd_sequence] == :CMD_QUIT) || io.closed? || shutdown?
      end
      # graceful end of connection
      output = +"221 Service closing transmission channel\r\n"
      # smtp dialog response
      io.print(output) unless io.closed?

    # connection was simply closed / aborted by remote closing socket
    rescue EOFError => e
      # log info but only while debugging otherwise ignore message
      on_logging_event(session[:ctx], Logger::DEBUG, 'Connection lost due abort by client! (EOFError)', err: e)

    rescue StandardError => e
      # inc number of detected exceptions during this session
      session[:ctx][:server][:exceptions] += 1
      # save clone of error object to context
      session[:ctx][:server][:errors].push(e.clone)
      # log error info if logging
      on_logging_event(session[:ctx], Logger::ERROR, "#{e} (#{e.class})".strip, err: e)
      # power down connection
      # ignore IOErrors when sending final smtp abort return code 421
      begin
        output = +"#{Smtpd421Exception.new.smtpd_result}\r\n"
        # smtp dialog response
        io.print(output) unless io.closed?
      rescue StandardError => e
        on_logging_event(session[:ctx], Logger::DEBUG, 'Can\'t send 421 abort code! (IOError)', err: e)
      end
    end

  ensure
    # event for cleanup at end of communication
    on_disconnect_event(session[:ctx])
  end

  # return socket handler, maybe replaced with ssl
  return io
end
serve_service() click to toggle source

Start the listeners for all hosts

# File lib/midi-smtp-server.rb, line 477
def serve_service
  raise 'Service was already started' unless stopped?

  # set flag to signal shutdown by stop / shutdown command
  @shutdown = false

  # instantiate the service for all @addresses (ip_address:port)
  @addresses.each do |address|
    # break address into ip_address and port and serve service
    ip_address = address.rpartition(':').first
    port = address.rpartition(':').last
    serve_service_on_ip_address_and_port(ip_address, port)
  end
end
serve_service_on_ip_address_and_port(ip_address, port) click to toggle source

Start the listener thread on single ip_address and port

# File lib/midi-smtp-server.rb, line 493
def serve_service_on_ip_address_and_port(ip_address, port)
  # log information
  logger.info("Starting service on #{ip_address}:#{port}")
  # check that there is a specific ip_address defined
  raise 'Unable to start TCP listener on missing or empty ip_address!' if ip_address.empty?
  # instantiate the service for ip_address and port
  tcp_server = TCPServer.new(ip_address, port)
  # append this server to the list of TCPServers
  @tcp_servers << tcp_server

  # run thread until shutdown
  @tcp_server_threads << Thread.new do
    begin
      # always check for shutdown request
      until shutdown?
        # get new client and start additional thread
        # to handle client process
        client = tcp_server.accept
        Thread.new(client) do |io|
          # add to list of connections
          @connections << Thread.current
          # handle connection
          begin
            # initialize a session storage hash
            Thread.current[:session] = {}
            # process smtp service on io socket
            io = serve_client(Thread.current[:session], io)
            # save returned io value due to maybe
            # established ssl io socket
          rescue SmtpdStopConnectionException
            # ignore this exception due to service shutdown
          rescue StandardError => e
            # log fatal error while handling connection
            on_logging_event(nil, Logger::FATAL, "#{e} (#{e.class})".strip, err: e.clone)
          ensure
            begin
              # always gracefully shutdown connection.
              # if the io object was overriden by the
              # result from serve_client() due to ssl
              # io, the ssl + io socket will be closed
              io.close
            rescue StandardError
              # ignore any exception from here
            end
            # remove closed session from connections
            @connections_mutex.synchronize do
              # drop this thread from connections
              @connections.delete(Thread.current)
              # drop this thread from processings
              @processings.delete(Thread.current)
              # signal mutex for next waiting thread
              @connections_cv.signal
            end
          end
        end
      end
    rescue SmtpdStopServiceException
      # ignore this exception due to service shutdown
    rescue StandardError => e
      # log fatal error while starting new thread
      on_logging_event(nil, Logger::FATAL, "#{e} (#{e.class})".strip, err: e.clone)
    ensure
      begin
        # drop the service
        tcp_server.close
        # remove from list
        @tcp_servers.delete(tcp_server)
        # reset local var
        tcp_server = nil
      rescue StandardError
        # ignore any error from here
      end
      if shutdown?
        # wait for finishing opened connections
        @connections_mutex.synchronize do
          @connections_cv.wait(@connections_mutex) until @connections.empty?
        end
      else
        # drop any open session immediately
        @connections.each { |c| c.raise SmtpdStopConnectionException }
      end
      # remove this thread from list
      @tcp_server_threads.delete(Thread.current)
    end
  end
end