rubymta 0.0.1

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