mailcatcher_v0.7.1 0.7.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a853ef962eff875bdb2eea87b896b59cf3c5bbef40d228dffaef761fc3052f81
4
+ data.tar.gz: 03d6ad8b7a904558a16dab0ad16ca7baae8694f1dfcf039a9acf6ddefefdc7f5
5
+ SHA512:
6
+ metadata.gz: e2ac1fed8b8b484b3cf8d44bb1deb133b937457eaf89d7e79ba509e5fb244ffa75cd706b91876c4d2b3f49a120e8adeba7aee04b87337554e18988e60ecbc72b
7
+ data.tar.gz: ac5479825d079974d3e4fcd62a88d765191cab0b2b934a38aef60385d9a0b25f0187406cb19caa0403e7f79ec0f532c57be6eb21ab99a808d5f0352cab40bbd7
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010-2011 Samuel Cochran
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,112 @@
1
+ # MailCatcher
2
+
3
+ Catches mail and serves it through a dream.
4
+
5
+ MailCatcher runs a super simple SMTP server which catches any message sent to it to display in a web interface. Run mailcatcher, set your favourite app to deliver to smtp://127.0.0.1:1025 instead of your default SMTP server, then check out http://127.0.0.1:1080 to see the mail that's arrived so far.
6
+
7
+ ![MailCatcher screenshot](https://cloud.githubusercontent.com/assets/14028/14093249/4100f904-f598-11e5-936b-e6a396f18e39.png)
8
+
9
+ ## Features
10
+
11
+ * Catches all mail and stores it for display.
12
+ * Shows HTML, Plain Text and Source version of messages, as applicable.
13
+ * Rewrites HTML enabling display of embedded, inline images/etc and opens links in a new window.
14
+ * Lists attachments and allows separate downloading of parts.
15
+ * Download original email to view in your native mail client(s).
16
+ * Command line options to override the default SMTP/HTTP IP and port settings.
17
+ * Mail appears instantly if your browser supports [WebSockets][websockets], otherwise updates every thirty seconds.
18
+ * Runs as a daemon in the background, optionally in foreground.
19
+ * Sendmail-analogue command, `catchmail`, makes using mailcatcher from PHP a lot easier.
20
+ * Keyboard navigation between messages
21
+
22
+ ## How
23
+
24
+ 1. `gem install mailcatcher`
25
+ 2. `mailcatcher`
26
+ 3. Go to http://127.0.0.1:1080/
27
+ 4. Send mail through smtp://127.0.0.1:1025
28
+
29
+ Use `mailcatcher --help` to see the command line options. The brave can get the source from [the GitHub repository][mailcatcher-github].
30
+
31
+ ### Bundler
32
+
33
+ Please don't put mailcatcher into your Gemfile. It will conflict with your applications gems at some point.
34
+
35
+ Instead, pop a note in your README stating you use mailcatcher, and to run `gem install mailcatcher` then `mailcatcher` to get started.
36
+
37
+ ### RVM
38
+
39
+ Under RVM your mailcatcher command may only be available under the ruby you install mailcatcher into. To prevent this, and to prevent gem conflicts, install mailcatcher into a dedicated gemset with a wrapper script:
40
+
41
+ rvm default@mailcatcher --create do gem install mailcatcher
42
+ ln -s "$(rvm default@mailcatcher do rvm wrapper show mailcatcher)" "$rvm_bin_path/"
43
+
44
+ ### Rails
45
+
46
+ To set up your rails app, I recommend adding this to your `environments/development.rb`:
47
+
48
+ config.action_mailer.delivery_method = :smtp
49
+ config.action_mailer.smtp_settings = { :address => '127.0.0.1', :port => 1025 }
50
+ config.action_mailer.raise_delivery_errors = false
51
+
52
+ ### PHP
53
+
54
+ For projects using PHP, or PHP frameworks and application platforms like Drupal, you can set [PHP's mail configuration](http://www.php.net/manual/en/mail.configuration.php) in your [php.ini](http://www.php.net/manual/en/configuration.file.php) to send via MailCatcher with:
55
+
56
+ sendmail_path = /usr/bin/env catchmail -f some@from.address
57
+
58
+ You can do this in your [Apache configuration](http://php.net/manual/en/configuration.changes.php) like so:
59
+
60
+ php_admin_value sendmail_path "/usr/bin/env catchmail -f some@from.address"
61
+
62
+ If you've installed via RVM this probably won't work unless you've manually added your RVM bin paths to your system environment's PATH. In that case, run `which catchmail` and put that path into the `sendmail_path` directive above instead of `/usr/bin/env catchmail`.
63
+
64
+ If starting `mailcatcher` on alternative SMTP IP and/or port with parameters like `--smtp-ip 192.168.0.1 --smtp-port 10025`, add the same parameters to your `catchmail` command:
65
+
66
+ sendmail_path = /usr/bin/env catchmail --smtp-ip 192.160.0.1 --smtp-port 10025 -f some@from.address
67
+
68
+ ### Django
69
+
70
+ For use in Django, add the following configuration to your projects' settings.py
71
+
72
+ ```python
73
+ if DEBUG:
74
+ EMAIL_HOST = '127.0.0.1'
75
+ EMAIL_HOST_USER = ''
76
+ EMAIL_HOST_PASSWORD = ''
77
+ EMAIL_PORT = 1025
78
+ EMAIL_USE_TLS = False
79
+ ```
80
+
81
+ ### API
82
+
83
+ A fairly RESTful URL schema means you can download a list of messages in JSON from `/messages`, each message's metadata with `/messages/:id.json`, and then the pertinent parts with `/messages/:id.html` and `/messages/:id.plain` for the default HTML and plain text version, `/messages/:id/:cid` for individual attachments by CID, or the whole message with `/messages/:id.source`.
84
+
85
+ ## Caveats
86
+
87
+ * Mail processing is fairly basic but easily modified. If something doesn't work for you, fork and fix it or [file an issue][mailcatcher-issues] and let me know. Include the whole message you're having problems with.
88
+ * Encodings are difficult. MailCatcher does not completely support utf-8 straight over the wire, you must use a mail library which encodes things properly based on SMTP server capabilities.
89
+
90
+ ## TODO
91
+
92
+ * Add mail delivery on request, optionally multiple times.
93
+ * Compatibility testing against CampaignMonitor's [design guidelines](http://www.campaignmonitor.com/design-guidelines/) and [CSS support matrix](http://www.campaignmonitor.com/css/).
94
+ * Forward mail to rendering service, maybe CampaignMonitor?
95
+
96
+ ## Thanks
97
+
98
+ MailCatcher is just a mishmash of other people's hard work. Thank you so much to the people who have built the wonderful guts on which this project relies.
99
+
100
+ ## Donations
101
+
102
+ I work on MailCatcher mostly in my own spare time. If you've found Mailcatcher useful and would like to help feed me and fund continued development and new features, please [donate via PayPal][donate]. If you'd like a specific feature added to MailCatcher and are willing to pay for it, please [email me](mailto:sj26@sj26.com).
103
+
104
+ ## License
105
+
106
+ Copyright © 2010-2018 Samuel Cochran (sj26@sj26.com). Released under the MIT License, see [LICENSE][license] for details.
107
+
108
+ [donate]: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=522WUPLRWUSKE
109
+ [license]: https://github.com/sj26/mailcatcher/blob/master/LICENSE
110
+ [mailcatcher-github]: https://github.com/sj26/mailcatcher
111
+ [mailcatcher-issues]: https://github.com/sj26/mailcatcher/issues
112
+ [websockets]: http://www.whatwg.org/specs/web-socket-protocol/
data/bin/catchmail ADDED
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ begin
4
+ require 'mail'
5
+ rescue LoadError
6
+ require 'rubygems'
7
+ require 'mail'
8
+ end
9
+
10
+ require 'optparse'
11
+
12
+ options = {:smtp_ip => '127.0.0.1', :smtp_port => 1025}
13
+
14
+ OptionParser.new do |parser|
15
+ parser.banner = <<-BANNER.gsub /^ +/, ""
16
+ Usage: catchmail [options] [recipient ...]
17
+ sendmail-like interface to forward mail to MailCatcher.
18
+ BANNER
19
+
20
+ parser.on('--ip IP') do |ip|
21
+ options[:smtp_ip] = ip
22
+ end
23
+
24
+ parser.on('--smtp-ip IP', 'Set the ip address of the smtp server') do |ip|
25
+ options[:smtp_ip] = ip
26
+ end
27
+
28
+ parser.on('--smtp-port PORT', Integer, 'Set the port of the smtp server') do |port|
29
+ options[:smtp_port] = port
30
+ end
31
+
32
+ parser.on('-f FROM', 'Set the sending address') do |from|
33
+ options[:from] = from
34
+ end
35
+
36
+ parser.on('-oi', 'Ignored option -oi') do |ignored|
37
+ end
38
+ parser.on('-t', 'Ignored option -t') do |ignored|
39
+ end
40
+ parser.on('-q', 'Ignored option -q') do |ignored|
41
+ end
42
+
43
+ parser.on('-x', '--no-exit', 'Can\'t exit from the application') do
44
+ options[:no_exit] = true
45
+ end
46
+
47
+ parser.on('-h', '--help', 'Display this help information') do
48
+ puts parser
49
+ exit!
50
+ end
51
+ end.parse!
52
+
53
+ Mail.defaults do
54
+ delivery_method :smtp,
55
+ :address => options[:smtp_ip],
56
+ :port => options[:smtp_port]
57
+ end
58
+
59
+ message = Mail.new($stdin.read)
60
+
61
+ message.return_path = options[:from] if options[:from]
62
+
63
+ ARGV.each do |recipient|
64
+ if message.to.nil?
65
+ message.to = recipient
66
+ else
67
+ message.to << recipient
68
+ end
69
+ end
70
+
71
+ message.deliver
data/bin/mailcatcher ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'mail_catcher'
4
+
5
+ MailCatcher.run!
@@ -0,0 +1,7 @@
1
+ require "eventmachine"
2
+
3
+ module MailCatcher
4
+ module Events
5
+ MessageAdded = EventMachine::Channel.new
6
+ end
7
+ end
@@ -0,0 +1,166 @@
1
+ require "eventmachine"
2
+ require "json"
3
+ require "mail"
4
+ require "sqlite3"
5
+
6
+ module MailCatcher::Mail extend self
7
+ def db
8
+ @__db ||= begin
9
+ SQLite3::Database.new(":memory:", :type_translation => true).tap do |db|
10
+ db.execute(<<-SQL)
11
+ CREATE TABLE message (
12
+ id INTEGER PRIMARY KEY ASC,
13
+ sender TEXT,
14
+ recipients TEXT,
15
+ subject TEXT,
16
+ source BLOB,
17
+ size TEXT,
18
+ type TEXT,
19
+ created_at DATETIME DEFAULT CURRENT_DATETIME
20
+ )
21
+ SQL
22
+ db.execute(<<-SQL)
23
+ CREATE TABLE message_part (
24
+ id INTEGER PRIMARY KEY ASC,
25
+ message_id INTEGER NOT NULL,
26
+ cid TEXT,
27
+ type TEXT,
28
+ is_attachment INTEGER,
29
+ filename TEXT,
30
+ charset TEXT,
31
+ body BLOB,
32
+ size INTEGER,
33
+ created_at DATETIME DEFAULT CURRENT_DATETIME
34
+ )
35
+ SQL
36
+ end
37
+ end
38
+ end
39
+
40
+ def add_message(message)
41
+ @add_message_query ||= db.prepare("INSERT INTO message (sender, recipients, subject, source, type, size, created_at) VALUES (?, ?, ?, ?, ?, ?, datetime('now'))")
42
+
43
+ mail = Mail.new(message[:source])
44
+ @add_message_query.execute(message[:sender], JSON.generate(message[:recipients]), mail.subject, message[:source], mail.mime_type || "text/plain", message[:source].length)
45
+ message_id = db.last_insert_row_id
46
+ parts = mail.all_parts
47
+ parts = [mail] if parts.empty?
48
+ parts.each do |part|
49
+ body = part.body.to_s
50
+ # Only parts have CIDs, not mail
51
+ cid = part.cid if part.respond_to? :cid
52
+ add_message_part(message_id, cid, part.mime_type || "text/plain", part.attachment? ? 1 : 0, part.filename, part.charset, body, body.length)
53
+ end
54
+
55
+ EventMachine.next_tick do
56
+ message = MailCatcher::Mail.message message_id
57
+ MailCatcher::Events::MessageAdded.push message
58
+ end
59
+ end
60
+
61
+ def add_message_part(*args)
62
+ @add_message_part_query ||= db.prepare "INSERT INTO message_part (message_id, cid, type, is_attachment, filename, charset, body, size, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))"
63
+ @add_message_part_query.execute(*args)
64
+ end
65
+
66
+ def latest_created_at
67
+ @latest_created_at_query ||= db.prepare "SELECT created_at FROM message ORDER BY created_at DESC LIMIT 1"
68
+ @latest_created_at_query.execute.next
69
+ end
70
+
71
+ def messages
72
+ @messages_query ||= db.prepare "SELECT id, sender, recipients, subject, size, created_at FROM message ORDER BY created_at, id ASC"
73
+ @messages_query.execute.map do |row|
74
+ Hash[row.fields.zip(row)].tap do |message|
75
+ message["recipients"] &&= JSON.parse(message["recipients"])
76
+ end
77
+ end
78
+ end
79
+
80
+ def message(id)
81
+ @message_query ||= db.prepare "SELECT id, sender, recipients, subject, size, type, created_at FROM message WHERE id = ? LIMIT 1"
82
+ row = @message_query.execute(id).next
83
+ row && Hash[row.fields.zip(row)].tap do |message|
84
+ message["recipients"] &&= JSON.parse(message["recipients"])
85
+ end
86
+ end
87
+
88
+ def message_source(id)
89
+ @message_source_query ||= db.prepare "SELECT source FROM message WHERE id = ? LIMIT 1"
90
+ row = @message_source_query.execute(id).next
91
+ row && row.first
92
+ end
93
+
94
+ def message_has_html?(id)
95
+ @message_has_html_query ||= db.prepare "SELECT 1 FROM message_part WHERE message_id = ? AND is_attachment = 0 AND type IN ('application/xhtml+xml', 'text/html') LIMIT 1"
96
+ (!!@message_has_html_query.execute(id).next) || ["text/html", "application/xhtml+xml"].include?(message(id)["type"])
97
+ end
98
+
99
+ def message_has_plain?(id)
100
+ @message_has_plain_query ||= db.prepare "SELECT 1 FROM message_part WHERE message_id = ? AND is_attachment = 0 AND type = 'text/plain' LIMIT 1"
101
+ (!!@message_has_plain_query.execute(id).next) || message(id)["type"] == "text/plain"
102
+ end
103
+
104
+ def message_parts(id)
105
+ @message_parts_query ||= db.prepare "SELECT cid, type, filename, size FROM message_part WHERE message_id = ? ORDER BY filename ASC"
106
+ @message_parts_query.execute(id).map do |row|
107
+ Hash[row.fields.zip(row)]
108
+ end
109
+ end
110
+
111
+ def message_attachments(id)
112
+ @message_parts_query ||= db.prepare "SELECT cid, type, filename, size FROM message_part WHERE message_id = ? AND is_attachment = 1 ORDER BY filename ASC"
113
+ @message_parts_query.execute(id).map do |row|
114
+ Hash[row.fields.zip(row)]
115
+ end
116
+ end
117
+
118
+ def message_part(message_id, part_id)
119
+ @message_part_query ||= db.prepare "SELECT * FROM message_part WHERE message_id = ? AND id = ? LIMIT 1"
120
+ row = @message_part_query.execute(message_id, part_id).next
121
+ row && Hash[row.fields.zip(row)]
122
+ end
123
+
124
+ def message_part_type(message_id, part_type)
125
+ @message_part_type_query ||= db.prepare "SELECT * FROM message_part WHERE message_id = ? AND type = ? AND is_attachment = 0 LIMIT 1"
126
+ row = @message_part_type_query.execute(message_id, part_type).next
127
+ row && Hash[row.fields.zip(row)]
128
+ end
129
+
130
+ def message_part_html(message_id)
131
+ part = message_part_type(message_id, "text/html")
132
+ part ||= message_part_type(message_id, "application/xhtml+xml")
133
+ part ||= begin
134
+ message = message(message_id)
135
+ message if message and ["text/html", "application/xhtml+xml"].include? message["type"]
136
+ end
137
+ end
138
+
139
+ def message_part_plain(message_id)
140
+ message_part_type message_id, "text/plain"
141
+ end
142
+
143
+ def message_part_cid(message_id, cid)
144
+ @message_part_cid_query ||= db.prepare "SELECT * FROM message_part WHERE message_id = ?"
145
+ @message_part_cid_query.execute(message_id).map do |row|
146
+ Hash[row.fields.zip(row)]
147
+ end.find do |part|
148
+ part["cid"] == cid
149
+ end
150
+ end
151
+
152
+ def delete!
153
+ @delete_all_messages_query ||= db.prepare "DELETE FROM message"
154
+ @delete_all_message_parts_query ||= db.prepare "DELETE FROM message_part"
155
+
156
+ @delete_all_messages_query.execute and
157
+ @delete_all_message_parts_query.execute
158
+ end
159
+
160
+ def delete_message!(message_id)
161
+ @delete_messages_query ||= db.prepare "DELETE FROM message WHERE id = ?"
162
+ @delete_message_parts_query ||= db.prepare "DELETE FROM message_part WHERE message_id = ?"
163
+ @delete_messages_query.execute(message_id) and
164
+ @delete_message_parts_query.execute(message_id)
165
+ end
166
+ end
@@ -0,0 +1,55 @@
1
+ require "eventmachine"
2
+
3
+ require "mail_catcher/mail"
4
+
5
+ class MailCatcher::Smtp < EventMachine::Protocols::SmtpServer
6
+ # We override EM's mail from processing to allow multiple mail-from commands
7
+ # per [RFC 2821](http://tools.ietf.org/html/rfc2821#section-4.1.1.2)
8
+ def process_mail_from sender
9
+ if @state.include? :mail_from
10
+ @state -= [:mail_from, :rcpt, :data]
11
+ receive_reset
12
+ end
13
+
14
+ super
15
+ end
16
+
17
+ def current_message
18
+ @current_message ||= {}
19
+ end
20
+
21
+ def receive_reset
22
+ @current_message = nil
23
+ true
24
+ end
25
+
26
+ def receive_sender(sender)
27
+ current_message[:sender] = sender
28
+ true
29
+ end
30
+
31
+ def receive_recipient(recipient)
32
+ current_message[:recipients] ||= []
33
+ current_message[:recipients] << recipient
34
+ true
35
+ end
36
+
37
+ def receive_data_chunk(lines)
38
+ current_message[:source] ||= ""
39
+ lines.each do |line|
40
+ current_message[:source] << line << "\r\n"
41
+ end
42
+ true
43
+ end
44
+
45
+ def receive_message
46
+ MailCatcher::Mail.add_message current_message
47
+ puts "==> SMTP: Received message from '#{current_message[:sender]}' (#{current_message[:source].length} bytes)"
48
+ true
49
+ rescue => exception
50
+ MailCatcher.log_exception("Error receiving message", @current_message, exception)
51
+ false
52
+ ensure
53
+ @current_message = nil
54
+ end
55
+ end
@@ -0,0 +1,3 @@
1
+ module MailCatcher
2
+ VERSION = "0.7.1"
3
+ end
@@ -0,0 +1,180 @@
1
+ require "pathname"
2
+ require "net/http"
3
+ require "uri"
4
+
5
+ require "sinatra"
6
+ require "skinny"
7
+
8
+ require "mail_catcher/events"
9
+ require "mail_catcher/mail"
10
+
11
+ class Sinatra::Request
12
+ include Skinny::Helpers
13
+ end
14
+
15
+ module MailCatcher
16
+ module Web
17
+ class Application < Sinatra::Base
18
+ set :environment, MailCatcher.env
19
+ set :prefix, MailCatcher.options[:http_path]
20
+ set :asset_prefix, File.join(prefix, "assets")
21
+ set :root, File.expand_path("#{__FILE__}/../../../..")
22
+
23
+ if development?
24
+ require "sprockets-helpers"
25
+
26
+ configure do
27
+ require "mail_catcher/web/assets"
28
+ Sprockets::Helpers.configure do |config|
29
+ config.environment = Assets
30
+ config.prefix = settings.asset_prefix
31
+ config.digest = false
32
+ config.public_path = public_folder
33
+ config.debug = true
34
+ end
35
+ end
36
+
37
+ helpers do
38
+ include Sprockets::Helpers
39
+ end
40
+ else
41
+ helpers do
42
+ def asset_path(filename)
43
+ File.join(settings.asset_prefix, filename)
44
+ end
45
+ end
46
+ end
47
+
48
+ get "/" do
49
+ erb :index
50
+ end
51
+
52
+ delete "/" do
53
+ if MailCatcher.quittable?
54
+ MailCatcher.quit!
55
+ status 204
56
+ else
57
+ status 403
58
+ end
59
+ end
60
+
61
+ get "/messages" do
62
+ if request.websocket?
63
+ request.websocket!(
64
+ :on_start => proc do |websocket|
65
+ subscription = Events::MessageAdded.subscribe do |message|
66
+ begin
67
+ websocket.send_message(JSON.generate(message))
68
+ rescue => exception
69
+ MailCatcher.log_exception("Error sending message through websocket", message, exception)
70
+ end
71
+ end
72
+
73
+ websocket.on_close do |*|
74
+ Events::MessageAdded.unsubscribe subscription
75
+ end
76
+ end)
77
+ else
78
+ content_type :json
79
+ JSON.generate(Mail.messages)
80
+ end
81
+ end
82
+
83
+ delete "/messages" do
84
+ Mail.delete!
85
+ status 204
86
+ end
87
+
88
+ get "/messages/:id.json" do
89
+ id = params[:id].to_i
90
+ if message = Mail.message(id)
91
+ content_type :json
92
+ JSON.generate(message.merge({
93
+ "formats" => [
94
+ "source",
95
+ ("html" if Mail.message_has_html? id),
96
+ ("plain" if Mail.message_has_plain? id)
97
+ ].compact,
98
+ "attachments" => Mail.message_attachments(id).map do |attachment|
99
+ path_name = MailCatcher.options[:http_path] == "/" ? "" : MailCatcher.options[:http_path]
100
+ attachment.merge({"href" => "#{path_name}/messages/#{escape(id)}/parts/#{escape(attachment["cid"])}"})
101
+ end,
102
+ }))
103
+ else
104
+ not_found
105
+ end
106
+ end
107
+
108
+ get "/messages/:id.html" do
109
+ id = params[:id].to_i
110
+ if part = Mail.message_part_html(id)
111
+ content_type :html, :charset => (part["charset"] || "utf8")
112
+
113
+ body = part["body"]
114
+
115
+ # Rewrite body to link to embedded attachments served by cid
116
+ body.gsub! /cid:([^'"> ]+)/, "#{id}/parts/\\1"
117
+
118
+ body
119
+ else
120
+ not_found
121
+ end
122
+ end
123
+
124
+ get "/messages/:id.plain" do
125
+ id = params[:id].to_i
126
+ if part = Mail.message_part_plain(id)
127
+ content_type part["type"], :charset => (part["charset"] || "utf8")
128
+ part["body"]
129
+ else
130
+ not_found
131
+ end
132
+ end
133
+
134
+ get "/messages/:id.source" do
135
+ id = params[:id].to_i
136
+ if message_source = Mail.message_source(id)
137
+ content_type "text/plain"
138
+ message_source
139
+ else
140
+ not_found
141
+ end
142
+ end
143
+
144
+ get "/messages/:id.eml" do
145
+ id = params[:id].to_i
146
+ if message_source = Mail.message_source(id)
147
+ content_type "message/rfc822"
148
+ message_source
149
+ else
150
+ not_found
151
+ end
152
+ end
153
+
154
+ get "/messages/:id/parts/:cid" do
155
+ id = params[:id].to_i
156
+ if part = Mail.message_part_cid(id, params[:cid])
157
+ content_type part["type"], :charset => (part["charset"] || "utf8")
158
+ attachment part["filename"] if part["is_attachment"] == 1
159
+ body part["body"].to_s
160
+ else
161
+ not_found
162
+ end
163
+ end
164
+
165
+ delete "/messages/:id" do
166
+ id = params[:id].to_i
167
+ if Mail.message(id)
168
+ Mail.delete_message!(id)
169
+ status 204
170
+ else
171
+ not_found
172
+ end
173
+ end
174
+
175
+ not_found do
176
+ erb :"404"
177
+ end
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,27 @@
1
+ require "rack/builder"
2
+
3
+ require "mail_catcher/web/application"
4
+
5
+ module MailCatcher
6
+ module Web extend self
7
+ def app
8
+ @@app ||= Rack::Builder.new do
9
+ map(MailCatcher.options[:http_path]) do
10
+ if MailCatcher.development?
11
+ require "mail_catcher/web/assets"
12
+ map("/assets") { run Assets }
13
+ end
14
+
15
+ run Application
16
+ end
17
+
18
+ # This should only affect when http_path is anything but "/" above
19
+ run lambda { |env| [302, {"Location" => MailCatcher.options[:http_path]}, []] }
20
+ end
21
+ end
22
+
23
+ def call(env)
24
+ app.call(env)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,255 @@
1
+ # Apparently rubygems won't activate these on its own, so here we go. Let's
2
+ # repeat the invention of Bundler all over again.
3
+ gem "eventmachine", "1.0.9.1"
4
+ gem "mail", "~> 2.3"
5
+ gem "rack", "~> 1.5"
6
+ gem "sinatra", "~> 1.2"
7
+ gem "sqlite3", "~> 1.3"
8
+ gem "thin", "~> 1.5.0"
9
+ gem "skinny", "~> 0.2.3"
10
+
11
+ require "open3"
12
+ require "optparse"
13
+ require "rbconfig"
14
+
15
+ require "eventmachine"
16
+ require "thin"
17
+
18
+ module EventMachine
19
+ # Monkey patch fix for 10deb4
20
+ # See https://github.com/eventmachine/eventmachine/issues/569
21
+ def self.reactor_running?
22
+ (@reactor_running || false)
23
+ end
24
+ end
25
+
26
+ require "mail_catcher/version"
27
+
28
+ module MailCatcher extend self
29
+ autoload :Events, "mail_catcher/events"
30
+ autoload :Mail, "mail_catcher/mail"
31
+ autoload :Smtp, "mail_catcher/smtp"
32
+ autoload :Web, "mail_catcher/web"
33
+
34
+ def env
35
+ ENV.fetch("MAILCATCHER_ENV", "production")
36
+ end
37
+
38
+ def development?
39
+ env == "development"
40
+ end
41
+
42
+ def which?(command)
43
+ ENV["PATH"].split(File::PATH_SEPARATOR).any? do |directory|
44
+ File.executable?(File.join(directory, command.to_s))
45
+ end
46
+ end
47
+
48
+ def mac?
49
+ RbConfig::CONFIG["host_os"] =~ /darwin/
50
+ end
51
+
52
+ def windows?
53
+ RbConfig::CONFIG["host_os"] =~ /mswin|mingw/
54
+ end
55
+
56
+ def macruby?
57
+ mac? and const_defined? :MACRUBY_VERSION
58
+ end
59
+
60
+ def browseable?
61
+ windows? or which? "open"
62
+ end
63
+
64
+ def browse url
65
+ if windows?
66
+ system "start", "/b", url
67
+ elsif which? "open"
68
+ system "open", url
69
+ end
70
+ end
71
+
72
+ def log_exception(message, context, exception)
73
+ gems_paths = (Gem.path | [Gem.default_dir]).map { |path| Regexp.escape(path) }
74
+ gems_regexp = %r{(?:#{gems_paths.join("|")})/gems/([^/]+)-([\w.]+)/(.*)}
75
+ gems_replace = '\1 (\2) \3'
76
+
77
+ puts "*** #{message}: #{context.inspect}"
78
+ puts " Exception: #{exception}"
79
+ puts " Backtrace:", *exception.backtrace.map { |line| " #{line.sub(gems_regexp, gems_replace)}" }
80
+ puts " Please submit this as an issue at http://github.com/sj26/mailcatcher/issues"
81
+ end
82
+
83
+ @@defaults = {
84
+ :smtp_ip => "127.0.0.1",
85
+ :smtp_port => "1025",
86
+ :http_ip => "127.0.0.1",
87
+ :http_port => "1080",
88
+ :http_path => "/",
89
+ :verbose => false,
90
+ :daemon => !windows?,
91
+ :browse => false,
92
+ :quit => true,
93
+ }
94
+
95
+ def options
96
+ @@options
97
+ end
98
+
99
+ def quittable?
100
+ options[:quit]
101
+ end
102
+
103
+ def parse! arguments=ARGV, defaults=@defaults
104
+ @@defaults.dup.tap do |options|
105
+ OptionParser.new do |parser|
106
+ parser.banner = "Usage: mailcatcher [options]"
107
+ parser.version = VERSION
108
+
109
+ parser.on("--ip IP", "Set the ip address of both servers") do |ip|
110
+ options[:smtp_ip] = options[:http_ip] = ip
111
+ end
112
+
113
+ parser.on("--smtp-ip IP", "Set the ip address of the smtp server") do |ip|
114
+ options[:smtp_ip] = ip
115
+ end
116
+
117
+ parser.on("--smtp-port PORT", Integer, "Set the port of the smtp server") do |port|
118
+ options[:smtp_port] = port
119
+ end
120
+
121
+ parser.on("--http-ip IP", "Set the ip address of the http server") do |ip|
122
+ options[:http_ip] = ip
123
+ end
124
+
125
+ parser.on("--http-port PORT", Integer, "Set the port address of the http server") do |port|
126
+ options[:http_port] = port
127
+ end
128
+
129
+ parser.on("--http-path PATH", String, "Add a prefix to all HTTP paths") do |path|
130
+ clean_path = Rack::Utils.clean_path_info("/#{path}")
131
+
132
+ options[:http_path] = clean_path
133
+ end
134
+
135
+ parser.on("--no-quit", "Don't allow quitting the process") do
136
+ options[:quit] = false
137
+ end
138
+
139
+ if mac?
140
+ parser.on("--[no-]growl") do |growl|
141
+ puts "Growl is no longer supported"
142
+ exit -2
143
+ end
144
+ end
145
+
146
+ unless windows?
147
+ parser.on("-f", "--foreground", "Run in the foreground") do
148
+ options[:daemon] = false
149
+ end
150
+ end
151
+
152
+ if browseable?
153
+ parser.on("-b", "--browse", "Open web browser") do
154
+ options[:browse] = true
155
+ end
156
+ end
157
+
158
+ parser.on("-v", "--verbose", "Be more verbose") do
159
+ options[:verbose] = true
160
+ end
161
+
162
+ parser.on("-h", "--help", "Display this help information") do
163
+ puts parser
164
+ exit
165
+ end
166
+ end.parse!
167
+ end
168
+ end
169
+
170
+ def run! options=nil
171
+ # If we are passed options, fill in the blanks
172
+ options &&= @@defaults.merge options
173
+ # Otherwise, parse them from ARGV
174
+ options ||= parse!
175
+
176
+ # Stash them away for later
177
+ @@options = options
178
+
179
+ # If we're running in the foreground sync the output.
180
+ unless options[:daemon]
181
+ $stdout.sync = $stderr.sync = true
182
+ end
183
+
184
+ puts "Starting MailCatcher"
185
+
186
+ Thin::Logging.debug = development?
187
+ Thin::Logging.silent = !development?
188
+
189
+ # One EventMachine loop...
190
+ EventMachine.run do
191
+ # Set up an SMTP server to run within EventMachine
192
+ rescue_port options[:smtp_port] do
193
+ EventMachine.start_server options[:smtp_ip], options[:smtp_port], Smtp
194
+ puts "==> #{smtp_url}"
195
+ end
196
+
197
+ # Let Thin set itself up inside our EventMachine loop
198
+ # (Skinny/WebSockets just works on the inside)
199
+ rescue_port options[:http_port] do
200
+ Thin::Server.start(options[:http_ip], options[:http_port], Web)
201
+ puts "==> #{http_url}"
202
+ end
203
+
204
+ # Open the web browser before detatching console
205
+ if options[:browse]
206
+ EventMachine.next_tick do
207
+ browse http_url
208
+ end
209
+ end
210
+
211
+ # Daemonize, if we should, but only after the servers have started.
212
+ if options[:daemon]
213
+ EventMachine.next_tick do
214
+ if quittable?
215
+ puts "*** MailCatcher runs as a daemon by default. Go to the web interface to quit."
216
+ else
217
+ puts "*** MailCatcher is now running as a daemon that cannot be quit."
218
+ end
219
+ Process.daemon
220
+ end
221
+ end
222
+ end
223
+ end
224
+
225
+ def quit!
226
+ EventMachine.next_tick { EventMachine.stop_event_loop }
227
+ end
228
+
229
+ protected
230
+
231
+ def smtp_url
232
+ "smtp://#{@@options[:smtp_ip]}:#{@@options[:smtp_port]}"
233
+ end
234
+
235
+ def http_url
236
+ "http://#{@@options[:http_ip]}:#{@@options[:http_port]}#{@@options[:http_path]}"
237
+ end
238
+
239
+ def rescue_port port
240
+ begin
241
+ yield
242
+
243
+ # XXX: EventMachine only spits out RuntimeError with a string description
244
+ rescue RuntimeError
245
+ if $!.to_s =~ /\bno acceptor\b/
246
+ puts "~~> ERROR: Something's using port #{port}. Are you already running MailCatcher?"
247
+ puts "==> #{smtp_url}"
248
+ puts "==> #{http_url}"
249
+ exit -1
250
+ else
251
+ raise
252
+ end
253
+ end
254
+ end
255
+ end
@@ -0,0 +1,3 @@
1
+ require "mail_catcher"
2
+
3
+ Mailcatcher = MailCatcher
Binary file
data/views/404.erb ADDED
@@ -0,0 +1,6 @@
1
+ <html>
2
+ <body>
3
+ <h1>No Dice</h1>
4
+ <p>The message you were looking for does not exist, or doesn't have content of this type.</p>
5
+ </body>
6
+ </html>
data/views/index.erb ADDED
@@ -0,0 +1,63 @@
1
+ <!DOCTYPE html>
2
+ <html class="mailcatcher">
3
+ <head>
4
+ <title>MailCatcher</title>
5
+ <base href="<%= settings.prefix.chomp("/") %>/">
6
+ <link href="favicon.ico" rel="icon">
7
+ <script src="<%= asset_path("mailcatcher.js") %>"></script>
8
+ <link rel="stylesheet" href="<%= asset_path("mailcatcher.css") %>">
9
+ </head>
10
+ <body>
11
+ <header>
12
+ <h1><a href="http://mailcatcher.me" target="_blank">MailCatcher</a></h1>
13
+ <nav class="app">
14
+ <ul>
15
+ <li class="search"><input type="search" name="search" placeholder="Search messages..." incremental="true" /></li>
16
+ <li class="clear"><a href="#" title="Clear all messages">Clear</a></li>
17
+ <% if MailCatcher.quittable? %>
18
+ <li class="quit"><a href="#" title="Quit MailCatcher">Quit</a></li>
19
+ <% end %>
20
+ </ul>
21
+ </nav>
22
+ </header>
23
+ <nav id="messages">
24
+ <table>
25
+ <thead>
26
+ <tr>
27
+ <th>From</th>
28
+ <th>To</th>
29
+ <th>Subject</th>
30
+ <th>Received</th>
31
+ </tr>
32
+ </thead>
33
+ <tbody></tbody>
34
+ </table>
35
+ </nav>
36
+ <div id="resizer"><div class="ruler"></div></div>
37
+ <article id="message">
38
+ <header>
39
+ <dl class="metadata">
40
+ <dt class="created_at">Received</dt>
41
+ <dd class="created_at"></dd>
42
+ <dt class="from">From</dt>
43
+ <dd class="from"></dd>
44
+ <dt class="to">To</dt>
45
+ <dd class="to"></dd>
46
+ <dt class="subject">Subject</dt>
47
+ <dd class="subject"></dd>
48
+ <dt class="attachments">Attachments</dt>
49
+ <dd class="attachments"></dd>
50
+ </dl>
51
+ <nav class="views">
52
+ <ul>
53
+ <li class="format tab html selected" data-message-format="html"><a href="#">HTML</a></li>
54
+ <li class="format tab plain" data-message-format="plain"><a href="#">Plain Text</a></li>
55
+ <li class="format tab source" data-message-format="source"><a href="#">Source</a></li>
56
+ <li class="action download" data-message-format="html"><a href="#" class="button"><span>Download</span></a></li>
57
+ </ul>
58
+ </nav>
59
+ </header>
60
+ <iframe class="body"></iframe>
61
+ </article>
62
+ </body>
63
+ </html>
metadata ADDED
@@ -0,0 +1,332 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mailcatcher_v0.7.1
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.7.1
5
+ platform: ruby
6
+ authors:
7
+ - Samuel Cochran
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-11-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: eventmachine
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '='
18
+ - !ruby/object:Gem::Version
19
+ version: 1.0.9.1
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '='
25
+ - !ruby/object:Gem::Version
26
+ version: 1.0.9.1
27
+ - !ruby/object:Gem::Dependency
28
+ name: mail
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.3'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rack
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.5'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.5'
55
+ - !ruby/object:Gem::Dependency
56
+ name: sinatra
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.2'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.2'
69
+ - !ruby/object:Gem::Dependency
70
+ name: sqlite3
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '='
74
+ - !ruby/object:Gem::Version
75
+ version: 1.3.9
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '='
81
+ - !ruby/object:Gem::Version
82
+ version: 1.3.9
83
+ - !ruby/object:Gem::Dependency
84
+ name: thin
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 1.5.0
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 1.5.0
97
+ - !ruby/object:Gem::Dependency
98
+ name: skinny
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: 0.2.3
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: 0.2.3
111
+ - !ruby/object:Gem::Dependency
112
+ name: coffee-script
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: compass
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: 1.0.3
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: 1.0.3
139
+ - !ruby/object:Gem::Dependency
140
+ name: minitest
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '5.0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '5.0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: rake
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: rdoc
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ - !ruby/object:Gem::Dependency
182
+ name: sass
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ">="
186
+ - !ruby/object:Gem::Version
187
+ version: '0'
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - ">="
193
+ - !ruby/object:Gem::Version
194
+ version: '0'
195
+ - !ruby/object:Gem::Dependency
196
+ name: selenium-webdriver
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - "~>"
200
+ - !ruby/object:Gem::Version
201
+ version: '3.7'
202
+ type: :development
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - "~>"
207
+ - !ruby/object:Gem::Version
208
+ version: '3.7'
209
+ - !ruby/object:Gem::Dependency
210
+ name: sprockets
211
+ requirement: !ruby/object:Gem::Requirement
212
+ requirements:
213
+ - - ">="
214
+ - !ruby/object:Gem::Version
215
+ version: '0'
216
+ type: :development
217
+ prerelease: false
218
+ version_requirements: !ruby/object:Gem::Requirement
219
+ requirements:
220
+ - - ">="
221
+ - !ruby/object:Gem::Version
222
+ version: '0'
223
+ - !ruby/object:Gem::Dependency
224
+ name: sprockets-sass
225
+ requirement: !ruby/object:Gem::Requirement
226
+ requirements:
227
+ - - ">="
228
+ - !ruby/object:Gem::Version
229
+ version: '0'
230
+ type: :development
231
+ prerelease: false
232
+ version_requirements: !ruby/object:Gem::Requirement
233
+ requirements:
234
+ - - ">="
235
+ - !ruby/object:Gem::Version
236
+ version: '0'
237
+ - !ruby/object:Gem::Dependency
238
+ name: sprockets-helpers
239
+ requirement: !ruby/object:Gem::Requirement
240
+ requirements:
241
+ - - ">="
242
+ - !ruby/object:Gem::Version
243
+ version: '0'
244
+ type: :development
245
+ prerelease: false
246
+ version_requirements: !ruby/object:Gem::Requirement
247
+ requirements:
248
+ - - ">="
249
+ - !ruby/object:Gem::Version
250
+ version: '0'
251
+ - !ruby/object:Gem::Dependency
252
+ name: uglifier
253
+ requirement: !ruby/object:Gem::Requirement
254
+ requirements:
255
+ - - ">="
256
+ - !ruby/object:Gem::Version
257
+ version: '0'
258
+ type: :development
259
+ prerelease: false
260
+ version_requirements: !ruby/object:Gem::Requirement
261
+ requirements:
262
+ - - ">="
263
+ - !ruby/object:Gem::Version
264
+ version: '0'
265
+ - !ruby/object:Gem::Dependency
266
+ name: ffi
267
+ requirement: !ruby/object:Gem::Requirement
268
+ requirements:
269
+ - - "<"
270
+ - !ruby/object:Gem::Version
271
+ version: 1.17.0
272
+ type: :development
273
+ prerelease: false
274
+ version_requirements: !ruby/object:Gem::Requirement
275
+ requirements:
276
+ - - "<"
277
+ - !ruby/object:Gem::Version
278
+ version: 1.17.0
279
+ description: |2
280
+ MailCatcher runs a super simple SMTP server which catches any
281
+ message sent to it to display in a web interface. Run
282
+ mailcatcher, set your favourite app to deliver to
283
+ smtp://127.0.0.1:1025 instead of your default SMTP server,
284
+ then check out http://127.0.0.1:1080 to see the mail.
285
+ email: sj26@sj26.com
286
+ executables:
287
+ - mailcatcher
288
+ - catchmail
289
+ extensions: []
290
+ extra_rdoc_files:
291
+ - README.md
292
+ - LICENSE
293
+ files:
294
+ - LICENSE
295
+ - README.md
296
+ - bin/catchmail
297
+ - bin/mailcatcher
298
+ - lib/mail_catcher.rb
299
+ - lib/mail_catcher/events.rb
300
+ - lib/mail_catcher/mail.rb
301
+ - lib/mail_catcher/smtp.rb
302
+ - lib/mail_catcher/version.rb
303
+ - lib/mail_catcher/web.rb
304
+ - lib/mail_catcher/web/application.rb
305
+ - lib/mailcatcher.rb
306
+ - public/favicon.ico
307
+ - views/404.erb
308
+ - views/index.erb
309
+ homepage: http://mailcatcher.me
310
+ licenses:
311
+ - MIT
312
+ metadata: {}
313
+ post_install_message:
314
+ rdoc_options: []
315
+ require_paths:
316
+ - lib
317
+ required_ruby_version: !ruby/object:Gem::Requirement
318
+ requirements:
319
+ - - ">="
320
+ - !ruby/object:Gem::Version
321
+ version: 2.0.0
322
+ required_rubygems_version: !ruby/object:Gem::Requirement
323
+ requirements:
324
+ - - ">="
325
+ - !ruby/object:Gem::Version
326
+ version: '0'
327
+ requirements: []
328
+ rubygems_version: 3.1.2
329
+ signing_key:
330
+ specification_version: 4
331
+ summary: Runs an SMTP server, catches and displays email in a web interface.
332
+ test_files: []