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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +112 -0
- data/MIT-LICENSE.txt +1 -1
- data/README.md +35 -950
- data/lib/midi-smtp-server/logger.rb +37 -0
- data/lib/midi-smtp-server/tls-transport.rb +54 -33
- data/lib/midi-smtp-server/version.rb +2 -2
- data/lib/midi-smtp-server.rb +178 -121
- metadata +5 -3
data/lib/midi-smtp-server.rb
CHANGED
@@ -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
|
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
|
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,
|
182
|
-
# +max_processings+:: maximum number of simultaneous processed connections, this does not limit the number of concurrent TCP connections
|
183
|
-
# +
|
184
|
-
# +
|
185
|
-
# +
|
186
|
-
# +
|
187
|
-
# +
|
188
|
-
# +
|
189
|
-
# +
|
190
|
-
# +
|
191
|
-
# +
|
192
|
-
# +
|
193
|
-
# +
|
194
|
-
# +
|
195
|
-
# +
|
196
|
-
# +
|
197
|
-
# +
|
198
|
-
# +
|
199
|
-
# +
|
200
|
-
# +
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
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
|
-
|
208
|
-
@
|
209
|
-
|
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
|
-
|
251
|
-
|
252
|
-
|
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 =
|
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 =
|
309
|
-
raise "Unknown CRLF mode #{@crlf_mode} was given
|
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 =
|
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 =
|
318
|
-
@io_buffer_chunk_size =
|
319
|
-
@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 =
|
323
|
-
@internationalization_extensions =
|
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 =
|
327
|
-
raise "Unknown authentification mode #{@auth_mode} was given
|
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 =
|
331
|
-
raise "Unknown encryption mode #{@encrypt_mode} was given
|
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
|
-
|
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
|
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
|
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(
|
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
|
-
|
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
|
-
|
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
|
-
|
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 '
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
869
|
-
|
870
|
-
|
871
|
-
|
872
|
-
|
873
|
-
|
874
|
-
|
875
|
-
|
876
|
-
|
877
|
-
|
878
|
-
|
879
|
-
|
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
|
-
|
883
|
-
|
884
|
-
|
885
|
-
|
886
|
-
|
887
|
-
|
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
|
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
|
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
|
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
|
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
|
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 '
|
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:
|
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/
|
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:
|
46
|
+
version: 2.6.0
|
45
47
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
46
48
|
requirements:
|
47
49
|
- - ">="
|