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 +7 -0
- data/.gitignore +15 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +90 -0
- data/Rakefile +12 -0
- data/eventmachine-email_server.gemspec +28 -0
- data/lib/eventmachine/email_server/base.rb +66 -0
- data/lib/eventmachine/email_server/eventmachine_dns_monkeypatch.rb +55 -0
- data/lib/eventmachine/email_server/memory.rb +61 -0
- data/lib/eventmachine/email_server/null.rb +56 -0
- data/lib/eventmachine/email_server/pop3_server.rb +250 -0
- data/lib/eventmachine/email_server/smtp_server.rb +242 -0
- data/lib/eventmachine/email_server/sqlite3.rb +153 -0
- data/lib/eventmachine/email_server/version.rb +5 -0
- data/lib/eventmachine/email_server.rb +8 -0
- data/test/helper.rb +5 -0
- data/test/test_email_server.rb +334 -0
- metadata +162 -0
@@ -0,0 +1,242 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
require 'eventmachine/dnsbl'
|
3
|
+
|
4
|
+
module EventMachine
|
5
|
+
module EmailServer
|
6
|
+
class SMTPServer < EventMachine::Connection
|
7
|
+
@@graylist = nil
|
8
|
+
@@dnsbl_check = nil
|
9
|
+
@@ratelimiter = nil
|
10
|
+
@@reverse_ptr_check = false
|
11
|
+
@@spf_check = false
|
12
|
+
@@reject_filters = Array.new
|
13
|
+
|
14
|
+
def self.reverse_ptr_check(ptr=nil)
|
15
|
+
if not ptr.nil?
|
16
|
+
@@reverse_ptr_check = ptr
|
17
|
+
end
|
18
|
+
@@reverse_ptr_check
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.graylist(graylist=nil)
|
22
|
+
if graylist
|
23
|
+
@@graylist = graylist
|
24
|
+
end
|
25
|
+
@@graylist
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.dnsbl_check(dnsbl_check=nil)
|
29
|
+
if not dnsbl_check.nil?
|
30
|
+
@@dnsbl_check = dnsbl_check
|
31
|
+
end
|
32
|
+
@@dnsbl_check
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.ratelimiter(ratelimiter=nil)
|
36
|
+
if ratelimiter
|
37
|
+
@@ratelimiter = ratelimiter
|
38
|
+
end
|
39
|
+
@@ratelimiter
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.reject_filters(filters=nil)
|
43
|
+
if filters
|
44
|
+
@@reject_filters = filters
|
45
|
+
end
|
46
|
+
@@reject_filters
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.spf_check(spf=nil)
|
50
|
+
if not spf.nil?
|
51
|
+
@@spf_check = spf
|
52
|
+
end
|
53
|
+
@@spf_check
|
54
|
+
end
|
55
|
+
|
56
|
+
def initialize(hostname, userstore, emailstore)
|
57
|
+
@hostname = hostname
|
58
|
+
@userstore = userstore
|
59
|
+
@emailstore = emailstore
|
60
|
+
@debug = true
|
61
|
+
@data_mode = false
|
62
|
+
@email_body = ""
|
63
|
+
@ptr_ok = true
|
64
|
+
@dnsbl_ok = true
|
65
|
+
@rate_ok = true
|
66
|
+
@gray_ok = true
|
67
|
+
@reject_ok = true
|
68
|
+
@pending_checks = Array.new
|
69
|
+
end
|
70
|
+
|
71
|
+
def post_init
|
72
|
+
puts ">> 220 hello" if @debug
|
73
|
+
send_data "220 #{@hostname} ESMTP Service ready\n"
|
74
|
+
end
|
75
|
+
|
76
|
+
def receive_data(data)
|
77
|
+
puts ">> #{data}" if @debug
|
78
|
+
data.split(/\n/).each do |line|
|
79
|
+
ok, op = process_line(line+"\n")
|
80
|
+
if op
|
81
|
+
puts "<< #{op}" if @debug
|
82
|
+
send_data(op+"\r\n")
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def check_ptr(helo, ip)
|
88
|
+
if @@reverse_ptr_check
|
89
|
+
@ptr_ok = false
|
90
|
+
@pending_checks << :ptr
|
91
|
+
d = EM::DNS::Resolver.resolve helo
|
92
|
+
d.callback { |r|
|
93
|
+
@ptr_ok = r.include?(ip)
|
94
|
+
@pending_checks -= [:ptr]
|
95
|
+
if @pending_checks.length == 0
|
96
|
+
send_answer
|
97
|
+
end
|
98
|
+
}
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def check_dnsbl(ip)
|
103
|
+
if @@dnsbl_check
|
104
|
+
@dnsbl_ok = false
|
105
|
+
@pending_checks << :dnsbl
|
106
|
+
EventMachine::DNSBL::Client.check(ip) do |results|
|
107
|
+
@dnsbl_ok = ! EventMachine::DNSBL::Client.blacklisted?(results)
|
108
|
+
@pending_checks -= [:dnsbl]
|
109
|
+
if @pending_checks.length == 0
|
110
|
+
send_answer
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def check_ratelimit(ip)
|
117
|
+
if @@ratelimiter
|
118
|
+
@rate_ok = @@ratelimiter.use(ip)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def check_gray(ip)
|
123
|
+
if @@graylist
|
124
|
+
@gray_ok = @@graylist.has_key?(ip)
|
125
|
+
@@graylist[ip] = true
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def check_reject
|
130
|
+
@@reject_filters.each do |filter|
|
131
|
+
if filter.match(@email_body)
|
132
|
+
@reject_ok = false
|
133
|
+
return
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def check_spf(from_domain)
|
139
|
+
if @@spf_check
|
140
|
+
@spf_ok = false
|
141
|
+
@pending_checks << :spf
|
142
|
+
d = EM::DNS::Resolver.resolve(from_domain, Resolv::DNS::Resource::IN::TXT)
|
143
|
+
d.errback { |r|
|
144
|
+
# fail open?
|
145
|
+
@spf_ok = true
|
146
|
+
@pending_checks -= [:spf]
|
147
|
+
if @pending_checks.length == 0
|
148
|
+
send_answer
|
149
|
+
end
|
150
|
+
}
|
151
|
+
d.callback { |r|
|
152
|
+
r.each do |rec|
|
153
|
+
#pp rec
|
154
|
+
if rec.start_with?('v=spf1')
|
155
|
+
if rec == "v=spf1 -all"
|
156
|
+
@spf_ok = false
|
157
|
+
else
|
158
|
+
# I need to create an SPF checker now :(
|
159
|
+
@spf_ok = true
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
@pending_checks -= [:spf]
|
164
|
+
if @pending_checks.length == 0
|
165
|
+
send_answer
|
166
|
+
end
|
167
|
+
}
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def send_answer
|
172
|
+
if @ptr_ok and @rate_ok and @gray_ok and @reject_ok and @dnsbl_ok and @spf_ok
|
173
|
+
ans = "250 OK"
|
174
|
+
else
|
175
|
+
ans = "451 Requested action aborted: local error in processing"
|
176
|
+
end
|
177
|
+
puts "<< #{ans}" if @debug
|
178
|
+
send_data(ans+"\r\n")
|
179
|
+
end
|
180
|
+
|
181
|
+
def process_line(line)
|
182
|
+
if (@data_mode) && (line.chomp =~ /^\.$/)
|
183
|
+
@data_mode = false
|
184
|
+
check_reject
|
185
|
+
if @pending_checks.length == 0
|
186
|
+
send_answer
|
187
|
+
end
|
188
|
+
return true, nil
|
189
|
+
elsif @data_mode
|
190
|
+
@email_body += line
|
191
|
+
return true, nil
|
192
|
+
elsif (line =~ /^(HELO|EHLO) (.*)/)
|
193
|
+
helo = $2.chomp
|
194
|
+
port, ip = Socket.unpack_sockaddr_in(get_peername)
|
195
|
+
check_ptr(helo, ip)
|
196
|
+
check_dnsbl(ip)
|
197
|
+
check_gray(ip)
|
198
|
+
check_ratelimit(ip)
|
199
|
+
return true, "250 hello #{ip} (#{helo})"
|
200
|
+
elsif (line =~ /^QUIT/)
|
201
|
+
return false, "221 bye bye"
|
202
|
+
elsif (line =~ /^MAIL FROM\:/)
|
203
|
+
@mail_from = (/^MAIL FROM\:<(.+)>.*$/).match(line)[1]
|
204
|
+
if @@spf_check
|
205
|
+
check_spf(@mail_from.split(/@/,2)[1])
|
206
|
+
end
|
207
|
+
return true, "250 OK"
|
208
|
+
elsif (line =~ /^RCPT TO\:/)
|
209
|
+
rcpt_to = (/^RCPT TO\:<(.+)>.*$/).match(line)[1]
|
210
|
+
if @userstore.user_by_emailaddress(rcpt_to.strip)
|
211
|
+
@rcpt_to = rcpt_to
|
212
|
+
return true, "250 OK"
|
213
|
+
end
|
214
|
+
return false, "550 No such user here"
|
215
|
+
elsif (line =~ /^DATA/)
|
216
|
+
if @rcpt_to
|
217
|
+
@data_mode = true
|
218
|
+
@email_body = ''
|
219
|
+
return true, "354 Enter message, ending with \".\" on a line by itself"
|
220
|
+
end
|
221
|
+
return true, "500 ERROR"
|
222
|
+
else
|
223
|
+
return true, "500 ERROR"
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
def save
|
228
|
+
begin
|
229
|
+
subject = @email_body.match(/Subject\: (.*?)[\r\n]/i)[1]
|
230
|
+
u = @userstore.user_by_emailaddress(@rcpt_to.strip)
|
231
|
+
rescue Exception => err
|
232
|
+
puts err
|
233
|
+
return
|
234
|
+
end
|
235
|
+
if u and @mail_from and @rcpt_to
|
236
|
+
subject ||= ''
|
237
|
+
@emailstore << Email.new(nil, @mail_from, @rcpt_to, subject, @email_body, u.id)
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
require 'sqlite3'
|
3
|
+
|
4
|
+
module EventMachine
|
5
|
+
module EmailServer
|
6
|
+
class Sqlite3UserStore < AbstractUserStore
|
7
|
+
def initialize(sqlite3, tablename = "users")
|
8
|
+
@class = User
|
9
|
+
@fields = @class.members.map {|x| x.to_s}.join(", ")
|
10
|
+
@tablename = tablename
|
11
|
+
if sqlite3.class == SQLite3::Database
|
12
|
+
@db = sqlite3
|
13
|
+
else
|
14
|
+
@db = SQLite3::Database.new(sqlite3)
|
15
|
+
end
|
16
|
+
if @db.table_info(tablename).length == 0
|
17
|
+
@db.execute("CREATE TABLE #{@tablename} (#{@fields})")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def add_user(user)
|
22
|
+
if user.id
|
23
|
+
u = user_by_id(user.id)
|
24
|
+
end
|
25
|
+
if u
|
26
|
+
@db.execute("UPDATE #{@tablename} SET username=?, password=?, address=? WHERE id=?",
|
27
|
+
user.username,
|
28
|
+
user.password,
|
29
|
+
user.address,
|
30
|
+
user.id)
|
31
|
+
else
|
32
|
+
@db.execute("INSERT INTO #{@tablename} (id,username,password,address) VALUES (?,?,?,?)",
|
33
|
+
user.id,
|
34
|
+
user.username,
|
35
|
+
user.password,
|
36
|
+
user.address)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def delete_user(user)
|
41
|
+
if user.id
|
42
|
+
@db.execute("DELETE FROM #{@tablename} WHERE id = ?", user.id)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def user_by_field(field, value)
|
47
|
+
rs = @db.execute("SELECT #{@fields} FROM #{@tablename} WHERE #{field}='#{value}'")
|
48
|
+
return nil unless rs
|
49
|
+
rs.each do |row|
|
50
|
+
return User.new(*row)
|
51
|
+
end
|
52
|
+
return nil
|
53
|
+
end
|
54
|
+
|
55
|
+
def user_by_username(username)
|
56
|
+
user_by_field("username", username)
|
57
|
+
end
|
58
|
+
|
59
|
+
def user_by_emailaddress(address)
|
60
|
+
user_by_field("address", address)
|
61
|
+
end
|
62
|
+
|
63
|
+
def user_by_id(id)
|
64
|
+
user_by_field("id", id)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class Sqlite3EmailStore < AbstractEmailStore
|
69
|
+
def initialize(sqlite3, tablename = "emails")
|
70
|
+
@class = Email
|
71
|
+
@fields = "'"+@class.members.map {|x| x.to_s}.join("', '")+"'"
|
72
|
+
@tablename = tablename
|
73
|
+
if sqlite3.class == SQLite3::Database
|
74
|
+
@db = sqlite3
|
75
|
+
else
|
76
|
+
@db = SQLite3::Database.new(sqlite3)
|
77
|
+
end
|
78
|
+
if @db.table_info(tablename).length == 0
|
79
|
+
fields = @fields.gsub(/'id'/, 'id integer primary key autoincrement')
|
80
|
+
@db.execute("CREATE TABLE #{@tablename} (#{fields})")
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def emails_by_field(field, value)
|
85
|
+
rs = @db.execute("SELECT #{@fields} FROM #{@tablename} WHERE ?=?", field, value)
|
86
|
+
return nil unless rs
|
87
|
+
emails = Array.new
|
88
|
+
rs.each do |row|
|
89
|
+
emails << Email.new(*row)
|
90
|
+
end
|
91
|
+
emails
|
92
|
+
end
|
93
|
+
|
94
|
+
def emails_by_userid(uid)
|
95
|
+
emails_by_field("uid", uid)
|
96
|
+
end
|
97
|
+
|
98
|
+
def quote( string )
|
99
|
+
string.gsub( /'/, "''" )
|
100
|
+
end
|
101
|
+
private :quote
|
102
|
+
|
103
|
+
def save_email(email)
|
104
|
+
if email.id
|
105
|
+
# I'm being too crafty here.. this is bad style
|
106
|
+
args = (@class.members - [:id]).map{|f| email.send(f)}
|
107
|
+
args << email.send(:id)
|
108
|
+
@db.execute("UPDATE #{@tablename} SET " +
|
109
|
+
(@class.members - [:id]).map { |field| "#{field} = ?"}.join(", ") +
|
110
|
+
" WHERE id = ?", *args)
|
111
|
+
else
|
112
|
+
email.id = "NULL"
|
113
|
+
args = (@class.members).map{|f| email.send(f)}
|
114
|
+
qs = args.map{|x| "'#{quote(x.to_s)}'"}.join(",").gsub(/'NULL'/, "NULL")
|
115
|
+
@db.execute("INSERT INTO #{@tablename} (#{@fields}) VALUES (#{qs})")
|
116
|
+
rs = @db.execute("SELECT last_insert_rowid()")
|
117
|
+
rs.each do |row|
|
118
|
+
email.id = *row
|
119
|
+
end
|
120
|
+
end
|
121
|
+
email.id
|
122
|
+
end
|
123
|
+
|
124
|
+
def delete_email(email)
|
125
|
+
if email.id
|
126
|
+
@db.execute("DROP FROM #{@tablename} WHERE id = ?", email.id)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def delete_by_field(field, value)
|
131
|
+
@db.execute("DROP FROM #{@tablename} WHERE #{field} = ?", value)
|
132
|
+
end
|
133
|
+
|
134
|
+
def delete_id(id)
|
135
|
+
delete_by_field("id", id)
|
136
|
+
end
|
137
|
+
|
138
|
+
def delete_user(uid)
|
139
|
+
delete_by_field("uid", uid)
|
140
|
+
end
|
141
|
+
|
142
|
+
def count
|
143
|
+
sql = "SELECT COUNT(*) FROM #{@tablename}"
|
144
|
+
rs = @db.execute(sql)
|
145
|
+
c = 0
|
146
|
+
rs.each do |row|
|
147
|
+
c = row[0]
|
148
|
+
end
|
149
|
+
c
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
require "eventmachine/email_server/version"
|
2
|
+
require 'eventmachine/email_server/base'
|
3
|
+
require 'eventmachine/email_server/memory'
|
4
|
+
require 'eventmachine/email_server/null'
|
5
|
+
require 'eventmachine/email_server/sqlite3'
|
6
|
+
require 'eventmachine/email_server/pop3_server'
|
7
|
+
require 'eventmachine/email_server/smtp_server'
|
8
|
+
require 'eventmachine/email_server/eventmachine_dns_monkeypatch'
|