midi-smtp-server 2.3.3 → 3.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'logger'
3
4
  require 'socket'
4
5
  require 'resolv'
5
6
  require 'base64'
@@ -10,6 +11,7 @@ module MidiSmtpServer
10
11
  # import sources
11
12
  require 'midi-smtp-server/version'
12
13
  require 'midi-smtp-server/exceptions'
14
+ require 'midi-smtp-server/logger'
13
15
  require 'midi-smtp-server/tls-transport'
14
16
 
15
17
  # default values
@@ -47,7 +49,7 @@ module MidiSmtpServer
47
49
  end
48
50
 
49
51
  # Stop the server
50
- def stop(wait_seconds_before_close = 2, gracefully = true)
52
+ def stop(wait_seconds_before_close: 2, gracefully: true)
51
53
  # always signal shutdown
52
54
  shutdown if gracefully
53
55
  # wait if some connection(s) need(s) more time to handle shutdown
@@ -101,7 +103,7 @@ module MidiSmtpServer
101
103
  # Join with the server thread(s)
102
104
  # before joining the server threads, check and wait optionally a few seconds
103
105
  # to let the service(s) come up
104
- def join(sleep_seconds_before_join = 1)
106
+ def join(sleep_seconds_before_join: 1)
105
107
  # check already existing TCPServers
106
108
  return if @tcp_servers.empty?
107
109
  # wait some seconds before joininig the upcoming threads
@@ -125,30 +127,23 @@ module MidiSmtpServer
125
127
  @ports.dup
126
128
  end
127
129
 
128
- # New but deprecated method to access the old port attr for compatibility reasons
129
- def port
130
- logger.debug('Deprecated method port is used. Please update to ports.join() etc.')
131
- ports.join(', ')
132
- end
133
-
134
130
  # Array of hosts / ip_addresses on which to bind, set as string seperated by commata like 'name.domain.com, 127.0.0.1, ::1'
135
131
  def hosts
136
132
  # prevent original array from being changed
137
133
  @hosts.dup
138
134
  end
139
135
 
140
- # New but deprecated method to access the old host attr for compatibility reasons
141
- def host
142
- logger.debug('Deprecated method host is used. Please update to hosts.join() etc.')
143
- hosts.join(', ')
144
- end
145
-
146
136
  # Array of ip_address:port which get bound and build up from given hosts and ports
147
137
  def addresses
148
138
  # prevent original array from being changed
149
139
  @addresses.dup
150
140
  end
151
141
 
142
+ # Current TLS OpenSSL::SSL::SSLContext when initalized by :TLS_OPTIONAL, :TLS_REQUIRED
143
+ def ssl_context
144
+ @tls&.ssl_context
145
+ end
146
+
152
147
  # Maximum number of simultaneous processed connections, this does not limit the TCP connections itself, as a FixNum
153
148
  attr_reader :max_processings
154
149
  # Maximum number of allowed connections, this does limit the TCP connections, as a FixNum
@@ -177,38 +172,63 @@ module MidiSmtpServer
177
172
 
178
173
  # Initialize SMTP Server class
179
174
  #
