mailcatcher 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (6) hide show
  1. data/LICENSE +19 -0
  2. data/README.md +40 -0
  3. data/bin/mailcatcher +28 -0
  4. data/lib/mail_catcher.rb +214 -0
  5. data/views/index.haml +139 -0
  6. metadata +158 -0
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2010 Samuel Cochran <sj26@sj26.com>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # MailCatcher
2
+
3
+ Catches mail and serves it through a dream.
4
+
5
+ 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 that's arrived so far.
6
+
7
+ ## Features
8
+
9
+ * Catches all mail and stores it for display.
10
+ * Shows HTML, Plain Text and Source version of messages, as applicable.
11
+ * Rewrites HTML enabling display of embedded, inline images/etc. (currently very basic)
12
+ * Lists attachments and allows separate downloading of parts.
13
+ * Written super-simply in EventMachine, easy to dig in and change.
14
+
15
+ ## Caveats
16
+
17
+ * For now you need to refresh the page to see new mail. Websockets support is coming soon to show new mail immediately.
18
+ * Mail proccessing is fairly basic but easily modified. If something doesn't work for you, fork and fix it or file an issue and let me know. Include the whole message you're having problems with.
19
+ * The interface is very basic and has not been tested on many browsers yet.
20
+
21
+ ## TODO
22
+
23
+ * Command line options.
24
+ * Websockets for immediate mail viewing.
25
+ * Download link to view original message in mail client.
26
+ * Growl support.
27
+ * Test suite.
28
+ * Better organisation.
29
+ * Better interface. SproutCore?
30
+ * Add mail delivery on request, optionally multiple times.
31
+ * Forward mail to rendering service, maybe CampaignMonitor?
32
+ * Package as an app? Native interfaces?
33
+
34
+ ## License
35
+
36
+ MailCatcher is released under the MIT License. See LICENSE.
37
+
38
+ ## Dreams
39
+
40
+ For dream catching, try [this](http://goo.gl/kgbh).
data/bin/mailcatcher ADDED
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $: << File.expand_path(File.join(File.dirname(__FILE__), '../lib'))
4
+
5
+ require 'mail_catcher'
6
+
7
+ require 'optparse'
8
+
9
+ options = {}
10
+
11
+ OptionParser.new do |opts|
12
+ opts.banner = 'Usage: mailcatcher [options]'
13
+
14
+ options[:verbose] = false
15
+ opts.on('-v', '--verbose', 'Be more verbose') do
16
+ options[:verbose] = true
17
+ end
18
+
19
+ opts.on('-h', '--help', 'Display this help information') do
20
+ puts opts
21
+ exit
22
+ end
23
+ end.parse!
24
+
25
+ puts 'Starting mail catcher'
26
+ puts '==> smtp://127.0.0.1:1025'
27
+ puts '==> http://127.0.0.1:1080'
28
+ MailCatcher.run
@@ -0,0 +1,214 @@
1
+ require 'rubygems'
2
+ require 'eventmachine'
3
+ require 'json'
4
+ require 'ostruct'
5
+ require 'mail'
6
+ require 'thin'
7
+ require 'sqlite3'
8
+ require 'sinatra/base'
9
+ require 'sunshowers'
10
+
11
+ # Monkey-patch Sinatra to use Sunshowers
12
+ class Sinatra::Request < Rack::Request
13
+ include Sunshowers::WebSocket
14
+ end
15
+
16
+ module MailCatcher
17
+ def self.db
18
+ @@__db ||= begin
19
+ SQLite3::Database.new(':memory:', :results_as_hash => true, :type_translation => true).tap do |db|
20
+ begin
21
+ db.execute(<<-SQL)
22
+ CREATE TABLE mail (
23
+ id INTEGER PRIMARY KEY ASC,
24
+ sender TEXT,
25
+ recipients TEXT,
26
+ subject TEXT,
27
+ source BLOB,
28
+ size TEXT,
29
+ created_at DATETIME DEFAULT CURRENT_DATETIME
30
+ )
31
+ SQL
32
+ db.execute(<<-SQL)
33
+ CREATE TABLE part (
34
+ id INTEGER PRIMARY KEY ASC,
35
+ mail_id INTEGER NOT NULL,
36
+ cid TEXT,
37
+ type TEXT,
38
+ is_attachment INTEGER,
39
+ filename TEXT,
40
+ body BLOB,
41
+ size INTEGER,
42
+ created_at DATETIME DEFAULT CURRENT_DATETIME
43
+ )
44
+ SQL
45
+ rescue SQLite3::SQLException
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ def self.subscribers
52
+ @@subscribers ||= []
53
+ end
54
+
55
+ class SmtpServer < EventMachine::Protocols::SmtpServer
56
+ def insert_message
57
+ @@insert_message ||= MailCatcher.db.prepare("INSERT INTO mail (sender, recipients, subject, source, size, created_at) VALUES (?, ?, ?, ?, ?, datetime('now'))")
58
+ end
59
+
60
+ def insert_part
61
+ @@insert_part ||= MailCatcher.db.prepare("INSERT INTO part (mail_id, cid, type, is_attachment, filename, body, size, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'))")
62
+ end
63
+
64
+ def current_message
65
+ @current_message ||= OpenStruct.new
66
+ end
67
+
68
+ def receive_reset
69
+ @current_message = nil
70
+ true
71
+ end
72
+
73
+ def receive_sender(sender)
74
+ current_message.sender = sender
75
+ true
76
+ end
77
+
78
+ def receive_recipient(recipient)
79
+ current_message.recipients ||= []
80
+ current_message.recipients << recipient
81
+ true
82
+ end
83
+
84
+ def receive_data_chunk(lines)
85
+ current_message.source ||= ""
86
+ current_message.source += lines.join("\n")
87
+ true
88
+ end
89
+
90
+ def receive_message
91
+ mail = Mail.new(current_message.source)
92
+ result = insert_message.execute(current_message.sender, current_message.recipients.inspect, mail.subject, current_message.source, current_message.source.length)
93
+ mail_id = MailCatcher.db.last_insert_row_id
94
+ if mail.multipart?
95
+ mail.all_parts.each do |part|
96
+ body = part.body.to_s
97
+ insert_part.execute(mail_id, part.cid, part.mime_type, part.attachment? ? 1 : 0, part.filename, body, body.length)
98
+ end
99
+ else
100
+ body = mail.body.to_s
101
+ insert_part.execute(mail_id, nil, mail.mime_type, 0, mail.filename, body, body.length)
102
+ end
103
+ puts "==> SMTP: Received message '#{mail.subject}' from '#{current_message.sender}'"
104
+ @current_message = nil
105
+ true
106
+ end
107
+ end
108
+
109
+ class WebApp < Sinatra::Base
110
+ set :views, File.expand_path(File.join(File.dirname(__FILE__), '..', 'views'))
111
+ set :haml, {:format => :html5 }
112
+
113
+ get '/' do
114
+ haml :index
115
+ end
116
+
117
+ get '/mail' do
118
+ if latest = MailCatcher.db.query('SELECT created_at FROM mail ORDER BY created_at DESC LIMIT 1').next
119
+ last_modified latest["created_at"]
120
+ end
121
+ MailCatcher.db.query('SELECT id, sender, recipients, subject, size, created_at FROM mail ORDER BY created_at DESC').to_a.to_json
122
+ end
123
+
124
+ get '/mail/:id.json' do
125
+ mail_id = params[:id].to_i
126
+ message = MailCatcher.db.query('SELECT * FROM mail WHERE id = ? LIMIT 1', mail_id).to_a.first
127
+ if message
128
+ last_modified message["created_at"]
129
+ message["formats"] = ['eml']
130
+ message["formats"] << 'html' if MailCatcher.db.query('SELECT id FROM part WHERE mail_id = ? AND type = "text/html" LIMIT 1', mail_id).next
131
+ message["formats"] << 'txt' if MailCatcher.db.query('SELECT id FROM part WHERE mail_id = ? AND type = "text/plain" LIMIT 1', mail_id).next
132
+ 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|
133
+ attachment.merge({"href" => "/mail/#{escape(params[:id])}/#{escape(attachment['cid'])}"})
134
+ end
135
+ message.to_json
136
+ else
137
+ not_found
138
+ end
139
+ end
140
+
141
+ get '/mail/:id.html' do
142
+ mail_id = params[:id].to_i
143
+ part = MailCatcher.db.query('SELECT body, created_at FROM part WHERE mail_id = ? AND type = "text/html" LIMIT 1', mail_id).to_a.first
144
+ if part
145
+ content_type 'text/html'
146
+ last_modified part["created_at"]
147
+ part["body"].gsub(/cid:([^'"> ]+)/, "#{mail_id}/\\1")
148
+ else
149
+ not_found
150
+ end
151
+ end
152
+
153
+ get '/mail/:id.txt' do
154
+ 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
155
+ if part
156
+ content_type 'text/plain'
157
+ last_modified part["created_at"]
158
+ part["body"]
159
+ else
160
+ not_found
161
+ end
162
+ end
163
+
164
+ get '/mail/:id.eml' do
165
+ content_type 'text/plain'
166
+ 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
167
+ if message
168
+ last_modified message["created_at"]
169
+ message["source"]
170
+ else
171
+ not_found
172
+ end
173
+ end
174
+
175
+ get '/mail/:id/:cid' do
176
+ result = MailCatcher.db.query('SELECT * FROM part WHERE mail_id = ?', params[:id].to_i)
177
+ part = result.find { |part| part["cid"] == params[:cid] }
178
+ if part
179
+ content_type part["type"]
180
+ attachment part["filename"] if part["is_attachment"] == 1
181
+ last_modified part["created_at"]
182
+ body part["body"].to_s
183
+ else
184
+ not_found
185
+ end
186
+ end
187
+
188
+ get '/mail/subscribe' do
189
+ return head 400 unless request.ws?
190
+
191
+ request.ws_handshake!
192
+ request.ws_io.each do |message|
193
+ ws_quit! if message == "goodbye"
194
+ end
195
+ end
196
+
197
+ not_found do
198
+ "<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>"
199
+ end
200
+ end
201
+
202
+ def self.run(options = {})
203
+ options[:smtp_ip] ||= '127.0.0.1'
204
+ options[:smtp_port] ||= 1025
205
+ options[:http_ip] ||= '127.0.0.1'
206
+ options[:http_port] ||= 1080
207
+
208
+ Thin::Logging.silent = true
209
+ EM::run do
210
+ EM::start_server options[:smtp_ip], options[:smtp_port], SmtpServer
211
+ Thin::Server.start WebApp, options[:http_ip], options[:http_port]
212
+ end
213
+ end
214
+ end
data/views/index.haml ADDED
@@ -0,0 +1,139 @@
1
+ !!!
2
+ %html
3
+ %head
4
+ %title MailCatcher
5
+ %script{:src => "http://code.jquery.com/jquery-1.4.3.min.js"}
6
+ :javascript
7
+ var MailCatcher = {
8
+ refresh: function() {
9
+ console.log('Refreshing mail');
10
+ $.getJSON('/mail', function(mail) {
11
+ $.each(mail, function(i, message) {
12
+ var row = $('<tr />').attr('data-message-id', message.id.toString());
13
+ $.each(['sender', 'recipients', 'subject', 'created_at'], function (i, property) {
14
+ row.append($('<td />').text(message[property]));
15
+ });
16
+ $('#mail tbody').append(row);
17
+ });
18
+ });
19
+ },
20
+ load: function(id) {
21
+ var id = id || $('#mail tr.selected').attr('data-message-id');
22
+
23
+ if (id !== null) {
24
+ console.log('Loading message', id);
25
+
26
+ $('#mail tbody tr:not([data-message-id="'+id+'"])').removeClass('selected');
27
+ $('#mail tbody tr[data-message-id="'+id+'"]').addClass('selected');
28
+ $.getJSON('/mail/' + id + '.json', function(message) {
29
+ $('#message .received span').text(message.created_at);
30
+ $('#message .from span').text(message.sender);
31
+ $('#message .to span').text(message.recipients);
32
+ $('#message .subject span').text(message.subject);
33
+ $('#message .formats ul li').each(function(i, el) {
34
+ var $el = $(el),
35
+ format = $el.attr('data-message-format');
36
+ if ($.inArray(format, message.formats) >= 0) {
37
+ $el.show();
38
+ } else {
39
+ $el.hide();
40
+ }
41
+ })
42
+ if ($("#message .formats ul li.selected:not(:visible)")) {
43
+ $("#message .formats ul li.selected").removeClass("selected");
44
+ $("#message .formats ul li:visible:first").addClass("selected");
45
+ }
46
+ if (message.attachments.length > 0) {
47
+ console.log(message.attachments);
48
+ $('#message .attachments ul').empty();
49
+ $.each(message.attachments, function (i, attachment) {
50
+ $('#message .attachments ul').append($('<li>').append($('<a>').attr('href', attachment['href']).addClass(attachment['type'].split('/', 1)[0]).addClass(attachment['type'].replace('/', '-')).text(attachment['filename'])));
51
+ });
52
+ $('#message .attachments').show();
53
+ } else {
54
+ $('#message .attachments').hide();
55
+ }
56
+ MailCatcher.loadBody();
57
+ });
58
+ }
59
+ },
60
+ loadBody: function(id, format) {
61
+ var id = id || $('#mail tr.selected').attr('data-message-id');
62
+ var format = format || $('#message .formats ul li.selected').first().attr('data-message-format') || 'html';
63
+
64
+ $('#message .formats ul li[data-message-format="'+format+'"]').addClass('selected');
65
+ $('#message .formats ul li:not([data-message-format="'+format+'"])').removeClass('selected');
66
+
67
+ if (id != undefined && id !== null) {
68
+ console.log('Loading message', id, 'in format', format);
69
+
70
+ $('#message iframe').attr('src', '/mail/' + id + '.' + format);
71
+ }
72
+ }
73
+ };
74
+
75
+ $('#mail tr').live('click', function() {
76
+ MailCatcher.load($(this).attr('data-message-id'));
77
+ });
78
+
79
+ $('#message .formats ul li').live('click', function() {
80
+ MailCatcher.loadBody($('#mail tr.selected').attr('data-message-id'), $(this).attr('data-message-format'));
81
+ });
82
+
83
+ $(MailCatcher.refresh);
84
+ :css
85
+ 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; }
86
+ body { line-height:1; color:black; background:white; font-size:12px; font-family:Helvetica, Arial, sans-serif; }
87
+ ol, ul { list-style:none; }
88
+ table { border-collapse:separate; border-spacing:0px; }
89
+ caption, th, td { text-align:left; font-weight:normal; }
90
+
91
+ #mail { height: 10em; overflow: scroll; }
92
+ #mail table { width: 100%; }
93
+ #mail table thead tr { background: #ccc; }
94
+ #mail table thead tr th { padding: .25em; font-weight: bold; }
95
+ #mail table tbody tr:nth-child(even) { background: #eee; }
96
+ #mail table tbody tr.selected { background: Highlight; color: HighlightText; }
97
+ #mail table tbody tr td { padding: .25em; }
98
+ #message .metadata { padding: 1em; background: #ccc; }
99
+ #message .metadata div { padding: .25em;; }
100
+ #message .metadata div label { display: inline-block; width: 8em; text-align: right; font-weight: bold; }
101
+ #message .metadata .attachments { display: none; }
102
+ #message .metadata .attachments ul { display: inline; }
103
+ #message .metadata .attachments ul li { display: inline-block; }
104
+ #message .formats ul { background: #ccc; border-bottom: 1px solid #666; padding: 0 .5em; }
105
+ #message .formats ul li { display: inline-block; padding: .5em; border: solid #666; border-width: 1px 1px 0 1px; }
106
+ #message .formats ul li.selected { background: white; height: 13px; margin-bottom: -1px; }
107
+ #message iframe { width: 100%; height: 42em; }
108
+ %body
109
+ #mail
110
+ %table
111
+ %thead
112
+ %th From
113
+ %th To
114
+ %th Subject
115
+ %th Received
116
+ %tbody
117
+ #message
118
+ .metadata
119
+ .received
120
+ %label Received
121
+ %span
122
+ .from
123
+ %label From
124
+ %span
125
+ .to
126
+ %label To
127
+ %span
128
+ .subject
129
+ %label Subject
130
+ %span
131
+ .attachments
132
+ %label Attachments
133
+ %ul
134
+ .formats
135
+ %ul
136
+ %li.selected{'data-message-format' => 'html'} HTML
137
+ %li{'data-message-format' => 'txt'} Plain Text
138
+ %li{'data-message-format' => 'eml'} Source
139
+ %iframe
metadata ADDED
@@ -0,0 +1,158 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mailcatcher
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 1
9
+ version: 0.1.1
10
+ platform: ruby
11
+ authors:
12
+ - Samuel Cochran
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-10-25 00:00:00 +08:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: eventmachine
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ segments:
29
+ - 0
30
+ version: "0"
31
+ type: :runtime
32
+ version_requirements: *id001
33
+ - !ruby/object:Gem::Dependency
34
+ name: json
35
+ prerelease: false
36
+ requirement: &id002 !ruby/object:Gem::Requirement
37
+ none: false
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ segments:
42
+ - 0
43
+ version: "0"
44
+ type: :runtime
45
+ version_requirements: *id002
46
+ - !ruby/object:Gem::Dependency
47
+ name: mail
48
+ prerelease: false
49
+ requirement: &id003 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ segments:
55
+ - 0
56
+ version: "0"
57
+ type: :runtime
58
+ version_requirements: *id003
59
+ - !ruby/object:Gem::Dependency
60
+ name: thin
61
+ prerelease: false
62
+ requirement: &id004 !ruby/object:Gem::Requirement
63
+ none: false
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ segments:
68
+ - 0
69
+ version: "0"
70
+ type: :runtime
71
+ version_requirements: *id004
72
+ - !ruby/object:Gem::Dependency
73
+ name: sqlite3-ruby
74
+ prerelease: false
75
+ requirement: &id005 !ruby/object:Gem::Requirement
76
+ none: false
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ segments:
81
+ - 0
82
+ version: "0"
83
+ type: :runtime
84
+ version_requirements: *id005
85
+ - !ruby/object:Gem::Dependency
86
+ name: sinatra
87
+ prerelease: false
88
+ requirement: &id006 !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ segments:
94
+ - 0
95
+ version: "0"
96
+ type: :runtime
97
+ version_requirements: *id006
98
+ - !ruby/object:Gem::Dependency
99
+ name: sunshowers
100
+ prerelease: false
101
+ requirement: &id007 !ruby/object:Gem::Requirement
102
+ none: false
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ segments:
107
+ - 0
108
+ version: "0"
109
+ type: :runtime
110
+ version_requirements: *id007
111
+ description: " MailCatcher runs a super simple SMTP server which catches any\n message sent to it to display in a web interface. Run\n mailcatcher, set your favourite app to deliver to\n smtp://127.0.0.1:1025 instead of your default SMTP server,\n then check out http://127.0.0.1:1080 to see the mail.\n"
112
+ email: sj26@sj26.com
113
+ executables:
114
+ - mailcatcher
115
+ extensions: []
116
+
117
+ extra_rdoc_files: []
118
+
119
+ files:
120
+ - bin/mailcatcher
121
+ - lib/mail_catcher.rb
122
+ - views/index.haml
123
+ - README.md
124
+ - LICENSE
125
+ has_rdoc: true
126
+ homepage: http://github.com/sj26/mailcatcher
127
+ licenses: []
128
+
129
+ post_install_message:
130
+ rdoc_options: []
131
+
132
+ require_paths:
133
+ - lib
134
+ required_ruby_version: !ruby/object:Gem::Requirement
135
+ none: false
136
+ requirements:
137
+ - - ">="
138
+ - !ruby/object:Gem::Version
139
+ segments:
140
+ - 0
141
+ version: "0"
142
+ required_rubygems_version: !ruby/object:Gem::Requirement
143
+ none: false
144
+ requirements:
145
+ - - ">="
146
+ - !ruby/object:Gem::Version
147
+ segments:
148
+ - 0
149
+ version: "0"
150
+ requirements: []
151
+
152
+ rubyforge_project:
153
+ rubygems_version: 1.3.7
154
+ signing_key:
155
+ specification_version: 3
156
+ summary: Runs an SMTP server, catches and displays email in a web interface.
157
+ test_files: []
158
+