mailcatcher-cors 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ ZTk0NzJhNDJkODY2YWZlZjU4ZjQwZGZlNTgyNjM3ODdmMDY3ZmQ3OA==
5
+ data.tar.gz: !binary |-
6
+ N2RlYTg1NzgwM2U0Y2JiZWM2NzE3ZGYxMzMwMzkxNDQyMWFlMTFmNQ==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ MDQ3MmU0YWY3NDY0M2VmODEyYjIxMmFkZjBkMGEzNmI3ZTE1MTZlYzhmYjdk
10
+ ZjVkYjY4ZjcxOTc3MzZlNjRhMzlhN2I5NDRmOGI5ZDA2MGZmMDhmMGQyMDM4
11
+ YTA3OGU5NjZmNjVjYmE4NDYwMmJhMTk5MTQwZWVmMjkwMmRhNDM=
12
+ data.tar.gz: !binary |-
13
+ MTQ5NjAxZjIzM2JlOWY4ZGIwMmRkZTU0ODgxYWY5YWEwYWEzZWM5Mzg2ZTcy
14
+ OTY0YTA3MWYzOTk3ZmQ5ODRmY2UyMWIzOGM0NTI4NTBmNGZkMzM1ZjZmZjhi
15
+ MjQwNzAzN2ZmYTc3ZTYyOWUwMGZlODAzYWI2NmNmNTI1NWRmYTY=
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010-2011 Samuel Cochran
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,120 @@
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
+ ![MailCatcher screenshot](http://f.cl.ly/items/3w2T1p0F3g003b2i1F2z/Screen%20shot%202011-06-23%20at%2011.39.03%20PM.png)
8
+
9
+ ## Features
10
+
11
+ * Catches all mail and stores it for display.
12
+ * Shows HTML, Plain Text and Source version of messages, as applicable.
13
+ * Rewrites HTML enabling display of embedded, inline images/etc and open links in a new window.
14
+ * Lists attachments and allows separate downloading of parts.
15
+ * Download original email to view in your native mail client(s).
16
+ * Command line options to override the default SMTP/HTTP IP and port settings.
17
+ * Mail appears instantly if your browser supports [WebSockets][websockets], otherwise updates every thirty seconds.
18
+ * Runs as a daemon run in the background.
19
+ * Sendmail-analogue command, `catchmail`, makes [using mailcatcher from PHP][withphp] a lot easier.
20
+ * Written super-simply in EventMachine, easy to dig in and change.
21
+ * Keyboard navigation between messages
22
+
23
+ ## How
24
+
25
+ 1. `gem install mailcatcher`
26
+ 2. `mailcatcher`
27
+ 3. Go to http://localhost:1080/
28
+ 4. Send mail through smtp://localhost:1025
29
+
30
+ Use `mailcatcher --help` to see the command line options. The brave can get the source from [the GitHub repository][mailcatcher-github].
31
+
32
+ ### Bundler
33
+
34
+ Please don't put mailcatcher into your Gemfile. It will conflict with your applications gems at some point.
35
+
36
+ Instead, pop a note in your README stating you use mailcatcher. Simply run `gem install mailcatcher` then `mailcatcher` to get started.
37
+
38
+ ### RVM
39
+
40
+ Under RVM your mailcatcher command may only be available under the ruby you install mailcatcher into. To prevent this, and to prevent gem conflicts, install mailcatcher into a dedicated gemset and create wrapper scripts:
41
+
42
+ rvm default@mailcatcher --create do gem install mailcatcher
43
+ rvm wrapper default@mailcatcher --no-prefix mailcatcher catchmail
44
+
45
+ ### Rails
46
+
47
+ To set up your rails app, I recommend adding this to your `environments/development.rb`:
48
+
49
+ config.action_mailer.delivery_method = :smtp
50
+ config.action_mailer.smtp_settings = { :address => "localhost", :port => 1025 }
51
+
52
+ ### PHP
53
+
54
+ For projects using PHP, or PHP frameworks and application platforms like Drupal, you can set [PHP's mail configuration](http://www.php.net/manual/en/mail.configuration.php) in your [php.ini](http://www.php.net/manual/en/configuration.file.php) to send via MailCatcher with:
55
+
56
+ sendmail_path = /usr/bin/env catchmail -f some@from.address
57
+
58
+ You can do this in your [Apache configuration](http://php.net/manual/en/configuration.changes.php) like so:
59
+
60
+ php_admin_value sendmail_path "/usr/bin/env catchmail -f some@from.address"
61
+
62
+ If you've installed via RVM this probably won't work unless you've manually added your RVM bin paths to your system environment's PATH. In that case, run `which catchmail` and put that path into the `sendmail_path` directive above instead of `/usr/bin/env catchmail`.
63
+
64
+ If starting `mailcatcher` on alternative SMTP IP and/or port with parameters like `--smtp-ip 192.168.0.1 --smtp-port 10025`, add the same parameters to your `catchmail` command:
65
+
66
+ sendmail_path = /usr/bin/env catchmail --smtp-ip 192.160.0.1 --smtp-port 10025 -f some@from.address
67
+
68
+ ### Django
69
+
70
+ For use in Django, simply add the following configuration to your projects' settings.py
71
+
72
+ ```python
73
+ if DEBUG:
74
+ EMAIL_HOST = '127.0.0.1'
75
+ EMAIL_HOST_USER = ''
76
+ EMAIL_HOST_PASSWORD = ''
77
+ EMAIL_PORT = 1025
78
+ EMAIL_USE_TLS = False
79
+ ```
80
+
81
+ ### API
82
+
83
+ A fairly RESTful URL schema means you can download a list of messages in JSON from `/messages`, each message's metadata with `/messages/:id.json`, and then the pertinent parts with `/messages/:id.html` and `/messages/:id.plain` for the default HTML and plain text version, `/messages/:id/:cid` for individual attachments by CID, or the whole message with `/messages/:id.source`.
84
+
85
+ ## Caveats
86
+
87
+ * Mail processing is fairly basic but easily modified. If something doesn't work for you, fork and fix it or [file an issue][mailcatcher-issues] and let me know. Include the whole message you're having problems with.
88
+ * The interface is very basic and has not been tested on many browsers yet.
89
+
90
+ ## TODO
91
+
92
+ * Add mail delivery on request, optionally multiple times.
93
+ * Compatibility testing against CampaignMonitor's [design guidelines](http://www.campaignmonitor.com/design-guidelines/) and [CSS support matrix](http://www.campaignmonitor.com/css/).
94
+ * Forward mail to rendering service, maybe CampaignMonitor?
95
+
96
+ ## Thanks
97
+
98
+ 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.
99
+
100
+ Thanks also to [The Frontier Group][tfg] for giving me the idea, being great guinea pigs and letting me steal pieces of time to keep the project alive.
101
+
102
+ ## Donations
103
+
104
+ I work on MailCatcher mostly in my own spare time. If you've found Mailcatcher useful and would like to help feed me and fund continued development and new features, please [donate via PayPal][donate]. If you'd like a specific feature added to MailCatcher and are willing to pay for it, please [email me](mailto:sj26@sj26.com).
105
+
106
+ ## License
107
+
108
+ Copyright © 2010-2011 Samuel Cochran (sj26@sj26.com). Released under the MIT License, see [LICENSE][license] for details.
109
+
110
+ ## Dreams
111
+
112
+ For dream catching, try [this](http://goo.gl/kgbh). OR [THIS](http://www.nyanicorn.com), OMG.
113
+
114
+ [donate]: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=522WUPLRWUSKE
115
+ [license]: https://github.com/sj26/mailcatcher/blob/master/LICENSE
116
+ [mailcatcher-github]: https://github.com/sj26/mailcatcher
117
+ [mailcatcher-issues]: https://github.com/sj26/mailcatcher/issues
118
+ [tfg]: http://www.thefrontiergroup.com.au
119
+ [websockets]: http://www.whatwg.org/specs/web-socket-protocol/
120
+ [withphp]: http://webschuur.com/publications/blogs/2011-05-29-catchmail_for_drupal_and_other_phpapplications_the_simple_version
data/bin/catchmail ADDED
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ begin
4
+ require 'mail'
5
+ rescue LoadError
6
+ require 'rubygems'
7
+ require 'mail'
8
+ end
9
+
10
+ require 'optparse'
11
+
12
+ options = {:smtp_ip => '127.0.0.1', :smtp_port => 1025}
13
+
14
+ OptionParser.new do |parser|
15
+ parser.banner = <<-BANNER.gsub /^ +/, ""
16
+ Usage: catchmail [options] [recipient ...]
17
+ sendmail-like interface to forward mail to MailCatcher.
18
+ BANNER
19
+
20
+ parser.on('--ip IP') do |ip|
21
+ options[:smtp_ip] = ip
22
+ end
23
+
24
+ parser.on('--smtp-ip IP', 'Set the ip address of the smtp server') do |ip|
25
+ options[:smtp_ip] = ip
26
+ end
27
+
28
+ parser.on('--smtp-port PORT', Integer, 'Set the port of the smtp server') do |port|
29
+ options[:smtp_port] = port
30
+ end
31
+
32
+ parser.on('-f FROM', 'Set the sending address') do |from|
33
+ options[:from] = from
34
+ end
35
+
36
+ parser.on('-oi', 'Ignored option -oi') do |ignored|
37
+ end
38
+ parser.on('-t', 'Ignored option -t') do |ignored|
39
+ end
40
+ parser.on('-q', 'Ignored option -q') do |ignored|
41
+ end
42
+
43
+ parser.on('-x', '--no-exit', 'Can\'t exit from the application') do
44
+ options[:no_exit] = true
45
+ end
46
+
47
+ parser.on('-h', '--help', 'Display this help information') do
48
+ puts parser
49
+ exit!
50
+ end
51
+ end.parse!
52
+
53
+ Mail.defaults do
54
+ delivery_method :smtp,
55
+ :address => options[:smtp_ip],
56
+ :port => options[:smtp_port]
57
+ end
58
+
59
+ message = Mail.new($stdin.read)
60
+
61
+ message.return_path = options[:from] if options[:from]
62
+
63
+ ARGV.each do |recipient|
64
+ if message.to.nil?
65
+ message.to = recipient
66
+ else
67
+ message.to << recipient
68
+ end
69
+ end
70
+
71
+ message.deliver
data/bin/mailcatcher ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'mail_catcher'
4
+
5
+ MailCatcher.run!
@@ -0,0 +1,7 @@
1
+ require "eventmachine"
2
+
3
+ module MailCatcher
4
+ module Events
5
+ MessageAdded = EventMachine::Channel.new
6
+ end
7
+ end
@@ -0,0 +1,160 @@
1
+ require "active_support/json"
2
+ require "eventmachine"
3
+ require "mail"
4
+ require "sqlite3"
5
+
6
+ module MailCatcher::Mail extend self
7
+ def db
8
+ @__db ||= begin
9
+ SQLite3::Database.new(":memory:", :type_translation => true).tap do |db|
10
+ db.execute(<<-SQL)
11
+ CREATE TABLE message (
12
+ id INTEGER PRIMARY KEY ASC,
13
+ sender TEXT,
14
+ recipients TEXT,
15
+ subject TEXT,
16
+ source BLOB,
17
+ size TEXT,
18
+ type TEXT,
19
+ created_at DATETIME DEFAULT CURRENT_DATETIME
20
+ )
21
+ SQL
22
+ db.execute(<<-SQL)
23
+ CREATE TABLE message_part (
24
+ id INTEGER PRIMARY KEY ASC,
25
+ message_id INTEGER NOT NULL,
26
+ cid TEXT,
27
+ type TEXT,
28
+ is_attachment INTEGER,
29
+ filename TEXT,
30
+ charset TEXT,
31
+ body BLOB,
32
+ size INTEGER,
33
+ created_at DATETIME DEFAULT CURRENT_DATETIME
34
+ )
35
+ SQL
36
+ end
37
+ end
38
+ end
39
+
40
+ def add_message(message)
41
+ @add_message_query ||= db.prepare("INSERT INTO message (sender, recipients, subject, source, type, size, created_at) VALUES (?, ?, ?, ?, ?, ?, datetime('now'))")
42
+
43
+ mail = Mail.new(message[:source])
44
+ @add_message_query.execute(message[:sender], message[:recipients].to_json, mail.subject, message[:source], mail.mime_type || "text/plain", message[:source].length)
45
+ message_id = db.last_insert_row_id
46
+ parts = mail.all_parts
47
+ parts = [mail] if parts.empty?
48
+ parts.each do |part|
49
+ body = part.body.to_s
50
+ # Only parts have CIDs, not mail
51
+ cid = part.cid if part.respond_to? :cid
52
+ add_message_part(message_id, cid, part.mime_type || "text/plain", part.attachment? ? 1 : 0, part.filename, part.charset, body, body.length)
53
+ end
54
+
55
+ EventMachine.next_tick do
56
+ message = MailCatcher::Mail.message message_id
57
+ MailCatcher::Events::MessageAdded.push message
58
+ end
59
+ end
60
+
61
+ def add_message_part(*args)
62
+ @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'))"
63
+ @add_message_part_query.execute(*args)
64
+ end
65
+
66
+ def latest_created_at
67
+ @latest_created_at_query ||= db.prepare "SELECT created_at FROM message ORDER BY created_at DESC LIMIT 1"
68
+ @latest_created_at_query.execute.next
69
+ end
70
+
71
+ def messages
72
+ @messages_query ||= db.prepare "SELECT id, sender, recipients, subject, size, created_at FROM message ORDER BY created_at, id ASC"
73
+ @messages_query.execute.map do |row|
74
+ Hash[row.fields.zip(row)].tap do |message|
75
+ message["recipients"] &&= ActiveSupport::JSON.decode message["recipients"]
76
+ end
77
+ end
78
+ end
79
+
80
+ def message(id)
81
+ @message_query ||= db.prepare "SELECT * FROM message WHERE id = ? LIMIT 1"
82
+ row = @message_query.execute(id).next
83
+ row && Hash[row.fields.zip(row)].tap do |message|
84
+ message["recipients"] &&= ActiveSupport::JSON.decode message["recipients"]
85
+ end
86
+ end
87
+
88
+ def message_has_html?(id)
89
+ @message_has_html_query ||= db.prepare "SELECT 1 FROM message_part WHERE message_id = ? AND is_attachment = 0 AND type IN ('application/xhtml+xml', 'text/html') LIMIT 1"
90
+ (!!@message_has_html_query.execute(id).next) || ["text/html", "application/xhtml+xml"].include?(message(id)["type"])
91
+ end
92
+
93
+ def message_has_plain?(id)
94
+ @message_has_plain_query ||= db.prepare "SELECT 1 FROM message_part WHERE message_id = ? AND is_attachment = 0 AND type = 'text/plain' LIMIT 1"
95
+ (!!@message_has_plain_query.execute(id).next) || message(id)["type"] == "text/plain"
96
+ end
97
+
98
+ def message_parts(id)
99
+ @message_parts_query ||= db.prepare "SELECT cid, type, filename, size FROM message_part WHERE message_id = ? ORDER BY filename ASC"
100
+ @message_parts_query.execute(id).map do |row|
101
+ Hash[row.fields.zip(row)]
102
+ end
103
+ end
104
+
105
+ def message_attachments(id)
106
+ @message_parts_query ||= db.prepare "SELECT cid, type, filename, size FROM message_part WHERE message_id = ? AND is_attachment = 1 ORDER BY filename ASC"
107
+ @message_parts_query.execute(id).map do |row|
108
+ Hash[row.fields.zip(row)]
109
+ end
110
+ end
111
+
112
+ def message_part(message_id, part_id)
113
+ @message_part_query ||= db.prepare "SELECT * FROM message_part WHERE message_id = ? AND id = ? LIMIT 1"
114
+ row = @message_part_query.execute(message_id, part_id).next
115
+ row && Hash[row.fields.zip(row)]
116
+ end
117
+
118
+ def message_part_type(message_id, part_type)
119
+ @message_part_type_query ||= db.prepare "SELECT * FROM message_part WHERE message_id = ? AND type = ? AND is_attachment = 0 LIMIT 1"
120
+ row = @message_part_type_query.execute(message_id, part_type).next
121
+ row && Hash[row.fields.zip(row)]
122
+ end
123
+
124
+ def message_part_html(message_id)
125
+ part = message_part_type(message_id, "text/html")
126
+ part ||= message_part_type(message_id, "application/xhtml+xml")
127
+ part ||= begin
128
+ message = message(message_id)
129
+ message if message.present? and ["text/html", "application/xhtml+xml"].include? message["type"]
130
+ end
131
+ end
132
+
133
+ def message_part_plain(message_id)
134
+ message_part_type message_id, "text/plain"
135
+ end
136
+
137
+ def message_part_cid(message_id, cid)
138
+ @message_part_cid_query ||= db.prepare "SELECT * FROM message_part WHERE message_id = ?"
139
+ @message_part_cid_query.execute(message_id).map do |row|
140
+ Hash[row.fields.zip(row)]
141
+ end.find do |part|
142
+ part["cid"] == cid
143
+ end
144
+ end
145
+
146
+ def delete!
147
+ @delete_all_messages_query ||= db.prepare "DELETE FROM message"
148
+ @delete_all_message_parts_query ||= db.prepare "DELETE FROM message_part"
149
+
150
+ @delete_all_messages_query.execute and
151
+ @delete_all_message_parts_query.execute
152
+ end
153
+
154
+ def delete_message!(message_id)
155
+ @delete_messages_query ||= db.prepare "DELETE FROM message WHERE id = ?"
156
+ @delete_message_parts_query ||= db.prepare "DELETE FROM message_part WHERE message_id = ?"
157
+ @delete_messages_query.execute(message_id) and
158
+ @delete_message_parts_query.execute(message_id)
159
+ end
160
+ end
@@ -0,0 +1,61 @@
1
+ require "eventmachine"
2
+
3
+ require "mail_catcher/mail"
4
+
5
+ class MailCatcher::Smtp < EventMachine::Protocols::SmtpServer
6
+ # We override EM's mail from processing to allow multiple mail-from commands
7
+ # per [RFC 2821](http://tools.ietf.org/html/rfc2821#section-4.1.1.2)
8
+ def process_mail_from sender
9
+ if @state.include? :mail_from
10
+ @state -= [:mail_from, :rcpt, :data]
11
+ receive_reset
12
+ end
13
+
14
+ super
15
+ end
16
+
17
+ def current_message
18
+ @current_message ||= {}
19
+ end
20
+
21
+ def receive_reset
22
+ @current_message = nil
23
+ true
24
+ end
25
+
26
+ def receive_sender(sender)
27
+ current_message[:sender] = sender
28
+ true
29
+ end
30
+
31
+ def receive_recipient(recipient)
32
+ current_message[:recipients] ||= []
33
+ current_message[:recipients] << recipient
34
+ true
35
+ end
36
+
37
+ def receive_data_chunk(lines)
38
+ current_message[:source] ||= ""
39
+ lines.each do |line|
40
+ current_message[:source] << line << "\r\n"
41
+ end
42
+ true
43
+ end
44
+
45
+ def receive_message
46
+ MailCatcher::Mail.add_message current_message
47
+ puts "==> SMTP: Received message from '#{current_message[:sender]}' (#{current_message[:source].length} bytes)"
48
+ true
49
+ rescue
50
+ puts "*** Error receiving message: #{current_message.inspect}"
51
+ puts " Exception: #{$!}"
52
+ puts " Backtrace:"
53
+ $!.backtrace.each do |line|
54
+ puts " #{line}"
55
+ end
56
+ puts " Please submit this as an issue at http://github.com/sj26/mailcatcher/issues"
57
+ false
58
+ ensure
59
+ @current_message = nil
60
+ end
61
+ end
@@ -0,0 +1,3 @@
1
+ module MailCatcher
2
+ VERSION = "0.7.0"
3
+ end
@@ -0,0 +1,199 @@
1
+ require "pathname"
2
+ require "net/http"
3
+ require "uri"
4
+
5
+ require "sinatra"
6
+ require "skinny"
7
+
8
+ require "mail_catcher/events"
9
+ require "mail_catcher/mail"
10
+
11
+ class Sinatra::Request
12
+ include Skinny::Helpers
13
+ end
14
+
15
+ module MailCatcher
16
+ module Web
17
+ class Application < Sinatra::Base
18
+ set :development, ENV["MAILCATCHER_ENV"] == "development"
19
+ set :root, File.expand_path("#{__FILE__}/../../../..")
20
+
21
+ if development?
22
+ require "sprockets-helpers"
23
+
24
+ configure do
25
+ require "mail_catcher/web/assets"
26
+ Sprockets::Helpers.configure do |config|
27
+ config.environment = Assets
28
+ config.prefix = "/assets"
29
+ config.digest = false
30
+ config.public_path = public_folder
31
+ config.debug = true
32
+ end
33
+ end
34
+
35
+ helpers do
36
+ include Sprockets::Helpers
37
+ end
38
+ else
39
+ helpers do
40
+ def javascript_tag(name)
41
+ %{<script src="/assets/#{name}.js"></script>}
42
+ end
43
+
44
+ def stylesheet_tag(name)
45
+ %{<link rel="stylesheet" href="/assets/#{name}.css">}
46
+ end
47
+ end
48
+ end
49
+
50
+ before do
51
+ response.headers['Access-Control-Allow-Origin'] = "*"
52
+ response.headers['Access-Control-Allow-Methods'] = "HEAD,GET,PUT,POST,DELETE,OPTIONS"
53
+ end
54
+
55
+ options "*" do
56
+ response.headers["Allow"] = "HEAD,GET,PUT,POST,DELETE,OPTIONS"
57
+ response.headers["Access-Control-Allow-Headers"] = "X-Requested-With, X-HTTP-Method-Override, Content-Type, Cache-Control, Accept"
58
+ 200
59
+ end
60
+
61
+ get "/" do
62
+ erb :index
63
+ end
64
+
65
+ delete "/" do
66
+ if MailCatcher.quittable?
67
+ MailCatcher.quit!
68
+ status 204
69
+ else
70
+ status 403
71
+ end
72
+ end
73
+
74
+ get "/messages" do
75
+ if request.websocket?
76
+ request.websocket!(
77
+ :on_start => proc do |websocket|
78
+ subscription = Events::MessageAdded.subscribe { |message| websocket.send_message message.to_json }
79
+ websocket.on_close do |websocket|
80
+ Events::MessageAdded.unsubscribe subscription
81
+ end
82
+ end)
83
+ else
84
+ content_type :json
85
+ Mail.messages.to_json
86
+ end
87
+ end
88
+
89
+ delete "/messages" do
90
+ Mail.delete!
91
+ status 204
92
+ end
93
+
94
+ get "/messages/:id.json" do
95
+ id = params[:id].to_i
96
+ if message = Mail.message(id)
97
+ content_type :json
98
+ message.merge({
99
+ "formats" => [
100
+ "source",
101
+ ("html" if Mail.message_has_html? id),
102
+ ("plain" if Mail.message_has_plain? id)
103
+ ].compact,
104
+ "attachments" => Mail.message_attachments(id).map do |attachment|
105
+ attachment.merge({"href" => "/messages/#{escape(id)}/parts/#{escape(attachment["cid"])}"})
106
+ end,
107
+ }).to_json
108
+ else
109
+ not_found
110
+ end
111
+ end
112
+
113
+ get "/messages/:id.html" do
114
+ id = params[:id].to_i
115
+ if part = Mail.message_part_html(id)
116
+ content_type part["type"], :charset => (part["charset"] || "utf8")
117
+
118
+ body = part["body"]
119
+
120
+ # Rewrite body to link to embedded attachments served by cid
121
+ body.gsub! /cid:([^'"> ]+)/, "#{id}/parts/\\1"
122
+
123
+ content_type :html
124
+ body
125
+ else
126
+ not_found
127
+ end
128
+ end
129
+
130
+ get "/messages/:id.plain" do
131
+ id = params[:id].to_i
132
+ if part = Mail.message_part_plain(id)
133
+ content_type part["type"], :charset => (part["charset"] || "utf8")
134
+ part["body"]
135
+ else
136
+ not_found
137
+ end
138
+ end
139
+
140
+ get "/messages/:id.source" do
141
+ id = params[:id].to_i
142
+ if message = Mail.message(id)
143
+ content_type "text/plain"
144
+ message["source"]
145
+ else
146
+ not_found
147
+ end
148
+ end
149
+
150
+ get "/messages/:id.eml" do
151
+ id = params[:id].to_i
152
+ if message = Mail.message(id)
153
+ content_type "message/rfc822"
154
+ message["source"]
155
+ else
156
+ not_found
157
+ end
158
+ end
159
+
160
+ get "/messages/:id/parts/:cid" do
161
+ id = params[:id].to_i
162
+ if part = Mail.message_part_cid(id, params[:cid])
163
+ content_type part["type"], :charset => (part["charset"] || "utf8")
164
+ attachment part["filename"] if part["is_attachment"] == 1
165
+ body part["body"].to_s
166
+ else
167
+ not_found
168
+ end
169
+ end
170
+
171
+ get "/messages/:id/analysis.?:format?" do
172
+ id = params[:id].to_i
173
+ if part = Mail.message_part_html(id)
174
+ # TODO: Server-side cache? Make the browser cache based on message create time? Hmm.
175
+ uri = URI.parse("http://api.getfractal.com/api/v2/validate#{"/format/#{params[:format]}" if params[:format].present?}")
176
+ response = Net::HTTP.post_form(uri, :api_key => "5c463877265251386f516f7428", :html => part["body"])
177
+ content_type ".#{params[:format]}" if params[:format].present?
178
+ body response.body
179
+ else
180
+ not_found
181
+ end
182
+ end
183
+
184
+ delete "/messages/:id" do
185
+ id = params[:id].to_i
186
+ if message = Mail.message(id)
187
+ Mail.delete_message!(id)
188
+ status 204
189
+ else
190
+ not_found
191
+ end
192
+ end
193
+
194
+ not_found do
195
+ erb :"404"
196
+ end
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,21 @@
1
+ require "active_support/core_ext/module/delegation"
2
+ require "rack/builder"
3
+
4
+ require "mail_catcher/web/application"
5
+
6
+ module MailCatcher
7
+ module Web extend self
8
+ def app
9
+ @@app ||= Rack::Builder.new do
10
+ if ENV["MAILCATCHER_ENV"] == "development"
11
+ require "mail_catcher/web/assets"
12
+ map("/assets") { run Assets }
13
+ end
14
+
15
+ map("/") { run Application }
16
+ end
17
+ end
18
+
19
+ delegate :call, :to => :app
20
+ end
21
+ end