rubymta 0.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,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