Using events

MidiSmtpServer can be easily customized via subclassing. Simply subclass the MidiSmtpServer class as given in the examples and re-define some of the event handlers.


Server welcome response

While connecting from a client, the server will show up with a first local welcome message. The response is build and stored in ctx values. You may change the content on overriding the on_connect_event.

  # update local welcome and helo response
  def on_connect_event(ctx)
    ctx[:server][:local_response] = 'My welcome message!'
    ctx[:server][:helo_response] = 'My simple greeting message!'
  end


HELO/EHLO event response

After HELO or EHLO the server will show a greeting message as well as the capabilities (EHLO). This response is also build and stored in ctx values. You may change the content during on_connect_event or with extended check in the on_helo_event.

If you want to show your local_ip or hostname etc. you may also include the context vars for that. Be aware to expose only necessary internal information and addresses etc.

  def on_connect_event(ctx)
    ctx[:server][:local_response] = "#{ctx[:server][:local_host]} [#{ctx[:server][:local_ip]}] says welcome!"
  end

  # update helo response
  def on_helo_event(ctx, helo_data)
    ctx[:server][:helo_response] = "#{ctx[:server][:local_host]} [#{ctx[:server][:local_ip]}] is serving you!"
  end


Modify MAIL FROM, RCPT TO

Since release 1.1.4 the on_mail_from_event and on_rcpt_to_event allows to return values that should be added to the lists. This is useful if you want to e.g. normalize all incoming addresses. Format defined by RFC for <path> as a MAIL FROM or RCPT TO addresses is:

  "<" | <path> | ">"

Most mail servers allows also <path> only given addresses without leading and ending < >.

To make it easier for processing addresses, you are able to normalize them like:

  # simple rewrite and return value
  def on_mail_from_event(ctx, mail_from_data)
    # strip and normalize addresses like: <path> to path
    mail_from_data.gsub!(/^\s*<\s*(.*)\s*>\s*$/, '\1')
    # we believe in downcase addresses
    mail_from_data.downcase!
    # return address
    mail_from_data
  end

  # rewrite, process more checks and return value
  def on_rcpt_to_event(ctx, rcpt_to_data)
    # strip and normalize addresses like: <path> to path
    rcpt_to_data.gsub!(/^\s*<\s*(.*)\s*>\s*$/, '\1')
    # we believe in downcase addresses
    rcpt_to_data.downcase!
    # Output for debug
    logger.debug("Normalized to: [#{rcpt_to_data}]...")
    # return address
    rcpt_to_data
  end


Adding and testing headers

Since release 2.3.1 the on_message_data_start_event and on_message_data_headers_event enable the injection of additional headers like Received on DATA streaming. To add a Received header before any incoming header, use:

  # event when beginning with message DATA
  def on_message_data_start_event(ctx)
    ctx[:message][:data] <<
      "Received: " <<
      "from #{ctx[:server][:remote_host]} (#{ctx[:server][:remote_ip]}) " <<
      "by #{ctx[:server][:local_host]} (#{ctx[:server][:local_ip]}) " <<
      "with MySmtpd Server; " <<
      Time.now.strftime("%a, %d %b %Y %H:%M:%S %z") <<
      ctx[:message][:crlf]
  end

The Received header may be given with more or less additional information like encryption, recipient, sender etc. This should be done while being aware of system safety. Don't reveal too much internal information and choose wisely the published attributes.

Samples for Received headers are:

Received: from localhost ([127.0.0.1])
  by mail.domain.test with esmtp (Exim 4.86)
  (envelope-from <user@sample.com>)
  id 3gIFk7-0006RC-FG
  for my.user@mydomain.net; Thu, 01 Nov 2018 12:00:00 +0000
Received: from localhost ([127.0.0.1:10025])
  by mail.domain.test with ESMTPSA id 3gIFk7-0006RC-FG
  for <my.user@mydomain.net>
  (version=TLS1_2 cipher=ECDHE-RSA-AES128-GCM-SHA256 bits=128/128);
  Thu, 01 Nov 2018 12:00:00 +0000

To append special headers or do some checks on transmitted headers, the on_message_data_headers_event is called when end of header transmission was automatically discovered.

  # event when headers are received while receiving message DATA
  def on_message_data_headers_event(ctx)
    ctx[:message][:data] << 'X-MyHeader: 1.0' << ctx[:message][:crlf]
  end


