Switch to sprockets for assets

This commit is contained in:
Samuel Cochran 2014-03-17 16:53:13 +11:00
parent 272b4fa855
commit 0398d2d6a3
22 changed files with 299 additions and 1050 deletions

11
.gitignore vendored
View File

@ -1,5 +1,10 @@
rdoc
*.gem
# Caches
.bundle
.sass-cache
# Generated documentation and assets
/doc
/public/assets
# Build gems
*.gem

View File

@ -1,10 +1,10 @@
PATH
remote: .
specs:
mailcatcher (0.5.11)
activesupport (~> 3.0)
mailcatcher (0.6.0)
activesupport (>= 3.0.0, < 4.1)
eventmachine (~> 1.0.0)
haml (>= 3.1, < 5)
haml (>= 3.1, < 4.1)
mail (~> 2.3)
sinatra (~> 1.2)
skinny (~> 0.2.3)
@ -14,55 +14,72 @@ PATH
GEM
remote: https://rubygems.org/
specs:
activesupport (3.2.12)
i18n (~> 0.6)
multi_json (~> 1.0)
chunky_png (1.2.7)
activesupport (4.0.4)
i18n (~> 0.6, >= 0.6.9)
minitest (~> 4.2)
multi_json (~> 1.3)
thread_safe (~> 0.1)
tzinfo (~> 0.3.37)
atomic (1.1.15)
chunky_png (1.3.0)
coffee-script (2.2.0)
coffee-script-source
execjs
coffee-script-source (1.6.1)
coffee-script-source (1.7.0)
compass (0.12.2)
chunky_png (~> 1.2)
fssm (>= 0.2.7)
sass (~> 3.1)
daemons (1.1.9)
eventmachine (1.0.3)
execjs (1.4.0)
multi_json (~> 1.0)
execjs (2.0.2)
fssm (0.2.10)
haml (4.0.0)
haml (4.0.5)
tilt
i18n (0.6.4)
mail (2.5.3)
i18n (>= 0.4.0)
hike (1.2.3)
i18n (0.6.9)
json (1.8.1)
mail (2.5.4)
mime-types (~> 1.16)
treetop (~> 1.4.8)
mime-types (1.21)
multi_json (1.6.1)
polyglot (0.3.3)
mime-types (1.25.1)
minitest (4.7.5)
multi_json (1.9.0)
polyglot (0.3.4)
rack (1.5.2)
rack-protection (1.4.0)
rack-protection (1.5.2)
rack
rake (10.0.3)
rdoc (4.0.0)
sass (3.2.7)
sinatra (1.3.5)
rake (10.1.1)
rdoc (4.1.1)
json (~> 1.4)
sass (3.2.14)
sinatra (1.4.4)
rack (~> 1.4)
rack-protection (~> 1.3)
tilt (~> 1.3, >= 1.3.3)
rack-protection (~> 1.4)
tilt (~> 1.3, >= 1.3.4)
skinny (0.2.3)
eventmachine (~> 1.0.0)
thin (~> 1.5.0)
sqlite3 (1.3.7)
thin (1.5.0)
sprockets (2.12.0)
hike (~> 1.2)
multi_json (~> 1.0)
rack (~> 1.0)
tilt (~> 1.1, != 1.3.0)
sprockets-sass (1.0.3)
sprockets (~> 2.0)
tilt (~> 1.1)
sqlite3 (1.3.8)
thin (1.5.1)
daemons (>= 1.0.9)
eventmachine (>= 0.12.6)
rack (>= 1.0.0)
tilt (1.3.5)
treetop (1.4.12)
thread_safe (0.2.0)
atomic (>= 1.1.7, < 2)
tilt (1.4.1)
treetop (1.4.15)
polyglot
polyglot (>= 0.3.1)
tzinfo (0.3.39)
PLATFORMS
ruby
@ -74,3 +91,5 @@ DEPENDENCIES
rake
rdoc
sass
sprockets
sprockets-sass

View File

