mailcatcher-jruby 1.0

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,5 @@
1
+ require 'eventmachine'
2
+
3
+ module MailCatcher::Events
4
+ MessageAdded = EventMachine::Channel.new
5
+ end
@@ -0,0 +1,16 @@
1
+ module MailCatcher
2
+ module Growl extend self
3
+ def start
4
+ MailCatcher::Events::MessageAdded.subscribe MailCatcher::Growl.method :notify
5
+ end
6
+
7
+ def notify message
8
+ image_path = File.expand_path(File.join(__FILE__, '..', '..', '..', 'public', 'images', 'logo_large.png'))
9
+ system "growlnotify", "--image", image_path, "--name", "MailCatcher", "--message", "Message received:\n#{message["subject"]}"
10
+ end
11
+
12
+ # TODO: Native support on MacRuby with click backs
13
+ #def click
14
+ #end
15
+ end
16
+ end
@@ -0,0 +1,203 @@
1
+ require 'active_support/json'
2
+ require 'active_record'
3
+ require 'mail'
4
+ #require 'sqlite3'
5
+ require 'eventmachine'
6
+
7
+ ActiveRecord::Base.configurations["db"] = { adapter: 'jdbcsqlite3', database: 'db', pool: 20 }
8
+
9
+ class Message < ActiveRecord::Base
10
+ establish_connection :db
11
+ has_many :message_parts, dependent: :destroy
12
+ self.inheritance_column = nil
13
+
14
+ def recipients
15
+ self[:recipients] &&= [ActiveSupport::JSON.decode(self[:recipients])].flatten!
16
+ end
17
+ end
18
+ class MessagePart < ActiveRecord::Base
19
+ establish_connection :db
20
+ belongs_to :message
21
+ self.inheritance_column = nil
22
+ end
23
+
24
+
25
+ Message.connection.create_table :messages do |t|
26
+ t.text :sender
27
+ t.text :recipients
28
+ t.text :subject
29
+ t.binary :source
30
+ t.text :size
31
+ t.text :type
32
+
33
+ t.timestamps
34
+ end unless Message.table_exists?
35
+
36
+ MessagePart.connection.create_table :message_parts do |t|
37
+ t.integer :message_id
38
+ t.text :cid
39
+ t.text :type
40
+ t.integer :is_attachment
41
+ t.text :filename
42
+ t.text :charset
43
+ t.binary :body
44
+ t.integer :size
45
+ end unless MessagePart.table_exists?
46
+
47
+ module MailCatcher::Mail extend self
48
+
49
+ def add_message(message)
50
+ #@add_message_query ||= db.prepare("INSERT INTO message (sender, recipients, subject, source, type, size, created_at) VALUES (?, ?, ?, ?, ?, ?, datetime('now'))")
51
+
52
+ mail = Mail.new(message[:source])
53
+ #@add_message_query.execute(message[:sender], message[:recipients].to_json, mail.subject, message[:source], mail.mime_type || 'text/plain', message[:source].length)
54
+
55
+ m = Message.create(
56
+ sender: message[:sender],
57
+ recipients: message[:recipients].to_json,
58
+ subject: mail.subject,
59
+ source: message[:source],
60
+ type: mail.mime_type || 'text/plain',
61
+ size: message[:source].length
62
+ )
63
+
64
+ #message_id = db.last_insert_row_id
65
+ message_id = m.id
66
+ parts = mail.all_parts
67
+ parts = [mail] if parts.empty?
68
+ parts.each do |part|
69
+ body = part.body.to_s
70
+ # Only parts have CIDs, not mail
71
+ cid = part.cid if part.respond_to? :cid
72
+ #add_message_part(message_id, cid, part.mime_type || 'text/plain', part.attachment? ? 1 : 0, part.filename, part.charset, body, body.length)
73
+ MessagePart.create(
74
+ message_id: message_id,
75
+ cid: cid,
76
+ type: part.mime_type || 'text/plain',
77
+ is_attachment: part.attachment? ? 1 : 0,
78
+ filename: part.filename,
79
+ charset: part.charset,
80
+ body: body,
81
+ size: body.length
82
+ )
83
+ end
84
+
85
+ EventMachine.next_tick do
86
+ message = MailCatcher::Mail.message message_id
87
+ MailCatcher::Events::MessageAdded.push message
88
+ end
89
+ end
90
+
91
+ #def add_message_part(*args)
92
+ #@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'))"
93
+ #@add_message_part_query.execute(*args)
94
+ #end
95
+
96
+ #def latest_created_at
97
+ #@latest_created_at_query ||= db.prepare "SELECT created_at FROM message ORDER BY created_at DESC LIMIT 1"
98
+ #@latest_created_at_query.execute.next
99
+ #end
100
+
101
+ def messages
102
+ Message.all.map(&:attributes)
103
+ #@messages_query ||= db.prepare "SELECT id, sender, recipients, subject, size, created_at FROM message ORDER BY created_at ASC"
104
+ #@messages_query.execute.map do |row|
105
+ #Hash[row.fields.zip(row)].tap do |message|
106
+ #message["recipients"] &&= ActiveSupport::JSON.decode message["recipients"]
107
+ #end
108
+ #end
109
+ end
110
+
111
+ def message(id)
112
+ Message.find(id).attributes
113
+ #@message_query ||= db.prepare "SELECT * FROM message WHERE id = ? LIMIT 1"
114
+ #row = @message_query.execute(id).next
115
+ #row && Hash[row.fields.zip(row)].tap do |message|
116
+ #message["recipients"] &&= ActiveSupport::JSON.decode message["recipients"]
117
+ #end
118
+ end
119
+
120
+ def message_has_html?(id)
121
+ part = MessagePart.where(message_id: id, is_attachment: 0).where("type IN ('application/xhtml+xml', 'text/html')").first
122
+ part.present? || ['text/html', 'application/xhtml+xml'].include?(Message.find(id).type)
123
+ #@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"
124
+ #(!!@message_has_html_query.execute(id).next) || ['text/html', 'application/xhtml+xml'].include?(message(id)["type"])
125
+ end
126
+
127
+ def message_has_plain?(id)
128
+ part = MessagePart.where(message_id: id, is_attachment: 0, type: 'text/plain').first
129
+ part.present? || Message.find(id).type == 'text/plain'
130
+ #@message_has_plain_query ||= db.prepare "SELECT 1 FROM message_part WHERE message_id = ? AND is_attachment = 0 AND type = 'text/plain' LIMIT 1"
131
+ #(!!@message_has_plain_query.execute(id).next) || message(id)["type"] == "text/plain"
132
+ end
133
+
134
+ def message_parts(id)
135
+ MessagePart.where(:message_id => id).order('filename ASC').map(&:attributes)
136
+ #@message_parts_query ||= db.prepare "SELECT cid, type, filename, size FROM message_part WHERE message_id = ? ORDER BY filename ASC"
137
+ #@message_parts_query.execute(id).map do |row|
138
+ #Hash[row.fields.zip(row)]
139
+ #end
140
+ end
141
+
142
+ def message_attachments(id)
143
+ MessagePart.where(message_id: id, is_attachment: 1).order('filename ASC').map(&:attributes)
144
+ #@message_parts_query ||= db.prepare "SELECT cid, type, filename, size FROM message_part WHERE message_id = ? AND is_attachment = 1 ORDER BY filename ASC"
145
+ #@message_parts_query.execute(id).map do |row|
146
+ #Hash[row.fields.zip(row)]
147
+ #end
148
+ end
149
+
150
+ def message_part(message_id, part_id)
151
+ MessagePart.where(message_id: message_id, id: part_id).first.try(:attributes)
152
+ #@message_part_query ||= db.prepare "SELECT * FROM message_part WHERE message_id = ? AND id = ? LIMIT 1"
153
+ #row = @message_part_query.execute(message_id, part_id).next
154
+ #row && Hash[row.fields.zip(row)]
155
+ end
156
+
157
+ def message_part_type(message_id, part_type)
158
+ MessagePart.where(message_id: message_id, type: part_type, is_attachment: 0).first.try(:attributes)
159
+ #@message_part_type_query ||= db.prepare "SELECT * FROM message_part WHERE message_id = ? AND type = ? AND is_attachment = 0 LIMIT 1"
160
+ #row = @message_part_type_query.execute(message_id, part_type).next
161
+ #row && Hash[row.fields.zip(row)]
162
+ end
163
+
164
+ def message_part_html(message_id)
165
+ part = message_part_type(message_id, "text/html")
166
+ part ||= message_part_type(message_id, "application/xhtml+xml")
167
+ part ||= begin
168
+ message = message(message_id)
169
+ message if message.present? and ['text/html', 'application/xhtml+xml'].include? message["type"]
170
+ end
171
+ end
172
+
173
+ def message_part_plain(message_id)
174
+ message_part_type message_id, "text/plain"
175
+ end
176
+
177
+ def message_part_cid(message_id, cid)
178
+ MessagePart.where(message_id: message_id, cid: cid).first.try(:attributes)
179
+ #@message_part_cid_query ||= db.prepare 'SELECT * FROM message_part WHERE message_id = ?'
180
+ #@message_part_cid_query.execute(message_id).map do |row|
181
+ #Hash[row.fields.zip(row)]
182
+ #end.find do |part|
183
+ #part["cid"] == cid
184
+ #end
185
+ end
186
+
187
+ def delete!
188
+ Message.destroy_all
189
+ #@delete_messages_query ||= db.prepare 'DELETE FROM message'
190
+ #@delete_message_parts_query ||= db.prepare 'DELETE FROM message_part'
191
+
192
+ #@delete_messages_query.execute and
193
+ #@delete_message_parts_query.execute
194
+ end
195
+
196
+ def delete_message!(message_id)
197
+ Message.where(id: message_id).destroy_all
198
+ #@delete_messages_query ||= db.prepare 'DELETE FROM message WHERE id = ?'
199
+ #@delete_message_parts_query ||= db.prepare 'DELETE FROM message_part WHERE message_id = ?'
200
+ #@delete_messages_query.execute(message_id) and
201
+ #@delete_message_parts_query.execute(message_id)
202
+ end
203
+ end
@@ -0,0 +1,57 @@
1
+ require 'eventmachine'
2
+
3
+ class MailCatcher::Smtp < EventMachine::Protocols::SmtpServer
4
+ # We override EM's mail from processing to allow multiple mail-from commands
5
+ # per [RFC 2821](http://tools.ietf.org/html/rfc2821#section-4.1.1.2)
6
+ def process_mail_from sender
7
+ if @state.include? :mail_from
8
+ @state -= [:mail_from, :rcpt, :data]
9
+ receive_reset
10
+ end
11
+
12
+ super
13
+ end
14
+
15
+ def current_message
16
+ @current_message ||= {}
17
+ end
18
+
19
+ def receive_reset
20
+ @current_message = nil
21
+ true
22
+ end
23
+
24
+ def receive_sender(sender)
25
+ current_message[:sender] = sender
26
+ true
27
+ end
28
+
29
+ def receive_recipient(recipient)
30
+ current_message[:recipients] ||= []
31
+ current_message[:recipients] << recipient
32
+ true
33
+ end
34
+
35
+ def receive_data_chunk(lines)
36
+ current_message[:source] ||= ""
37
+ current_message[:source] << lines.join("\n")
38
+ true
39
+ end
40
+
41
+ def receive_message
42
+ MailCatcher::Mail.add_message current_message
43
+ puts "==> SMTP: Received message from '#{current_message[:sender]}' (#{current_message[:source].length} bytes)"
44
+ true
45
+ rescue
46
+ puts "*** Error receiving message: #{current_message.inspect}"
47
+ puts " Exception: #{$!}"
48
+ puts " Backtrace:"
49
+ $!.backtrace.each do |line|
50
+ puts " #{line}"
51
+ end
52
+ puts " Please submit this as an issue at http://github.com/sj26/mailcatcher/issues"
53
+ false
54
+ ensure
55
+ @current_message = nil
56
+ end
57
+ end
@@ -0,0 +1,3 @@
1
+ module MailCatcher
2
+ VERSION = "1.0"
3
+ end
@@ -0,0 +1,173 @@
1
+ require 'sinatra'
2
+ require 'pathname'
3
+ require 'net/http'
4
+ require 'uri'
5
+ #require 'skinny'
6
+
7
+
8
+ # #-=--------------------
9
+ # module MailCatcher
10
+ # end
11
+
12
+ # DB_CONF = { adapter: 'jdbcsqlite3', database: 'db' }
13
+
14
+ # class Message < ActiveRecord::Base
15
+ # establish_connection DB_CONF
16
+ # has_many :message_parts, dependent: :destroy
17
+ # self.inheritance_column = nil
18
+
19
+ # def recipients
20
+ # self[:recipients] &&= ActiveSupport::JSON.decode self[:recipients]
21
+ # end
22
+ # end
23
+ # class MessagePart < ActiveRecord::Base
24
+ # establish_connection DB_CONF
25
+ # belongs_to :message
26
+ # self.inheritance_column = nil
27
+ # end
28
+
29
+ # #---------------------------
30
+
31
+
32
+ class Sinatra::Request
33
+ #include Skinny::Helpers
34
+ end
35
+
36
+ class MailCatcher::Web < Sinatra::Base
37
+ set :root, File.expand_path("#{__FILE__}/../../..")
38
+ set :haml, :format => :html5
39
+
40
+ get '/' do
41
+ haml :index
42
+ end
43
+
44
+ delete '/' do
45
+ if MailCatcher.quittable?
46
+ MailCatcher.quit!
47
+ status 204
48
+ else
49
+ status 403
50
+ end
51
+ end
52
+
53
+ get '/messages' do
54
+ # if request.websocket?
55
+ # request.websocket!(
56
+ # :on_start => proc do |websocket|
57
+ # subscription = MailCatcher::Events::MessageAdded.subscribe { |message| websocket.send_message message.to_json }
58
+ # websocket.on_close do |websocket|
59
+ # MailCatcher::Events::MessageAdded.unsubscribe subscription
60
+ # end
61
+ # end)
62
+ # else
63
+ MailCatcher::Mail.messages.to_json
64
+ # end
65
+ end
66
+
67
+ delete '/messages' do
68
+ MailCatcher::Mail.delete!
69
+ status 204
70
+ end
71
+
72
+ get '/messages/:id.json' do
73
+ id = params[:id].to_i
74
+ if message = MailCatcher::Mail.message(id)
75
+ message.merge({
76
+ "formats" => [
77
+ "source",
78
+ ("html" if MailCatcher::Mail.message_has_html? id),
79
+ ("plain" if MailCatcher::Mail.message_has_plain? id)
80
+ ].compact,
81
+ "attachments" => MailCatcher::Mail.message_attachments(id).map do |attachment|
82
+ attachment.merge({"href" => "/messages/#{escape(id)}/parts/#{escape(attachment['cid'])}"})
83
+ end,
84
+ }).to_json
85
+ else
86
+ not_found
87
+ end
88
+ end
89
+
90
+ get '/messages/:id.html' do
91
+ id = params[:id].to_i
92
+ if part = MailCatcher::Mail.message_part_html(id)
93
+ content_type part["type"], :charset => (part["charset"] || "utf8")
94
+
95
+ body = part["body"]
96
+
97
+ # Rewrite body to link to embedded attachments served by cid
98
+ body.gsub! /cid:([^'"> ]+)/, "#{id}/parts/\\1"
99
+
100
+ body
101
+ else
102
+ not_found
103
+ end
104
+ end
105
+
106
+ get "/messages/:id.plain" do
107
+ id = params[:id].to_i
108
+ if part = MailCatcher::Mail.message_part_plain(id)
109
+ content_type part["type"], :charset => (part["charset"] || "utf8")
110
+ part["body"]
111
+ else
112
+ not_found
113
+ end
114
+ end
115
+
116
+ get "/messages/:id.source" do
117
+ id = params[:id].to_i
118
+ if message = MailCatcher::Mail.message(id)
119
+ content_type "text/plain"
120
+ message["source"]
121
+ else
122
+ not_found
123
+ end
124
+ end
125
+
126
+ get "/messages/:id.eml" do
127
+ id = params[:id].to_i
128
+ if message = MailCatcher::Mail.message(id)
129
+ content_type "message/rfc822"
130
+ message["source"]
131
+ else
132
+ not_found
133
+ end
134
+ end
135
+
136
+ get "/messages/:id/parts/:cid" do
137
+ id = params[:id].to_i
138
+ if part = MailCatcher::Mail.message_part_cid(id, params[:cid])
139
+ content_type part["type"], :charset => (part["charset"] || "utf8")
140
+ attachment part["filename"] if part["is_attachment"] == 1
141
+ body part["body"].to_s
142
+ else
143
+ not_found
144
+ end
145
+ end
146
+
147
+ get "/messages/:id/analysis.?:format?" do
148
+ id = params[:id].to_i
149
+ if part = MailCatcher::Mail.message_part_html(id)
150
+ # TODO: Server-side cache? Make the browser cache based on message create time? Hmm.
151
+ uri = URI.parse("http://api.getfractal.com/api/v2/validate#{"/format/#{params[:format]}" if params[:format].present?}")
152
+ response = Net::HTTP.post_form(uri, :api_key => "5c463877265251386f516f7428", :html => part["body"])
153
+ content_type ".#{params[:format]}" if params[:format].present?
154
+ body response.body
155
+ else
156
+ not_found
157
+ end
158
+ end
159
+
160
+ delete '/messages/:id' do
161
+ id = params[:id].to_i
162
+ if message = MailCatcher::Mail.message(id)
163
+ MailCatcher::Mail.delete_message!(id)
164
+ status 204
165
+ else
166
+ not_found
167
+ end
168
+ end
169
+
170
+ not_found do
171
+ "<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>"
172
+ end
173
+ end