groat-smtpd 0.1.0
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.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/LICENSE +202 -0
- data/Rakefile +2 -0
- data/groat-smtpd.gemspec +30 -0
- data/lib/groat/smtpd.rb +22 -0
- data/lib/groat/smtpd/LICENSE +202 -0
- data/lib/groat/smtpd/base.rb +159 -0
- data/lib/groat/smtpd/extensions/authentication.rb +164 -0
- data/lib/groat/smtpd/extensions/binarymime.rb +48 -0
- data/lib/groat/smtpd/extensions/chunking.rb +57 -0
- data/lib/groat/smtpd/extensions/eightbitmime.rb +47 -0
- data/lib/groat/smtpd/extensions/help.rb +42 -0
- data/lib/groat/smtpd/extensions/mechanism-login.rb +74 -0
- data/lib/groat/smtpd/extensions/no-soliciting.rb +41 -0
- data/lib/groat/smtpd/extensions/onex.rb +37 -0
- data/lib/groat/smtpd/extensions/pipelining.rb +59 -0
- data/lib/groat/smtpd/extensions/size.rb +46 -0
- data/lib/groat/smtpd/extensions/starttls.rb +69 -0
- data/lib/groat/smtpd/extensions/verb.rb +37 -0
- data/lib/groat/smtpd/server.rb +39 -0
- data/lib/groat/smtpd/smtp.rb +277 -0
- data/lib/groat/smtpd/smtpsyntax.rb +377 -0
- data/lib/groat/smtpd/version.rb +25 -0
- metadata +112 -0
@@ -0,0 +1,159 @@
|
|
1
|
+
# vim: set sw=2 sts=2 ts=2 et syntax=ruby: #
|
2
|
+
=begin license
|
3
|
+
Copyright 2011 Novell, Inc.
|
4
|
+
|
5
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
you may not use this file except in compliance with the License.
|
7
|
+
You may obtain a copy of the License at
|
8
|
+
|
9
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
|
11
|
+
Unless required by applicable law or agreed to in writing, software
|
12
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
See the License for the specific language governing permissions and
|
15
|
+
limitations under the License.
|
16
|
+
|
17
|
+
Author(s):
|
18
|
+
Peter Bowen <pzbowen@gmail.com> Ottawa, Ontario, Canada
|
19
|
+
=end
|
20
|
+
|
21
|
+
require 'rubygems'
|
22
|
+
require 'hooks'
|
23
|
+
require 'timeout'
|
24
|
+
|
25
|
+
module Groat
|
26
|
+
module SMTPD
|
27
|
+
class Response < Exception
|
28
|
+
def initialize(args = {})
|
29
|
+
@message = args[:message] || "Unknown"
|
30
|
+
@terminate = args[:terminate] || false
|
31
|
+
end
|
32
|
+
|
33
|
+
def terminate?
|
34
|
+
@terminate
|
35
|
+
end
|
36
|
+
|
37
|
+
def reply_text
|
38
|
+
if @message.is_a? Array
|
39
|
+
@message.join("\r\n") + "\r\n"
|
40
|
+
else
|
41
|
+
@message.to_s + "\r\n"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class Base
|
47
|
+
include Hooks
|
48
|
+
|
49
|
+
@@numinstances = 0
|
50
|
+
|
51
|
+
def initialize
|
52
|
+
@response_class = Response
|
53
|
+
@instanceid = @@numinstances = @@numinstances + 1
|
54
|
+
@s = nil
|
55
|
+
@remote_address = nil
|
56
|
+
@remote_port = nil
|
57
|
+
reset_connection
|
58
|
+
end
|
59
|
+
|
60
|
+
def reply(args)
|
61
|
+
raise @response_class, args
|
62
|
+
end
|
63
|
+
|
64
|
+
def run(method, *args, &block)
|
65
|
+
if block_given?
|
66
|
+
yield
|
67
|
+
else
|
68
|
+
send method, *args
|
69
|
+
end
|
70
|
+
rescue Response => r
|
71
|
+
toclient r.reply_text
|
72
|
+
not r.terminate?
|
73
|
+
end
|
74
|
+
|
75
|
+
def process_line(line)
|
76
|
+
end
|
77
|
+
|
78
|
+
def send_greeting
|
79
|
+
end
|
80
|
+
|
81
|
+
def service_shutdown
|
82
|
+
end
|
83
|
+
|
84
|
+
def reset_connection
|
85
|
+
end
|
86
|
+
|
87
|
+
# Nothing in the base implements security
|
88
|
+
def secure?
|
89
|
+
false
|
90
|
+
end
|
91
|
+
|
92
|
+
def set_socket(io)
|
93
|
+
@s = io
|
94
|
+
x, @remote_port, x, @remote_address = io.peeraddr
|
95
|
+
end
|
96
|
+
|
97
|
+
def serve(io)
|
98
|
+
set_socket io
|
99
|
+
reset_connection
|
100
|
+
run :send_greeting
|
101
|
+
continue = true
|
102
|
+
while continue do
|
103
|
+
line = fromclient
|
104
|
+
break if line.nil?
|
105
|
+
continue = process_line line
|
106
|
+
end
|
107
|
+
rescue TimeoutError
|
108
|
+
run :service_shutdown
|
109
|
+
end
|
110
|
+
|
111
|
+
def sockop_timeout(method, arg, wait = 30)
|
112
|
+
begin
|
113
|
+
timeout(wait){
|
114
|
+
return @s.__send__(method, arg)
|
115
|
+
}
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def getline
|
120
|
+
sockop_timeout(:gets, "\n")
|
121
|
+
end
|
122
|
+
|
123
|
+
def getdata(size)
|
124
|
+
sockop_timeout(:read, size)
|
125
|
+
end
|
126
|
+
|
127
|
+
def fromclient
|
128
|
+
line = getline
|
129
|
+
log_line(:in, line)
|
130
|
+
end
|
131
|
+
|
132
|
+
def log_line(direction, line)
|
133
|
+
if direction == :in
|
134
|
+
if line.nil?
|
135
|
+
puts "#{@instanceid}>/nil"
|
136
|
+
else
|
137
|
+
puts "#{@instanceid}>>" + line
|
138
|
+
end
|
139
|
+
else
|
140
|
+
if line.nil?
|
141
|
+
puts "#{@instanceid}</nil"
|
142
|
+
else
|
143
|
+
puts "#{@instanceid}<<" + line
|
144
|
+
end
|
145
|
+
end
|
146
|
+
line
|
147
|
+
end
|
148
|
+
|
149
|
+
def toclient(msg)
|
150
|
+
log_line(:out, msg)
|
151
|
+
@s.print(msg)
|
152
|
+
end
|
153
|
+
|
154
|
+
def clientdata?
|
155
|
+
IO.select([@s], nil, nil, 0.1)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,164 @@
|
|
1
|
+
# vim: set sw=2 sts=2 ts=2 et syntax=ruby: #
|
2
|
+
=begin license
|
3
|
+
Copyright 2011 Novell, Inc.
|
4
|
+
|
5
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
you may not use this file except in compliance with the License.
|
7
|
+
You may obtain a copy of the License at
|
8
|
+
|
9
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
|
11
|
+
Unless required by applicable law or agreed to in writing, software
|
12
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
See the License for the specific language governing permissions and
|
15
|
+
limitations under the License.
|
16
|
+
|
17
|
+
Author(s):
|
18
|
+
Peter Bowen <pzbowen@gmail.com> Ottawa, Ontario, Canada
|
19
|
+
=end
|
20
|
+
|
21
|
+
require 'base64'
|
22
|
+
|
23
|
+
module Groat
|
24
|
+
module SMTPD
|
25
|
+
module Extensions
|
26
|
+
module Authentication
|
27
|
+
module ClassMethods
|
28
|
+
def auth_mechanism(name, method, condition = nil)
|
29
|
+
sym = name.to_s.upcase.intern
|
30
|
+
auth_mechanisms[sym] = {} unless auth_mechanisms.has_key? sym
|
31
|
+
auth_mechanisms[sym] = {:method => method, :condition => condition}
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def reset_connection
|
36
|
+
@authenticated = false
|
37
|
+
super
|
38
|
+
end
|
39
|
+
|
40
|
+
def reset_buffers
|
41
|
+
@mail_auth = nil
|
42
|
+
super
|
43
|
+
end
|
44
|
+
|
45
|
+
def authenticated?
|
46
|
+
@authenticated
|
47
|
+
end
|
48
|
+
|
49
|
+
def auth_params
|
50
|
+
list = []
|
51
|
+
self.class.auth_mechanisms.each do |k, v|
|
52
|
+
valid = false
|
53
|
+
|
54
|
+
if v[:condition].nil?
|
55
|
+
valid = true
|
56
|
+
else
|
57
|
+
valid = send v[:condition]
|
58
|
+
end
|
59
|
+
|
60
|
+
list << k if valid
|
61
|
+
end
|
62
|
+
list
|
63
|
+
end
|
64
|
+
|
65
|
+
def show_auth_keyword?
|
66
|
+
not authenticated?
|
67
|
+
end
|
68
|
+
|
69
|
+
def response_auth_ok(args = {})
|
70
|
+
defaults = {:code => 235, :message => "Authentication Succeeded"}
|
71
|
+
reply defaults.merge(args)
|
72
|
+
end
|
73
|
+
|
74
|
+
def response_auth_temp_fail(args = {})
|
75
|
+
defaults = {:code => 454, :message => "Temporary Failure"}
|
76
|
+
reply defaults.merge(args)
|
77
|
+
end
|
78
|
+
|
79
|
+
def response_auth_required(args = {})
|
80
|
+
defaults = {:code => 530, :message => "Authentication required"}
|
81
|
+
reply defaults.merge(args)
|
82
|
+
end
|
83
|
+
|
84
|
+
def response_auth_failure(args = {})
|
85
|
+
defaults = {:code => 535, :message => "Credentials Invalid"}
|
86
|
+
reply defaults.merge(args)
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.included mod
|
90
|
+
puts "Included RFC 4954: Authentication"
|
91
|
+
mod.extend ClassMethods
|
92
|
+
mod.inheritable_attr(:auth_mechanisms)
|
93
|
+
mod.auth_mechanisms = {}
|
94
|
+
mod.ehlo_keyword :auth, :auth_params, :show_auth_keyword?
|
95
|
+
mod.verb :auth, :smtp_verb_auth
|
96
|
+
mod.mail_param :auth, :mail_param_auth
|
97
|
+
mod.auth_mechanism :plain, :auth_mech_plain, :secure?
|
98
|
+
super
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
def validate_auth_plain(cid, zid, pass)
|
103
|
+
response_auth_temp_fail
|
104
|
+
end
|
105
|
+
|
106
|
+
# RFC 4616
|
107
|
+
def auth_mech_plain(arg)
|
108
|
+
response_bad_command_parameter(:message => "Encrypted session required",
|
109
|
+
:terminate => false) unless secure?
|
110
|
+
pipelinable unless arg.nil?
|
111
|
+
check_command_group
|
112
|
+
if arg.nil?
|
113
|
+
toclient "334 \r\n"
|
114
|
+
arg = fromclient
|
115
|
+
arg.chomp!
|
116
|
+
end
|
117
|
+
if arg.eql? '*'
|
118
|
+
response_syntax_error(:message => "Authentication Quit")
|
119
|
+
end
|
120
|
+
if arg !~ BASE64_VALID
|
121
|
+
response_syntax_error(:message => "Bad response")
|
122
|
+
end
|
123
|
+
decoded = Base64.decode64(arg)
|
124
|
+
cid, zid, pass = decoded.split("\000")
|
125
|
+
res = validate_auth_plain(cid, zid, pass)
|
126
|
+
if res
|
127
|
+
@authenticated = true
|
128
|
+
response_auth_ok
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def auth_mechanism_method(name)
|
133
|
+
mech = self.class.auth_mechanisms[name]
|
134
|
+
unless mech.nil?
|
135
|
+
self.class.auth_mechanisms[name][:method]
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
BASE64_VALID = /\A[A-Z0-9\/+]*=*\Z/i
|
140
|
+
|
141
|
+
def smtp_verb_auth(args)
|
142
|
+
response_bad_syntax unless esmtp?
|
143
|
+
response_bad_sequence(:message => 'Already authenticated',
|
144
|
+
:terminate=> false) if authenticated?
|
145
|
+
response_bad_sequence if in_mail_transaction?
|
146
|
+
mechanism, *initial_response = args.split(" ")
|
147
|
+
response_bad_command_parameter if mechanism.nil?
|
148
|
+
response_bad_command_parameter if initial_response.count > 1
|
149
|
+
if initial_response.count == 1 and initial_response[0] !~ BASE64_VALID
|
150
|
+
response_bad_command_parameter
|
151
|
+
end
|
152
|
+
mechanism = mechanism.to_s.upcase.intern
|
153
|
+
response_bad_command_parameter if auth_mechanism_method(mechanism).nil?
|
154
|
+
send auth_mechanism_method(mechanism), initial_response[0]
|
155
|
+
end
|
156
|
+
|
157
|
+
def mail_param_auth(param)
|
158
|
+
@mail_auth = from_xtext param
|
159
|
+
puts "MAIL AUTH=#{@mail_auth}"
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# vim: set sw=2 sts=2 ts=2 et syntax=ruby: #
|
2
|
+
=begin license
|
3
|
+
Copyright 2011 Novell, Inc.
|
4
|
+
|
5
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
you may not use this file except in compliance with the License.
|
7
|
+
You may obtain a copy of the License at
|
8
|
+
|
9
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
|
11
|
+
Unless required by applicable law or agreed to in writing, software
|
12
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
See the License for the specific language governing permissions and
|
15
|
+
limitations under the License.
|
16
|
+
|
17
|
+
Author(s):
|
18
|
+
Peter Bowen <pzbowen@gmail.com> Ottawa, Ontario, Canada
|
19
|
+
=end
|
20
|
+
|
21
|
+
module Groat
|
22
|
+
module SMTPD
|
23
|
+
module Extensions
|
24
|
+
module BinaryMIME
|
25
|
+
def self.included mod
|
26
|
+
puts "Included RFC 3030: BINARYMIME"
|
27
|
+
raise SMTPExtensionError.new("BINARYMIME requires CHUNKING") unless mod.ehlo_keyword_known? :chunking
|
28
|
+
mod.ehlo_keyword :binarymime
|
29
|
+
mod.mail_param :body, :mail_param_body
|
30
|
+
@body_encodings = [] if @body_encodings.nil?
|
31
|
+
@body_encodings << "BINARYMIME" unless @body_encodings.include? "BINARYMIME"
|
32
|
+
@body_encodings << "7BIT" unless @body_encodings.include? "7BIT"
|
33
|
+
super
|
34
|
+
end
|
35
|
+
|
36
|
+
def mail_param_body(param)
|
37
|
+
param.upcase!
|
38
|
+
unless @body_encodings.include? param
|
39
|
+
response_bad_parameter(:message => "Unown mail body type")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
@mail_body = param
|
43
|
+
puts "MAIL BODY=#{@mail_body}"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# vim: set sw=2 sts=2 ts=2 et syntax=ruby: #
|
2
|
+
=begin license
|
3
|
+
Copyright 2011 Novell, Inc.
|
4
|
+
|
5
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
you may not use this file except in compliance with the License.
|
7
|
+
You may obtain a copy of the License at
|
8
|
+
|
9
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
|
11
|
+
Unless required by applicable law or agreed to in writing, software
|
12
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
See the License for the specific language governing permissions and
|
15
|
+
limitations under the License.
|
16
|
+
|
17
|
+
Author(s):
|
18
|
+
Peter Bowen <pzbowen@gmail.com> Ottawa, Ontario, Canada
|
19
|
+
=end
|
20
|
+
|
21
|
+
module Groat
|
22
|
+
module SMTPD
|
23
|
+
module Extensions
|
24
|
+
module Chunking
|
25
|
+
def self.included mod
|
26
|
+
puts "Included RFC 3030: CHUNKING"
|
27
|
+
mod.ehlo_keyword :chunking
|
28
|
+
mod.verb :bdat, :smtp_verb_bdat
|
29
|
+
end
|
30
|
+
|
31
|
+
# BDAT is unusual in that the sender just shoves data at us
|
32
|
+
# We need to grab that data before we response so as not to
|
33
|
+
# try to parse it as commands
|
34
|
+
def smtp_verb_bdat(args)
|
35
|
+
arglist = args.split(' ')
|
36
|
+
# No size means nothing to do
|
37
|
+
if arglist.count < 1
|
38
|
+
response_syntax_error :message => "Chunk size must be specified"
|
39
|
+
end
|
40
|
+
# The chunk size must be numeric
|
41
|
+
if arglist[0] !~ /\A[0-9]+\Z/
|
42
|
+
response_syntax_error :message => "Bad chunk size"
|
43
|
+
end
|
44
|
+
# Basic sanity passed, we must grab the data
|
45
|
+
data = getdata(arglist[0].to_i)
|
46
|
+
return response_no_valid_rcpt if @rcptto.count < 1
|
47
|
+
if arglist.count > 2
|
48
|
+
response_syntax_error
|
49
|
+
elsif arglist.count == 2 and arglist[1].upcase != "LAST"
|
50
|
+
response_syntax_error :message => "Bad end marker"
|
51
|
+
end
|
52
|
+
response_ok
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# vim: set sw=2 sts=2 ts=2 et syntax=ruby: #
|
2
|
+
=begin license
|
3
|
+
Copyright 2011 Novell, Inc.
|
4
|
+
|
5
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
you may not use this file except in compliance with the License.
|
7
|
+
You may obtain a copy of the License at
|
8
|
+
|
9
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
|
11
|
+
Unless required by applicable law or agreed to in writing, software
|
12
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
See the License for the specific language governing permissions and
|
15
|
+
limitations under the License.
|
16
|
+
|
17
|
+
Author(s):
|
18
|
+
Peter Bowen <pzbowen@gmail.com> Ottawa, Ontario, Canada
|
19
|
+
=end
|
20
|
+
|
21
|
+
module Groat
|
22
|
+
module SMTPD
|
23
|
+
module Extensions
|
24
|
+
module EightBitMIME
|
25
|
+
def self.included mod
|
26
|
+
puts "Included RFC 1652: 8bit-MIMEtransport"
|
27
|
+
mod.ehlo_keyword :"8bitmime"
|
28
|
+
mod.mail_param :body, :mail_param_body
|
29
|
+
@body_encodings = [] if @body_encodings.nil?
|
30
|
+
@body_encodings << "8BITMIME" unless @body_encodings.include? "8BITMIME"
|
31
|
+
@body_encodings << "7BIT" unless @body_encodings.include? "7BIT"
|
32
|
+
super
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
def mail_param_body(param)
|
37
|
+
param.upcase!
|
38
|
+
unless @body_encodings.include? param
|
39
|
+
response_bad_parameter(:message => "Unown mail body type")
|
40
|
+
end
|
41
|
+
@mail_body = param
|
42
|
+
puts "MAIL BODY=#{@mail_body}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|