percolate-mail 1.0.0 → 1.0.1
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.
- 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
|