percolate-mail 1.0.0 → 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/percolate/exceptions.rb +29 -0
- data/lib/percolate/listener.rb +19 -20
- data/lib/percolate/mail_object.rb +31 -6
- data/lib/percolate/smtp/responder.rb +343 -0
- data/test/test_percolate_listener.rb +1 -1
- data/test/{test_percolate_responder.rb → test_percolate_smtp_responder.rb} +82 -6
- metadata +46 -37
- data/lib/percolate/responder.rb +0 -347
@@ -0,0 +1,29 @@
|
|
1
|
+
module Percolate
|
2
|
+
# A ResponderError exception is raised when something went horribly
|
3
|
+
# wrong for whatever reason.
|
4
|
+
#
|
5
|
+
# If you actually find one of these leaping into your lap in your
|
6
|
+
# own code, and you're not trying anything gratuitously silly, I
|
7
|
+
# want to know about it, because I have gone out of my way that
|
8
|
+
# these errors manifest themselves in an SMTPish way as error codes.
|
9
|
+
#
|
10
|
+
# If you are trying something silly though, I reserve the right to
|
11
|
+
# laugh at you. And possibly mock you on my blog. (Are you scared
|
12
|
+
# now?)
|
13
|
+
class ResponderError < Exception; end
|
14
|
+
|
15
|
+
# This is an exception that you *should* expect to receive, if you
|
16
|
+
# deal with the SMTP Responder yourself (you probably won't
|
17
|
+
# though--if you're smart, you'll just use the Listener and let it
|
18
|
+
# park itself on some port for other network services to talk to).
|
19
|
+
# All it means is that the client has just sent a "quit" message,
|
20
|
+
# and all is kosher, indicating that now would be a good time to
|
21
|
+
# clean up shop.
|
22
|
+
#
|
23
|
+
# NOTE VERY WELL THOUGH!: When you get one of these exceptions,
|
24
|
+
# there is still one response left in the pipe that you have to
|
25
|
+
# deliver to the client ("221 Pleasure doing business with you") so
|
26
|
+
# make sure you deliver that before closing the connection. It's
|
27
|
+
# only polite, after all.
|
28
|
+
class TransactionFinishedException < Exception; end
|
29
|
+
end
|
data/lib/percolate/listener.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
require "percolate/responder"
|
1
|
+
require "percolate/smtp/responder"
|
2
2
|
require "socket"
|
3
3
|
|
4
4
|
module Percolate
|
@@ -31,16 +31,11 @@ module Percolate
|
|
31
31
|
# Postfix or Sendmail or your real MTA of
|
32
32
|
# choice filter through it.
|
33
33
|
def initialize(opts = {})
|
34
|
-
@
|
35
|
-
@
|
36
|
-
@port
|
37
|
-
@
|
38
|
-
|
39
|
-
@verbose_debug = opts[:debug]
|
40
|
-
@ipaddress = opts[:ipaddr] if opts[:ipaddr]
|
41
|
-
@port = opts[:port] if opts[:port]
|
42
|
-
@hostname = opts[:hostname] if opts[:hostname]
|
43
|
-
@responder = opts[:responder] if opts[:responder]
|
34
|
+
@verbose_debug = opts[:debug] || false
|
35
|
+
@ipaddress = opts[:ipaddr] || "0.0.0.0"
|
36
|
+
@port = opts[:port] || 10025
|
37
|
+
@hostname = opts[:hostname] || "localhost"
|
38
|
+
@responder = opts[:responder] || SMTP::Responder
|
44
39
|
|
45
40
|
@socket = TCPServer.new @ipaddress, @port
|
46
41
|
end
|
@@ -50,7 +45,7 @@ module Percolate
|
|
50
45
|
|
51
46
|
# Once the listener is running, let it start handling mail by
|
52
47
|
# invoking the poorly-named "go" method.
|
53
|
-
def go
|
48
|
+
def go &block
|
54
49
|
trap 'CLD' do
|
55
50
|
debug "Got SIGCHLD"
|
56
51
|
reap_children
|
@@ -63,7 +58,9 @@ module Percolate
|
|
63
58
|
@pids = []
|
64
59
|
while mailsocket=@socket.accept
|
65
60
|
debug "Got connection from #{mailsocket.peeraddr[3]}"
|
66
|
-
|
61
|
+
|
62
|
+
pid = handle_connection mailsocket, &block
|
63
|
+
|
67
64
|
mailsocket.close
|
68
65
|
@pids << pid
|
69
66
|
end
|
@@ -71,10 +68,10 @@ module Percolate
|
|
71
68
|
|
72
69
|
private
|
73
70
|
|
74
|
-
def handle_connection mailsocket
|
71
|
+
def handle_connection mailsocket, &block
|
75
72
|
fork do # I can't imagine the contortions required
|
76
|
-
|
77
|
-
|
73
|
+
# in Win32 to get "fork" to work, but hey,
|
74
|
+
# maybe someone did so anyway.
|
78
75
|
responder = @responder.new hostname,
|
79
76
|
:originating_ip => mailsocket.peeraddr[3]
|
80
77
|
begin
|
@@ -84,14 +81,16 @@ module Percolate
|
|
84
81
|
|
85
82
|
cmd = mailsocket.readline
|
86
83
|
cmd.chomp! CRLF
|
87
|
-
responder.command cmd
|
84
|
+
responder.command cmd, &block
|
88
85
|
end
|
89
86
|
rescue TransactionFinishedException
|
90
|
-
mailsocket.
|
87
|
+
mailsocket.print responder.response + CRLF
|
91
88
|
mailsocket.close
|
92
89
|
exit!
|
93
|
-
rescue
|
94
|
-
mailsocket.
|
90
|
+
rescue Exception => e
|
91
|
+
mailsocket.print e.exception + CRLF
|
92
|
+
mailsocket.print "From " + e.traceback.join(CRLF + "from ") + CRLF
|
93
|
+
mailsocket.print "421 Server confused, shutting down" +
|
95
94
|
CRLF
|
96
95
|
mailsocket.close
|
97
96
|
exit!
|
@@ -31,7 +31,8 @@
|
|
31
31
|
module Percolate
|
32
32
|
|
33
33
|
# The SMTP::MailObject is mostly a class. It's what is produced by the
|
34
|
-
# Percolate::Responder class as a result of a complete SMTP
|
34
|
+
# Percolate::SMTP::Responder class as a result of a complete SMTP
|
35
|
+
# transaction.
|
35
36
|
class MailObject
|
36
37
|
|
37
38
|
# The constructor. It takes a whole bunch of optional
|
@@ -69,7 +70,7 @@ module Percolate
|
|
69
70
|
attr_reader :smtp_id, :heloname, :origin_ip, :myhostname
|
70
71
|
|
71
72
|
begin
|
72
|
-
require "gurgitate
|
73
|
+
require "gurgitate-mail"
|
73
74
|
|
74
75
|
# Converts a SMTP::MailObject object into a Gurgitate-Mail
|
75
76
|
# MailMessage object.
|
@@ -87,14 +88,38 @@ module Percolate
|
|
87
88
|
# an SMTP server should accept pretty well any old
|
88
89
|
# crap.)
|
89
90
|
message_text = received + "From: #{@envelope_from}\n" +
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
@content
|
91
|
+
"To: undisclosed recipients:;\n" +
|
92
|
+
"X-Gurgitate-Error: #{$!}\n" +
|
93
|
+
"\n" + @content
|
94
94
|
return Gurgitate::Mailmessage.new(message_text, @envelope_to,
|
95
95
|
@envelope_from)
|
96
96
|
end
|
97
97
|
end
|
98
|
+
|
99
|
+
# Lets you process a message with Gurgitate-Mail. The
|
100
|
+
# gurgitate-rules segment is given in the block.
|
101
|
+
def gurgitate &block
|
102
|
+
received = "Received: from #{@heloname} (#{@origin_ip}) " +
|
103
|
+
"by #{@myhostname} with SMTP ID #{smtp_id} " +
|
104
|
+
"for <#{@envelope_to}>; #{@timestamp.to_s}\n"
|
105
|
+
message = @content.gsub "\r",""
|
106
|
+
begin
|
107
|
+
Gurgitate::Gurgitate.new(received + message, @envelope_to,
|
108
|
+
@envelope_from).process &block
|
109
|
+
rescue Gurgitate::IllegalHeader
|
110
|
+
# okay, let's MAKE a mail message (the RFC actually
|
111
|
+
# says that this is okay. It says that after DATA,
|
112
|
+
# an SMTP server should accept pretty well any old
|
113
|
+
# crap.)
|
114
|
+
message_text = received + "From: #{@envelope_from}\n" +
|
115
|
+
"To: undisclosed recipients:;\n" +
|
116
|
+
"X-Gurgitate-Error: #{$!}\n" +
|
117
|
+
"\n" + @content
|
118
|
+
Gurgitate::Gurgitate.new(message_text, @envelope_to,
|
119
|
+
@envelope_from).process &block
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
98
123
|
rescue LoadError => e
|
99
124
|
nil # and don't define to_gurgitate_mailmessage. I'm a huge
|
100
125
|
# egotist so I'm not including an rmail variant
|
@@ -0,0 +1,343 @@
|
|
1
|
+
require "percolate/mail_object"
|
2
|
+
require "percolate/exceptions"
|
3
|
+
|
4
|
+
module Percolate
|
5
|
+
module SMTP
|
6
|
+
# This is the bit that actually handles the SMTP conversation with
|
7
|
+
# the client. Basically, you send it commands, and it acts on them.
|
8
|
+
# There is a small amount of Rubyish magic but that's there mainly
|
9
|
+
# because I'm lazy. And besides, if I weren't lazy, some guys on
|
10
|
+
# IRC would flame me. Haha! I kid. Greetz to RubyPanther by the
|
11
|
+
# way.
|
12
|
+
class Responder
|
13
|
+
|
14
|
+
# Sets up the new smtp responder, with the parameter "mailhostname"
|
15
|
+
# as the SMTP hostname (as returned in the response to the SMTP
|
16
|
+
# HELO command.) Note that "mailhostname" can be anything at
|
17
|
+
# all, because I no longer believe that it's possible to
|
18
|
+
# actually figure out your own full hostname without actually
|
19
|
+
# literally being told it. This is probably excessively-cynical
|
20
|
+
# of me, but I've seen what happens when wide-eyed optimists try
|
21
|
+
# to guess hostnames, and it just isn't pretty.
|
22
|
+
#
|
23
|
+
# Let's just leave it at "mailhostname is a required parameter",
|
24
|
+
# shall we?
|
25
|
+
#
|
26
|
+
# Also, there are some interesting options you can give this.
|
27
|
+
# Well, only a couple. The first is :debug which you can set to
|
28
|
+
# true or false, which will cause it to print the SMTP
|
29
|
+
# conversation out. This is, of course, mostly only useful for
|
30
|
+
# debugging.
|
31
|
+
#
|
32
|
+
# The other option, :originating_ip, probably more interesting,
|
33
|
+
# comes from the listener--the IP address of the client that
|
34
|
+
# connected.
|
35
|
+
def initialize(mailhostname, opts={})
|
36
|
+
@verbose_debug = opts[:debug]
|
37
|
+
@originating_ip = opts[:originating_ip]
|
38
|
+
|
39
|
+
@mailhostname = mailhostname
|
40
|
+
@current_state = nil
|
41
|
+
@current_command = nil
|
42
|
+
@response = connect
|
43
|
+
@mail_object=nil
|
44
|
+
@debug_output = []
|
45
|
+
debug "\n\n"
|
46
|
+
end
|
47
|
+
|
48
|
+
# This is one of the methods you have to override in a subclass
|
49
|
+
# in order to use this class properly (unless chuckmail really
|
50
|
+
# is acceptable for you, in which case excellent! Also its
|
51
|
+
# default behaviour is to pretend to be an open relay, which
|
52
|
+
# should delight spammers until they figure out that all of
|
53
|
+
# their mail is being silently and cheerfully discarded, but
|
54
|
+
# they're spammers so they won't).
|
55
|
+
#
|
56
|
+
# Parameters:
|
57
|
+
# +message_object+:: A SMTP::MessageObject object, with envelope
|
58
|
+
# data and the message itself (which could, as
|
59
|
+
# the RFC says, be any old crap at all! Don't
|
60
|
+
# even expect an RFC2822-formatted message)
|
61
|
+
def process_message message_object
|
62
|
+
yield :data, message_object if block_given?
|
63
|
+
return true
|
64
|
+
end
|
65
|
+
|
66
|
+
# Override this if you care about who the sender is (you
|
67
|
+
# probably do care who the sender is).
|
68
|
+
#
|
69
|
+
# Incidentally, you probably Do Not Want To Become An Open Spam
|
70
|
+
# Relay--you really should validate both senders and recipients,
|
71
|
+
# and only accept mail if:
|
72
|
+
#
|
73
|
+
# (a) the sender is local, and the recipient is remote, or
|
74
|
+
# (b) the sender is remote, and the recipient is local.
|
75
|
+
#
|
76
|
+
# The definition of "local" and "remote" are, of course, up to
|
77
|
+
# you--if you're using this to handle mail for a hundred
|
78
|
+
# domains, then all those hundred domains are local for you--but
|
79
|
+
# the idea is that you _shoud_ be picky about who your mail is
|
80
|
+
# from and to.
|
81
|
+
#
|
82
|
+
# This method takes one argument:
|
83
|
+
# +address+:: The email address you're validating
|
84
|
+
def validate_sender address
|
85
|
+
return true
|
86
|
+
end
|
87
|
+
|
88
|
+
# Override this if you care about the recipient (which you
|
89
|
+
# should). When you get to this point, the accessor "sender"
|
90
|
+
# will work to return the sender, so that you can deal with both
|
91
|
+
# recipient and sender here.
|
92
|
+
def validate_recipient address
|
93
|
+
return true
|
94
|
+
end
|
95
|
+
|
96
|
+
# The current message's sender, if the MAIL FROM: command has
|
97
|
+
# been processed yet. If it hasn't, then it returns nil, and
|
98
|
+
# probably isn't meaningful anyway.
|
99
|
+
def sender
|
100
|
+
if @mail_object
|
101
|
+
@mail_object.envelope_from
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Returns the response from the server. When there's no response,
|
106
|
+
# returns nil.
|
107
|
+
#
|
108
|
+
# I still haven't figured out what to do when there's
|
109
|
+
# more than one response (like in the case of an ESMTP capabilities
|
110
|
+
# list).
|
111
|
+
def response
|
112
|
+
resp = @response
|
113
|
+
@response = nil
|
114
|
+
debug "<<< #{resp}"
|
115
|
+
resp
|
116
|
+
end
|
117
|
+
|
118
|
+
# Send an SMTP command to the responder. Use this to send a
|
119
|
+
# line of input to the responder (including a single line of
|
120
|
+
# DATA).
|
121
|
+
#
|
122
|
+
# Parameters:
|
123
|
+
# +offered_command+:: The SMTP command (with end-of-line characters
|
124
|
+
# removed)
|
125
|
+
def command offered_command
|
126
|
+
debug ">>> #{offered_command}"
|
127
|
+
begin
|
128
|
+
dispatch offered_command
|
129
|
+
rescue ResponderError => error
|
130
|
+
@response = error.message
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
private
|
135
|
+
|
136
|
+
attr_reader :current_state
|
137
|
+
attr_reader :current_command
|
138
|
+
|
139
|
+
CommandTable = {
|
140
|
+
/^helo ([a-z0-9.-]+)$/i => :helo,
|
141
|
+
/^ehlo ([a-z0-9.-]+)$/i => :ehlo,
|
142
|
+
/^mail (.+)$/i => :mail,
|
143
|
+
/^rcpt (.+)$/i => :rcpt,
|
144
|
+
/^data$/i => :data,
|
145
|
+
/^\.$/ => :endmessage,
|
146
|
+
/^rset$/i => :rset,
|
147
|
+
/^quit$/i => :quit
|
148
|
+
}
|
149
|
+
|
150
|
+
StateTable = {
|
151
|
+
:connect => { :states => [ nil ],
|
152
|
+
:error => "This should never happen" },
|
153
|
+
:smtp_greeted_helo => { :states => [ :connect,
|
154
|
+
:smtp_greeted_helo,
|
155
|
+
:smtp_greeted_ehlo,
|
156
|
+
:smtp_mail_started,
|
157
|
+
:smtp_rcpt_received,
|
158
|
+
:message_received ],
|
159
|
+
:error => nil },
|
160
|
+
:smtp_greeted_ehlo => { :states => [ :connect ],
|
161
|
+
:error => nil },
|
162
|
+
:smtp_mail_started => { :states => [ :smtp_greeted_helo,
|
163
|
+
:smtp_greeted_ehlo,
|
164
|
+
:message_received ],
|
165
|
+
:error => "Can't say MAIL right now" },
|
166
|
+
:smtp_rcpt_received => { :states => [ :smtp_mail_started,
|
167
|
+
:smtp_rcpt_received ],
|
168
|
+
:error => "need MAIL FROM: first" },
|
169
|
+
:data => { :states => [ :smtp_rcpt_received ],
|
170
|
+
:error => "Specify sender and recipient first" },
|
171
|
+
:message_received => { :states => [ :data ],
|
172
|
+
:error => "500 command not recognized" },
|
173
|
+
:quit => { :states => [ :ANY ],
|
174
|
+
:error => nil },
|
175
|
+
}
|
176
|
+
|
177
|
+
def debug debug_string
|
178
|
+
@debugging_stream ||= []
|
179
|
+
@debugging_stream << debug_string
|
180
|
+
if @verbose_debug
|
181
|
+
$stderr.puts debug_string
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def dispatch offered_command
|
186
|
+
if @current_state == :data
|
187
|
+
handle_message_data offered_command
|
188
|
+
return true
|
189
|
+
end
|
190
|
+
|
191
|
+
CommandTable.keys.each do |regex|
|
192
|
+
matchdata = regex.match offered_command
|
193
|
+
if matchdata then
|
194
|
+
return send(CommandTable[regex],*(matchdata.to_a[1..-1]))
|
195
|
+
end
|
196
|
+
end
|
197
|
+
raise ResponderError, "500 command not recognized"
|
198
|
+
end
|
199
|
+
|
200
|
+
def connect
|
201
|
+
validate_state :connect
|
202
|
+
respond "220 Ok"
|
203
|
+
end
|
204
|
+
|
205
|
+
# Makes sure that the commands are all happening in the right
|
206
|
+
# order (and complains if they're not)
|
207
|
+
def validate_state target_state
|
208
|
+
unless StateTable[target_state][:states].member? current_state or
|
209
|
+
StateTable[target_state][:states] == [ :ANY ]
|
210
|
+
raise ResponderError, "503 #{StateTable[target_state][:error]}"
|
211
|
+
end
|
212
|
+
@target_state = target_state
|
213
|
+
end
|
214
|
+
|
215
|
+
def respond response
|
216
|
+
@current_state = @target_state
|
217
|
+
@response = response
|
218
|
+
end
|
219
|
+
|
220
|
+
# The smtp commands. This is thus far not a complete set of
|
221
|
+
# every single imagineable SMTP command, but it's certainly
|
222
|
+
# enough to be able to fairly call this an "SMTP server".
|
223
|
+
#
|
224
|
+
# If you want documentation about what each of these does, see
|
225
|
+
# RFC2821, which explains it much better and in far greater
|
226
|
+
# detail than I could.
|
227
|
+
|
228
|
+
def helo remotehost
|
229
|
+
validate_state :smtp_greeted_helo
|
230
|
+
@heloname = remotehost
|
231
|
+
@mail_object = nil
|
232
|
+
@greeting_state = :smtp_greeted_helo # for rset
|
233
|
+
respond "250 #{@mailhostname}"
|
234
|
+
end
|
235
|
+
|
236
|
+
def ehlo remotehost
|
237
|
+
validate_state :smtp_greeted_ehlo
|
238
|
+
@heloname = remotehost
|
239
|
+
@mail_object = nil
|
240
|
+
@greeting_state = :smtp_greeted_ehlo # for rset
|
241
|
+
respond "250 #{@mailhostname}"
|
242
|
+
end
|
243
|
+
|
244
|
+
def mail sender, &block
|
245
|
+
validate_state :smtp_mail_started
|
246
|
+
matchdata=sender.match(/^From:\<(.*)\>$/i);
|
247
|
+
unless matchdata
|
248
|
+
raise ResponderError, "501 bad MAIL FROM: parameter"
|
249
|
+
end
|
250
|
+
|
251
|
+
mail_from = matchdata[1]
|
252
|
+
|
253
|
+
validated, message = validate_sender mail_from
|
254
|
+
|
255
|
+
unless validated
|
256
|
+
raise ResponderError, "551 #{message || 'no'}"
|
257
|
+
end
|
258
|
+
|
259
|
+
@mail_object = MailObject.new(:envelope_from => mail_from,
|
260
|
+
:heloname => @heloname,
|
261
|
+
:origin_ip => @originating_ip,
|
262
|
+
:myhostname => @mailhostname)
|
263
|
+
|
264
|
+
yield :mail, @mail_object if block_given?
|
265
|
+
|
266
|
+
respond "250 #{message || 'ok'}"
|
267
|
+
end
|
268
|
+
|
269
|
+
def rcpt recipient, &block
|
270
|
+
validate_state :smtp_rcpt_received
|
271
|
+
matchdata=recipient.match(/^To:\<(.*)\>$/i);
|
272
|
+
unless matchdata
|
273
|
+
raise ResponderError, "501 bad RCPT TO: parameter"
|
274
|
+
end
|
275
|
+
|
276
|
+
rcpt_to = matchdata[1]
|
277
|
+
|
278
|
+
@mail_object.envelope_to ||= []
|
279
|
+
|
280
|
+
validated, message = validate_recipient rcpt_to
|
281
|
+
|
282
|
+
unless validated
|
283
|
+
raise ResponderError, "551 #{message || 'no'}"
|
284
|
+
end
|
285
|
+
|
286
|
+
yield :mail, @mail_object if block_given?
|
287
|
+
|
288
|
+
@mail_object.envelope_to << rcpt_to
|
289
|
+
respond "250 #{message || 'ok'}"
|
290
|
+
end
|
291
|
+
|
292
|
+
def data
|
293
|
+
validate_state :data
|
294
|
+
|
295
|
+
@mail_object.content ||= ""
|
296
|
+
respond "354 end data with <cr><lf>.<cr><lf>"
|
297
|
+
end
|
298
|
+
|
299
|
+
def rset
|
300
|
+
if @mail_object
|
301
|
+
@mail_object = nil
|
302
|
+
@target_state = @greeting_state
|
303
|
+
end
|
304
|
+
respond "250 Ok"
|
305
|
+
end
|
306
|
+
|
307
|
+
def quit
|
308
|
+
validate_state :quit
|
309
|
+
if @mail_object
|
310
|
+
@mail_object = nil
|
311
|
+
end
|
312
|
+
respond "221 Pleasure doing business with you"
|
313
|
+
raise TransactionFinishedException
|
314
|
+
end
|
315
|
+
|
316
|
+
# The special-case code for message data, which unlike other
|
317
|
+
# data, is delivered as lots and lots of lines with a sentinel.
|
318
|
+
def handle_message_data message_line, &block
|
319
|
+
if message_line == "."
|
320
|
+
@current_state = :message_received
|
321
|
+
result, text = process_message @mail_object, &block
|
322
|
+
if result or
|
323
|
+
( String === result and text.nil? )
|
324
|
+
if String === result and text.nil?
|
325
|
+
@response = "250 #{result}"
|
326
|
+
elsif result == true and text.nil?
|
327
|
+
@response = "250 accepted, SMTP id is " +
|
328
|
+
@mail_object.smtp_id
|
329
|
+
elsif result == true and text
|
330
|
+
@response = "250 #{text}"
|
331
|
+
end
|
332
|
+
else
|
333
|
+
@response = "550 #{text || 'Email rejected, sorry'}"
|
334
|
+
end
|
335
|
+
elsif message_line == ".."
|
336
|
+
@mail_object.content << "." + "\r\n"
|
337
|
+
else
|
338
|
+
@mail_object.content << message_line + "\r\n"
|
339
|
+
end
|
340
|
+
end
|
341
|
+
end
|
342
|
+
end
|
343
|
+
end
|
@@ -50,7 +50,7 @@ class TestPercolateResponder < Test::Unit::TestCase
|
|
50
50
|
sleep 0.2 # to give the listener time to fire up
|
51
51
|
|
52
52
|
@responder ||= SMTPConnection.new
|
53
|
-
# @responder = Percolate::Responder.new TestHostName, :debug => false
|
53
|
+
# @responder = Percolate::SMTP::Responder.new TestHostName, :debug => false
|
54
54
|
end
|
55
55
|
|
56
56
|
def teardown
|
@@ -2,16 +2,17 @@ require "test/unit"
|
|
2
2
|
|
3
3
|
$:.unshift File.join(File.dirname(__FILE__),"..","lib")
|
4
4
|
|
5
|
-
require "percolate/responder"
|
5
|
+
require "percolate/smtp/responder"
|
6
6
|
|
7
|
-
class MyResponder < Percolate::Responder
|
8
|
-
attr_writer :sender_validation, :recipient_validation
|
7
|
+
class MyResponder < Percolate::SMTP::Responder
|
8
|
+
attr_writer :sender_validation, :recipient_validation, :message_validation
|
9
9
|
|
10
10
|
Responses = { false => "no", true => "ok" }
|
11
11
|
|
12
12
|
def initialize(hostname,opts={})
|
13
13
|
@sender_validation = true
|
14
14
|
@recipient_validation = true
|
15
|
+
@message_validation = true
|
15
16
|
super(hostname, opts)
|
16
17
|
end
|
17
18
|
|
@@ -22,13 +23,29 @@ class MyResponder < Percolate::Responder
|
|
22
23
|
def validate_recipient addr
|
23
24
|
return @recipient_validation, Responses[@recipient_validation]
|
24
25
|
end
|
26
|
+
|
27
|
+
def process_message message
|
28
|
+
return @message_validation
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
if $LOADED_FEATURES.include? "gurgitate/mailmessage.rb"
|
33
|
+
class GurgitatedResponder < MyResponder
|
34
|
+
def process_message message
|
35
|
+
message.gurgitate do
|
36
|
+
true
|
37
|
+
end
|
38
|
+
return @message_validation
|
39
|
+
end
|
40
|
+
end
|
25
41
|
end
|
26
42
|
|
43
|
+
|
27
44
|
class TestPercolateResponder < Test::Unit::TestCase
|
28
45
|
TestHostName="testhost"
|
29
46
|
|
30
47
|
def setup
|
31
|
-
@responder = Percolate::Responder.new TestHostName, :debug => false
|
48
|
+
@responder = Percolate::SMTP::Responder.new TestHostName, :debug => false
|
32
49
|
end
|
33
50
|
|
34
51
|
def test_initialize
|
@@ -241,6 +258,21 @@ class TestPercolateResponder < Test::Unit::TestCase
|
|
241
258
|
end
|
242
259
|
end
|
243
260
|
|
261
|
+
|
262
|
+
#and let's do this all again in uppercase
|
263
|
+
class UPPERCASERESPONDER < Percolate::SMTP::Responder
|
264
|
+
def command cmdtext
|
265
|
+
super(cmdtext.sub(/^w+/) do |word| word.upcase end)
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
class TestUPPERCASEResponder < TestPercolateResponder
|
270
|
+
def setup
|
271
|
+
@responder = UPPERCASERESPONDER.new TestHostName, :debug => false
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
|
244
276
|
class TestSubclassedSMTPResponder < TestPercolateResponder
|
245
277
|
def setup
|
246
278
|
@responder = MyResponder.new TestHostName, :debug => false
|
@@ -322,6 +354,42 @@ class TestSubclassedSMTPResponder < TestPercolateResponder
|
|
322
354
|
assert_match /^250 accepted, SMTP id is [0-9A-F]{16}$/, @responder.response
|
323
355
|
end
|
324
356
|
|
357
|
+
def test_data_nonapproved_message
|
358
|
+
test_rcpt_to_valid
|
359
|
+
@responder.message_validation = false
|
360
|
+
@responder.command "data"
|
361
|
+
assert_equal "354 end data with <cr><lf>.<cr><lf>",
|
362
|
+
@responder.response
|
363
|
+
@responder.command "Boxcar!"
|
364
|
+
assert_equal nil, @responder.response
|
365
|
+
@responder.command "."
|
366
|
+
assert_match /^550/, @responder.response
|
367
|
+
end
|
368
|
+
|
369
|
+
def test_data_approved_custom_response
|
370
|
+
test_rcpt_to_valid
|
371
|
+
@responder.message_validation = [ true, "honk" ]
|
372
|
+
@responder.command "data"
|
373
|
+
assert_equal "354 end data with <cr><lf>.<cr><lf>",
|
374
|
+
@responder.response
|
375
|
+
@responder.command "Boxcar!"
|
376
|
+
assert_equal nil, @responder.response
|
377
|
+
@responder.command "."
|
378
|
+
assert_match /^250 honk/, @responder.response
|
379
|
+
end
|
380
|
+
|
381
|
+
def test_data_doofus_response_message_with_text_only
|
382
|
+
test_rcpt_to_valid
|
383
|
+
@responder.message_validation = "honk"
|
384
|
+
@responder.command "data"
|
385
|
+
assert_equal "354 end data with <cr><lf>.<cr><lf>",
|
386
|
+
@responder.response
|
387
|
+
@responder.command "Boxcar!"
|
388
|
+
assert_equal nil, @responder.response
|
389
|
+
@responder.command "."
|
390
|
+
assert_match /^250 honk/, @responder.response
|
391
|
+
end
|
392
|
+
|
325
393
|
if $LOADED_FEATURES.include? "gurgitate/mailmessage.rb"
|
326
394
|
|
327
395
|
def test_to_gurgitate_mailmessage
|
@@ -340,16 +408,24 @@ class TestSubclassedSMTPResponder < TestPercolateResponder
|
|
340
408
|
assert_instance_of Gurgitate::Mailmessage, gm
|
341
409
|
assert gm.headers.match('From', /validaddress/)
|
342
410
|
assert gm.headers.match('To', /undisclosed/)
|
343
|
-
assert_match
|
411
|
+
assert_match(/Boxcar!/, gm.body)
|
344
412
|
end
|
345
413
|
end
|
346
414
|
end
|
347
415
|
|
416
|
+
if $LOADED_FEATURES.include? "gurgitate/mailmessage.rb"
|
417
|
+
class TestSubclassedSMTPResponder < TestPercolateResponder
|
418
|
+
def setup
|
419
|
+
@responder = GurgitatedResponder.new TestHostName, :debug => false
|
420
|
+
end
|
421
|
+
end
|
422
|
+
end
|
423
|
+
|
348
424
|
class TestDebug < TestPercolateResponder
|
349
425
|
def setup
|
350
426
|
@old_stderr = $stderr
|
351
427
|
$stderr = File.open("/dev/null","w")
|
352
|
-
@responder = Percolate::Responder.new TestHostName, :debug => true
|
428
|
+
@responder = Percolate::SMTP::Responder.new TestHostName, :debug => true
|
353
429
|
end
|
354
430
|
|
355
431
|
def teardown
|
metadata
CHANGED
@@ -1,54 +1,63 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
|
-
rubygems_version: 0.9.1
|
3
|
-
specification_version: 1
|
4
2
|
name: percolate-mail
|
5
3
|
version: !ruby/object:Gem::Version
|
6
|
-
version: 1.0.
|
7
|
-
date: 2007-02-11 00:00:00 +09:00
|
8
|
-
summary: Skeleton smtp daemon for you to subclass
|
9
|
-
require_paths:
|
10
|
-
- lib
|
11
|
-
email: dagbrown@lart.ca
|
12
|
-
homepage: http://percolate-mail.rubyforge.org/
|
13
|
-
rubyforge_project: percolate-mail
|
14
|
-
description:
|
15
|
-
autorequire:
|
16
|
-
default_executable:
|
17
|
-
bindir: bin
|
18
|
-
has_rdoc: true
|
19
|
-
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
|
-
requirements:
|
21
|
-
- - ">="
|
22
|
-
- !ruby/object:Gem::Version
|
23
|
-
version: 1.8.4
|
24
|
-
version:
|
4
|
+
version: 1.0.1
|
25
5
|
platform: ruby
|
26
|
-
signing_key:
|
27
|
-
cert_chain:
|
28
|
-
post_install_message:
|
29
6
|
authors:
|
30
7
|
- Dave Brown
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-10-27 00:00:00 +09:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description:
|
17
|
+
email: dagbrown@lart.ca
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files: []
|
23
|
+
|
31
24
|
files:
|
32
|
-
- lib/percolate
|
33
|
-
- lib/percolate
|
34
|
-
- lib/percolate/responder.rb
|
25
|
+
- lib/percolate/smtp/responder.rb
|
26
|
+
- lib/percolate/exceptions.rb
|
35
27
|
- lib/percolate/mail_object.rb
|
36
28
|
- lib/percolate/listener.rb
|
37
|
-
-
|
29
|
+
- lib/percolate-mail.rb
|
30
|
+
- test/test_percolate_smtp_responder.rb
|
38
31
|
- test/test_percolate_listener.rb
|
39
|
-
- test/
|
32
|
+
- test/unittest_percolate_listener.rb
|
40
33
|
- examples/percolate-chuckmail
|
41
|
-
|
34
|
+
has_rdoc: true
|
35
|
+
homepage: http://percolate-mail.rubyforge.org/
|
36
|
+
licenses: []
|
42
37
|
|
38
|
+
post_install_message:
|
43
39
|
rdoc_options: []
|
44
40
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
41
|
+
require_paths:
|
42
|
+
- lib
|
43
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 1.8.4
|
48
|
+
version:
|
49
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: "0"
|
54
|
+
version:
|
51
55
|
requirements: []
|
52
56
|
|
53
|
-
|
57
|
+
rubyforge_project: percolate-mail
|
58
|
+
rubygems_version: 1.3.5
|
59
|
+
signing_key:
|
60
|
+
specification_version: 3
|
61
|
+
summary: Skeleton smtp daemon for you to subclass
|
62
|
+
test_files: []
|
54
63
|
|
data/lib/percolate/responder.rb
DELETED
@@ -1,347 +0,0 @@
|
|
1
|
-
require "percolate/mail_object"
|
2
|
-
|
3
|
-
module Percolate
|
4
|
-
# A ResponderError exception is raised when something went horribly
|
5
|
-
# wrong for whatever reason.
|
6
|
-
#
|
7
|
-
# If you actually find one of these leaping into your lap in your
|
8
|
-
# own code, and you're not trying anything gratuitously silly, I
|
9
|
-
# want to know about it, because I have gone out of my way that
|
10
|
-
# these errors manifest themselves in an SMTPish way as error codes.
|
11
|
-
#
|
12
|
-
# If you are trying something silly though, I reserve the right to
|
13
|
-
# laugh at you. And possibly mock you on my blog. (Are you scared
|
14
|
-
# now?)
|
15
|
-
class ResponderError < Exception; end
|
16
|
-
|
17
|
-
# This is an exception that you *should* expect to receive, if you
|
18
|
-
# deal with the SMTP Responder yourself (you probably won't
|
19
|
-
# though--if you're smart, you'll just use the Listener and let it
|
20
|
-
# park itself on some port for other network services to talk to).
|
21
|
-
# All it means is that the client has just sent a "quit" message,
|
22
|
-
# and all is kosher, indicating that now would be a good time to
|
23
|
-
# clean up shop.
|
24
|
-
#
|
25
|
-
# NOTE VERY WELL THOUGH!: When you get one of these exceptions,
|
26
|
-
# there is still one response left in the pipe that you have to
|
27
|
-
# deliver to the client ("221 Pleasure doing business with you") so
|
28
|
-
# make sure you deliver that before closing the connection. It's
|
29
|
-
# only polite, after all.
|
30
|
-
class TransactionFinishedException < Exception; end
|
31
|
-
|
32
|
-
# This is the bit that actually handles the SMTP conversation with
|
33
|
-
# the client. Basically, you send it commands, and it acts on them.
|
34
|
-
# There is a small amount of Rubyish magic but that's there mainly
|
35
|
-
# because I'm lazy. And besides, if I weren't lazy, some guys on
|
36
|
-
# IRC would flame me. Haha! I kid. Greetz to RubyPanther by the
|
37
|
-
# wya. Even though I know you've never contributed anything and
|
38
|
-
# never will, at least you can live with the satisfaction that you
|
39
|
-
# don't have to deal with my evil racist ass any more.
|
40
|
-
class Responder
|
41
|
-
|
42
|
-
# Sets up the new smtp responder, with the parameter "mailhostname"
|
43
|
-
# as the SMTP hostname (as returned in the response to the SMTP
|
44
|
-
# HELO command.) Note that "mailhostname" can be anything at
|
45
|
-
# all, because I no longer believe that it's possible to
|
46
|
-
# actually figure out your own full hostname without actually
|
47
|
-
# literally being told it. This is probably excessively-cynical
|
48
|
-
# of me, but I've seen what happens when wide-eyed optimists try
|
49
|
-
# to guess hostnames, and it just isn't pretty.
|
50
|
-
#
|
51
|
-
# Let's just leave it at "mailhostname is a required parameter",
|
52
|
-
# shall we?
|
53
|
-
#
|
54
|
-
# Also, there are some interesting options you can give this.
|
55
|
-
# Well, only a couple. The first is :debug which you can set to
|
56
|
-
# true or false, which will cause it to print the SMTP
|
57
|
-
# conversation out. This is, of course, mostly only useful for
|
58
|
-
# debugging.
|
59
|
-
#
|
60
|
-
# The other option, :originating_ip, probably more interesting,
|
61
|
-
# comes from the listener--the IP address of the client that
|
62
|
-
# connected.
|
63
|
-
def initialize(mailhostname, opts={})
|
64
|
-
@verbose_debug = opts[:debug]
|
65
|
-
@originating_ip = opts[:originating_ip]
|
66
|
-
|
67
|
-
@mailhostname = mailhostname
|
68
|
-
@current_state = nil
|
69
|
-
@current_command = nil
|
70
|
-
@response = connect
|
71
|
-
@mail_object=nil
|
72
|
-
@debug_output = []
|
73
|
-
debug "\n\n"
|
74
|
-
end
|
75
|
-
|
76
|
-
# This is one of the methods you have to override in a subclass
|
77
|
-
# in order to use this class properly (unless chuckmail really
|
78
|
-
# is acceptable for you, in which case excellent! Also its
|
79
|
-
# default behaviour is to pretend to be an open relay, which
|
80
|
-
# should delight spammers until they figure out that all of
|
81
|
-
# their mail is being silently and cheerfully discarded, but
|
82
|
-
# they're spammers so they won't).
|
83
|
-
#
|
84
|
-
# Parameters:
|
85
|
-
# +message_object+:: A SMTP::MessageObject object, with envelope
|
86
|
-
# data and the message itself (which could, as
|
87
|
-
# the RFC says, be any old crap at all! Don't
|
88
|
-
# even expect an RFC2822-formatted message)
|
89
|
-
def process_message message_object
|
90
|
-
return "250 accepted, SMTP id is #{@mail_object.smtp_id}"
|
91
|
-
end
|
92
|
-
|
93
|
-
# Override this if you care about who the sender is (you
|
94
|
-
# probably do care who the sender is).
|
95
|
-
#
|
96
|
-
# Incidentally, you probably Do Not Want To Become An Open Spam
|
97
|
-
# Relay--you really should validate both senders and recipients,
|
98
|
-
# and only accept mail if:
|
99
|
-
#
|
100
|
-
# (a) the sender is local, and the recipient is remote, or
|
101
|
-
# (b) the sender is remote, and the recipient is local.
|
102
|
-
#
|
103
|
-
# The definition of "local" and "remote" are, of course, up to
|
104
|
-
# you--if you're using this to handle mail for a hundred
|
105
|
-
# domains, then all those hundred domains are local for you--but
|
106
|
-
# the idea is that you _shoud_ be picky about who your mail is
|
107
|
-
# from and to.
|
108
|
-
#
|
109
|
-
# This method takes one argument:
|
110
|
-
# +address+:: The email address you're validating
|
111
|
-
def validate_sender address
|
112
|
-
return true, "ok"
|
113
|
-
end
|
114
|
-
|
115
|
-
# Override this if you care about the recipient (which you
|
116
|
-
# should). When you get to this point, the accessor "sender"
|
117
|
-
# will work to return the sender, so that you can deal with both
|
118
|
-
# recipient and sender here.
|
119
|
-
def validate_recipient address
|
120
|
-
return true, "ok"
|
121
|
-
end
|
122
|
-
|
123
|
-
# The current message's sender, if the MAIL FROM: command has
|
124
|
-
# been processed yet. If it hasn't, then it returns nil, and
|
125
|
-
# probably isn't meaningful anyway.
|
126
|
-
def sender
|
127
|
-
if @mail_object
|
128
|
-
@mail_object.envelope_from
|
129
|
-
end
|
130
|
-
end
|
131
|
-
|
132
|
-
# Returns the response from the server. When there's no response,
|
133
|
-
# returns nil.
|
134
|
-
#
|
135
|
-
# I still haven't figured out what to do when there's
|
136
|
-
# more than one response (like in the case of an ESMTP capabilities
|
137
|
-
# list).
|
138
|
-
def response
|
139
|
-
resp = @response
|
140
|
-
@response = nil
|
141
|
-
debug "<<< #{resp}"
|
142
|
-
resp
|
143
|
-
end
|
144
|
-
|
145
|
-
# Send an SMTP command to the responder. Use this to send a
|
146
|
-
# line of input to the responder (including a single line of
|
147
|
-
# DATA).
|
148
|
-
#
|
149
|
-
# Parameters:
|
150
|
-
# +offered_command+:: The SMTP command (with end-of-line characters
|
151
|
-
# removed)
|
152
|
-
def command offered_command
|
153
|
-
debug ">>> #{offered_command}"
|
154
|
-
begin
|
155
|
-
dispatch offered_command
|
156
|
-
rescue ResponderError => error
|
157
|
-
@response = error.message
|
158
|
-
end
|
159
|
-
end
|
160
|
-
|
161
|
-
private
|
162
|
-
|
163
|
-
attr_reader :current_state
|
164
|
-
attr_reader :current_command
|
165
|
-
|
166
|
-
CommandTable = {
|
167
|
-
/^helo ([a-z0-9.-]+)$/i => :helo,
|
168
|
-
/^ehlo ([a-z0-9.-]+)$/i => :ehlo,
|
169
|
-
/^mail (.+)$/i => :mail,
|
170
|
-
/^rcpt (.+)$/i => :rcpt,
|
171
|
-
/^data$/i => :data,
|
172
|
-
/^\.$/ => :endmessage,
|
173
|
-
/^rset$/i => :rset,
|
174
|
-
/^quit$/ => :quit
|
175
|
-
}
|
176
|
-
|
177
|
-
StateTable = {
|
178
|
-
:connect => { :states => [ nil ],
|
179
|
-
:error => "This should never happen" },
|
180
|
-
:smtp_greeted_helo => { :states => [ :connect,
|
181
|
-
:smtp_greeted_helo,
|
182
|
-
:smtp_greeted_ehlo,
|
183
|
-
:smtp_mail_started,
|
184
|
-
:smtp_rcpt_received,
|
185
|
-
:message_received ],
|
186
|
-
:error => nil },
|
187
|
-
:smtp_greeted_ehlo => { :states => [ :connect ],
|
188
|
-
:error => nil },
|
189
|
-
:smtp_mail_started => { :states => [ :smtp_greeted_helo,
|
190
|
-
:smtp_greeted_ehlo,
|
191
|
-
:message_received ],
|
192
|
-
:error => "Can't say MAIL right now" },
|
193
|
-
:smtp_rcpt_received => { :states => [ :smtp_mail_started,
|
194
|
-
:smtp_rcpt_received ],
|
195
|
-
:error => "need MAIL FROM: first" },
|
196
|
-
:data => { :states => [ :smtp_rcpt_received ],
|
197
|
-
:error => "Specify sender and recipient first" },
|
198
|
-
:message_received => { :states => [ :data ],
|
199
|
-
:error => "500 command not recognized" },
|
200
|
-
:quit => { :states => [ :ANY ],
|
201
|
-
:error => nil },
|
202
|
-
}
|
203
|
-
|
204
|
-
def debug debug_string
|
205
|
-
@debugging_stream ||= []
|
206
|
-
@debugging_stream << debug_string
|
207
|
-
if @verbose_debug
|
208
|
-
$stderr.puts debug_string
|
209
|
-
end
|
210
|
-
end
|
211
|
-
|
212
|
-
def dispatch offered_command
|
213
|
-
if @current_state == :data
|
214
|
-
handle_message_data offered_command
|
215
|
-
return true
|
216
|
-
end
|
217
|
-
|
218
|
-
CommandTable.keys.each do |regex|
|
219
|
-
matchdata = regex.match offered_command
|
220
|
-
if matchdata then
|
221
|
-
return send(CommandTable[regex],*(matchdata.to_a[1..-1]))
|
222
|
-
end
|
223
|
-
end
|
224
|
-
raise ResponderError, "500 command not recognized"
|
225
|
-
end
|
226
|
-
|
227
|
-
def connect
|
228
|
-
validate_state :connect
|
229
|
-
respond "220 Ok"
|
230
|
-
end
|
231
|
-
|
232
|
-
# Makes sure that the commands are all happening in the right
|
233
|
-
# order (and complains if they're not)
|
234
|
-
def validate_state target_state
|
235
|
-
unless StateTable[target_state][:states].member? current_state or
|
236
|
-
StateTable[target_state][:states] == [ :ANY ]
|
237
|
-
raise ResponderError, "503 #{StateTable[target_state][:error]}"
|
238
|
-
end
|
239
|
-
@target_state = target_state
|
240
|
-
end
|
241
|
-
|
242
|
-
def respond response
|
243
|
-
@current_state = @target_state
|
244
|
-
@response = response
|
245
|
-
end
|
246
|
-
|
247
|
-
# The smtp commands. This is thus far not a complete set of
|
248
|
-
# every single imagineable SMTP command, but it's certainly
|
249
|
-
# enough to be able to fairly call this an "SMTP server".
|
250
|
-
#
|
251
|
-
# If you want documentation about what each of these does, see
|
252
|
-
# RFC2821, which explains it much better and in far greater
|
253
|
-
# detail than I could.
|
254
|
-
|
255
|
-
def helo remotehost
|
256
|
-
validate_state :smtp_greeted_helo
|
257
|
-
@heloname = remotehost
|
258
|
-
@mail_object = nil
|
259
|
-
@greeting_state = :smtp_greeted_helo # for rset
|
260
|
-
respond "250 #{@mailhostname}"
|
261
|
-
end
|
262
|
-
|
263
|
-
def ehlo remotehost
|
264
|
-
validate_state :smtp_greeted_ehlo
|
265
|
-
@heloname = remotehost
|
266
|
-
@mail_object = nil
|
267
|
-
@greeting_state = :smtp_greeted_ehlo # for rset
|
268
|
-
respond "250 #{@mailhostname}"
|
269
|
-
end
|
270
|
-
|
271
|
-
def mail sender
|
272
|
-
validate_state :smtp_mail_started
|
273
|
-
matchdata=sender.match(/^From:\<(.*)\>$/i);
|
274
|
-
unless matchdata
|
275
|
-
raise ResponderError, "501 bad MAIL FROM: parameter"
|
276
|
-
end
|
277
|
-
|
278
|
-
mail_from = matchdata[1]
|
279
|
-
|
280
|
-
validated, message = validate_sender mail_from
|
281
|
-
unless validated
|
282
|
-
raise ResponderError, "551 #{message}"
|
283
|
-
end
|
284
|
-
|
285
|
-
@mail_object = MailObject.new(:envelope_from => mail_from,
|
286
|
-
:heloname => @heloname,
|
287
|
-
:origin_ip => @originating_ip,
|
288
|
-
:myhostname => @mailhostname)
|
289
|
-
respond "250 #{message}"
|
290
|
-
end
|
291
|
-
|
292
|
-
def rcpt recipient
|
293
|
-
validate_state :smtp_rcpt_received
|
294
|
-
matchdata=recipient.match(/^To:\<(.*)\>$/i);
|
295
|
-
unless matchdata
|
296
|
-
raise ResponderError, "501 bad RCPT TO: parameter"
|
297
|
-
end
|
298
|
-
|
299
|
-
rcpt_to = matchdata[1]
|
300
|
-
|
301
|
-
@mail_object.envelope_to ||= []
|
302
|
-
|
303
|
-
validated, message = validate_recipient rcpt_to
|
304
|
-
unless validated
|
305
|
-
raise ResponderError, "551 #{message}"
|
306
|
-
end
|
307
|
-
@mail_object.envelope_to << rcpt_to
|
308
|
-
respond "250 #{message}"
|
309
|
-
end
|
310
|
-
|
311
|
-
def data
|
312
|
-
validate_state :data
|
313
|
-
@mail_object.content ||= ""
|
314
|
-
respond "354 end data with <cr><lf>.<cr><lf>"
|
315
|
-
end
|
316
|
-
|
317
|
-
def rset
|
318
|
-
if @mail_object
|
319
|
-
@mail_object = nil
|
320
|
-
@target_state = @greeting_state
|
321
|
-
end
|
322
|
-
respond "250 Ok"
|
323
|
-
end
|
324
|
-
|
325
|
-
def quit
|
326
|
-
validate_state :quit
|
327
|
-
if @mail_object
|
328
|
-
@mail_object = nil
|
329
|
-
end
|
330
|
-
respond "221 Pleasure doing business with you"
|
331
|
-
raise TransactionFinishedException
|
332
|
-
end
|
333
|
-
|
334
|
-
# The special-case code for message data, which unlike other
|
335
|
-
# data, is delivered as lots and lots of lines with a sentinel.
|
336
|
-
def handle_message_data message_line
|
337
|
-
if message_line == "."
|
338
|
-
@current_state = :message_received
|
339
|
-
@response = process_message @mail_object
|
340
|
-
elsif message_line == ".."
|
341
|
-
@mail_object.content << "." + "\r\n"
|
342
|
-
else
|
343
|
-
@mail_object.content << message_line + "\r\n"
|
344
|
-
end
|
345
|
-
end
|
346
|
-
end
|
347
|
-
end
|