mailcatcher 0.5.12 → 0.8.1
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.
- checksums.yaml +5 -5
- checksums.yaml.gz.sig +0 -0
- data/README.md +77 -42
- data/bin/catchmail +24 -2
- data/bin/mailcatcher +2 -0
- data/lib/mail_catcher/bus.rb +7 -0
- data/lib/mail_catcher/mail.rb +47 -24
- data/lib/mail_catcher/smtp.rb +23 -12
- data/lib/mail_catcher/version.rb +3 -1
- data/lib/mail_catcher/web/application.rb +179 -0
- data/lib/mail_catcher/web.rb +18 -137
- data/lib/mail_catcher.rb +117 -67
- data/lib/mailcatcher.rb +5 -0
- data/public/assets/logo.png +0 -0
- data/public/assets/logo_2x.png +0 -0
- data/public/assets/logo_large.png +0 -0
- data/public/assets/mailcatcher.css +1 -0
- data/public/assets/mailcatcher.js +5 -0
- data/public/favicon.ico +0 -0
- data/views/404.erb +6 -0
- data/views/index.erb +70 -0
- data.tar.gz.sig +0 -0
- metadata +174 -88
- metadata.gz.sig +0 -0
- data/lib/mail_catcher/events.rb +0 -5
- data/lib/mail_catcher/growl.rb +0 -16
- data/public/images/logo.png +0 -0
- data/public/images/logo_large.png +0 -0
- data/public/javascripts/application.js +0 -408
- data/public/javascripts/date.js +0 -104
- data/public/javascripts/flexie.min.js +0 -36
- data/public/javascripts/jquery.js +0 -6883
- data/public/javascripts/keymaster.min.js +0 -4
- data/public/javascripts/modernizr.js +0 -3
- data/public/javascripts/xslt-3.2.js +0 -1
- data/public/stylesheets/analysis.xsl +0 -33
- data/public/stylesheets/application.css +0 -375
- data/views/index.haml +0 -63
@@ -0,0 +1,179 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pathname"
|
4
|
+
require "net/http"
|
5
|
+
require "uri"
|
6
|
+
|
7
|
+
require "sinatra"
|
8
|
+
require "skinny"
|
9
|
+
|
10
|
+
require "mail_catcher/bus"
|
11
|
+
require "mail_catcher/mail"
|
12
|
+
|
13
|
+
class Sinatra::Request
|
14
|
+
include Skinny::Helpers
|
15
|
+
end
|
16
|
+
|
17
|
+
module MailCatcher
|
18
|
+
module Web
|
19
|
+
class Application < Sinatra::Base
|
20
|
+
set :environment, MailCatcher.env
|
21
|
+
set :prefix, MailCatcher.options[:http_path]
|
22
|
+
set :asset_prefix, File.join(prefix, "assets")
|
23
|
+
set :root, File.expand_path("#{__FILE__}/../../../..")
|
24
|
+
|
25
|
+
if development?
|
26
|
+
require "sprockets-helpers"
|
27
|
+
|
28
|
+
configure do
|
29
|
+
require "mail_catcher/web/assets"
|
30
|
+
Sprockets::Helpers.configure do |config|
|
31
|
+
config.environment = Assets
|
32
|
+
config.prefix = settings.asset_prefix
|
33
|
+
config.digest = false
|
34
|
+
config.public_path = public_folder
|
35
|
+
config.debug = true
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
helpers do
|
40
|
+
include Sprockets::Helpers
|
41
|
+
end
|
42
|
+
else
|
43
|
+
helpers do
|
44
|
+
def asset_path(filename)
|
45
|
+
File.join(settings.asset_prefix, filename)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
get "/" do
|
51
|
+
erb :index
|
52
|
+
end
|
53
|
+
|
54
|
+
delete "/" do
|
55
|
+
if MailCatcher.quittable?
|
56
|
+
MailCatcher.quit!
|
57
|
+
status 204
|
58
|
+
else
|
59
|
+
status 403
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
get "/messages" do
|
64
|
+
if request.websocket?
|
65
|
+
request.websocket!(
|
66
|
+
:on_start => proc do |websocket|
|
67
|
+
bus_subscription = MailCatcher::Bus.subscribe do |message|
|
68
|
+
begin
|
69
|
+
websocket.send_message(JSON.generate(message))
|
70
|
+
rescue => exception
|
71
|
+
MailCatcher.log_exception("Error sending message through websocket", message, exception)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
websocket.on_close do |*|
|
76
|
+
MailCatcher::Bus.unsubscribe bus_subscription
|
77
|
+
end
|
78
|
+
end)
|
79
|
+
else
|
80
|
+
content_type :json
|
81
|
+
JSON.generate(Mail.messages)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
delete "/messages" do
|
86
|
+
Mail.delete!
|
87
|
+
status 204
|
88
|
+
end
|
89
|
+
|
90
|
+
get "/messages/:id.json" do
|
91
|
+
id = params[:id].to_i
|
92
|
+
if message = Mail.message(id)
|
93
|
+
content_type :json
|
94
|
+
JSON.generate(message.merge({
|
95
|
+
"formats" => [
|
96
|
+
"source",
|
97
|
+
("html" if Mail.message_has_html? id),
|
98
|
+
("plain" if Mail.message_has_plain? id)
|
99
|
+
].compact,
|
100
|
+
"attachments" => Mail.message_attachments(id),
|
101
|
+
}))
|
102
|
+
else
|
103
|
+
not_found
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
get "/messages/:id.html" do
|
108
|
+
id = params[:id].to_i
|
109
|
+
if part = Mail.message_part_html(id)
|
110
|
+
content_type :html, :charset => (part["charset"] || "utf8")
|
111
|
+
|
112
|
+
body = part["body"]
|
113
|
+
|
114
|
+
# Rewrite body to link to embedded attachments served by cid
|
115
|
+
body.gsub! /cid:([^'"> ]+)/, "#{id}/parts/\\1"
|
116
|
+
|
117
|
+
body
|
118
|
+
else
|
119
|
+
not_found
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
get "/messages/:id.plain" do
|
124
|
+
id = params[:id].to_i
|
125
|
+
if part = Mail.message_part_plain(id)
|
126
|
+
content_type part["type"], :charset => (part["charset"] || "utf8")
|
127
|
+
part["body"]
|
128
|
+
else
|
129
|
+
not_found
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
get "/messages/:id.source" do
|
134
|
+
id = params[:id].to_i
|
135
|
+
if message_source = Mail.message_source(id)
|
136
|
+
content_type "text/plain"
|
137
|
+
message_source
|
138
|
+
else
|
139
|
+
not_found
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
get "/messages/:id.eml" do
|
144
|
+
id = params[:id].to_i
|
145
|
+
if message_source = Mail.message_source(id)
|
146
|
+
content_type "message/rfc822"
|
147
|
+
message_source
|
148
|
+
else
|
149
|
+
not_found
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
get "/messages/:id/parts/:cid" do
|
154
|
+
id = params[:id].to_i
|
155
|
+
if part = Mail.message_part_cid(id, params[:cid])
|
156
|
+
content_type part["type"], :charset => (part["charset"] || "utf8")
|
157
|
+
attachment part["filename"] if part["is_attachment"] == 1
|
158
|
+
body part["body"].to_s
|
159
|
+
else
|
160
|
+
not_found
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
delete "/messages/:id" do
|
165
|
+
id = params[:id].to_i
|
166
|
+
if Mail.message(id)
|
167
|
+
Mail.delete_message!(id)
|
168
|
+
status 204
|
169
|
+
else
|
170
|
+
not_found
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
not_found do
|
175
|
+
erb :"404"
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
data/lib/mail_catcher/web.rb
CHANGED
@@ -1,148 +1,29 @@
|
|
1
|
-
|
2
|
-
require 'pathname'
|
3
|
-
require 'net/http'
|
4
|
-
require 'uri'
|
1
|
+
# frozen_string_literal: true
|
5
2
|
|
6
|
-
require
|
3
|
+
require "rack/builder"
|
7
4
|
|
8
|
-
|
9
|
-
include Skinny::Helpers
|
10
|
-
end
|
11
|
-
|
12
|
-
class MailCatcher::Web < Sinatra::Base
|
13
|
-
set :root, File.expand_path("#{__FILE__}/../../..")
|
14
|
-
set :haml, :format => :html5
|
5
|
+
require "mail_catcher/web/application"
|
15
6
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
get '/messages' do
|
26
|
-
if request.websocket?
|
27
|
-
request.websocket!(
|
28
|
-
:on_start => proc do |websocket|
|
29
|
-
subscription = MailCatcher::Events::MessageAdded.subscribe { |message| websocket.send_message message.to_json }
|
30
|
-
websocket.on_close do |websocket|
|
31
|
-
MailCatcher::Events::MessageAdded.unsubscribe subscription
|
7
|
+
module MailCatcher
|
8
|
+
module Web extend self
|
9
|
+
def app
|
10
|
+
@@app ||= Rack::Builder.new do
|
11
|
+
map(MailCatcher.options[:http_path]) do
|
12
|
+
if MailCatcher.development?
|
13
|
+
require "mail_catcher/web/assets"
|
14
|
+
map("/assets") { run Assets }
|
32
15
|
end
|
33
|
-
end)
|
34
|
-
else
|
35
|
-
MailCatcher::Mail.messages.to_json
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
delete '/messages' do
|
40
|
-
MailCatcher::Mail.delete!
|
41
|
-
status 204
|
42
|
-
end
|
43
|
-
|
44
|
-
get '/messages/:id.json' do
|
45
|
-
id = params[:id].to_i
|
46
|
-
if message = MailCatcher::Mail.message(id)
|
47
|
-
message.merge({
|
48
|
-
"formats" => [
|
49
|
-
"source",
|
50
|
-
("html" if MailCatcher::Mail.message_has_html? id),
|
51
|
-
("plain" if MailCatcher::Mail.message_has_plain? id),
|
52
|
-
].compact,
|
53
|
-
"attachments" => MailCatcher::Mail.message_attachments(id).map do |attachment|
|
54
|
-
attachment.merge({"href" => "/messages/#{escape(id)}/parts/#{escape(attachment['cid'])}"})
|
55
|
-
end,
|
56
|
-
}).to_json
|
57
|
-
else
|
58
|
-
not_found
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
get '/messages/:id.html' do
|
63
|
-
id = params[:id].to_i
|
64
|
-
if part = MailCatcher::Mail.message_part_html(id)
|
65
|
-
content_type part["type"], :charset => (part["charset"] || "utf8")
|
66
|
-
|
67
|
-
body = part["body"]
|
68
|
-
|
69
|
-
# Rewrite body to link to embedded attachments served by cid
|
70
|
-
body.gsub! /cid:([^'"> ]+)/, "#{id}/parts/\\1"
|
71
|
-
|
72
|
-
# Rewrite body to open links in a new window
|
73
|
-
body.gsub! /<a\s+/, '<a target="_blank" '
|
74
16
|
|
75
|
-
|
76
|
-
|
77
|
-
not_found
|
78
|
-
end
|
79
|
-
end
|
80
|
-
|
81
|
-
get "/messages/:id.plain" do
|
82
|
-
id = params[:id].to_i
|
83
|
-
if part = MailCatcher::Mail.message_part_plain(id)
|
84
|
-
content_type part["type"], :charset => (part["charset"] || "utf8")
|
85
|
-
part["body"]
|
86
|
-
else
|
87
|
-
not_found
|
88
|
-
end
|
89
|
-
end
|
17
|
+
run Application
|
18
|
+
end
|
90
19
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
content_type "text/plain"
|
95
|
-
message["source"]
|
96
|
-
else
|
97
|
-
not_found
|
20
|
+
# This should only affect when http_path is anything but "/" above
|
21
|
+
run lambda { |env| [302, {"Location" => MailCatcher.options[:http_path]}, []] }
|
22
|
+
end
|
98
23
|
end
|
99
|
-
end
|
100
|
-
|
101
|
-
get "/messages/:id.eml" do
|
102
|
-
id = params[:id].to_i
|
103
|
-
if message = MailCatcher::Mail.message(id)
|
104
|
-
content_type "message/rfc822"
|
105
|
-
message["source"]
|
106
|
-
else
|
107
|
-
not_found
|
108
|
-
end
|
109
|
-
end
|
110
24
|
|
111
|
-
|
112
|
-
|
113
|
-
if part = MailCatcher::Mail.message_part_cid(id, params[:cid])
|
114
|
-
content_type part["type"], :charset => (part["charset"] || "utf8")
|
115
|
-
attachment part["filename"] if part["is_attachment"] == 1
|
116
|
-
body part["body"].to_s
|
117
|
-
else
|
118
|
-
not_found
|
25
|
+
def call(env)
|
26
|
+
app.call(env)
|
119
27
|
end
|
120
28
|
end
|
121
|
-
|
122
|
-
get "/messages/:id/analysis.?:format?" do
|
123
|
-
id = params[:id].to_i
|
124
|
-
if part = MailCatcher::Mail.message_part_html(id)
|
125
|
-
# TODO: Server-side cache? Make the browser cache based on message create time? Hmm.
|
126
|
-
uri = URI.parse("http://api.getfractal.com/api/v2/validate#{"/format/#{params[:format]}" if params[:format].present?}")
|
127
|
-
response = Net::HTTP.post_form(uri, :api_key => "5c463877265251386f516f7428", :html => part["body"])
|
128
|
-
content_type ".#{params[:format]}" if params[:format].present?
|
129
|
-
body response.body
|
130
|
-
else
|
131
|
-
not_found
|
132
|
-
end
|
133
|
-
end
|
134
|
-
|
135
|
-
delete '/messages/:id' do
|
136
|
-
id = params[:id].to_i
|
137
|
-
if message = MailCatcher::Mail.message(id)
|
138
|
-
MailCatcher::Mail.delete_message!(id)
|
139
|
-
status 204
|
140
|
-
else
|
141
|
-
not_found
|
142
|
-
end
|
143
|
-
end
|
144
|
-
|
145
|
-
not_found do
|
146
|
-
"<html><body><h1>No Dice</h1><p>The message you were looking for does not exist, or doesn't have content of this type.</p></body></html>"
|
147
|
-
end
|
148
29
|
end
|
data/lib/mail_catcher.rb
CHANGED
@@ -1,67 +1,98 @@
|
|
1
|
-
|
2
|
-
require 'eventmachine'
|
3
|
-
require 'open3'
|
4
|
-
require 'optparse'
|
5
|
-
require 'rbconfig'
|
6
|
-
require 'thin'
|
1
|
+
# frozen_string_literal: true
|
7
2
|
|
8
|
-
require
|
3
|
+
require "open3"
|
4
|
+
require "optparse"
|
5
|
+
require "rbconfig"
|
9
6
|
|
10
|
-
|
11
|
-
|
12
|
-
not windows? and Open3.popen3 'which', 'command' do |stdin, stdout, stderr|
|
13
|
-
return stdout.read.chomp.presence
|
14
|
-
end
|
15
|
-
end
|
7
|
+
require "eventmachine"
|
8
|
+
require "thin"
|
16
9
|
|
17
|
-
|
18
|
-
|
10
|
+
module EventMachine
|
11
|
+
# Monkey patch fix for 10deb4
|
12
|
+
# See https://github.com/eventmachine/eventmachine/issues/569
|
13
|
+
def self.reactor_running?
|
14
|
+
(@reactor_running || false)
|
19
15
|
end
|
16
|
+
end
|
20
17
|
|
21
|
-
|
22
|
-
|
18
|
+
require "mail_catcher/version"
|
19
|
+
|
20
|
+
module MailCatcher extend self
|
21
|
+
autoload :Bus, "mail_catcher/bus"
|
22
|
+
autoload :Mail, "mail_catcher/mail"
|
23
|
+
autoload :Smtp, "mail_catcher/smtp"
|
24
|
+
autoload :Web, "mail_catcher/web"
|
25
|
+
|
26
|
+
def env
|
27
|
+
ENV.fetch("MAILCATCHER_ENV", "production")
|
23
28
|
end
|
24
29
|
|
25
|
-
def
|
26
|
-
|
30
|
+
def development?
|
31
|
+
env == "development"
|
27
32
|
end
|
28
33
|
|
29
|
-
def
|
30
|
-
|
34
|
+
def which?(command)
|
35
|
+
ENV["PATH"].split(File::PATH_SEPARATOR).any? do |directory|
|
36
|
+
File.executable?(File.join(directory, command.to_s))
|
37
|
+
end
|
31
38
|
end
|
32
39
|
|
33
|
-
def
|
34
|
-
|
40
|
+
def windows?
|
41
|
+
RbConfig::CONFIG["host_os"] =~ /mswin|mingw/
|
35
42
|
end
|
36
43
|
|
37
|
-
def
|
38
|
-
windows? or which "open"
|
44
|
+
def browseable?
|
45
|
+
windows? or which? "open"
|
39
46
|
end
|
40
47
|
|
41
48
|
def browse url
|
42
49
|
if windows?
|
43
50
|
system "start", "/b", url
|
44
|
-
elsif which "open"
|
51
|
+
elsif which? "open"
|
45
52
|
system "open", url
|
46
53
|
end
|
47
54
|
end
|
48
55
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
56
|
+
def log_exception(message, context, exception)
|
57
|
+
gems_paths = (Gem.path | [Gem.default_dir]).map { |path| Regexp.escape(path) }
|
58
|
+
gems_regexp = %r{(?:#{gems_paths.join("|")})/gems/([^/]+)-([\w.]+)/(.*)}
|
59
|
+
gems_replace = '\1 (\2) \3'
|
60
|
+
|
61
|
+
puts "*** #{message}: #{context.inspect}"
|
62
|
+
puts " Exception: #{exception}"
|
63
|
+
puts " Backtrace:", *exception.backtrace.map { |line| " #{line.sub(gems_regexp, gems_replace)}" }
|
64
|
+
puts " Please submit this as an issue at https://github.com/sj26/mailcatcher/issues"
|
65
|
+
end
|
66
|
+
|
67
|
+
@@defaults = {
|
68
|
+
:smtp_ip => "127.0.0.1",
|
69
|
+
:smtp_port => "1025",
|
70
|
+
:http_ip => "127.0.0.1",
|
71
|
+
:http_port => "1080",
|
72
|
+
:http_path => "/",
|
73
|
+
:messages_limit => nil,
|
54
74
|
:verbose => false,
|
55
75
|
:daemon => !windows?,
|
56
|
-
:growl => growlnotify?,
|
57
76
|
:browse => false,
|
77
|
+
:quit => true,
|
58
78
|
}
|
59
79
|
|
80
|
+
def options
|
81
|
+
@@options
|
82
|
+
end
|
83
|
+
|
84
|
+
def quittable?
|
85
|
+
options[:quit]
|
86
|
+
end
|
87
|
+
|
60
88
|
def parse! arguments=ARGV, defaults=@defaults
|
61
|
-
|
89
|
+
@@defaults.dup.tap do |options|
|
62
90
|
OptionParser.new do |parser|
|
63
91
|
parser.banner = "Usage: mailcatcher [options]"
|
64
92
|
parser.version = VERSION
|
93
|
+
parser.separator ""
|
94
|
+
parser.separator "MailCatcher v#{VERSION}"
|
95
|
+
parser.separator ""
|
65
96
|
|
66
97
|
parser.on("--ip IP", "Set the ip address of both servers") do |ip|
|
67
98
|
options[:smtp_ip] = options[:http_ip] = ip
|
@@ -83,38 +114,44 @@ module MailCatcher extend self
|
|
83
114
|
options[:http_port] = port
|
84
115
|
end
|
85
116
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
puts "You'll need to install growlnotify from the Growl installer."
|
90
|
-
puts
|
91
|
-
puts "See: http://growl.info/extras.php#growlnotify"
|
92
|
-
exit!
|
93
|
-
end
|
117
|
+
parser.on("--messages-limit COUNT", Integer, "Only keep up to COUNT most recent messages") do |count|
|
118
|
+
options[:messages_limit] = count
|
119
|
+
end
|
94
120
|
|
95
|
-
|
96
|
-
|
121
|
+
parser.on("--http-path PATH", String, "Add a prefix to all HTTP paths") do |path|
|
122
|
+
clean_path = Rack::Utils.clean_path_info("/#{path}")
|
123
|
+
|
124
|
+
options[:http_path] = clean_path
|
125
|
+
end
|
126
|
+
|
127
|
+
parser.on("--no-quit", "Don't allow quitting the process") do
|
128
|
+
options[:quit] = false
|
97
129
|
end
|
98
130
|
|
99
131
|
unless windows?
|
100
|
-
parser.on(
|
132
|
+
parser.on("-f", "--foreground", "Run in the foreground") do
|
101
133
|
options[:daemon] = false
|
102
134
|
end
|
103
135
|
end
|
104
136
|
|
105
|
-
if
|
106
|
-
parser.on(
|
137
|
+
if browseable?
|
138
|
+
parser.on("-b", "--browse", "Open web browser") do
|
107
139
|
options[:browse] = true
|
108
140
|
end
|
109
141
|
end
|
110
142
|
|
111
|
-
parser.on(
|
143
|
+
parser.on("-v", "--verbose", "Be more verbose") do
|
112
144
|
options[:verbose] = true
|
113
145
|
end
|
114
146
|
|
115
|
-
parser.
|
147
|
+
parser.on_tail("-h", "--help", "Display this help information") do
|
116
148
|
puts parser
|
117
|
-
exit
|
149
|
+
exit
|
150
|
+
end
|
151
|
+
|
152
|
+
parser.on_tail("--version", "Display the current version") do
|
153
|
+
puts "MailCatcher v#{VERSION}"
|
154
|
+
exit
|
118
155
|
end
|
119
156
|
end.parse!
|
120
157
|
end
|
@@ -122,22 +159,25 @@ module MailCatcher extend self
|
|
122
159
|
|
123
160
|
def run! options=nil
|
124
161
|
# If we are passed options, fill in the blanks
|
125
|
-
options &&=
|
162
|
+
options &&= @@defaults.merge options
|
126
163
|
# Otherwise, parse them from ARGV
|
127
164
|
options ||= parse!
|
128
165
|
|
129
|
-
|
166
|
+
# Stash them away for later
|
167
|
+
@@options = options
|
130
168
|
|
131
|
-
|
169
|
+
# If we're running in the foreground sync the output.
|
170
|
+
unless options[:daemon]
|
171
|
+
$stdout.sync = $stderr.sync = true
|
172
|
+
end
|
132
173
|
|
133
|
-
|
134
|
-
EventMachine.run do
|
135
|
-
# Get our lion on if asked
|
136
|
-
MailCatcher::Growl.start if options[:growl]
|
174
|
+
puts "Starting MailCatcher v#{VERSION}"
|
137
175
|
|
138
|
-
|
139
|
-
|
176
|
+
Thin::Logging.debug = development?
|
177
|
+
Thin::Logging.silent = !development?
|
140
178
|
|
179
|
+
# One EventMachine loop...
|
180
|
+
EventMachine.run do
|
141
181
|
# Set up an SMTP server to run within EventMachine
|
142
182
|
rescue_port options[:smtp_port] do
|
143
183
|
EventMachine.start_server options[:smtp_ip], options[:smtp_port], Smtp
|
@@ -147,7 +187,7 @@ module MailCatcher extend self
|
|
147
187
|
# Let Thin set itself up inside our EventMachine loop
|
148
188
|
# (Skinny/WebSockets just works on the inside)
|
149
189
|
rescue_port options[:http_port] do
|
150
|
-
Thin::Server.start
|
190
|
+
Thin::Server.start(options[:http_ip], options[:http_port], Web)
|
151
191
|
puts "==> #{http_url}"
|
152
192
|
end
|
153
193
|
|
@@ -161,7 +201,11 @@ module MailCatcher extend self
|
|
161
201
|
# Daemonize, if we should, but only after the servers have started.
|
162
202
|
if options[:daemon]
|
163
203
|
EventMachine.next_tick do
|
164
|
-
|
204
|
+
if quittable?
|
205
|
+
puts "*** MailCatcher runs as a daemon by default. Go to the web interface to quit."
|
206
|
+
else
|
207
|
+
puts "*** MailCatcher is now running as a daemon that cannot be quit."
|
208
|
+
end
|
165
209
|
Process.daemon
|
166
210
|
end
|
167
211
|
end
|
@@ -169,11 +213,21 @@ module MailCatcher extend self
|
|
169
213
|
end
|
170
214
|
|
171
215
|
def quit!
|
216
|
+
MailCatcher::Bus.push(type: "quit")
|
217
|
+
|
172
218
|
EventMachine.next_tick { EventMachine.stop_event_loop }
|
173
219
|
end
|
174
220
|
|
175
221
|
protected
|
176
222
|
|
223
|
+
def smtp_url
|
224
|
+
"smtp://#{@@options[:smtp_ip]}:#{@@options[:smtp_port]}"
|
225
|
+
end
|
226
|
+
|
227
|
+
def http_url
|
228
|
+
"http://#{@@options[:http_ip]}:#{@@options[:http_port]}#{@@options[:http_path]}".chomp("/")
|
229
|
+
end
|
230
|
+
|
177
231
|
def rescue_port port
|
178
232
|
begin
|
179
233
|
yield
|
@@ -182,16 +236,12 @@ protected
|
|
182
236
|
rescue RuntimeError
|
183
237
|
if $!.to_s =~ /\bno acceptor\b/
|
184
238
|
puts "~~> ERROR: Something's using port #{port}. Are you already running MailCatcher?"
|
185
|
-
|
239
|
+
puts "==> #{smtp_url}"
|
240
|
+
puts "==> #{http_url}"
|
241
|
+
exit -1
|
186
242
|
else
|
187
243
|
raise
|
188
244
|
end
|
189
245
|
end
|
190
246
|
end
|
191
247
|
end
|
192
|
-
|
193
|
-
require 'mail_catcher/events'
|
194
|
-
require 'mail_catcher/growl'
|
195
|
-
require 'mail_catcher/mail'
|
196
|
-
require 'mail_catcher/smtp'
|
197
|
-
require 'mail_catcher/web'
|
data/lib/mailcatcher.rb
ADDED
Binary file
|
Binary file
|
Binary file
|
@@ -0,0 +1 @@
|
|
1
|
+
html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font:inherit;font-size:100%;vertical-align:baseline}html{line-height:1}ol,ul{list-style:none}table{border-collapse:collapse;border-spacing:0}caption,th,td{text-align:left;font-weight:normal;vertical-align:middle}q,blockquote{quotes:none}q:before,q:after,blockquote:before,blockquote:after{content:"";content:none}a img{border:none}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}html,body{width:100%;height:100%}body{display:-moz-box;display:-webkit-box;display:box;-moz-box-orient:vertical;-webkit-box-orient:vertical;box-orient:vertical;background:#eee;color:#000;font-size:12px;font-family:Helvetica, sans-serif}body html{font-size:75%;line-height:1.5em}body.iframe{background:#fff}body.iframe h1{font-size:1.3em;margin:12px}body.iframe p,body.iframe form{margin:0 12px 12px 12px;line-height:1.25}body.iframe .loading{color:#666;margin-left:0.5em}.button{padding:0.5em 1em;border:1px solid #ccc;-moz-border-radius:2px;-webkit-border-radius:2px;border-radius:2px;background:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2Y0ZjRmNCIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2VjZWNlYyIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=="),#ececec;background:-webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #f4f4f4), color-stop(100%, #ececec)),#ececec;background:-moz-linear-gradient(#f4f4f4, #ececec),#ececec;background:-webkit-linear-gradient(#f4f4f4, #ececec),#ececec;background:linear-gradient(#f4f4f4, #ececec),#ececec;color:#666;text-shadow:1px 1px 0 #fff;text-decoration:none}.button:hover,.button:focus{border-color:#999;border-bottom-color:#666;background:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2VlZWVlZSIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2RkZGRkZCIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=="),#ddd;background:-webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #eee), color-stop(100%, #ddd)),#ddd;background:-moz-linear-gradient(#eee, #ddd),#ddd;background:-webkit-linear-gradient(#eee, #ddd),#ddd;background:linear-gradient(#eee, #ddd),#ddd;color:#333;text-decoration:none}.button:active,.button.active{border-color:#666;border-bottom-color:#999;background:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2RkZGRkZCIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2VlZWVlZSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=="),#eee;background:-webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ddd), color-stop(100%, #eee)),#eee;background:-moz-linear-gradient(#ddd, #eee),#eee;background:-webkit-linear-gradient(#ddd, #eee),#eee;background:linear-gradient(#ddd, #eee),#eee;color:#333;text-decoration:none;text-shadow:-1px -1px 0 #eee}body>header{overflow:hidden;*zoom:1;border-bottom:1px solid #ccc}body>header h1{float:left;margin-left:6px;padding:6px;padding-left:30px;background:url(logo.png) left no-repeat;background-size:24px 24px;font-size:18px;font-weight:bold}body>header h1 a{color:black;text-decoration:none;text-shadow:0 1px 0 white;-moz-transition:ease 0.1s;-o-transition:ease 0.1s;-webkit-transition:ease 0.1s;transition:ease 0.1s}body>header h1 a:hover{color:#4183C4}body>header nav{border-left:1px solid #ccc}body>header nav.project{float:left}body>header nav.app{float:right}body>header nav li{display:block;float:left;border-left:1px solid #fff;border-right:1px solid #ccc}body>header nav li input{margin:6px}body>header nav li a{display:block;padding:10px;text-decoration:none;text-shadow:0 1px 0 white;background:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2Y0ZjRmNCIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2VjZWNlYyIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=="),#ececec;background:-webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #f4f4f4), color-stop(100%, #ececec)),#ececec;background:-moz-linear-gradient(#f4f4f4, #ececec),#ececec;background:-webkit-linear-gradient(#f4f4f4, #ececec),#ececec;background:linear-gradient(#f4f4f4, #ececec),#ececec;color:#666;text-shadow:1px 1px 0 #fff;text-decoration:none}body>header nav li a:hover,body>header nav li a:focus{background:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2VlZWVlZSIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2RkZGRkZCIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=="),#ddd;background:-webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #eee), color-stop(100%, #ddd)),#ddd;background:-moz-linear-gradient(#eee, #ddd),#ddd;background:-webkit-linear-gradient(#eee, #ddd),#ddd;background:linear-gradient(#eee, #ddd),#ddd;color:#333;text-decoration:none}body>header nav li a:active,body>header nav li a.active{background:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2RkZGRkZCIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2VlZWVlZSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=="),#eee;background:-webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ddd), color-stop(100%, #eee)),#eee;background:-moz-linear-gradient(#ddd, #eee),#eee;background:-webkit-linear-gradient(#ddd, #eee),#eee;background:linear-gradient(#ddd, #eee),#eee;color:#333;text-decoration:none;text-shadow:-1px -1px 0 #eee}#messages{width:100%;height:10em;min-height:3em;overflow:auto;background:#fff;border-top:1px solid #fff}#messages table{overflow:hidden;*zoom:1;width:100%}#messages table thead tr{background:#eee;color:#333}#messages table thead tr th{padding:0.25em;font-weight:bold;color:#666;text-shadow:0 1px 0 white}#messages table tbody tr{cursor:pointer;-moz-transition:ease 0.1s;-o-transition:ease 0.1s;-webkit-transition:ease 0.1s;transition:ease 0.1s;color:#333}#messages table tbody tr:hover{color:#000}#messages table tbody tr:nth-child(even){background:#f0f0f0}#messages table tbody tr.selected{background:Highlight;color:HighlightText}#messages table tbody tr td{padding:0.25em}#messages table tbody tr td.blank{color:#666;font-style:italic}#resizer{padding-bottom:5px;cursor:ns-resize}#resizer .ruler{border-top:1px solid #ccc;border-bottom:1px solid #fff}#message{display:-moz-box;display:-webkit-box;display:box;-moz-box-orient:vertical;-webkit-box-orient:vertical;box-orient:vertical;-moz-box-flex:1;-webkit-box-flex:1;box-flex:1}#message>header{overflow:hidden;*zoom:1}#message>header .metadata{overflow:hidden;*zoom:1;padding:0.5em;padding-top:0}#message>header .metadata dt,#message>header .metadata dd{padding:0.25em}#message>header .metadata dt{float:left;clear:left;width:8em;margin-right:0.5em;text-align:right;font-weight:bold;color:#666;text-shadow:0 1px 0 white}#message>header .metadata dd.subject{font-weight:bold}#message>header .metadata .attachments{display:none}#message>header .metadata .attachments ul{display:inline}#message>header .metadata .attachments ul li{display:inline-block;vertical-align:middle;*vertical-align:auto;*zoom:1;*display:inline;margin-right:0.5em}#message>header .views ul{padding:0 0.5em;border-bottom:1px solid #ccc}#message>header .views .tab{display:inline-block;vertical-align:middle;*vertical-align:auto;*zoom:1;*display:inline}#message>header .views .tab a{display:inline-block;vertical-align:middle;*vertical-align:auto;*zoom:1;*display:inline;padding:0.5em;border:1px solid #ccc;background:#ddd;color:#333;border-width:1px 1px 0 1px;cursor:pointer;text-shadow:0 1px 0 #eeeeee;text-decoration:none}#message>header .views .tab:not(.selected):hover a{background-color:#eee}#message>header .views .tab.selected a{background:#fff;color:#000;height:13px;-moz-box-shadow:1px 1px 0 #ccc;-webkit-box-shadow:1px 1px 0 #ccc;box-shadow:1px 1px 0 #ccc;margin-bottom:-2px;cursor:default}#message>header .views .action{display:inline-block;vertical-align:middle;*vertical-align:auto;*zoom:1;*display:inline;float:right;margin:0 0.25em}.fractal-analysis{margin:12px 0}.fractal-analysis .report-intro{font-weight:bold}.fractal-analysis .report-intro.valid{color:#090}.fractal-analysis .report-intro.invalid{color:#c33}.fractal-analysis code{font-family:Monaco, "Courier New", Courier, monospace;background-color:#f8f8ff;color:#444;padding:0 0.2em;border:1px solid #dedede}.fractal-analysis ul{margin:1em 0 1em 1em;list-style-type:square}.fractal-analysis ol{margin:1em 0 1em 2em;list-style-type:decimal}.fractal-analysis ul li,.fractal-analysis ol li{display:list-item;margin:0.5em 0 0.5em 1em}.fractal-analysis .error-intro strong{font-weight:bold}.fractal-analysis .unsupported-clients dt{padding-left:1em}.fractal-analysis .unsupported-clients dd{padding-left:2em}.fractal-analysis .unsupported-clients dd ul li{display:list-item}iframe{display:-moz-box;display:-webkit-box;display:box;-moz-box-flex:1;-webkit-box-flex:1;box-flex:1;background:#fff}@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi){body>header h1{background-image:url(logo_2x.png)}}#noscript-overlay{position:absolute;top:0;left:0;right:0;bottom:0;display:flex;align-items:center;justify-content:center;background-color:rgba(0,0,0,0.5);cursor:default;outline:none}#noscript{display:block;max-width:100%;margin:2rem;padding:2rem;border-radius:0.5rem;background-color:#fff;box-shadow:0 0 1rem 0 rgba(0,0,0,0.4)}
|