rubymta 0.0.1

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