fakesmtpd 0.1.0 → 0.2.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.
@@ -3,3 +3,7 @@ language: ruby
3
3
  rvm:
4
4
  - 1.9.3
5
5
  - 2.0.0
6
+ notifications:
7
+ email:
8
+ recipients:
9
+ - github+fakesmtpd@modcloth.com
data/README.md CHANGED
@@ -1,12 +1,15 @@
1
1
  `fakesmtpd`
2
2
  ===========
3
3
 
4
+ [![Build Status](https://travis-ci.org/modcloth-labs/fakesmtpd.png?branch=master)](https://travis-ci.org/modcloth-labs/fakesmtpd)
5
+
4
6
  A fake SMTP server with a minimal HTTP API.
5
7
  Inspired by [mailtrap](https://github.com/mmower/mailtrap).
6
8
 
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.:
9
+ ## Installation
10
+ `fakesmtpd` may be installed either as a Ruby gem or by directly
11
+ downloading the server file which may then be used as an executable,
12
+ e.g.:
10
13
 
11
14
  ``` bash
12
15
  gem install fakesmtpd
@@ -19,3 +22,69 @@ curl -o fakesmtpd https://raw.github.com/modcloth-labs/fakesmtpd/master/lib/fake
19
22
  chmod +x fakesmtpd
20
23
  ```
21
24
 
25
+ ## Usage
26
+ `fakesmtpd` is usable either as an executable or from within a Ruby
27
+ process. The following examples are roughly equivalent.
28
+
29
+ ``` bash
30
+ # example command line invocation:
31
+ fakesmtpd 9025 ./fakesmtpd-messages -p ./fakesmtpd.pid -l ./fakesmtpd.log
32
+ ```
33
+
34
+ ``` ruby
35
+ # example in-process Ruby invocation:
36
+ require 'fakesmtpd'
37
+ FakeSMTPd::Server.main(
38
+ %w(9025 ./fakesmtpd-messages -p ./fakesmtpd.pid -l ./fakesmtpd.log)
39
+ )
40
+ ```
41
+
42
+ The `FakeSMTPd::Runner` class is intended to provide a
43
+ formalized way to spawn and manage a separate Ruby process running
44
+ `FakeSMTPd::Server`. This example is also equivalent to those provided
45
+ above:
46
+
47
+ ``` ruby
48
+ require 'fakesmtpd'
49
+
50
+ fakesmtpd = FakeSMTPd::Runner.new(
51
+ dir: File.expand_path('../fakesmtpd-messages', __FILE__),
52
+ port: 9025,
53
+ logfile: File.expand_path('../fakesmtpd.log', __FILE__),
54
+ pidfile: File.expand_path('../fakesmtpd.pid', __FILE__)
55
+ )
56
+ # This spawns another Ruby process via `RbConfig.ruby`
57
+ fakesmtpd.start
58
+
59
+ # ... do other stuff ...
60
+
61
+ # This will kill the previously-spawned process
62
+ fakesmtpd.stop
63
+ ```
64
+
65
+ Once `FakeSMTPd::Server.main` has been invoked via one of the above
66
+ methods, there will be both an SMTP and HTTP server running in a single
67
+ Ruby process. The HTTP port is one more than the SMTP port specified.
68
+
69
+ ### HTTP API Usage
70
+
71
+ The HTTP API provides an index of all sent messages, the ability to
72
+ fetch sent message content, and the ability to clear sent messages
73
+ via the following:
74
+
75
+ #### `GET /`
76
+
77
+ Contains almost nothing but a link to `/messages`.
78
+
79
+ #### `GET /messages`
80
+
81
+ Contains an index of all sent messages, their IDs, their absolute
82
+ filenames, and links to the content for each.
83
+
84
+ #### `GET /messages/[[:digit:]]`
85
+
86
+ Returns the content of the message with the given ID.
87
+
88
+ #### `DELETE /messages`
89
+
90
+ Clears all files out of the sent messages directory.
data/Rakefile CHANGED
@@ -1,7 +1,7 @@
1
1
  require 'bundler/gem_tasks'
2
2
 
3
3
  desc 'Run minitest tests in ./test'
4
- task test: :load_minitest do
4
+ task test: [:load_minitest, :clean_artifacts] do
5
5
  Dir.glob("#{File.expand_path('../test', __FILE__)}/*_test.rb").each do |f|
6
6
  require f
7
7
  end
@@ -14,4 +14,38 @@ task :load_minitest do
14
14
  require 'minitest/spec'
15
15
  end
16
16
 
17
+ task :clean_artifacts do
18
+ rm_rf(File.expand_path('../.artifacts', __FILE__))
19
+ end
20
+
21
+ desc 'Send a test email to a local SMTP server (on $PORT)'
22
+ task :send_email do
23
+ smtp_port = Integer(ENV['PORT'])
24
+ require 'net/smtp'
25
+ msg = <<-EOMSG.gsub(/^ {4}/, '')
26
+ From: Fake SMTPd <fakesmtpd@example.org>
27
+ To: Recipient Person <recipient@example.org>
28
+ Subject: Test Sandwich
29
+ Date: Sun, 11 Aug 2013 21:54:13 +0500
30
+ Message-Id: <this.sandwich.is.a.test.#{rand(999..1999)}@example.org>
31
+
32
+ Why have one sandwich when you can have #{rand(2..9)}?
33
+ ____
34
+ .----------' '-.
35
+ / . ' . \\
36
+ / ' . /|
37
+ / . \ /
38
+ / ' . . . || |
39
+ /.___________ ' / //
40
+ |._ '------'| /|
41
+ '.............______.-' /
42
+ jgs |-. | /
43
+ `"""""""""""""-.....-'
44
+
45
+ EOMSG
46
+ Net::SMTP.start('localhost', smtp_port) do |smtp|
47
+ smtp.send_message(msg, 'fakesmtpd@example.org', 'recipient@example.org')
48
+ end
49
+ end
50
+
17
51
  task default: :test
@@ -21,5 +21,4 @@ Gem::Specification.new do |spec|
21
21
 
22
22
  spec.add_development_dependency 'bundler', '~> 1.3'
23
23
  spec.add_development_dependency 'rake'
24
- spec.add_development_dependency 'pry'
25
24
  end
@@ -5,7 +5,7 @@ require 'logger'
5
5
 
6
6
  class FakeSMTPd::Runner
7
7
  attr_reader :port, :dir, :pidfile, :http_port
8
- attr_reader :startup_sleep, :server_pid, :options
8
+ attr_reader :startup_sleep, :server_pid, :options, :logfile
9
9
 
10
10
  def initialize(options = {})
11
11
  @dir = options.fetch(:dir)
@@ -13,6 +13,7 @@ class FakeSMTPd::Runner
13
13
  @http_port = Integer(options[:http_port] || port + 1)
14
14
  @pidfile = options[:pidfile] || 'fakesmtpd.pid'
15
15
  @startup_sleep = options[:startup_sleep] || 0.5
16
+ @logfile = options[:logfile] || $stderr
16
17
  end
17
18
 
18
19
  def description
@@ -20,13 +21,15 @@ class FakeSMTPd::Runner
20
21
  end
21
22
 
22
23
  def command
23
- [
24
- RbConfig.ruby,
25
- File.expand_path('../server.rb', __FILE__),
26
- port.to_s,
27
- dir,
28
- pidfile,
29
- ].join(' ')
24
+ (
25
+ [
26
+ RbConfig.ruby,
27
+ File.expand_path('../server.rb', __FILE__),
28
+ port.to_s,
29
+ dir,
30
+ '--pidfile', pidfile,
31
+ ] + (logfile == $stderr ? [] : ['--logfile', logfile])
32
+ ).join(' ')
30
33
  end
31
34
 
32
35
  def start
@@ -54,9 +57,9 @@ class FakeSMTPd::Runner
54
57
  end
55
58
 
56
59
  def log
57
- @log ||= Logger.new($stderr).tap do |l|
60
+ @log ||= Logger.new(@logfile).tap do |l|
58
61
  l.formatter = proc do |severity, datetime, _, msg|
59
- "[fakesmtpd] #{severity} #{datetime} - #{msg}\n"
62
+ "[fakesmtpd-runner] #{severity} #{datetime} - #{msg}\n"
60
63
  end
61
64
  end
62
65
  end
@@ -25,13 +25,13 @@
25
25
  # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
26
26
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
27
27
 
28
+ require 'fileutils'
28
29
  require 'json'
29
30
  require 'logger'
31
+ require 'optparse'
30
32
  require 'socket'
31
33
  require 'thread'
32
34
 
33
- $fakesmtpd_semaphore = Mutex.new
34
-
35
35
  module FakeSMTPd
36
36
  class HTTPServer
37
37
  attr_reader :server, :port, :smtpd, :log
@@ -39,7 +39,7 @@ module FakeSMTPd
39
39
  def initialize(options = {})
40
40
  @port = options.fetch(:port)
41
41
  @smtpd = options.fetch(:smtpd)
42
- @log = Logger.new($stderr).tap do |l|
42
+ @log = Logger.new(options[:logfile]).tap do |l|
43
43
  l.formatter = proc do |severity, datetime, _, msg|
44
44
  "[fakesmtpd-http] #{severity} #{datetime} - #{msg}\n"
45
45
  end
@@ -53,76 +53,203 @@ module FakeSMTPd
53
53
  log.info "PID=#{$$} Thread=#{Thread.current.inspect}"
54
54
  loop do
55
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.'
56
+ begin
57
+ request_line = client.gets
58
+ path = request_line.split[1]
59
+ handle_client(request_line, path, client)
60
+ rescue => e
61
+ handle_500(path, client, e)
78
62
  end
79
- client.close
80
63
  end
81
64
  end
82
65
  end
83
66
 
84
67
  def kill!
85
- @server && @server.kill
68
+ if @server
69
+ log.info "FakeSMTPd HTTP server stopping"
70
+ @server.kill
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ def handle_client(request_line, path, client)
77
+ log.info request_line.chomp
78
+ case request_line
79
+ when /^GET \/ /
80
+ handle_get_root(path, client)
81
+ when /^GET \/messages /
82
+ handle_get_messages(path, client)
83
+ when /^GET \/messages\/([[:digit:]]+) /
84
+ handle_get_message(path, client, $1)
85
+ when /^DELETE \/messages /
86
+ handle_clear_messages(path, client)
87
+ else
88
+ handle_404(path, client)
89
+ end
90
+ client.close
91
+ end
92
+
93
+ def handle_get_root(path, client)
94
+ client.puts 'HTTP/1.1 200 OK'
95
+ client.puts 'Content-type: application/json;charset=utf-8'
96
+ client.puts
97
+ client.puts JSON.pretty_generate(
98
+ _links: {
99
+ self: {href: path},
100
+ messages: {href: '/messages'},
101
+ }
102
+ )
103
+ end
104
+
105
+ def handle_get_messages(path, client)
106
+ client.puts 'HTTP/1.1 200 OK'
107
+ client.puts 'Content-type: application/json;charset=utf-8'
108
+ client.puts
109
+ client.puts JSON.pretty_generate(
110
+ _links: {
111
+ self: {href: path}
112
+ },
113
+ _embedded: {
114
+ messages: smtpd.messages.to_hash.map do |message_id, filename|
115
+ {
116
+ _links: {
117
+ self: {href: "/messages/#{message_id}"}
118
+ },
119
+ message_id: message_id,
120
+ filename: filename
121
+ }
122
+ end
123
+ }
124
+ )
125
+ end
126
+
127
+ def handle_get_message(path, client, message_id)
128
+ message_file = smtpd.messages[message_id]
129
+ if message_file
130
+ client.puts 'HTTP/1.1 200 OK'
131
+ client.puts 'Content-type: application/json;charset=utf-8'
132
+ client.puts
133
+ message = JSON.parse(File.read(message_file))
134
+ client.puts JSON.pretty_generate(
135
+ message.merge(
136
+ _links: {
137
+ self: {href: path}
138
+ },
139
+ filename: message_file
140
+ )
141
+ )
142
+ else
143
+ client.puts 'HTTP/1.1 404 Not Found'
144
+ client.puts 'Content-type: application/json;charset=utf-8'
145
+ client.puts
146
+ client.puts JSON.pretty_generate(
147
+ _links: {
148
+ self: {href: path}
149
+ },
150
+ error: "Message #{message_id.inspect} not found"
151
+ )
152
+ end
153
+ end
154
+
155
+ def handle_clear_messages(path, client)
156
+ smtpd.messages.clear
157
+ client.puts 'HTTP/1.1 204 No Content'
158
+ client.puts
159
+ end
160
+
161
+ def handle_404(path, client)
162
+ client.puts 'HTTP/1.1 404 Not Found'
163
+ client.puts 'Content-type: application/json;charset=utf-8'
164
+ client.puts
165
+ client.puts JSON.pretty_generate(
166
+ _links: {
167
+ self: {href: path}
168
+ },
169
+ error: 'Nothing is here'
170
+ )
171
+ end
172
+
173
+ def handle_500(path, client, e)
174
+ client.puts 'HTTP/1.1 500 Internal Server Error'
175
+ client.puts 'Content-type: application/json;charset=utf-8'
176
+ client.puts
177
+ client.puts JSON.pretty_generate(
178
+ _links: {
179
+ self: {href: path}
180
+ },
181
+ error: "#{e.class.name} #{e.message}",
182
+ backtrace: e.backtrace
183
+ )
86
184
  end
87
185
  end
88
186
 
89
187
  class Server
90
- VERSION = '0.1.0'
91
- USAGE = "Usage: #{File.basename($0)} <port> <message-dir> [pidfile]"
188
+ VERSION = '0.2.0'
189
+ USAGE = <<-EOU.gsub(/^ {6}/, '')
190
+ Usage: #{File.basename($0)} <smtp-port> <message-dir> [options]
92
191
 
93
- attr_reader :port, :message_dir, :log, :pidfile, :message_files_written
192
+ The `<smtp-port>` argument will be incremented by 1 for the HTTP API port.
193
+ The `<message-dir>` is where each SMTP transaction will be written as a
194
+ JSON file containing the "smtp client id" (timestamp from beginning of SMTP
195
+ transaction), the sender, recipients, and combined headers and body as
196
+ an array of strings.
197
+
198
+ EOU
199
+
200
+ attr_reader :port, :message_dir, :log, :logfile, :pidfile
201
+ attr_reader :messages
94
202
 
95
203
  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
204
+ def main(argv = [])
205
+ options = {
206
+ pidfile: nil,
207
+ logfile: $stderr,
208
+ }
209
+
210
+ OptionParser.new do |opts|
211
+ opts.banner = USAGE
212
+ opts.on('--version', 'Show version and exit') do |*|
213
+ puts "fakesmtpd #{FakeSMTPd::Server::VERSION}"
214
+ exit 0
215
+ end
216
+ opts.on('-p PIDFILE', '--pidfile PIDFILE',
217
+ 'Optional file where process PID will be written') do |pidfile|
218
+ options[:pidfile] = pidfile
219
+ end
220
+ opts.on('-l LOGFILE', '--logfile LOGFILE',
221
+ 'Optional file where all log messages will be written ' <<
222
+ '(default $stderr)') do |logfile|
223
+ options[:logfile] = logfile
224
+ end
225
+ end.parse!(argv)
226
+
227
+ unless argv.length == 2
106
228
  abort USAGE
107
229
  end
230
+
108
231
  @smtpd = FakeSMTPd::Server.new(
109
232
  port: Integer(argv.fetch(0)),
110
233
  dir: argv.fetch(1),
111
- pidfile: argv[2]
234
+ pidfile: options[:pidfile],
235
+ logfile: options[:logfile],
112
236
  )
113
237
  @httpd = FakeSMTPd::HTTPServer.new(
114
238
  port: Integer(argv.fetch(0)) + 1,
115
239
  smtpd: @smtpd,
240
+ logfile: options[:logfile],
116
241
  )
117
242
 
118
- $stderr.puts '--- Starting up ---'
119
243
  @httpd.start
120
244
  @smtpd.start
121
245
  loop { sleep 1 }
122
246
  rescue Exception => e
123
- $stderr.puts '--- Shutting down ---'
124
- @httpd && @httpd.kill!
125
- @smtpd && @smtpd.kill!
247
+ if @httpd
248
+ @httpd.kill!
249
+ end
250
+ if @smtpd
251
+ @smtpd.kill!
252
+ end
126
253
  unless e.is_a?(Interrupt)
127
254
  raise e
128
255
  end
@@ -133,12 +260,12 @@ module FakeSMTPd
133
260
  @port = options.fetch(:port)
134
261
  @message_dir = options.fetch(:dir)
135
262
  @pidfile = options[:pidfile] || 'fakesmtpd.pid'
136
- @log = Logger.new($stderr).tap do |l|
263
+ @log = Logger.new(options[:logfile]).tap do |l|
137
264
  l.formatter = proc do |severity, datetime, _, msg|
138
265
  "[fakesmtpd-smtp] #{severity} #{datetime} - #{msg}\n"
139
266
  end
140
267
  end
141
- @message_files_written = []
268
+ @messages = MessageStore.new(@message_dir)
142
269
  end
143
270
 
144
271
  def start
@@ -160,7 +287,10 @@ module FakeSMTPd
160
287
  end
161
288
 
162
289
  def kill!
163
- @server && @server.kill
290
+ if @server
291
+ log.info "FakeSMTPd SMTP server stopping"
292
+ @server.kill
293
+ end
164
294
  end
165
295
 
166
296
  def serve(client)
@@ -226,20 +356,56 @@ module FakeSMTPd
226
356
  end
227
357
 
228
358
  def record(client, from, recipients, body)
229
- outfile = File.join(message_dir, "fakesmtpd-client-#{client.client_id}.json")
359
+ messages.store(
360
+ client.client_id, from, recipients, body
361
+ )
362
+ end
363
+ end
364
+
365
+ class MessageStore
366
+ attr_reader :message_dir
367
+
368
+ def initialize(message_dir)
369
+ @message_dir = message_dir
370
+ end
371
+
372
+ def store(message_id, from, recipients, body)
373
+ outfile = File.join(message_dir, "fakesmtpd-client-#{message_id}.json")
230
374
  File.open(outfile, 'w') do |f|
231
375
  f.write JSON.pretty_generate(
232
- client_id: client.client_id,
376
+ message_id: message_id,
233
377
  from: from,
234
378
  recipients: recipients,
235
379
  body: body,
236
380
  )
237
381
  end
238
- $fakesmtpd_semaphore.synchronize do
239
- message_files_written << outfile
240
- end
241
382
  outfile
242
383
  end
384
+
385
+ def to_hash
386
+ message_files.each_with_object({}) do |filename, h|
387
+ message_id = File.basename(filename, '.json').gsub(/[^0-9]+/, '')
388
+ h[message_id] = File.expand_path(filename)
389
+ end
390
+ end
391
+
392
+ def [](message_id)
393
+ message_file = "#{message_dir}/fakesmtpd-client-#{message_id}.json"
394
+ if File.exists?(message_file)
395
+ return message_file
396
+ end
397
+ nil
398
+ end
399
+
400
+ def clear
401
+ FileUtils.rm_f(message_files)
402
+ end
403
+
404
+ private
405
+
406
+ def message_files
407
+ Dir.glob("#{message_dir}/fakesmtpd-client-*.json")
408
+ end
243
409
  end
244
410
  end
245
411
 
@@ -1,3 +1,3 @@
1
1
  module FakeSMTPd
2
- VERSION = '0.1.0'
2
+ VERSION = '0.2.0'
3
3
  end
@@ -8,11 +8,17 @@ require 'fakesmtpd'
8
8
  describe 'fakesmtpd server' do
9
9
  RUNNER = FakeSMTPd::Runner.new(
10
10
  dir: File.expand_path('../../.artifacts', __FILE__),
11
- port: rand(9100..9199)
11
+ port: rand(9100..9199),
12
+ logfile: File.expand_path('../../.artifacts/fakesmtpd.log', __FILE__),
13
+ pidfile: File.expand_path('../../.artifacts/fakesmtpd.pid', __FILE__),
12
14
  )
13
15
  RUNNER.start
14
16
 
15
- at_exit { RUNNER.stop }
17
+ at_exit do
18
+ RUNNER.stop
19
+ end
20
+
21
+ after(:each) { clear_messages }
16
22
 
17
23
  def randint
18
24
  @randint ||= rand(999..1999)
@@ -36,21 +42,49 @@ describe 'fakesmtpd server' do
36
42
 
37
43
  def send_message
38
44
  Net::SMTP.start('localhost', RUNNER.port) do |smtp|
39
- smtp.send_message msg, 'fakesmtpd@example.org', 'fruitcake@example.org'
45
+ smtp.send_message(msg, 'fakesmtpd@example.org', 'fruitcake@example.org')
40
46
  end
41
47
  end
42
48
 
49
+ def get_messages
50
+ uri = URI("http://localhost:#{RUNNER.http_port}/messages")
51
+ JSON.parse(Net::HTTP.get_response(uri).body)
52
+ end
53
+
54
+ def clear_messages
55
+ Net::HTTP.start('localhost', RUNNER.http_port) do |http|
56
+ http.request(Net::HTTP::Delete.new('/messages'))
57
+ end
58
+ end
59
+
60
+ def get_message(message_id)
61
+ uri = URI("http://localhost:#{RUNNER.http_port}/messages/#{message_id}")
62
+ JSON.parse(Net::HTTP.get_response(uri).body)
63
+ end
64
+
43
65
  it 'accepts messages via SMTP' do
44
66
  send_message
45
67
  end
46
68
 
47
- it 'reports messages sent via HTTP' do
69
+ it 'supports clearing sent messages via HTTP' do
70
+ clear_messages
71
+ end
72
+
73
+ it 'supports getting messages sent via HTTP' do
48
74
  send_message
75
+ response = get_messages
49
76
 
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
77
+ message_file = response.fetch('_embedded').fetch('messages').last.fetch('filename')
53
78
  message_body = JSON.parse(File.read(message_file)).fetch('body')
54
79
  message_body.must_include("Subject: #{subject_header}")
55
80
  end
81
+
82
+ it 'supports getting individual messages sent via HTTP' do
83
+ send_message
84
+ response = get_messages
85
+
86
+ message_id = response.fetch('_embedded').fetch('messages').last.fetch('message_id')
87
+ message_body = get_message(message_id).fetch('body')
88
+ message_body.must_include("Subject: #{subject_header}")
89
+ end
56
90
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fakesmtpd
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-08-11 00:00:00.000000000 Z
12
+ date: 2013-08-12 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -43,22 +43,6 @@ dependencies:
43
43
  - - ! '>='
44
44
  - !ruby/object:Gem::Version
45
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
46
  description: A fake SMTP server with a minimal HTTP API
63
47
  email:
64
48
  - d.buch@modcloth.com
@@ -96,7 +80,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
96
80
  version: '0'
97
81
  segments:
98
82
  - 0
99
- hash: 55468274672838870
83
+ hash: 2803386961380935915
100
84
  required_rubygems_version: !ruby/object:Gem::Requirement
101
85
  none: false
102
86
  requirements:
@@ -105,7 +89,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
105
89
  version: '0'
106
90
  segments:
107
91
  - 0
108
- hash: 55468274672838870
92
+ hash: 2803386961380935915
109
93
  requirements: []
110
94
  rubyforge_project:
111
95
  rubygems_version: 1.8.23