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