180
- # +ports+:: ports to listen on. Allows multiple ports like "2525, 3535" or "2525:3535, 2525"
181
- # +hosts+:: interface ip or hostname to listen on or "*" to listen on all interfaces, wildcard ("") is deprecated, allows multiple hostnames and ip_addresses like "name.domain.com, 127.0.0.1, ::1"
182
- # +max_processings+:: maximum number of simultaneous processed connections, this does not limit the number of concurrent TCP connections
183
- # +opts+:: hash with optional settings
184
- # +opts.max_connections+:: maximum number of connections, this does limit the number of concurrent TCP connections (not set or nil => unlimited)
185
- # +opts.crlf_mode+:: CRLF handling support (:CRLF_ENSURE [default], :CRLF_LEAVE, :CRLF_STRICT)
186
- # +opts.do_dns_reverse_lookup+:: flag if this smtp server should do reverse DNS lookups on incoming connections
187
- # +opts.io_cmd_timeout+:: time in seconds to wait until complete line of data is expected (DEFAULT_IO_CMD_TIMEOUT, nil => disabled test)
188
- # +opts.io_buffer_chunk_size+:: size of chunks (bytes) to read non-blocking from socket (DEFAULT_IO_BUFFER_CHUNK_SIZE)
189
- # +opts.io_buffer_max_size+:: max size of buffer (max line length) until \lf ist expected (DEFAULT_IO_BUFFER_MAX_SIZE, nil => disabled test)
190
- # +opts.pipelining_extension+:: set to true for support of SMTP PIPELINING extension (DEFAULT_PIPELINING_EXTENSION_ENABLED)
191
- # +opts.internationalization_extensions+:: set to true for support of SMTP 8BITMIME and SMTPUTF8 extensions (DEFAULT_INTERNATIONALIZATION_EXTENSIONS_ENABLED)
192
- # +opts.auth_mode+:: enable builtin authentication support (:AUTH_FORBIDDEN [default], :AUTH_OPTIONAL, :AUTH_REQUIRED)
193
- # +opts.tls_mode+:: enable builtin TLS support (:TLS_FORBIDDEN [default], :TLS_OPTIONAL, :TLS_REQUIRED)
194
- # +opts.tls_cert_path+:: path to tls cerificate chain file
195
- # +opts.tls_key_path+:: path to tls key file
196
- # +opts.tls_ciphers+:: allowed ciphers for connection
197
- # +opts.tls_methods+:: allowed methods for protocol
198
- # +opts.tls_cert_cn+:: set subject (CN) for self signed certificate "cn.domain.com"
199
- # +opts.tls_cert_san+:: set subject alternative (SAN) for self signed certificate, allows multiple names like "alt1.domain.com, alt2.domain.com"
200
- # +opts.logger+:: own logger class, otherwise default logger is created
201
- # +opts.logger_severity+:: logger level when default logger is used
202
- def initialize(ports = DEFAULT_SMTPD_PORT, hosts = DEFAULT_SMTPD_HOST, max_processings = DEFAULT_SMTPD_MAX_PROCESSINGS, opts = {})
203
- # logging
204
- if opts.include?(:logger)
205
- @logger = opts[:logger]
175
+ # +ports+:: ports to listen on. Allows multiple ports like "2525, 3535" or "2525:3535, 2525". Default value = DEFAULT_SMTPD_PORT
176
+ # +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
177
+ # +max_processings+:: maximum number of simultaneous processed connections, this does not limit the number of concurrent TCP connections. Default value = DEFAULT_SMTPD_MAX_PROCESSINGS
178
+ # +max_connections+:: maximum number of connections, this does limit the number of concurrent TCP connections (not set or nil => unlimited)
179
+ # +crlf_mode+:: CRLF handling support (:CRLF_ENSURE [default], :CRLF_LEAVE, :CRLF_STRICT)
180
+ # +do_dns_reverse_lookup+:: flag if this smtp server should do reverse DNS lookups on incoming connections
181
+ # +io_cmd_timeout+:: time in seconds to wait until complete line of data is expected (DEFAULT_IO_CMD_TIMEOUT, nil => disabled test)
182
+ # +io_buffer_chunk_size+:: size of chunks (bytes) to read non-blocking from socket (DEFAULT_IO_BUFFER_CHUNK_SIZE)
183
+ # +io_buffer_max_size+:: max size of buffer (max line length) until \lf ist expected (DEFAULT_IO_BUFFER_MAX_SIZE, nil => disabled test)
184
+ # +pipelining_extension+:: set to true for support of SMTP PIPELINING extension (DEFAULT_PIPELINING_EXTENSION_ENABLED)
185
+ # +internationalization_extensions+:: set to true for support of SMTP 8BITMIME and SMTPUTF8 extensions (DEFAULT_INTERNATIONALIZATION_EXTENSIONS_ENABLED)
186
+ # +auth_mode+:: enable builtin authentication support (:AUTH_FORBIDDEN [default], :AUTH_OPTIONAL, :AUTH_REQUIRED)
187
+ # +tls_mode+:: enable builtin TLS support (:TLS_FORBIDDEN [default], :TLS_OPTIONAL, :TLS_REQUIRED)
188
+ # +tls_cert_path+:: path to tls cerificate chain file
189
+ # +tls_key_path+:: path to tls key file
190
+ # +tls_ciphers+:: allowed ciphers for connection
191
+ # +tls_methods+:: allowed methods for protocol
192
+ # +tls_cert_cn+:: set subject (CN) for self signed certificate "cn.domain.com"
193
+ # +tls_cert_san+:: set subject alternative (SAN) for self signed certificate, allows multiple names like "alt1.domain.com, alt2.domain.com"
194
+ # +logger+:: own logger class, otherwise default logger is created (DEPRECATED: use on_logging_event for special needs on logging)
195
+ # +logger_severity+:: set or override the logger level if given
196
+ def initialize(
197
+ ports: DEFAULT_SMTPD_PORT,
198
+ hosts: DEFAULT_SMTPD_HOST,
199
+ max_processings: DEFAULT_SMTPD_MAX_PROCESSINGS,
200
+ max_connections: nil,
201
+ crlf_mode: nil,
202
+ do_dns_reverse_lookup: nil,
203
+ io_cmd_timeout: nil,
204
+ io_buffer_chunk_size: nil,
205
+ io_buffer_max_size: nil,
206
+ pipelining_extension: nil,
207
+ internationalization_extensions: nil,
208
+ auth_mode: nil,
209
+ tls_mode: nil,
210
+ tls_cert_path: nil,
211
+ tls_key_path: nil,
212
+ tls_ciphers: nil,
213
+ tls_methods: nil,
214
+ tls_cert_cn: nil,
215
+ tls_cert_san: nil,
216
+ logger: nil,
217
+ logger_severity: nil
218
+ )
219
+ # create an exposed logger to forward loggings to the on_logging_event
220
+ @logger = MidiSmtpServer::ForwardingLogger.new(method(:on_logging_event))
221
+
222
+ # external logging
223
+ if logger.nil?
224
+ @logger_protected = Logger.new($stdout)
225
+ @logger_protected.datetime_format = '%Y-%m-%d %H:%M:%S'
226
+ @logger_protected.formatter = proc { |severity, datetime, _progname, msg| "#{datetime}: [#{severity}] #{msg.chomp}\n" }
227
+ @logger_protected.level = logger_severity.nil? ? Logger::DEBUG : logger_severity
206
228
  else
