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,168 @@
|
|
1
|
+
require 'resolv'
|
2
|
+
require 'base64'
|
3
|
+
require 'unix_crypt'
|
4
|
+
|
5
|
+
class QueryError < StandardError; end
|
6
|
+
|
7
|
+
class NilClass
|
8
|
+
|
9
|
+
# these defs allow for the case where something wasn't found to
|
10
|
+
# give a nil response rather than crashing--for example:
|
11
|
+
# mx = "example.com" # => nil (because example.com has no MX record)
|
12
|
+
# ip = mx.dig_a # => nil, without crashing
|
13
|
+
# otherwise, it would be necessary to write:
|
14
|
+
# mx = "example.com" # => nil (because example.com has no MX record)
|
15
|
+
# ip = if mx then ip = mx.dig_a else ip = nil end
|
16
|
+
def dig_a; nil; end
|
17
|
+
def dig_aaaa; nil; end
|
18
|
+
def dig_mx; nil; end
|
19
|
+
def dig_dk; nil; end
|
20
|
+
def dig_ptr; nil; end
|
21
|
+
def mta_live?(port); nil; end
|
22
|
+
def validate_plain; return "", false; end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
class String
|
27
|
+
|
28
|
+
# returns list of IPV4 addresses, or nil
|
29
|
+
# (there should only be one IPV4 address)
|
30
|
+
def dig_a
|
31
|
+
Resolv::DNS.open do |dns|
|
32
|
+
txts = dns.getresources(self,Resolv::DNS::Resource::IN::A).collect { |r| r.address.to_s }
|
33
|
+
if txts.empty? then nil else txts[0] end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# returns list of IPV6 addresses, or nil
|
38
|
+
# (there should only be one IPV6 address)
|
39
|
+
def dig_aaaa
|
40
|
+
Resolv::DNS.open do |dns|
|
41
|
+
txts = dns.getresources(self,Resolv::DNS::Resource::IN::AAAA).collect { |r| r.address.to_s.downcase }
|
42
|
+
if txts.empty? then nil else txts[0] end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# returns list of MX names, or nil
|
47
|
+
# (there may be multiple MX names for a domain)
|
48
|
+
# WARNING: use the #dig_mxs to get the preferences also
|
49
|
+
def dig_mx
|
50
|
+
Resolv::DNS.open do |dns|
|
51
|
+
txts = dns.getresources(self,Resolv::DNS::Resource::IN::MX).collect { |r| r.exchange.to_s }
|
52
|
+
if txts.empty? then nil else txts end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# returns a hash of { <preference> => [ <mx,ip>, ... ] }
|
57
|
+
# preferences sorted in numerical order from low-->high
|
58
|
+
# (IOW, highest preference to lowest preference)
|
59
|
+
# if there is no mx, returns {}
|
60
|
+
# ex: {10=>[["aspmx.l.google.com", "173.194.78.26"]],
|
61
|
+
# 20=>[["alt1.aspmx.l.google.com", "173.194.219.27"]],
|
62
|
+
# 30=>[["alt2.aspmx.l.google.com", "74.125.192.27"], ["alt3.aspmx.l.google.com", "74.125.141.27"]],
|
63
|
+
# 40=>[["alt4.aspmx.l.google.com", "64.233.190.26"]]}
|
64
|
+
def dig_mxs
|
65
|
+
mxs = {}
|
66
|
+
Resolv::DNS.open do |dns|
|
67
|
+
res = dns.getresources(self,Resolv::DNS::Resource::IN::MX)
|
68
|
+
if res.size>0
|
69
|
+
res = res.sort {|a,b| a.preference<=>b.preference }
|
70
|
+
res.each do |mx|
|
71
|
+
mxs[mx.preference] ||= []
|
72
|
+
domain = mx.exchange.to_s
|
73
|
+
ip = domain.dig_a
|
74
|
+
mxs[mx.preference] << [domain,ip]
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
return mxs
|
79
|
+
end
|
80
|
+
|
81
|
+
# returns a publibdomainkey, or nil
|
82
|
+
# (there should only be one DKIM public key)
|
83
|
+
def dig_dk
|
84
|
+
Resolv::DNS.open do |dns|
|
85
|
+
txts = dns.getresources(self,Resolv::DNS::Resource::IN::TXT).collect { |r| r.strings }
|
86
|
+
if txts.empty? then nil else txts[0][0] end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# returns a reverse DNS hostname or nil
|
91
|
+
def dig_ptr
|
92
|
+
begin
|
93
|
+
Resolv.new.getname(self.downcase)
|
94
|
+
rescue Resolv::ResolvError
|
95
|
+
nil
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# returns true if the IP is blacklisted; otherwise false
|
100
|
+
# examples:
|
101
|
+
# barracuda = 'b.barracudacentral.org'.blacklisted?(ip)
|
102
|
+
# spamhaus = 'zen.spamhaus.org'.blacklisted?(ip)
|
103
|
+
def blacklisted?(dx)
|
104
|
+
domain = dx.split('.').reverse.join('.')+"."+self
|
105
|
+
a = []
|
106
|
+
Resolv::DNS.open do |dns|
|
107
|
+
begin
|
108
|
+
a = dns.getresources(domain, Resolv::DNS::Resource::IN::A)
|
109
|
+
rescue Resolv::NXDomainError
|
110
|
+
a=[]
|
111
|
+
end
|
112
|
+
end
|
113
|
+
if a.size>0 then true else false end
|
114
|
+
end
|
115
|
+
|
116
|
+
# returns a UTF-8 encoded string -- be carefule using this with email:
|
117
|
+
# email has to be received and transported with NO changes, except the
|
118
|
+
# addition of extra headers at the beginning (before any DKIM headers)
|
119
|
+
def utf8
|
120
|
+
self.encode('UTF-8', 'binary', :invalid => :replace, :undef => :replace, :replace => '?')
|
121
|
+
end
|
122
|
+
|
123
|
+
# opens a socket to the IP/port to see if there is an SMTP server
|
124
|
+
# there - returns "250 ..." if the server is there, or
|
125
|
+
# times out in 5 seconds to prevent hanging the process
|
126
|
+
def mta_live?(port)
|
127
|
+
tcp_socket = nil
|
128
|
+
welcome = nil
|
129
|
+
begin
|
130
|
+
Timeout.timeout(60) do
|
131
|
+
begin
|
132
|
+
tcp_socket = TCPSocket.open(self,port)
|
133
|
+
rescue Errno::ECONNREFUSED => e
|
134
|
+
return "421 Service not available (port closed)"
|
135
|
+
end
|
136
|
+
begin
|
137
|
+
welcome = tcp_socket.gets
|
138
|
+
return welcome if welcome[1]!='2'
|
139
|
+
tcp_socket.write("QUIT\r\n")
|
140
|
+
line = tcp_socket.gets
|
141
|
+
return line if line[1]!='2'
|
142
|
+
ensure
|
143
|
+
tcp_socket.close if tcp_socket
|
144
|
+
end
|
145
|
+
end
|
146
|
+
return "250 #{welcome.chomp[4..-1]}"
|
147
|
+
rescue SocketError => e
|
148
|
+
return "421 Service not available (#{e.to_s})"
|
149
|
+
rescue Timeout::Error => e
|
150
|
+
return "421 Service not available (#{e.to_s})"
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# this validates a password with the base64 plaintext in an AUTH command
|
155
|
+
# encoded -> AGNvY29AY3phcm1haWwuY29tAG15LXBhc3N3b3Jk => ["coco@example.com", "my-password"]
|
156
|
+
# call UnixCrypt::SHA256.build("my-password")
|
157
|
+
# "my-password" --> "$5$BsHk6IIvndgdBmo9$iuO6WMaXzgzpGmGreV4uiH72VRGG1USNK/e5tL7P9jC"
|
158
|
+
# "AGNvY29AY3phcm1haWwuY29tAG15LXBhc3N3b3Jk".validate_plain { "$5$BsHk6IIvndgdBmo9$iuO6WMaXzgzpGmGreV4uiH72VRGG1USNK/e5tL7P9jC" } => "coco@example.com", true
|
159
|
+
def validate_plain
|
160
|
+
# decode and split up the username and password)
|
161
|
+
username, password = Base64::decode64(self).split("\x00")[1..-1]
|
162
|
+
return "", false if username.nil? || password.nil?
|
163
|
+
passwd_hash = yield(username) # get the hash
|
164
|
+
return username, false if passwd_hash.nil?
|
165
|
+
return username, UnixCrypt.valid?(password, passwd_hash)
|
166
|
+
end
|
167
|
+
|
168
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require_relative 'base-x'
|
2
|
+
require_relative 'deepclone'
|
3
|
+
require "./config" # config.rb is in user space
|
4
|
+
|
5
|
+
class SaveError < StandardError; end
|
6
|
+
|
7
|
+
class ItemOfMail < Hash
|
8
|
+
|
9
|
+
include Config
|
10
|
+
|
11
|
+
def initialize(mail=nil)
|
12
|
+
if mail
|
13
|
+
# clone, if a mail is provided
|
14
|
+
mail.each {|k,v| self[k]=v.deepclone }
|
15
|
+
else
|
16
|
+
# assign a new message id for new mail
|
17
|
+
new_id = []
|
18
|
+
new_id[0] = Time.now.tv_sec.to_b(MessageIdBase)
|
19
|
+
new_id[1] = ("00000"+(2176782336*rand).to_i.to_b(MessageIdBase))[-6..-1]
|
20
|
+
new_id[2] = ("00"+(Time.now.usec/1000).to_i.to_b(MessageIdBase))[-2..-1]
|
21
|
+
self[:mail_id] = new_id.join("-")
|
22
|
+
end
|
23
|
+
|
24
|
+
# always set the time of creation
|
25
|
+
self[:time] = Time.now.strftime("%a, %d %b %Y %H:%M:%S %z")
|
26
|
+
self[:saved] = nil
|
27
|
+
self[:accepted] = nil
|
28
|
+
end
|
29
|
+
|
30
|
+
def parse_headers
|
31
|
+
self[:data][:headers] = {}
|
32
|
+
header = ""
|
33
|
+
self[:data][:text].each do |line|
|
34
|
+
case
|
35
|
+
when line.nil?
|
36
|
+
break
|
37
|
+
when line =~ /^[ \t]/
|
38
|
+
header << String::new(line)
|
39
|
+
when line.empty?
|
40
|
+
break
|
41
|
+
when !header.empty?
|
42
|
+
keyword, value = header.split(":", 2)
|
43
|
+
self[:data][:headers][keyword.downcase.gsub("-","_").to_sym] = value.strip
|
44
|
+
header = String::new(line)
|
45
|
+
else
|
46
|
+
header = String::new(line)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
if !header.empty?
|
50
|
+
keyword, value = header.split(":", 2)
|
51
|
+
self[:data][:headers][keyword.downcase.gsub("-","_").to_sym] = if !value.nil? then value.strip else "" end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def insert_parcels
|
56
|
+
begin
|
57
|
+
return true if !self[:rcptto]
|
58
|
+
self[:rcptto].each do |rcptto|
|
59
|
+
# make entries into the database for tracking the deliveries
|
60
|
+
parcel = {}
|
61
|
+
parcel[:id] = nil
|
62
|
+
parcel[:mail_id] = self[:mail_id]
|
63
|
+
parcel[:from_url] = self[:mailfrom][:url]
|
64
|
+
parcel[:to_url] = rcptto[:url]
|
65
|
+
parcel[:delivery_msg] = rcptto[:message]
|
66
|
+
parcel[:retry_at] = nil
|
67
|
+
parcel[:delivery] = if rcptto[:accepted] then rcptto[:delivery].to_s else "none" end
|
68
|
+
parcel[:created_at] = parcel[:updated_at] = Time.now.strftime("%Y-%m-%d %H:%M:%S")
|
69
|
+
rcptto[:parcel_id] = S3DB[:parcels].insert(parcel)
|
70
|
+
end
|
71
|
+
return true
|
72
|
+
rescue => e
|
73
|
+
LOG.error(self[:mail_id]) {e.to_s}
|
74
|
+
e.backtrace.each { |line| LOG.error(self[:mail_id]) {line} }
|
75
|
+
return false
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def update_parcels(parcels)
|
80
|
+
parcels.each { |parcel| S3DB[:parcels].where(:id=>parcel[:id]).update(parcel) }
|
81
|
+
end
|
82
|
+
|
83
|
+
def save_mail_into_queue_folder
|
84
|
+
begin
|
85
|
+
# save the mail in the Queue folder
|
86
|
+
File::open("#{MailQueue}/#{self[:mail_id]}","w") do |f|
|
87
|
+
self[:saved] = true
|
88
|
+
f.write(self.pretty_inspect)
|
89
|
+
end
|
90
|
+
return true
|
91
|
+
rescue => e
|
92
|
+
LOG.error(self[:mail_id]) {e.to_s}
|
93
|
+
e.backtrace.each { |line| LOG.error(self[:mail_id]) {line} }
|
94
|
+
return false
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def self::retrieve_mail_from_queue_folder(mail_id)
|
99
|
+
item = nil
|
100
|
+
begin
|
101
|
+
mail = nil
|
102
|
+
File::open("#{MailQueue}/#{mail_id}","r") do |f|
|
103
|
+
mail = eval(f.read)
|
104
|
+
item = ItemOfMail::new(mail)
|
105
|
+
end
|
106
|
+
rescue => e
|
107
|
+
LOG.error(self[:mail_id]) {e.to_s}
|
108
|
+
e.backtrace.each { |line| LOG.error(self[:mail_id]) {line} }
|
109
|
+
end
|
110
|
+
return item
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
@@ -0,0 +1,376 @@
|
|
1
|
+
require 'timeout'
|
2
|
+
require 'pretty_inspect'
|
3
|
+
require 'openssl'
|
4
|
+
require 'logger'
|
5
|
+
require 'pdkim'
|
6
|
+
|
7
|
+
def manually_run_queue_runner
|
8
|
+
exit unless File::open(LockFilePath,"w").flock(File::LOCK_NB | File::LOCK_EX)
|
9
|
+
QueueRunner.new.run_queue
|
10
|
+
end
|
11
|
+
|
12
|
+
class QueueRunner
|
13
|
+
|
14
|
+
include Socket::Constants
|
15
|
+
include PDKIM
|
16
|
+
|
17
|
+
RetryInterval = 5*60;
|
18
|
+
|
19
|
+
def initialize
|
20
|
+
end
|
21
|
+
|
22
|
+
# send text to the client
|
23
|
+
def send_text(text,echo=:command)
|
24
|
+
puts "<- #{text.inspect}" if DisplayQueueRunnerDialog
|
25
|
+
if text.class==Array
|
26
|
+
text.each do |line|
|
27
|
+
@connection.write(line+CRLF)
|
28
|
+
LOG.info(@mail_id) {"<- %s"%text} if LogQueueRunnerConversation
|
29
|
+
end
|
30
|
+
else
|
31
|
+
@connection.write(text+CRLF)
|
32
|
+
LOG.info(@mail_id) {"<- %s"%text} if LogQueueRunnerConversation
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# receive text from the client
|
37
|
+
def recv_text
|
38
|
+
begin
|
39
|
+
lines = []
|
40
|
+
Timeout.timeout(QueueRunnerTimeout) do
|
41
|
+
tmp = @connection.gets
|
42
|
+
lines << (line = if tmp.nil? then "" else tmp.chomp end)
|
43
|
+
LOG.info((@mail_id)) {" -> %s"%line} if LogQueueRunnerConversation
|
44
|
+
while line[3]=='-'
|
45
|
+
tmp = @connection.gets
|
46
|
+
lines << (line = if tmp.nil? then "" else tmp.chomp end)
|
47
|
+
LOG.info((@mail_id)) {" -> %s"%line} if LogQueueRunnerConversation
|
48
|
+
end
|
49
|
+
ok = lines.last[0]
|
50
|
+
lines.each {|line| puts " -> #{line.inspect}"} if DisplayQueueRunnerDialog
|
51
|
+
return ok, lines
|
52
|
+
end
|
53
|
+
rescue Timeout::Error => e
|
54
|
+
raise "Fix this in transporter.rb::recv_text rescue"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def run_queue
|
59
|
+
@mail_id = nil
|
60
|
+
LOG.info(Time.now.strftime("%Y-%m-%d %H:%M:%S")) {"Queue runner started"}
|
61
|
+
n=3 # used for a sanity check
|
62
|
+
while true
|
63
|
+
# sqlite3 has a bug: "<=" doesn't work with time, ex. "retry_at<='#{Time.now}'"
|
64
|
+
# we have to add 1 second and use "<"; ex. "retry_at<'#{Time.now+1}'"
|
65
|
+
parcels = S3DB[:parcels].where("(delivery<>'none') and (delivery_at is null) and ((retry_at is null) or (retry_at<'#{Time.now + 1}'))").all
|
66
|
+
return if parcels.empty?
|
67
|
+
|
68
|
+
# aggregate the emails by destination domain
|
69
|
+
deliver = {}
|
70
|
+
parcels.each do |parcel|
|
71
|
+
if parcel[:delivery]!='none' && !parcel[:delivery_at]
|
72
|
+
mail_id = deliver[parcel[:mail_id]] ||= {}
|
73
|
+
domain = mail_id[parcel[:to_url].split('@')[1]] ||= {}
|
74
|
+
domain[parcel[:to_url]] = parcel
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# send mail to each domain--success or failure will be
|
79
|
+
# handled in the respective mail routine
|
80
|
+
mail = {}
|
81
|
+
deliver.each do |mail_id, domains|
|
82
|
+
(mail = ItemOfMail::retrieve_mail_from_queue_folder(mail_id)) if mail[:mail_id]!=mail_id
|
83
|
+
domains.each do |domain, parcels|
|
84
|
+
|
85
|
+
#=== compare before and after ======================================
|
86
|
+
#puts "--> *2* domain=>#{domain.inspect}"
|
87
|
+
#parcels.values.each { |parcel| puts "--> *3* #{parcel.inspect}" }
|
88
|
+
#===================================================================
|
89
|
+
|
90
|
+
@mail_id = mail[:mail_id]
|
91
|
+
deliver_and_save_status(mail, domain, parcels.values)
|
92
|
+
@mail_id = nil
|
93
|
+
|
94
|
+
#===================================================================
|
95
|
+
#parcels.values.each { |parcel| puts "--> *4* #{parcel.inspect}" }
|
96
|
+
#===================================================================
|
97
|
+
end
|
98
|
+
end
|
99
|
+
if (n-=1)<0
|
100
|
+
LOG.info(Time.now.strftime("%Y-%m-%d %H:%M:%S")) {"In QueueRunner, the loop repeated 3 times. Is somthing wrong?"}
|
101
|
+
return nil
|
102
|
+
end
|
103
|
+
end
|
104
|
+
ensure
|
105
|
+
LOG.info(Time.now.strftime("%Y-%m-%d %H:%M:%S")) {"Queue runner finished"}
|
106
|
+
end
|
107
|
+
|
108
|
+
def deliver_and_save_status(mail,domain,parcels)
|
109
|
+
# the methods lmtp_delivery and smtp_delivery will change
|
110
|
+
# the status upon successful delivery
|
111
|
+
|
112
|
+
# get the certificates, if any; they're needed for STARTTLS
|
113
|
+
$prv = if PrivateKey then OpenSSL::PKey::RSA.new File.read(PrivateKey) else nil end
|
114
|
+
$crt = if Certificate then OpenSSL::X509::Certificate.new File.read(Certificate) else nil end
|
115
|
+
|
116
|
+
# establish an SSL context
|
117
|
+
$ctx = OpenSSL::SSL::SSLContext.new
|
118
|
+
$ctx.key = $prv
|
119
|
+
$ctx.cert = $crt
|
120
|
+
|
121
|
+
begin
|
122
|
+
case parcels.first[:delivery]
|
123
|
+
#==================================================
|
124
|
+
when "local"
|
125
|
+
begin
|
126
|
+
ssl_socket = TCPSocket.open('localhost',LocalLMTPPort)
|
127
|
+
@connection = OpenSSL::SSL::SSLSocket.new(ssl_socket, $ctx);
|
128
|
+
lmtp_delivery('localhost',LocalLMTPPort,mail,domain,parcels)
|
129
|
+
mail.update_parcels(parcels)
|
130
|
+
rescue Errno::ECONNREFUSED => e
|
131
|
+
LOG.info(@mail_id) {"Connection to localhost failed: #{e}"}
|
132
|
+
mark_parcels(parcels, "441 4.0.0 Connection to localhost failed")
|
133
|
+
end
|
134
|
+
#==================================================
|
135
|
+
when "remote"
|
136
|
+
# this looks through the list of MXs and finds the
|
137
|
+
# first one that can communicate
|
138
|
+
mail[:rcptto].each do |rcptto|
|
139
|
+
if rcptto[:domain]==domain
|
140
|
+
rcptto[:mxs].each do |preference,pairs|
|
141
|
+
pairs.each do |mx,ip|
|
142
|
+
begin
|
143
|
+
# open the connection
|
144
|
+
ssl_socket = TCPSocket.open(mx,RemoteSMTPPort)
|
145
|
+
@connection = OpenSSL::SSL::SSLSocket.new(ssl_socket, $ctx);
|
146
|
+
smtp_delivery(mx, RemoteSMTPPort, mail, domain, parcels)
|
147
|
+
mail.update_parcels(parcels)
|
148
|
+
return
|
149
|
+
rescue Errno::ETIMEDOUT => e
|
150
|
+
LOG.info(@mail_id) {"Service for #{mx} not available (timeout)"}
|
151
|
+
rescue Errno::ECONNREFUSED => e
|
152
|
+
LOG.info(@mail_id) {"Service for #{mx} not available (refused)"}
|
153
|
+
end
|
154
|
+
end
|
155
|
+
# delivery was remote, and no MX was connectable
|
156
|
+
mark_parcels(parcels, "441 4.0.0 No MX for <#{domain}> has an operational mail server")
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
#==================================================
|
161
|
+
when 'none'
|
162
|
+
# just ignore the email--it will not be delivered
|
163
|
+
else
|
164
|
+
# we didn't program a delivery option, so it got here
|
165
|
+
mark_parcels(parcels, "500 5.0.0 Delivery option '#{parcel[:delivery]} not supported")
|
166
|
+
end
|
167
|
+
rescue => e
|
168
|
+
LOG.info(@mail_id) {"Rescue #{e.inspect}"}
|
169
|
+
e.backtrace.each { |line| LOG.fatal(mail[:mail_id]) { line } }
|
170
|
+
mark_parcels(parcels, "441 4.0.0 Rescue #{e.inspect}")
|
171
|
+
end
|
172
|
+
ensure
|
173
|
+
if @connection
|
174
|
+
send_text("QUIT")
|
175
|
+
ok, lines = recv_text # ignore returns
|
176
|
+
@connection.close
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def mark_parcels(parcels, responses)
|
181
|
+
response = if responses.kind_of?(Array) then responses.last else responses end
|
182
|
+
parcels.each do |parcel|
|
183
|
+
parcel[:delivery_at] = if response[0]!='4' then Time.now else nil end
|
184
|
+
parcel[:delivery_msg] = response
|
185
|
+
parcel[:retry_at] = if response[0]=='4' then Time.now + RetryInterval else nil end
|
186
|
+
end
|
187
|
+
nil
|
188
|
+
end
|
189
|
+
|
190
|
+
# SAMPLE SMTP TRANSFER
|
191
|
+
# <- 220 2.0.0 mail.xyz.com ESMTP Xyz, LLC 0.01 THU, 24 NOV 2016 20:22:39 +0000
|
192
|
+
# -> EHLO foo.com
|
193
|
+
# <- 250-2.0.0 mail.xyz.com Hello foo.com at 213.33.76.136
|
194
|
+
# <- 250-AUTH PLAIN
|
195
|
+
# <- 250-STARTTLS
|
196
|
+
# <- 250 HELP
|
197
|
+
#---- This part is only if there is a logon/password supplied ----
|
198
|
+
# -> STARTTLS
|
199
|
+
# <- 220 2.0.0 TLS go ahead
|
200
|
+
# === TLS started with cipher TLSv1.2:DHE-RSA-AES256-GCM-SHA384:256
|
201
|
+
# === TLS no local certificate set
|
202
|
+
# === TLS peer DN="/C=US/ST=CA/L=Los Angeles/O=Xyz, LLC/CN=xyz.com/emailAddress=admin@xyz.com"
|
203
|
+
# ~> EHLO foo.com
|
204
|
+
# <~ 250-2.0.0 mail.xyz.com Hello foo.com at 213.33.76.136
|
205
|
+
# <~ 250-AUTH PLAIN
|
206
|
+
# <~ 250 HELP
|
207
|
+
# ~> MAIL FROM:John Q. Public <JQP@foo.com>
|
208
|
+
# <~ 250 2.0.0 OK
|
209
|
+
# ~> RCPT TO:<Jones@xyz.com>
|
210
|
+
# <~ 250 2.0.0 OK
|
211
|
+
# ~> DATA
|
212
|
+
# <~ 354 Enter message, ending with "." on a line by itself
|
213
|
+
# ~> Date: Thu, 24 Nov 2016 12:22:39 -0800
|
214
|
+
# ~> To: Jones@xyz.com
|
215
|
+
# ~> From: John Q. Public <JQP@bar.com>
|
216
|
+
# ~> Subject: test Thu, 24 Nov 2016 12:22:39 -0800
|
217
|
+
# ~>
|
218
|
+
# ~> Bill:
|
219
|
+
# ~> The next meeting of the board of directors will be
|
220
|
+
# ~> on Tuesday.
|
221
|
+
# ~> John.
|
222
|
+
# ~> .
|
223
|
+
# <~ 250 OK
|
224
|
+
# ~> QUIT
|
225
|
+
# <~ 221 2.0.0 OK mail.xyz.com closing connection
|
226
|
+
|
227
|
+
def smtp_delivery(host, port, mail, domain, parcels, username=nil, password=nil)
|
228
|
+
LOG.info(@mail_id) {"Beginning delivery of #{mail[:id]} to remote server at #{host}"}
|
229
|
+
|
230
|
+
# receive the server's welcome message
|
231
|
+
ok, lines = recv_text
|
232
|
+
return mark_parcels(parcels, lines) if ok!='2'
|
233
|
+
|
234
|
+
# send the EHLO
|
235
|
+
send_text("EHLO #{mail[:local_hostname]}")
|
236
|
+
ok, lines = recv_text
|
237
|
+
return mark_parcels(parcels, lines) if ok!='2'
|
238
|
+
|
239
|
+
# check for STARTTLS supported by server
|
240
|
+
if !lines.select{ |line| line.index("STARTTLS") }.empty?
|
241
|
+
send_text("STARTTLS")
|
242
|
+
ok, lines = recv_text
|
243
|
+
return mark_parcels(parcels, lines) if ok!='2'
|
244
|
+
|
245
|
+
# enable TLS
|
246
|
+
@connection.connect
|
247
|
+
LOG.info(@mail_id) {"<-> (handshake)"}
|
248
|
+
|
249
|
+
send_text("EHLO #{mail[:local_hostname]}")
|
250
|
+
ok, lines = recv_text
|
251
|
+
return mark_parcels(parcels, lines) if ok!='2'
|
252
|
+
end
|
253
|
+
|
254
|
+
# AUTH PLAIN -- log onto server
|
255
|
+
if username
|
256
|
+
user_pass = Base64::encode64("\0#{username}\0#{password}").chomp
|
257
|
+
send_text("AUTH PLAIN #{user_pass}")
|
258
|
+
ok, lines = recv_text
|
259
|
+
return mark_parcels(parcels, lines) if ok!='2'
|
260
|
+
end
|
261
|
+
|
262
|
+
# MAIL FROM
|
263
|
+
send_text("MAIL FROM:<#{mail[:mailfrom][:url]}>")
|
264
|
+
ok, lines = recv_text
|
265
|
+
return mark_parcels(parcels, lines) if ok!='2'
|
266
|
+
|
267
|
+
# RCPT TO
|
268
|
+
parcels.each do |parcel|
|
269
|
+
send_text("RCPT TO:<#{parcel[:to_url]}>")
|
270
|
+
ok, lines = recv_text
|
271
|
+
# if there's a problem, we mark this parcel (recipient), but keep processing others
|
272
|
+
mark_parcels(parcels, lines) if ok!='2'
|
273
|
+
end
|
274
|
+
|
275
|
+
# DATA -- send the email
|
276
|
+
send_text("DATA")
|
277
|
+
ok, lines = recv_text
|
278
|
+
return mark_parcels(parcels, lines) if ok!='3'
|
279
|
+
|
280
|
+
LOG.info(@mail_id) {"<- (data)"} if LogQueueRunnerConversation
|
281
|
+
mail[:data][:text].each do |line|
|
282
|
+
send_text(line, :data)
|
283
|
+
end
|
284
|
+
|
285
|
+
# send the end of the message prompt
|
286
|
+
send_text(".", :data)
|
287
|
+
|
288
|
+
# get one final message for all parcels (recipients)
|
289
|
+
ok, lines = recv_text
|
290
|
+
return mark_parcels(parcels, lines)
|
291
|
+
end
|
292
|
+
|
293
|
+
# SAMPLE LMTP TRANSFER
|
294
|
+
# <- 220 foo.edu LMTP server ready
|
295
|
+
# -> LHLO foo.edu
|
296
|
+
# <- 250-foo.edu
|
297
|
+
# <- 250-PIPELINING
|
298
|
+
# <- 250 SIZE
|
299
|
+
# -> MAIL FROM:<chris@bar.com>
|
300
|
+
# <- 250 OK
|
301
|
+
# -> RCPT TO:<pat@foo.edu>
|
302
|
+
# <- 250 OK
|
303
|
+
# -> RCPT TO:<jones@foo.edu>
|
304
|
+
# <- 550 No such user here
|
305
|
+
# -> RCPT TO:<green@foo.edu>
|
306
|
+
# <- 250 OK
|
307
|
+
# -> DATA
|
308
|
+
# <~ 354 Enter message, ending with "." on a line by itself
|
309
|
+
# ~> Date: Thu, 24 Nov 2016 12:22:39 -0800
|
310
|
+
# ~> To: Jones@xyz.com
|
311
|
+
# ~> From: John Q. Public <JQP@bar.com>
|
312
|
+
# ~> Subject: test Thu, 24 Nov 2016 12:22:39 -0800
|
313
|
+
# ~>
|
314
|
+
# ~> Bill:
|
315
|
+
# ~> The next meeting of the board of directors will be
|
316
|
+
# ~> on Tuesday.
|
317
|
+
# ~> John.
|
318
|
+
# ~> .
|
319
|
+
# <~ 250 OK
|
320
|
+
# <- 452 <green@foo.edu> is temporarily over quota (reply for <green@foo.edu>)
|
321
|
+
# -> QUIT
|
322
|
+
# <- 221 foo.edu closing connection
|
323
|
+
#
|
324
|
+
# Note: there was no reply for <jones@foo.edu> because it failed in the RCPT TO command
|
325
|
+
|
326
|
+
# domain (for local) is 'localhost', and parcels is an array of hashes (recipients) --
|
327
|
+
# the single key is the domain to which to send the email -- since all recipients
|
328
|
+
# are in the same domain, we can send only one email with all recipients named in
|
329
|
+
# RCPT TOs -- this delivery is for Dovecot, so we believe the domain is 'ServerName'
|
330
|
+
def lmtp_delivery(host, port, mail, domain, parcels)
|
331
|
+
LOG.info(@mail_id) {"Beginning delivery of #{mail[:mail_id]} to Dovecot at #{ServerName}"}
|
332
|
+
|
333
|
+
# receive the server's welcome message
|
334
|
+
ok, lines = recv_text
|
335
|
+
return mark_parcels(parcels, lines) if ok!='2'
|
336
|
+
|
337
|
+
# send the LHLO
|
338
|
+
send_text("LHLO #{mail[:local_hostname]}")
|
339
|
+
ok, lines = recv_text
|
340
|
+
return mark_parcels(parcels, lines) if ok!='2'
|
341
|
+
|
342
|
+
# MAIL FROM
|
343
|
+
send_text("MAIL FROM:<#{mail[:mailfrom][:url]}>")
|
344
|
+
ok, lines = recv_text
|
345
|
+
return mark_parcels(parcels, lines) if ok!='2'
|
346
|
+
|
347
|
+
# RCPT TO
|
348
|
+
parcels.each do |parcel|
|
349
|
+
send_text("RCPT TO:<#{parcel[:to_url]}>")
|
350
|
+
ok, lines = recv_text
|
351
|
+
# if there's a problem, we mark this pacel (recipient), but keep processing others
|
352
|
+
mark_parcels([parcel], lines) if ok!='2'
|
353
|
+
end
|
354
|
+
|
355
|
+
# DATA -- send the email
|
356
|
+
send_text("DATA")
|
357
|
+
ok, lines = recv_text
|
358
|
+
return mark_parcels(parcels, lines) if ok!='3'
|
359
|
+
|
360
|
+
LOG.info(@mail_id) {"<- (data)"} if LogQueueRunnerConversation
|
361
|
+
mail[:data][:text].each do |line|
|
362
|
+
send_text(line, :data)
|
363
|
+
end
|
364
|
+
|
365
|
+
# send the end of the message prompt
|
366
|
+
send_text(".", :data)
|
367
|
+
|
368
|
+
# get one final message for each recipient
|
369
|
+
parcels.each do |parcel|
|
370
|
+
# get the response from DoveCot
|
371
|
+
ok, lines = recv_text
|
372
|
+
mark_parcels([parcel], lines)
|
373
|
+
end
|
374
|
+
end
|
375
|
+
|
376
|
+
end
|