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,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
|