maildiode 0.2.1 → 0.2.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.
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'