midi-smtp-server 2.3.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.
@@ -0,0 +1,1248 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'socket'
4
+ require 'resolv'
5
+ require 'base64'
6
+
7
+ # A small and highly customizable ruby SMTP-Server.
8
+ module MidiSmtpServer
9
+
10
+ # import sources
11
+ require 'midi-smtp-server/version'
12
+ require 'midi-smtp-server/exceptions'
13
+ require 'midi-smtp-server/tls-transport'
14
+
15
+ # default values
16
+ DEFAULT_SMTPD_HOST = '127.0.0.1'
17
+ DEFAULT_SMTPD_PORT = 2525
18
+ DEFAULT_SMTPD_MAX_PROCESSINGS = 4
19
+
20
+ # default values for conformity to RFC(2)822 and addtionals
21
+ # if interested in details, checkout discussion on issue queue at:
22
+ # https://github.com/4commerce-technologies-AG/midi-smtp-server/issues/16
23
+ CRLF_MODES = [:CRLF_ENSURE, :CRLF_LEAVE, :CRLF_STRICT].freeze
24
+ DEFAULT_CRLF_MODE = :CRLF_ENSURE
25
+
26
+ # default values for IO operations
27
+ DEFAULT_IO_CMD_TIMEOUT = 30
28
+ DEFAULT_IO_BUFFER_CHUNK_SIZE = 4 * 1024
29
+ DEFAULT_IO_BUFFER_MAX_SIZE = 1 * 1024 * 1024
30
+
31
+ # default value for SMTPD extensions support
32
+ DEFAULT_PIPELINING_EXTENSION_ENABLED = false
33
+ DEFAULT_INTERNATIONALIZATION_EXTENSIONS_ENABLED = false
34
+
35
+ # Authentification modes
36
+ AUTH_MODES = [:AUTH_FORBIDDEN, :AUTH_OPTIONAL, :AUTH_REQUIRED].freeze
37
+ DEFAULT_AUTH_MODE = :AUTH_FORBIDDEN
38
+
39
+ # class for SmtpServer
40
+ class Smtpd
41
+
42
+ public
43
+
44
+ # Start the server
45
+ def start
46
+ serve_service
47
+ end
48
+
49
+ # Stop the server
50
+ def stop(wait_seconds_before_close = 2, gracefully = true)
51
+ # always signal shutdown
52
+ shutdown if gracefully
53
+ # wait if some connection(s) need(s) more time to handle shutdown
54
+ sleep wait_seconds_before_close if connections?
55
+ # drop tcp_servers while raising SmtpdStopServiceException
56
+ @connections_mutex.synchronize do
57
+ @tcp_server_threads.each do |tcp_server_thread|
58
+ # use safe navigation (&.) to make sure that obj exists like ... if tcp_server_thread
59
+ tcp_server_thread&.raise SmtpdStopServiceException
60
+ end
61
+ end
62
+ # wait if some connection(s) still need(s) more time to come down
63
+ sleep wait_seconds_before_close if connections? || !stopped?
64
+ end
65
+
66
+ # Returns true if the server has stopped.
67
+ def stopped?
68
+ @tcp_server_threads.empty? && @tcp_servers.empty?
69
+ end
70
+
71
+ # Schedule a shutdown for the server
72
+ def shutdown
73
+ @shutdown = true
74
+ end
75
+
76
+ # test for shutdown state
77
+ def shutdown?
78
+ @shutdown
79
+ end
80
+
81
+ # Return the current number of connected clients
82
+ def connections
83
+ @connections.size
84
+ end
85
+
86
+ # Return if has active connected clients
87
+ def connections?
88
+ @connections.any?
89
+ end
90
+
91
+ # Return the current number of processed clients
92
+ def processings
93
+ @processings.size
94
+ end
95
+
96
+ # Return if has active processed clients
97
+ def processings?
98
+ @processings.any?
99
+ end
100
+
101
+ # Join with the server thread(s)
102
+ # before joining the server threads, check and wait optionally a few seconds
103
+ # to let the service(s) come up
104
+ def join(sleep_seconds_before_join = 1)
105
+ # check already existing TCPServers
106
+ return if @tcp_servers.empty?
107
+ # wait some seconds before joininig the upcoming threads
108
+ # and check that all TCPServers gots one thread
109
+ while (@tcp_server_threads.length < @tcp_servers.length) && sleep_seconds_before_join.positive?
110
+ sleep_seconds_before_join -= 1
111
+ sleep 1
112
+ end
113
+ # try to join any thread
114
+ begin
115
+ @tcp_server_threads.each(&:join)
116
+
117
+ # catch ctrl-c to stop service
118
+ rescue Interrupt
119
+ end
120
+ end
121
+
122
+ # Array of ports on which to bind, set as string seperated by commata like '2525, 3535' or '2525:3535, 2525'
123
+ def ports
124
+ # prevent original array from being changed
125
+ @ports.dup
126
+ end
127
+
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
+ # 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
+ def hosts
136
+ # prevent original array from being changed
137
+ @hosts.dup
138
+ end
139
+
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
+ # Array of ip_address:port which get bound and build up from given hosts and ports
147
+ def addresses
148
+ # prevent original array from being changed
149
+ @addresses.dup
150
+ end
151
+
152
+ # Maximum number of simultaneous processed connections, this does not limit the TCP connections itself, as a FixNum
153
+ attr_reader :max_processings
154
+ # Maximum number of allowed connections, this does limit the TCP connections, as a FixNum
155
+ attr_reader :max_connections
156
+ # CRLF handling based on conformity to RFC(2)822
157
+ attr_reader :crlf_mode
158
+ # Maximum time in seconds to wait for a complete incoming data line, as a FixNum
159
+ attr_reader :io_cmd_timeout
160
+ # Bytes to read non-blocking from socket into buffer, as a FixNum
161
+ attr_reader :io_buffer_chunk_size
162
+ # Maximum bytes to read as buffer before expecting completet incoming data line, as a FixNum
163
+ attr_reader :io_buffer_max_size
164
+ # Flag if should do reverse DNS lookups on incoming connections
165
+ attr_reader :do_dns_reverse_lookup
166
+ # Authentification mode
167
+ attr_reader :auth_mode
168
+ # Encryption mode
169
+ attr_reader :encrypt_mode
170
+ # handle SMTP PIPELINING extension
171
+ attr_reader :pipelining_extension
172
+ # handle SMTP 8BITMIME and SMTPUTF8 extension
173
+ attr_reader :internationalization_extensions
174
+
175
+ # logging object, may be overrriden by special loggers like YELL or others
176
+ attr_reader :logger
177
+
178
+ # Initialize SMTP Server class
179
+ #
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]
206
+ 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
212
+ end
213
+
214
+ # list of TCPServers
215
+ @tcp_servers = []
216
+ # list of running threads
217
+ @tcp_server_threads = []
218
+
219
+ # lists for connections and thread management
220
+ @connections = []
221
+ @processings = []
222
+ @connections_mutex = Mutex.new
223
+ @connections_cv = ConditionVariable.new
224
+
225
+ # settings
226
+
227
+ # build array of ports
228
+ # split string into array to instantiate multiple servers
229
+ @ports = ports.to_s.delete(' ').split(',')
230
+ # check for at least one port specification
231
+ raise 'Missing port(s) to bind service(s) to!' if @ports.empty?
232
+ # check that not also a '' empty item for port is added to the list
233
+ raise 'Do not use empty value "" for port(s). Please use specific port(s)!' if @ports.include?('')
234
+
235
+ # build array of hosts
236
+ # split string into array to instantiate multiple servers
237
+ @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
+ #
241
+ # Check source of TCPServer.c at https://github.com/ruby/ruby/blob/trunk/ext/socket/tcpserver.c#L25-L31
242
+ # * Internally, TCPServer.new calls getaddrinfo() function to obtain ip_addresses.
243
+ # * If getaddrinfo() returns multiple ip_addresses,
244
+ # * TCPServer.new TRIES to create a server socket for EACH address and RETURNS FIRST one that is SUCCESSFUL.
245
+ #
246
+ # So for that it was a small portion of luck which address had been used then.
247
+ # We won't support that magic anymore. If wish to bind on all local ip_addresses
248
+ # and interfaces, use new "*" wildcard, otherwise specify ip_addresses and / or hostnames
249
+ #
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
258
+
259
+ # build array of addresses for ip_addresses and ports to use
260
+ @addresses = []
261
+ @hosts.each_with_index do |host, index|
262
+ # resolv ip_addresses for host if not wildcard / all hosts
263
+ # if host is "*" wildcard (all) interfaces are used
264
+ # otherwise it will be bind to the found host ip_addresses
265
+ if host == '*'
266
+ ip_addresses_for_host = []
267
+ Socket.ip_address_list.each do |a|
268
+ # test for all local valid ipv4 and ipv6 ip_addresses
269
+ # check question on stackoverflow for details
270
+ # https://stackoverflow.com/questions/59770803/identify-all-relevant-ip-addresses-from-ruby-socket-ip-address-list
271
+ ip_addresses_for_host << a.ip_address if \
272
+ (a.ipv4? &&
273
+ (a.ipv4_loopback? || a.ipv4_private? ||
274
+ !(a.ipv4_loopback? || a.ipv4_private? || a.ipv4_multicast?)
275
+ )
276
+ ) ||
277
+ (a.ipv6? &&
278
+ (a.ipv6_loopback? || a.ipv6_unique_local? ||
279
+ !(a.ipv6_loopback? || a.ipv6_unique_local? || a.ipv6_linklocal? || a.ipv6_multicast? || a.ipv6_sitelocal? ||
280
+ a.ipv6_mc_global? || a.ipv6_mc_linklocal? || a.ipv6_mc_nodelocal? || a.ipv6_mc_orglocal? || a.ipv6_mc_sitelocal? ||
281
+ a.ipv6_v4compat? || a.ipv6_v4mapped? || a.ipv6_unspecified?)
282
+ )
283
+ )
284
+ end
285
+ else
286
+ ip_addresses_for_host = Resolv.new.getaddresses(host).uniq
287
+ end
288
+ # get ports for that host entry
289
+ # if ports at index are not specified, use last item
290
+ # of ports array. if multiple ports specified by
291
+ # item like 2525:3535:4545, then all ports will be instantiated
292
+ ports_for_host = (index < @ports.length ? @ports[index] : @ports.last).to_s.split(':')
293
+ # append combination of ip_address and ports to the list of serving addresses
294
+ ip_addresses_for_host.each do |ip_address|
295
+ ports_for_host.each do |port|
296
+ @addresses << "#{ip_address}:#{port}"
297
+ end
298
+ end
299
+ end
300
+
301
+ # read max_processings
302
+ @max_processings = max_processings
303
+ # check max_connections
304
+ @max_connections = opts.include?(:max_connections) ? opts[:max_connections] : nil
305
+ raise 'Number of concurrent connections is lower than number of simultaneous processings!' if @max_connections && @max_connections < @max_processings
306
+
307
+ # 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)
310
+
311
+ # always prevent auto resolving hostnames to prevent a delay on socket connect
312
+ BasicSocket.do_not_reverse_lookup = true
313
+ # 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
315
+
316
+ # 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
320
+
321
+ # 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
324
+
325
+ # 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)
328
+
329
+ # 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)
332
+ # SSL transport layer for STARTTLS
333
+ if @encrypt_mode == :TLS_FORBIDDEN
334
+ @tls = nil
335
+ else
336
+ require 'openssl'
337
+ # 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
342
+ # build generic set of "valid" self signed certificate CN and SAN
343
+ # using all given hosts and detected ip_addresses but not "*" wildcard
344
+ tls_cert_san = ([] + @hosts + @addresses.map { |address| address.rpartition(':').first }).uniq
345
+ tls_cert_san.delete('*')
346
+ # 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
348
+ # used generic localhost.local
349
+ tls_cert_cn = 'localhost.local'
350
+ else
351
+ # use first element from detected hosts and ip_addresses
352
+ # drop that element from SAN
353
+ tls_cert_cn = tls_cert_san.first
354
+ tls_cert_san.slice!(0)
355
+ end
356
+ end
357
+ # 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)
359
+ end
360
+ end
361
+
362
+ # event on CONNECTION
363
+ # you may change the ctx[:server][:local_response] and
364
+ # you may change the ctx[:server][:helo_response] in here so
365
+ # that these will be used as local welcome and greeting strings
366
+ # the values are not allowed to return CR nor LF chars and will be stripped
367
+ 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]}")
369
+ end
370
+
371
+ # event before DISONNECT
372
+ 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]}")
374
+ end
375
+
376
+ # event on HELO/EHLO
377
+ # you may change the ctx[:server][:helo_response] in here so
378
+ # that this will be used as greeting string
379
+ # the value is not allowed to return CR nor LF chars and will be stripped
380
+ def on_helo_event(ctx, helo_data) end
381
+
382
+ # check the authentification on AUTH
383
+ # if any value returned, that will be used for ongoing processing
384
+ # otherwise the original value will be used for authorization_id
385
+ def on_auth_event(ctx, authorization_id, authentication_id, authentication)
386
+ # if authentification is used, override this event
387
+ # and implement your own user management.
388
+ # 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}")
390
+ raise Smtpd535Exception
391
+ end
392
+
393
+ # check the status of authentication for a given context
394
+ def authenticated?(ctx)
395
+ ctx[:server][:authenticated] && !ctx[:server][:authenticated].to_s.empty?
396
+ end
397
+
398
+ # check the status of encryption for a given context
399
+ def encrypted?(ctx)
400
+ ctx[:server][:encrypted] && !ctx[:server][:encrypted].to_s.empty?
401
+ end
402
+
403
+ # get address send in MAIL FROM
404
+ # if any value returned, that will be used for ongoing processing
405
+ # otherwise the original value will be used
406
+ def on_mail_from_event(ctx, mail_from_data) end
407
+
408
+ # get each address send in RCPT TO
409
+ # if any value returned, that will be used for ongoing processing
410
+ # otherwise the original value will be used
411
+ def on_rcpt_to_event(ctx, rcpt_to_data) end
412
+
413
+ # event when beginning with message DATA
414
+ def on_message_data_start_event(ctx) end
415
+
416
+ # event while receiving message DATA
417
+ def on_message_data_receiving_event(ctx) end
418
+
419
+ # event when headers are received while receiving message DATA
420
+ def on_message_data_headers_event(ctx) end
421
+
422
+ # get each message after DATA <message>
423
+ def on_message_data_event(ctx) end
424
+
425
+ # event when process_line identifies an unknown command line
426
+ # allows to abort sessions for a series of unknown activities to
427
+ # prevent denial of service attacks etc.
428
+ def on_process_line_unknown_event(_ctx, _line)
429
+ # per default we encounter an error
430
+ raise Smtpd500Exception
431
+ end
432
+
433
+ protected
434
+
435
+ # Start the listeners for all hosts
436
+ def serve_service
437
+ raise 'Service was already started' unless stopped?
438
+
439
+ # set flag to signal shutdown by stop / shutdown command
440
+ @shutdown = false
441
+
442
+ # instantiate the service for all @addresses (ip_address:port)
443
+ @addresses.each do |address|
444
+ # break address into ip_address and port and serve service
445
+ ip_address = address.rpartition(':').first
446
+ port = address.rpartition(':').last
447
+ serve_service_on_ip_address_and_port(ip_address, port)
448
+ end
449
+ end
450
+
451
+ # Start the listener thread on single ip_address and port
452
+ def serve_service_on_ip_address_and_port(ip_address, port)
453
+ # log information
454
+ logger.info("Starting service on #{ip_address}:#{port}")
455
+ # 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?
457
+ # instantiate the service for ip_address and port
458
+ tcp_server = TCPServer.new(ip_address, port)
459
+ # append this server to the list of TCPServers
460
+ @tcp_servers << tcp_server
461
+
462
+ # run thread until shutdown
463
+ @tcp_server_threads << Thread.new do
464
+ begin
465
+ # always check for shutdown request
466
+ until shutdown?
467
+ # get new client and start additional thread
468
+ # to handle client process
469
+ client = tcp_server.accept
470
+ Thread.new(client) do |io|
471
+ # add to list of connections
472
+ @connections << Thread.current
473
+ # handle connection
474
+ begin
475
+ # initialize a session storage hash
476
+ Thread.current[:session] = {}
477
+ # process smtp service on io socket
478
+ io = serve_client(Thread.current[:session], io)
479
+ # save returned io value due to maybe
480
+ # established ssl io socket
481
+ rescue SmtpdStopConnectionException
482
+ # ignore this exception due to service shutdown
483
+ rescue StandardError => e
484
+ # log fatal error while handling connection
485
+ logger.fatal(e.backtrace.join("\n"))
486
+ ensure
487
+ begin
488
+ # always gracefully shutdown connection.
489
+ # if the io object was overriden by the
490
+ # result from serve_client() due to ssl
491
+ # io, the ssl + io socket will be closed
492
+ io.close
493
+ rescue StandardError
494
+ # ignore any exception from here
495
+ end
496
+ # remove closed session from connections
497
+ @connections_mutex.synchronize do
498
+ # drop this thread from connections
499
+ @connections.delete(Thread.current)
500
+ # drop this thread from processings
501
+ @processings.delete(Thread.current)
502
+ # signal mutex for next waiting thread
503
+ @connections_cv.signal
504
+ end
505
+ end
506
+ end
507
+ end
508
+ rescue SmtpdStopServiceException
509
+ # ignore this exception due to service shutdown
510
+ rescue StandardError => e
511
+ # log fatal error while starting new thread
512
+ logger.fatal(e.backtrace.join("\n"))
513
+ ensure
514
+ begin
515
+ # drop the service
516
+ tcp_server.close
517
+ # remove from list
518
+ @tcp_servers.delete(tcp_server)
519
+ # reset local var
520
+ tcp_server = nil
521
+ rescue StandardError
522
+ # ignore any error from here
523
+ end
524
+ if shutdown?
525
+ # wait for finishing opened connections
526
+ @connections_mutex.synchronize do
527
+ @connections_cv.wait(@connections_mutex) until @connections.empty?
528
+ end
529
+ else
530
+ # drop any open session immediately
531
+ @connections.each { |c| c.raise SmtpdStopConnectionException }
532
+ end
533
+ # remove this thread from list
534
+ @tcp_server_threads.delete(Thread.current)
535
+ end
536
+ end
537
+ end
538
+
539
+ # handle connection
540
+ def serve_client(session, io)
541
+ # handle connection
542
+ begin
543
+ begin
544
+ # ON CONNECTION
545
+ # 220 <domain> Service ready
546
+ # 421 <domain> Service not available, closing transmission channel
547
+ # Reset and initialize message
548
+ process_reset_session(session, true)
549
+
550
+ # get local address info
551
+ _, 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)
552
+ # get remote partner hostname and address
553
+ _, session[:ctx][:server][:remote_port], session[:ctx][:server][:remote_host], session[:ctx][:server][:remote_ip] = @do_dns_reverse_lookup ? io.peeraddr(:hostname) : io.peeraddr(:numeric)
554
+
555
+ # save connection date/time
556
+ session[:ctx][:server][:connected] = Time.now.utc
557
+
558
+ # build and save the local welcome and greeting response strings
559
+ session[:ctx][:server][:local_response] = "#{session[:ctx][:server][:local_host]} says welcome!"
560
+ session[:ctx][:server][:helo_response] = "#{session[:ctx][:server][:local_host]} at your service!"
561
+
562
+ # check if we want to let this remote station connect us
563
+ on_connect_event(session[:ctx])
564
+
565
+ # drop connection (respond 421) if too busy
566
+ raise 'Abort connection while too busy, exceeding max_connections!' if max_connections && connections > max_connections
567
+
568
+ # check active processings for new client
569
+ @connections_mutex.synchronize do
570
+ # when processings exceed maximum number of simultaneous allowed processings, then wait for next free slot
571
+ @connections_cv.wait(@connections_mutex) until processings < max_processings
572
+ end
573
+
574
+ # append this to list of processings
575
+ @processings << Thread.current
576
+
577
+ # reply local welcome message
578
+ output = +"220 #{session[:ctx][:server][:local_response].to_s.strip}\r\n"
579
+
580
+ # log and show to client
581
+ logger.debug(+'>>> ' << output)
582
+ io.print output unless io.closed?
583
+
584
+ # initialize \r\n for line_break, this is used for CRLF_ENSURE and CRLF_STRICT and mark as mutable
585
+ line_break = +"\r\n"
586
+
587
+ # initialize io_buffer for input data and mark as mutable
588
+ io_buffer = +''
589
+
590
+ # initialize io_buffer_line_lf index
591
+ io_buffer_line_lf = nil
592
+
593
+ # initialize timeout timestamp
594
+ timestamp_timeout = Time.now.to_i
595
+
596
+ # while input data handle communication
597
+ loop do
598
+ # test if STARTTLS sequence
599
+ if session[:cmd_sequence] == :CMD_STARTTLS
600
+ # start ssl tunnel
601
+ io = @tls.start(io)
602
+ # save enabled tls
603
+ session[:ctx][:server][:encrypted] = Time.now.utc
604
+ # set sequence back to HELO/EHLO
605
+ session[:cmd_sequence] = :CMD_HELO
606
+ # reset timeout timestamp
607
+ timestamp_timeout = Time.now.to_i
608
+ end
609
+
610
+ # read input data from Socket / SSLSocket into io_buffer
611
+ # by non-blocking action until \n is found
612
+ begin
613
+ unless io_buffer_line_lf
614
+ # check for timeout on IO
615
+ raise SmtpdIOTimeoutException if @io_cmd_timeout && Time.now.to_i - timestamp_timeout > @io_cmd_timeout
616
+ # read chunks of input data until line-feed
617
+ io_buffer << io.read_nonblock(@io_buffer_chunk_size)
618
+ # check for buffersize
619
+ raise SmtpdIOBufferOverrunException if @io_buffer_max_size && io_buffer.length > @io_buffer_max_size
620
+ # check for lf in current io_buffer
621
+ io_buffer_line_lf = io_buffer.index("\n")
622
+ end
623
+
624
+ # ignore exception when no input data is available yet
625
+ rescue IO::WaitReadable
626
+ # but wait a few moment to slow down system utilization
627
+ sleep 0.1
628
+ end
629
+
630
+ # check if io_buffer is filled and contains already a line-feed
631
+ while io_buffer_line_lf
632
+ # extract line (containing \n) from io_buffer and slice io_buffer
633
+ line = io_buffer.slice!(0, io_buffer_line_lf + 1)
634
+
635
+ # check for next line-feed already in io_buffer
636
+ io_buffer_line_lf = io_buffer.index("\n")
637
+
638
+ # process commands and handle special SmtpdExceptions
639
+ begin
640
+ # check for pipelining extension or violation
641
+ raise Smtpd500PipeliningException unless @pipelining_extension || !io_buffer_line_lf || (session[:cmd_sequence] == :CMD_DATA)
642
+
643
+ # handle input line based on @crlf_mode
644
+ case crlf_mode
645
+ when :CRLF_ENSURE
646
+ # remove any \r or \n occurence from line
647
+ line.delete!("\r\n")
648
+ # log line, verbosity based on log severity and command sequence
649
+ logger.debug(+'<<< ' << line << "\n") if session[:cmd_sequence] != :CMD_DATA
650
+
651
+ when :CRLF_LEAVE
652
+ # use input line_break for line_break
653
+ line_break = line[-2..-1] == "\r\n" ? "\r\n" : "\n"
654
+ # check to override session crlf info, only when CRLF_LEAVE is used and in DATA mode
655
+ session[:ctx][:message][:crlf] = line_break if session[:cmd_sequence] == :CMD_DATA
656
+ # remove any line_break from line
657
+ line.chomp!
658
+ # log line, verbosity based on log severity and command sequence
659
+ logger.debug(+'<<< ' << line.gsub("\r", '[\r]') << "\n") if session[:cmd_sequence] != :CMD_DATA
660
+
661
+ when :CRLF_STRICT
662
+ # check line ends up by \r\n
663
+ raise Smtpd500CrLfSequenceException unless line[-2..-1] == "\r\n"
664
+ # remove any line_break from line
665
+ line.chomp!
666
+ # check line for additional \r
667
+ raise Smtpd500Exception, 'Line contains additional CR chars!' if line.index("\r")
668
+ # log line, verbosity based on log severity and command sequence
669
+ logger.debug(+'<<< ' << line << "\n") if session[:cmd_sequence] != :CMD_DATA
670
+ end
671
+
672
+ # process line and mark output as mutable
673
+ output = +process_line(session, line, line_break)
674
+
675
+ # defined abort channel exception
676
+ rescue Smtpd421Exception => e
677
+ # just re-raise this exception and exit loop and communication
678
+ raise
679
+
680
+ # defined SmtpdException
681
+ rescue SmtpdException => e
682
+ # inc number of detected exceptions during this session
683
+ session[:ctx][:server][:exceptions] += 1
684
+ # log error info if logging
685
+ logger.error("#{e}")
686
+ # get the given smtp dialog result
687
+ output = +"#{e.smtpd_result}"
688
+
689
+ # Unknown general Exception during processing
690
+ rescue StandardError => e
691
+ # inc number of detected exceptions during this session
692
+ session[:ctx][:server][:exceptions] += 1
693
+ # log error info if logging
694
+ logger.error("#{e}")
695
+ # set default smtp server dialog error
696
+ output = +"#{Smtpd500Exception.new.smtpd_result}"
697
+ end
698
+
699
+ # check result
700
+ unless output.empty?
701
+ # log smtp dialog // message data is stored separate
702
+ logger.debug(+'>>> ' << output)
703
+ # append line feed
704
+ output << "\r\n"
705
+ # smtp dialog response
706
+ io.print(output) unless io.closed? || shutdown?
707
+ end
708
+
709
+ # reset timeout timestamp
710
+ timestamp_timeout = Time.now.to_i
711
+ end
712
+
713
+ # check for valid quit or broken communication
714
+ break if (session[:cmd_sequence] == :CMD_QUIT) || io.closed? || shutdown?
715
+ end
716
+ # graceful end of connection
717
+ output = +"221 Service closing transmission channel\r\n"
718
+ # smtp dialog response
719
+ io.print(output) unless io.closed?
720
+
721
+ # connection was simply closed / aborted by remote closing socket
722
+ rescue EOFError
723
+ # log info but only while debugging otherwise ignore message
724
+ logger.debug('EOFError - Connection lost due abort by client!')
725
+
726
+ rescue StandardError => e
727
+ # log error info if logging
728
+ logger.error("#{e}")
729
+ # power down connection
730
+ # ignore IOErrors when sending final smtp abort return code 421
731
+ begin
732
+ output = +"#{Smtpd421Exception.new.smtpd_result}\r\n"
733
+ # smtp dialog response
734
+ io.print(output) unless io.closed?
735
+ rescue StandardError
736
+ logger.debug('IOError - Can\'t send 421 abort code!')
737
+ end
738
+ end
739
+
740
+ ensure
741
+ # event for cleanup at end of communication
742
+ on_disconnect_event(session[:ctx])
743
+ end
744
+
745
+ # return socket handler, maybe replaced with ssl
746
+ return io
747
+ end
748
+
749
+ def process_line(session, line, line_break)
750
+ # check whether in auth challenge modes
751
+ if session[:cmd_sequence] == :CMD_AUTH_PLAIN_VALUES
752
+ # handle authentication
753
+ process_auth_plain(session, line)
754
+
755
+ # check whether in auth challenge modes
756
+ elsif session[:cmd_sequence] == :CMD_AUTH_LOGIN_USER
757
+ # handle authentication
758
+ process_auth_login_user(session, line)
759
+
760
+ # check whether in auth challenge modes
761
+ elsif session[:cmd_sequence] == :CMD_AUTH_LOGIN_PASS
762
+ # handle authentication
763
+ process_auth_login_pass(session, line)
764
+
765
+ # check whether in data or command mode
766
+ elsif session[:cmd_sequence] != :CMD_DATA
767
+
768
+ # Handle specific messages from the client
769
+ case line
770
+
771
+ when (/^(HELO|EHLO)(\s+.*)?$/i)
772
+ # HELO/EHLO
773
+ # 250 Requested mail action okay, completed
774
+ # 421 <domain> Service not available, closing transmission channel
775
+ # 500 Syntax error, command unrecognised
776
+ # 501 Syntax error in parameters or arguments
777
+ # 504 Command parameter not implemented
778
+ # 521 <domain> does not accept mail [rfc1846]
779
+ # ---------
780
+ # check valid command sequence
781
+ raise Smtpd503Exception if session[:cmd_sequence] != :CMD_HELO
782
+ # handle command
783
+ cmd_data = line.gsub(/^(HELO|EHLO)\ /i, '').strip
784
+ # call event to handle data
785
+ on_helo_event(session[:ctx], cmd_data)
786
+ # if no error raised, append to message hash
787
+ session[:ctx][:server][:helo] = cmd_data
788
+ # set sequence state as RSET
789
+ session[:cmd_sequence] = :CMD_RSET
790
+ # check whether to answer as HELO or EHLO
791
+ case line
792
+ when (/^EHLO/i)
793
+ # reply supported extensions
794
+ return "250-#{session[:ctx][:server][:helo_response].to_s.strip}\r\n" +
795
+ # respond with 8BITMIME extension
796
+ (@internationalization_extensions ? "250-8BITMIME\r\n" : '') +
797
+ # respond with SMTPUTF8 extension
798
+ (@internationalization_extensions ? "250-SMTPUTF8\r\n" : '') +
799
+ # respond with PIPELINING if enabled
800
+ (@pipelining_extension ? "250-PIPELINING\r\n" : '') +
801
+ # respond with AUTH extensions if enabled
802
+ (@auth_mode == :AUTH_FORBIDDEN ? '' : "250-AUTH LOGIN PLAIN\r\n") +
803
+ # respond with STARTTLS if available and not already enabled
804
+ (@encrypt_mode == :TLS_FORBIDDEN || encrypted?(session[:ctx]) ? '' : "250-STARTTLS\r\n") +
805
+ '250 OK'
806
+ else
807
+ # reply ok only
808
+ return "250 OK #{session[:ctx][:server][:helo_response].to_s.strip}".strip
809
+ end
810
+
811
+ when /^STARTTLS\s*$/i
812
+ # STARTTLS
813
+ # 220 Ready to start TLS
814
+ # 454 TLS not available
815
+ # 501 Syntax error (no parameters allowed)
816
+ # ---------
817
+ # check that encryption is allowed
818
+ raise Smtpd500Exception if @encrypt_mode == :TLS_FORBIDDEN
819
+ # check valid command sequence
820
+ raise Smtpd503Exception if session[:cmd_sequence] == :CMD_HELO
821
+ # check initialized TlsTransport object
822
+ raise Tls454Exception unless @tls
823
+ # check valid command sequence
824
+ raise Smtpd503Exception if encrypted?(session[:ctx])
825
+ # set sequence for next command input
826
+ session[:cmd_sequence] = :CMD_STARTTLS
827
+ # return with new service ready message
828
+ return '220 Ready to start TLS'
829
+
830
+ when (/^AUTH(\s+)((LOGIN|PLAIN)(\s+[A-Z0-9=]+)?|CRAM-MD5)\s*$/i)
831
+ # AUTH
832
+ # 235 Authentication Succeeded
833
+ # 432 A password transition is needed
834
+ # 454 Temporary authentication failure
835
+ # 500 Authentication Exchange line is too long
836
+ # 530 Authentication required
837
+ # 534 Authentication mechanism is too weak
838
+ # 535 Authentication credentials invalid
839
+ # 538 Encryption required for requested authentication mechanism
840
+ # ---------
841
+ # check that authentication is allowed
842
+ raise Smtpd500Exception if @auth_mode == :AUTH_FORBIDDEN
843
+ # check valid command sequence
844
+ raise Smtpd503Exception if session[:cmd_sequence] != :CMD_RSET
845
+ # check that encryption is enabled if necessary
846
+ raise Tls530Exception if @encrypt_mode == :TLS_REQUIRED && !encrypted?(session[:ctx])
847
+ # check that not already authenticated
848
+ raise Smtpd503Exception if authenticated?(session[:ctx])
849
+ # handle command line
850
+ auth_data = line.gsub(/^AUTH\ /i, '').strip.gsub(/\s+/, ' ').split(' ')
851
+ # handle auth command
852
+ case auth_data[0]
853
+
854
+ when (/PLAIN/i)
855
+ # check if only command was given
856
+ if auth_data.length == 1
857
+ # set sequence for next command input
858
+ session[:cmd_sequence] = :CMD_AUTH_PLAIN_VALUES
859
+ # response code include post ending with a space
860
+ return '334 '
861
+ else
862
+ # handle authentication with given auth_id and password
863
+ process_auth_plain(session, auth_data.length == 2 ? auth_data[1] : [])
864
+ end
865
+
866
+ when (/LOGIN/i)
867
+ # 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
880
+ end
881
+
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
888
+
889
+ else
890
+ # unknown auth method
891
+ raise Smtpd500Exception
892
+
893
+ end
894
+
895
+ when (/^NOOP\s*$/i)
896
+ # NOOP
897
+ # 250 Requested mail action okay, completed
898
+ # 421 <domain> Service not available, closing transmission channel
899
+ # 500 Syntax error, command unrecognised
900
+ return '250 OK'
901
+
902
+ when (/^RSET\s*$/i)
903
+ # RSET
904
+ # 250 Requested mail action okay, completed
905
+ # 421 <domain> Service not available, closing transmission channel
906
+ # 500 Syntax error, command unrecognised
907
+ # 501 Syntax error in parameters or arguments
908
+ # ---------
909
+ # check valid command sequence
910
+ raise Smtpd503Exception if session[:cmd_sequence] == :CMD_HELO
911
+ # check that encryption is enabled if necessary
912
+ raise Tls530Exception if @encrypt_mode == :TLS_REQUIRED && !encrypted?(session[:ctx])
913
+ # handle command
914
+ process_reset_session(session)
915
+ return '250 OK'
916
+
917
+ when (/^QUIT\s*$/i)
918
+ # QUIT
919
+ # 221 <domain> Service closing transmission channel
920
+ # 500 Syntax error, command unrecognised
921
+ session[:cmd_sequence] = :CMD_QUIT
922
+ return ''
923
+
924
+ when (/^MAIL FROM\:/i)
925
+ # MAIL
926
+ # 250 Requested mail action okay, completed
927
+ # 421 <domain> Service not available, closing transmission channel
928
+ # 451 Requested action aborted: local error in processing
929
+ # 452 Requested action not taken: insufficient system storage
930
+ # 500 Syntax error, command unrecognised
931
+ # 501 Syntax error in parameters or arguments
932
+ # 552 Requested mail action aborted: exceeded storage allocation
933
+ # ---------
934
+ # check valid command sequence
935
+ raise Smtpd503Exception if session[:cmd_sequence] != :CMD_RSET
936
+ # check that encryption is enabled if necessary
937
+ raise Tls530Exception if @encrypt_mode == :TLS_REQUIRED && !encrypted?(session[:ctx])
938
+ # check that authentication is enabled if necessary
939
+ raise Smtpd530Exception if @auth_mode == :AUTH_REQUIRED && !authenticated?(session[:ctx])
940
+ # handle command
941
+ cmd_data = line.gsub(/^MAIL FROM\:/i, '').strip
942
+ # check for BODY= parameter
943
+ case cmd_data
944
+ # test for explicit 7bit
945
+ when (/\sBODY=7BIT(\s|$)/i)
946
+ # raise exception if not supported
947
+ raise Smtpd501Exception unless @internationalization_extensions
948
+ # save info about encoding
949
+ session[:ctx][:envelope][:encoding_body] = '7bit'
950
+ # test for 8bit
951
+ when (/\sBODY=8BITMIME(\s|$)/i)
952
+ # raise exception if not supported
953
+ raise Smtpd501Exception unless @internationalization_extensions
954
+ # save info about encoding
955
+ session[:ctx][:envelope][:encoding_body] = '8bitmime'
956
+ # test for unknown encoding
957
+ when (/\sBODY=.*$/i)
958
+ # unknown BODY encoding
959
+ raise Smtpd501Exception
960
+ end
961
+ # check for SMTPUTF8 parameter
962
+ case cmd_data
963
+ # test for explicit 7bit
964
+ when (/\sSMTPUTF8(\s|$)/i)
965
+ # raise exception if not supported
966
+ raise Smtpd501Exception unless @internationalization_extensions
967
+ # save info about encoding
968
+ session[:ctx][:envelope][:encoding_utf8] = 'utf8'
969
+ end
970
+ # drop any BODY= and SMTPUTF8 content
971
+ cmd_data = cmd_data.gsub(/\sBODY=(7BIT|8BITMIME)/i, '').gsub(/\sSMTPUTF8/i, '').strip if @internationalization_extensions
972
+ # call event to handle data
973
+ return_value = on_mail_from_event(session[:ctx], cmd_data)
974
+ if return_value
975
+ # overwrite data with returned value
976
+ cmd_data = return_value
977
+ end
978
+ # if no error raised, append to message hash
979
+ session[:ctx][:envelope][:from] = cmd_data
980
+ # set sequence state
981
+ session[:cmd_sequence] = :CMD_MAIL
982
+ # reply ok
983
+ return '250 OK'
984
+
985
+ when (/^RCPT TO\:/i)
986
+ # RCPT
987
+ # 250 Requested mail action okay, completed
988
+ # 251 User not local; will forward to <forward-path>
989
+ # 421 <domain> Service not available, closing transmission channel
990
+ # 450 Requested mail action not taken: mailbox unavailable
991
+ # 451 Requested action aborted: local error in processing
992
+ # 452 Requested action not taken: insufficient system storage
993
+ # 500 Syntax error, command unrecognised
994
+ # 501 Syntax error in parameters or arguments
995
+ # 503 Bad sequence of commands
996
+ # 521 <domain> does not accept mail [rfc1846]
997
+ # 550 Requested action not taken: mailbox unavailable
998
+ # 551 User not local; please try <forward-path>
999
+ # 552 Requested mail action aborted: exceeded storage allocation
1000
+ # 553 Requested action not taken: mailbox name not allowed
1001
+ # ---------
1002
+ # check valid command sequence
1003
+ raise Smtpd503Exception unless [:CMD_MAIL, :CMD_RCPT].include?(session[:cmd_sequence])
1004
+ # check that encryption is enabled if necessary
1005
+ raise Tls530Exception if @encrypt_mode == :TLS_REQUIRED && !encrypted?(session[:ctx])
1006
+ # check that authentication is enabled if necessary
1007
+ raise Smtpd530Exception if @auth_mode == :AUTH_REQUIRED && !authenticated?(session[:ctx])
1008
+ # handle command
1009
+ cmd_data = line.gsub(/^RCPT TO\:/i, '').strip
1010
+ # call event to handle data
1011
+ return_value = on_rcpt_to_event(session[:ctx], cmd_data)
1012
+ if return_value
1013
+ # overwrite data with returned value
1014
+ cmd_data = return_value
1015
+ end
1016
+ # if no error raised, append to message hash
1017
+ session[:ctx][:envelope][:to] << cmd_data
1018
+ # set sequence state
1019
+ session[:cmd_sequence] = :CMD_RCPT
1020
+ # reply ok
1021
+ return '250 OK'
1022
+
1023
+ when (/^DATA\s*$/i)
1024
+ # DATA
1025
+ # 354 Start mail input; end with <CRLF>.<CRLF>
1026
+ # 250 Requested mail action okay, completed
1027
+ # 421 <domain> Service not available, closing transmission channel received data
1028
+ # 451 Requested action aborted: local error in processing
1029
+ # 452 Requested action not taken: insufficient system storage
1030
+ # 500 Syntax error, command unrecognised
1031
+ # 501 Syntax error in parameters or arguments
1032
+ # 503 Bad sequence of commands
1033
+ # 552 Requested mail action aborted: exceeded storage allocation
1034
+ # 554 Transaction failed
1035
+ # ---------
1036
+ # check valid command sequence
1037
+ raise Smtpd503Exception if session[:cmd_sequence] != :CMD_RCPT
1038
+ # check that encryption is enabled if necessary
1039
+ raise Tls530Exception if @encrypt_mode == :TLS_REQUIRED && !encrypted?(session[:ctx])
1040
+ # check that authentication is enabled if necessary
1041
+ raise Smtpd530Exception if @auth_mode == :AUTH_REQUIRED && !authenticated?(session[:ctx])
1042
+ # handle command
1043
+ # set sequence state
1044
+ session[:cmd_sequence] = :CMD_DATA
1045
+ # save incoming UTC time
1046
+ session[:ctx][:message][:received] = Time.now.utc
1047
+ # reply ok / proceed with message data
1048
+ return '354 Enter message, ending with "." on a line by itself'
1049
+
1050
+ else
1051
+ # If we somehow get to this point then
1052
+ # allow handling of unknown command line
1053
+ on_process_line_unknown_event(session[:ctx], line)
1054
+ end
1055
+
1056
+ else
1057
+ # If we are in date mode then ...
1058
+
1059
+ # call event to signal beginning of message data transfer
1060
+ on_message_data_start_event(session[:ctx]) unless session[:ctx][:message][:data][0]
1061
+
1062
+ # ... and the entire new message data (line) does NOT consists
1063
+ # solely of a period (.) on a line by itself then we are being
1064
+ # told to continue data mode and the command sequence state
1065
+ # will stay on :CMD_DATA
1066
+ unless line == '.'
1067
+ # remove a preceding first dot as defined by RFC 5321 (section-4.5.2)
1068
+ line.slice!(0) if line[0] == '.'
1069
+
1070
+ # if received an empty line the first time, that identifies
1071
+ # end of headers.
1072
+ unless session[:ctx][:message][:headers][0] || line[0]
1073
+ # change flag to do not signal this again for the
1074
+ # active message data transmission
1075
+ session[:ctx][:message][:headers] = true.to_s
1076
+ # call event to process received headers
1077
+ on_message_data_headers_event(session[:ctx])
1078
+ end
1079
+
1080
+ # we need to add the new message data (line) to the message
1081
+ # and make sure to add CR LF as defined by RFC
1082
+ session[:ctx][:message][:data] << line << line_break
1083
+
1084
+ # call event to inspect message data while recording line by line
1085
+ # e.g. abort while receiving too big incoming mail or
1086
+ # create a teergrube for spammers etc.
1087
+ on_message_data_receiving_event(session[:ctx])
1088
+
1089
+ # just return and stay on :CMD_DATA
1090
+ return ''
1091
+ end
1092
+
1093
+ # otherwise the entire new message data (line) consists
1094
+ # solely of a period on a line by itself then we are being
1095
+ # told to finish data mode
1096
+
1097
+ # remove last CR LF pair or single LF in buffer
1098
+ session[:ctx][:message][:data].chomp!
1099
+ # save delivered UTC time
1100
+ session[:ctx][:message][:delivered] = Time.now.utc
1101
+ # save bytesize of message data
1102
+ session[:ctx][:message][:bytesize] = session[:ctx][:message][:data].bytesize
1103
+ # call event to process message
1104
+ begin
1105
+ on_message_data_event(session[:ctx])
1106
+ return '250 Requested mail action okay, completed'
1107
+
1108
+ # test for SmtpdException
1109
+ rescue SmtpdException
1110
+ # just re-raise exception set by app
1111
+ raise
1112
+
1113
+ # test all other Exceptions
1114
+ rescue StandardError => e
1115
+ # send correct aborted message to smtp dialog
1116
+ raise Smtpd451Exception, e
1117
+
1118
+ ensure
1119
+ # always start with empty values after finishing incoming message
1120
+ # and rset command sequence
1121
+ process_reset_session(session)
1122
+ end
1123
+
1124
+ end
1125
+ end
1126
+
1127
+ # reset the context of current smtpd dialog
1128
+ def process_reset_session(session, connection_initialize = false)
1129
+ # set active command sequence info
1130
+ session[:cmd_sequence] = connection_initialize ? :CMD_HELO : :CMD_RSET
1131
+ # drop any auth challenge
1132
+ session[:auth_challenge] = {}
1133
+ # test existing of :ctx hash
1134
+ session[:ctx] || session[:ctx] = {}
1135
+ # reset server values (only on connection start)
1136
+ if connection_initialize
1137
+ # create or rebuild :ctx hash
1138
+ # and mark strings as mutable
1139
+ session[:ctx].merge!(
1140
+ server: {
1141
+ local_host: +'',
1142
+ local_ip: +'',
1143
+ local_port: +'',
1144
+ local_response: +'',
1145
+ remote_host: +'',
1146
+ remote_ip: +'',
1147
+ remote_port: +'',
1148
+ helo: +'',
1149
+ helo_response: +'',
1150
+ connected: +'',
1151
+ exceptions: 0,
1152
+ authorization_id: +'',
1153
+ authentication_id: +'',
1154
+ authenticated: +'',
1155
+ encrypted: +''
1156
+ }
1157
+ )
1158
+ end
1159
+ # reset envelope values
1160
+ session[:ctx].merge!(
1161
+ envelope: {
1162
+ from: +'',
1163
+ to: [],
1164
+ encoding_body: +'',
1165
+ encoding_utf8: +''
1166
+ }
1167
+ )
1168
+ # reset message data
1169
+ session[:ctx].merge!(
1170
+ message: {
1171
+ received: -1,
1172
+ delivered: -1,
1173
+ bytesize: -1,
1174
+ headers: +'',
1175
+ crlf: +"\r\n",
1176
+ data: +''
1177
+ }
1178
+ )
1179
+ end
1180
+
1181
+ # handle plain authentification
1182
+ def process_auth_plain(session, encoded_auth_response)
1183
+ begin
1184
+ # extract auth id (and password)
1185
+ auth_values = Base64.decode64(encoded_auth_response).split("\x00")
1186
+ # check for valid credentials parameters
1187
+ raise Smtpd500Exception unless auth_values.length == 3
1188
+ # call event function to test credentials
1189
+ return_value = on_auth_event(session[:ctx], auth_values[0], auth_values[1], auth_values[2])
1190
+ if return_value
1191
+ # overwrite data with returned value as authorization id
1192
+ auth_values[0] = return_value
1193
+ end
1194
+ # save authentication information to ctx
1195
+ session[:ctx][:server][:authorization_id] = auth_values[0].to_s.empty? ? auth_values[1] : auth_values[0]
1196
+ session[:ctx][:server][:authentication_id] = auth_values[1]
1197
+ session[:ctx][:server][:authenticated] = Time.now.utc
1198
+ # response code
1199
+ return '235 OK'
1200
+
1201
+ ensure
1202
+ # whatever happens in this check, reset next sequence
1203
+ session[:cmd_sequence] = :CMD_RSET
1204
+ end
1205
+ end
1206
+
1207
+ def process_auth_login_user(session, encoded_auth_response)
1208
+ # save challenged auth_id
1209
+ session[:auth_challenge][:authorization_id] = ''
1210
+ session[:auth_challenge][:authentication_id] = Base64.decode64(encoded_auth_response)
1211
+ # set sequence for next command input
1212
+ session[:cmd_sequence] = :CMD_AUTH_LOGIN_PASS
1213
+ # response code with request for Password
1214
+ return '334 ' + Base64.strict_encode64('Password:')
1215
+ end
1216
+
1217
+ def process_auth_login_pass(session, encoded_auth_response)
1218
+ begin
1219
+ # extract auth id (and password)
1220
+ auth_values = [
1221
+ session[:auth_challenge][:authorization_id],
1222
+ session[:auth_challenge][:authentication_id],
1223
+ Base64.decode64(encoded_auth_response)
1224
+ ]
1225
+ # check for valid credentials
1226
+ return_value = on_auth_event(session[:ctx], auth_values[0], auth_values[1], auth_values[2])
1227
+ if return_value
1228
+ # overwrite data with returned value as authorization id
1229
+ auth_values[0] = return_value
1230
+ end
1231
+ # save authentication information to ctx
1232
+ session[:ctx][:server][:authorization_id] = auth_values[0].to_s.empty? ? auth_values[1] : auth_values[0]
1233
+ session[:ctx][:server][:authentication_id] = auth_values[1]
1234
+ session[:ctx][:server][:authenticated] = Time.now.utc
1235
+ # response code
1236
+ return '235 OK'
1237
+
1238
+ ensure
1239
+ # whatever happens in this check, reset next sequence
1240
+ session[:cmd_sequence] = :CMD_RSET
1241
+ # and reset auth_challenge
1242
+ session[:auth_challenge] = {}
1243
+ end
1244
+ end
1245
+
1246
+ end
1247
+
1248
+ end