rubymta 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,615 @@
1
+ require 'timeout'
2
+ require 'pdkim'
3
+ require_relative "item_of_mail"
4
+ require_relative "contact"
5
+ require_relative "extended_classes"
6
+ require_relative "queue_runner"
7
+ require 'pretty_inspect'
8
+
9
+ CRLF = "\r\n"
10
+
11
+ class Receiver
12
+ # PDKIM Verify Codes
13
+ PdkimReturnCodes = ["0-Verify not completed", "1-Verify invalid", "2-Verify failed", "3-Verify passed"]
14
+
15
+ include Config
16
+ include PDKIM
17
+ include Version
18
+
19
+ Patterns = [
20
+ [0, "[ /t]*QUIT[ /t]*", :quit],
21
+ [1, "[ /t]*AUTH[ /t]*(.+)", :auth_base],
22
+ [1, "[ /t]*EHLO(.*)", :ehlo_base],
23
+ [1, "[ /t]*EXPN[ /t]*(.*)", :expn_base],
24
+ [1, "[ /t]*HELO[ /t]+(.*)", :ehlo_base],
25
+ [1, "[ /t]*HELP[ /t]*(.*)", :help_base],
26
+ [1, "[ /t]*NOOP[ /t]*(.*)", :noop_base],
27
+ [1, "[ /t]*RSET[ /t]*(.*)", :rset_base],
28
+ [1, "[ /t]*TIMEOUT[ /t]*", :timeout],
29
+ [1, "[ /t]*VFRY[ /t]*(.*)", :vfry_base],
30
+ [2, "[ /t]*STARTTLS[ /t]*", :starttls],
31
+ [2, "[ /t]*MAIL FROM[ /t]*:[ \t]*(.+)", :mail_from_base],
32
+ [3, "[ /t]*RCPT TO[ /t]*:[ \t]*(.+)", :rcpt_to_base],
33
+ [4, "[ /t]*DATA[ /t]*", :data_base]
34
+ ]
35
+
36
+ def initialize(connection)
37
+ @connection = connection
38
+ end
39
+
40
+ Unexpectedly = "; probably caused by the client closing the connection unexpectedly"
41
+
42
+ #-------------------------------------------------------#
43
+ #--- Send text to the client ---------------------------#
44
+ #-------------------------------------------------------#
45
+ def send_text(text,echo=true)
46
+ puts "<- #{text.inspect}" if DisplayReceiverDialog
47
+ begin
48
+ case
49
+ when text.nil?
50
+ # do nothing
51
+ when text.class==Array
52
+ text.each do |line|
53
+ @connection.write(line+CRLF)
54
+ LOG.info(@mail[:mail_id]) {"<- #{line}"} if echo && LogReceiverConversation
55
+ end
56
+ return text.last[0]
57
+ else
58
+ @connection.write(text+CRLF)
59
+ LOG.info(@mail[:mail_id]) {"<- #{text}"} if echo && LogReceiverConversation
60
+ return text[0]
61
+ end
62
+ rescue Errno::EPIPE => e
63
+ LOG.error(@mail[:mail_id]) {"#{e.to_s}#{Unexpectedly}"}
64
+ raise Quit
65
+ rescue Errno::EIO => e
66
+ LOG.error(@mail[:mail_id]) {"#{e.to_s}#{Unexpectedly}"}
67
+ raise Quit
68
+ end
69
+ end
70
+
71
+ #-------------------------------------------------------#
72
+ #--- Receive text from the client ----------------------#
73
+ #-------------------------------------------------------#
74
+ def recv_text(echo=true)
75
+ begin
76
+ Timeout.timeout(ReceiverTimeout) do
77
+ begin
78
+ temp = @connection.gets
79
+ if temp.nil?
80
+ LOG.warn(@mail[:mail_id]) {"The client abruptly closed the connection"}
81
+ text = nil
82
+ else
83
+ text = temp.chomp
84
+ end
85
+ rescue Errno::ECONNRESET => e
86
+ LOG.warn(@mail[:mail_id]) {"The client slammed the connection shut"}
87
+ text = nil
88
+ end
89
+ LOG.info(@mail[:mail_id]) {" -> #{if text.nil? then "<eod>" else text end}"} \
90
+ if echo && LogReceiverConversation
91
+ puts " -> #{text.inspect}" if DisplayReceiverDialog
92
+ return text
93
+ end
94
+ rescue Errno::EIO => e
95
+ LOG.error(@mail[:mail_id]) {"#{e.to_s}#{Unexpectedly}"}
96
+ raise Quit
97
+ rescue Timeout::Error => e
98
+ LOG.info(@mail[:mail_id]) {" -> <eod>"} if LogReceiverConversation
99
+ return nil
100
+ end
101
+ end
102
+
103
+ #-------------------------------------------------------#
104
+ #--- Parse the email address and investigate it --------#
105
+ #-------------------------------------------------------#
106
+ def psych_value(part, value)
107
+ # these get set in both MAIL FROM and RCPT TO
108
+ part[:value] = value
109
+ part[:accepted] = false
110
+
111
+ # check for the special case of "... <postmaster>"
112
+ n = value.match(/^(.*)<(.+)>$/)
113
+ if n && n[2].downcase=="postmaster"
114
+ m = Array.new
115
+ m[0] = n[0]
116
+ m[1] = n[1]
117
+ m[2] = PostMasterName
118
+ else
119
+ # parse out the name (if any) and the address (required)
120
+ m = value.match(/^(.*)<(.+@.+\..+)>$/)
121
+ # there MUST be a sender/recipient address
122
+ return false if m.nil?
123
+ end
124
+
125
+ # break up the address
126
+ part[:name] = m[1].strip
127
+ part[:url] = url = m[2].strip
128
+
129
+ # parse out the local-part and domain
130
+ local_part, domain = url.split("@")
131
+ part[:local_part] = local_part
132
+ part[:domain] = domain
133
+
134
+ # check the local part for validity?
135
+ # Uppercase and lowercase English letters (a-z, A-Z)
136
+ # Digits 0 to 9
137
+ # Characters ! # $ % & ' * + - / = ? ^ _ ` { | } ~
138
+ # Character . provided that it is not the first or last character,
139
+ # and provided also that it does not appear two or more times consecutively.
140
+ part[:dot_error] = true if (local_part[0]=='.' || local_part[-1]=='.' || local_part.index('..'))
141
+ m = local_part.match(/^[a-zA-Z0-9\!\#\$%&'*+-\/?^_`{|}~]+$/)
142
+ part[:char_error] = m.nil?
143
+
144
+ # lookup the email to see if it's one of ours
145
+ if respond_to?(:client_lookup)
146
+ part[:mailbox_id], part[:owner_id], part[:delivery] = client_lookup(part[:url])
147
+ else
148
+ part[:mailbox_id], part[:owner_id], part[:delivery] = [nil, nil, :remote]
149
+ end
150
+
151
+ # get the MXs, if needed and if any --
152
+ # if we deliver to a mailbox which has an owner_id,
153
+ # delivery will be made with LMTP and no MXs will be needed
154
+ part[:mxs] = mxs = if part[:owner_id].nil? then domain.dig_mxs else nil end
155
+
156
+ return true
157
+ #---------------------------------------------------------------------------------------#
158
+ #--- WHAT WE KNOW AFTER PSYCH
159
+ #--- 1. if the return value is true, the value has the correct form
160
+ #--- 2. the url is in part[:url]
161
+ #--- 3. the part[:local_part] and part[:domain] are have values
162
+ #--- 4. the MXs, if any, are in part[:mxs] => { preference => [ [mx,ip], ... ], ... }
163
+ #--- 5. if it's our member, part[:mailbox_id], part[:owner_id] have values
164
+ #---------------------------------------------------------------------------------------#
165
+ end
166
+
167
+ #-------------------------------------------------------#
168
+ #--- LOOP TO RECEIVE COMMANDS --------------------------#
169
+ #-------------------------------------------------------#
170
+
171
+ def receive(local_port, local_hostname, remote_port, remote_hostname, remote_ip)
172
+ # Start a hash to collect the information gathered from the receive process
173
+ @mail = ItemOfMail::new
174
+ @mail[:local_port] = local_port
175
+ @mail[:local_hostname] = local_hostname
176
+ @mail[:remote_port] = remote_port
177
+ @mail[:remote_hostname] = remote_hostname
178
+ @mail[:remote_ip] = remote_ip
179
+
180
+ # start the main receiving process here
181
+ @done = false
182
+ @encrypted = false
183
+ @authenticated = false
184
+ @mail[:encrypted] = false
185
+ @mail[:authenticated] = nil
186
+ send_text(connect_base)
187
+ @level = 1
188
+ response = "252 2.5.1 Administrative prohibition"
189
+ begin
190
+ begin
191
+ break if @done
192
+ text = recv_text
193
+ if (text.nil?) # the client closed the channel abruptly
194
+ text = "QUIT"
195
+ @contact.violation
196
+ end
197
+ unrecognized = true
198
+ Patterns.each do |pattern|
199
+ break if pattern[0]>@level
200
+ m = text.match(/^#{pattern[1].upcase}$/i)
201
+ if m
202
+ case
203
+ when pattern[2]==:quit
204
+ send_text(quit(m[1]))
205
+ when pattern[0]>@level
206
+ send_text("500 5.5.1 Command out of sequence")
207
+ else
208
+ response = send(pattern[2], m[1])
209
+ @contact.violation if send_text(response)=='5'
210
+ end
211
+ unrecognized = false
212
+ break
213
+ end
214
+ end
215
+ if unrecognized
216
+ response = "500 5.5.1 Unrecognized command #{text.inspect}, incorrectly formatted command, or command out of sequence"
217
+ @contact.violation
218
+ send_text(response)
219
+ end
220
+ rescue =>e #OpenSSL::SSL::SSLError => e
221
+ LOG.error(@mail[:mail_id]) {"SSL error: #{e.to_s}"}
222
+ e.backtrace.each { |line| LOG.error(@mail[:mail_id]) {line} }
223
+ @done = true
224
+ end until @done
225
+ rescue => e
226
+ LOG.fatal(@mail[:mail_id]) {e.to_s}
227
+ exit(1)
228
+ end
229
+
230
+ # print the intermediate structure into the log (for debugging)
231
+ (LOG.info(@mail[:mail_id]) { "Received Mail:\n#{@mail.pretty_inspect}" }) if DumpMailIntoLog
232
+
233
+ ensure
234
+ # make sure the incoming email is saved, in case there was a receive error;
235
+ # otherwise, it gets saved just before the "250 OK" in the DATA section
236
+ if !@mail[:saved]
237
+ LOG.error(@mail[:mail_id]) {"#{@mail[:mail_id]} was not received completely. Saving the partial copy to queue."}
238
+
239
+ # the email is faulty--save for reference
240
+ case
241
+ when !@mail.insert_parcels
242
+ LOG.error(@mail[:mail_id]) {"#{ServerName} error: unable to save packet id=#{@mail[:mail_id]}"}
243
+ when !@mail.save_mail_into_queue_folder
244
+ LOG.error(@mail[:mail_id]) {"#{ServerName} error: unable to save queue id=#{@mail[:mail_id]}"}
245
+ end
246
+ end
247
+
248
+ # run the mail queue queue runner now, if it's not running already
249
+ ok = nil
250
+ File.open(LockFilePath,"w") do |f|
251
+ ok = f.flock( File::LOCK_NB | File::LOCK_EX )
252
+ f.flock(File::LOCK_UN) if ok!=false
253
+ end
254
+ if ok!=false
255
+ pid = Process::spawn("#{$app[:path]}/run_queue.rb")
256
+ Process::detach(pid)
257
+ end
258
+ end
259
+
260
+ #-------------------------------------------------------#
261
+ #--- SMTP COMMAND HANDLING METHODS ---------------------#
262
+ #-------------------------------------------------------#
263
+
264
+ def connect_base
265
+ @contact = Contact.new(@mail[:remote_ip])
266
+ raise StandardError.new("contact.new failed; see log") if @contact.nil?
267
+
268
+ LOG.info(@mail[:mail_id]) {"New item of mail opened with id '#{@mail[:mail_id]}'"}
269
+
270
+ if @contact.prohibited?
271
+ # after the first denied message, we just slam the channel shut: no more nice guy
272
+ LOG.warn(@mail[:mail_id]) {"Slammed connection shut. No more nice guy with #{@mail[:remote_ip]}"}
273
+ raise Quit
274
+ end
275
+
276
+ if @contact.warning?
277
+ # this is the first denied message
278
+ expires_at = @contact.violation.strftime('%Y-%m-%d %H:%M:%S %Z') # to kick it up to prohibited
279
+ LOG.warn(@mail[:mail_id]) {"Access TEMPORARILY denied to #{@mail[:remote_ip]} (#{@mail[:remote_hostname]}) until #{expires_at}"}
280
+ return "454 4.7.1 Access TEMPORARILY denied to #{@mail[:remote_ip]}: you may try again after #{expires_at}"
281
+ end
282
+
283
+ if respond_to?(:connect)
284
+ msg = connect(value)
285
+ return msg if !msg.nil?
286
+ end
287
+
288
+ # 8 bells and all is well
289
+ @level = 1
290
+ return "220 2.0.0 #{@mail[:local_hostname]} ESMTP RubyMTA 0.01 #{Time.new.strftime("%^a, %d %^b %Y %H:%M:%S %z")}"
291
+ end
292
+
293
+ def ehlo_base(value)
294
+ @mail[:ehlo] = ehlo = {}
295
+ ehlo[:value] = value
296
+
297
+ # The email specs call for EHLO or HELO to be followed by a domain,
298
+ # but this behavior can be turned off, if you want -- also, we look
299
+ # to see if it's a real domain (well, duh! makes sense to do that)
300
+ if EhloDomainRequired
301
+ if value.index(".")
302
+ ehlo[:domain] = domain = value.split(".").collect{ |item| item.strip }[-2..-1].join(".")
303
+ ehlo[:ip] = ip = if EhloDomainVerifies then domain.dig_a else nil end
304
+ else
305
+ ehlo[:domain] = nil
306
+ ehlo[:ip] = nil
307
+ end
308
+
309
+ return "501 5.5.1 Domain required after EHLO/HELO" \
310
+ if ehlo.nil? || ehlo[:domain].nil?
311
+ return "502 5.1.8 EHLO domain #{ehlo[:domain].inspect} was not found in the DNS system (maybe a fake domain?)" \
312
+ if EhloDomainVerifies && ehlo[:ip].nil? && @mail[:local_port]==StandardMailPort
313
+ end
314
+
315
+ if respond_to?(:ehlo)
316
+ msg = ehlo(value)
317
+ return msg if !msg.nil?
318
+ end
319
+
320
+ text = "250-2.0.0 #{ServerName} Hello"
321
+ text << " #{domain}" if domain
322
+ text << " at #{ip}" if ip
323
+ @level = 2
324
+ return [text, "250-AUTH PLAIN", "250-STARTTLS", "250 HELP"]
325
+ end
326
+
327
+ #-------------------------------------------------------#
328
+ #--- Sender --------------------------------------------#
329
+ #-------------------------------------------------------#
330
+ def mail_from_base(value)
331
+ @mail[:mailfrom] = from = {}
332
+ @mail[:rcptto] = []
333
+ from[:accepted] = false
334
+ ok = psych_value(from, value)
335
+
336
+ # these criteria MUST be met for any sender
337
+ return "550 5.1.7 '#{from[:value]}' No proper sender (<...>) on the MAIL FROM line" if !ok
338
+
339
+ # we check to see if this is a reasonable MAIL FROM address, or garbage
340
+ return "550-5.1.7 local part #{from[:local_part].inspect} cannot contain", \
341
+ "550 5.1.7 beginning or ending '.' or 2 or more '.'s in a row" \
342
+ if from[:dot_error]
343
+ return "550-5.1.7 #{from[:local_part].inspect} can only", \
344
+ "550 5.1.7 contain a-z, A_Z, 0-9, and !#\$%&'*+-/?^_`{|}~." \
345
+ if from[:char_error]
346
+
347
+ LOG.info(@mail[:mail_id]) {"Receiving mail from sender #{from[:url]}"}
348
+
349
+ # Check to see if this sender is one of ours -- how that is done is up to you --
350
+ # You must implement 'client_lookup(url)' where url is the full email address --
351
+ # Also, members MUST use use authenticated email on the SubmissionPort to
352
+ # submit mail; non-members MUST use non-authenticated email on the
353
+ # StandardMailPort to submit mail
354
+ if (from[:mailbox_id]) && (@mail[:local_port]!=InternalSubmitPort)
355
+ # traffic is from our member
356
+ return "556 5.7.27 #{ServerTitle} members must use port #{SubmissionPort} to send mail" \
357
+ if @mail[:local_port]!=SubmissionPort
358
+ return "556 5.7.27 Traffic on port #{SubmissionPort} must be authenticated (i.e., #{ServerTitle} client)" \
359
+ if !@mail[:authenticated]
360
+ return "556 5.7.27 Traffic on port #{SubmissionPort} must be encrypted" \
361
+ if !@mail[:encrypted]
362
+ else
363
+ # traffic is from a non-member
364
+ return "556 5.7.27 Non #{ServerTitle} members must use port #{StandardMailPort} to send mail" \
365
+ if !from[:mailbox_id] && @mail[:local_port]!=StandardMailPort
366
+ end
367
+
368
+ if respond_to?(:mail_from)
369
+ msg = mail_from(value)
370
+ return msg if !msg.nil?
371
+ end
372
+
373
+ @level = 3
374
+ from[:accepted] = true
375
+ return "250 2.0.0 OK"
376
+ end
377
+
378
+ #-------------------------------------------------------#
379
+ #--- Recipient -----------------------------------------#
380
+ #-------------------------------------------------------#
381
+ def rcpt_to_base(value)
382
+ @mail[:rcptto] ||= []
383
+ @mail[:rcptto] << rcpt = {}
384
+ rcpt[:accepted] = false
385
+ ok = psych_value(rcpt, value)
386
+
387
+ # these criteria MUST be met for any recipient
388
+ if !ok
389
+ rcpt[:message] = "'#{value}' No proper recipient (<...>) on the RCPT TO line"
390
+ LOG.info(@mail[:mail_id]) {rcpt[:message]}
391
+ return "550 5.1.7 #{rcpt[:message]}"
392
+ end
393
+
394
+ # use the rcpt_to(value) method in the configuration file to add
395
+ # more rules for filtering recipients; psych_value will determine if
396
+ # the recipient is a member, if you have a 'client_lookup(url)', as mentioned above
397
+ if respond_to?(:rcpt_to)
398
+ msg = rcpt_to(value)
399
+ return msg if !msg.nil?
400
+ end
401
+
402
+ @contact.allow
403
+ @level = 4
404
+ rcpt[:accepted] = true
405
+ return "250 2.0.0 ACCEPTED"
406
+ end
407
+
408
+ #-------------------------------------------------------#
409
+ #--- Data ----------------------------------------------#
410
+ #-------------------------------------------------------#
411
+ def data_base(value)
412
+ @mail[:data] = body = {}
413
+
414
+ # make sure that there is at least 1 recipient
415
+ count = 0;
416
+ @mail[:rcptto].each { |rcpt| count += 1 if rcpt[:accepted] }
417
+ @mail[:recipients] = count
418
+ return "500 5.0.0 There must be at least 1 acceptable recipient" if count==0
419
+
420
+ # receive the body of the mail
421
+ body[:value] = value # this should be nil -- no argument on the DATA command
422
+ body[:text] = lines = []
423
+ send_text("354 Enter message, ending with \".\" on a line by itself")
424
+ LOG.info(@mail[:mail_id]) {" -> (email message)"} if LogReceiverConversation && !ShowIncomingData
425
+ while true
426
+ text = recv_text(ShowIncomingData)
427
+ if text.nil? # the client closed the channel abruptly
428
+ @mail.add_block(nil, 5)
429
+ break
430
+ end
431
+ break if text=="."
432
+ lines << text
433
+ end
434
+
435
+ # hold the new headers here (insert them down below)
436
+ new_headers = []
437
+
438
+ # check DKIM signatures, if any
439
+ pdkim = []
440
+ ok, signatures = pdkim_verify_an_email(PDKIM_INPUT_NORMAL, lines)
441
+ signatures.each do |signature|
442
+ pdkim << (status = PdkimReturnCodes[signature[:verify_status]])
443
+ end
444
+ if !pdkim.empty?
445
+ body[:pdkim] = pdkim
446
+ LOG.info(@mail[:mail_id]) {"DKIM signatures (from last to first): #{body[:pdkim].inspect})"}
447
+ new_headers << "DKIM-Status: #{body[:pdkim].inspect[1..-2]}" # strip off the '[]'
448
+ end
449
+
450
+ #Return-Path: <coco@tzarmail.com>
451
+ new_headers << "Return-Path: <#{@mail[:mailfrom][:url]}>"
452
+
453
+ #Delivered-To: <mike@tzarmail.com>
454
+ new_headers << "Delivered-To: <#{@mail[:rcptto][0][:url]}>"
455
+ @mail[:rcptto][1..-1].each do |rcpt|
456
+ new_headers << "\t<#{rcpt[:url]}>"
457
+ end
458
+
459
+ #Received: from cpe-107-185-187-182.socal.res.rr.com ([::ffff:107.185.187.182])
460
+ # by mail.tzarmail.com (RubyMTA 0.0.1) with ESMTP
461
+ # (envelope from <coco@tzarmail.com>)
462
+ # id 1dYwrI-0iDWWN-0y; Sat, 22 Jul 2017 16:02:24 +0000
463
+ new_headers << "Received: from #{@mail[:remote_hostname]} ([#{@mail[:remote_ip]}])"
464
+ new_headers << "\tby #{@mail[:local_hostname]} (RubyMTA #{VERSION}) with ESMTP"
465
+ new_headers << "\t(envelope from <#{@mail[:mailfrom][:url]}>)"
466
+ new_headers << "\tid #{@mail[:mail_id]}; #{@mail[:time]}"
467
+
468
+ # insert the new headers into the message text
469
+ new_headers.reverse.each { |hdr| @mail[:data][:text].insert(0,hdr) }
470
+
471
+ # always add a DKIM signature which will include our headers
472
+ if $app[:dkim] && !@mail[:dkim_added]
473
+ ok, signed_message = pdkim_sign_an_email(PDKIM_INPUT_NORMAL, ServerName, 'key', $app[:dkim], PDKIM_CANON_SIMPLE, PDKIM_CANON_SIMPLE, @mail[:data][:text])
474
+ if ok==PDKIM_OK
475
+ @mail[:data][:text] = signed_message
476
+ @mail[:dkim_added] = true
477
+ else
478
+ LOG.info(@mail[:mail_id]) {"Unsuccessful at signing #{mail[:id]} to #{host}"}
479
+ end
480
+ end
481
+
482
+ # parse the headers for easier inspection, if any
483
+ @mail.parse_headers
484
+ @level = 1
485
+
486
+ if respond_to?(:data)
487
+ msg = data(value)
488
+ return msg if !msg.nil?
489
+ end
490
+
491
+ #-------------------------------------------------------#
492
+ #--- EMail queueing here -------------------------------#
493
+ #-------------------------------------------------------#
494
+ LOG.info(@mail[:mail_id]) {"#{@mail[:mail_id]} accepted with #{count} recipient#{if count>1 then 's' end}"}
495
+
496
+ # the email appears good, queue it
497
+ @mail[:accepted] = true
498
+ case
499
+ when !@mail.insert_parcels
500
+ "500 5.0.0 #{ServerName} error: unable to save packet id=#{@mail[:mail_id]}"
501
+ when !@mail.save_mail_into_queue_folder
502
+ "500 5.0.0 #{ServerName} error: unable to save queue id=#{@mail[:mail_id]}"
503
+ else
504
+ "250 2.0.0 OK id=#{@mail[:mail_id]}"
505
+ end
506
+ end
507
+
508
+ #-------------------------------------------------------#
509
+ #--- Reset ---------------------------------------------#
510
+ #-------------------------------------------------------#
511
+ def rset_base(value)
512
+ if respond_to?(:rset)
513
+ msg = rset(value)
514
+ return msg if !msg.nil?
515
+ end
516
+
517
+ @level = 0
518
+ return "250 2.0.0 Reset OK"
519
+ end
520
+
521
+ def vfry_base(value)
522
+ # SMTP includes commands called "VRFY" and "EXPN" which do exactly what verification services offer.
523
+ # While those two functions are technically different, they both reveal to a third party whether email
524
+ # addresses exist in the server's userbase. Nearly every Postmaster (mail server administrator) on the
525
+ # Internet has turned off VRFY and EXPN due to abuse by spammers trying to harvest addresses, as well
526
+ # as a general security and privacy measure required by most network's operational policies. In fact,
527
+ # since about 1999 or before, all mail servers are installed with those off by default. That should
528
+ # give a clear indication to email verifiers about the opinion of Postmasters of the service they
529
+ # intend to offer. Doing verification against systems that have disabled those functions, whether
530
+ # successful or not, constitutes an attempted breach of the receiver's security policies and may be
531
+ # considered a hostile act by site administrators. Sending high volumes of verification probes without
532
+ # an attempt to actually send an email will often trigger filters or firewalls, thus invalidating the
533
+ # data and impairing future verification accuracy.
534
+ # -- http://www.spamhaus.org/news/article/722/on-the-dubious-merits-of-email-verification-services
535
+ #
536
+ # What this means for us is: if a spammer sends spam and we try to validate the sender's email
537
+ # address, or bounce the message, and it's a SPAMHAUS or other blacklist company's trap address,
538
+ # *WE* will be blacklisted. So we don't use VFRY or EXPN, and don't use a EHLO, MAIL FROM, RCPT TO,
539
+ # QUIT sequence either. The takeaway here: thanks to spammers and Spamhaus, one can't verify a
540
+ # sender's or recipient's address safely.
541
+
542
+ if respond_to?(:vfry)
543
+ msg = vfry(value)
544
+ return msg if !msg.nil?
545
+ end
546
+
547
+ return "252 2.5.1 Administrative prohibition"
548
+ end
549
+
550
+ def expn_base(value)
551
+ if respond_to?(:expn)
552
+ msg = expn(value)
553
+ return msg if !msg.nil?
554
+ end
555
+
556
+ return "252 2.5.1 Administrative prohibition"
557
+ end
558
+
559
+ def help_base(value)
560
+ if respond_to?(:help)
561
+ msg = help(value)
562
+ return msg if !msg.nil?
563
+ end
564
+
565
+ return "250 2.0.0 QUIT AUTH, EHLO, EXPN, HELO, HELP, NOOP, RSET, VFRY, STARTTLS, MAIL FROM, RCPT TO, DATA"
566
+ end
567
+
568
+ def noop_base(value)
569
+ if respond_to?(:noop)
570
+ msg = noop(value)
571
+ return msg if !msg.nil?
572
+ end
573
+
574
+ return "250 2.0.0 OK"
575
+ end
576
+
577
+ def quit(value)
578
+ @done = true
579
+ if @mail[:saved].nil?
580
+ LOG.warn(@mail[:mail_id]) {"Quitting before a message is finished is considered a violation"}
581
+ @contact.violation
582
+ end
583
+ return "221 2.0.0 OK #{ServerName} closing connection"
584
+ end
585
+
586
+ # This method MUST be supplemented to use AUTH -- if the authentication
587
+ # succeeds, a "235 2.0.0 Authentication succeeded" message should be
588
+ # returned; otherwise a "530 5.7.8 Authentication failed" error should
589
+ # be returned
590
+ def auth_base(value)
591
+ if respond_to?(:auth)
592
+ msg = auth(value)
593
+ return msg if !msg.nil?
594
+ end
595
+
596
+ return "504 5.7.4 authentication mechanism not supported"
597
+ end
598
+
599
+ # These are not overrideable
600
+
601
+ def starttls(value)
602
+ send_text("220 2.0.0 TLS go ahead")
603
+ LOG.info(@mail[:mail_id]) {"<-> (handshake)"} if LogReceiverConversation
604
+ @connection.accept
605
+ @encrypted = true
606
+ @mail[:encrypted] = true
607
+ return nil
608
+ end
609
+
610
+ def timeout(value)
611
+ @done = true
612
+ return ("500 5.7.1 #{"<mail id>"} closing connection due to inactivity--%s was NOT saved")
613
+ end
614
+
615
+ end