mydriver-mailcatcher 0.6.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.6.5"
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