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.
@@ -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
@@ -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
- @ipaddress = "0.0.0.0"
35
- @hostname = "localhost"
36
- @port = 10025
37
- @responder = Responder
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
- pid = handle_connection mailsocket
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
- # in Win32 to get "fork" to work, but hey,
77
- # maybe someone did so anyway.
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.puts responder.response + CRLF
87
+ mailsocket.print responder.response + CRLF
91
88
  mailsocket.close
92
89
  exit!
93
- rescue
94
- mailsocket.puts "421 Server confused, shutting down" +
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 transaction.
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/mailmessage"
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
- "To: undisclosed recipients:;\n" +
91
- "X-Gurgitate-Error: #{$!}\n" +
92
- "\n" +
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 /Boxcar!/, gm.body
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.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-mail.rb
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
- - test/unittest_percolate_listener.rb
29
+ - lib/percolate-mail.rb
30
+ - test/test_percolate_smtp_responder.rb
38
31
  - test/test_percolate_listener.rb
39
- - test/test_percolate_responder.rb
32
+ - test/unittest_percolate_listener.rb
40
33
  - examples/percolate-chuckmail
41
- test_files: []
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
- extra_rdoc_files: []
46
-
47
- executables: []
48
-
49
- extensions: []
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
- dependencies: []
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
 
@@ -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