207
- require 'logger'
208
- @logger = Logger.new(STDOUT)
209
- @logger.datetime_format = '%Y-%m-%d %H:%M:%S'
210
- @logger.formatter = proc { |severity, datetime, _progname, msg| "#{datetime}: [#{severity}] #{msg.chomp}\n" }
211
- @logger.level = opts.include?(:logger_severity) ? opts[:logger_severity] : Logger::DEBUG
229
+ @logger_protected = logger
230
+ @logger_protected.level = logger_severity unless logger_severity.nil?
231
+ logger.warn('Deprecated: "logger" was set on new! Please use "on_logging_event" instead.')
212
232
  end
213
233
 
214
234
  # list of TCPServers
@@ -235,8 +255,6 @@ module MidiSmtpServer
235
255
  # build array of hosts
236
256
  # split string into array to instantiate multiple servers
237
257
  @hosts = hosts.to_s.delete(' ').split(',')
238
- # Deprecated default if empty bind to (first found) local host ip_address
239
- # Check that not also the '' wildcard for hosts is added somewhere to the list
240
258
  #
241
259
  # Check source of TCPServer.c at https://github.com/ruby/ruby/blob/trunk/ext/socket/tcpserver.c#L25-L31
242
260
  # * Internally, TCPServer.new calls getaddrinfo() function to obtain ip_addresses.
@@ -247,14 +265,9 @@ module MidiSmtpServer
247
265
  # We won't support that magic anymore. If wish to bind on all local ip_addresses
248
266
  # and interfaces, use new "*" wildcard, otherwise specify ip_addresses and / or hostnames
249
267
  #
250
- if @hosts.empty?
251
- # info and change to "*" wildcard if only "" was given as hosts
252
- logger.debug('Deprecated empty hosts wildcard "" is used. Please use specific hostnames and / or ip_addresses or "*" for wildcard!')
253
- @hosts << '*'
254
- elsif @hosts.include?('')
255
- # raise exception when founding inner wildcard like "a.b.c.d,,e.f.g.h", guess miss-coding
256
- raise 'Deprecated empty hosts wildcard "" is used. Please use specific hostnames and / or ip_addresses or "*" for wildcard!'
257
- end
268
+ # raise exception when found empty or inner empty hosts specification like "" or "a.b.c.d,,e.f.g.h", guess miss-coding
269
+ raise 'No hosts defined! Please use specific hostnames and / or ip_addresses or "*" for wildcard!' if @hosts.empty?
270
+ raise 'Detected an empty identifier in given hosts! Please use specific hostnames and / or ip_addresses or "*" for wildcard!' if @hosts.include?('')
258
271
 
259
272
  # build array of addresses for ip_addresses and ports to use
260
273
  @addresses = []
@@ -300,51 +313,55 @@ module MidiSmtpServer
300
313
 
301
314
  # read max_processings
302
315
  @max_processings = max_processings
316
+ raise 'Number of simultaneous processings (max_processings) must be a positive integer!' unless @max_processings.is_a?(Integer) && @max_processings.positive?
303
317
  # check max_connections
304
- @max_connections = opts.include?(:max_connections) ? opts[:max_connections] : nil
318
+ @max_connections = max_connections
305
319
  raise 'Number of concurrent connections is lower than number of simultaneous processings!' if @max_connections && @max_connections < @max_processings
306
320
 
307
321
  # check for crlf mode
308
- @crlf_mode = opts.include?(:crlf_mode) ? opts[:crlf_mode] : DEFAULT_CRLF_MODE
309
- raise "Unknown CRLF mode #{@crlf_mode} was given by opts!" unless CRLF_MODES.include?(@crlf_mode)
322
+ @crlf_mode = crlf_mode.nil? ? DEFAULT_CRLF_MODE : crlf_mode
323
+ raise "Unknown CRLF mode #{@crlf_mode} was given!" unless CRLF_MODES.include?(@crlf_mode)
310
324
 
311
325
  # always prevent auto resolving hostnames to prevent a delay on socket connect
312
326
  BasicSocket.do_not_reverse_lookup = true
313
327
  # do reverse lookups manually if enabled by io.addr and io.peeraddr
