maildiode 0.0.2

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/lib/settings.rb ADDED
@@ -0,0 +1,66 @@
1
+ # Copyright 2007-2008 Kevin B. Smith
2
+ # This file is part of MailDiode.
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License version 3, as
6
+ # published by the Free Software Foundation.
7
+
8
+ # This program is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ # GNU General Public License for more details.
12
+
13
+ # You should have received a copy of the GNU General Public License
14
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
15
+
16
+
17
+ module MailDiode
18
+
19
+ class Settings
20
+ def initialize(config_file)
21
+ @settings = {}
22
+ File.foreach(config_file) do | line |
23
+ line = line.gsub(/#.*/, '').strip
24
+ if !line.empty?
25
+ component, keyword, args = line.split(' ', 3)
26
+ load_setting(component.downcase, keyword.downcase, args)
27
+ end
28
+ end
29
+ end
30
+
31
+ def load_setting(component, keyword, args)
32
+ component_settings = get_settings(component)
33
+ component_settings[keyword] = args
34
+ end
35
+
36
+ def get_settings(component)
37
+ component_settings = @settings[component]
38
+ if(!component_settings)
39
+ component_settings = {}
40
+ @settings[component] = component_settings
41
+ end
42
+ return component_settings
43
+ end
44
+
45
+ def get_setting(component, keyword)
46
+ component_settings = get_settings(component)
47
+ if(!component_settings)
48
+ return nil
49
+ end
50
+ return component_settings[keyword]
51
+ end
52
+
53
+ def get_int(component, keyword, default_value)
54
+ value = get_setting(component, keyword)
55
+ if(!value)
56
+ return default_value
57
+ end
58
+ return value.to_i
59
+ end
60
+
61
+ def Settings.default_file
62
+ '/etc/maildiode/maildiode.conf'
63
+ end
64
+ end
65
+
66
+ end
data/lib/util.rb ADDED
@@ -0,0 +1,107 @@
1
+ # Copyright 2007-2008 Kevin B. Smith
2
+ # This file is part of MailDiode.
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License version 3, as
6
+ # published by the Free Software Foundation.
7
+
8
+ # This program is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ # GNU General Public License for more details.
12
+
13
+ # You should have received a copy of the GNU General Public License
14
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
15
+
16
+
17
+ require 'logger'
18
+
19
+ module MailDiode
20
+ NEWLINE = "\r\n"
21
+ end
22
+
23
+
24
+ module MailDiode
25
+ class UTCFormatter < Logger::Formatter
26
+ def initialize
27
+ @datetime_format = '%Y-%m-%d %H:%M:%SZ'
28
+ end
29
+
30
+ def call(severity, time, progname, msg)
31
+ time.utc
32
+ super(severity, time, progname, msg)
33
+ end
34
+ end
35
+
36
+ def self.log_to_string
37
+ @log_buffer = StringIO.new('', 'w')
38
+ log_to_stream(@log_buffer)
39
+ end
40
+
41
+ def self.log_to_console
42
+ @log_buffer = nil
43
+ log_to_stream(STDERR)
44
+ end
45
+
46
+ def self.log_to_file(file_path)
47
+ is_safe = !file_path.index('..')
48
+ if !is_safe
49
+ raise "Log file path cannot contain .."
50
+ end
51
+ file_path.untaint
52
+ @log_buffer = nil
53
+ out = File.open(file_path, 'a')
54
+ log_to_stream(out)
55
+ end
56
+
57
+ def self.log_to_stream(destination)
58
+ level = @log ? @log.level : Logger::WARN
59
+ @log_stream = destination
60
+ @log = Logger.new(destination, 'monthly')
61
+ @log.formatter = UTCFormatter.new
62
+ @log.level = level
63
+ end
64
+
65
+ def self.get_log
66
+ return @log_buffer.string
67
+ end
68
+
69
+ def self.clear_log
70
+ @log_buffer.truncate(0)
71
+ end
72
+
73
+ def self.flush_log
74
+ @log_stream.flush
75
+ end
76
+
77
+ def self.log_error(message)
78
+ @log.error(message)
79
+ flush_log
80
+ end
81
+
82
+ def self.log_warning(message)
83
+ @log.warn(message)
84
+ flush_log
85
+ end
86
+
87
+ def self.log_info(message)
88
+ @log.info(message)
89
+ flush_log
90
+ end
91
+
92
+ def self.log_debug(message)
93
+ @log.debug(message)
94
+ flush_log
95
+ end
96
+
97
+ def self.log_success(command, args, result)
98
+ log_info("#{result}: #{command} #{args}")
99
+ end
100
+
101
+ def self.set_log_level(level)
102
+ @log.level = level
103
+ end
104
+
105
+ log_to_console
106
+
107
+ end
@@ -0,0 +1,239 @@
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
@@ -0,0 +1,20 @@
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'
metadata ADDED
@@ -0,0 +1,96 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: maildiode
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Kevin Smith
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-12-03 00:00:00 -05:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: daemons
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 1.0.10
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: gurgitate-mail
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 1.10.0
34
+ version:
35
+ - !ruby/object:Gem::Dependency
36
+ name: KirbyBase
37
+ type: :runtime
38
+ version_requirement:
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: 2.6.0
44
+ version:
45
+ description:
46
+ email: kevins@qualitycode.com
47
+ executables:
48
+ - maildiode
49
+ extensions: []
50
+
51
+ extra_rdoc_files:
52
+ - README
53
+ - doc/index.html
54
+ files:
55
+ - bin/maildiode
56
+ - lib/alias.rb
57
+ - lib/blacklist.rb
58
+ - lib/delay.rb
59
+ - lib/engine.rb
60
+ - lib/greylist.rb
61
+ - lib/maildir.rb
62
+ - lib/server.rb
63
+ - lib/util.rb
64
+ - lib/settings.rb
65
+ - test/test_engine.rb
66
+ - test/test_suite.rb
67
+ - README
68
+ - doc/index.html
69
+ has_rdoc: false
70
+ homepage: http://maildiode.rubyforge.org/
71
+ post_install_message:
72
+ rdoc_options: []
73
+
74
+ require_paths:
75
+ - lib
76
+ required_ruby_version: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: 1.8.1
81
+ version:
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: "0"
87
+ version:
88
+ requirements: []
89
+
90
+ rubyforge_project: maildiode
91
+ rubygems_version: 1.2.0
92
+ signing_key:
93
+ specification_version: 2
94
+ summary: MailDiode is a simple incoming SMTP server daemon.
95
+ test_files:
96
+ - test/test_suite.rb