fakesmtpd 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ *.pid
4
+
5
+ /.bundle/
6
+ /.config/
7
+ /.yardoc/
8
+ /Gemfile.lock
9
+ /_yardoc/
10
+ /coverage/
11
+ /doc/
12
+ /lib/bundler/man/
13
+ /pkg/
14
+ /rdoc/
15
+ /spec/reports/
16
+ /test/tmp/
17
+ /test/version_tmp/
18
+ /tmp/
19
+ /.artifacts/
@@ -0,0 +1 @@
1
+ 1.9.3-p448
@@ -0,0 +1,5 @@
1
+ ---
2
+ language: ruby
3
+ rvm:
4
+ - 1.9.3
5
+ - 2.0.0
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 ModCloth, Inc.
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,21 @@
1
+ `fakesmtpd`
2
+ ===========
3
+
4
+ A fake SMTP server with a minimal HTTP API.
5
+ Inspired by [mailtrap](https://github.com/mmower/mailtrap).
6
+
7
+ ## installation
8
+ You may install either via `gem` or by directly downloading the server
9
+ file which may then be used as an executable, e.g.:
10
+
11
+ ``` bash
12
+ gem install fakesmtpd
13
+ ```
14
+
15
+ **OR**
16
+
17
+ ``` bash
18
+ curl -o fakesmtpd https://raw.github.com/modcloth-labs/fakesmtpd/master/lib/fakesmtpd/server.rb
19
+ chmod +x fakesmtpd
20
+ ```
21
+
@@ -0,0 +1,17 @@
1
+ require 'bundler/gem_tasks'
2
+
3
+ desc 'Run minitest tests in ./test'
4
+ task test: :load_minitest do
5
+ Dir.glob("#{File.expand_path('../test', __FILE__)}/*_test.rb").each do |f|
6
+ require f
7
+ end
8
+
9
+ mkdir_p(File.expand_path('../.artifacts', __FILE__))
10
+ exit(MiniTest::Unit.new.run(%W(#{ENV['MINITEST_ARGS'] || ''})) || 1)
11
+ end
12
+
13
+ task :load_minitest do
14
+ require 'minitest/spec'
15
+ end
16
+
17
+ task default: :test
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require 'fakesmtpd'
3
+ FakeSMTPd::Server.main(ARGV)
@@ -0,0 +1,25 @@
1
+ # vim:fileencoding=utf-8
2
+
3
+ lib = File.expand_path('../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'fakesmtpd'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'fakesmtpd'
9
+ spec.version = FakeSMTPd::VERSION
10
+ spec.authors = ['Dan Buch']
11
+ spec.email = ['d.buch@modcloth.com']
12
+ spec.description = %q{A fake SMTP server with a minimal HTTP API}
13
+ spec.summary = %q{A fake SMTP server with a minimal HTTP API}
14
+ spec.homepage = 'https://github.com/modcloth-labs/fakesmtpd'
15
+ spec.license = 'MIT'
16
+
17
+ spec.files = `git ls-files`.split($/)
18
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
19
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
20
+ spec.require_paths = %w(lib)
21
+
22
+ spec.add_development_dependency 'bundler', '~> 1.3'
23
+ spec.add_development_dependency 'rake'
24
+ spec.add_development_dependency 'pry'
25
+ end
@@ -0,0 +1,6 @@
1
+ module FakeSMTPd
2
+ autoload :Server, 'fakesmtpd/server'
3
+ autoload :HTTPServer, 'fakesmtpd/server'
4
+ autoload :Runner, 'fakesmtpd/runner'
5
+ autoload :VERSION, 'fakesmtpd/version'
6
+ end
@@ -0,0 +1,63 @@
1
+ # vim:fileencoding=utf-8
2
+
3
+ require 'fileutils'
4
+ require 'logger'
5
+
6
+ class FakeSMTPd::Runner
7
+ attr_reader :port, :dir, :pidfile, :http_port
8
+ attr_reader :startup_sleep, :server_pid, :options
9
+
10
+ def initialize(options = {})
11
+ @dir = options.fetch(:dir)
12
+ @port = Integer(options.fetch(:port))
13
+ @http_port = Integer(options[:http_port] || port + 1)
14
+ @pidfile = options[:pidfile] || 'fakesmtpd.pid'
15
+ @startup_sleep = options[:startup_sleep] || 0.5
16
+ end
17
+
18
+ def description
19
+ "fakesmtpd server on port #{port}"
20
+ end
21
+
22
+ def command
23
+ [
24
+ RbConfig.ruby,
25
+ File.expand_path('../server.rb', __FILE__),
26
+ port.to_s,
27
+ dir,
28
+ pidfile,
29
+ ].join(' ')
30
+ end
31
+
32
+ def start
33
+ if dir
34
+ FileUtils.mkdir_p(dir)
35
+ end
36
+ process_command = command
37
+ log.info "Starting #{description}"
38
+ log.info " ---> #{process_command}"
39
+ @server_pid = Process.spawn(process_command)
40
+ sleep startup_sleep
41
+ server_pid
42
+ end
43
+
44
+ def stop
45
+ real_pid = Integer(File.read(pidfile).chomp) rescue nil
46
+ if server_pid && real_pid
47
+ log.info "Stopping #{description} " <<
48
+ "(shell PID=#{server_pid}, server PID=#{real_pid})"
49
+
50
+ [real_pid, server_pid].each do |pid|
51
+ Process.kill(:TERM, pid) rescue nil
52
+ end
53
+ end
54
+ end
55
+
56
+ def log
57
+ @log ||= Logger.new($stderr).tap do |l|
58
+ l.formatter = proc do |severity, datetime, _, msg|
59
+ "[fakesmtpd] #{severity} #{datetime} - #{msg}\n"
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,248 @@
1
+ #!/usr/bin/env ruby
2
+ # vim:fileencoding=utf-8
3
+ # Inspired by mailtrap (https://github.com/mmower/mailtrap)
4
+ #
5
+ # Copyright (c) 2013 ModCloth, Inc.
6
+ #
7
+ # MIT License
8
+ #
9
+ # Permission is hereby granted, free of charge, to any person obtaining
10
+ # a copy of this software and associated documentation files (the
11
+ # "Software"), to deal in the Software without restriction, including
12
+ # without limitation the rights to use, copy, modify, merge, publish,
13
+ # distribute, sublicense, and/or sell copies of the Software, and to
14
+ # permit persons to whom the Software is furnished to do so, subject to
15
+ # the following conditions:
16
+ #
17
+ # The above copyright notice and this permission notice shall be
18
+ # included in all copies or substantial portions of the Software.
19
+ #
20
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
24
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
25
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
26
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
27
+
28
+ require 'json'
29
+ require 'logger'
30
+ require 'socket'
31
+ require 'thread'
32
+
33
+ $fakesmtpd_semaphore = Mutex.new
34
+
35
+ module FakeSMTPd
36
+ class HTTPServer
37
+ attr_reader :server, :port, :smtpd, :log
38
+
39
+ def initialize(options = {})
40
+ @port = options.fetch(:port)
41
+ @smtpd = options.fetch(:smtpd)
42
+ @log = Logger.new($stderr).tap do |l|
43
+ l.formatter = proc do |severity, datetime, _, msg|
44
+ "[fakesmtpd-http] #{severity} #{datetime} - #{msg}\n"
45
+ end
46
+ end
47
+ end
48
+
49
+ def start
50
+ @server = Thread.new do
51
+ httpd = TCPServer.new(port)
52
+ log.info "FakeSMTPd HTTP server serving on #{port}"
53
+ log.info "PID=#{$$} Thread=#{Thread.current.inspect}"
54
+ loop do
55
+ client = httpd.accept
56
+ request_line = client.gets
57
+ log.info request_line.chomp
58
+ if request_line =~ /^GET \/messages /
59
+ client.puts 'HTTP/1.1 200 OK'
60
+ client.puts 'Content-type: application/json;charset=utf-8'
61
+ client.puts
62
+ $fakesmtpd_semaphore.synchronize do
63
+ client.puts JSON.pretty_generate(
64
+ message_files: smtpd.message_files_written
65
+ )
66
+ end
67
+ elsif request_line =~ /^DELETE \/messages /
68
+ $fakesmtpd_semaphore.synchronize do
69
+ smtpd.message_files_written.clear
70
+ end
71
+ client.puts 'HTTP/1.1 204 No Content'
72
+ client.puts
73
+ else
74
+ client.puts 'HTTP/1.1 405 Method Not Allowed'
75
+ client.puts 'Content-type: text/plain;charset=utf-8'
76
+ client.puts
77
+ client.puts 'Only "(GET|DELETE) /messages" is supported, eh.'
78
+ end
79
+ client.close
80
+ end
81
+ end
82
+ end
83
+
84
+ def kill!
85
+ @server && @server.kill
86
+ end
87
+ end
88
+
89
+ class Server
90
+ VERSION = '0.1.0'
91
+ USAGE = "Usage: #{File.basename($0)} <port> <message-dir> [pidfile]"
92
+
93
+ attr_reader :port, :message_dir, :log, :pidfile, :message_files_written
94
+
95
+ class << self
96
+ def main(argv = [].freeze)
97
+ if argv.include?('-h') || argv.include?('--help')
98
+ puts USAGE
99
+ exit 0
100
+ end
101
+ if argv.include?('--version')
102
+ puts FakeSMTPd::Server::VERSION
103
+ exit 0
104
+ end
105
+ unless argv.length > 1
106
+ abort USAGE
107
+ end
108
+ @smtpd = FakeSMTPd::Server.new(
109
+ port: Integer(argv.fetch(0)),
110
+ dir: argv.fetch(1),
111
+ pidfile: argv[2]
112
+ )
113
+ @httpd = FakeSMTPd::HTTPServer.new(
114
+ port: Integer(argv.fetch(0)) + 1,
115
+ smtpd: @smtpd,
116
+ )
117
+
118
+ $stderr.puts '--- Starting up ---'
119
+ @httpd.start
120
+ @smtpd.start
121
+ loop { sleep 1 }
122
+ rescue Exception => e
123
+ $stderr.puts '--- Shutting down ---'
124
+ @httpd && @httpd.kill!
125
+ @smtpd && @smtpd.kill!
126
+ unless e.is_a?(Interrupt)
127
+ raise e
128
+ end
129
+ end
130
+ end
131
+
132
+ def initialize(options = {})
133
+ @port = options.fetch(:port)
134
+ @message_dir = options.fetch(:dir)
135
+ @pidfile = options[:pidfile] || 'fakesmtpd.pid'
136
+ @log = Logger.new($stderr).tap do |l|
137
+ l.formatter = proc do |severity, datetime, _, msg|
138
+ "[fakesmtpd-smtp] #{severity} #{datetime} - #{msg}\n"
139
+ end
140
+ end
141
+ @message_files_written = []
142
+ end
143
+
144
+ def start
145
+ @server = Thread.new do
146
+ smtpd = TCPServer.new(port)
147
+ log.info "FakeSMTPd SMTP server serving on #{port}, " <<
148
+ "writing messages to #{message_dir.inspect}"
149
+ log.info "PID=#{$$}, Thread=#{Thread.current.inspect}"
150
+ File.open(pidfile, 'w') { |f| f.puts($$) }
151
+
152
+ loop do
153
+ begin
154
+ serve(smtpd.accept)
155
+ rescue => e
156
+ log.error "WAT: #{e.class.name} #{e.message}"
157
+ end
158
+ end
159
+ end
160
+ end
161
+
162
+ def kill!
163
+ @server && @server.kill
164
+ end
165
+
166
+ def serve(client)
167
+ class << client
168
+ attr_reader :client_id
169
+
170
+ def getline
171
+ line = gets
172
+ line.chomp! unless line.nil?
173
+ line
174
+ end
175
+
176
+ def to_s
177
+ @client_id ||= Time.now.utc.strftime('%Y%m%d%H%M%S%N')
178
+ "<smtp client #{@client_id}>"
179
+ end
180
+ end
181
+
182
+ client.puts '220 localhost fakesmtpd ready ESMTP'
183
+ helo = client.getline
184
+ log.info "#{client} Helo: #{helo.inspect}"
185
+
186
+ if helo =~ /^EHLO\s+/
187
+ log.info "#{client} Seen an EHLO"
188
+ client.puts '250-localhost only has this one extension'
189
+ client.puts '250 HELP'
190
+ end
191
+
192
+ from = client.getline
193
+ client.puts '250 OK'
194
+ log.info "#{client} From: #{from.inspect}"
195
+
196
+ recipients = []
197
+ loop do
198
+ to = client.getline
199
+ break if to.nil?
200
+
201
+ if to =~ /^DATA/
202
+ client.puts '354 Lemme have it'
203
+ break
204
+ else
205
+ log.info "#{client} To: #{to.inspect}"
206
+ recipients << to
207
+ client.puts '250 OK'
208
+ end
209
+ end
210
+
211
+ lines = []
212
+ loop do
213
+ line = client.getline
214
+ break if line.nil? || line == '.'
215
+ lines << line
216
+ log.debug "#{client} + #{line}"
217
+ end
218
+
219
+ client.puts '250 OK'
220
+ client.gets
221
+ client.puts '221 Buhbye'
222
+ client.close
223
+ log.info "#{client} ding!"
224
+
225
+ record(client, from, recipients, lines)
226
+ end
227
+
228
+ def record(client, from, recipients, body)
229
+ outfile = File.join(message_dir, "fakesmtpd-client-#{client.client_id}.json")
230
+ File.open(outfile, 'w') do |f|
231
+ f.write JSON.pretty_generate(
232
+ client_id: client.client_id,
233
+ from: from,
234
+ recipients: recipients,
235
+ body: body,
236
+ )
237
+ end
238
+ $fakesmtpd_semaphore.synchronize do
239
+ message_files_written << outfile
240
+ end
241
+ outfile
242
+ end
243
+ end
244
+ end
245
+
246
+ if __FILE__ == $0
247
+ FakeSMTPd::Server.main(ARGV)
248
+ end
@@ -0,0 +1,3 @@
1
+ module FakeSMTPd
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,56 @@
1
+ require 'json'
2
+ require 'net/http'
3
+ require 'net/smtp'
4
+ require 'uri'
5
+
6
+ require 'fakesmtpd'
7
+
8
+ describe 'fakesmtpd server' do
9
+ RUNNER = FakeSMTPd::Runner.new(
10
+ dir: File.expand_path('../../.artifacts', __FILE__),
11
+ port: rand(9100..9199)
12
+ )
13
+ RUNNER.start
14
+
15
+ at_exit { RUNNER.stop }
16
+
17
+ def randint
18
+ @randint ||= rand(999..1999)
19
+ end
20
+
21
+ def subject_header
22
+ @subject_header ||= "DERF DERF DERF #{randint}"
23
+ end
24
+
25
+ def msg
26
+ <<-EOM.gsub(/^ {6}/, '')
27
+ From: fakesmtpd <fakesmtpd@example.org>
28
+ To: Fruit Cake <fruitcake@example.org>
29
+ Subject: #{subject_header}
30
+ Date: Sat, 10 Aug 2013 16:59:20 +0500
31
+ Message-Id: <fancy.pants.are.fancy.#{randint}@example.org>
32
+
33
+ Herp derp derp herp.
34
+ EOM
35
+ end
36
+
37
+ def send_message
38
+ Net::SMTP.start('localhost', RUNNER.port) do |smtp|
39
+ smtp.send_message msg, 'fakesmtpd@example.org', 'fruitcake@example.org'
40
+ end
41
+ end
42
+
43
+ it 'accepts messages via SMTP' do
44
+ send_message
45
+ end
46
+
47
+ it 'reports messages sent via HTTP' do
48
+ send_message
49
+
50
+ uri = URI("http://localhost:#{RUNNER.http_port}/messages")
51
+ response = JSON.parse(Net::HTTP.get_response(uri).body)
52
+ message_file = response.fetch('message_files').last
53
+ message_body = JSON.parse(File.read(message_file)).fetch('body')
54
+ message_body.must_include("Subject: #{subject_header}")
55
+ end
56
+ end
metadata ADDED
@@ -0,0 +1,116 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fakesmtpd
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Dan Buch
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-08-11 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bundler
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '1.3'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '1.3'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rake
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: pry
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ description: A fake SMTP server with a minimal HTTP API
63
+ email:
64
+ - d.buch@modcloth.com
65
+ executables:
66
+ - fakesmtpd
67
+ extensions: []
68
+ extra_rdoc_files: []
69
+ files:
70
+ - .gitignore
71
+ - .ruby-version
72
+ - .travis.yml
73
+ - Gemfile
74
+ - LICENSE.txt
75
+ - README.md
76
+ - Rakefile
77
+ - bin/fakesmtpd
78
+ - fakesmtpd.gemspec
79
+ - lib/fakesmtpd.rb
80
+ - lib/fakesmtpd/runner.rb
81
+ - lib/fakesmtpd/server.rb
82
+ - lib/fakesmtpd/version.rb
83
+ - test/fakesmtpd_test.rb
84
+ homepage: https://github.com/modcloth-labs/fakesmtpd
85
+ licenses:
86
+ - MIT
87
+ post_install_message:
88
+ rdoc_options: []
89
+ require_paths:
90
+ - lib
91
+ required_ruby_version: !ruby/object:Gem::Requirement
92
+ none: false
93
+ requirements:
94
+ - - ! '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ segments:
98
+ - 0
99
+ hash: 55468274672838870
100
+ required_rubygems_version: !ruby/object:Gem::Requirement
101
+ none: false
102
+ requirements:
103
+ - - ! '>='
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ segments:
107
+ - 0
108
+ hash: 55468274672838870
109
+ requirements: []
110
+ rubyforge_project:
111
+ rubygems_version: 1.8.23
112
+ signing_key:
113
+ specification_version: 3
114
+ summary: A fake SMTP server with a minimal HTTP API
115
+ test_files:
116
+ - test/fakesmtpd_test.rb