rubymta 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|