letter_opener 0.1.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -1,4 +1,4 @@
1
- Copyright (c) 2011 Ryan Bates
1
+ Copyright (c) 2012 Ryan Bates
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
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 => "tmp/letter_opener"
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 [Vasiliy Ermolovich](https://github.com/nashby) for helping manage this project.
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,6 +1,7 @@
1
1
  require "fileutils"
2
2
  require "digest/sha1"
3
3
  require "cgi"
4
+ require "uri"
4
5
  require "launchy"
5
6
 
6
7
  require "letter_opener/message"
@@ -1,16 +1,17 @@
1
1
  module LetterOpener
2
2
  class DeliveryMethod
3
- def initialize(options = {})
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 = mail.parts.map { |part| Message.new(location, mail, part) }
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: 62px;
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-left: 72px;
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>&nbsp;
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
- <% else %>
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"><%= CGI.escapeHTML(body) %></pre>
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 ||= (@part && @part.body || @mail.body).to_s
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 field' do
54
- plain.should include("World!")
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 &lt;html&gt;")
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: 0.1.0
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-01 00:00:00.000000000 Z
12
+ date: 2012-10-10 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: launchy