midi-smtp-server 2.3.2

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of midi-smtp-server might be problematic. Click here for more details.

@@ -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