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/README +85 -0
- data/bin/maildiode +142 -0
- data/doc/index.html +83 -0
- data/lib/alias.rb +47 -0
- data/lib/blacklist.rb +50 -0
- data/lib/delay.rb +48 -0
- data/lib/engine.rb +344 -0
- data/lib/greylist.rb +137 -0
- data/lib/maildir.rb +113 -0
- data/lib/server.rb +93 -0
- data/lib/settings.rb +66 -0
- data/lib/util.rb +107 -0
- data/test/test_engine.rb +239 -0
- data/test/test_suite.rb +20 -0
- metadata +96 -0
data/lib/engine.rb
ADDED
@@ -0,0 +1,344 @@
|
|
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
|
+
NOOP = 'NOOP'
|
19
|
+
HELO = 'HELO'
|
20
|
+
EHLO = 'EHLO'
|
21
|
+
QUIT = 'QUIT'
|
22
|
+
RSET = 'RSET'
|
23
|
+
VRFY = 'VRFY'
|
24
|
+
MAIL = 'MAIL'
|
25
|
+
RCPT = 'RCPT'
|
26
|
+
DATA = 'DATA'
|
27
|
+
|
28
|
+
RESULT_BYE = '221 Bye'
|
29
|
+
RESULT_OK = '250 Ok'
|
30
|
+
RESULT_UNSURE = '252'
|
31
|
+
RESULT_DATA_OK = '354'
|
32
|
+
|
33
|
+
ACCEPTED = :accepted
|
34
|
+
NO_OPINION = :no_opinion
|
35
|
+
end
|
36
|
+
|
37
|
+
module MailDiode
|
38
|
+
|
39
|
+
class FilterableData
|
40
|
+
attr_accessor :sender_ip
|
41
|
+
attr_accessor :helo
|
42
|
+
attr_accessor :sender
|
43
|
+
attr_accessor :recipient
|
44
|
+
end
|
45
|
+
|
46
|
+
class Filter
|
47
|
+
def process(filterable_data)
|
48
|
+
filterable_data
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class MailHandler
|
53
|
+
def valid_recipient?(recipient)
|
54
|
+
false
|
55
|
+
end
|
56
|
+
|
57
|
+
def process_message(sender, recipient, message)
|
58
|
+
nil
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
class Envelope
|
64
|
+
def initialize(from_address)
|
65
|
+
@from = from_address
|
66
|
+
@to = []
|
67
|
+
end
|
68
|
+
|
69
|
+
def add_recipient(to_address)
|
70
|
+
@to << to_address
|
71
|
+
end
|
72
|
+
|
73
|
+
def sender
|
74
|
+
@from
|
75
|
+
end
|
76
|
+
|
77
|
+
def recipients
|
78
|
+
@to
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# State machine that handles incoming SMTP commands
|
83
|
+
class Engine
|
84
|
+
def initialize(hostname)
|
85
|
+
@hostname = hostname
|
86
|
+
@greeting = "220 #{hostname} ESMTP"
|
87
|
+
@helo_response = "250 #{hostname}"
|
88
|
+
@filters = []
|
89
|
+
end
|
90
|
+
|
91
|
+
def set_mail_handler(handler)
|
92
|
+
@mail_handler = handler
|
93
|
+
end
|
94
|
+
|
95
|
+
def add_filter(filter)
|
96
|
+
@filters << filter
|
97
|
+
end
|
98
|
+
|
99
|
+
def start(sender_ip)
|
100
|
+
@sender_ip = sender_ip
|
101
|
+
return @greeting
|
102
|
+
end
|
103
|
+
|
104
|
+
def process_line(line)
|
105
|
+
begin
|
106
|
+
if @message_text
|
107
|
+
return do_message_text(line)
|
108
|
+
end
|
109
|
+
|
110
|
+
command, args = extract_args(line)
|
111
|
+
result = process_command(command, args)
|
112
|
+
MailDiode::log_success(command, args, result)
|
113
|
+
return result
|
114
|
+
rescue SMTPError => error
|
115
|
+
MailDiode.log_info(error.text)
|
116
|
+
return error.text
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
|
121
|
+
def terminate?
|
122
|
+
return @should_terminate
|
123
|
+
end
|
124
|
+
|
125
|
+
def process_command(command, args)
|
126
|
+
case command.upcase
|
127
|
+
when NOOP then return do_noop(args)
|
128
|
+
when QUIT then return do_quit(args)
|
129
|
+
when HELO then return do_helo(args)
|
130
|
+
when EHLO then return do_helo(args)
|
131
|
+
when VRFY then return do_vrfy(args)
|
132
|
+
when RSET then return do_rset(args)
|
133
|
+
when MAIL then return do_mail(args)
|
134
|
+
when RCPT then return do_rcpt(args)
|
135
|
+
when DATA then return do_data(args)
|
136
|
+
end
|
137
|
+
|
138
|
+
raise_smtp_error(SMTPError::BAD_COMMAND + ': ' + command)
|
139
|
+
end
|
140
|
+
|
141
|
+
def do_noop(args)
|
142
|
+
enforce_no_args(args, SMTPError::SYNTAX_NOOP)
|
143
|
+
return RESULT_OK
|
144
|
+
end
|
145
|
+
|
146
|
+
def do_quit(args)
|
147
|
+
enforce_no_args(args, SMTPError::SYNTAX_QUIT)
|
148
|
+
@should_terminate = true
|
149
|
+
return RESULT_BYE
|
150
|
+
end
|
151
|
+
|
152
|
+
def do_helo(args)
|
153
|
+
enforce_host_arg(args, SMTPError::SYNTAX_HELO)
|
154
|
+
@helo = args
|
155
|
+
@filters.each do | filter |
|
156
|
+
if filter.respond_to? :helo
|
157
|
+
filter.helo @helo
|
158
|
+
end
|
159
|
+
end
|
160
|
+
return @helo_response
|
161
|
+
end
|
162
|
+
|
163
|
+
def do_vrfy(args)
|
164
|
+
enforce_host_arg(args, SMTPError::SYNTAX_VRFY)
|
165
|
+
return RESULT_UNSURE
|
166
|
+
end
|
167
|
+
|
168
|
+
def do_rset(args)
|
169
|
+
enforce_no_args(args, SMTPError::SYNTAX_RSET)
|
170
|
+
@envelope = nil
|
171
|
+
return RESULT_OK
|
172
|
+
end
|
173
|
+
|
174
|
+
def do_mail(args)
|
175
|
+
from_address = enforce_address_arg(args, 'from', SMTPError::SYNTAX_MAIL)
|
176
|
+
@envelope = Envelope.new(from_address)
|
177
|
+
@filters.each do | filter |
|
178
|
+
if filter.respond_to? :mail
|
179
|
+
filter.mail from_address
|
180
|
+
end
|
181
|
+
end
|
182
|
+
return RESULT_OK
|
183
|
+
end
|
184
|
+
|
185
|
+
def do_rcpt(args)
|
186
|
+
if(! @envelope)
|
187
|
+
raise_smtp_error(SMTPError::NEED_MAIL_BEFORE_RCPT)
|
188
|
+
end
|
189
|
+
if(@envelope.recipients.size >= 100)
|
190
|
+
raise_smtp_error(SMTPError::TOO_MANY_RECIPIENTS)
|
191
|
+
end
|
192
|
+
recipient = enforce_address_arg(args, 'to', SMTPError::SYNTAX_RCPT)
|
193
|
+
@filters.each do | filter |
|
194
|
+
if filter.respond_to? :rcpt
|
195
|
+
filter.rcpt recipient
|
196
|
+
end
|
197
|
+
end
|
198
|
+
filterable_data = apply_filters(@envelope.sender, recipient)
|
199
|
+
if !@mail_handler.valid_recipient?(filterable_data.recipient)
|
200
|
+
raise_smtp_error(SMTPError::UNKNOWN_RECIPIENT)
|
201
|
+
end
|
202
|
+
@envelope.add_recipient(recipient)
|
203
|
+
return RESULT_OK
|
204
|
+
end
|
205
|
+
|
206
|
+
def do_data(args)
|
207
|
+
if(! @envelope || ! @envelope.sender)
|
208
|
+
raise_smtp_error(SMTPError::NEED_RCPT_BEFORE_DATA)
|
209
|
+
end
|
210
|
+
enforce_no_args(args, SMTPError::SYNTAX_DATA)
|
211
|
+
@message_text = []
|
212
|
+
return RESULT_DATA_OK
|
213
|
+
end
|
214
|
+
|
215
|
+
|
216
|
+
|
217
|
+
|
218
|
+
|
219
|
+
def apply_filters(sender, recipient)
|
220
|
+
filterable_data = FilterableData.new
|
221
|
+
filterable_data.sender_ip = @sender_ip
|
222
|
+
filterable_data.helo = @helo
|
223
|
+
filterable_data.sender = sender
|
224
|
+
filterable_data.recipient = recipient
|
225
|
+
@filters.each do | filter |
|
226
|
+
if filter.respond_to? :process
|
227
|
+
filter.process(filterable_data)
|
228
|
+
end
|
229
|
+
end
|
230
|
+
return filterable_data
|
231
|
+
end
|
232
|
+
|
233
|
+
def do_message_text(line)
|
234
|
+
if line == '.'
|
235
|
+
return terminate_message
|
236
|
+
end
|
237
|
+
|
238
|
+
if line[0..0] == '.'
|
239
|
+
line = line[1..-1]
|
240
|
+
end
|
241
|
+
|
242
|
+
@message_text << line
|
243
|
+
return nil
|
244
|
+
end
|
245
|
+
|
246
|
+
def terminate_message
|
247
|
+
sender = @envelope.sender
|
248
|
+
recipients = @envelope.recipients
|
249
|
+
text = @message_text
|
250
|
+
@envelope = nil
|
251
|
+
@message_text = nil
|
252
|
+
|
253
|
+
ids = []
|
254
|
+
recipients.each do | recipient |
|
255
|
+
ids << process_message(sender, recipient, text)
|
256
|
+
end
|
257
|
+
return "#{RESULT_OK} #{ids.join(';')}"
|
258
|
+
end
|
259
|
+
|
260
|
+
def process_message(sender, recipient, text)
|
261
|
+
received = "Received: from #{@helo} (#{@sender_ip}) " +
|
262
|
+
"by #{@hostname} " +
|
263
|
+
"for <#{recipient}> " +
|
264
|
+
"at #{Time.now.to_s}"
|
265
|
+
full_text = received + NEWLINE + text.join(NEWLINE)
|
266
|
+
|
267
|
+
filterable_data = apply_filters(sender, recipient)
|
268
|
+
id = @mail_handler.process_message(filterable_data.recipient, full_text)
|
269
|
+
return id
|
270
|
+
end
|
271
|
+
|
272
|
+
def enforce_no_args(args, error_text)
|
273
|
+
if ! args.empty?
|
274
|
+
raise_smtp_error(error_text)
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
def enforce_host_arg(args, error_text)
|
279
|
+
args = args.split(/\s+/)
|
280
|
+
if args.size != 1
|
281
|
+
raise_smtp_error(error_text)
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
def enforce_address_arg(args, keyword, error_text)
|
286
|
+
re = /#{keyword}:\s*(.+)/i
|
287
|
+
match = re.match(args)
|
288
|
+
if !match
|
289
|
+
raise_smtp_error(error_text)
|
290
|
+
end
|
291
|
+
candidate = match[1].strip
|
292
|
+
strip_brackets = /<(.*)>/
|
293
|
+
match = strip_brackets.match(candidate)
|
294
|
+
if(match)
|
295
|
+
candidate = match[1]
|
296
|
+
end
|
297
|
+
|
298
|
+
return candidate
|
299
|
+
end
|
300
|
+
|
301
|
+
def extract_args(line)
|
302
|
+
command, args = line.split(nil, 2)
|
303
|
+
if command.nil?
|
304
|
+
command = ''
|
305
|
+
end
|
306
|
+
if args.nil?
|
307
|
+
args = ''
|
308
|
+
end
|
309
|
+
return command, args
|
310
|
+
end
|
311
|
+
|
312
|
+
def raise_smtp_error(text)
|
313
|
+
raise SMTPError.new(text)
|
314
|
+
end
|
315
|
+
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
module MailDiode
|
320
|
+
|
321
|
+
# SMTP errors (not success results)
|
322
|
+
class SMTPError < RuntimeError
|
323
|
+
attr_reader :text
|
324
|
+
|
325
|
+
def initialize(text)
|
326
|
+
@text = text
|
327
|
+
end
|
328
|
+
|
329
|
+
MESSAGE_NOT_HANDLED = "451 Failed: message not handled"
|
330
|
+
SYNTAX_NOOP = "501 Syntax: NOOP"
|
331
|
+
SYNTAX_QUIT = "501 Syntax: QUIT"
|
332
|
+
SYNTAX_HELO = "501 Syntax: HELO"
|
333
|
+
SYNTAX_RSET = "501 Syntax: RSET"
|
334
|
+
SYNTAX_VRFY = "501 Syntax: VRFY"
|
335
|
+
SYNTAX_MAIL = "501 Syntax: MAIL"
|
336
|
+
SYNTAX_RCPT = "501 Syntax: RCPT"
|
337
|
+
SYNTAX_DATA = "501 Syntax: DATA"
|
338
|
+
BAD_COMMAND = '502 Error: command not implemented'
|
339
|
+
TOO_MANY_RECIPIENTS = '552 Error: Too many recipients'
|
340
|
+
NEED_MAIL_BEFORE_RCPT = '503 Error: need MAIL before RCPT'
|
341
|
+
NEED_RCPT_BEFORE_DATA = '503 Error: need RCPT before DATA'
|
342
|
+
UNKNOWN_RECIPIENT = "550 Error: No such recipient here"
|
343
|
+
end
|
344
|
+
end
|
data/lib/greylist.rb
ADDED
@@ -0,0 +1,137 @@
|
|
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
|
+
require 'kirbybase'
|
17
|
+
|
18
|
+
ONE_HOUR_IN_SECONDS = 60*60
|
19
|
+
|
20
|
+
module MailDiode
|
21
|
+
class GreylistFilter < Filter
|
22
|
+
# Traditional (client IP, sender address, recipient address)
|
23
|
+
# or selective (client IP, HELO name, sender domain)?
|
24
|
+
GREYLIST_ADDED = "450 You are being put on a waiting list--please try again later"
|
25
|
+
GREYLIST_WAITING = "450 You are still on the waiting list--please try later"
|
26
|
+
GREYLIST_DELAY_IN_SECONDS = 60
|
27
|
+
|
28
|
+
def initialize(settings)
|
29
|
+
@delay_minutes = 60
|
30
|
+
load_settings(settings)
|
31
|
+
@kirby = KirbyBase.new(:local, nil, nil, '/var/local/maildiode/')
|
32
|
+
ensure_database_table_exists
|
33
|
+
end
|
34
|
+
|
35
|
+
def delay_seconds
|
36
|
+
@settings.delay_minutes * 60
|
37
|
+
end
|
38
|
+
|
39
|
+
def process(filterable_data)
|
40
|
+
ip = filterable_data.sender_ip
|
41
|
+
helo = filterable_data.helo
|
42
|
+
from = filterable_data.sender
|
43
|
+
to = filterable_data.recipient
|
44
|
+
|
45
|
+
if is_bounce_message(from)
|
46
|
+
MailDiode::log_info "Greylist allowing bounce message"
|
47
|
+
return true
|
48
|
+
end
|
49
|
+
from_user, from_domain = from.split('@')
|
50
|
+
table = get_database_table
|
51
|
+
found = table.select { | r | (r.ip == ip && r.helo == helo && r.from_domain = from_domain) }
|
52
|
+
case found.size
|
53
|
+
when 0
|
54
|
+
record = create_record(ip, helo, from_user, from_domain, to)
|
55
|
+
MailDiode::log_info "Greylist added #{key(record)}"
|
56
|
+
raise SMTPError.new(GREYLIST_ADDED)
|
57
|
+
when 1
|
58
|
+
if !check_existing_record(found[0])
|
59
|
+
raise SMTPError.new(GREYLIST_WAITING)
|
60
|
+
end
|
61
|
+
else
|
62
|
+
MailDiode::log_warning "Greylist found multiple entries for #{ip};#{helo};#{from};#{to}"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def is_bounce_message(from)
|
67
|
+
return from.empty?
|
68
|
+
end
|
69
|
+
|
70
|
+
def key(record)
|
71
|
+
return "#{record.ip};#{record.helo};#{record.from_domain}"
|
72
|
+
end
|
73
|
+
|
74
|
+
def create_record(ip, helo, from_user, from_domain, to)
|
75
|
+
recno = get_database_table.insert do |r|
|
76
|
+
r.ip = ip
|
77
|
+
r.helo = helo
|
78
|
+
r.from_user = from_user
|
79
|
+
r.from_domain = from_domain
|
80
|
+
r.to = to
|
81
|
+
r.first_seen = now
|
82
|
+
r.last_seen = now
|
83
|
+
r.approved = false
|
84
|
+
record = r
|
85
|
+
end
|
86
|
+
return get_database_table.select { | r | r.recno == recno }
|
87
|
+
end
|
88
|
+
|
89
|
+
def check_existing_record(record)
|
90
|
+
if(record.approved)
|
91
|
+
MailDiode::log_info "Greylist approved"
|
92
|
+
return true
|
93
|
+
end
|
94
|
+
|
95
|
+
table = get_database_table
|
96
|
+
delay_until = record.first_seen + delay_seconds
|
97
|
+
if now > delay_until
|
98
|
+
table.update(:approved => true) { | r | r.recno == record.recno }
|
99
|
+
MailDiode::log_info "Greylist approved #{key(record)}"
|
100
|
+
return true
|
101
|
+
end
|
102
|
+
|
103
|
+
table.update(:last_seen => now) { | r | r.recno == record.recno }
|
104
|
+
MailDiode::log_info "Greylist delaying #{key(record)} until #{Time.at(delay_until)}"
|
105
|
+
return false
|
106
|
+
end
|
107
|
+
|
108
|
+
def now
|
109
|
+
return Time.now.to_i
|
110
|
+
end
|
111
|
+
|
112
|
+
def get_database_table
|
113
|
+
return @kirby.get_table(:greylist)
|
114
|
+
end
|
115
|
+
|
116
|
+
def ensure_database_table_exists
|
117
|
+
if @kirby.table_exists?(:greylist)
|
118
|
+
return
|
119
|
+
end
|
120
|
+
MailDiode::log_info "Creating greylist database"
|
121
|
+
@kirby.create_table(:greylist,
|
122
|
+
:ip, :String,
|
123
|
+
:helo, :String,
|
124
|
+
:from_user, :String,
|
125
|
+
:from_domain, :String,
|
126
|
+
:to, :String,
|
127
|
+
:first_seen, :Integer,
|
128
|
+
:last_seen, :Integer,
|
129
|
+
:approved, :Boolean
|
130
|
+
)
|
131
|
+
end
|
132
|
+
|
133
|
+
def load_settings(settings)
|
134
|
+
@delay_minutes = settings.get_setting('greylist', 'delayminutes').to_i
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
data/lib/maildir.rb
ADDED
@@ -0,0 +1,113 @@
|
|
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
|
+
require 'gurgitate/deliver'
|
17
|
+
|
18
|
+
module MailDiode
|
19
|
+
class FolderMissingError < IOError
|
20
|
+
attr_reader :folder
|
21
|
+
|
22
|
+
def initialize(folder_name)
|
23
|
+
@folder = folder_name
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class DeliverableMessage
|
28
|
+
include Gurgitate::Deliver::Maildir
|
29
|
+
|
30
|
+
def initialize(message_content)
|
31
|
+
@message = message_content
|
32
|
+
end
|
33
|
+
|
34
|
+
def save(maildir)
|
35
|
+
deliver_message(maildir)
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_s
|
39
|
+
@message
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class MaildirMessageHandler < MailHandler
|
44
|
+
def initialize(settings)
|
45
|
+
@maildirs = {}
|
46
|
+
load_settings(settings)
|
47
|
+
end
|
48
|
+
|
49
|
+
def valid_recipient?(recipient)
|
50
|
+
return get_maildir(recipient)
|
51
|
+
end
|
52
|
+
|
53
|
+
def process_message(recipient, full_text)
|
54
|
+
message = DeliverableMessage.new(full_text)
|
55
|
+
raw_dir = get_maildir(recipient)
|
56
|
+
folder = ''
|
57
|
+
dir = "#{raw_dir.chomp('/')}#{folder}/"
|
58
|
+
MailDiode::log_info "Delivering to #{dir}"
|
59
|
+
if !File.exist?(dir)
|
60
|
+
MailDiode::log_error "ERROR: Missing maildir folder: #{dir}"
|
61
|
+
raise FolderMissingError.new(File.basename(dir))
|
62
|
+
end
|
63
|
+
message.save(dir)
|
64
|
+
end
|
65
|
+
|
66
|
+
def get_maildir(recipient)
|
67
|
+
return @maildirs[recipient]
|
68
|
+
end
|
69
|
+
=begin
|
70
|
+
def valid_recipient?(recipient)
|
71
|
+
if (get_maildir(recipient))
|
72
|
+
return true
|
73
|
+
end
|
74
|
+
# @core.log.debug "Rejecting recipient: #{recipient}"
|
75
|
+
return false
|
76
|
+
end
|
77
|
+
|
78
|
+
def deliver_message(recipient, envelope_to, folder, raw_message_object)
|
79
|
+
received = "Received: from #{raw_message_object.heloname} " +
|
80
|
+
"(#{raw_message_object.origin_ip}) " +
|
81
|
+
"by #{raw_message_object.myhostname} " +
|
82
|
+
"with SMTP ID #{raw_message_object.smtp_id} " +
|
83
|
+
"for <#{recipient}>; " +
|
84
|
+
#"#{raw_message_object.timestamp.to_s}\n"
|
85
|
+
"#{Time.now.to_s}\n"
|
86
|
+
full_text = received + raw_message_object.content.gsub("\r","")
|
87
|
+
message = DeliverableMessage.new(full_text)
|
88
|
+
dir = "#{get_maildir(recipient).chomp('/')}#{folder}/"
|
89
|
+
# @core.log.info "Delivering for #{envelope_to} to #{dir}"
|
90
|
+
if !File.exist?(dir)
|
91
|
+
# @core.log.error "ERROR: Missing maildir folder: #{dir}"
|
92
|
+
raise FolderMissingError.new(File.basename(dir))
|
93
|
+
end
|
94
|
+
message.save(dir)
|
95
|
+
|
96
|
+
return true
|
97
|
+
end
|
98
|
+
|
99
|
+
=end
|
100
|
+
|
101
|
+
def load_settings(settings)
|
102
|
+
settings.get_settings('maildir').each do | setting |
|
103
|
+
name, maildir = setting
|
104
|
+
if(maildir.index('/') != 0)
|
105
|
+
raise "Maildir must start with slash (/): #{maildir}"
|
106
|
+
end
|
107
|
+
maildir.untaint
|
108
|
+
@maildirs[name] = maildir
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
data/lib/server.rb
ADDED
@@ -0,0 +1,93 @@
|
|
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
|
+
require 'gserver'
|
17
|
+
require 'engine'
|
18
|
+
require 'io/wait'
|
19
|
+
|
20
|
+
module MailDiode
|
21
|
+
class Server < GServer
|
22
|
+
def initialize(engine_factory, settings)
|
23
|
+
load_settings(settings)
|
24
|
+
|
25
|
+
@factory = engine_factory
|
26
|
+
super(@port, @ip, @max_connections)
|
27
|
+
end
|
28
|
+
|
29
|
+
def serve(client)
|
30
|
+
begin
|
31
|
+
MailDiode::log_debug("Accepting client")
|
32
|
+
MailDiode::log_info("Connections: #{connections}")
|
33
|
+
client.binmode
|
34
|
+
engine = @factory.create_engine(@hostname)
|
35
|
+
family, port, hostname, ip = client.addr
|
36
|
+
greeting = engine.start(ip)
|
37
|
+
MailDiode::log_info("Greeting: #{greeting}")
|
38
|
+
client << response(greeting)
|
39
|
+
while !engine.terminate?
|
40
|
+
line = get_line(client, TIMEOUT_SECONDS)
|
41
|
+
if(!line)
|
42
|
+
MailDiode::log_info("---client disconnected")
|
43
|
+
return
|
44
|
+
end
|
45
|
+
line.chomp!(NEWLINE)
|
46
|
+
result = engine.process_line(line)
|
47
|
+
if(result)
|
48
|
+
discard_early_input(client)
|
49
|
+
client << response(result)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
rescue Errno::ECONNRESET
|
53
|
+
MailDiode::log_info "---client dropped connection"
|
54
|
+
rescue Exception => e
|
55
|
+
MailDiode::log_error("Exception #{e.class}: #{e}")
|
56
|
+
e.backtrace.each do | line |
|
57
|
+
MailDiode::log_error(line)
|
58
|
+
end
|
59
|
+
client << response("451 Internal Server Error")
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def get_line(input, timeout_seconds)
|
64
|
+
line = nil
|
65
|
+
t = Thread.new { line = input.gets(NEWLINE) }
|
66
|
+
t.join(timeout_seconds)
|
67
|
+
return line
|
68
|
+
end
|
69
|
+
|
70
|
+
def discard_early_input(client)
|
71
|
+
while(client.ready?)
|
72
|
+
client.getc
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def response(text)
|
77
|
+
return "#{text}#{NEWLINE}"
|
78
|
+
end
|
79
|
+
|
80
|
+
def load_settings(settings)
|
81
|
+
@hostname = settings.get_setting('server', 'hostname')
|
82
|
+
@ip = settings.get_setting('server', 'ip')
|
83
|
+
@ip.untaint
|
84
|
+
@port = settings.get_int('server', 'port', DEFAULT_PORT)
|
85
|
+
@port.untaint
|
86
|
+
@max_connections = settings.get_int('server', 'maxconnections', DEFAULT_MAX_CONNECTIONS)
|
87
|
+
end
|
88
|
+
|
89
|
+
DEFAULT_PORT = 10025
|
90
|
+
DEFAULT_MAX_CONNECTIONS = 20
|
91
|
+
TIMEOUT_SECONDS = 1200 # djb recommends 1200
|
92
|
+
end
|
93
|
+
end
|