Split into modules, updated style a little, switched to Jeweler, added EventMachine Channel and Skinny WebSockets for no-refresh mail notification, got rid of Sunshowers.

This commit is contained in:
Samuel Cochran 2010-10-28 03:12:26 +08:00
parent 04e727a8cd
commit 8db4f674f4
15 changed files with 7354 additions and 375 deletions

6
.gitignore vendored
View File

@ -1,4 +1,2 @@
.bundle
Gemfile.lock
.rvmrc
mailcatcher-0.1.0.gem mailcatcher-0.1.1.gem mailcatcher-0.1.2.gem mailcatcher-0.1.3.gem
*.gemspec
pkg

33
LICENSE
View File

@ -1,19 +1,20 @@
Copyright (c) 2010 Samuel Cochran <sj26@sj26.com>
Copyright (c) 2010 Samuel Cochran
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -32,9 +32,13 @@ MailCatcher runs a super simple SMTP server which catches any message sent to it
* Forward mail to rendering service, maybe CampaignMonitor?
* Package as an app? Native interfaces?
## License
## Thanks
MailCatcher is released under the MIT License. See LICENSE.
MailCatcher is just a mishmash of other people's hard work. Thank you so much to the people who have built the wonderful guts on which this project relies.
## Copyright
Copyright (c) 2010 Samuel Cochran. See LICENSE for details.
## Dreams

43
Rakefile Normal file
View File

@ -0,0 +1,43 @@
require 'rubygems'
require 'rake'
begin
require 'jeweler'
Jeweler::Tasks.new do |gem|
gem.name = "mailcatcher"
gem.summary = %Q{Runs an SMTP server, catches and displays email in a web interface.}
gem.description = <<-EOD
MailCatcher runs a super simple SMTP server which catches any
message sent to it to display in a web interface. Run
mailcatcher, set your favourite app to deliver to
smtp://127.0.0.1:1025 instead of your default SMTP server,
then check out http://127.0.0.1:1080 to see the mail.
EOD
gem.email = "sj26@sj26.com"
gem.homepage = "http://github.com/sj26/mailcatcher"
gem.authors = ["Samuel Cochran"]
gem.add_dependency 'eventmachine'
gem.add_dependency 'mail'
gem.add_dependency 'i18n'
gem.add_dependency 'sqlite3-ruby'
gem.add_dependency 'thin'
gem.add_dependency 'skinny'
gem.add_dependency 'haml'
gem.add_dependency 'json'
end
Jeweler::GemcutterTasks.new
rescue LoadError
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
end
require 'rake/rdoctask'
Rake::RDocTask.new do |rdoc|
version = File.exist?('VERSION') ? File.read('VERSION') : ""
rdoc.rdoc_dir = 'rdoc'
rdoc.title = "MailCatcher #{version}"
rdoc.rdoc_files.include('README*')
rdoc.rdoc_files.include('lib/*.rb')
rdoc.rdoc_files.include('lib/**/*.rb')
end

1
VERSION Normal file
View File

@ -0,0 +1 @@
0.2.0

View File

