fakesmtpd 0.1.0

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,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