maildiode 0.2.1 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
data/README CHANGED
@@ -22,7 +22,7 @@ FEATURES:
22
22
  4. Full unit test suite
23
23
  5. Runs in ruby SAFE mode for security
24
24
 
25
- NON-FEATURES:
25
+ INTENTIONAL NON-FEATURES:
26
26
  1. No outgoing mail
27
27
  2. No TLS/SSL
28
28
  3. No filtering, greylisting, aliases, etc. in the core
@@ -71,11 +71,24 @@ INSTALLATION AND CONFIGURATION
71
71
 
72
72
 
73
73
  TODO
74
- - Allow integrating milters and/or other external filters
74
+ - Replace generic plugin system with:
75
+ alias - given a recipient, returns a recipient; called for RCPT
76
+ (probably doesn't need to be a plugin)
77
+ envelope_rejector - can reject based on envelope; called
78
+ for HELO/EHLO, MAIL, and RCPT
79
+ header_rejector - can reject message delivery based on envelope
80
+ plus email headers
81
+ message_modifier - given an envelope and entire message,
82
+ returns an entire message
83
+ delivery - given a recipient an entire message, delivers it or
84
+ rejects it
85
+ - Add rspec tests for each filter/plugin
75
86
  - Blacklist: Should allow filtering on from, subject, etc
76
87
  - Add debounce filter to reject bounces for mail we didn't send
77
88
  - Greylist: Implement greylisting IP exemptions
78
89
  - Greylist: Periodically clear out old database entries
90
+ - Allow integrating milters and/or other external filters
91
+ - Remove gurgitate dependency
79
92
  - Properly handle multiple recipients where some work and some fail
80
93
  (Reply OK but send bounces to the failures)
81
94
  - Add a return path header?
data/bin/maildiode CHANGED
@@ -28,9 +28,9 @@ module MailDiode
28
28
  @settings = settings
29
29
  end
30
30
 
31
- def create_engine(hostname)
31
+ def create_engine(hostname, max_recipients)
32
32
 
33
- engine = Engine.new(hostname)
33
+ engine = Engine.new(hostname, max_recipients)
34
34
 
35
35
  engine.set_mail_handler(MaildirMessageHandler.new(@settings))
36
36
  @plugins.each do | plugin |
data/lib/engine.rb CHANGED
@@ -41,6 +41,7 @@ module MailDiode
41
41
  attr_accessor :helo
42
42
  attr_accessor :sender
43
43
  attr_accessor :recipient
44
+ attr_accessor :original_recipient
44
45
  end
45
46
 
46
47
  class Filter
@@ -81,11 +82,14 @@ module MailDiode
81
82
 
82
83
  # State machine that handles incoming SMTP commands
83
84
  class Engine
84
- def initialize(hostname)
85
+ attr_reader :envelope
86
+
87
+ def initialize(hostname, max_recipients)
85
88
  @hostname = hostname
86
89
  @greeting = "220 #{hostname} ESMTP"
87
90
  @helo_response = "250 #{hostname}"
88
91
  @filters = []
92
+ @max_recipients = max_recipients
89
93
  end
90
94
 
91
95
  def set_mail_handler(handler)
@@ -186,7 +190,7 @@ module MailDiode
186
190
  if(! @envelope)
187
191
  raise_smtp_error(SMTPError::NEED_MAIL_BEFORE_RCPT)
188
192
  end
189
- if(@envelope.recipients.size >= 100)
193
+ if(@envelope.recipients.size >= @max_recipients)
190
194
  raise_smtp_error(SMTPError::TOO_MANY_RECIPIENTS)
191
195
  end
192
196
  recipient = enforce_address_arg(args, 'to', SMTPError::SYNTAX_RCPT)
@@ -222,6 +226,7 @@ module MailDiode
222
226
  filterable_data.helo = @helo
223
227
  filterable_data.sender = sender
224
228
  filterable_data.recipient = recipient
229
+ filterable_data.original_recipient = recipient
225
230
  @filters.each do | filter |
226
231
  if filter.respond_to? :process
227
232
  filter.process(filterable_data)
data/lib/server.rb CHANGED
@@ -28,16 +28,14 @@ module MailDiode
28
28
 
29
29
  @factory = engine_factory
30
30
  super(@port, @ip, @max_connections)
31
+ @audit = true
31
32
  end
32
33
 
33
34
  def serve(client)
34
35
  begin
35
- MailDiode::log_debug("Accepting client")
36
- if(connections > 1)
37
- MailDiode::log_info("Connections: #{connections}")
38
- end
36
+ MailDiode::log_debug("serve")
39
37
  client.binmode
40
- engine = @factory.create_engine(@hostname)
38
+ engine = @factory.create_engine(@hostname, @max_recipients)
41
39
  family, port, hostname, ip = client.addr
42
40
  greeting = engine.start(ip)
43
41
  MailDiode::log_debug("Greeting: #{greeting}")
@@ -56,9 +54,11 @@ module MailDiode
56
54
  end
57
55
  end
58
56
  rescue Errno::ECONNRESET
59
- MailDiode::log_debug "Client dropped connection"
57
+ MailDiode::log_debug "Client reset connection"
60
58
  rescue Errno::ETIMEDOUT
61
59
  MailDiode::log_info "Client input timeout"
60
+ rescue Errno::ENOTCONN
61
+ MailDiode::log_info "Client dropped connection"
62
62
  rescue Exception => e
63
63
  MailDiode::log_error("Exception #{e.class}: #{e}")
64
64
  e.backtrace.each do | line |
@@ -66,8 +66,24 @@ module MailDiode
66
66
  end
67
67
  client << response("451 Internal Server Error")
68
68
  end
69
+
69
70
  end
70
-
71
+
72
+ def connecting(client)
73
+ log_new_connection(client)
74
+ true
75
+ end
76
+
77
+ def log_new_connection(client)
78
+ addr = client.peeraddr
79
+ show = "#{addr[1]} #{addr[2]}<#{addr[3]}>"
80
+ MailDiode::log_info("connecting: #{show} (#{connections})")
81
+ end
82
+
83
+ def disconnecting(clientPort)
84
+ MailDiode::log_info("disconnecting: #{clientPort} (#{connections})")
85
+ end
86
+
71
87
  def get_line(input, timeout_seconds)
72
88
  line = nil
73
89
  t = Thread.new { line = input.gets(NEWLINE) }
@@ -76,11 +92,16 @@ module MailDiode
76
92
  end
77
93
 
78
94
  def discard_early_input(client)
79
- while(client.ready?)
80
- client.getc
95
+ t = Thread.new { consume_all_input(client) }
96
+ t.join(TIMEOUT_SECONDS)
97
+ end
98
+
99
+ def consume_all_input(input)
100
+ while(input.ready?)
101
+ input.getc
81
102
  end
82
103
  end
83
-
104
+
84
105
  def response(text)
85
106
  return "#{text}#{NEWLINE}"
86
107
  end
@@ -92,10 +113,12 @@ module MailDiode
92
113
  @port = settings.get_int('server', 'port', DEFAULT_PORT)
93
114
  @port.untaint
94
115
  @max_connections = settings.get_int('server', 'maxconnections', DEFAULT_MAX_CONNECTIONS)
116
+ @max_recipients = settings.get_int('server', 'maxrecipients', DEFAULT_MAX_RECIPIENTS)
95
117
  end
96
118
 
97
119
  DEFAULT_PORT = 10025
98
- DEFAULT_MAX_CONNECTIONS = 20
120
+ DEFAULT_MAX_CONNECTIONS = 20
121
+ DEFAULT_MAX_RECIPIENTS = 5
99
122
  TIMEOUT_SECONDS = 1200 # djb recommends 1200
100
123
  end
101
124
  end
data/lib/settings.rb CHANGED
@@ -51,15 +51,17 @@ module MailDiode
51
51
  end
52
52
 
53
53
  class Settings
54
- def initialize(config_file)
55
- @settings = {}
56
- File.foreach(config_file) do | line |
57
- line = line.gsub(/#.*/, '').strip
58
- if !line.empty?
59
- component, keyword, args = line.split(' ', 3)
60
- load_setting(component.downcase, keyword.downcase, args)
61
- end
62
- end
54
+ def initialize(config_file = nil)
55
+ @settings = {}
56
+ if(config_file)
57
+ File.foreach(config_file) do | line |
58
+ line = line.gsub(/#.*/, '').strip
59
+ if !line.empty?
60
+ component, keyword, args = line.split(' ', 3)
61
+ load_setting(component.downcase, keyword.downcase, args)
62
+ end
63
+ end
64
+ end
63
65
  end
64
66
 
65
67
  def load_setting(component, keyword, args)
@@ -5,6 +5,7 @@ server hostname localhost
5
5
  server ip 0.0.0.0
6
6
  server port 10025
7
7
  server maxconnections 20
8
+ server maxrecipients 5
8
9
  #server user maildiode
9
10
 
10
11
  # Define some maildirs: name and location
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Copyright 2007-2008 Kevin B. Smith
4
+ # This file is part of MailDiode.
5
+ #
6
+ # This program is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License version 3, as
8
+ # published by the Free Software Foundation.
9
+
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+ require 'spec'
19
+
20
+ require 'util'
21
+ require 'engine'
22
+ require 'maildiode-plugins/alias'
23
+ require 'settings'
24
+
25
+ describe MailDiode::AliasFilter do
26
+ describe "Basic Settings" do
27
+ before(:each) do
28
+ settings = MailDiode::Settings.new
29
+ settings.load_setting('alias', 'from', 'to')
30
+ settings.load_setting('alias', 'alsofrom', 'to')
31
+ settings.load_setting('alias', '\d\d', 'digits')
32
+ @filter = MailDiode::AliasFilter.new(settings)
33
+ @filterable = MailDiode::FilterableData.new
34
+ end
35
+
36
+ it "should perform a simple alias replacement" do
37
+ @filterable.recipient = 'from'
38
+ @filter.process(@filterable)
39
+ @filterable.recipient.should == 'to'
40
+ end
41
+
42
+ it "should allow a second alias to the same destination" do
43
+ @filterable = MailDiode::FilterableData.new
44
+ @filterable.recipient = 'alsofrom'
45
+ @filter.process(@filterable)
46
+ @filterable.recipient.should == 'to'
47
+ end
48
+
49
+ it "should ignore case" do
50
+ @filterable.recipient = 'FROM'
51
+ @filter.process(@filterable)
52
+ @filterable.recipient.should == 'to'
53
+ end
54
+
55
+ it "should allow REGEXP aliases" do
56
+ @filterable = MailDiode::FilterableData.new
57
+ @filterable.recipient = '27'
58
+ @filter.process(@filterable)
59
+ @filterable.recipient.should == 'digits'
60
+ end
61
+
62
+ it "should not affect original_recipient" do
63
+ @filterable = MailDiode::FilterableData.new
64
+ @filterable.original_recipient = 'untouched'
65
+ @filterable.recipient = 'from'
66
+ @filter.process(@filterable)
67
+ @filterable.original_recipient.should == 'untouched'
68
+ end
69
+ end
70
+
71
+ end
@@ -0,0 +1,458 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Copyright 2007-2008 Kevin B. Smith
4
+ # This file is part of MailDiode.
5
+ #
6
+ # This program is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License version 3, as
8
+ # published by the Free Software Foundation.
9
+
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+ require 'spec'
19
+
20
+ require 'util'
21
+ require 'engine'
22
+
23
+ FAKE_IP = '10.10.10.10'
24
+ FAKE_HOSTNAME = 'woohoo'
25
+ MAX_RECIPIENTS = 7
26
+
27
+ class DummyFilter < MailDiode::Filter
28
+ attr_accessor :reject
29
+ attr_accessor :reject_process
30
+ attr_accessor :alias
31
+ attr_reader :data
32
+ attr_reader :helo_text
33
+ attr_reader :mail_text
34
+ attr_reader :rcpt_text
35
+
36
+ def helo(host)
37
+ if(reject)
38
+ raise MailDiode::SMTPError.new(reject)
39
+ end
40
+ @helo_text = host
41
+ end
42
+
43
+ def mail(sender)
44
+ if(reject)
45
+ raise MailDiode::SMTPError.new(reject)
46
+ end
47
+ @mail_text = sender
48
+ end
49
+
50
+ def rcpt(recipient)
51
+ if(reject)
52
+ raise MailDiode::SMTPError.new(reject)
53
+ end
54
+ @rcpt_text = recipient
55
+ end
56
+
57
+ def process(filterable_data)
58
+ if(reject_process)
59
+ raise MailDiode::SMTPError.new(reject_process)
60
+ end
61
+ filterable_data.recipient = @alias
62
+ @data = filterable_data
63
+ end
64
+ end
65
+
66
+ $SAMPLE_MAIL_ID = '12345'
67
+
68
+ class DummyMailHandler < MailDiode::MailHandler
69
+ attr_reader :message_text
70
+ attr_accessor :reject
71
+
72
+ def initialize
73
+ @to = []
74
+ valid = true
75
+ end
76
+
77
+ def valid_recipient?(to_address)
78
+ return !reject
79
+ end
80
+
81
+ def process_message(original_recipient, recipient, text_lines)
82
+ @message_text = text_lines
83
+ return $SAMPLE_MAIL_ID
84
+ end
85
+
86
+ def message_without_first_line
87
+ return message_text.split("\r\n")[1..-1].join("\r\n")
88
+ end
89
+ end
90
+
91
+ describe MailDiode::Engine do
92
+ before(:each) do
93
+ MailDiode::set_log_level(Logger::WARN)
94
+ MailDiode::log_to_console
95
+
96
+ @engine = MailDiode::Engine.new(FAKE_HOSTNAME, MAX_RECIPIENTS)
97
+ @handler = DummyMailHandler.new
98
+ @engine.set_mail_handler(@handler)
99
+
100
+ @greeting = @engine.start(FAKE_IP)
101
+ end
102
+
103
+ describe "Normal Logging" do
104
+ it "should log nothing for a valid command" do
105
+ MailDiode::log_to_string
106
+ @engine.process_line('noop')
107
+ MailDiode::get_log.should == ''
108
+ end
109
+
110
+ it "should log nothing for a bad command" do
111
+ MailDiode::log_to_string
112
+ @engine.process_line('xxx')
113
+ MailDiode::get_log.should == ''
114
+ end
115
+ end
116
+
117
+ describe "Verbose Logging" do
118
+ it "should log every command when verbose" do
119
+ MailDiode::set_log_level(Logger::DEBUG)
120
+ MailDiode::log_to_string
121
+ @engine.process_line('noop')
122
+ (MailDiode::get_log.index('DEBUG')>0).should == true
123
+ end
124
+ end
125
+
126
+ describe "Greeting" do
127
+ it "should start its greeting with 220" do
128
+ @greeting.should == "220 #{FAKE_HOSTNAME} ESMTP"
129
+ end
130
+
131
+ it "should include hostname in its greeting" do
132
+ @greeting.index(FAKE_HOSTNAME).should_not == nil
133
+ end
134
+ end
135
+
136
+ describe "Unknown commands" do
137
+ it "should reject a blank command" do
138
+ @engine.process_line('').index(MailDiode::SMTPError::BAD_COMMAND).should == 0
139
+ end
140
+
141
+ it "should reject an unknown command" do
142
+ @engine.process_line('xxx').index(MailDiode::SMTPError::BAD_COMMAND).should == 0
143
+ end
144
+ end
145
+
146
+ describe "Commands" do
147
+ describe "NOOP" do
148
+ it "should allow NOOP in upper or mixed or lower case" do
149
+ @engine.process_line('NOOP').should == MailDiode::RESULT_OK
150
+ @engine.process_line('Noop').should == MailDiode::RESULT_OK
151
+ @engine.process_line('noop').should == MailDiode::RESULT_OK
152
+ end
153
+
154
+ it "should allow trailing space" do
155
+ @engine.process_line('noop ').should == MailDiode::RESULT_OK
156
+ end
157
+
158
+ it "should disallow a parameter for NOOP" do
159
+ @engine.process_line('noop x').should == MailDiode::SMTPError::SYNTAX_NOOP
160
+ end
161
+ end
162
+
163
+ describe "QUIT" do
164
+ it "should say BYE in response to QUIT" do
165
+ @engine.process_line('quit').should == MailDiode::RESULT_BYE
166
+ end
167
+
168
+ it "should terminate after getting a QUIT" do
169
+ @engine.should_not be_terminate
170
+ @engine.process_line('quit')
171
+ @engine.should be_terminate
172
+ end
173
+
174
+ it "should disallow a parameter for QUIT" do
175
+ @engine.process_line('quit x').should == MailDiode::SMTPError::SYNTAX_QUIT
176
+ end
177
+ end
178
+
179
+ describe "HELO without parameters" do
180
+ it "should disallow HELO and EHLO without a parameter" do
181
+ @engine.process_line('helo').should == MailDiode::SMTPError::SYNTAX_HELO
182
+ @engine.process_line('ehlo').should == MailDiode::SMTPError::SYNTAX_HELO
183
+ end
184
+ end
185
+
186
+ describe "HELO" do
187
+ it "should reply with 250 to a simple HELO" do
188
+ @engine.process_line('helo localhost').index('250').should == 0
189
+ end
190
+
191
+ it "should reply the same for each HELO or EHLO" do
192
+ reply = @engine.process_line('helo localhost')
193
+ @engine.process_line('helo other').should == reply
194
+ @engine.process_line('ehlo 1.2.3.4').should == reply
195
+ @engine.process_line('helo some.domain.name').should == reply
196
+ end
197
+
198
+ it "should allow tab before parameter" do
199
+ @engine.process_line("helo\tx").index('250').should == 0
200
+ end
201
+
202
+ end
203
+
204
+ describe "RSET" do
205
+ it "should allow a simple initial RSET" do
206
+ @engine.process_line('rset').should == MailDiode::RESULT_OK
207
+ end
208
+
209
+ it "should disallow a parameter for RSET" do
210
+ @engine.process_line('rset x').should == MailDiode::SMTPError::SYNTAX_RSET
211
+ end
212
+
213
+ it "should clear FROM when it gets a RSET" do
214
+ @engine.process_line('mail From:<sender@example.com>')
215
+ @engine.process_line('rset')
216
+ result = @engine.process_line('rcpt To:<recipient@example.com>')
217
+ result.should == MailDiode::SMTPError::NEED_MAIL_BEFORE_RCPT
218
+ end
219
+ end
220
+
221
+ describe "VRFY" do
222
+ it "should disallow VRFY without a parameter" do
223
+ @engine.process_line('vrfy').should == MailDiode::SMTPError::SYNTAX_VRFY
224
+ end
225
+
226
+ it "should respond with UNSURE for any VRFY request" do
227
+ @engine.process_line('vrfy anything').should == MailDiode::RESULT_UNSURE
228
+ end
229
+ end
230
+
231
+ describe "MAIL" do
232
+ before(:each) do
233
+ @valid_sender = "correct@example.com"
234
+ end
235
+
236
+ it "should disallow MAIL with no parameters" do
237
+ @engine.process_line('mail').should == MailDiode::SMTPError::SYNTAX_MAIL
238
+ end
239
+ it "should disallow MAIL without from:" do
240
+ @engine.process_line('mail foo').should == MailDiode::SMTPError::SYNTAX_MAIL
241
+ end
242
+ it "should disallow MAIL that doesn't specify an address after from:" do
243
+ @engine.process_line('mail from:').should == MailDiode::SMTPError::SYNTAX_MAIL
244
+ end
245
+
246
+ it "should allow mail from:address" do
247
+ result = @engine.process_line("mail From:#{@valid_sender}")
248
+ result.should == MailDiode::RESULT_OK
249
+ @engine.envelope.sender.should == @valid_sender
250
+ end
251
+
252
+ it "should strip < > from around sender email" do
253
+ @engine.process_line("mail From:<#{@valid_sender}>")
254
+ @engine.envelope.sender.should == @valid_sender
255
+ end
256
+
257
+ it "should allow space between from: and the sender" do
258
+ @engine.process_line("mail From: #{@valid_sender}").should == MailDiode::RESULT_OK
259
+ end
260
+
261
+ it "should allow a second MAIL command in a row" do
262
+ @engine.process_line('mail From:Someone')
263
+ result = @engine.process_line("mail From:#{@valid_sender}")
264
+ result.should == MailDiode::RESULT_OK
265
+ @engine.envelope.sender.should == @valid_sender
266
+ end
267
+ end
268
+
269
+ describe "RCPT before MAIL" do
270
+ it "should disallow RCPT if no sender has been specified" do
271
+ @engine.process_line('rcpt To:<correct@example.com>').should == MailDiode::SMTPError::NEED_MAIL_BEFORE_RCPT
272
+ end
273
+ end
274
+
275
+ describe "RCPT" do
276
+ before(:each) do
277
+ @valid_recipient = 'correct@example.com'
278
+ @engine.process_line('mail From:Someone')
279
+ @engine.envelope.recipients.size.should == 0
280
+ end
281
+
282
+ it "should disallow RCPT with no parameters" do
283
+ @engine.process_line('rcpt').should == MailDiode::SMTPError::SYNTAX_RCPT
284
+ end
285
+ it "should disallow RCPT without to:" do
286
+ @engine.process_line('rcpt foo').should == MailDiode::SMTPError::SYNTAX_RCPT
287
+ end
288
+ it "should disallow RCPT that doesn't specify an address after to:" do
289
+ @engine.process_line('rcpt to:').should == MailDiode::SMTPError::SYNTAX_RCPT
290
+ end
291
+
292
+ it "should allow RCPT to:address" do
293
+ result = @engine.process_line("rcpt To:#{@valid_recipient}")
294
+ result.should == MailDiode::RESULT_OK
295
+ @engine.envelope.recipients.size.should == 1
296
+ @engine.envelope.recipients[0].should == @valid_recipient
297
+ end
298
+
299
+ it "should allow space after to: in RCPT" do
300
+ result = @engine.process_line("rcpt To: #{@valid_recipient}")
301
+ result.should == MailDiode::RESULT_OK
302
+ @engine.envelope.recipients.size.should == 1
303
+ @engine.envelope.recipients[0].should == @valid_recipient
304
+ end
305
+
306
+ it "should remove < > from recipient address" do
307
+ result = @engine.process_line("rcpt To:<#{@valid_recipient}>")
308
+ result.should == MailDiode::RESULT_OK
309
+ @engine.envelope.recipients.size.should == 1
310
+ @engine.envelope.recipients[0].should == @valid_recipient
311
+ end
312
+
313
+ it "should clear recipients when a second FROM is processed" do
314
+ @engine.process_line("rcpt To:#{@valid_recipient}")
315
+ @engine.process_line('mail From:Someone')
316
+ @engine.envelope.recipients.size.should == 0
317
+ end
318
+
319
+ it "should fail if there are too many recipients" do
320
+ (MAX_RECIPIENTS-1).times do
321
+ @engine.process_line("rcpt To:#{@valid_recipient}")
322
+ end
323
+ @engine.process_line("rcpt To:#{@valid_recipient}").should == MailDiode::RESULT_OK
324
+ @engine.process_line("rcpt To:#{@valid_recipient}").should == MailDiode::SMTPError::TOO_MANY_RECIPIENTS
325
+ end
326
+ end
327
+
328
+ describe "DATA before RCPT" do
329
+ it "should disallow DATA before RCPT" do
330
+ @engine.process_line('data').should == MailDiode::SMTPError::NEED_RCPT_BEFORE_DATA
331
+ end
332
+ end
333
+
334
+ describe "DATA" do
335
+ before(:each) do
336
+ @engine.process_line('mail From:Someone')
337
+ @engine.process_line("rcpt To:Someone")
338
+ end
339
+
340
+ it "should disallow DATA with a parameter" do
341
+ @engine.process_line('data xxx').should == MailDiode::SMTPError::SYNTAX_DATA
342
+ end
343
+
344
+ it "should allow lines of message text after DATA" do
345
+ @engine.process_line('data').should == MailDiode::RESULT_DATA_OK
346
+ @engine.process_line('This is message data').should == nil
347
+ @engine.process_line('Second line').should == nil
348
+ @engine.process_line('.').should == "#{MailDiode::RESULT_OK} #{$SAMPLE_MAIL_ID}"
349
+ end
350
+
351
+ it "should allow escaped dot at start of message text line" do
352
+ @engine.process_line('data')
353
+ line_with_dot = '..Line with a dot'
354
+ @engine.process_line(line_with_dot)
355
+ @engine.process_line('.')
356
+ @handler.message_without_first_line.should == line_with_dot[1..-1]
357
+ end
358
+
359
+ it "should reset sender after data" do
360
+ @engine.process_line('data')
361
+ @engine.process_line('.')
362
+ @engine.process_line('data').should == MailDiode::SMTPError::NEED_RCPT_BEFORE_DATA
363
+ end
364
+ end
365
+ end
366
+
367
+ describe "Filter" do
368
+ before(:each) do
369
+ @filter = DummyFilter.new
370
+ @engine.add_filter(@filter)
371
+ end
372
+
373
+ it "should call filter#helo for HELO/EHLO" do
374
+ host = 'test'
375
+ @engine.process_line("helo #{host}")
376
+ @filter.helo_text.should == host
377
+ host = 'another'
378
+ @engine.process_line("ehlo #{host}")
379
+ @filter.helo_text.should == host
380
+ end
381
+
382
+ it "should allow filter to reject HELO" do
383
+ @filter.reject = "450 Try Later"
384
+ @engine.process_line('helo test').should == @filter.reject
385
+ end
386
+
387
+ it "should call filter#mail for MAIL" do
388
+ sender = 'someone'
389
+ @engine.process_line("mail from:#{sender}")
390
+ @filter.mail_text.should == sender
391
+ end
392
+
393
+ it "should allow filter to reject MAIL" do
394
+ @filter.reject = "450 Try Later"
395
+ @engine.process_line('mail from: test').should == @filter.reject
396
+ end
397
+
398
+ it "should not call filter#process for MAIL command" do
399
+ @engine.process_line('mail From:<sender@example.com>')
400
+ @filter.data.should == nil
401
+ end
402
+
403
+ it "should call filter#rcpt for RCPT" do
404
+ recipient = 'whoever'
405
+ @engine.process_line("mail from:someone")
406
+ @engine.process_line("rcpt to:#{recipient}").should == MailDiode::RESULT_OK
407
+ @filter.rcpt_text.should == recipient
408
+ end
409
+
410
+ it "should allow filter to reject RCPT" do
411
+ @engine.process_line('mail From:<sender@example.com>')
412
+ @filter.reject = "450 Try Later"
413
+ @engine.process_line('rcpt To:<recipient@example.com>').should == @filter.reject
414
+ end
415
+
416
+ it "should call filter#process for RCPT command" do
417
+ sender = 'sender@example.com'
418
+ @filter.alias = 'real_recipient'
419
+ recipient = 'recipient@example.com'
420
+ @engine.process_line("mail From:#{sender}")
421
+ @engine.process_line("rcpt To:#{recipient}")
422
+ @filter.data.sender_ip == FAKE_IP
423
+ @filter.data.helo == FAKE_HOSTNAME
424
+ @filter.data.sender.should == sender
425
+ @filter.data.recipient.should == @filter.alias
426
+ @filter.data.original_recipient.should == recipient
427
+ end
428
+
429
+ it "should allow filter#process to reject" do
430
+ @engine.process_line('mail From:<sender@example.com>')
431
+ @filter.reject_process = "450 Try process Later"
432
+ @engine.process_line('rcpt To:<recipient@example.com>').should == @filter.reject_process
433
+ end
434
+ end
435
+
436
+ describe "Mail Handler" do
437
+ it "should pass the message to the handler" do
438
+ @engine.process_line('mail From:<sender@example.com>')
439
+ @engine.process_line('rcpt To:<recipient@example.com>')
440
+
441
+ sample_text = ['First line', 'Second', '', ' . not a dot line', 'After blank']
442
+ @engine.process_line('DATA')
443
+ sample_text.each do | line |
444
+ @engine.process_line(line)
445
+ end
446
+ @engine.process_line('.').index(MailDiode::RESULT_OK).should == 0
447
+ @handler.message_without_first_line.should == sample_text.join("\r\n")
448
+ end
449
+
450
+ it "should allow message handler to reject RCPT" do
451
+ address = 'sender@example.com'
452
+ @handler.reject = true
453
+ @engine.process_line("mail From:Someone")
454
+ error = "#{MailDiode::SMTPError::UNKNOWN_RECIPIENT}: #{address}"
455
+ @engine.process_line("rcpt To:#{address}").should == error
456
+ end
457
+ end
458
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: maildiode
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kevin Smith
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-02-08 00:00:00 -05:00
12
+ date: 2009-04-05 00:00:00 -04:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -52,8 +52,8 @@ files:
52
52
  - lib/maildiode-plugins/alias.rb
53
53
  - lib/maildiode-plugins/blacklist.rb
54
54
  - lib/maildiode-plugins/delay.rb
55
- - test/test_engine.rb
56
- - test/test_suite.rb
55
+ - test/alias_spec.rb
56
+ - test/engine_spec.rb
57
57
  - maildiode.conf.sample
58
58
  - README
59
59
  - COPYING
@@ -85,4 +85,5 @@ signing_key:
85
85
  specification_version: 2
86
86
  summary: MailDiode is a simple incoming SMTP server daemon.
87
87
  test_files:
88
- - test/test_suite.rb
88
+ - test/engine_spec.rb
89
+ - test/alias_spec.rb
data/test/test_engine.rb DELETED
@@ -1,239 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- # Copyright 2007-2008 Kevin B. Smith
4
- # This file is part of MailDiode.
5
- #
6
- # This program is free software: you can redistribute it and/or modify
7
- # it under the terms of the GNU General Public License version 3, as
8
- # published by the Free Software Foundation.
9
-
10
- # This program is distributed in the hope that it will be useful,
11
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
- # GNU General Public License for more details.
14
-
15
- # You should have received a copy of the GNU General Public License
16
- # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
-
18
- require 'test/unit'
19
- require 'stringio'
20
-
21
- require 'util'
22
- require 'engine'
23
-
24
- FAKE_IP = '10.10.10.10'
25
-
26
- class DummyFilter < MailDiode::Filter
27
- attr_accessor :reject
28
- attr_reader :data
29
-
30
- def process(filterable_data)
31
- if(reject)
32
- raise MailDiode::SMTPError.new(reject)
33
- end
34
- @data = filterable_data
35
- end
36
- end
37
-
38
- class DummyMailHandler < MailDiode::MailHandler
39
- attr_reader :message_text
40
- attr_accessor :reject
41
-
42
- SAMPLE_ID = '12345'
43
-
44
- def initialize
45
- @to = []
46
- valid = true
47
- end
48
-
49
- def valid_recipient?(to_address)
50
- return !reject
51
- end
52
-
53
- def process_message(recipient, text_lines)
54
- @message_text = text_lines
55
- return SAMPLE_ID
56
- end
57
-
58
- def message_without_first_line
59
- return message_text.split("\r\n")[1..-1].join("\r\n")
60
- end
61
- end
62
-
63
- class TestEngine < Test::Unit::TestCase
64
- def setup
65
- MailDiode::set_log_level(Logger::WARN)
66
- MailDiode::log_to_console
67
-
68
- @engine = MailDiode::Engine.new('woohoo')
69
- @handler = DummyMailHandler.new
70
- @engine.set_mail_handler(@handler)
71
-
72
- @greeting = @engine.start(FAKE_IP)
73
- end
74
-
75
- def teardown
76
- end
77
-
78
- def test_greeting
79
- assert_equal(0, @greeting.index('220 '), "Missing 220?")
80
- assert_match('woohoo', @greeting, "missing hostname?")
81
- end
82
-
83
- def test_unknown_commands
84
- assert_equal(0, @engine.process_line('').index(MailDiode::SMTPError::BAD_COMMAND), "Blank cmd accepted?")
85
- assert_equal(0, @engine.process_line('xxx').index(MailDiode::SMTPError::BAD_COMMAND), "Bogus cmd accepted?")
86
- end
87
-
88
- def test_noop
89
- assert_equal(MailDiode::RESULT_OK, @engine.process_line('noop'), "noop failed?")
90
- assert_equal(MailDiode::SMTPError::SYNTAX_NOOP, @engine.process_line('noop x'), "noop allowed a parameter?")
91
- end
92
-
93
- def test_quit
94
- assert(!@engine.terminate?, 'terminating before QUIT?')
95
- assert_equal(MailDiode::RESULT_BYE, @engine.process_line('quit'), "didn't say bye?")
96
- assert(@engine.terminate?, 'not terminating after QUIT?')
97
- assert_equal(MailDiode::SMTPError::SYNTAX_QUIT, @engine.process_line('quit x'), "quit allowed a parameter?")
98
- end
99
-
100
- def test_helo
101
- assert_equal(MailDiode::SMTPError::SYNTAX_HELO, @engine.process_line('helo'), "helo without arg worked?")
102
- assert_equal(MailDiode::SMTPError::SYNTAX_HELO, @engine.process_line('ehlo'), "ehlo without arg worked?")
103
-
104
- helo_result = @engine.process_line('helo localhost')
105
- assert_equal(0, helo_result.index('250'), "helo failed?")
106
- assert_equal(helo_result, @engine.process_line('helo localhost'), "second helo failed?")
107
- assert_equal(helo_result, @engine.process_line('ehlo localhost'), "ehlo failed?")
108
- assert_equal(helo_result, @engine.process_line('ehlo [1.2.3.4]'), "ehlo failed?")
109
- assert_equal(helo_result, @engine.process_line('ehlo winnie.the.poo'), "ehlo failed?")
110
- end
111
-
112
- def test_rset
113
- assert_equal(MailDiode::RESULT_OK, @engine.process_line('rset'), "rset failed?")
114
- assert_equal(MailDiode::SMTPError::SYNTAX_RSET, @engine.process_line('rset x'), "rset allowed a parameter?")
115
-
116
- @engine.process_line('mail From:<sender@example.com>')
117
- @engine.process_line('rset')
118
- assert_equal(MailDiode::SMTPError::NEED_MAIL_BEFORE_RCPT, @engine.process_line('rcpt To:<recipient@example.com>'), "rset didn't clear from?")
119
- end
120
-
121
- def test_vrfy
122
- assert_equal(MailDiode::SMTPError::SYNTAX_VRFY, @engine.process_line('vrfy'), "vrfy without arg worked?")
123
-
124
- assert_equal(MailDiode::RESULT_UNSURE, @engine.process_line('vrfy anything'), "vrfy failed?")
125
- end
126
-
127
- def test_mail
128
- assert_equal(MailDiode::RESULT_OK, @engine.process_line('mail From:<correct@example.com>'), "mail rejected good from?")
129
- assert_equal(MailDiode::RESULT_OK, @engine.process_line('mail From: <correct@example.com>'), "mail rejected because of space after from?")
130
- assert_equal(MailDiode::SMTPError::SYNTAX_MAIL, @engine.process_line('mail'), "mail without arg worked?")
131
- assert_equal(MailDiode::SMTPError::SYNTAX_MAIL, @engine.process_line('mail foo'), "mail without From: worked?")
132
- assert_equal(MailDiode::SMTPError::SYNTAX_MAIL, @engine.process_line('mail from:'), "mail without from arg worked?")
133
- end
134
-
135
- def test_rcpt
136
- assert_equal(MailDiode::SMTPError::NEED_MAIL_BEFORE_RCPT, @engine.process_line('rcpt To:<correct@example.com>'), "rcpt before mail worked?")
137
-
138
- @engine.process_line('mail From:<correct@example.com>')
139
- assert_equal(MailDiode::RESULT_OK, @engine.process_line('rcpt To:<correct@example.com>'), "mail rejected good from?")
140
- assert_equal(MailDiode::RESULT_OK, @engine.process_line('rcpt To: <correct@example.com>'), "mail rejected because of space after to?")
141
- assert_equal(MailDiode::SMTPError::SYNTAX_RCPT, @engine.process_line('rcpt'), "mail without arg worked?")
142
- assert_equal(MailDiode::SMTPError::SYNTAX_RCPT, @engine.process_line('rcpt foo'), "mail without To: worked?")
143
- assert_equal(MailDiode::SMTPError::SYNTAX_RCPT, @engine.process_line('rcpt to:'), "mail without to arg worked?")
144
-
145
- @engine.process_line('mail From:<correct@example.com>')
146
- 99.times do
147
- @engine.process_line('rcpt To:<correct@example.com>')
148
- end
149
- assert_equal(MailDiode::RESULT_OK, @engine.process_line('rcpt To:<correct@example.com>'), "100th recipient rejected?")
150
- assert_equal(MailDiode::SMTPError::TOO_MANY_RECIPIENTS, @engine.process_line('rcpt To:<correct@example.com>'), "101st recipient not rejected?")
151
- end
152
-
153
- def test_data
154
- assert_equal(MailDiode::SMTPError::NEED_RCPT_BEFORE_DATA, @engine.process_line('data'), "data before rcpt worked?")
155
-
156
- @engine.process_line('mail From:<sender@example.com>')
157
- @engine.process_line('rcpt To:<recipient@example.com>')
158
- assert_equal(MailDiode::SMTPError::SYNTAX_DATA, @engine.process_line('data xxx'), "data with arg accepted?")
159
- assert_equal(MailDiode::RESULT_DATA_OK, @engine.process_line('data'), "data rejected?")
160
- assert_equal(nil, @engine.process_line('This is message data'), "responded to message data?")
161
- @engine.process_line('Second line')
162
- assert_equal(0, @engine.process_line('.').index(MailDiode::RESULT_OK), "didn't pretend to save?")
163
- end
164
-
165
-
166
-
167
-
168
-
169
- def test_filter
170
- filter = DummyFilter.new
171
- @engine.add_filter(filter)
172
- @engine.process_line('mail From:<sender@example.com>')
173
- @engine.process_line('rcpt To:<recipient@example.com>')
174
- assert_equal('sender@example.com', filter.data.sender, "didn't call handler validate_sender?")
175
- assert_equal('recipient@example.com', filter.data.recipient, "didn't call handler validate_recipient?")
176
-
177
- filter.reject = "450 Try Later"
178
- assert_equal(filter.reject, @engine.process_line('rcpt To:<recipient@example.com>'), "filter rejection not propagated?")
179
- end
180
-
181
- def test_mail_handler
182
- @engine.process_line('mail From:<sender@example.com>')
183
- @engine.process_line('rcpt To:<recipient@example.com>')
184
-
185
- sample_text = ['First line', 'Second', '', ' . not a dot line', 'After blank']
186
- @engine.process_line('DATA')
187
- sample_text.each do | line |
188
- @engine.process_line(line)
189
- end
190
- assert_equal(0, @engine.process_line('.').index(MailDiode::RESULT_OK), "handler didn't accept?")
191
- assert_equal(sample_text.join("\r\n"), @handler.message_without_first_line, "bad message data?")
192
-
193
- assert_equal(MailDiode::SMTPError::NEED_RCPT_BEFORE_DATA, @engine.process_line('data'), "didn't reset after .?")
194
-
195
- MailDiode::set_log_level(Logger::INFO)
196
- MailDiode::log_to_string
197
-
198
- @handler.reject = true
199
- @engine.process_line('mail From:<sender@example.com>')
200
- assert_equal(MailDiode::SMTPError::UNKNOWN_RECIPIENT, @engine.process_line('rcpt To:<recipient@example.com>'), "reject to not propagated?")
201
- end
202
-
203
- def test_dot_in_data
204
- @engine.process_line('mail From:<sender@example.com>')
205
- @engine.process_line('rcpt To:<recipient@example.com>')
206
- @engine.process_line('data')
207
- line_with_dot = '..Line with a dot'
208
- @engine.process_line(line_with_dot)
209
- @engine.process_line('.')
210
- assert_equal(line_with_dot[1..-1], @handler.message_without_first_line, "didn't unescape the dot?")
211
- end
212
-
213
- def test_args
214
- assert_equal(MailDiode::RESULT_OK, @engine.process_line('noop '), "choked on trailing space?")
215
- assert_equal(0, @engine.process_line("helo\tx").index('250'), "choked on tab?")
216
- end
217
-
218
- def test_case_insensitive_commands
219
- assert_equal(MailDiode::RESULT_OK, @engine.process_line('noop'), "lower case failed?")
220
- assert_equal(MailDiode::RESULT_OK, @engine.process_line('NOOP'), "upper case failed?")
221
- assert_equal(MailDiode::RESULT_OK, @engine.process_line('nOOp'), "mixed case failed?")
222
- end
223
-
224
- def test_normal_logging
225
- MailDiode::log_to_string
226
- @engine.process_line('noop')
227
- assert_equal('', MailDiode::get_log)
228
- MailDiode::clear_log
229
- @engine.process_line('xxx')
230
- assert_equal('', MailDiode::get_log)
231
- end
232
-
233
- def test_verbose_logging
234
- MailDiode::set_log_level(Logger::INFO)
235
- MailDiode::log_to_string
236
- @engine.process_line('noop')
237
- assert(MailDiode::get_log.index('INFO'))
238
- end
239
- end
data/test/test_suite.rb DELETED
@@ -1,20 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- # Copyright 2007-2008 Kevin B. Smith
4
- # This file is part of MailDiode.
5
- #
6
- # This program is free software: you can redistribute it and/or modify
7
- # it under the terms of the GNU General Public License version 3, as
8
- # published by the Free Software Foundation.
9
-
10
- # This program is distributed in the hope that it will be useful,
11
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
- # GNU General Public License for more details.
14
-
15
- # You should have received a copy of the GNU General Public License
16
- # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
-
18
- require 'test/unit'
19
-
20
- require 'test_engine.rb'