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.
- checksums.yaml +7 -0
- data/.gitignore +3 -0
- data/CHANGELOG.md +0 -0
- data/README.md +345 -0
- data/lib/rubymta/base-x.rb +47 -0
- data/lib/rubymta/contact.rb +100 -0
- data/lib/rubymta/deepclone.rb +23 -0
- data/lib/rubymta/extended_classes.rb +168 -0
- data/lib/rubymta/item_of_mail.rb +113 -0
- data/lib/rubymta/queue_runner.rb +376 -0
- data/lib/rubymta/receiver.rb +615 -0
- data/lib/rubymta/server.rb +306 -0
- data/lib/rubymta/version.rb +5 -0
- data/lib/rubymta.rb +2 -0
- data/rubymta.gemspec +15 -0
- data/spec/coco +3 -0
- data/spec/rubymta.rb +199 -0
- metadata +60 -0
@@ -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
|