Responding errors

If you return from event class without an exception, the server will respond to client with the appropriate success code, otherwise the client will be noticed about an error.

So you can build SPAM protection, when raising exception while getting RCPT TO events.

  # get each address send in RCPT TO:
  def on_rcpt_to_event(ctx, rcpt_to_data)
    raise MidiSmtpServer::Smtpd550Exception if rcpt_to_data == "not.name@domain.con"
  end

You are able to use exceptions on any level of events, so for an example you could raise an exception on on_message_data_event if you checked attachments for a pdf-document and fail or so on. If you use the defined MidiSmtpServer::Smtpd???Exception classes the remote client gets correct SMTP Server results. For logging purpose the Exception.message is written to log.

When using MidiSmtpServer::Smtpd421Exception you are able to abort the active connection to the client by replying 421 Service not available, closing transmission channel. Be aware, that this Exception will actively close the current connection to the client.

Attention: For logging purposes you may set a message to log for yourself - but - this message will not be transmitted to the client in order not to leak too much (internal) information outside. SMTP Server replies to clients only standardized ones.

  # drop connection immediately on SPAM
  def on_rcpt_to_event(ctx, rcpt_to_data)
    raise MidiSmtpServer::Smtpd421Exception, '421 Abort: Identified spammer!' if rcpt_to_data == "not.name@domain.con"
  end

In the above example, the message `421 Abort: Identified spammer! is written to log - and - the client receives the standardized message for code 421.

Please check RFC821 and additional for correct response dialog sequences:

COMMAND-REPLY SEQUENCES

   Each command is listed with its possible replies.  The prefixes
   used before the possible replies are "P" for preliminary (not
   used in SMTP), "I" for intermediate, "S" for success, "F" for
   failure, and "E" for error.  The 421 reply (service not
   available, closing transmission channel) may be given to any
   command if the SMTP-receiver knows it must shut down.  This
   listing forms the basis for the State Diagrams in Section 4.4.

CONNECTION ESTABLISHMENT
   S: 220
   F: 421
HELO
   S: 250
   E: 500, 501, 504, 421
MAIL
   S: 250
   F: 552, 451, 452
   E: 500, 501, 421
RCPT
   S: 250, 251
   F: 550, 551, 552, 553, 450, 451, 452
   E: 500, 501, 503, 421
DATA
   I: 354 -> data -> S: 250
                     F: 552, 554, 451, 452
   F: 451, 554
   E: 500, 501, 503, 421
RSET
   S: 250
   E: 500, 501, 504, 421
NOOP
   S: 250
   E: 500, 421
QUIT
   S: 221
   E: 500
AUTH
   S: 235
   F: 530, 534, 535, 454
   E: 500, 421


Incoming data validation

With release 2.2.3 there is an extended control about incoming data before processing. New options allow to set a timeout and maximum size of io_buffer for receiving client data up to a complete data line.

# timeout in seconds before a data line has to be completely sent by client or connection abort
io_cmd_timeout: DEFAULT_IO_CMD_TIMEOUT

# maximum size in bytes to read in buffer for a complete data line from client or connection abort
io_buffer_max_size: DEFAULT_IO_BUFFER_MAX_SIZE

There are new events on_process_line_unknown_event and on_message_data_receiving_event to handle the incoming transmission of unknown commands and message data.

As an example to abort on to many unknown commands to prevent a denial of service attack etc.:

  # event if process_line has identified an unknown command line
  def on_process_line_unknown_event(ctx, line)
    # check
    raise MidiSmtpServer::Smtpd421Exception.new("421 Abort: too many unknown commands where sent!") if ctx[:server][:exceptions] >= 5
    # otherwise call the super method
    super
  end

As an example while receiving message data: abort when message data is going to exceed a maximum size:

  # event while receiving message DATA
  def on_message_data_receiving_event(ctx)
    raise MidiSmtpServer::Smtpd552Exception if ctx[:message][:data].bytesize > MAX_MSG_SIZE
  end

Or to implement something like a Teergrube for spammers etc.:

  # event while receiving message DATA
  def on_message_data_receiving_event(ctx)
    # don't allow the spammer to continue fast
    # let him wait always 15 seconds before sending next data line
    sleep 15 if ctx[:server][:helo] =~ /domain/
  end

Or to check already the message headers before receiving the complete message data. And lots more.