eventmachine-email_server 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 45064a738c416e1fce2c8726b3bab01c10f55860
4
+ data.tar.gz: 25c163465275237d7caf9f48d257d20348a812c9
5
+ SHA512:
6
+ metadata.gz: cecf95b16f4d4e38cf83a65c2f4f9a69c5d4807b71a31a37fbc1cf4f723eb7476c95b7450adef582e8939019c5b4d6b96e8449a4070cb2ff0b78fa5b5bab4aa0
7
+ data.tar.gz: 3cb06186363faef276e41cd92ea2127938d1d1bd3109a2bcf50f343a3d844b021e8969bd4213d3e01976547b8e8b8292482d8157bbf440cc27e45380b38a2e93
data/.gitignore ADDED
@@ -0,0 +1,15 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
15
+ /test/*.sqlite3
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in eventmachine-email_server.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 chrislee35
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # EventMachine::EmailServer
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.
4
+
5
+ There are several email and user backends so that the POP3 and SMTP servers can share: Memory, Sqlite3, and Null.
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, and simple filters.
8
+
9
+ Writing a full-featured mail server is a multi-year, multi-person project. I would need some help.
10
+
11
+ ## Installation
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ ```ruby
16
+ gem 'eventmachine-email_server'
17
+ ```
18
+
19
+ And then execute:
20
+
21
+ $ bundle
22
+
23
+ Or install it yourself as:
24
+
25
+ $ gem install eventmachine-email_server
26
+
27
+ ## Usage
28
+
29
+ Simple usage:
30
+
31
+ require 'eventmachine/email_server'
32
+ include EventMachine::EmailServer
33
+ EM.run {
34
+ pop3 = EventMachine::start_server "0.0.0.0", 2110, POP3Server, "example.org", userstore, emailstore
35
+ smtp = EventMachine::start_server "0.0.0.0", 2025, SMTPServer, "example.org", userstore, emailstore
36
+ }
37
+
38
+ Everything turned on:
39
+
40
+ require 'eventmachine/email_server'
41
+ include EventMachine::EmailServer
42
+ require 'ratelimit/bucketbased'
43
+ require 'dnsbl/client'
44
+ require 'sqlite3'
45
+
46
+ s = SQLite3::Database.new("test/test.sqlite3")
47
+ userstore = Sqlite3UserStore.new(s)
48
+ emailstore = Sqlite3EmailStore.new(s)
49
+ userstore << User.new(1, "chris", "chris", "chris@example.org")
50
+
51
+ config = {
52
+ 'default' => RateLimit::Config.new('default', 2, 2, -2, 1, 1, 1),
53
+ }
54
+ storage = RateLimit::Memory.new
55
+ rl = RateLimit::BucketBased.new(storage, config, 'default')
56
+
57
+ dnsbl = DNSBL::Client.new
58
+
59
+ SMTPServer.reverse_ptr_check(true)
60
+ SMTPServer.graylist(Hash.new)
61
+ SMTPServer.ratelimiter(rl)
62
+ SMTPServer.dnsbl(dnsbl)
63
+ SMTPServer.reject_filters << /viagra/i
64
+
65
+ EM.run {
66
+ pop3 = EventMachine::start_server "0.0.0.0", 2110, POP3Server, "example.org", userstore, emailstore
67
+ smtp = EventMachine::start_server "0.0.0.0", 2025, SMTPServer, "example.org", userstore, emailstore
68
+ }
69
+
70
+
71
+ ## Contributing
72
+
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
+ 1. Fork it ( https://github.com/[my-github-username]/eventmachine-email_server/fork )
87
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
88
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
89
+ 4. Push to the branch (`git push origin my-new-feature`)
90
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+
4
+ require 'rake/testtask'
5
+
6
+ Rake::TestTask.new do |t|
7
+ t.libs << 'lib'
8
+ t.test_files = FileList['test/test_*.rb']
9
+ t.verbose = true
10
+ end
11
+
12
+ task :default => :test
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'eventmachine/email_server/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "eventmachine-email_server"
8
+ spec.version = EventMachine::EmailServer::VERSION
9
+ spec.authors = ["chrislee35"]
10
+ spec.email = ["rubygems@chrislee.dhs.org"]
11
+ spec.summary = %q{EventMachine-based implementations of a POP3 and SMTP server}
12
+ spec.description = %q{Simple POP3 and SMTP implementation in EventMachine for use in the Rubot framework}
13
+ spec.homepage = "https://github.com/chrislee35/eventmachine-email_server/"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_runtime_dependency "eventmachine", ">= 0.12.10"
22
+ spec.add_runtime_dependency "sqlite3", ">= 1.3.6"
23
+ spec.add_runtime_dependency "ratelimit-bucketbased", ">= 0.0.1"
24
+ spec.add_runtime_dependency "eventmachine-dnsbl", ">= 0.0.2"
25
+ spec.add_development_dependency "minitest", "~> 5.5"
26
+ spec.add_development_dependency "bundler", "~> 1.6"
27
+ spec.add_development_dependency "rake", "~> 10.0"
28
+ end
@@ -0,0 +1,66 @@
1
+ module EventMachine
2
+ module EmailServer
3
+ class User < Struct.new(:id,:username,:password,:address); end
4
+ class Email < Struct.new(:id,:from,:to,:subject,:body,:uid,:marked); end
5
+
6
+ class AbstractUserStore
7
+ def add_user(user)
8
+ raise "Unimplemented, please use a subclass of #{self.class}"
9
+ end
10
+
11
+ def <<(user)
12
+ add_user(user)
13
+ end
14
+
15
+ def delete_user(user)
16
+ raise "Unimplemented, please use a subclass of #{self.class}"
17
+ end
18
+
19
+ def -(user)
20
+ delete_user(user)
21
+ end
22
+
23
+ def user_by_username(username)
24
+ raise "Unimplemented, please use a subclass of #{self.class}"
25
+ end
26
+
27
+ def user_by_emailaddress(email)
28
+ raise "Unimplemented, please use a subclass of #{self.class}"
29
+ end
30
+ end
31
+
32
+ class AbstractEmailStore
33
+ def emails_by_userid(id)
34
+ raise "Unimplemented, please use a subclass of #{self.class}"
35
+ end
36
+
37
+ def save_email(email)
38
+ raise "Unimplemented, please use a subclass of #{self.class}"
39
+ end
40
+
41
+ def <<(email)
42
+ save_email(email)
43
+ end
44
+
45
+ def delete_email(email)
46
+ raise "Unimplemented, please use a subclass of #{self.class}"
47
+ end
48
+
49
+ def -(email)
50
+ delete_email(email)
51
+ end
52
+
53
+ def delete_id(id)
54
+ raise "Unimplemented, please use a subclass of #{self.class}"
55
+ end
56
+
57
+ def delete_user(uid)
58
+ raise "Unimplemented, please use a subclass of #{self.class}"
59
+ end
60
+
61
+ def count
62
+ raise "Unimplemented, please use a subclass of #{self.class}"
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,55 @@
1
+ module EventMachine
2
+ module DNS
3
+ class Resolver
4
+ def self.resolve(hostname, qclass=Resolv::DNS::Resource::IN::A)
5
+ Request.new(socket, hostname, qclass)
6
+ end
7
+ end
8
+ class Request
9
+ def initialize(socket, hostname, qclass)
10
+ @socket = socket
11
+ @hostname = hostname
12
+ @qclass = qclass
13
+ @tries = 0
14
+ @last_send = Time.at(0)
15
+ @retry_interval = 3
16
+ @max_tries = 5
17
+ if addrs = Resolver.hosts[hostname]
18
+ succeed addrs
19
+ else
20
+ EM.next_tick { tick }
21
+ end
22
+ end
23
+
24
+ def receive_answer(msg)
25
+ addrs = []
26
+ msg.each_answer do |name,ttl,data|
27
+ if data.respond_to? :address
28
+ addrs << data.address.to_s
29
+ elsif data.respond_to? :name
30
+ addrs << data.name.to_s
31
+ elsif data.respond_to? :strings
32
+ addrs << data.strings.join("\n")
33
+ else
34
+ addrs << data.to_s
35
+ end
36
+ end
37
+ if addrs.empty?
38
+ fail "rcode=#{msg.rcode}"
39
+ else
40
+ succeed addrs
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def packet
47
+ msg = Resolv::DNS::Message.new
48
+ msg.id = id
49
+ msg.rd = 1
50
+ msg.add_question @hostname, @qclass
51
+ msg
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,61 @@
1
+ require_relative 'base'
2
+
3
+ module EventMachine
4
+ module EmailServer
5
+ class MemoryUserStore < AbstractUserStore
6
+ def initialize
7
+ @users = Array.new
8
+ end
9
+
10
+ def add_user(user)
11
+ @users << user
12
+ end
13
+
14
+ def delete_user(user)
15
+ @users -= [user]
16
+ end
17
+
18
+ def user_by_username(username)
19
+ @users.find {|user| user.username == username}
20
+ end
21
+
22
+ def user_by_emailaddress(address)
23
+ @users.find {|user| user.address == address}
24
+ end
25
+
26
+ def user_by_id(id)
27
+ @users.find {|user| user.id == id}
28
+ end
29
+ end
30
+
31
+ class MemoryEmailStore < AbstractEmailStore
32
+ def initialize
33
+ @emails = Array.new
34
+ end
35
+
36
+ def emails_by_userid(uid)
37
+ @emails.find_all {|email| email.uid == uid}
38
+ end
39
+
40
+ def save_email(email)
41
+ @emails << email
42
+ end
43
+
44
+ def delete_email(email)
45
+ @emails -= [email]
46
+ end
47
+
48
+ def delete_id(id)
49
+ @emails.delete_if {|email| email.id == id}
50
+ end
51
+
52
+ def delete_user(uid)
53
+ @emails.delete_if {|email| email.uid == uid}
54
+ end
55
+
56
+ def count
57
+ @emails.length
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,56 @@
1
+ require_relative 'base'
2
+
3
+ module EventMachine
4
+ module EmailServer
5
+ class NullUserStore < AbstractUserStore
6
+ def initialize
7
+ @user = User.new(1,"null","null","null")
8
+ end
9
+
10
+ def add_user(user)
11
+ end
12
+
13
+ def delete_user(user)
14
+ end
15
+
16
+ def user_by_username(username)
17
+ u = @user.clone
18
+ u.userame = username
19
+ u
20
+ end
21
+
22
+ def user_by_emailaddress(address)
23
+ u = @user.clone
24
+ u.address = address
25
+ u
26
+ end
27
+
28
+ def user_by_id(id)
29
+ u = @user.clone
30
+ u.id = id
31
+ u
32
+ end
33
+ end
34
+
35
+ class NullEmailStore < AbstractEmailStore
36
+ def initialize
37
+ end
38
+
39
+ def emails_by_userid(uid)
40
+ []
41
+ end
42
+
43
+ def save_email(email)
44
+ end
45
+
46
+ def delete_email(email)
47
+ end
48
+
49
+ def delete_id(id)
50
+ end
51
+
52
+ def delete_user(uid)
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,250 @@
1
+ require 'eventmachine'
2
+ require 'digest/md5'
3
+
4
+ module EventMachine
5
+ module EmailServer
6
+ class POP3Server < EventMachine::Connection
7
+ @@capabilities = [ "TOP", "USER", "UIDL" ]
8
+ def initialize(hostname, userstore, emailstore)
9
+ @hostname = hostname
10
+ @userstore = userstore
11
+ @emailstore = emailstore
12
+ @state = 'auth' # 'trans' and 'data'
13
+ @auth_attempts = 0
14
+ @apop_challenge = "<#{rand(10**4 - 1)}.#{rand(10**9 - 1)}@#{@hostname}>"
15
+ @debug = true
16
+ @emails = Array.new
17
+ end
18
+
19
+ def post_init
20
+ puts ">> +OK POP3 server ready #{@apop_challenge}" if @debug
21
+ send_data("+OK POP3 server ready #{@apop_challenge}\r\n")
22
+ end
23
+
24
+ def receive_data(data)
25
+ puts ">> #{data}" if @debug
26
+ ok, op = process_line(data)
27
+ if ok
28
+ puts "<< #{op}" if @debug
29
+ send_data(op+"\r\n")
30
+ end
31
+ end
32
+
33
+ def unbind(reason=nil)
34
+ @emails.find_all {|e| e.marked}.each do |email|
35
+ @emailstore.delete_email(email)
36
+ puts "deleted #{email.id}" if @debug
37
+ end
38
+ end
39
+
40
+ def process_line(line)
41
+ line.chomp!
42
+ case @state
43
+ when 'auth'
44
+ case line
45
+ when /^QUIT$/
46
+ return false, "+OK dewey POP3 server signing off"
47
+ when /^CAPA$/
48
+ return true, "+OK Capability list follows\r\n"+@@capabilities.join("\r\n")+"\r\n."
49
+ when /^USER (.+)$/
50
+ user($1)
51
+ if @user
52
+ return true, "+OK #{@user.username} is most welcome here"
53
+ else
54
+ @failed += 1
55
+ if @failed > 2
56
+ return false, "-ERR you're out!"
57
+ end
58
+ return true, "-ERR sorry, no mailbox for #{$1} here"
59
+ end
60
+ when /^PASS (.+)$/
61
+ if pass($1)
62
+ @state = 'trans'
63
+ emails
64
+ msgs, bytes = stat
65
+ return true, "+OK #{@user.username}'s maildrop has #{msgs} messages (#{bytes} octets)"
66
+ else
67
+ @failed += 1
68
+ if @failed > 2
69
+ return false, "-ERR you're out!"
70
+ end
71
+ return true, "-ERR no dope."
72
+ end
73
+ when /^APOP ([^\s]+) (.+)$/
74
+ if apop($1,$2)
75
+ @state = 'trans'
76
+ emails
77
+ return true, "+OK #{@user.username} is most welcome here"
78
+ else
79
+ @failed += 1
80
+ if @failed > 2
81
+ return false, "-ERR you're out!"
82
+ end
83
+ return true, "-ERR sorry, no mailbox for #{$1} here"
84
+ end
85
+ end
86
+ when 'trans'
87
+ case line
88
+ when /^NOOP$/
89
+ return true, "+OK"
90
+ when /^STAT$/
91
+ msgs, bytes = stat
92
+ return true, "+OK #{msgs} #{bytes}"
93
+ when /^LIST$/
94
+ msgs, bytes = stat
95
+ msg = "+OK #{msgs} messages (#{bytes} octets)\r\n"
96
+ list.each do |num, bytes|
97
+ msg += "#{num} #{bytes}\r\n"
98
+ end
99
+ msg += "."
100
+ return true, msg
101
+ when /^LIST (\d+)$/
102
+ msgs, bytes = stat
103
+ num, bytes = list($1)
104
+ if num
105
+ return true, "+OK #{num} #{bytes}"
106
+ else
107
+ return true, "-ERR no such message, only #{msgs} messages in maildrop"
108
+ end
109
+ when /^RETR (\d+)$/
110
+ msg = retr($1)
111
+ if msg
112
+ msg = "+OK #{msg.length} octets\r\n" + msg + "\r\n."
113
+ else
114
+ msg = "-ERR no such message"
115
+ end
116
+ return true, msg
117
+ when /^DELE (\d+)$/
118
+ if dele($1)
119
+ return true, "+OK message #{$1} marked"
120
+ else
121
+ return true, "-ERR message #{$1} already marked"
122
+ end
123
+ when /^RSET$/
124
+ rset
125
+ msgs, bytes = stat
126
+ return true, "+OK maildrop has #{msgs} messages (#{bytes} octets)"
127
+ when /^QUIT$/
128
+ @state = 'update'
129
+ quit
130
+ msgs, bytes = stat
131
+ if msgs > 0
132
+ return true, "+OK dewey POP3 server signing off (#{msgs} messages left)"
133
+ else
134
+ return true, "+OK dewey POP3 server signing off (maildrop empty)"
135
+ end
136
+ when /^TOP (\d+) (\d+)$/
137
+ lines = $2
138
+ msg = retr($1)
139
+ unless msg
140
+ return true, "-ERR no such message"
141
+ end
142
+ cnt = nil
143
+ final = ""
144
+ msg.split(/\n/).each do |l|
145
+ final += l+"\n"
146
+ if cnt
147
+ cnt += 1
148
+ break if cnt > lines
149
+ end
150
+ if l !~ /\w/
151
+ cnt = 0
152
+ end
153
+ end
154
+ return true, "+OK\r\n"+final+"\r\n."
155
+ when /^UIDL$/
156
+ msgid = 0
157
+ msg = ''
158
+ @emails.each do |e|
159
+ msgid += 1
160
+ next if e.marked
161
+ msg += "#{msgid} #{Digest::MD5.new.update(msg).hexdigest}"
162
+ end
163
+ return true, "+OK\r\n#{msg}\r\n.";
164
+ end
165
+ when 'update'
166
+ case line
167
+ when /^QUIT$/
168
+ return true, "+OK dewey POP3 server signing off"
169
+ end
170
+ end
171
+ return true, "-ERR unknown command"
172
+ end
173
+
174
+ def user(username)
175
+ @user = @userstore.user_by_username(username)
176
+ end
177
+
178
+ def pass(password)
179
+ return false unless @user
180
+ return false unless @user.password == password
181
+ true
182
+ end
183
+
184
+ def emails
185
+ @emails = @emailstore.emails_by_userid(@user.id)
186
+ end
187
+
188
+ def stat
189
+ msgs = bytes = 0
190
+ @emails.each do |e|
191
+ p e
192
+ p e.body.length
193
+ next if e.marked
194
+ msgs += 1
195
+ bytes += e.body.length
196
+ end
197
+ [msgs, bytes]
198
+ end
199
+
200
+ def list(msgid = nil)
201
+ msgid = msgid.to_i if msgid
202
+ if msgid
203
+ return false if msgid > @emails.length or @emails[msgid-1].marked
204
+ return [ [msgid, @emails[msgid].body.length] ]
205
+ else
206
+ msgs = []
207
+ @emails.each_with_index do |e,i|
208
+ msgs << [ i + 1, e.body.length ]
209
+ end
210
+ msgs
211
+ end
212
+ end
213
+
214
+ def retr(msgid)
215
+ msgid = msgid.to_i
216
+ return false if msgid > @emails.length or @emails[msgid-1].marked
217
+ @emails[msgid-1].body
218
+ end
219
+
220
+ def dele(msgid)
221
+ msgid = msgid.to_i
222
+ return false if msgid > @emails.length
223
+ @emails[msgid-1].marked = true
224
+ end
225
+
226
+ def rset
227
+ @emails.each do |e|
228
+ e.marked = false
229
+ end
230
+ end
231
+
232
+ def quit
233
+ @emails.find_all {|e| e.marked}.each do |email|
234
+ @emailstore.delete_email(email)
235
+ end
236
+ end
237
+
238
+ def apop(username, hash)
239
+ user(username)
240
+ return false unless @user
241
+ if Digest::MD5.new.update("#{@apop_challenge}#{@user.password}").hexdigest == hash
242
+ return true
243
+ end
244
+ false
245
+ end
246
+
247
+ end
248
+ end
249
+ end
250
+