mailcatcher 0.5.12 → 0.8.1
Sign up to get free protection for your applications and to get access to all the features.
- 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)}
|