letter_opener 0.1.0 → 1.0.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.
- data/CHANGELOG.md +9 -0
- data/LICENSE +1 -1
- data/README.rdoc +8 -2
- data/lib/letter_opener.rb +1 -0
- data/lib/letter_opener/delivery_method.rb +7 -6
- data/lib/letter_opener/message.html.erb +21 -11
- data/lib/letter_opener/message.rb +49 -2
- data/spec/letter_opener/delivery_method_spec.rb +67 -4
- data/spec/letter_opener/message_spec.rb +8 -0
- metadata +2 -2
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,12 @@
|
|
1
|
+
## 1.0.0 ##
|
2
|
+
|
3
|
+
* Attachment Support (thanks David Cornu)
|
4
|
+
* Escape HTML in subject and other fields
|
5
|
+
* Raise an exception if the :location option is not present instead of using a default
|
6
|
+
* Open rich version by default (thanks Damir)
|
7
|
+
* Override margin on dt and dd elements in CSS (thanks Edgars Beigarts)
|
8
|
+
* Autolink URLs in plain version (thanks Matt Burke)
|
9
|
+
|
1
10
|
## 0.1.0 ##
|
2
11
|
|
3
12
|
* From and To show name and Email when specified
|
data/LICENSE
CHANGED
data/README.rdoc
CHANGED
@@ -22,12 +22,18 @@ If you aren't using Rails, this can be easily set up with the Mail gem. Just set
|
|
22
22
|
|
23
23
|
require "letter_opener"
|
24
24
|
Mail.defaults do
|
25
|
-
delivery_method LetterOpener::DeliveryMethod, :location =>
|
25
|
+
delivery_method LetterOpener::DeliveryMethod, :location => File.expand_path('../tmp/letter_opener', __FILE__)
|
26
26
|
end
|
27
27
|
|
28
|
+
Alternatively, if you are using ActionMailer directly (without Rails) you will need to add the delivery method.
|
29
|
+
|
30
|
+
require "letter_opener"
|
31
|
+
ActionMailer::Base.add_delivery_method :letter_opener, LetterOpener::DeliveryMethod, :location => File.expand_path('../tmp/letter_opener', __FILE__)
|
32
|
+
ActionMailer::Base.delivery_method = :letter_opener
|
33
|
+
|
28
34
|
|
29
35
|
== Development & Feedback
|
30
36
|
|
31
37
|
Questions or problems? Please use the {issue tracker}[https://github.com/ryanb/letter_opener/issues]. If you would like to contribute to this project, fork this repository and run +bundle+ and +rake+ to run the tests. Pull requests appreciated.
|
32
38
|
|
33
|
-
Special thanks to the {mail_view}[https://github.com/37signals/mail_view/] gem for inspiring this project and for their mail template. Also thanks to
|
39
|
+
Special thanks to the {mail_view}[https://github.com/37signals/mail_view/] gem for inspiring this project and for their mail template. Also thanks to {Vasiliy Ermolovich}[https://github.com/nashby] for helping manage this project.
|
data/lib/letter_opener.rb
CHANGED
@@ -1,16 +1,17 @@
|
|
1
1
|
module LetterOpener
|
2
2
|
class DeliveryMethod
|
3
|
-
|
4
|
-
self.settings = {:location => './letter_opener'}.merge!(options)
|
5
|
-
end
|
3
|
+
class InvalidOption < StandardError; end
|
6
4
|
|
7
5
|
attr_accessor :settings
|
8
6
|
|
7
|
+
def initialize(options = {})
|
8
|
+
raise InvalidOption, "A location option is required when using the Letter Opener delivery method" if options[:location].nil?
|
9
|
+
self.settings = options
|
10
|
+
end
|
11
|
+
|
9
12
|
def deliver!(mail)
|
10
13
|
location = File.join(settings[:location], "#{Time.now.to_i}_#{Digest::SHA1.hexdigest(mail.encoded)[0..6]}")
|
11
|
-
messages =
|
12
|
-
messages << Message.new(location, mail) if messages.empty?
|
13
|
-
messages.each(&:render)
|
14
|
+
messages = Message.rendered_messages(location, mail)
|
14
15
|
Launchy.open(URI.parse(URI.escape(messages.first.filepath)))
|
15
16
|
end
|
16
17
|
end
|
@@ -19,8 +19,9 @@
|
|
19
19
|
}
|
20
20
|
|
21
21
|
#message_headers dt {
|
22
|
-
width:
|
22
|
+
width: 92px;
|
23
23
|
padding: 1px;
|
24
|
+
margin: 0;
|
24
25
|
float: left;
|
25
26
|
text-align: right;
|
26
27
|
font-weight: bold;
|
@@ -28,7 +29,7 @@
|
|
28
29
|
}
|
29
30
|
|
30
31
|
#message_headers dd {
|
31
|
-
margin
|
32
|
+
margin: 0 0 0 102px;
|
32
33
|
padding: 1px;
|
33
34
|
}
|
34
35
|
|
@@ -50,40 +51,49 @@
|
|
50
51
|
<div id="message_headers">
|
51
52
|
<dl>
|
52
53
|
<dt>From:</dt>
|
53
|
-
<dd><%= from %></dd>
|
54
|
+
<dd><%= h from %></dd>
|
54
55
|
|
55
56
|
<% unless reply_to.empty? %>
|
56
57
|
<dt>Reply-To:</dt>
|
57
|
-
<dd><%= reply_to %></dd>
|
58
|
+
<dd><%= h reply_to %></dd>
|
58
59
|
<% end %>
|
59
60
|
|
60
61
|
<dt>Subject:</dt>
|
61
|
-
<dd><strong><%= mail.subject %></strong></dd>
|
62
|
+
<dd><strong><%= h mail.subject %></strong></dd>
|
62
63
|
|
63
64
|
<dt>Date:</dt>
|
64
65
|
<dd><%= Time.now.strftime("%b %e, %Y %I:%M:%S %p %Z") %></dd>
|
65
66
|
|
66
67
|
<% unless to.empty? %>
|
67
68
|
<dt>To:</dt>
|
68
|
-
<dd><%= to %></dd>
|
69
|
+
<dd><%= h to %></dd>
|
69
70
|
<% end %>
|
70
71
|
|
71
72
|
<% if mail.cc %>
|
72
73
|
<dt>CC:</dt>
|
73
|
-
<dd><%= mail.cc.join(", ") %></dd>
|
74
|
+
<dd><%= h mail.cc.join(", ") %></dd>
|
74
75
|
<% end %>
|
75
76
|
|
76
77
|
<% if mail.bcc %>
|
77
78
|
<dt>BCC:</dt>
|
78
|
-
<dd><%= mail.bcc.join(", ") %></dd>
|
79
|
+
<dd><%= h mail.bcc.join(", ") %></dd>
|
80
|
+
<% end %>
|
81
|
+
|
82
|
+
<% if @attachments.any? %>
|
83
|
+
<dt>Attachments:</dt>
|
84
|
+
<dd>
|
85
|
+
<% @attachments.each do |filename, path| %>
|
86
|
+
<a href="<%= path %>"><%= filename %></a>
|
87
|
+
<% end %>
|
88
|
+
</dd>
|
79
89
|
<% end %>
|
80
90
|
</dl>
|
81
91
|
|
82
92
|
<% if mail.multipart? %>
|
83
93
|
<p class="alternate">
|
84
|
-
<% if type == "plain" %>
|
94
|
+
<% if type == "plain" && mail.html_part %>
|
85
95
|
<a href="rich.html">View HTML version</a>
|
86
|
-
<%
|
96
|
+
<% elsif type == "rich" && mail.text_part %>
|
87
97
|
<a href="plain.html">View plain text version</a>
|
88
98
|
<% end %>
|
89
99
|
</p>
|
@@ -91,7 +101,7 @@
|
|
91
101
|
</div>
|
92
102
|
|
93
103
|
<% if type == "plain" %>
|
94
|
-
<pre id="message_body"><%=
|
104
|
+
<pre id="message_body"><%= auto_link(h(body)) %></pre>
|
95
105
|
<% else %>
|
96
106
|
<%= body %>
|
97
107
|
<% end %>
|
@@ -2,14 +2,39 @@ module LetterOpener
|
|
2
2
|
class Message
|
3
3
|
attr_reader :mail
|
4
4
|
|
5
|
+
def self.rendered_messages(location, mail)
|
6
|
+
messages = []
|
7
|
+
messages << new(location, mail, mail.html_part) if mail.html_part
|
8
|
+
messages << new(location, mail, mail.text_part) if mail.text_part
|
9
|
+
messages << new(location, mail) if messages.empty?
|
10
|
+
messages.each(&:render)
|
11
|
+
messages.sort
|
12
|
+
end
|
13
|
+
|
5
14
|
def initialize(location, mail, part = nil)
|
6
15
|
@location = location
|
7
16
|
@mail = mail
|
8
17
|
@part = part
|
18
|
+
@attachments = []
|
9
19
|
end
|
10
20
|
|
11
21
|
def render
|
12
22
|
FileUtils.mkdir_p(@location)
|
23
|
+
|
24
|
+
if mail.attachments.any?
|
25
|
+
attachments_dir = File.join(@location, 'attachments')
|
26
|
+
FileUtils.mkdir_p(attachments_dir)
|
27
|
+
mail.attachments.each do |attachment|
|
28
|
+
path = File.join(attachments_dir, attachment.filename)
|
29
|
+
|
30
|
+
unless File.exists?(path) # true if other parts have already been rendered
|
31
|
+
File.open(path, 'wb') { |f| f.write(attachment.body.raw_source) }
|
32
|
+
end
|
33
|
+
|
34
|
+
@attachments << [attachment.filename, "attachments/#{URI.escape(attachment.filename)}"]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
13
38
|
File.open(filepath, 'w') do |f|
|
14
39
|
f.write ERB.new(template).result(binding)
|
15
40
|
end
|
@@ -28,7 +53,15 @@ module LetterOpener
|
|
28
53
|
end
|
29
54
|
|
30
55
|
def body
|
31
|
-
@body ||=
|
56
|
+
@body ||= begin
|
57
|
+
body = (@part && @part.body || @mail.body).to_s
|
58
|
+
|
59
|
+
mail.attachments.each do |attachment|
|
60
|
+
body.gsub!(attachment.url, "attachments/#{attachment.filename}")
|
61
|
+
end
|
62
|
+
|
63
|
+
body
|
64
|
+
end
|
32
65
|
end
|
33
66
|
|
34
67
|
def from
|
@@ -50,6 +83,20 @@ module LetterOpener
|
|
50
83
|
def encoding
|
51
84
|
body.respond_to?(:encoding) ? body.encoding : "utf-8"
|
52
85
|
end
|
86
|
+
|
87
|
+
def auto_link(text)
|
88
|
+
text.gsub(URI.regexp(%W[https http])) do
|
89
|
+
"<a href=\"#{$&}\">#{$&}</a>"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def h(content)
|
94
|
+
CGI.escapeHTML(content)
|
95
|
+
end
|
96
|
+
|
97
|
+
def <=>(other)
|
98
|
+
order = %w[rich plain]
|
99
|
+
order.index(type) <=> order.index(other.type)
|
100
|
+
end
|
53
101
|
end
|
54
102
|
end
|
55
|
-
|
@@ -13,6 +13,11 @@ describe LetterOpener::DeliveryMethod do
|
|
13
13
|
end
|
14
14
|
end
|
15
15
|
|
16
|
+
it 'raises an exception if no location passed' do
|
17
|
+
lambda { LetterOpener::DeliveryMethod.new }.should raise_exception(LetterOpener::DeliveryMethod::InvalidOption)
|
18
|
+
lambda { LetterOpener::DeliveryMethod.new(location: "foo") }.should_not raise_exception
|
19
|
+
end
|
20
|
+
|
16
21
|
context 'content' do
|
17
22
|
let(:plain_file) { Dir["#{location}/*/plain.html"].first }
|
18
23
|
let(:plain) { File.read(plain_file) }
|
@@ -26,7 +31,7 @@ describe LetterOpener::DeliveryMethod do
|
|
26
31
|
reply_to 'No Reply no-reply@example.com'
|
27
32
|
to 'Bar bar@example.com'
|
28
33
|
subject 'Hello'
|
29
|
-
body 'World!'
|
34
|
+
body 'World! http://example.com'
|
30
35
|
end
|
31
36
|
end
|
32
37
|
|
@@ -50,8 +55,8 @@ describe LetterOpener::DeliveryMethod do
|
|
50
55
|
plain.should include("Hello")
|
51
56
|
end
|
52
57
|
|
53
|
-
it 'saves Body
|
54
|
-
plain.should include(
|
58
|
+
it 'saves Body with autolink' do
|
59
|
+
plain.should include('World! <a href="http://example.com">http://example.com</a>')
|
55
60
|
end
|
56
61
|
end
|
57
62
|
|
@@ -65,7 +70,7 @@ describe LetterOpener::DeliveryMethod do
|
|
65
70
|
Mail.deliver do
|
66
71
|
from 'foo@example.com'
|
67
72
|
to 'bar@example.com'
|
68
|
-
subject 'Many parts'
|
73
|
+
subject 'Many parts with <html>'
|
69
74
|
text_part do
|
70
75
|
body 'This is <plain> text'
|
71
76
|
end
|
@@ -95,6 +100,10 @@ describe LetterOpener::DeliveryMethod do
|
|
95
100
|
it 'saves html part' do
|
96
101
|
rich.should include("<h1>This is HTML</h1>")
|
97
102
|
end
|
103
|
+
|
104
|
+
it 'saves escaped Subject field' do
|
105
|
+
plain.should include("Many parts with <html>")
|
106
|
+
end
|
98
107
|
end
|
99
108
|
end
|
100
109
|
|
@@ -157,4 +166,58 @@ describe LetterOpener::DeliveryMethod do
|
|
157
166
|
plain.should include("World!")
|
158
167
|
end
|
159
168
|
end
|
169
|
+
|
170
|
+
context 'attachments in plain text mail' do
|
171
|
+
before do
|
172
|
+
Mail.deliver do
|
173
|
+
from 'foo@example.com'
|
174
|
+
to 'bar@example.com'
|
175
|
+
subject 'With attachments'
|
176
|
+
text_part do
|
177
|
+
body 'This is <plain> text'
|
178
|
+
end
|
179
|
+
attachments[File.basename(__FILE__)] = File.read(__FILE__)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
it 'creates attachments dir with attachment' do
|
184
|
+
attachment = Dir["#{location}/*/attachments/#{File.basename(__FILE__)}"].first
|
185
|
+
File.exists?(attachment).should be_true
|
186
|
+
end
|
187
|
+
|
188
|
+
it 'saves attachment name' do
|
189
|
+
plain = File.read(Dir["#{location}/*/plain.html"].first)
|
190
|
+
plain.should include(File.basename(__FILE__))
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
context 'attachments in rich mail' do
|
195
|
+
let(:url) { mail.attachments[0].url }
|
196
|
+
|
197
|
+
let!(:mail) do
|
198
|
+
Mail.deliver do
|
199
|
+
from 'foo@example.com'
|
200
|
+
to 'bar@example.com'
|
201
|
+
subject 'With attachments'
|
202
|
+
attachments[File.basename(__FILE__)] = File.read(__FILE__)
|
203
|
+
url = attachments[0].url
|
204
|
+
html_part do
|
205
|
+
content_type 'text/html; charset=UTF-8'
|
206
|
+
body "Here's an image: <img src='#{url}' />"
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
it 'creates attachments dir with attachment' do
|
212
|
+
attachment = Dir["#{location}/*/attachments/#{File.basename(__FILE__)}"].first
|
213
|
+
File.exists?(attachment).should be_true
|
214
|
+
end
|
215
|
+
|
216
|
+
it 'replaces inline attachment urls' do
|
217
|
+
text = File.read(Dir["#{location}/*/rich.html"].first)
|
218
|
+
mail.parts[0].body.should include(url)
|
219
|
+
text.should_not include(url)
|
220
|
+
text.should include("attachments/#{File.basename(__FILE__)}")
|
221
|
+
end
|
222
|
+
end
|
160
223
|
end
|
@@ -26,4 +26,12 @@ describe LetterOpener::Message do
|
|
26
26
|
message.to.should eq('test1@example.com, test2@example.com')
|
27
27
|
end
|
28
28
|
end
|
29
|
+
|
30
|
+
describe '#<=>' do
|
31
|
+
it 'sorts rich type before plain type' do
|
32
|
+
plain = described_class.new(location, mock(content_type: 'text/plain'))
|
33
|
+
rich = described_class.new(location, mock(content_type: 'text/html'))
|
34
|
+
[plain, rich].sort.should eq([rich, plain])
|
35
|
+
end
|
36
|
+
end
|
29
37
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: letter_opener
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.0.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-10-
|
12
|
+
date: 2012-10-10 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: launchy
|