mirror of
https://github.com/moparisthebest/mailcatcher
synced 2024-12-22 07:18:53 -05:00
First cut of mailcatcher
This commit is contained in:
commit
0fb61d040b
10
bin/mailcatcher
Executable file
10
bin/mailcatcher
Executable file
@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env ruby
|
||||
|
||||
$: << File.expand_path(File.join(File.dirname(__FILE__), '../lib'))
|
||||
|
||||
require 'mail_catcher'
|
||||
|
||||
puts 'Starting mail catcher'
|
||||
puts '==> smtp://0.0.0.0:1025'
|
||||
puts '==> http://0.0.0.0:1080'
|
||||
MailCatcher.run
|
209
lib/mail_catcher.rb
Normal file
209
lib/mail_catcher.rb
Normal file
@ -0,0 +1,209 @@
|
||||
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
|
||||
|
||||
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
|
||||
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}'"
|
||||
@current_message = nil
|
||||
true
|
||||
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
|
||||
Thin::Logging.silent = true
|
||||
EM::run do
|
||||
EM::start_server '127.0.0.1', 1025, SmtpServer
|
||||
Thin::Server.start WebApp, '127.0.0.1', 1080
|
||||
end
|
||||
end
|
||||
end
|
139
views/index.haml
Normal file
139
views/index.haml
Normal file
@ -0,0 +1,139 @@
|
||||
!!!
|
||||
%html
|
||||
%head
|
||||
%title MailCatcher
|
||||
%script{:src => "http://ipv.local/javascripts/jquery-1.4.2.min.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; }
|
||||
%body
|
||||
#mail
|
||||
%table
|
||||
%thead
|
||||
%th From
|
||||
%th To
|
||||
%th Subject
|
||||
%th Received
|
||||
%tbody
|
||||
#message
|
||||
.metadata
|
||||
.received
|
||||
%label Received
|
||||
%span
|
||||
.from
|
||||
%label From
|
||||
%span
|
||||
.to
|
||||
%label To
|
||||
%span
|
||||
.subject
|
||||
%label Subject
|
||||
%span
|
||||
.attachments
|
||||
%label Attachments
|
||||
%ul
|
||||
.formats
|
||||
%ul
|
||||
%li.selected{'data-message-format' => 'html'} HTML
|
||||
%li{'data-message-format' => 'txt'} Plain Text
|
||||
%li{'data-message-format' => 'eml'} Source
|
||||
%iframe
|
Loading…
Reference in New Issue
Block a user