314
- @do_dns_reverse_lookup = opts.include?(:do_dns_reverse_lookup) ? opts[:do_dns_reverse_lookup] : true
328
+ @do_dns_reverse_lookup = do_dns_reverse_lookup.nil? ? true : do_dns_reverse_lookup
315
329
 
316
330
  # io and buffer settings
317
- @io_cmd_timeout = opts.include?(:io_cmd_timeout) ? opts[:io_cmd_timeout] : DEFAULT_IO_CMD_TIMEOUT
318
- @io_buffer_chunk_size = opts.include?(:io_buffer_chunk_size) ? opts[:io_buffer_chunk_size] : DEFAULT_IO_BUFFER_CHUNK_SIZE
319
- @io_buffer_max_size = opts.include?(:io_buffer_max_size) ? opts[:io_buffer_max_size] : DEFAULT_IO_BUFFER_MAX_SIZE
331
+ @io_cmd_timeout = io_cmd_timeout.nil? ? DEFAULT_IO_CMD_TIMEOUT : io_cmd_timeout
332
+ @io_buffer_chunk_size = io_buffer_chunk_size.nil? ? DEFAULT_IO_BUFFER_CHUNK_SIZE : io_buffer_chunk_size
333
+ @io_buffer_max_size = io_buffer_max_size.nil? ? DEFAULT_IO_BUFFER_MAX_SIZE : io_buffer_max_size
320
334
 
321
335
  # smtp extensions
322
- @pipelining_extension = opts.include?(:pipelining_extension) ? opts[:pipelining_extension] : DEFAULT_PIPELINING_EXTENSION_ENABLED
323
- @internationalization_extensions = opts.include?(:internationalization_extensions) ? opts[:internationalization_extensions] : DEFAULT_INTERNATIONALIZATION_EXTENSIONS_ENABLED
336
+ @pipelining_extension = pipelining_extension.nil? ? DEFAULT_PIPELINING_EXTENSION_ENABLED : pipelining_extension
337
+ @internationalization_extensions = internationalization_extensions.nil? ? DEFAULT_INTERNATIONALIZATION_EXTENSIONS_ENABLED : internationalization_extensions
324
338
 
325
339
  # check for authentification
326
- @auth_mode = opts.include?(:auth_mode) ? opts[:auth_mode] : DEFAULT_AUTH_MODE
327
- raise "Unknown authentification mode #{@auth_mode} was given by opts!" unless AUTH_MODES.include?(@auth_mode)
340
+ @auth_mode = auth_mode.nil? ? DEFAULT_AUTH_MODE : auth_mode
341
+ raise "Unknown authentification mode #{@auth_mode} was given!" unless AUTH_MODES.include?(@auth_mode)
328
342
 
329
343
  # check for encryption
330
- @encrypt_mode = opts.include?(:tls_mode) ? opts[:tls_mode] : DEFAULT_ENCRYPT_MODE
331
- raise "Unknown encryption mode #{@encrypt_mode} was given by opts!" unless ENCRYPT_MODES.include?(@encrypt_mode)
344
+ @encrypt_mode = tls_mode.nil? ? DEFAULT_ENCRYPT_MODE : tls_mode
345
+ raise "Unknown encryption mode #{@encrypt_mode} was given!" unless ENCRYPT_MODES.include?(@encrypt_mode)
332
346
  # SSL transport layer for STARTTLS
333
347
  if @encrypt_mode == :TLS_FORBIDDEN
334
348
  @tls = nil
335
349
  else
336
- require 'openssl'
350
+ # try to load openssl gem now
351
+ begin
352
+ require 'openssl'
353
+ rescue LoadError
354
+ end
355
+ # check for openssl gem when enabling TLS / SSL
356
+ raise 'The openssl library gem is not installed!' unless defined?(OpenSSL)
337
357
  # check for given CN and SAN
338
- if opts.include?(:tls_cert_cn)
339
- tls_cert_cn = opts[:tls_cert_cn].to_s.strip
340
- tls_cert_san = opts[:tls_cert_san].to_s.delete(' ').split(',')
341
- else
358
+ if tls_cert_cn.nil?
342
359
  # build generic set of "valid" self signed certificate CN and SAN
343
360
  # using all given hosts and detected ip_addresses but not "*" wildcard
344
361
  tls_cert_san = ([] + @hosts + @addresses.map { |address| address.rpartition(':').first }).uniq
345
362
  tls_cert_san.delete('*')
346
363
  # build generic CN based on first SAN
347
- if tls_cert_san.first =~ /^(127\.0?0?0\.0?0?0\.0?0?1|::1|localhost)$/i
364
+ if tls_cert_san.first.match?(/^(127\.0?0?0\.0?0?0\.0?0?1|::1|localhost)$/i)
348
365
  # used generic localhost.local
349
366
  tls_cert_cn = 'localhost.local'
350
367
  else
@@ -353,9 +370,33 @@ module MidiSmtpServer
353
370
  tls_cert_cn = tls_cert_san.first
