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,306 @@
1
+ require 'logger'
2
+ require 'socket'
3
+ require 'ostruct'
4
+ require 'optparse'
5
+ require 'openssl'
6
+ require 'sequel'
7
+ require 'sqlite3'
8
+ require 'etc'
9
+ require 'pretty_inspect'
10
+
11
+ class Terminate < Exception; end
12
+ class Quit < Exception; end
13
+
14
+ # Set up the $app hash for systemwide parameters
15
+ $app = {}
16
+ $app[:path] = Dir::pwd
17
+ $app[:mode] = ENV['MODE']
18
+
19
+ require './config'
20
+ include Config
21
+ $app[:uinfo] = Etc::getpwnam(UserName) if UserName
22
+ $app[:ginfo] = Etc::getgrnam(GroupName) if GroupName
23
+
24
+ require_relative 'receiver'
25
+
26
+ # get setup and open the log
27
+ LOG = Logger::new(LogPathAndFile, LogFileLife)
28
+ LOG.formatter = proc do |severity, datetime, progname, msg|
29
+ pname = if progname then '('+progname+') ' else nil end
30
+ "#{datetime.strftime("%Y-%m-%d %H:%M:%S")} [#{severity}] #{pname}#{msg}\n"
31
+ end
32
+
33
+ # Make sure the MODE environmental variable is valid
34
+ if ['dev','live'].index(ENV['MODE']).nil?
35
+ msg = "Environmental variable MODE not set properly--must be dev or live"
36
+ LOG.fatal(msg)
37
+ puts msg
38
+ exit(1)
39
+ end
40
+
41
+ # set the owner of the LOG file to UserName and
42
+ # GroupName--this is because otherwise, a new log file
43
+ # will be created as root:root, and run_queue won't be
44
+ # able to open it--this only is needed if 'server' is
45
+ # running as root; same for the sqlite database
46
+ if !UserName.nil?
47
+ File::chown($app[:uinfo].uid, $app[:ginfo].gid, LogPathAndFile)
48
+ File::chown($app[:uinfo].uid, $app[:ginfo].gid, S3DBPath)
49
+ end
50
+
51
+ # Open the sqlite3 database for rubymta use
52
+ S3DB = Sequel.connect("sqlite://#{S3DBPath}")
53
+ LOG.info("Database '#{S3DBPath}' opened")
54
+
55
+ # Create the tables we need if they don't already exist
56
+
57
+ # The contacts table is just used to count the number of 'hits' for
58
+ # a particular IP address in order to choose between sending an
59
+ # "Access TEMPORARILY denied" or just slam the port shut
60
+ S3DB::create_table?(:contacts) do
61
+ primary_key(:id)
62
+ string(:remote_ip)
63
+ integer(:hits)
64
+ integer(:locks)
65
+ integer(:violations)
66
+ datetime(:expires_at)
67
+ datetime(:created_at)
68
+ datetime(:updated_at)
69
+ index(:remote_ip)
70
+ end
71
+
72
+ # The queue table is used to track the emails server/receiver receive. The
73
+ # queue runner will read this queue and attempt to deliver the emails.
74
+ S3DB::create_table?(:parcels) do
75
+ primary_key(:id)
76
+ string(:mail_id)
77
+ string(:from_url)
78
+ string(:to_url)
79
+ string(:delivery) # 'local' or 'remote'
80
+ datetime(:delivery_at)
81
+ text(:delivery_msg)
82
+ datetime(:retry_at)
83
+ datetime(:created_at)
84
+ datetime(:updated_at)
85
+ index([:mail_id,:from_url])
86
+ index([:mail_id,:to_url])
87
+ end
88
+
89
+ # load the DKIM private key for this domain, if any
90
+ $app[:dkim] = nil
91
+ File::open(DKIMPrivateKeyFile, "r") { |f| $app[:dkim] = f.read } if DKIMPrivateKeyFile
92
+
93
+ # MAIN server class -- starts at Server#start
94
+ class Server
95
+
96
+ include Socket::Constants
97
+
98
+ # def restart
99
+ # # handle a HUP request here
100
+ # end
101
+
102
+ # this is the code executed after the process has been
103
+ # forked and root privileges have been dropped
104
+ def process_call(connection, local_port, remote_port, remote_ip, remote_hostname, remote_service)
105
+ begin
106
+ Signal.trap("INT") { } # ignore ^C in the child process
107
+ LOG.info("%06d"%Process::pid) {"Connection accepted on port #{local_port} from port #{remote_port} at #{remote_ip} (#{remote_hostname})"}
108
+
109
+ # a new object is created here to provide separation between server and receiver
110
+ # this call receives the email and does basic validation
111
+ Receiver::new(connection).receive(local_port, Socket::gethostname, remote_port, remote_hostname, remote_ip)
112
+ rescue Quit
113
+ # nothing to do here
114
+ ensure
115
+ # close the database (the child's copy)
116
+ S3DB.disconnect if S3DB
117
+ nil # don't return the Receiver object
118
+ end
119
+ end
120
+
121
+ # this method drops the process's root privileges for security reasons
122
+ def drop_root_privileges
123
+ if Process::Sys.getuid==0
124
+ Dir.chdir($app[:path]) if not $app[:path].nil?
125
+ Process::GID.change_privilege($app[:ginfo].gid)
126
+ Process::UID.change_privilege($app[:uinfo].uid)
127
+ end
128
+ end
129
+
130
+ # both the AF_INET and AF_INET6 families use this DRY method
131
+ def bind_socket(family,port,ip)
132
+ socket = Socket.new(family, SOCK_STREAM, 0)
133
+ sockaddr = Socket.sockaddr_in(port.to_i,ip)
134
+ socket.setsockopt(:SOCKET, :REUSEADDR, true)
135
+ socket.bind(sockaddr)
136
+ socket.listen(0)
137
+ return socket
138
+ end
139
+
140
+ # the listening thread is established in this method depending on the ListenPort
141
+ # argument passed to it -- it can be '<ipv6>/<port>', '<ipv4>:<port>', or just '<port>'
142
+ def listening_thread(local_port)
143
+ LOG.info("%06d"%Process::pid) {"listening on port #{local_port}..."}
144
+
145
+ # establish an SSL context
146
+ $ctx = OpenSSL::SSL::SSLContext.new
147
+ $ctx.key = $prv
148
+ $ctx.cert = $crt
149
+
150
+ # check the parameter to see if it's valid
151
+ m = /^(([0-9a-fA-F]{0,4}:{0,1}){1,8})\/([0-9]{1,5})|(([0-9]{1,3}\.{0,1}){4}):([0-9]{1,5})|([0-9]{1,5})$/.match(local_port)
152
+ #<MatchData "2001:4800:7817:104:be76:4eff:fe05:3b18/2000" 1:"2001:4800:7817:104:be76:4eff:fe05:3b18" 2:"3b18" 3:"2000" 4:nil 5:nil 6:nil 7:nil>
153
+ #<MatchData "23.253.107.107:2000" 1:nil 2:nil 3:nil 4:"23.253.107.107" 5:"107" 6:"2000" 7:nil>
154
+ #<MatchData "2000" 1:nil 2:nil 3:nil 4:nil 5:nil 6:nil 7:"2000">
155
+ case
156
+ when !m[1].nil? # its AF_INET6
157
+ socket = bind_socket(AF_INET6,m[3],m[1])
158
+ when !m[4].nil? # its AF_INET
159
+ socket = bind_socket(AF_INET,m[6],m[4])
160
+ when !m[7].nil?
161
+ socket = bind_socket(AF_INET6,m[7],"0:0:0:0:0:0:0:0")
162
+ else
163
+ raise ArgumentError.new(local_port)
164
+ end
165
+ ssl_server = OpenSSL::SSL::SSLServer.new(socket, $ctx);
166
+
167
+ # main listening loop starts in non-encrypted mode
168
+ ssl_server.start_immediately = false
169
+ loop do
170
+ # we can't use threads because if we drop root privileges on any thread,
171
+ # they will be dropped for all threads in the process--so we have to fork
172
+ # a process here in order that the reception be able to drop root privileges
173
+ # and run at a user level--this is a security precaution
174
+ connection = ssl_server.accept
175
+ Process::fork do
176
+ begin
177
+ drop_root_privileges if !UserName.nil?
178
+ remote_hostname, remote_service = connection.io.remote_address.getnameinfo
179
+ remote_ip, remote_port = connection.io.remote_address.ip_unpack
180
+ process_call(connection, local_port, remote_port.to_s, remote_ip, remote_hostname, remote_service)
181
+ LOG.info("%06d"%Process::pid) {"Connection closed on port #{local_port} by #{ServerName}"}
182
+ rescue Errno::ENOTCONN => e
183
+ LOG.info("%06d"%Process::pid) {"Remote Port scan on port #{local_port}"}
184
+ ensure
185
+ # here we close the child's copy of the connection --
186
+ # since the parent already closed it's copy, this
187
+ # one will send a FIN to the client, so the client
188
+ # can terminate gracefully
189
+ connection.close
190
+ # and finally, close the child's link to the log
191
+ LOG.close
192
+ end
193
+ end
194
+ # here we close the parent's copy of the connection --
195
+ # the child (created by the Process::fork above) has another copy --
196
+ # if this one is not closed, when the child closes it's copy,
197
+ # the child's copy won't send a FIN to the client -- the FIN
198
+ # is only sent when the last process holding a copy to the
199
+ # socket closes it's copy
200
+ connection.close
201
+ end
202
+ end
203
+
204
+ # this method parses the command line options
205
+ def process_options
206
+ options = OpenStruct.new
207
+ options.log = Logger::INFO
208
+ options.daemonize = false
209
+ begin
210
+ OptionParser.new do |opts|
211
+ opts.on("--debug", "Log all messages") { |v| options.log = Logger::DEBUG }
212
+ opts.on("--info", "Log all messages") { |v| options.log = Logger::INFO }
213
+ opts.on("--warn", "Log all messages") { |v| options.log = Logger::WARN }
214
+ opts.on("--error", "Log all messages") { |v| options.log = Logger::ERROR }
215
+ opts.on("--fatal", "Log all messages") { |v| options.log = Logger::FATAL }
216
+ opts.on("--daemonize", "Run as system daemon") { |v| options.daemonize = true }
217
+ end.parse!
218
+ rescue OptionParser::InvalidOption => e
219
+ LOG.warn("%06d"%Process::pid) {"#{e.inspect}"}
220
+ end
221
+ options
222
+ end # process_options
223
+
224
+ def start
225
+ # generate the first log messages
226
+ LOG.info("%06d"%Process::pid) {"Starting RubyMTA at #{Time.now.strftime("%Y-%m-%d %H:%M:%S %Z")}, pid=#{Process::pid}"}
227
+ LOG.info("%06d"%Process::pid) {"Options specified: #{ARGV.join(", ")}"} if ARGV.size>0
228
+
229
+ # get the options from the command line
230
+ @options = process_options
231
+ LOG.level = @options.log
232
+
233
+ # get the certificates, if any; they're needed for STARTTLS
234
+ # we do this before daemonizing because the working folder might change
235
+ $prv = if PrivateKey then OpenSSL::PKey::RSA.new File.read(PrivateKey) else nil end
236
+ $crt = if Certificate then OpenSSL::X509::Certificate.new File.read(Certificate) else nil end
237
+
238
+ # daemonize it if the option was set--it doesn't have to be root to daemonize it
239
+ Process::daemon if @options.daemonize
240
+
241
+ # get the process ID and the user id AFTER demonizing, if that was requested
242
+ pid = Process::pid
243
+ uid = Process::Sys.getuid
244
+ gid = Process::Sys.getgid
245
+
246
+ LOG.info("%06d"%Process::pid) {"Daemonized at #{Time.now.strftime("%Y-%m-%d %H:%M:%S %Z")}, pid=#{pid}, uid=#{uid}, gid=#{gid}"} #if @options.daemonize
247
+
248
+ # store the pid of the server session
249
+ begin
250
+ LOG.info("%06d"%Process::pid) {"RubyMTA running as PID=>#{pid}, UID=>#{uid}, GID=>#{gid}"}
251
+ File::open("#{PidPath}/rubymta.pid","w") { |f| f.write(pid.to_s) }
252
+ rescue Errno::EACCES => e
253
+ LOG.warn("%06d"%Process::pid) {"The pid couldn't be written. To save the pid, create a directory '#{PidPath}' with r/w permissions for this user."}
254
+ LOG.warn("%06d"%Process::pid) {"Proceeding without writing the pid."}
255
+ end
256
+
257
+ # if rubymta was started as root, make sure UserName and
258
+ # GroupName have values because we have to drop root privileges
259
+ # after we fork a process for the receiver
260
+ if uid==0 # it's root
261
+ if UserName.nil? || GroupName.nil?
262
+ LOG.error("%06d"%Process::pid) {"rubymta can't be started as root unless UserName and GroupName are set."}
263
+ exit(1)
264
+ end
265
+ end
266
+
267
+ # this is the main loop which runs until admin enters ^C
268
+ Signal.trap("INT") { raise Terminate.new }
269
+ Signal.trap("HUP") { restart if defined?(restart) }
270
+ Signal.trap("CHLD") do
271
+ begin
272
+ Process.wait(-1, Process::WNOHANG)
273
+ rescue Errno::ECHILD => e
274
+ # ignore the error
275
+ end
276
+ end
277
+ threads = []
278
+ # start the server on multiple ports (the usual case)
279
+ begin
280
+ ListeningPorts.each do |port|
281
+ threads << Thread.start(port) do |port|
282
+ listening_thread(port)
283
+ end
284
+ end
285
+ # the joins are done ONLY after all threads are started
286
+ threads.each { |thread| thread.join }
287
+ rescue Terminate
288
+ LOG.info("%06d"%Process::pid) {"#{ServerName} terminated by admin ^C"}
289
+ end
290
+
291
+ ensure
292
+
293
+ # attempt to remove the pid file
294
+ begin
295
+ File.delete("#{PidPath}/rubymta.pid")
296
+ rescue Errno::ENOENT => e
297
+ LOG.warn("%06d"%Process::pid) {"No such file: #{e.inspect}"}
298
+ rescue Errno::EACCES, Errno::EPERM
299
+ LOG.warn("%06d"%Process::pid) {"Permission denied: #{e.inspect}"}
300
+ end
301
+
302
+ # close the log
303
+ LOG.close if LOG
304
+ end
305
+
306
+ end
@@ -0,0 +1,5 @@
1
+ module Version
2
+ VERSION = "0.0.1"
3
+ MODIFIED = "2017-07-29"
4
+ end
5
+
data/lib/rubymta.rb ADDED
@@ -0,0 +1,2 @@
1
+ require_relative "rubymta/version"
2
+ require_relative "rubymta/server"
data/rubymta.gemspec ADDED
@@ -0,0 +1,15 @@
1
+ require_relative 'lib/rubymta/version'
2
+ include Version
3
+ Gem::Specification.new do |s|
4
+ s.author = "Michael J. Welch, Ph.D."
5
+ s.files = Dir.glob(["CHANGELOG.md", "LICENSE.md", "README.md", "rubymta.gemspec", "lib/*", "lib/rubymta/*", "spec/*", ".gitignore"])
6
+ s.name = 'rubymta'
7
+ s.require_paths = ["lib"]
8
+ s.summary = "A Ruby Gem providing a complete Mail Transport Agent package."
9
+ s.version = VERSION
10
+ s.date = MODIFIED
11
+ s.email = 'mjwelchphd@gmail.com'
12
+ s.homepage = 'http://rubygems.org/gems/rubymta'
13
+ s.license = 'MIT'
14
+ s.description = "RubyMta is an experimental mail transport agent written in Ruby. See the README."
15
+ end
data/spec/coco ADDED
@@ -0,0 +1,3 @@
1
+ coco@tzarmail.com
2
+ my-password
3
+
data/spec/rubymta.rb ADDED
@@ -0,0 +1,199 @@
1
+ #require "bacon"
2
+ require 'sequel'
3
+ require "sqlite3"
4
+
5
+ Sender = "mjwelchphd@gmail.com"
6
+ Recipient = "coco@tzarmail.com"
7
+ S3DBPath = "gmta-dev.db"
8
+ RemoteIP = "2001:4800:7811:513:be76:4eff:fe04:f0b4"
9
+ EhloDomainRequired = true
10
+
11
+
12
+ # Open the sqlite3 database for rubymta use
13
+ S3DB = Sequel.connect("sqlite://#{S3DBPath}")
14
+
15
+ # swaks -tls -s mail.tzarmail.com:25 -t mike@tzarmail.com -f mjwelchphd@gmail.com --ehlo bluecrapduck345at789zappo.com
16
+
17
+ # {:id=>5, :remote_ip=>"::ffff:107.185.187.182", :hits=>142, :locks=>1, :violations=>1,
18
+ # :expires_at=>2017-07-07 22:44:52 +0000, :created_at=>2017-05-22 19:58:51 +0000,
19
+ # :updated_at=>2017-07-07 22:41:11 +0000}
20
+ ds = S3DB[:contacts].where(:remote_ip=>RemoteIP)
21
+
22
+ describe "Rubymta Gem 'contacts table' Tests" do
23
+
24
+ # be sure we can set/reset the contacts table
25
+ it "reset the test contact record" do
26
+ ds.update(:locks=>0, :violations=>0, :expires_at=>Time.now)
27
+ rs = ds.select(:locks, :violations).first
28
+ (rs.inspect).should.be.equal "{:locks=>0, :violations=>0}"
29
+ end
30
+
31
+ # send a simple email just as a sanity check
32
+ it "the MTA should accept a simple email" do
33
+ response = `swaks -tls -s mail.tzarmail.com:25 -t #{Recipient} -f #{Sender}`
34
+ ok = response.index("250 2.0.0 OK id=")
35
+ ok.should.not.equal 0
36
+ end
37
+
38
+ # 1st violation -- reject a relay attempt
39
+ it "1st violation -- reject a relay attempt" do
40
+ response = `swaks -tls -s mail.tzarmail.com:25 -t mike.w@tzarmail.com -f #{Sender}`
41
+ ok = response.index("556 5.7.27 Czar Mail does not support relaying")
42
+ ok.should.not.equal 0
43
+ end
44
+
45
+ # the first failure should have set violations=1
46
+ it "should have 1 violation" do
47
+ rs = ds.select(:locks, :violations).first
48
+ (rs.inspect).should.be.equal "{:locks=>0, :violations=>1}"
49
+ end
50
+
51
+ # 2nd violation -- reject a relay attempt
52
+ it "2nd violation -- reject a relay attempt" do
53
+ response = `swaks -tls -s mail.tzarmail.com:25 -t mike.w@tzarmail.com -f #{Sender}`
54
+ ok = response.index("556 5.7.27 Czar Mail does not support relaying")
55
+ ok.should.not.equal 0
56
+ end
57
+
58
+ # the second failure should have set violations=2
59
+ it "should have 2 violations" do
60
+ rs = ds.select(:locks, :violations).first
61
+ (rs.inspect).should.be.equal "{:locks=>0, :violations=>2}"
62
+ end
63
+
64
+ # 3rd violation -- reject a relay attempt
65
+ it "3rd violation -- reject a relay attempt" do
66
+ response = `swaks -tls -s mail.tzarmail.com:25 -t mike.w@tzarmail.com -f #{Sender}`
67
+ ok = response.index("556 5.7.27 Czar Mail does not support relaying")
68
+ ok.should.not.equal 0
69
+ end
70
+
71
+ # the third failure should have set violations=3
72
+ it "should have 3 violations" do
73
+ rs = ds.select(:locks, :violations).first
74
+ (rs.inspect).should.be.equal "{:locks=>0, :violations=>3}"
75
+ end
76
+
77
+ # the 4th violation should respond with a message and set the lock
78
+ it "the 4th violation should set the lock" do
79
+ response = `swaks -tls -s mail.tzarmail.com:25 -t mike.w@tzarmail.com -f #{Sender}`
80
+ ok = response.index("454 4.7.1 Access TEMPORARILY denied")
81
+ ok.should.not.equal 0
82
+ end
83
+
84
+ # the fourth failure should have set violations=4 and locks=1
85
+ it "should have 4 violations and 1 lock" do
86
+ rs = ds.select(:locks, :violations).first
87
+ (rs.inspect).should.be.equal "{:locks=>1, :violations=>4}"
88
+ end
89
+
90
+ # the 5th (and following) violations should have to door slammed
91
+ it "5th violation -- slam the connection shut" do
92
+ response = `swaks -tls -s mail.tzarmail.com:25 -t mike.w@tzarmail.com -f #{Sender}`
93
+ ok = response.end_with?("=== Connected to mail.tzarmail.com.\n")
94
+ ok.inspect.should.equal "true"
95
+ end
96
+
97
+ # the 5th failure doesn't change the lock or violations
98
+ it "5th failure should still have 4 violations and 1 lock" do
99
+ rs = ds.select(:locks, :violations).first
100
+ (rs.inspect).should.be.equal "{:locks=>1, :violations=>4}"
101
+ end
102
+
103
+ end
104
+
105
+ describe "Rubymta Gem EHLO Tests" do
106
+
107
+ if EhloDomainRequired
108
+ ds.update(:locks=>0, :violations=>0, :expires_at=>Time.now)
109
+ it "EHLO requires a domain" do
110
+ response = `swaks -tls -s mail.tzarmail.com:25 -t #{Recipient} -f #{Sender} --ehlo 'not-valid-domain'`
111
+ ok = response.index("454 4.7.1 Access TEMPORARILY denied")
112
+ ok.should.not.equal 0
113
+ end
114
+
115
+ ds.update(:locks=>0, :violations=>0, :expires_at=>Time.now)
116
+ it "the EHLO domain must be found in the DNS" do
117
+ response = `swaks -tls -s mail.tzarmail.com:25 -t #{Recipient} -f #{Sender} --ehlo 'domain-not-in-dns.lost'`
118
+ ok = response.index("was not found in the DNS system")
119
+ ok.should.not.equal 0
120
+ end
121
+ end
122
+
123
+ end
124
+
125
+ describe "Rubymta Gem MAIL FROM Tests" do
126
+
127
+ ds.update(:locks=>0, :violations=>0, :expires_at=>Time.now)
128
+ it "the MAIL FROM line must have a sender" do
129
+ response = `swaks -tls -s mail.tzarmail.com:25 -t #{Recipient} -f mr-nobody`
130
+ ok = response.index("No proper sender (<...>) on the MAIL part line")
131
+ ok.should.not.equal 0
132
+ end
133
+
134
+ ds.update(:locks=>0, :violations=>0, :expires_at=>Time.now)
135
+ it "the MAIL FROM sender cannot have '.' as the first character" do
136
+ response = `swaks -tls -s mail.tzarmail.com:25 -t #{Recipient} -f .dennis@gmail.com`
137
+ ok = response.index("550 5.1.7 beginning or ending '.' or 2 or more '.'s in a row")
138
+ ok.should.not.equal 0
139
+ end
140
+
141
+ ds.update(:locks=>0, :violations=>0, :expires_at=>Time.now)
142
+ it "the MAIL FROM sender cannot have '.' as the last character" do
143
+ response = `swaks -tls -s mail.tzarmail.com:25 -t #{Recipient} -f dennis.@gmail.com`
144
+ ok = response.index("550 5.1.7 beginning or ending '.' or 2 or more '.'s in a row")
145
+ ok.should.not.equal 0
146
+ end
147
+
148
+ ds.update(:locks=>0, :violations=>0, :expires_at=>Time.now)
149
+ it "the MAIL FROM sender cannot have two '.'s in a row" do
150
+ response = `swaks -tls -s mail.tzarmail.com:25 -t #{Recipient} -f dennis..george@gmail.com`
151
+ ok = response.index("550 5.1.7 beginning or ending '.' or 2 or more '.'s in a row")
152
+ ok.should.not.equal 0
153
+ end
154
+
155
+ # send a MAIL FROM <a member>
156
+ ds.update(:locks=>0, :violations=>0, :expires_at=>Time.now)
157
+ it "the MTA expects member mail on port 587" do
158
+ response = `swaks -tls -s mail.tzarmail.com:25 -t #{Sender} -f #{Recipient}`
159
+ ok = response.index("556 5.7.27 Czar Mail members must use port 587 to send mail")
160
+ ok.should.not.equal 0
161
+ end
162
+
163
+ ds.update(:locks=>0, :violations=>0, :expires_at=>Time.now)
164
+ it "traffic on port 587 must be authenticated" do
165
+ response = `swaks -tls -s mail.tzarmail.com:587 -t #{Sender} -f #{Recipient}`
166
+ ok = response.index("556 5.7.27 Traffic on port 587 must be authenticated")
167
+ ok.should.not.equal 0
168
+ end
169
+
170
+ ds.update(:locks=>0, :violations=>0, :expires_at=>Time.now)
171
+ it "traffic on port 587 needs to be encrypted" do
172
+ response = `swaks -a PLAIN -s mail.tzarmail.com:587 -t #{Sender} -f #{Recipient} < spec/coco`
173
+ ok = response.index("556 5.7.27 Traffic on port 587 must be encrypted")
174
+ ok.should.not.equal 0
175
+ end
176
+
177
+ # send mail from a <non-member>
178
+ ds.update(:locks=>0, :violations=>0, :expires_at=>Time.now)
179
+ it "the MTA expects non-member mail on port 25" do
180
+ response = `swaks -tls -s mail.tzarmail.com:587 -t #{Recipient} -f #{Sender}`
181
+ ok = response.index("556 5.7.27 Non Czar Mail members must use port 25 to send mail")
182
+ ok.should.not.equal 0
183
+ end
184
+
185
+ end
186
+
187
+ describe "Rubymta Gem RCPT TO Tests" do
188
+
189
+ ds.update(:locks=>0, :violations=>0, :expires_at=>Time.now)
190
+ it "the RCPT TO line must have a recipient" do
191
+ response = `swaks -tls -s mail.tzarmail.com:25 -t mrs-nobody -f #{Sender}`
192
+ ok = response.index("No proper recipient (<...>) on the RCPT part line")
193
+ ok.should.not.equal 0
194
+ end
195
+
196
+ end
197
+
198
+ # There is only 1 unreachable test in DATA (I think it's unreachable),
199
+ # so the DATA tests here have been omitted.
metadata ADDED
@@ -0,0 +1,60 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rubymta
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Michael J. Welch, Ph.D.
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-07-29 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: RubyMta is an experimental mail transport agent written in Ruby. See
14
+ the README.
15
+ email: mjwelchphd@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - ".gitignore"
21
+ - CHANGELOG.md
22
+ - README.md
23
+ - lib/rubymta.rb
24
+ - lib/rubymta/base-x.rb
25
+ - lib/rubymta/contact.rb
26
+ - lib/rubymta/deepclone.rb
27
+ - lib/rubymta/extended_classes.rb
28
+ - lib/rubymta/item_of_mail.rb
29
+ - lib/rubymta/queue_runner.rb
30
+ - lib/rubymta/receiver.rb
31
+ - lib/rubymta/server.rb
32
+ - lib/rubymta/version.rb
33
+ - rubymta.gemspec
34
+ - spec/coco
35
+ - spec/rubymta.rb
36
+ homepage: http://rubygems.org/gems/rubymta
37
+ licenses:
38
+ - MIT
39
+ metadata: {}
40
+ post_install_message:
41
+ rdoc_options: []
42
+ require_paths:
43
+ - lib
44
+ required_ruby_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ required_rubygems_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ requirements: []
55
+ rubyforge_project:
56
+ rubygems_version: 2.5.1
57
+ signing_key:
58
+ specification_version: 4
59
+ summary: A Ruby Gem providing a complete Mail Transport Agent package.
60
+ test_files: []