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.
@@ -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