354
371
  tls_cert_san.slice!(0)
355
372
  end
373
+ else
374
+ tls_cert_cn = tls_cert_cn.to_s.strip
375
+ tls_cert_san = tls_cert_san.to_s.delete(' ').split(',')
356
376
  end
357
377
  # create ssl transport service
358
- @tls = TlsTransport.new(opts[:tls_cert_path], opts[:tls_key_path], opts[:tls_ciphers], opts[:tls_methods], tls_cert_cn, tls_cert_san, @logger)
378
+ @tls = TlsTransport.new(tls_cert_path, tls_key_path, tls_ciphers, tls_methods, tls_cert_cn, tls_cert_san, @logger)
379
+ end
380
+ end
381
+
382
+ # event on LOGGING
383
+ # the exposed logger property is from class MidiSmtpServer::ForwardingLogger
384
+ # and pushes any logging message to this on_logging_event.
385
+ # if logging occurs from inside session, the _ctx should be not nil
386
+ # if logging occurs from an error, the err object should be filled
387
+ def on_logging_event(_ctx, severity, msg, err: nil)
388
+ case severity
389
+ when Logger::INFO
390
+ @logger_protected.info(msg)
391
+ when Logger::WARN
392
+ @logger_protected.warn(msg)
393
+ when Logger::ERROR
394
+ @logger_protected.error(msg)
395
+ when Logger::FATAL
396
+ @logger_protected.fatal(msg)
397
+ err.backtrace.each { |log| @logger_protected.fatal("#{log}") }
398
+ else
399
+ @logger_protected.debug(msg)
359
400
  end
360
401
  end
361
402
 
@@ -365,12 +406,12 @@ module MidiSmtpServer
365
406
  # that these will be used as local welcome and greeting strings
366
407
  # the values are not allowed to return CR nor LF chars and will be stripped
367
408
  def on_connect_event(ctx)
368
- logger.debug("Client connect from #{ctx[:server][:remote_ip]}:#{ctx[:server][:remote_port]} to #{ctx[:server][:local_ip]}:#{ctx[:server][:local_port]}")
409
+ 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]}")
369
410
  end
370
411
 
371
412
  # event before DISONNECT
372
413
  def on_disconnect_event(ctx)
373
- logger.debug("Client disconnect from #{ctx[:server][:remote_ip]}:#{ctx[:server][:remote_port]} on #{ctx[:server][:local_ip]}:#{ctx[:server][:local_port]}")
414
+ 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]}")
374
415
  end
375
416
 
376
417
  # event on HELO/EHLO
@@ -386,7 +427,7 @@ module MidiSmtpServer
386
427
  # if authentification is used, override this event
387
428
  # and implement your own user management.
388
429
  # otherwise all authentifications are blocked per default
389
- logger.debug("Deny access from #{ctx[:server][:remote_ip]}:#{ctx[:server][:remote_port]} for #{authentication_id}" + (authorization_id == '' ? '' : "/#{authorization_id}") + " with #{authentication}")
430
+ 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}")
390
431
  raise Smtpd535Exception
391
432
  end
392
433
 
@@ -453,7 +494,7 @@ module MidiSmtpServer
453
494
  # log information
454
495
  logger.info("Starting service on #{ip_address}:#{port}")
455
496
  # check that there is a specific ip_address defined
456
- raise 'Deprecated wildcard "" ist not allowed anymore to start a listener on!' if ip_address.empty?
497
+ raise 'Unable to start TCP listener on missing or empty ip_address!' if ip_address.empty?
457
498
  # instantiate the service for ip_address and port
458
499
  tcp_server = TCPServer.new(ip_address, port)
459
500
  # append this server to the list of TCPServers
@@ -482,7 +523,7 @@ module MidiSmtpServer
482
523
  # ignore this exception due to service shutdown
483
524
  rescue StandardError => e
484
525
  # log fatal error while handling connection
485
- logger.fatal(e.backtrace.join("\n"))
526
+ on_logging_event(nil, Logger::FATAL, "#{e} (#{e.class})".strip, err: e.clone)
486
527
  ensure
487
528
  begin
488
529
  # always gracefully shutdown connection.
@@ -509,7 +550,7 @@ module MidiSmtpServer
509
550
  # ignore this exception due to service shutdown
510
551
  rescue StandardError => e
511
552
  # log fatal error while starting new thread
512
- logger.fatal(e.backtrace.join("\n"))
553
+ on_logging_event(nil, Logger::FATAL, "#{e} (#{e.class})".strip, err: e.clone)
513
554
  ensure
514
555
  begin
515
556
  # drop the service
@@ -545,7 +586,7 @@ module MidiSmtpServer
545
586
  # 220 <domain> Service ready
546
587
  # 421 <domain> Service not available, closing transmission channel
547
588
  # Reset and initialize message
548
- process_reset_session(session, true)
589
+ process_reset_session(session, connection_initialize: true)
549
590
 
550
591
  # get local address info
