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 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