midi-smtp-server 2.3.3 → 3.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
  - - ">="