551
592
  _, 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)
@@ -578,7 +619,7 @@ module MidiSmtpServer
578
619
  output = +"220 #{session[:ctx][:server][:local_response].to_s.strip}\r\n"
579
620
 
580
621
  # log and show to client
581
- logger.debug(+'>>> ' << output)
622
+ on_logging_event(session[:ctx], Logger::DEBUG, +'>>> ' << output)
582
623
  io.print output unless io.closed?
583
624
 
584
625
  # initialize \r\n for line_break, this is used for CRLF_ENSURE and CRLF_STRICT and mark as mutable
@@ -646,27 +687,27 @@ module MidiSmtpServer
646
687
  # remove any \r or \n occurence from line
647
688
  line.delete!("\r\n")
648
689
  # log line, verbosity based on log severity and command sequence
649
- logger.debug(+'<<< ' << line << "\n") if session[:cmd_sequence] != :CMD_DATA
690
+ on_logging_event(session[:ctx], Logger::DEBUG, +'<<< ' << line << "\n") if session[:cmd_sequence] != :CMD_DATA
650
691
 
651
692
  when :CRLF_LEAVE
652
693
  # use input line_break for line_break
653
- line_break = line[-2..-1] == "\r\n" ? "\r\n" : "\n"
694
+ line_break = line[-2..] == "\r\n" ? "\r\n" : "\n"
654
695
  # check to override session crlf info, only when CRLF_LEAVE is used and in DATA mode
655
696
  session[:ctx][:message][:crlf] = line_break if session[:cmd_sequence] == :CMD_DATA
656
697
  # remove any line_break from line
657
698
  line.chomp!
658
699
  # log line, verbosity based on log severity and command sequence
659
- logger.debug(+'<<< ' << line.gsub("\r", '[\r]') << "\n") if session[:cmd_sequence] != :CMD_DATA
700
+ on_logging_event(session[:ctx], Logger::DEBUG, +'<<< ' << line.gsub("\r", '[\r]') << "\n") if session[:cmd_sequence] != :CMD_DATA
660
701
 
661
702
  when :CRLF_STRICT
662
703
  # check line ends up by \r\n
663
- raise Smtpd500CrLfSequenceException unless line[-2..-1] == "\r\n"
704
+ raise Smtpd500CrLfSequenceException unless line[-2..] == "\r\n"
664
705
  # remove any line_break from line
665
706
  line.chomp!
666
707
  # check line for additional \r
667
708
  raise Smtpd500Exception, 'Line contains additional CR chars!' if line.index("\r")
668
709
  # log line, verbosity based on log severity and command sequence
669
- logger.debug(+'<<< ' << line << "\n") if session[:cmd_sequence] != :CMD_DATA
710
+ on_logging_event(session[:ctx], Logger::DEBUG, +'<<< ' << line << "\n") if session[:cmd_sequence] != :CMD_DATA
670
711
  end
671
712
 
672
713
  # process line and mark output as mutable
@@ -681,8 +722,10 @@ module MidiSmtpServer
681
722
  rescue SmtpdException => e
682
723
  # inc number of detected exceptions during this session
683
724
  session[:ctx][:server][:exceptions] += 1
725
+ # save clone of error object to context
726
+ session[:ctx][:server][:errors].push(e.clone)
684
727
  # log error info if logging
685
- logger.error("#{e}")
728
+ on_logging_event(session[:ctx], Logger::ERROR, "#{e} (#{e.class})".strip, err: e)
686
729
  # get the given smtp dialog result
687
730
  output = +"#{e.smtpd_result}"
688
731
 
@@ -690,8 +733,10 @@ module MidiSmtpServer
690
733
  rescue StandardError => e
691
734
  # inc number of detected exceptions during this session
692
735
  session[:ctx][:server][:exceptions] += 1
736
+ # save clone of error object to context
737
+ session[:ctx][:server][:errors].push(e.clone)
693
738
  # log error info if logging
694
- logger.error("#{e}")
739
+ on_logging_event(session[:ctx], Logger::ERROR, "#{e} (#{e.class})".strip, err: e)
695
740
  # set default smtp server dialog error
696
741
  output = +"#{Smtpd500Exception.new.smtpd_result}"
697
742
  end
@@ -699,7 +744,7 @@ module MidiSmtpServer
699
744
  # check result
700
745
  unless output.empty?
701
746
  # log smtp dialog // message data is stored separate
702
- logger.debug(+'>>> ' << output)
747
+ on_logging_event(session[:ctx], Logger::DEBUG, +'>>> ' << output)
703
748
  # append line feed
704
749
  output << "\r\n"
705
750
  # smtp dialog response
@@ -719,21 +764,25 @@ module MidiSmtpServer
719
764
  io.print(output) unless io.closed?
720
765
 
721
766
  # connection was simply closed / aborted by remote closing socket
722
- rescue EOFError
767
+ rescue EOFError => e
723
768
  # log info but only while debugging otherwise ignore message