@ -1,215 +1,12 @@
require 'rubygems'
require 'eventmachine'
require 'json'
require 'ostruct'
require 'mail'
require 'thin'
require 'sqlite3'
require 'sinatra/base'
require 'sunshowers'
# Monkey-patch Sinatra to use Sunshowers
class Sinatra::Request < Rack::Request
include Sunshowers::WebSocket
end
module MailCatcher
def self.db
@@__db ||= begin
SQLite3::Database.new(':memory:', :results_as_hash => true, :type_translation => true).tap do |db|
begin
db.execute(<<-SQL)
CREATE TABLE mail (
id INTEGER PRIMARY KEY ASC,
sender TEXT,
recipients TEXT,
subject TEXT,
source BLOB,
size TEXT,
created_at DATETIME DEFAULT CURRENT_DATETIME
)
SQL
db.execute(<<-SQL)
CREATE TABLE part (
id INTEGER PRIMARY KEY ASC,
mail_id INTEGER NOT NULL,
cid TEXT,
type TEXT,
is_attachment INTEGER,
filename TEXT,
body BLOB,
size INTEGER,
created_at DATETIME DEFAULT CURRENT_DATETIME
)
SQL
rescue SQLite3::SQLException
end
end
end
end
autoload :Events, 'mail_catcher/events'
autoload :Mail, 'mail_catcher/mail'
autoload :Smtp, 'mail_catcher/smtp'
autoload :Web, 'mail_catcher/web'
def self.subscribers
@@subscribers ||= []
end
class SmtpServer < EventMachine::Protocols::SmtpServer
def insert_message
@@insert_message ||= MailCatcher.db.prepare("INSERT INTO mail (sender, recipients, subject, source, size, created_at) VALUES (?, ?, ?, ?, ?, datetime('now'))")
end
def insert_part
@@insert_part ||= MailCatcher.db.prepare("INSERT INTO part (mail_id, cid, type, is_attachment, filename, body, size, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'))")
end
def current_message
@current_message ||= OpenStruct.new
end
def receive_reset
@current_message = nil
true
end
def receive_sender(sender)
current_message.sender = sender
true
end
def receive_recipient(recipient)
current_message.recipients ||= []
current_message.recipients << recipient
true
end
def receive_data_chunk(lines)
current_message.source ||= ""
current_message.source += lines.join("\n")
true
end
def receive_message
MailCatcher.db.transaction do
mail = Mail.new(current_message.source)
result = insert_message.execute(current_message.sender, current_message.recipients.inspect, mail.subject, current_message.source, current_message.source.length)
mail_id = MailCatcher.db.last_insert_row_id
if mail.multipart?
mail.all_parts.each do |part|
body = part.body.to_s
insert_part.execute(mail_id, part.cid, part.mime_type, part.attachment? ? 1 : 0, part.filename, body, body.length)
end
else
body = mail.body.to_s
insert_part.execute(mail_id, nil, mail.mime_type, 0, mail.filename, body, body.length)
end
puts "==> SMTP: Received message '#{mail.subject}' from '#{current_message.sender}'"
true
end
rescue
puts "*** Error receiving message: #{current_message.inspect}"
puts " Exception: #{$!}"
puts " Backtrace:"
$!.backtrace.each do |line|
puts " #{line}"
end
puts " Please submit this as an issue at http://github.com/sj26/mailcatcher/issues"
ensure
@current_message = nil
end
end
class WebApp < Sinatra::Base
set :views, File.expand_path(File.join(File.dirname(__FILE__), '..', 'views'))
set :haml, {:format => :html5 }
get '/' do
haml :index
end
get '/mail' do
if latest = MailCatcher.db.query('SELECT created_at FROM mail ORDER BY created_at DESC LIMIT 1').next
last_modified latest["created_at"]
end
MailCatcher.db.query('SELECT id, sender, recipients, subject, size, created_at FROM mail ORDER BY created_at DESC').to_a.to_json
end
get '/mail/:id.json' do
mail_id = params[:id].to_i
message = MailCatcher.db.query('SELECT * FROM mail WHERE id = ? LIMIT 1', mail_id).to_a.first
if message
last_modified message["created_at"]
message["formats"] = ['eml']
message["formats"] << 'html' if MailCatcher.db.query('SELECT id FROM part WHERE mail_id = ? AND type = "text/html" LIMIT 1', mail_id).next
message["formats"] << 'txt' if MailCatcher.db.query('SELECT id FROM part WHERE mail_id = ? AND type = "text/plain" LIMIT 1', mail_id).next
message["attachments"] = MailCatcher.db.query('SELECT cid, type, filename, size FROM part WHERE mail_id = ? AND is_attachment = 1 ORDER BY filename ASC', mail_id).to_a.map do |attachment|
attachment.merge({"href" => "/mail/#{escape(params[:id])}/#{escape(attachment['cid'])}"})
end
message.to_json
else
not_found
end
end
get '/mail/:id.html' do
mail_id = params[:id].to_i
part = MailCatcher.db.query('SELECT body, created_at FROM part WHERE mail_id = ? AND type = "text/html" LIMIT 1', mail_id).to_a.first
if part
content_type 'text/html'
last_modified part["created_at"]
part["body"].gsub(/cid:([^'"> ]+)/, "#{mail_id}/\\1")
else
not_found
end
end
get '/mail/:id.txt' do
part = MailCatcher.db.query('SELECT body, created_at FROM part WHERE mail_id = ? AND type = "text/plain" LIMIT 1', params[:id].to_i).to_a.first
if part
content_type 'text/plain'
last_modified part["created_at"]
part["body"]
else
not_found
end
end
get '/mail/:id.eml' do
content_type 'text/plain'
message = MailCatcher.db.query('SELECT source, created_at FROM mail WHERE id = ? ORDER BY created_at DESC LIMIT 1', params[:id].to_i).to_a.first
if message
last_modified message["created_at"]
message["source"]
else
not_found
end
end
get '/mail/:id/:cid' do
result = MailCatcher.db.query('SELECT * FROM part WHERE mail_id = ?', params[:id].to_i)
part = result.find { |part| part["cid"] == params[:cid] }
if part
content_type part["type"]
attachment part["filename"] if part["is_attachment"] == 1
last_modified part["created_at"]
body part["body"].to_s
else
not_found
end
end
get '/mail/subscribe' do
return head 400 unless request.ws?
request.ws_handshake!
request.ws_io.each do |message|
ws_quit! if message == "goodbye"
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
def self.run(options = {})
options[:smtp_ip] ||= '127.0.0.1'
options[:smtp_port] ||= 1025
@ -221,9 +18,9 @@ module MailCatcher
puts "==> http://#{options[:http_ip]}:#{options[:http_port]}"
Thin::Logging.silent = true
EM.run do
EM.start_server options[:smtp_ip], options[:smtp_port], SmtpServer
Thin::Server.start WebApp, options[:http_ip], options[:http_port]
EventMachine.run do
EventMachine.start_server options[:smtp_ip], options[:smtp_port], Smtp
Thin::Server.start options[:http_ip], options[:http_port], Web
end
end
end

View File

@ -0,0 +1,7 @@
require 'eventmachine'
module MailCatcher
module Events
MessageAdded = EventMachine::Channel.new
end
end

119
lib/mail_catcher/mail.rb Normal file
View File

@ -0,0 +1,119 @@
require 'mail'
require 'sqlite3'
require 'eventmachine'
module MailCatcher::Mail
class << self
def db
@@__db ||= begin
SQLite3::Database.new(':memory:', :results_as_hash => true, :type_translation => true).tap do |db|
db.execute(<<-SQL)
CREATE TABLE message (
id INTEGER PRIMARY KEY ASC,
sender TEXT,
recipients TEXT,
subject TEXT,
source BLOB,
size TEXT,
created_at DATETIME DEFAULT CURRENT_DATETIME
)
SQL
db.execute(<<-SQL)
CREATE TABLE message_part (
id INTEGER PRIMARY KEY ASC,
message_id INTEGER NOT NULL,
cid TEXT,
type TEXT,
is_attachment INTEGER,
filename TEXT,
charset TEXT,
body BLOB,
size INTEGER,
created_at DATETIME DEFAULT CURRENT_DATETIME
)
SQL
end
end
end
def add_message(message)
@@add_message_query ||= db.prepare("INSERT INTO message (sender, recipients, subject, source, size, created_at) VALUES (?, ?, ?, ?, ?, datetime('now'))")
mail = Mail.new(message[:source])
result = @@add_message_query.execute(message[:sender], message[:recipients].inspect, mail.subject, message[:source], message[:source].length)
message_id = db.last_insert_row_id
(mail.all_parts || [mail]).each do |part|
body = part.body.to_s
add_message_part(message_id, part.cid, part.mime_type || 'text/plain', part.attachment? ? 1 : 0, part.filename, part.charset, body, body.length)
end
EventMachine.next_tick do
message = MailCatcher::Mail.message message_id
MailCatcher::Events::MessageAdded.push message
end
end
def add_message_part(*args)
@@add_message_part_query ||= db.prepare "INSERT INTO message_part (message_id, cid, type, is_attachment, filename, charset, body, size, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))"
@@add_message_part_query.execute(*args)
end
def latest_created_at
@@latest_created_at_query ||= db.prepare "SELECT created_at FROM message ORDER BY created_at DESC LIMIT 1"
@@latest_created_at_query.execute.next
end
def messages
@@messages_query ||= db.prepare "SELECT id, sender, recipients, subject, size, created_at FROM message ORDER BY created_at DESC"
@@messages_query.execute.to_a
end
def message(id)
@@message_query ||= db.prepare "SELECT * FROM message WHERE id = ? LIMIT 1"
@@message_query.execute(id).next
end
def message_has_html?(id)
@@message_has_html_query ||= db.prepare "SELECT 1 FROM message_part WHERE message_id = ? AND is_attachment = 0 AND type = 'text/html' LIMIT 1"
!!@@message_has_html_query.execute(id).next
end
def message_has_plain?(id)
@@message_has_html_query ||= db.prepare "SELECT 1 FROM message_part WHERE message_id = ? AND is_attachment = 0 AND type = 'text/plain' LIMIT 1"
!!@@message_has_html_query.execute(id).next
end
def message_parts(id)
@@message_parts_query ||= db.prepare "SELECT cid, type, filename, size FROM message_part WHERE message_id = ? ORDER BY filename ASC"
@@message_parts_query.execute(id).to_a
end
def message_attachments(id)
@@message_parts_query ||= db.prepare "SELECT cid, type, filename, size FROM message_part WHERE message_id = ? AND is_attachment = 1 ORDER BY filename ASC"
@@message_parts_query.execute(id).to_a
end
def message_part(message_id, part_id)
@@message_part_query ||= db.prepare "SELECT * FROM message_part WHERE message_id = ? AND id = ? LIMIT 1"
@@message_part_query.execute(message_id, part_id).next
end
def message_part_type(message_id, part_type)
@@message_part_type_query ||= db.prepare "SELECT * FROM message_part WHERE message_id = ? AND type = ? AND is_attachment = 0 LIMIT 1"
@@message_part_type_query.execute(message_id, part_type).next
end
def message_part_html(message_id)
message_part_type message_id, "text/html"
end
def message_part_plain(message_id)
message_part_type message_id, "text/plain"
end
def message_part_cid(message_id, cid)
@@message_part_cid_query ||= db.prepare 'SELECT * FROM message_part WHERE message_id = ?'
@@message_part_cid_query.execute(message_id).find { |part| part["cid"] == cid }
end
end
end

48
lib/mail_catcher/smtp.rb Normal file
View File

@ -0,0 +1,48 @@
require 'eventmachine'
module MailCatcher
class Smtp < EventMachine::Protocols::SmtpServer
def current_message
@current_message ||= {}
end
def receive_reset
@current_message = nil
true
end
def receive_sender(sender)
current_message[:sender] = sender
true
end
def receive_recipient(recipient)
current_message[:recipients] ||= []
current_message[:recipients] << recipient
true
end
def receive_data_chunk(lines)
current_message[:source] ||= ""
current_message[:source] += lines.join("\n")
true
end
def receive_message
MailCatcher::Mail.add_message current_message
puts "==> SMTP: Received message from '#{current_message[:sender]}' (#{current_message[:source].length} bytes)"
true
rescue
puts "*** Error receiving message: #{current_message.inspect}"
puts " Exception: #{$!}"
puts " Backtrace:"
$!.backtrace.each do |line|
puts " #{line}"
end
puts " Please submit this as an issue at http://github.com/sj26/mailcatcher/issues"
false
ensure
@current_message = nil
end
end
end

98
lib/mail_catcher/web.rb Normal file
View File

@ -0,0 +1,98 @@
require 'sinatra'
require 'json'
require 'pathname'
require 'skinny'
class Sinatra::Request
include Skinny::RequestHelpers
end
module MailCatcher
class Web < Sinatra::Base
set :root, Pathname.new(__FILE__).dirname.parent.parent
set :haml, :format => :html5
get '/' do
haml :index
end
get '/messages' do
if request.websocket?
request.websocket!(
:protocol => "MailCatcher 0.2 Message Push",
: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
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),
].flatten,
"attachments" => MailCatcher::Mail.message_attachments(id).map do |attachment|
attachment.merge({"href" => "/messages/#{escape(id)}/#{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")
part["body"].gsub(/cid:([^'"> ]+)/, "#{id}/\\1")
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/: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
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

View File

@ -1,39 +0,0 @@
Gem::Specification.new do |gem|
gem.rubygems_version = "1.3.7"
gem.name = "mailcatcher"
gem.version = "0.1.5"
gem.date = "2010-10-26"
gem.summary = "Runs an SMTP server, catches and displays email in a web interface."
gem.description = <<-EOD
MailCatcher runs a super simple SMTP server which catches any
message sent to it to display in a web interface. Run
mailcatcher, set your favourite app to deliver to
smtp://127.0.0.1:1025 instead of your default SMTP server,
then check out http://127.0.0.1:1080 to see the mail.
EOD
gem.homepage = "http://github.com/sj26/mailcatcher"
gem.authors = ["Samuel Cochran"]
gem.email = "sj26@sj26.com"
gem.add_dependency 'eventmachine'
gem.add_dependency 'haml'
gem.add_dependency 'json'
gem.add_dependency 'mail'
gem.add_dependency 'thin'
gem.add_dependency 'sqlite3-ruby'
gem.add_dependency 'sinatra'
gem.add_dependency 'sunshowers'
# Required by activesupport, required by mail, but not listed.
gem.add_dependency 'i18n'
gem.files = %w[
bin/mailcatcher
lib/mail_catcher.rb
views/index.haml
README.md
LICENSE
];
gem.executables = ["mailcatcher"]
end

View File

@ -0,0 +1,94 @@
var MailCatcher = {
init: function() {
$('#mail tr').live('click', function() {
MailCatcher.load($(this).attr('data-message-id'));
});
$('#message .formats ul li').live('click', function() {
MailCatcher.loadBody($('#mail tr.selected').attr('data-message-id'), $(this).attr('data-message-format'));
});
MailCatcher.refresh();
MailCatcher.subscribe();
},
addMessage: function(message) {
var row = $('<tr />').attr('data-message-id', message.id.toString());
$.each(['sender', 'recipients', 'subject', 'created_at'], function (i, property) {
row.append($('<td />').text(message[property]));
});
$('#mail tbody').append(row);
},
refresh: function() {
console.log('Refreshing messages');
$.getJSON('/messages', function(mail) {
$.each(mail, function(i, message) {
MailCatcher.addMessage(message);
});
});
},
subscribe: function () {
MailCatcher.websocket = new WebSocket("ws" + (window.location.scheme == 'https' ? 's' : '') + "://" + window.location.host + "/messages");
MailCatcher.websocket.onmessage = function (event) {
console.log('Message received:', event.data);
MailCatcher.addMessage($.parseJSON(event.data));
};
},
load: function(id) {
id = id || $('#mail tr.selected').attr('data-message-id');
if (id !== null) {
console.log('Loading message', id);
$('#mail tbody tr:not([data-message-id="'+id+'"])').removeClass('selected');
$('#mail tbody tr[data-message-id="'+id+'"]').addClass('selected');
$.getJSON('/messages/' + id + '.json', function(message) {
$('#message .received span').text(message.created_at);
$('#message .from span').text(message.sender);
$('#message .to span').text(message.recipients);
$('#message .subject span').text(message.subject);
$('#message .formats ul li').each(function(i, el) {
var $el = $(el),
format = $el.attr('data-message-format');
if ($.inArray(format, message.formats) >= 0) {
$el.show();
} else {
$el.hide();
}
});
if ($("#message .formats ul li.selected:not(:visible)")) {
$("#message .formats ul li.selected").removeClass("selected");
$("#message .formats ul li:visible:first").addClass("selected");
}
if (message.attachments.length > 0) {
console.log(message.attachments);
$('#message .attachments ul').empty();
$.each(message.attachments, function (i, attachment) {
$('#message .attachments ul').append($('<li>').append($('<a>').attr('href', attachment['href']).addClass(attachment['type'].split('/', 1)[0]).addClass(attachment['type'].replace('/', '-')).text(attachment['filename'])));
});
$('#message .attachments').show();
} else {
$('#message .attachments').hide();
}
MailCatcher.loadBody();
});
}
},
loadBody: function(id, format) {
id = id || $('#mail tr.selected').attr('data-message-id');
format = format || $('#message .formats ul li.selected').first().attr('data-message-format') || 'html';
$('#message .formats ul li[data-message-format="'+format+'"]').addClass('selected');
$('#message .formats ul li:not([data-message-format="'+format+'"])').removeClass('selected');
if (id !== undefined && id !== null) {
console.log('Loading message', id, 'in format', format);
$('#message iframe').attr('src', '/messages/' + id + '.' + format);
}
}
};

6883
public/javascripts/jquery.js vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,23 @@
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, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin:0px; padding:0px; border:0px; outline:0px; font-weight:inherit; font-style:inherit; font-size:100%; font-family:inherit; vertical-align:baseline; }
body { line-height: 1; color: black; background: Window; color: WindowText; font-size: 12px; font-family: Helvetica, Arial, sans-serif; }
ol, ul { list-style: none; }
table { border-collapse: separate; border-spacing: 0px; }
caption, th, td { text-align: left; font-weight: normal; }
#mail { height: 10em; overflow: auto; background: ThreeDHighlight; border-bottom: 1px solid ButtonFace; }
#mail table { width: 100%; }
#mail table thead tr { background: ButtonHighlight; color: ButtonText; }
#mail table thead tr th { padding: .25em; font-weight: bold; }
#mail table tbody tr:nth-child(even) { background: ButtonHighlight; color: ButtonText; }
#mail table tbody tr.selected { background: Highlight; color: HighlightText; }
#mail table tbody tr td { padding: .25em; }
#message .metadata { padding: 1em; }
#message .metadata div { padding: .25em;; }
#message .metadata div label { display: inline-block; width: 8em; text-align: right; font-weight: bold; }
#message .metadata .attachments { display: none; }
#message .metadata .attachments ul { display: inline; }
#message .metadata .attachments ul li { display: inline-block; margin-right: .5em; }
#message .formats ul { border-bottom: 1px solid WindowFrame; padding: 0 .5em; }
#message .formats ul li { display: inline-block; padding: .5em; border: solid WindowFrame; background: ButtonFace; color: ButtonText; border-width: 1px 1px 0 1px; }
#message .formats ul li.selected { background: ThreeDHighlight; color: WindowText; height: 13px; margin-bottom: -1px; }
#message iframe { width: 100%; height: 42em; background: ThreeDHighlight; }

View File

@ -2,109 +2,11 @@
%html
%head
%title MailCatcher
%script{:src => "http://code.jquery.com/jquery-1.4.3.min.js"}
%link{:rel => "stylesheet", :href => "/stylesheets/application.css"}
%script{:src => "/javascripts/jquery.js"}
%script{:src => "/javascripts/application.js"}
:javascript
var MailCatcher = {
refresh: function() {
console.log('Refreshing mail');
$.getJSON('/mail', function(mail) {
$.each(mail, function(i, message) {
var row = $('<tr />').attr('data-message-id', message.id.toString());
$.each(['sender', 'recipients', 'subject', 'created_at'], function (i, property) {
row.append($('<td />').text(message[property]));
});
$('#mail tbody').append(row);
});
});
},
load: function(id) {
var id = id || $('#mail tr.selected').attr('data-message-id');
if (id !== null) {
console.log('Loading message', id);
$('#mail tbody tr:not([data-message-id="'+id+'"])').removeClass('selected');
$('#mail tbody tr[data-message-id="'+id+'"]').addClass('selected');
$.getJSON('/mail/' + id + '.json', function(message) {
$('#message .received span').text(message.created_at);
$('#message .from span').text(message.sender);
$('#message .to span').text(message.recipients);
$('#message .subject span').text(message.subject);
$('#message .formats ul li').each(function(i, el) {
var $el = $(el),
format = $el.attr('data-message-format');
if ($.inArray(format, message.formats) >= 0) {
$el.show();
} else {
$el.hide();
}
})
if ($("#message .formats ul li.selected:not(:visible)")) {
$("#message .formats ul li.selected").removeClass("selected");
$("#message .formats ul li:visible:first").addClass("selected");
}
if (message.attachments.length > 0) {
console.log(message.attachments);
$('#message .attachments ul').empty();
$.each(message.attachments, function (i, attachment) {
$('#message .attachments ul').append($('<li>').append($('<a>').attr('href', attachment['href']).addClass(attachment['type'].split('/', 1)[0]).addClass(attachment['type'].replace('/', '-')).text(attachment['filename'])));
});
$('#message .attachments').show();
} else {
$('#message .attachments').hide();
}
MailCatcher.loadBody();
});
}
},
loadBody: function(id, format) {
var id = id || $('#mail tr.selected').attr('data-message-id');
var format = format || $('#message .formats ul li.selected').first().attr('data-message-format') || 'html';
$('#message .formats ul li[data-message-format="'+format+'"]').addClass('selected');
$('#message .formats ul li:not([data-message-format="'+format+'"])').removeClass('selected');
if (id != undefined && id !== null) {
console.log('Loading message', id, 'in format', format);
$('#message iframe').attr('src', '/mail/' + id + '.' + format);
}
}
};
$('#mail tr').live('click', function() {
MailCatcher.load($(this).attr('data-message-id'));
});
$('#message .formats ul li').live('click', function() {
MailCatcher.loadBody($('#mail tr.selected').attr('data-message-id'), $(this).attr('data-message-format'));
});
$(MailCatcher.refresh);
:css
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, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td { margin:0px; padding:0px; border:0px; outline:0px; font-weight:inherit; font-style:inherit; font-size:100%; font-family:inherit; vertical-align:baseline; }
body { line-height:1; color:black; background:white; font-size:12px; font-family:Helvetica, Arial, sans-serif; }
ol, ul { list-style:none; }
table { border-collapse:separate; border-spacing:0px; }
caption, th, td { text-align:left; font-weight:normal; }
#mail { height: 10em; overflow: scroll; }
#mail table { width: 100%; }
#mail table thead tr { background: #ccc; }
#mail table thead tr th { padding: .25em; font-weight: bold; }
#mail table tbody tr:nth-child(even) { background: #eee; }
#mail table tbody tr.selected { background: Highlight; color: HighlightText; }
#mail table tbody tr td { padding: .25em; }
#message .metadata { padding: 1em; background: #ccc; }
#message .metadata div { padding: .25em;; }
#message .metadata div label { display: inline-block; width: 8em; text-align: right; font-weight: bold; }
#message .metadata .attachments { display: none; }
#message .metadata .attachments ul { display: inline; }
#message .metadata .attachments ul li { display: inline-block; }
#message .formats ul { background: #ccc; border-bottom: 1px solid #666; padding: 0 .5em; }
#message .formats ul li { display: inline-block; padding: .5em; border: solid #666; border-width: 1px 1px 0 1px; }
#message .formats ul li.selected { background: white; height: 13px; margin-bottom: -1px; }
#message iframe { width: 100%; height: 42em; }
$(MailCatcher.init);
%body
#mail
%table
@ -134,6 +36,6 @@
.formats
%ul
%li.selected{'data-message-format' => 'html'} HTML
%li{'data-message-format' => 'txt'} Plain Text
%li{'data-message-format' => 'eml'} Source
%li{'data-message-format' => 'plain'} Plain Text
%li{'data-message-format' => 'source'} Source
%iframe