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.
@@ -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,5 @@
1
+ module EventMachine
2
+ module EmailServer
3
+ VERSION = "0.0.2"
4
+ end
5
+ 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'
data/test/helper.rb ADDED
@@ -0,0 +1,5 @@
1
+ require 'minitest/autorun'
2
+ require 'minitest/test'
3
+ require 'minitest/unit'
4
+ include MiniTest::Assertions
5
+ require File.expand_path('../../lib/eventmachine/email_server.rb', __FILE__)