724
- logger.debug('EOFError - Connection lost due abort by client!')
769
+ on_logging_event(session[:ctx], Logger::DEBUG, 'Connection lost due abort by client! (EOFError)', err: e)
725
770
 
726
771
  rescue StandardError => e
772
+ # inc number of detected exceptions during this session
773
+ session[:ctx][:server][:exceptions] += 1
774
+ # save clone of error object to context
775
+ session[:ctx][:server][:errors].push(e.clone)
727
776
  # log error info if logging
728
- logger.error("#{e}")
777
+ on_logging_event(session[:ctx], Logger::ERROR, "#{e} (#{e.class})".strip, err: e)
729
778
  # power down connection
730
779
  # ignore IOErrors when sending final smtp abort return code 421
731
780
  begin
732
781
  output = +"#{Smtpd421Exception.new.smtpd_result}\r\n"
733
782
  # smtp dialog response
734
783
  io.print(output) unless io.closed?
735
- rescue StandardError
736
- logger.debug('IOError - Can\'t send 421 abort code!')
784
+ rescue StandardError => e
785
+ on_logging_event(session[:ctx], Logger::DEBUG, 'Can\'t send 421 abort code! (IOError)', err: e)
737
786
  end
738
787
  end
739
788
 
@@ -790,6 +839,7 @@ module MidiSmtpServer
790
839
  # check whether to answer as HELO or EHLO
791
840
  case line
792
841
  when (/^EHLO/i)
842
+ # rubocop:disable Style/StringConcatenation
793
843
  # reply supported extensions
794
844
  return "250-#{session[:ctx][:server][:helo_response].to_s.strip}\r\n" +
795
845
  # respond with 8BITMIME extension
@@ -803,6 +853,7 @@ module MidiSmtpServer
803
853
  # respond with STARTTLS if available and not already enabled
804
854
  (@encrypt_mode == :TLS_FORBIDDEN || encrypted?(session[:ctx]) ? '' : "250-STARTTLS\r\n") +
805
855
  '250 OK'
856
+ # rubocop:enable all
806
857
  else
807
858
  # reply ok only
808
859
  return "250 OK #{session[:ctx][:server][:helo_response].to_s.strip}".strip
@@ -847,7 +898,7 @@ module MidiSmtpServer
847
898
  # check that not already authenticated
848
899
  raise Smtpd503Exception if authenticated?(session[:ctx])
849
900
  # handle command line
850
- auth_data = line.gsub(/^AUTH\ /i, '').strip.gsub(/\s+/, ' ').split(' ')
901
+ auth_data = line.gsub(/^AUTH\ /i, '').strip.gsub(/\s+/, ' ').split
851
902
  # handle auth command
852
903
  case auth_data[0]
853
904
 
@@ -865,26 +916,31 @@ module MidiSmtpServer
865
916
 
866
917
  when (/LOGIN/i)
867
918
  # check if auth_id was sent too
868
- if auth_data.length == 1
869
- # reset auth_challenge
870
- session[:auth_challenge] = {}
871
- # set sequence for next command input
872
- session[:cmd_sequence] = :CMD_AUTH_LOGIN_USER
873
- # response code with request for Username
874
- return '334 ' + Base64.strict_encode64('Username:')
875
- elsif auth_data.length == 2
876
- # handle next sequence
877
- process_auth_login_user(session, auth_data[1])
878
- else
879
- raise Smtpd500Exception
919
+ case auth_data.length
920
+
921
+ when 1
922
+ # reset auth_challenge
923
+ session[:auth_challenge] = {}
924
+ # set sequence for next command input
925
+ session[:cmd_sequence] = :CMD_AUTH_LOGIN_USER
926
+ # response code with request for Username
927
+ return +'' << '334 ' << Base64.strict_encode64('Username:')
928
+
929
+ when 2
930
+ # handle next sequence
931
+ process_auth_login_user(session, auth_data[1])
932
+
933
+ else
934
+ raise Smtpd500Exception
880
935
  end
881
936
 
882
- when (/CRAM-MD5/i)
883
- # not supported in case of also unencrypted data delivery
884
- # instead of supporting password encryption only, we will
885
- # provide optional SMTPS service instead
886
- # read discussion on https://github.com/4commerce-technologies-AG/midi-smtp-server/issues/3#issuecomment-126898711
887
- raise Smtpd500Exception
937
+ # not supported in case of also unencrypted data delivery
938
+ # instead of supporting password encryption only, we will
939
+ # provide optional SMTPS service instead
940
+ # read discussion on https://github.com/4commerce-technologies-AG/midi-smtp-server/issues/3#issuecomment-126898711
941
+ #
942
+ # when (/CRAM-MD5/i)
943
+ # raise Smtpd500Exception
888
944
 
889
945
  else
890
946
  # unknown auth method
@@ -921,7 +977,7 @@ module MidiSmtpServer
921
977
  session[:cmd_sequence] = :CMD_QUIT