@ -1,51 +1,46 @@
require 'rubygems'
require 'rubygems/package'
require File.expand_path('../lib/mail_catcher/version', __FILE__)
require "fileutils"
require "rubygems"
require "rubygems/package"
spec_file = File.expand_path __FILE__ + '/../mailcatcher.gemspec'
spec = Gem::Specification.load spec_file
require "mail_catcher/version"
require 'rdoc/task'
RDoc::Task.new :rdoc => "doc",
:clobber_rdoc => "doc:clean",
:rerdoc => "doc:force" do |rdoc|
spec_file = File.expand_path("../mailcatcher.gemspec", __FILE__)
spec = Gem::Specification.load(spec_file)
require "rdoc/task"
RDoc::Task.new(:rdoc => "doc",:clobber_rdoc => "doc:clean", :rerdoc => "doc:force") do |rdoc|
rdoc.title = "MailCatcher #{MailCatcher::VERSION}"
rdoc.rdoc_dir = 'doc'
rdoc.main = 'README.md'
rdoc.rdoc_files.include 'lib/**/*.rb'
rdoc.rdoc_dir = "doc"
rdoc.main = "README.md"
rdoc.rdoc_files.include "lib/**/*.rb"
end
desc "Compile SASS/SCSS files into SCSS"
task "build:sass" do
Dir["public/stylesheets/**/*.sass"].each do |file|
css_file = file.sub /\.sass$/, ".css"
system "sass", "--no-cache", "--compass", file, css_file
# XXX: Would prefer to use Rake::SprocketsTask but can't populate
# non-digest assets, and we don't want sprockets at runtime so
# can't use manifest directly. Perhaps index.html should be
# precompiled with digest assets paths?
desc "Compile assets"
task "assets" do
compiled_path = File.expand_path("../public/assets", __FILE__)
FileUtils.mkdir_p(compiled_path)
require "mail_catcher/web/assets"
sprockets = MailCatcher::Web::Assets
sprockets.each_logical_path(/\.(js|css|xsl|png)\Z/) do |logical_path|
if asset = sprockets.find_asset(logical_path)
target = File.join(compiled_path, logical_path)
asset.write_to target
end
end
end
desc "Compile CoffeeScript files into JavaScript"
task "build:coffee" do
require 'coffee-script'
Dir["public/javascripts/**/*.coffee"].each do |file|
js_file = file.sub /\.coffee$/, ".js"
File.new(js_file, "w").write CoffeeScript.compile File.read file
end
end
multitask "build" => ["build:sass", "build:coffee"]
desc "Package as Gem"
task "package:gem" do
task "package" => ["assets"] do
Gem::Package.build spec
end
task "package" => ["build", "package:gem"]
desc "Release Gem to RubyGems"
task "release:gem" do
task "release" => ["package"] do
%x[gem push mailcatcher-#{MailCatcher::VERSION}.gem]
end
task "release" => ["package", "release:gem"]
task "default" => "build"

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -22,7 +22,7 @@ class MailCatcher
$('#message .views .analysis.tab a').live 'click', (e) =>
e.preventDefault()
@loadMessageAnalysis @selectedMessage()
$('#message iframe').load =>
@decorateMessageBody()
@ -177,12 +177,12 @@ class MailCatcher
$('#messages tbody tr').show()
addMessage: (message) ->
$('#messages tbody').prepend \
$('<tr />').attr('data-message-id', message.id.toString())
.append($('<td/>').text(message.sender or "No sender").toggleClass("blank", !message.sender))
.append($('<td/>').text((message.recipients || []).join(', ') or "No receipients").toggleClass("blank", !message.recipients.length))
.append($('<td/>').text(message.subject or "No subject").toggleClass("blank", !message.subject))
.append($('<td/>').text @formatDate message.created_at)
$('<tr />').attr('data-message-id', message.id.toString())
.append($('<td/>').text(message.sender or "No sender").toggleClass("blank", !message.sender))
.append($('<td/>').text((message.recipients || []).join(', ') or "No receipients").toggleClass("blank", !message.recipients.length))
.append($('<td/>').text(message.subject or "No subject").toggleClass("blank", !message.subject))
.append($('<td/>').text(@formatDate(message.created_at)))
.prependTo($('#messages tbody'))
scrollToRow: (row) ->
relativePosition = row.offset().top - $('#messages').offset().top
@ -260,8 +260,8 @@ class MailCatcher
decorateMessageBody: ->
format = $('#message .views .tab.format.selected').attr 'data-message-format'
switch format
switch format
when 'html'
body = $('#message iframe').contents().find('body')
$("a", body).attr("target", "_blank")

View File

@ -1,5 +1,5 @@
@import compass
@import compass/reset
@import "compass"
@import "compass/reset"
html, body
width: 100%
@ -57,7 +57,7 @@ body > header
margin-left: 6px
padding: 6px
padding-left: 30px
background: url(/images/logo.png) left no-repeat
background: url(/assets/logo.png) left no-repeat
font-size: 18px
font-weight: bold
a

View File

@ -155,7 +155,7 @@ module MailCatcher extend self
puts "Starting MailCatcher"
Thin::Logging.silent = true
Thin::Logging.silent = (ENV["MAILCATCHER_ENV"] != "development")
# One EventMachine loop...
EventMachine.run do
@ -174,7 +174,7 @@ module MailCatcher extend self
# Let Thin set itself up inside our EventMachine loop
# (Skinny/WebSockets just works on the inside)
rescue_port options[:http_port] do
Thin::Server.start options[:http_ip], options[:http_port], Web
Thin::Server.start(options[:http_ip], options[:http_port], Web)
puts "==> #{http_url}"
end

View File

@ -1,152 +1,21 @@
require "pathname"
require "net/http"
require "uri"
require "active_support/core_ext/module/delegation"
require "rack/builder"
require "sinatra"
require "skinny"
require "mail_catcher/web/application"
require "mail_catcher/events"
require "mail_catcher/mail"
module MailCatcher
module Web extend self
def app
@@app ||= Rack::Builder.new do
if ENV["MAILCATCHER_ENV"] == "development"
require "mail_catcher/web/assets"
map("/assets") { run Assets }
end
class Sinatra::Request
include Skinny::Helpers
end
class MailCatcher::Web < Sinatra::Base
set :root, File.expand_path("#{__FILE__}/../../..")
set :haml, :format => :html5
get "/" do
haml :index
end
delete "/" do
if MailCatcher.quittable?
MailCatcher.quit!
status 204
else
status 403
map("/") { run Application }
end
end
end
get "/messages" do
if request.websocket?
request.websocket!(
:on_start => proc do |websocket|
subscription = MailCatcher::Events::MessageAdded.subscribe { |message| websocket.send_message message.to_json }
websocket.on_close do |websocket|
MailCatcher::Events::MessageAdded.unsubscribe subscription
end
end)
else
MailCatcher::Mail.messages.to_json
end
end
delete "/messages" do
MailCatcher::Mail.delete!
status 204
end
get "/messages/:id.json" do
id = params[:id].to_i
if message = MailCatcher::Mail.message(id)
message.merge({
"formats" => [
"source",
("html" if MailCatcher::Mail.message_has_html? id),
("plain" if MailCatcher::Mail.message_has_plain? id)
].compact,
"attachments" => MailCatcher::Mail.message_attachments(id).map do |attachment|
attachment.merge({"href" => "/messages/#{escape(id)}/parts/#{escape(attachment["cid"])}"})
end,
}).to_json
else
not_found
end
end
get "/messages/:id.html" do
id = params[:id].to_i
if part = MailCatcher::Mail.message_part_html(id)
content_type part["type"], :charset => (part["charset"] || "utf8")
body = part["body"]
# Rewrite body to link to embedded attachments served by cid
body.gsub! /cid:([^'"> ]+)/, "#{id}/parts/\\1"
body
else
not_found
end
end
get "/messages/:id.plain" do
id = params[:id].to_i
if part = MailCatcher::Mail.message_part_plain(id)
content_type part["type"], :charset => (part["charset"] || "utf8")
part["body"]
else
not_found
end
end
get "/messages/:id.source" do
id = params[:id].to_i
if message = MailCatcher::Mail.message(id)
content_type "text/plain"
message["source"]
else
not_found
end
end
get "/messages/:id.eml" do
id = params[:id].to_i
if message = MailCatcher::Mail.message(id)
content_type "message/rfc822"
message["source"]
else
not_found
end
end
get "/messages/:id/parts/:cid" do
id = params[:id].to_i
if part = MailCatcher::Mail.message_part_cid(id, params[:cid])
content_type part["type"], :charset => (part["charset"] || "utf8")
attachment part["filename"] if part["is_attachment"] == 1
body part["body"].to_s
else
not_found
end
end
get "/messages/:id/analysis.?:format?" do
id = params[:id].to_i
if part = MailCatcher::Mail.message_part_html(id)
# TODO: Server-side cache? Make the browser cache based on message create time? Hmm.
uri = URI.parse("http://api.getfractal.com/api/v2/validate#{"/format/#{params[:format]}" if params[:format].present?}")
response = Net::HTTP.post_form(uri, :api_key => "5c463877265251386f516f7428", :html => part["body"])
content_type ".#{params[:format]}" if params[:format].present?
body response.body
else
not_found
end
end
delete "/messages/:id" do
id = params[:id].to_i
if message = MailCatcher::Mail.message(id)
MailCatcher::Mail.delete_message!(id)
status 204
else
not_found
end
end
not_found do
"<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>"
delegate :call, :to => :app
end
end

View File

@ -0,0 +1,157 @@
require "pathname"
require "net/http"
require "uri"
require "sinatra"
require "skinny"
require "mail_catcher/events"
require "mail_catcher/mail"
require "mail_catcher/web"
class Sinatra::Request
include Skinny::Helpers
end
module MailCatcher
module Web
class Application < Sinatra::Base
set :root, File.expand_path("#{__FILE__}/../../../..")
set :haml, :format => :html5
get "/" do
haml :index
end
delete "/" do
if MailCatcher.quittable?
MailCatcher.quit!
status 204
else
status 403
end
end
get "/messages" do
if request.websocket?
request.websocket!(
:on_start => proc do |websocket|
subscription = Events::MessageAdded.subscribe { |message| websocket.send_message message.to_json }
websocket.on_close do |websocket|
Events::MessageAdded.unsubscribe subscription
end
end)
else
Mail.messages.to_json
end
end
delete "/messages" do
Mail.delete!
status 204
end
get "/messages/:id.json" do
id = params[:id].to_i
if message = Mail.message(id)
message.merge({
"formats" => [
"source",
("html" if Mail.message_has_html? id),
("plain" if Mail.message_has_plain? id)
].compact,
"attachments" => Mail.message_attachments(id).map do |attachment|
attachment.merge({"href" => "/messages/#{escape(id)}/parts/#{escape(attachment["cid"])}"})
end,
}).to_json
else
not_found
end
end
get "/messages/:id.html" do
id = params[:id].to_i
if part = Mail.message_part_html(id)
content_type part["type"], :charset => (part["charset"] || "utf8")
body = part["body"]
# Rewrite body to link to embedded attachments served by cid
body.gsub! /cid:([^'"> ]+)/, "#{id}/parts/\\1"
body
else
not_found
end
end
get "/messages/:id.plain" do
id = params[:id].to_i
if part = Mail.message_part_plain(id)
content_type part["type"], :charset => (part["charset"] || "utf8")
part["body"]
else
not_found
end
end
get "/messages/:id.source" do
id = params[:id].to_i
if message = Mail.message(id)
content_type "text/plain"
message["source"]
else
not_found
end
end
get "/messages/:id.eml" do
id = params[:id].to_i
if message = Mail.message(id)
content_type "message/rfc822"
message["source"]
else
not_found
end
end
get "/messages/:id/parts/:cid" do
id = params[:id].to_i
if part = Mail.message_part_cid(id, params[:cid])
content_type part["type"], :charset => (part["charset"] || "utf8")
attachment part["filename"] if part["is_attachment"] == 1
body part["body"].to_s
else
not_found
end
end
get "/messages/:id/analysis.?:format?" do
id = params[:id].to_i
if part = Mail.message_part_html(id)
# TODO: Server-side cache? Make the browser cache based on message create time? Hmm.
uri = URI.parse("http://api.getfractal.com/api/v2/validate#{"/format/#{params[:format]}" if params[:format].present?}")
response = Net::HTTP.post_form(uri, :api_key => "5c463877265251386f516f7428", :html => part["body"])
content_type ".#{params[:format]}" if params[:format].present?
body response.body
else
not_found
end
end
delete "/messages/:id" do
id = params[:id].to_i
if message = Mail.message(id)
MailCatcher::Mail.delete_message!(id)
status 204
else
not_found
end
end
not_found do
"<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>"
end
end
end
end

View File

@ -0,0 +1,15 @@
require "sprockets"
require "sprockets-sass"
require "compass"
require "mail_catcher/web/application"
module MailCatcher
module Web
Assets = Sprockets::Environment.new(Application.root).tap do |sprockets|
Dir["#{Application.root}/{,vendor}/assets/*"].each do |path|
sprockets.append_path(path)
end
end
end
end

View File

@ -21,12 +21,9 @@ Gem::Specification.new do |s|
"README.md", "LICENSE", "VERSION",
"bin/*",
"lib/**/*.rb",
"public/favicon.ico",
"public/images/**/*",
"public/javascripts/**/*.js",
"public/stylesheets/**/*.{css,xsl}",
"views/**/*"
]
"public/**/*",
"views/**/*",
] - Dir["lib/mail_catcher/web/assets.rb"]
s.require_paths = ["lib"]
s.executables = ["mailcatcher", "catchmail"]
s.extra_rdoc_files = ["README.md", "LICENSE"]
@ -47,4 +44,6 @@ Gem::Specification.new do |s|
s.add_development_dependency "rake"
s.add_development_dependency "rdoc"
s.add_development_dependency "sass"
s.add_development_dependency "sprockets"
s.add_development_dependency "sprockets-sass"
end

View File

@ -1,435 +0,0 @@
(function() {
var MailCatcher,
__bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
jQuery.expr[':'].icontains = function(a, i, m) {
var _ref, _ref1;
return ((_ref = (_ref1 = a.textContent) != null ? _ref1 : a.innerText) != null ? _ref : "").toUpperCase().indexOf(m[3].toUpperCase()) >= 0;
};
MailCatcher = (function() {
function MailCatcher() {
this.nextTab = __bind(this.nextTab, this);
this.previousTab = __bind(this.previousTab, this);
this.openTab = __bind(this.openTab, this);
this.selectedTab = __bind(this.selectedTab, this);
this.getTab = __bind(this.getTab, this);
var _this = this;
$('#messages tr').live('click', function(e) {
e.preventDefault();
return _this.loadMessage($(e.currentTarget).attr('data-message-id'));
});
$('input[name=search]').keyup(function(e) {
var query;
query = $.trim($(e.currentTarget).val());
if (query) {
return _this.searchMessages(query);
} else {
return _this.clearSearch();
}
});
$('#message .views .format.tab a').live('click', function(e) {
e.preventDefault();
return _this.loadMessageBody(_this.selectedMessage(), $($(e.currentTarget).parent('li')).data('message-format'));
});
$('#message .views .analysis.tab a').live('click', function(e) {
e.preventDefault();
return _this.loadMessageAnalysis(_this.selectedMessage());
});
$('#message iframe').load(function() {
return _this.decorateMessageBody();
});
$('#resizer').live({
mousedown: function(e) {
var events;
e.preventDefault();
return $(window).bind(events = {
mouseup: function(e) {
e.preventDefault();
return $(window).unbind(events);
},
mousemove: function(e) {
e.preventDefault();
return _this.resizeTo(e.clientY);
}
});
}
});
this.resizeToSaved();
$('nav.app .clear a').live('click', function(e) {
e.preventDefault();
if (confirm("You will lose all your received messages.\n\nAre you sure you want to clear all messages?")) {
return $.ajax({
url: '/messages',
type: 'DELETE',
success: function() {
return _this.unselectMessage();
},
error: function() {
return alert('Error while clearing all messages.');
}
});
}
});
$('nav.app .quit a').live('click', function(e) {
e.preventDefault();
if (confirm("You will lose all your received messages.\n\nAre you sure you want to quit?")) {
return $.ajax({
type: 'DELETE',
success: function() {
return location.replace($('body > header h1 a').attr('href'));
},
error: function() {
return alert('Error while quitting.');
}
});
}
});
key('up', function() {
if (_this.selectedMessage()) {
_this.loadMessage($('#messages tr.selected').prev().data('message-id'));
} else {
_this.loadMessage($('#messages tbody tr[data-message-id]:first').data('message-id'));
}
return false;
});
key('down', function() {
if (_this.selectedMessage()) {
_this.loadMessage($('#messages tr.selected').next().data('message-id'));
} else {
_this.loadMessage($('#messages tbody tr[data-message-id]:first').data('message-id'));
}
return false;
});
key('⌘+up, ctrl+up', function() {
_this.loadMessage($('#messages tbody tr[data-message-id]:first').data('message-id'));
return false;
});
key('⌘+down, ctrl+down', function() {
_this.loadMessage($('#messages tbody tr[data-message-id]:last').data('message-id'));
return false;
});
key('left', function() {
_this.openTab(_this.previousTab());
return false;
});
key('right', function() {
_this.openTab(_this.nextTab());
return false;
});
key('backspace, delete', function() {
var id;
id = _this.selectedMessage();
if (id != null) {
$.ajax({
url: '/messages/' + id,
type: 'DELETE',
success: function() {
var messageRow, switchTo;
messageRow = $("#messages tbody tr[data-message-id='" + id + "']");
switchTo = messageRow.next().data('message-id') || messageRow.prev().data('message-id');
messageRow.remove();
if (switchTo) {
return _this.loadMessage(switchTo);
} else {
return _this.unselectMessage();
}
},
error: function() {
return alert('Error while removing message.');
}
});
}
return false;
});
this.refresh();
this.subscribe();
}
MailCatcher.prototype.parseDateRegexp = /^(\d{4})[-\/\\](\d{2})[-\/\\](\d{2})(?:\s+|T)(\d{2})[:-](\d{2})[:-](\d{2})(?:([ +-]\d{2}:\d{2}|\s*\S+|Z?))?$/;
MailCatcher.prototype.parseDate = function(date) {
var match;
if (match = this.parseDateRegexp.exec(date)) {
return new Date(match[1], match[2] - 1, match[3], match[4], match[5], match[6], 0);
}
};
MailCatcher.prototype.offsetTimeZone = function(date) {
var offset;
offset = Date.now().getTimezoneOffset() * 60000;
date.setTime(date.getTime() - offset);
return date;
};
MailCatcher.prototype.formatDate = function(date) {
if (typeof date === "string") {
date && (date = this.parseDate(date));
}
date && (date = this.offsetTimeZone(date));
return date && (date = date.toString("dddd, d MMM yyyy h:mm:ss tt"));
};
MailCatcher.prototype.messagesCount = function() {
return $('#messages tr').length - 1;
};
MailCatcher.prototype.tabs = function() {
return $('#message ul').children('.tab');
};
MailCatcher.prototype.getTab = function(i) {
return $(this.tabs()[i]);
};
MailCatcher.prototype.selectedTab = function() {
return this.tabs().index($('#message li.tab.selected'));
};
MailCatcher.prototype.openTab = function(i) {
return this.getTab(i).children('a').click();
};
MailCatcher.prototype.previousTab = function(tab) {
var i;
i = tab || tab === 0 ? tab : this.selectedTab() - 1;
if (i < 0) {
i = this.tabs().length - 1;
}
if (this.getTab(i).is(":visible")) {
return i;
} else {
return this.previousTab(i - 1);
}
};
MailCatcher.prototype.nextTab = function(tab) {
var i;
i = tab ? tab : this.selectedTab() + 1;
if (i > this.tabs().length - 1) {
i = 0;
}
if (this.getTab(i).is(":visible")) {
return i;
} else {
return this.nextTab(i + 1);
}
};
MailCatcher.prototype.haveMessage = function(message) {
if (message.id != null) {
message = message.id;
}
return $("#messages tbody tr[data-message-id=\"" + message + "\"]").length > 0;
};
MailCatcher.prototype.selectedMessage = function() {
return $('#messages tr.selected').data('message-id');
};
MailCatcher.prototype.searchMessages = function(query) {
var $rows, selector, token;
selector = ((function() {
var _i, _len, _ref, _results;
_ref = query.split(/\s+/);
_results = [];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
token = _ref[_i];
_results.push(":icontains('" + token + "')");
}
return _results;
})()).join("");
$rows = $("#messages tbody tr");
$rows.not(selector).hide();
return $rows.filter(selector).show();
};
MailCatcher.prototype.clearSearch = function() {
return $('#messages tbody tr').show();
};
MailCatcher.prototype.addMessage = function(message) {
return $('#messages tbody').prepend($('<tr />').attr('data-message-id', message.id.toString()).append($('<td/>').text(message.sender || "No sender").toggleClass("blank", !message.sender)).append($('<td/>').text((message.recipients || []).join(', ') || "No receipients").toggleClass("blank", !message.recipients.length)).append($('<td/>').text(message.subject || "No subject").toggleClass("blank", !message.subject)).append($('<td/>').text(this.formatDate(message.created_at))));
};
MailCatcher.prototype.scrollToRow = function(row) {
var overflow, relativePosition;
relativePosition = row.offset().top - $('#messages').offset().top;
if (relativePosition < 0) {
return $('#messages').scrollTop($('#messages').scrollTop() + relativePosition - 20);
} else {
overflow = relativePosition + row.height() - $('#messages').height();
if (overflow > 0) {
return $('#messages').scrollTop($('#messages').scrollTop() + overflow + 20);
}
}
};
MailCatcher.prototype.unselectMessage = function() {
$('#messages tbody, #message .metadata dd').empty();
$('#message .metadata .attachments').hide();
$('#message iframe').attr('src', 'about:blank');
return null;
};
MailCatcher.prototype.loadMessage = function(id) {
var messageRow,
_this = this;
if ((id != null ? id.id : void 0) != null) {
id = id.id;
}
id || (id = $('#messages tr.selected').attr('data-message-id'));
if (id != null) {
$("#messages tbody tr:not([data-message-id='" + id + "'])").removeClass('selected');
messageRow = $("#messages tbody tr[data-message-id='" + id + "']");
messageRow.addClass('selected');
this.scrollToRow(messageRow);
return $.getJSON("/messages/" + id + ".json", function(message) {
var $ul;
$('#message .metadata dd.created_at').text(_this.formatDate(message.created_at));
$('#message .metadata dd.from').text(message.sender);
$('#message .metadata dd.to').text((message.recipients || []).join(', '));
$('#message .metadata dd.subject').text(message.subject);
$('#message .views .tab.format').each(function(i, el) {
var $el, format;
$el = $(el);
format = $el.attr('data-message-format');
if ($.inArray(format, message.formats) >= 0) {
$el.find('a').attr('href', "/messages/" + id + "." + format);
return $el.show();
} else {
return $el.hide();
}
});
if ($("#message .views .tab.selected:not(:visible)").length) {
$("#message .views .tab.selected").removeClass("selected");
$("#message .views .tab:visible:first").addClass("selected");
}
if (message.attachments.length) {
$ul = $('<ul/>').appendTo($('#message .metadata dd.attachments').empty());
$.each(message.attachments, function(i, attachment) {
return $ul.append($('<li>').append($('<a>').attr('href', attachment['href']).addClass(attachment['type'].split('/', 1)[0]).addClass(attachment['type'].replace('/', '-')).text(attachment['filename'])));
});
$('#message .metadata .attachments').show();
} else {
$('#message .metadata .attachments').hide();
}
$('#message .views .download a').attr('href', "/messages/" + id + ".eml");
if ($('#message .views .tab.analysis.selected').length) {
return _this.loadMessageAnalysis();
} else {
return _this.loadMessageBody();
}
});
}
};
MailCatcher.prototype.loadMessageBody = function(id, format) {
var app;
id || (id = this.selectedMessage());
format || (format = $('#message .views .tab.format.selected').attr('data-message-format'));
format || (format = 'html');
$("#message .views .tab[data-message-format=\"" + format + "\"]:not(.selected)").addClass('selected');
$("#message .views .tab:not([data-message-format=\"" + format + "\"]).selected").removeClass('selected');
if (id != null) {
$('#message iframe').attr("src", "/messages/" + id + "." + format);
return app = this;
}
};
MailCatcher.prototype.decorateMessageBody = function() {
var body, format, message_iframe, text;
format = $('#message .views .tab.format.selected').attr('data-message-format');
switch (format) {
case 'html':
body = $('#message iframe').contents().find('body');
return $("a", body).attr("target", "_blank");
case 'plain':
message_iframe = $('#message iframe').contents();
text = message_iframe.text();
text = text.replace(/((http|ftp|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&amp;:\/~\+#]*[\w\-\@?^=%&amp;\/~\+#])?)/g, '<a href="$1" target="_blank">$1</a>');
text = text.replace(/\n/g, '<br/>');
return message_iframe.find('html').html('<html><body>' + text + '</html></body>');
}
};
MailCatcher.prototype.loadMessageAnalysis = function(id) {
var $form, $iframe;
id || (id = this.selectedMessage());
$("#message .views .analysis.tab:not(.selected)").addClass('selected');
$("#message .views :not(.analysis).tab.selected").removeClass('selected');
if (id != null) {
$iframe = $('#message iframe').contents().children().html("<html>\n<head>\n<title>Analysis</title>\n" + ($('link[rel="stylesheet"]')[0].outerHTML) + "\n</head>\n<body class=\"iframe\">\n<h1>Analyse your email with Fractal</h1>\n<p><a href=\"http://getfractal.com/\" target=\"_blank\">Fractal</a> is a really neat service that applies common email design and development knowledge from <a href=\"http://www.email-standards.org/\" target=\"_blank\">Email Standards Project</a> to your HTML email and tells you what you've done wrong or what you should do instead.</p>\n<p>Please note that this <strong>sends your email to the Fractal service</strong> for analysis. Read their <a href=\"https://www.getfractal.com/page/terms\" target=\"_blank\">terms of service</a> if you're paranoid.</p>\n<form>\n<input type=\"submit\" value=\"Analyse\" /><span class=\"loading\" style=\"color: #999; display: none\">Analysing&hellip;</span>\n</form>\n</body>\n</html>");
return $form = $iframe.find('form').submit(function(e) {
e.preventDefault();
$(this).find('input[type="submit"]').attr('disabled', 'disabled').end().find('.loading').show();
return $('#message iframe').contents().find('body').xslt("/messages/" + id + "/analysis.xml", "/stylesheets/analysis.xsl");
});
}
};
MailCatcher.prototype.refresh = function() {
var _this = this;
return $.getJSON('/messages', function(messages) {
return $.each(messages, function(i, message) {
if (!_this.haveMessage(message)) {
return _this.addMessage(message);
}
});
});
};
MailCatcher.prototype.subscribe = function() {
if (typeof WebSocket !== "undefined" && WebSocket !== null) {
return this.subscribeWebSocket();
} else {
return this.subscribePoll();
}
};
MailCatcher.prototype.subscribeWebSocket = function() {
var secure,
_this = this;
secure = window.location.scheme === 'https';
this.websocket = new WebSocket("" + (secure ? 'wss' : 'ws') + "://" + window.location.host + "/messages");
return this.websocket.onmessage = function(event) {
return _this.addMessage($.parseJSON(event.data));
};
};
MailCatcher.prototype.subscribePoll = function() {
var _this = this;
if (this.refreshInterval == null) {
return this.refreshInterval = setInterval((function() {
return _this.refresh();
}), 1000);
}
};
MailCatcher.prototype.resizeToSavedKey = 'mailcatcherSeparatorHeight';
MailCatcher.prototype.resizeTo = function(height) {
var _ref;
$('#messages').css({
height: height - $('#messages').offset().top
});
return (_ref = window.localStorage) != null ? _ref.setItem(this.resizeToSavedKey, height) : void 0;
};
MailCatcher.prototype.resizeToSaved = function() {
var height, _ref;
height = parseInt((_ref = window.localStorage) != null ? _ref.getItem(this.resizeToSavedKey) : void 0);
if (!isNaN(height)) {
return this.resizeTo(height);
}
};
return MailCatcher;
})();
$(function() {
return window.MailCatcher = new MailCatcher;
});
}).call(this);

View File

@ -1,375 +0,0 @@
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, menu, nav, section, summary {
display: block; }
html, body {
width: 100%;
height: 100%; }
body {
display: -webkit-box;
display: -moz-box;
display: -ms-box;
display: box;
-webkit-box-orient: vertical;
-moz-box-orient: vertical;
-ms-box-orient: vertical;
box-orient: vertical;
background: #eeeeee;
color: black;
font-size: 12px;
font-family: Helvetica, sans-serif; }
body html {
font-size: 75%;
line-height: 2em; }
body.iframe {
background: white; }
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: #666666;
margin-left: 0.5em; }
.button {
padding: 0.5em 1em;
border: 1px solid #cccccc;
-webkit-border-radius: 2px;
-moz-border-radius: 2px;
-ms-border-radius: 2px;
-o-border-radius: 2px;
border-radius: 2px;
background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #f4f4f4), color-stop(100%, #ececec)), #ececec;
background: -webkit-linear-gradient(#f4f4f4, #ececec), #ececec;
background: -moz-linear-gradient(#f4f4f4, #ececec), #ececec;
background: -o-linear-gradient(#f4f4f4, #ececec), #ececec;
background: -ms-linear-gradient(#f4f4f4, #ececec), #ececec;
background: linear-gradient(#f4f4f4, #ececec), #ececec;
color: #666666;
text-shadow: 1px 1px 0 white;
text-decoration: none; }
.button:hover, .button:focus {
border-color: #999999;
border-bottom-color: #666666;
background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #eeeeee), color-stop(100%, #dddddd)), #dddddd;
background: -webkit-linear-gradient(#eeeeee, #dddddd), #dddddd;
background: -moz-linear-gradient(#eeeeee, #dddddd), #dddddd;
background: -o-linear-gradient(#eeeeee, #dddddd), #dddddd;
background: -ms-linear-gradient(#eeeeee, #dddddd), #dddddd;
background: linear-gradient(#eeeeee, #dddddd), #dddddd;
color: #333333;
text-decoration: none; }
.button:active, .button.active {
border-color: #666666;
border-bottom-color: #999999;
background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #dddddd), color-stop(100%, #eeeeee)), #eeeeee;
background: -webkit-linear-gradient(#dddddd, #eeeeee), #eeeeee;
background: -moz-linear-gradient(#dddddd, #eeeeee), #eeeeee;
background: -o-linear-gradient(#dddddd, #eeeeee), #eeeeee;
background: -ms-linear-gradient(#dddddd, #eeeeee), #eeeeee;
background: linear-gradient(#dddddd, #eeeeee), #eeeeee;
color: #333333;
text-decoration: none;
text-shadow: -1px -1px 0 #eeeeee; }
body > header {
overflow: hidden;
*zoom: 1;
border-bottom: 1px solid #cccccc; }
body > header h1 {
float: left;
margin-left: 6px;
padding: 6px;
padding-left: 30px;
background: url(/images/logo.png) left no-repeat;
font-size: 18px;
font-weight: bold; }
body > header h1 a {
color: black;
text-decoration: none;
text-shadow: 0 1px 0 white;
-webkit-transition: 0.1s ease;
-moz-transition: 0.1s ease;
-ms-transition: 0.1s ease;
-o-transition: 0.1s ease;
transition: 0.1s ease; }
body > header h1 a:hover {
color: #4183c4; }
body > header nav {
border-left: 1px solid #cccccc; }
body > header nav.project {
float: left; }
body > header nav.app {
float: right; }
body > header nav li {
display: block;
float: left;
border-left: 1px solid white;
border-right: 1px solid #cccccc; }
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: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #f4f4f4), color-stop(100%, #ececec)), #ececec;
background: -webkit-linear-gradient(#f4f4f4, #ececec), #ececec;
background: -moz-linear-gradient(#f4f4f4, #ececec), #ececec;
background: -o-linear-gradient(#f4f4f4, #ececec), #ececec;
background: -ms-linear-gradient(#f4f4f4, #ececec), #ececec;
background: linear-gradient(#f4f4f4, #ececec), #ececec;
color: #666666;
text-shadow: 1px 1px 0 white;
text-decoration: none; }
body > header nav li a:hover, body > header nav li a:focus {
background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #eeeeee), color-stop(100%, #dddddd)), #dddddd;
background: -webkit-linear-gradient(#eeeeee, #dddddd), #dddddd;
background: -moz-linear-gradient(#eeeeee, #dddddd), #dddddd;
background: -o-linear-gradient(#eeeeee, #dddddd), #dddddd;
background: -ms-linear-gradient(#eeeeee, #dddddd), #dddddd;
background: linear-gradient(#eeeeee, #dddddd), #dddddd;
color: #333333;
text-decoration: none; }
body > header nav li a:active, body > header nav li a.active {
background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #dddddd), color-stop(100%, #eeeeee)), #eeeeee;
background: -webkit-linear-gradient(#dddddd, #eeeeee), #eeeeee;
background: -moz-linear-gradient(#dddddd, #eeeeee), #eeeeee;
background: -o-linear-gradient(#dddddd, #eeeeee), #eeeeee;
background: -ms-linear-gradient(#dddddd, #eeeeee), #eeeeee;
background: linear-gradient(#dddddd, #eeeeee), #eeeeee;
color: #333333;
text-decoration: none;
text-shadow: -1px -1px 0 #eeeeee; }
#messages {
width: 100%;
height: 10em;
min-height: 3em;
overflow: auto;
background: white;
border-top: 1px solid white; }
#messages table {
overflow: hidden;
*zoom: 1;
width: 100%; }
#messages table thead tr {
background: #eeeeee;
color: #333333; }
#messages table thead tr th {
padding: 0.25em;
font-weight: bold;
color: #666666;
text-shadow: 0 1px 0 white; }
#messages table tbody tr {
cursor: pointer;
-webkit-transition: 0.1s ease;
-moz-transition: 0.1s ease;
-ms-transition: 0.1s ease;
-o-transition: 0.1s ease;
transition: 0.1s ease;
color: #333333; }
#messages table tbody tr:hover {
color: black; }
#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: #666666;
font-style: italic; }
#resizer {
padding-bottom: 5px;
cursor: ns-resize; }
#resizer .ruler {
border-top: 1px solid #cccccc;
border-bottom: 1px solid white; }
#message {
display: -webkit-box;
display: -moz-box;
display: -ms-box;
display: box;
-webkit-box-orient: vertical;
-moz-box-orient: vertical;
-ms-box-orient: vertical;
box-orient: vertical;
-webkit-box-flex: 1;
-moz-box-flex: 1;
-ms-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: #666666;
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: -moz-inline-box;
-moz-box-orient: vertical;
display: inline-block;
vertical-align: middle;
*vertical-align: auto;
margin-right: 0.5em; }
#message > header .metadata .attachments ul li {
*display: inline; }
#message > header .views ul {
padding: 0 0.5em;
border-bottom: 1px solid #cccccc; }
#message > header .views .tab {
display: -moz-inline-box;
-moz-box-orient: vertical;
display: inline-block;
vertical-align: middle;
*vertical-align: auto; }
#message > header .views .tab {
*display: inline; }
#message > header .views .tab a {
display: -moz-inline-box;
-moz-box-orient: vertical;
display: inline-block;
vertical-align: middle;
*vertical-align: auto;
padding: 0.5em;
border: 1px solid #cccccc;
background: #dddddd;
color: #333333;
border-width: 1px 1px 0 1px;
cursor: pointer;
text-shadow: 0 1px 0 #eeeeee;
text-decoration: none; }
#message > header .views .tab a {
*display: inline; }
#message > header .views .tab:not(.selected):hover a {
background-color: #eeeeee; }
#message > header .views .tab.selected a {
background: white;
color: black;
height: 13px;
-webkit-box-shadow: 1px 1px 0 #cccccc;
-moz-box-shadow: 1px 1px 0 #cccccc;
box-shadow: 1px 1px 0 #cccccc;
margin-bottom: -2px;
cursor: default; }
#message > header .views .action {
display: -moz-inline-box;
-moz-box-orient: vertical;
display: inline-block;
vertical-align: middle;
*vertical-align: auto;
float: right;
margin: 0 0.25em; }
#message > header .views .action {
*display: inline; }
.fractal-analysis {
margin: 12px 0; }
.fractal-analysis .report-intro {
font-weight: bold; }
.fractal-analysis .report-intro.valid {
color: #009900; }
.fractal-analysis .report-intro.invalid {
color: #cc3333; }
.fractal-analysis code {
font-family: Monaco, "Courier New", Courier, monospace;
background-color: ghostwhite;
color: #444444;
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: -webkit-box;
display: -moz-box;
display: -ms-box;
display: box;
-webkit-box-flex: 1;
-moz-box-flex: 1;
-ms-box-flex: 1;
box-flex: 1;
background: white; }

View File

@ -2,15 +2,15 @@
%html.mailcatcher
%head
%title MailCatcher
%link{:rel => "stylesheet", :href => "/stylesheets/application.css"}
%link{:rel => "stylesheet", :href => "/assets/application.css"}
%link{:href => "/favicon.ico", :rel => "shortcut icon" }
%script{:src => "/javascripts/modernizr.js"}
%script{:src => "/javascripts/jquery.js"}
%script{:src => "/javascripts/xslt-3.2.js"}
%script{:src => "/javascripts/date.js"}
%script{:src => "/javascripts/flexie.min.js"}
%script{:src => "/javascripts/keymaster.min.js"}
%script{:src => "/javascripts/application.js"}
%script{:src => "/assets/modernizr.js"}
%script{:src => "/assets/jquery.js"}
%script{:src => "/assets/xslt-3.2.js"}
%script{:src => "/assets/date.js"}
%script{:src => "/assets/flexie.min.js"}
%script{:src => "/assets/keymaster.min.js"}
%script{:src => "/assets/application.js"}
%body
%header
%h1