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.
- checksums.yaml +7 -0
- data/MIT-LICENSE.txt +21 -0
- data/README.md +1109 -0
- data/lib/midi-smtp-server/exceptions.rb +301 -0
- data/lib/midi-smtp-server/tls-transport.rb +92 -0
- data/lib/midi-smtp-server/version.rb +18 -0
- data/lib/midi-smtp-server.rb +1248 -0
- metadata +55 -0
@@ -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
|