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,301 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# A small and highly customizable ruby SMTP-Server.
|
4
|
+
module MidiSmtpServer
|
5
|
+
|
6
|
+
private
|
7
|
+
|
8
|
+
# special internal exception to signal timeout
|
9
|
+
# while waiting for incoming data line
|
10
|
+
class SmtpdIOTimeoutException < RuntimeError
|
11
|
+
end
|
12
|
+
|
13
|
+
# special internal exception to signal buffer size exceedance
|
14
|
+
# while waiting for incoming data line
|
15
|
+
class SmtpdIOBufferOverrunException < RuntimeError
|
16
|
+
end
|
17
|
+
|
18
|
+
# special internal exception to signal service stop
|
19
|
+
# without creating a fatal error message
|
20
|
+
class SmtpdStopServiceException < RuntimeError
|
21
|
+
end
|
22
|
+
|
23
|
+
# special internal exception to signal connection stop while
|
24
|
+
# server shutdown without creating a fatal error message
|
25
|
+
class SmtpdStopConnectionException < RuntimeError
|
26
|
+
end
|
27
|
+
|
28
|
+
public
|
29
|
+
|
30
|
+
# generic smtp server exception class
|
31
|
+
class SmtpdException < RuntimeError
|
32
|
+
|
33
|
+
attr_reader :smtpd_return_code
|
34
|
+
attr_reader :smtpd_return_text
|
35
|
+
|
36
|
+
def initialize(msg, smtpd_return_code, smtpd_return_text)
|
37
|
+
# save reference for smtp dialog
|
38
|
+
@smtpd_return_code = smtpd_return_code
|
39
|
+
@smtpd_return_text = smtpd_return_text
|
40
|
+
# call inherited constructor
|
41
|
+
super msg
|
42
|
+
end
|
43
|
+
|
44
|
+
def smtpd_result
|
45
|
+
return "#{@smtpd_return_code} #{@smtpd_return_text}"
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
# 421 <domain> Service not available, closing transmission channel
|
51
|
+
class Smtpd421Exception < SmtpdException
|
52
|
+
|
53
|
+
def initialize(msg = nil)
|
54
|
+
# call inherited constructor
|
55
|
+
super msg, 421, 'Service too busy or not available, closing transmission channel'
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
# 450 Requested mail action not taken: mailbox unavailable
|
61
|
+
# e.g. mailbox busy
|
62
|
+
class Smtpd450Exception < SmtpdException
|
63
|
+
|
64
|
+
def initialize(msg = nil)
|
65
|
+
# call inherited constructor
|
66
|
+
super msg, 450, 'Requested mail action not taken: mailbox unavailable'
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
# 451 Requested action aborted: local error in processing
|
72
|
+
class Smtpd451Exception < SmtpdException
|
73
|
+
|
74
|
+
def initialize(msg = nil)
|
75
|
+
# call inherited constructor
|
76
|
+
super msg, 451, 'Requested action aborted: local error in processing'
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
# 452 Requested action not taken: insufficient system storage
|
82
|
+
class Smtpd452Exception < SmtpdException
|
83
|
+
|
84
|
+
def initialize(msg = nil)
|
85
|
+
# call inherited constructor
|
86
|
+
super msg, 452, 'Requested action not taken: insufficient system storage'
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
# 500 Syntax error, command unrecognised or error in parameters or arguments.
|
92
|
+
# This may include errors such as command line too long
|
93
|
+
class Smtpd500Exception < SmtpdException
|
94
|
+
|
95
|
+
def initialize(msg = nil)
|
96
|
+
# call inherited constructor
|
97
|
+
super msg, 500, 'Syntax error, command unrecognised or error in parameters or arguments'
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
|
102
|
+
# 501 Syntax error in parameters or arguments
|
103
|
+
class Smtpd501Exception < SmtpdException
|
104
|
+
|
105
|
+
def initialize(msg = nil)
|
106
|
+
# call inherited constructor
|
107
|
+
super msg, 501, 'Syntax error in parameters or arguments'
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
|
112
|
+
# 502 Command not implemented
|
113
|
+
class Smtpd502Exception < SmtpdException
|
114
|
+
|
115
|
+
def initialize(msg = nil)
|
116
|
+
# call inherited constructor
|
117
|
+
super msg, 502, 'Command not implemented'
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
121
|
+
|
122
|
+
# 503 Bad sequence of commands
|
123
|
+
class Smtpd503Exception < SmtpdException
|
124
|
+
|
125
|
+
def initialize(msg = nil)
|
126
|
+
# call inherited constructor
|
127
|
+
super msg, 503, 'Bad sequence of commands'
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
131
|
+
|
132
|
+
# 504 Command parameter not implemented
|
133
|
+
class Smtpd504Exception < SmtpdException
|
134
|
+
|
135
|
+
def initialize(msg = nil)
|
136
|
+
# call inherited constructor
|
137
|
+
super msg, 504, 'Command parameter not implemented'
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|
141
|
+
|
142
|
+
# 521 <domain> does not accept mail [rfc1846]
|
143
|
+
class Smtpd521Exception < SmtpdException
|
144
|
+
|
145
|
+
def initialize(msg = nil)
|
146
|
+
# call inherited constructor
|
147
|
+
super msg, 521, 'Service does not accept mail'
|
148
|
+
end
|
149
|
+
|
150
|
+
end
|
151
|
+
|
152
|
+
# 550 Requested action not taken: mailbox unavailable
|
153
|
+
# e.g. mailbox not found, no access
|
154
|
+
class Smtpd550Exception < SmtpdException
|
155
|
+
|
156
|
+
def initialize(msg = nil)
|
157
|
+
# call inherited constructor
|
158
|
+
super msg, 550, 'Requested action not taken: mailbox unavailable'
|
159
|
+
end
|
160
|
+
|
161
|
+
end
|
162
|
+
|
163
|
+
# 552 Requested mail action aborted: exceeded storage allocation
|
164
|
+
class Smtpd552Exception < SmtpdException
|
165
|
+
|
166
|
+
def initialize(msg = nil)
|
167
|
+
# call inherited constructor
|
168
|
+
super msg, 552, 'Requested mail action aborted: exceeded storage allocation'
|
169
|
+
end
|
170
|
+
|
171
|
+
end
|
172
|
+
|
173
|
+
# 553 Requested action not taken: mailbox name not allowed
|
174
|
+
class Smtpd553Exception < SmtpdException
|
175
|
+
|
176
|
+
def initialize(msg = nil)
|
177
|
+
# call inherited constructor
|
178
|
+
super msg, 553, 'Requested action not taken: mailbox name not allowed'
|
179
|
+
end
|
180
|
+
|
181
|
+
end
|
182
|
+
|
183
|
+
# 554 Transaction failed
|
184
|
+
class Smtpd554Exception < SmtpdException
|
185
|
+
|
186
|
+
def initialize(msg = nil)
|
187
|
+
# call inherited constructor
|
188
|
+
super msg, 554, 'Transaction failed'
|
189
|
+
end
|
190
|
+
|
191
|
+
end
|
192
|
+
|
193
|
+
# Status when using authentication
|
194
|
+
|
195
|
+
# 432 Password transition is needed
|
196
|
+
class Smtpd432Exception < SmtpdException
|
197
|
+
|
198
|
+
def initialize(msg = nil)
|
199
|
+
# call inherited constructor
|
200
|
+
super msg, 432, 'Password transition is needed'
|
201
|
+
end
|
202
|
+
|
203
|
+
end
|
204
|
+
|
205
|
+
# 454 Temporary authentication failure
|
206
|
+
class Smtpd454Exception < SmtpdException
|
207
|
+
|
208
|
+
def initialize(msg = nil)
|
209
|
+
# call inherited constructor
|
210
|
+
super msg, 454, 'Temporary authentication failure'
|
211
|
+
end
|
212
|
+
|
213
|
+
end
|
214
|
+
|
215
|
+
# 530 Authentication required
|
216
|
+
class Smtpd530Exception < SmtpdException
|
217
|
+
|
218
|
+
def initialize(msg = nil)
|
219
|
+
# call inherited constructor
|
220
|
+
super msg, 530, 'Authentication required'
|
221
|
+
end
|
222
|
+
|
223
|
+
end
|
224
|
+
|
225
|
+
# 534 Authentication mechanism is too weak
|
226
|
+
class Smtpd534Exception < SmtpdException
|
227
|
+
|
228
|
+
def initialize(msg = nil)
|
229
|
+
# call inherited constructor
|
230
|
+
super msg, 534, 'Authentication mechanism is too weak'
|
231
|
+
end
|
232
|
+
|
233
|
+
end
|
234
|
+
|
235
|
+
# 535 Authentication credentials invalid
|
236
|
+
class Smtpd535Exception < SmtpdException
|
237
|
+
|
238
|
+
def initialize(msg = nil)
|
239
|
+
# call inherited constructor
|
240
|
+
super msg, 535, 'Authentication credentials invalid'
|
241
|
+
end
|
242
|
+
|
243
|
+
end
|
244
|
+
|
245
|
+
# 538 Encryption required for requested authentication mechanism
|
246
|
+
class Smtpd538Exception < SmtpdException
|
247
|
+
|
248
|
+
def initialize(msg = nil)
|
249
|
+
# call inherited constructor
|
250
|
+
super msg, 538, 'Encryption required for requested authentication mechanism'
|
251
|
+
end
|
252
|
+
|
253
|
+
end
|
254
|
+
|
255
|
+
# Status when using encryption
|
256
|
+
|
257
|
+
# 454 TLS not available
|
258
|
+
class Tls454Exception < SmtpdException
|
259
|
+
|
260
|
+
def initialize(msg = nil)
|
261
|
+
# call inherited constructor
|
262
|
+
super msg, 454, 'TLS not available'
|
263
|
+
end
|
264
|
+
|
265
|
+
end
|
266
|
+
|
267
|
+
# 530 Encryption required
|
268
|
+
class Tls530Exception < SmtpdException
|
269
|
+
|
270
|
+
def initialize(msg = nil)
|
271
|
+
# call inherited constructor
|
272
|
+
super msg, 530, 'Encryption required, must issue STARTTLS command first'
|
273
|
+
end
|
274
|
+
|
275
|
+
end
|
276
|
+
|
277
|
+
# Status when disabled PIPELINING
|
278
|
+
|
279
|
+
# 500 Bad input, no PIPELINING
|
280
|
+
class Smtpd500PipeliningException < SmtpdException
|
281
|
+
|
282
|
+
def initialize(msg = nil)
|
283
|
+
# call inherited constructor
|
284
|
+
super msg, 500, 'Bad input, PIPELINING is not allowed'
|
285
|
+
end
|
286
|
+
|
287
|
+
end
|
288
|
+
|
289
|
+
# Status when expeting CRLF sequence as line breaks (RFC(2)822)
|
290
|
+
|
291
|
+
# 500 Bad input, missing CRLF line termination
|
292
|
+
class Smtpd500CrLfSequenceException < SmtpdException
|
293
|
+
|
294
|
+
def initialize(msg = nil)
|
295
|
+
# call inherited constructor
|
296
|
+
super msg, 500, 'Bad input, Lines must be terminated by CRLF sequence'
|
297
|
+
end
|
298
|
+
|
299
|
+
end
|
300
|
+
|
301
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'resolv'
|
4
|
+
|
5
|
+
# A small and highly customizable ruby SMTP-Server.
|
6
|
+
module MidiSmtpServer
|
7
|
+
|
8
|
+
# Encryption modes
|
9
|
+
ENCRYPT_MODES = [:TLS_FORBIDDEN, :TLS_OPTIONAL, :TLS_REQUIRED].freeze
|
10
|
+
DEFAULT_ENCRYPT_MODE = :TLS_FORBIDDEN
|
11
|
+
|
12
|
+
# Encryption ciphers and methods
|
13
|
+
# check https://www.owasp.org/index.php/TLS_Cipher_String_Cheat_Sheet
|
14
|
+
TLS_CIPHERS_ADVANCED_PLUS = 'DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256'
|
15
|
+
TLS_CIPHERS_ADVANCED = (TLS_CIPHERS_ADVANCED_PLUS + ':DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256')
|
16
|
+
TLS_CIPHERS_BROAD_COMP = (TLS_CIPHERS_ADVANCED + ':ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA')
|
17
|
+
TLS_CIPHERS_WIDEST_COMP = (TLS_CIPHERS_ADVANCED + ':ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA')
|
18
|
+
TLS_CIPHERS_LEGACY = (TLS_CIPHERS_ADVANCED + ':ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA')
|
19
|
+
TLS_METHODS_ADVANCED = 'TLSv1_2'
|
20
|
+
TLS_METHODS_LEGACY = 'TLSv1_1'
|
21
|
+
|
22
|
+
# class for TlsTransport
|
23
|
+
class TlsTransport
|
24
|
+
|
25
|
+
def initialize(cert_path, key_path, ciphers, methods, cert_cn, cert_san, logger)
|
26
|
+
# if need to debug something while working with openssl
|
27
|
+
# OpenSSL::debug = true
|
28
|
+
|
29
|
+
# save references
|
30
|
+
@logger = logger
|
31
|
+
@cert_path = cert_path.to_s == '' ? nil : cert_path.strip
|
32
|
+
@key_path = key_path.to_s == '' ? nil : key_path.strip
|
33
|
+
# create SSL context
|
34
|
+
@ctx = OpenSSL::SSL::SSLContext.new
|
35
|
+
@ctx.ciphers = ciphers.to_s == '' ? TLS_CIPHERS_ADVANCED_PLUS : ciphers
|
36
|
+
@ctx.ssl_version = methods.to_s == '' ? TLS_METHODS_ADVANCED : methods
|
37
|
+
# check cert_path and key_path
|
38
|
+
if !@cert_path.nil? || !@key_path.nil?
|
39
|
+
# if any is set, test the pathes
|
40
|
+
raise "File \”#{@cert_path}\" does not exist or is not a regular file. Could not load certificate." unless File.file?(@cert_path.to_s)
|
41
|
+
raise "File \”#{@key_path}\" does not exist or is not a regular file. Could not load private key." unless File.file?(@key_path.to_s)
|
42
|
+
# try to load certificate and key
|
43
|
+
@ctx.cert = OpenSSL::X509::Certificate.new(File.open(@cert_path.to_s))
|
44
|
+
@ctx.key = OpenSSL::PKey::RSA.new(File.open(@key_path.to_s))
|
45
|
+
else
|
46
|
+
# if none cert_path was set, create a self signed test certificate
|
47
|
+
# and try to setup common subject and subject alt name(s) for cert
|
48
|
+
@cert_cn = cert_cn.to_s.strip
|
49
|
+
@cert_san = ([@cert_cn] + (cert_san.nil? ? [] : cert_san)).uniq
|
50
|
+
# as well as IP Address extension entries for subject alt name(s) if ipv4 or ipv6 address
|
51
|
+
@cert_san_ip = []
|
52
|
+
@cert_san.each { |san| @cert_san_ip << san if san =~ Resolv::IPv4::Regex || san =~ Resolv::IPv6::Regex }
|
53
|
+
# initialize self certificate and key
|
54
|
+
logger.debug("SSL: using self generated test certificate! CN=#{@cert_cn} SAN=[#{@cert_san.join(',')}]")
|
55
|
+
@ctx.key = OpenSSL::PKey::RSA.new 4096
|
56
|
+
@ctx.cert = OpenSSL::X509::Certificate.new
|
57
|
+
@ctx.cert.version = 2
|
58
|
+
@ctx.cert.serial = 1
|
59
|
+
# the subject and the issuer are identical only for test certificate
|
60
|
+
@ctx.cert.subject = OpenSSL::X509::Name.new [['CN', @cert_cn]]
|
61
|
+
@ctx.cert.issuer = @ctx.cert.subject
|
62
|
+
@ctx.cert.public_key = @ctx.key
|
63
|
+
# valid for 90 days
|
64
|
+
@ctx.cert.not_before = Time.now
|
65
|
+
@ctx.cert.not_after = Time.now + 60 * 60 * 24 * 90
|
66
|
+
# setup some cert extensions
|
67
|
+
@ef = OpenSSL::X509::ExtensionFactory.new
|
68
|
+
@ef.subject_certificate = @ctx.cert
|
69
|
+
@ef.issuer_certificate = @ctx.cert
|
70
|
+
@ctx.cert.add_extension(@ef.create_extension('basicConstraints', 'CA:FALSE', false))
|
71
|
+
@ctx.cert.add_extension(@ef.create_extension('keyUsage', 'digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment', false))
|
72
|
+
@ctx.cert.add_extension(@ef.create_extension('subjectAltName', (@cert_san.map { |san| "DNS:#{san}" } + @cert_san_ip.map { |ip| "IP:#{ip}" }).join(', '), false))
|
73
|
+
@ctx.cert.sign @ctx.key, OpenSSL::Digest::SHA256.new
|
74
|
+
logger.debug("SSL: generated test certificate\r\n#{@ctx.cert.to_text}")
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# start ssl connection over existing tcpserver socket
|
79
|
+
def start(io)
|
80
|
+
# start SSL negotiation
|
81
|
+
ssl = OpenSSL::SSL::SSLSocket.new(io, @ctx)
|
82
|
+
# connect to server socket
|
83
|
+
ssl.accept
|
84
|
+
# make sure to close also the underlying io
|
85
|
+
ssl.sync_close = true
|
86
|
+
# return as new io socket
|
87
|
+
return ssl
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# A small and highly customizable ruby SMTP-Server.
|
4
|
+
module MidiSmtpServer
|
5
|
+
|
6
|
+
module VERSION
|
7
|
+
|
8
|
+
MAJOR = 2
|
9
|
+
MINOR = 3
|
10
|
+
TINY = 3
|
11
|
+
|
12
|
+
STRING = [MAJOR, MINOR, TINY].compact.join('.')
|
13
|
+
|
14
|
+
DATE = '2022-02-12'
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|