922
978
  return ''
923
979
 
924
- when (/^MAIL FROM\:/i)
980
+ when (/^MAIL FROM:/i)
925
981
  # MAIL
926
982
  # 250 Requested mail action okay, completed
927
983
  # 421 <domain> Service not available, closing transmission channel
@@ -938,7 +994,7 @@ module MidiSmtpServer
938
994
  # check that authentication is enabled if necessary
939
995
  raise Smtpd530Exception if @auth_mode == :AUTH_REQUIRED && !authenticated?(session[:ctx])
940
996
  # handle command
941
- cmd_data = line.gsub(/^MAIL FROM\:/i, '').strip
997
+ cmd_data = line.gsub(/^MAIL FROM:/i, '').strip
942
998
  # check for BODY= parameter
943
999
  case cmd_data
944
1000
  # test for explicit 7bit
@@ -982,7 +1038,7 @@ module MidiSmtpServer
982
1038
  # reply ok
983
1039
  return '250 OK'
984
1040
 
985
- when (/^RCPT TO\:/i)
1041
+ when (/^RCPT TO:/i)
986
1042
  # RCPT
987
1043
  # 250 Requested mail action okay, completed
988
1044
  # 251 User not local; will forward to <forward-path>
@@ -1006,7 +1062,7 @@ module MidiSmtpServer
1006
1062
  # check that authentication is enabled if necessary
1007
1063
  raise Smtpd530Exception if @auth_mode == :AUTH_REQUIRED && !authenticated?(session[:ctx])
1008
1064
  # handle command
1009
- cmd_data = line.gsub(/^RCPT TO\:/i, '').strip
1065
+ cmd_data = line.gsub(/^RCPT TO:/i, '').strip
1010
1066
  # call event to handle data
1011
1067
  return_value = on_rcpt_to_event(session[:ctx], cmd_data)
1012
1068
  if return_value
@@ -1125,7 +1181,7 @@ module MidiSmtpServer
1125
1181
  end
1126
1182
 
1127
1183
  # reset the context of current smtpd dialog
1128
- def process_reset_session(session, connection_initialize = false)
1184
+ def process_reset_session(session, connection_initialize: false)
1129
1185
  # set active command sequence info
1130
1186
  session[:cmd_sequence] = connection_initialize ? :CMD_HELO : :CMD_RSET
1131
1187
  # drop any auth challenge
@@ -1149,6 +1205,7 @@ module MidiSmtpServer
1149
1205
  helo_response: +'',
1150
1206
  connected: +'',
1151
1207
  exceptions: 0,
1208
+ errors: [],
1152
1209
  authorization_id: +'',
1153
1210
  authentication_id: +'',
1154
1211
  authenticated: +'',
@@ -1211,7 +1268,7 @@ module MidiSmtpServer
1211
1268
  # set sequence for next command input
1212
1269
  session[:cmd_sequence] = :CMD_AUTH_LOGIN_PASS
1213
1270
  # response code with request for Password
1214
- return '334 ' + Base64.strict_encode64('Password:')
1271
+ return +'' << '334 ' << Base64.strict_encode64('Password:')
1215
1272
  end
1216
1273
 
1217
1274
  def process_auth_login_pass(session, encoded_auth_response)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: midi-smtp-server
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.3.3
4
+ version: 3.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tom Freudenberg
@@ -17,10 +17,12 @@ executables: []
17
17
  extensions: []
18
18
  extra_rdoc_files: []
19
19
  files:
20
+ - CHANGELOG.md
20
21
  - MIT-LICENSE.txt
21
22
  - README.md
22
23
  - lib/midi-smtp-server.rb
23
24
  - lib/midi-smtp-server/exceptions.rb
25
+ - lib/midi-smtp-server/logger.rb
24
26
  - lib/midi-smtp-server/tls-transport.rb
25
27
  - lib/midi-smtp-server/version.rb
26
28
  homepage:
@@ -31,7 +33,7 @@ metadata:
31
33
  source_code_uri: https://github.com/4commerce-technologies-AG/midi-smtp-server
32
34
  changelog_uri: https://github.com/4commerce-technologies-AG/midi-smtp-server#changes-and-updates
33
35
  bug_tracker_uri: https://github.com/4commerce-technologies-AG/midi-smtp-server/issues
34
- documentation_uri: https://www.rubydoc.info/gems/midi-smtp-server/2.3.3
36
+ documentation_uri: https://www.rubydoc.info/gems/midi-smtp-server/3.0.3
35
37
  wiki_uri: https://midi-smtp-server.readthedocs.io/
36
38
  post_install_message:
37
39
  rdoc_options: []
@@ -41,7 +43,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
41
43
  requirements:
42
44
  - - ">="
43
45
  - !ruby/object:Gem::Version
44
- version: '0'
46
+ version: 2.6.0
45
47
  required_rubygems_version: !ruby/object:Gem::Requirement
46
48
  requirements:
47
49
  - - ">="