eventmachine-email_server 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.
- 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'
|