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.
@@ -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