eventmachine-email_server 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/README.md +23 -21
- data/email_server.sqlite3 +0 -0
- data/eventmachine-email_server.gemspec +4 -0
- data/lib/eventmachine/email_server.rb +1 -0
- data/lib/eventmachine/email_server/classifier.rb +38 -0
- data/lib/eventmachine/email_server/smtp_server.rb +31 -2
- data/lib/eventmachine/email_server/version.rb +1 -1
- data/test/test_email_server.rb +69 -14
- metadata +60 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a217dda0576da20c2bace6cd1320f6bc6662a052
|
4
|
+
data.tar.gz: cf0f814c5b56a830300d89daeff7e43a457017bd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 03eb0a117e26b162c41b5a9068687d4224ca7fe39b97378004aab43f81aeb2551b3ee49bd97f346491470fc5c6ef27b0d8133052e5c28e3fa1c8a5a1208eb0a4
|
7
|
+
data.tar.gz: 6c091d1cc691535298fd2f296ac493a46cd3dacbd968a95080ea34ed25aeecb5ecc6804dc2ac836bd39c17524243ce55154dd5899a1cfb57122c9231388f9573
|
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -1,13 +1,24 @@
|
|
1
1
|
# EventMachine::EmailServer
|
2
2
|
|
3
|
-
This provides an EventMachine-based implementation of POP3 and SMTP services--primarily for use within the Rubot framework. However, as I add features, this might come in handy for other people as well.
|
3
|
+
This provides an EventMachine-based implementation of POP3 and SMTP services--primarily for use within the Rubot framework. However, as I add features, this might come in handy for other people as well (might make a good spamtrap).
|
4
4
|
|
5
|
-
There are several email and user backends so that the POP3 and SMTP servers can share: Memory, Sqlite3, and Null.
|
5
|
+
There are several email and user backends so that the POP3 and SMTP servers can share: Memory, Sqlite3, and Null. (In the future, I plan to move Sqlite3 support into an external module and add filesystem storage as an external module as well.)
|
6
6
|
|
7
|
-
The SMTP server currently only receives mail as an end-host (no relay or sending) and does no fancy routing of email (e.g., aliases and procmail). It does, however, have graylisting, DNS PTR checks, DNSBL checks, rate limiting,
|
7
|
+
The SMTP server currently only receives mail as an end-host (no relay or sending) and does no fancy routing of email (e.g., aliases and procmail). It does, however, have graylisting, DNS PTR checks, DNSBL checks, rate limiting, SPF checking, simple filters, and a baysian classifier.
|
8
8
|
|
9
|
-
Writing a full-featured mail server is a multi-year, multi-person project. I would need some help.
|
9
|
+
Writing a full-featured mail server is a multi-year, multi-person project. I would need some help. If you're interested, let me know.
|
10
10
|
|
11
|
+
Potential features if people request them:
|
12
|
+
|
13
|
+
* SSL, StartTLS, Peer authentication (medium)
|
14
|
+
* Cram-MD5-based authentication (medium)
|
15
|
+
* IMAP (hard)
|
16
|
+
* Create a launcher in bin/ and parse configuration files (easy)
|
17
|
+
* Domain-Keys (hard, unless I find a lib that does it)
|
18
|
+
* Relay (this is easy to implement, but hard to get right)
|
19
|
+
* Aliases (easy)
|
20
|
+
* Logging (easiest)
|
21
|
+
|
11
22
|
## Installation
|
12
23
|
|
13
24
|
Add this line to your application's Gemfile:
|
@@ -43,10 +54,10 @@ Everything turned on:
|
|
43
54
|
require 'dnsbl/client'
|
44
55
|
require 'sqlite3'
|
45
56
|
|
46
|
-
s = SQLite3::Database.new("
|
57
|
+
s = SQLite3::Database.new("email_server.sqlite3")
|
47
58
|
userstore = Sqlite3UserStore.new(s)
|
48
59
|
emailstore = Sqlite3EmailStore.new(s)
|
49
|
-
userstore << User.new(1, "chris", "
|
60
|
+
userstore << User.new(1, "chris", "password", "chris@example.org")
|
50
61
|
|
51
62
|
config = {
|
52
63
|
'default' => RateLimit::Config.new('default', 2, 2, -2, 1, 1, 1),
|
@@ -54,13 +65,17 @@ Everything turned on:
|
|
54
65
|
storage = RateLimit::Memory.new
|
55
66
|
rl = RateLimit::BucketBased.new(storage, config, 'default')
|
56
67
|
|
57
|
-
|
68
|
+
classifier = EventMachine::EmailServer::Classifier.new("test/test.classifier", [:spam, :ham], [:spam])
|
69
|
+
classifier.train(:spam, "Amazing pillz viagra cialis levitra staxyn")
|
70
|
+
classifier.train(:ham, "Big pigs make great bacon")
|
58
71
|
|
59
72
|
SMTPServer.reverse_ptr_check(true)
|
60
73
|
SMTPServer.graylist(Hash.new)
|
61
74
|
SMTPServer.ratelimiter(rl)
|
62
|
-
SMTPServer.
|
75
|
+
SMTPServer.dnsbl_check(true)
|
76
|
+
SMTPServer.spf_check(true)
|
63
77
|
SMTPServer.reject_filters << /viagra/i
|
78
|
+
SMTPServer.classifier(classifier)
|
64
79
|
|
65
80
|
EM.run {
|
66
81
|
pop3 = EventMachine::start_server "0.0.0.0", 2110, POP3Server, "example.org", userstore, emailstore
|
@@ -70,19 +85,6 @@ Everything turned on:
|
|
70
85
|
|
71
86
|
## Contributing
|
72
87
|
|
73
|
-
If we want this to be "professional":
|
74
|
-
*EventMachine-based SPF Checking
|
75
|
-
*EventMachine-based DNSBL::Client
|
76
|
-
*Abstract filtering into a class with a callback so that all sorts of filtering (e.g., baysian) could be done
|
77
|
-
*Create a launcher in bin/ and parse configuration files
|
78
|
-
*StartTLS
|
79
|
-
*SSL
|
80
|
-
*CRAM-MD5
|
81
|
-
*Domain-Keys
|
82
|
-
*Relay (this is easy to implement, but hard to get right)
|
83
|
-
*Aliases
|
84
|
-
*Logging
|
85
|
-
|
86
88
|
1. Fork it ( https://github.com/[my-github-username]/eventmachine-email_server/fork )
|
87
89
|
2. Create your feature branch (`git checkout -b my-new-feature`)
|
88
90
|
3. Commit your changes (`git commit -am 'Add some feature'`)
|
Binary file
|
@@ -23,6 +23,10 @@ Gem::Specification.new do |spec|
|
|
23
23
|
spec.add_runtime_dependency "ratelimit-bucketbased", ">= 0.0.1"
|
24
24
|
spec.add_runtime_dependency "eventmachine-dnsbl", ">= 0.0.2"
|
25
25
|
spec.add_runtime_dependency "spf", ">= 0.0.44"
|
26
|
+
spec.add_runtime_dependency "classifier", ">= 1.3.4"
|
27
|
+
spec.add_runtime_dependency "rb-gsl", ">= 1.16.0.4"
|
28
|
+
spec.add_runtime_dependency "fast-stemmer", ">= 1.0.2"
|
29
|
+
spec.add_runtime_dependency "madeleine", ">= 0.9.0"
|
26
30
|
spec.add_development_dependency "minitest", "~> 5.5"
|
27
31
|
spec.add_development_dependency "bundler", "~> 1.6"
|
28
32
|
spec.add_development_dependency "rake", "~> 10.0"
|
@@ -3,6 +3,7 @@ require 'eventmachine/email_server/base'
|
|
3
3
|
require 'eventmachine/email_server/memory'
|
4
4
|
require 'eventmachine/email_server/null'
|
5
5
|
require 'eventmachine/email_server/sqlite3'
|
6
|
+
require 'eventmachine/email_server/classifier'
|
6
7
|
require 'eventmachine/email_server/pop3_server'
|
7
8
|
require 'eventmachine/email_server/smtp_server'
|
8
9
|
require 'eventmachine/email_server/eventmachine_dns_monkeypatch'
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
require 'classifier'
|
3
|
+
require 'madeleine'
|
4
|
+
|
5
|
+
module EventMachine
|
6
|
+
module EmailServer
|
7
|
+
class Classifier
|
8
|
+
def initialize(datafile, categories, blocked_categories)
|
9
|
+
@categories = categories || [:good, :bad]
|
10
|
+
@blocked_categories = blocked_categories || [:bad]
|
11
|
+
@categories.map! { |c| c.prepare_category_name.to_s }
|
12
|
+
@blocked_categories.map! { |c| c.prepare_category_name.to_s }
|
13
|
+
@madeleine = SnapshotMadeleine.new(datafile) {
|
14
|
+
::Classifier::Bayes.new(*categories)
|
15
|
+
}
|
16
|
+
@classifier = @madeleine.system
|
17
|
+
end
|
18
|
+
|
19
|
+
def train(category, email)
|
20
|
+
@classifier.train(category, email)
|
21
|
+
@madeleine.take_snapshot
|
22
|
+
end
|
23
|
+
|
24
|
+
def classify(email)
|
25
|
+
@classifier.classify(email)
|
26
|
+
end
|
27
|
+
|
28
|
+
def block?(email)
|
29
|
+
c = classify(email)
|
30
|
+
if @blocked_categories.index(c).nil?
|
31
|
+
return false
|
32
|
+
end
|
33
|
+
true
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
@@ -11,6 +11,7 @@ module EventMachine
|
|
11
11
|
@@reverse_ptr_check = false
|
12
12
|
@@spf_check = false
|
13
13
|
@@reject_filters = Array.new
|
14
|
+
@@classifier = nil
|
14
15
|
|
15
16
|
def self.reset
|
16
17
|
@@graylist = nil
|
@@ -19,6 +20,7 @@ module EventMachine
|
|
19
20
|
@@reverse_ptr_check = false
|
20
21
|
@@spf_check = false
|
21
22
|
@@reject_filters = Array.new
|
23
|
+
@classifier = nil
|
22
24
|
end
|
23
25
|
|
24
26
|
def self.reverse_ptr_check(ptr=nil)
|
@@ -61,7 +63,14 @@ module EventMachine
|
|
61
63
|
@@spf_check = spf
|
62
64
|
end
|
63
65
|
@@spf_check
|
64
|
-
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.classifier(classifier=nil)
|
69
|
+
if not classifier.nil?
|
70
|
+
@@classifier = classifier
|
71
|
+
end
|
72
|
+
@@classifier
|
73
|
+
end
|
65
74
|
|
66
75
|
def initialize(hostname, userstore, emailstore)
|
67
76
|
@hostname = hostname
|
@@ -74,7 +83,9 @@ module EventMachine
|
|
74
83
|
@dnsbl_ok = true
|
75
84
|
@rate_ok = true
|
76
85
|
@gray_ok = true
|
86
|
+
@spf_ok = true
|
77
87
|
@reject_ok = true
|
88
|
+
@classifier_ok = true
|
78
89
|
@pending_checks = Array.new
|
79
90
|
end
|
80
91
|
|
@@ -145,6 +156,14 @@ module EventMachine
|
|
145
156
|
end
|
146
157
|
end
|
147
158
|
|
159
|
+
def check_classifier
|
160
|
+
if @@classifier
|
161
|
+
if @@classifier.block?(@email_body)
|
162
|
+
@classifier_ok = false
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
148
167
|
def check_spf(helo, client_ip, identity)
|
149
168
|
if @@spf_check
|
150
169
|
@spf_ok = false
|
@@ -196,7 +215,16 @@ module EventMachine
|
|
196
215
|
end
|
197
216
|
|
198
217
|
def send_answer
|
199
|
-
if @
|
218
|
+
if @debug
|
219
|
+
puts "ptr_ok = #{@ptr_ok}"
|
220
|
+
puts "rate_ok = #{@rate_ok}"
|
221
|
+
puts "gray_ok = #{@gray_ok}"
|
222
|
+
puts "reject_ok = #{@reject_ok}"
|
223
|
+
puts "dnsbl_ok = #{@dnsbl_ok}"
|
224
|
+
puts "spf_ok = #{@spf_ok}"
|
225
|
+
puts "classifier_ok = #{@classifier_ok}"
|
226
|
+
end
|
227
|
+
if @ptr_ok and @rate_ok and @gray_ok and @reject_ok and @dnsbl_ok and @spf_ok and @classifier_ok
|
200
228
|
ans = "250 OK"
|
201
229
|
else
|
202
230
|
ans = "451 Requested action aborted: local error in processing"
|
@@ -209,6 +237,7 @@ module EventMachine
|
|
209
237
|
if (@data_mode) && (line.chomp =~ /^\.$/)
|
210
238
|
@data_mode = false
|
211
239
|
check_reject
|
240
|
+
check_classifier
|
212
241
|
if @pending_checks.length == 0
|
213
242
|
send_answer
|
214
243
|
end
|
data/test/test_email_server.rb
CHANGED
@@ -27,21 +27,19 @@ class TestEmailServer < Minitest::Test
|
|
27
27
|
@test_vector = Proc.new { |test_name|
|
28
28
|
(test_name.to_s =~ /test/)
|
29
29
|
}
|
30
|
-
|
31
|
-
File.unlink("test/test.sqlite3")
|
32
|
-
end
|
33
|
-
if File.exist?("email_server.sqlite3")
|
34
|
-
File.unlink("email_server.sqlite3")
|
35
|
-
end
|
30
|
+
remove_scraps
|
36
31
|
end
|
37
32
|
|
38
|
-
def
|
39
|
-
|
40
|
-
File.
|
41
|
-
|
42
|
-
|
43
|
-
File.unlink("email_server.sqlite3")
|
33
|
+
def remove_scraps
|
34
|
+
["test.sqlite3", "email_server.sqlite3"].each do |f|
|
35
|
+
if File.exist?("test/#{f}")
|
36
|
+
File.unlink("test/#{f}")
|
37
|
+
end
|
44
38
|
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def teardown
|
42
|
+
remove_scraps
|
45
43
|
end
|
46
44
|
|
47
45
|
def setup_user(userstore)
|
@@ -69,6 +67,38 @@ Looks like we had fun!
|
|
69
67
|
end
|
70
68
|
end
|
71
69
|
|
70
|
+
def send_spam(expected_status="451")
|
71
|
+
from = "friend@example.org"
|
72
|
+
to = "chris@example.org"
|
73
|
+
msg = "From: friend@example.org
|
74
|
+
To: chris@example.org
|
75
|
+
Subject: What to do when you're not doing.
|
76
|
+
|
77
|
+
Could I interest you in some cialis?
|
78
|
+
"
|
79
|
+
Thread.new do
|
80
|
+
smtp = Net::SMTP.start('localhost', 2025)
|
81
|
+
ret = smtp.send_message msg, from, to
|
82
|
+
assert_equal(expected_status, ret.status)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def send_ham(expected_status="250")
|
87
|
+
from = "friend@example.org"
|
88
|
+
to = "chris@example.org"
|
89
|
+
msg = "From: friend@example.org
|
90
|
+
To: chris@example.org
|
91
|
+
Subject: Good show
|
92
|
+
|
93
|
+
Have you seen the latest Peppa Pig?
|
94
|
+
"
|
95
|
+
Thread.new do
|
96
|
+
smtp = Net::SMTP.start('localhost', 2025)
|
97
|
+
ret = smtp.send_message msg, from, to
|
98
|
+
assert_equal(expected_status, ret.status)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
72
102
|
def pop_some_email
|
73
103
|
Thread.new do
|
74
104
|
pop = Net::POP3.APOP(true).new('localhost',2110)
|
@@ -277,6 +307,28 @@ Looks like we had fun!
|
|
277
307
|
}
|
278
308
|
|
279
309
|
end
|
310
|
+
|
311
|
+
def test_classifier
|
312
|
+
return unless @test_vector.call(__method__)
|
313
|
+
userstore = MemoryUserStore.new
|
314
|
+
emailstore = MemoryEmailStore.new
|
315
|
+
setup_user(userstore)
|
316
|
+
SMTPServer.reset
|
317
|
+
classifier = EventMachine::EmailServer::Classifier.new("test/test.classifier", [:spam, :ham], [:spam])
|
318
|
+
classifier.train(:spam, "Amazing pillz viagra cialis levitra staxyn")
|
319
|
+
classifier.train(:ham, "Big pigs make great bacon")
|
320
|
+
SMTPServer.classifier(classifier)
|
321
|
+
EM.run {
|
322
|
+
smtp = EventMachine::start_server "0.0.0.0", 2025, SMTPServer, "example.org", userstore, emailstore
|
323
|
+
timer = EventMachine::Timer.new(0.1) do
|
324
|
+
send_spam("451")
|
325
|
+
send_ham("250")
|
326
|
+
end
|
327
|
+
timer2 = EventMachine::Timer.new(1) do
|
328
|
+
EM.stop
|
329
|
+
end
|
330
|
+
}
|
331
|
+
end
|
280
332
|
|
281
333
|
def test_example
|
282
334
|
return unless @test_vector.call(__method__)
|
@@ -297,13 +349,18 @@ Looks like we had fun!
|
|
297
349
|
storage = RateLimit::Memory.new
|
298
350
|
rl = RateLimit::BucketBased.new(storage, config, 'default')
|
299
351
|
|
352
|
+
classifier = EventMachine::EmailServer::Classifier.new("test/test.classifier", [:spam, :ham], [:spam])
|
353
|
+
classifier.train(:spam, "Amazing pillz viagra cialis levitra staxyn")
|
354
|
+
classifier.train(:ham, "Big pigs make great bacon")
|
300
355
|
|
301
356
|
SMTPServer.reset
|
302
357
|
SMTPServer.reverse_ptr_check(true)
|
303
358
|
SMTPServer.graylist(Hash.new)
|
304
359
|
SMTPServer.ratelimiter(rl)
|
305
360
|
SMTPServer.dnsbl_check(true)
|
361
|
+
SMTPServer.spf_check(true)
|
306
362
|
SMTPServer.reject_filters << /viagra/i
|
363
|
+
SMTPServer.classifier(classifier)
|
307
364
|
|
308
365
|
EM.run {
|
309
366
|
pop3 = EventMachine::start_server "0.0.0.0", 2110, POP3Server, "example.org", userstore, emailstore
|
@@ -313,7 +370,5 @@ Looks like we had fun!
|
|
313
370
|
end
|
314
371
|
}
|
315
372
|
end
|
316
|
-
|
317
|
-
|
318
373
|
|
319
374
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: eventmachine-email_server
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- chrislee35
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-03-
|
11
|
+
date: 2015-03-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: eventmachine
|
@@ -80,6 +80,62 @@ dependencies:
|
|
80
80
|
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: 0.0.44
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: classifier
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 1.3.4
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 1.3.4
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rb-gsl
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 1.16.0.4
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 1.16.0.4
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: fast-stemmer
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: 1.0.2
|
118
|
+
type: :runtime
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: 1.0.2
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: madeleine
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: 0.9.0
|
132
|
+
type: :runtime
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: 0.9.0
|
83
139
|
- !ruby/object:Gem::Dependency
|
84
140
|
name: minitest
|
85
141
|
requirement: !ruby/object:Gem::Requirement
|
@@ -135,9 +191,11 @@ files:
|
|
135
191
|
- LICENSE.txt
|
136
192
|
- README.md
|
137
193
|
- Rakefile
|
194
|
+
- email_server.sqlite3
|
138
195
|
- eventmachine-email_server.gemspec
|
139
196
|
- lib/eventmachine/email_server.rb
|
140
197
|
- lib/eventmachine/email_server/base.rb
|
198
|
+
- lib/eventmachine/email_server/classifier.rb
|
141
199
|
- lib/eventmachine/email_server/eventmachine_dns_monkeypatch.rb
|
142
200
|
- lib/eventmachine/email_server/memory.rb
|
143
201
|
- lib/eventmachine/email_server/null.rb
|