danxexe-mailcatcher 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,161 @@
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
+ message["source"].force_encoding('UTF-8')
86
+ end
87
+ end
88
+
89
+ def message_has_html?(id)
90
+ @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"
91
+ (!!@message_has_html_query.execute(id).next) || ["text/html", "application/xhtml+xml"].include?(message(id)["type"])
92
+ end
93
+
94
+ def message_has_plain?(id)
95
+ @message_has_plain_query ||= db.prepare "SELECT 1 FROM message_part WHERE message_id = ? AND is_attachment = 0 AND type = 'text/plain' LIMIT 1"
96
+ (!!@message_has_plain_query.execute(id).next) || message(id)["type"] == "text/plain"
97
+ end
98
+
99
+ def message_parts(id)
100
+ @message_parts_query ||= db.prepare "SELECT cid, type, filename, size FROM message_part WHERE message_id = ? ORDER BY filename ASC"
101
+ @message_parts_query.execute(id).map do |row|
102
+ Hash[row.fields.zip(row)]
103
+ end
104
+ end
105
+
106
+ def message_attachments(id)
107
+ @message_parts_query ||= db.prepare "SELECT cid, type, filename, size FROM message_part WHERE message_id = ? AND is_attachment = 1 ORDER BY filename ASC"
108
+ @message_parts_query.execute(id).map do |row|
109
+ Hash[row.fields.zip(row)]
110
+ end
111
+ end
112
+
113
+ def message_part(message_id, part_id)
114
+ @message_part_query ||= db.prepare "SELECT * FROM message_part WHERE message_id = ? AND id = ? LIMIT 1"
115
+ row = @message_part_query.execute(message_id, part_id).next
116
+ row && Hash[row.fields.zip(row)]
117
+ end
118
+
119
+ def message_part_type(message_id, part_type)
120
+ @message_part_type_query ||= db.prepare "SELECT * FROM message_part WHERE message_id = ? AND type = ? AND is_attachment = 0 LIMIT 1"
121
+ row = @message_part_type_query.execute(message_id, part_type).next
122
+ row && Hash[row.fields.zip(row)]
123
+ end
124
+
125
+ def message_part_html(message_id)
126
+ part = message_part_type(message_id, "text/html")
127
+ part ||= message_part_type(message_id, "application/xhtml+xml")
128
+ part ||= begin
129
+ message = message(message_id)
130
+ message if message.present? and ["text/html", "application/xhtml+xml"].include? message["type"]
131
+ end
132
+ end
133
+
134
+ def message_part_plain(message_id)
135
+ message_part_type message_id, "text/plain"
136
+ end
137
+
138
+ def message_part_cid(message_id, cid)
139
+ @message_part_cid_query ||= db.prepare "SELECT * FROM message_part WHERE message_id = ?"
140
+ @message_part_cid_query.execute(message_id).map do |row|
141
+ Hash[row.fields.zip(row)]
142
+ end.find do |part|
143
+ part["cid"] == cid
144
+ end
145
+ end
146
+
147
+ def delete!
148
+ @delete_messages_query ||= db.prepare "DELETE FROM message"
149
+ @delete_message_parts_query ||= db.prepare "DELETE FROM message_part"
150
+
151
+ @delete_messages_query.execute and
152
+ @delete_message_parts_query.execute
153
+ end
154
+
155
+ def delete_message!(message_id)
156
+ @delete_messages_query ||= db.prepare "DELETE FROM message WHERE id = ?"
157
+ @delete_message_parts_query ||= db.prepare "DELETE FROM message_part WHERE message_id = ?"
158
+ @delete_messages_query.execute(message_id) and
159
+ @delete_message_parts_query.execute(message_id)
160
+ end
161
+ 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.6.2"
3
+ 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
@@ -0,0 +1,188 @@
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
+ get "/" do
51
+ erb :index
52
+ end
53
+
54
+ delete "/" do
55
+ if MailCatcher.quittable?
56
+ MailCatcher.quit!
57
+ status 204
58
+ else
59
+ status 403
60
+ end
61
+ end
62
+
63
+ get "/messages" do
64
+ if request.websocket?
65
+ request.websocket!(
66
+ :on_start => proc do |websocket|
67
+ subscription = Events::MessageAdded.subscribe { |message| websocket.send_message message.to_json }
68
+ websocket.on_close do |websocket|
69
+ Events::MessageAdded.unsubscribe subscription
70
+ end
71
+ end)
72
+ else
73
+ content_type :json
74
+ Mail.messages.to_json
75
+ end
76
+ end
77
+
78
+ delete "/messages" do
79
+ Mail.delete!
80
+ status 204
81
+ end
82
+
83
+ get "/messages/:id.json" do
84
+ id = params[:id].to_i
85
+ if message = Mail.message(id)
86
+ content_type :json
87
+ message.merge({
88
+ "formats" => [
89
+ "source",
90
+ ("html" if Mail.message_has_html? id),
91
+ ("plain" if Mail.message_has_plain? id)
92
+ ].compact,
93
+ "attachments" => Mail.message_attachments(id).map do |attachment|
94
+ attachment.merge({"href" => "/messages/#{escape(id)}/parts/#{escape(attachment["cid"])}"})
95
+ end,
96
+ }).to_json
97
+ else
98
+ not_found
99
+ end
100
+ end
101
+
102
+ get "/messages/:id.html" do
103
+ id = params[:id].to_i
104
+ if part = Mail.message_part_html(id)
105
+ content_type part["type"], :charset => (part["charset"] || "utf8")
106
+
107
+ body = part["body"]
108
+
109
+ # Rewrite body to link to embedded attachments served by cid
110
+ body.gsub! /cid:([^'"> ]+)/, "#{id}/parts/\\1"
111
+
112
+ content_type :html
113
+ body
114
+ else
115
+ not_found
116
+ end
117
+ end
118
+
119
+ get "/messages/:id.plain" do
120
+ id = params[:id].to_i
121
+ if part = Mail.message_part_plain(id)
122
+ content_type part["type"], :charset => (part["charset"] || "utf8")
123
+ part["body"]
124
+ else
125
+ not_found
126
+ end
127
+ end
128
+
129
+ get "/messages/:id.source" do
130
+ id = params[:id].to_i
131
+ if message = Mail.message(id)
132
+ content_type "text/plain"
133
+ message["source"]
134
+ else
135
+ not_found
136
+ end
137
+ end
138
+
139
+ get "/messages/:id.eml" do
140
+ id = params[:id].to_i
141
+ if message = Mail.message(id)
142
+ content_type "message/rfc822"
143
+ message["source"]
144
+ else
145
+ not_found
146
+ end
147
+ end
148
+
149
+ get "/messages/:id/parts/:cid" do
150
+ id = params[:id].to_i
151
+ if part = Mail.message_part_cid(id, params[:cid])
152
+ content_type part["type"], :charset => (part["charset"] || "utf8")
153
+ attachment part["filename"] if part["is_attachment"] == 1
154
+ body part["body"].to_s
155
+ else
156
+ not_found
157
+ end
158
+ end
159
+
160
+ get "/messages/:id/analysis.?:format?" do
161
+ id = params[:id].to_i
162
+ if part = Mail.message_part_html(id)
163
+ # TODO: Server-side cache? Make the browser cache based on message create time? Hmm.
164
+ uri = URI.parse("http://api.getfractal.com/api/v2/validate#{"/format/#{params[:format]}" if params[:format].present?}")
165
+ response = Net::HTTP.post_form(uri, :api_key => "5c463877265251386f516f7428", :html => part["body"])
166
+ content_type ".#{params[:format]}" if params[:format].present?
167
+ body response.body
168
+ else
169
+ not_found
170
+ end
171
+ end
172
+
173
+ delete "/messages/:id" do
174
+ id = params[:id].to_i
175
+ if message = Mail.message(id)
176
+ Mail.delete_message!(id)
177
+ status 204
178
+ else
179
+ not_found
180
+ end
181
+ end
182
+
183
+ not_found do
184
+ erb :"404"
185
+ end
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,3 @@
1
+ require "mail_catcher"
2
+
3
+ Mailcatcher = MailCatcher
Binary file
Binary file