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:
parent
04e727a8cd
commit
8db4f674f4
|
@ -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
33
LICENSE
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,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
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
require 'eventmachine'
|
||||
|
||||
module MailCatcher
|
||||
module Events
|
||||
MessageAdded = EventMachine::Channel.new
|
||||
end
|
||||
end
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
};
|
File diff suppressed because it is too large
Load Diff
|
@ -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; }
|
110
views/index.haml
110
views/index.haml
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue