rails_mail 0.10.1 → 0.10.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 713d9716c2e0f883038bb954b1266575fa1f254e04367fa116b25dde46a69bd6
4
- data.tar.gz: 05f5791bb498a169f7ab2a90f8fa1165dba264666e39b3559ad1f25c4660970d
3
+ metadata.gz: 536a20c5a826b0b73dfa72b4fc50e2b3ec3821bf7af3407973c2c6aa02151072
4
+ data.tar.gz: ef136ee4aae7150889c7383850fd18ca37fa1f72decc3683d933e38b1d555cb4
5
5
  SHA512:
6
- metadata.gz: 20b6563ee2449cd8cc9f76a43c79ae35514c30365ef832f89d773e0722967a27a2aebeb5b89da8999fc7be857263e698120be66673229d1715af612ffcf6c951
7
- data.tar.gz: 17410e94c060fada4766fdd97238864386e2b4866ae4152b74b4279394448e1e6a9178df17c64cbd411e0f09ff69502569b9c393c3fed0fc419ec09eb948b66f
6
+ metadata.gz: 5d48f19287f7bc951ce9b293bfbbedec5850eb22cfd39f7339328122bea226d80cfeddfd1559e121e20419379553982c0bea22dd7a6de198340ca98a366c8e63
7
+ data.tar.gz: 9972c2e2f77d9202ea1b5b737a4168cf819371f44590eca9ea3dbd5e455dbcf00b2d0e164a601de860749f30b7d971842a89f6dfd2a84eed688359a157950593
data/README.md CHANGED
@@ -19,6 +19,7 @@ RailsMail saves all outgoing emails to your database instead of actually sending
19
19
  * Dynamic time ago in words using date-fns
20
20
  * Ability to customize how the job that trims emails is enqueued
21
21
  * Ability to customize the title in the top left of the page via a standard Rails view that overrides the engine's default view.
22
+ * Toggle between HTML & text-only views for emails.
22
23
 
23
24
  ## Installation
24
25
 
@@ -120,6 +121,86 @@ end
120
121
  - `trim_emails_max_count`: Keeps only the N most recent emails, deleting older ones.
121
122
  - `sync_via`: Controls whether the trimming job runs synchronously (:now) or asynchronously (:later)
122
123
 
124
+ ### Custom Renderers
125
+
126
+ RailsMail supports custom renderers that allow you to add new ways to display emails. For example, you could add a renderer for markdown emails or a special format your application uses.
127
+
128
+ By default, RailsMail includes these renderers:
129
+ - HTML renderer: Displays HTML email content
130
+ - Text renderer: Displays plain text email content
131
+ - Exception Notifier renderer: Provides formatted display of exception emails from the ExceptionNotifier gem
132
+
133
+ #### Creating a Custom Renderer
134
+
135
+ 1. Create a new renderer class that inherits from `RailsMail::Renderer::Base`:
136
+
137
+ ```ruby
138
+ # app/renderers/markdown_renderer.rb
139
+ module RailsMail
140
+ module Renderer
141
+ class MarkdownRenderer < Base
142
+ def self.handles?(email)
143
+ email.content_type&.include?("text/markdown")
144
+ end
145
+
146
+ def self.partial_name
147
+ "rails_mail/emails/markdown_content"
148
+ end
149
+
150
+ def self.title
151
+ "Markdown"
152
+ end
153
+
154
+ def self.priority
155
+ 5 # Between Text and Exception renderers
156
+ end
157
+
158
+ def self.data(email)
159
+ { markdown_content: process_markdown(email.text_body) }
160
+ end
161
+
162
+ private
163
+
164
+ def self.process_markdown(text)
165
+ # Your markdown processing logic here
166
+ text
167
+ end
168
+ end
169
+ end
170
+ end
171
+ ```
172
+
173
+ 2. Create a partial for your renderer:
174
+
175
+ ```erb
176
+ # app/views/rails_mail/emails/_markdown_content.html.erb
177
+ <div class="mt-3 markdown-content">
178
+ <%= markdown_content %>
179
+ </div>
180
+ ```
181
+
182
+ 3. Register your renderer in an initializer:
183
+
184
+ ```ruby
185
+ # config/initializers/rails_mail.rb
186
+ RailsMail.configure do |config|
187
+ # ... other configuration ...
188
+ end
189
+
190
+ # Register custom renderers after configuration
191
+ RailsMail::RendererRegistry.register(RailsMail::Renderer::MarkdownRenderer)
192
+ ```
193
+
194
+ #### Renderer API
195
+
196
+ Custom renderers must implement these class methods:
197
+
198
+ - `handles?(email)`: Returns true if this renderer should handle the email
199
+ - `partial_name`: Returns the path to the partial that renders the content
200
+ - `title`: (optional) The tab title. Defaults to the class name without "Renderer"
201
+ - `priority`: (optional) Order in which renderers appear. Lower numbers appear first
202
+ - `data(email)`: (optional) Additional data to pass to the partial. Returns a hash
203
+
123
204
  ### Customize the title
124
205
  Since this is a Rails engine, you can customize the title by creating a file at `app/views/layouts/rails_mail/_title.html.erb`.
125
206
 
@@ -165,6 +246,7 @@ RailsMail uses Turbo, TurboStreams, and ActionCable to provide real-time updates
165
246
 
166
247
  - In development environment, the typical default for ActionCable (cable.yml) is to use the async adapter which is an in-memory adapter. If you try to send an email from the rails console, it will not auto-update the ui. You can change the adapter to the development adapter by running `cable.yml` to use something like the redis, postgresql adapter, or solidcable.
167
248
  - In staging environments, the same idea typically applies that you need to use a multi-process adapter like redis, postgresql, or solidcable.
249
+ - Inline `<style>` tags will be sanitized in email bodies.
168
250
 
169
251
  ## Future work / ideas
170
252
 
@@ -176,6 +258,7 @@ RailsMail uses Turbo, TurboStreams, and ActionCable to provide real-time updates
176
258
  - Implement read/unread functionality
177
259
  - Implement individual email delete
178
260
  - Implement multi-part (text/html) email support
261
+ - Allow clients to add additional acceptable HTML tags to render
179
262
 
180
263
  ## Contributing
181
264
  Contribution directions go here.
@@ -1,4 +1,14 @@
1
1
  module RailsMail
2
2
  module EmailsHelper
3
+ def prepare_email_html(raw_html)
4
+ doc = Nokogiri::HTML::DocumentFragment.parse(raw_html)
5
+ doc.css("a[href]").each do |a|
6
+ a.set_attribute("target", "_blank")
7
+ a.set_attribute("data-turbo", "false")
8
+ end
9
+ sanitize(doc.to_html,
10
+ tags: ActionView::Base.sanitized_allowed_tags + [ "table", "tbody", "tr", "td" ],
11
+ attributes: ActionView::Base.sanitized_allowed_attributes + [ "style", "target", "data-turbo" ])
12
+ end
3
13
  end
4
14
  end
@@ -13,12 +13,8 @@ module RailsMail
13
13
  where("CAST(data AS CHAR) LIKE :q", q: "%#{query}%")
14
14
  }
15
15
 
16
- def text?
17
- content_type&.include?("text/plain") || content_type&.include?("multipart/alternative")
18
- end
19
-
20
- def html?
21
- content_type&.include?("text/html") || content_type&.include?("multipart/alternative")
16
+ def exception_parser
17
+ @exception_parser ||= ExceptionParser.new(text_body)
22
18
  end
23
19
 
24
20
  def next_email
@@ -26,15 +22,19 @@ module RailsMail
26
22
  end
27
23
 
28
24
  def html_body
29
- return nil unless html?
30
-
31
- html_part["raw_source"]
25
+ html_part&.dig("raw_source")
32
26
  end
33
27
 
34
28
  def text_body
35
- return nil unless text?
29
+ text_part&.dig("raw_source")
30
+ end
31
+
32
+ def renderers
33
+ RailsMail::RendererRegistry.matching_renderers(self)
34
+ end
36
35
 
37
- text_part["raw_source"]
36
+ def render_partials
37
+ renderers.map(&:partial_name)
38
38
  end
39
39
 
40
40
  private
@@ -1,28 +1,28 @@
1
1
  <div class="prose max-w-none break-words">
2
- <input type="radio" name="email_tab" id="html_tab" class="hidden peer/html" <%= email.html? ? 'checked' : '' %> <%= email.html? ? '' : 'disabled' %> />
3
- <label
4
- for="html_tab"
5
- class="px-4 py-2 text-sm font-medium cursor-pointer peer-checked/html:text-blue-600 peer-checked/html:border-blue-600 peer-checked/html:border-b-2 peer-disabled/html:text-gray-400 peer-disabled/html:cursor-not-allowed">
6
- HTML
7
- </label>
8
-
9
- <input type="radio" name="email_tab" id="text_tab" class="hidden peer/text" <%= email.text? ? (!email.html? ? 'checked' : '') : 'disabled' %> />
10
- <label
11
- for="text_tab"
12
- class="px-4 py-2 text-sm font-medium cursor-pointer peer-checked/text:text-blue-600 peer-checked/text:border-blue-600 peer-checked/text:border-b-2 peer-disabled/text:text-gray-400 peer-disabled/text:cursor-not-allowed">
13
- Text
14
- </label>
2
+ <% @email.renderers.each_with_index do |renderer, index| %>
3
+ <input
4
+ type="radio"
5
+ name="email_tab"
6
+ id="<%= renderer.partial_name.parameterize %>_tab"
7
+ class="hidden peer/<%= renderer.partial_name.parameterize %>"
8
+ <%= index == 0 ? 'checked' : '' %>
9
+ />
10
+ <label
11
+ for="<%= renderer.partial_name.parameterize %>_tab"
12
+ class="px-4 py-2 text-sm font-medium cursor-pointer
13
+ peer-checked/<%= renderer.partial_name.parameterize %>:text-blue-600
14
+ peer-checked/<%= renderer.partial_name.parameterize %>:border-blue-600
15
+ peer-checked/<%= renderer.partial_name.parameterize %>:border-b-2
16
+ peer-disabled/<%= renderer.partial_name.parameterize %>:text-gray-400
17
+ peer-disabled/<%= renderer.partial_name.parameterize %>:cursor-not-allowed">
18
+ <%= renderer.title %>
19
+ </label>
20
+ <% end %>
15
21
 
16
- <div class="hidden peer-checked/html:block mt-3">
17
- <% if email.html? %>
18
- <%= sanitize(email.html_body,
19
- tags: ActionView::Base.sanitized_allowed_tags + ['table', 'tbody', 'tr', 'td'],
20
- attributes: ActionView::Base.sanitized_allowed_attributes + ['style']) %>
21
- <% end %>
22
- </div>
23
- <div class="hidden peer-checked/text:block mt-3 bg-black text-white font-mono p-4 rounded">
24
- <% if email.text? %>
25
- <%= simple_format email.text_body %>
26
- <% end %>
27
- </div>
22
+ <% @email.renderers.each do |renderer| %>
23
+ <div class="hidden peer-checked/<%= renderer.partial_name.parameterize %>:block">
24
+ <%= render partial: renderer.partial_name,
25
+ locals: { email: @email }.merge(renderer.data(@email)) %>
26
+ </div>
27
+ <% end %>
28
28
  </div>
@@ -0,0 +1,54 @@
1
+ <div class="max-w-5xl mx-auto p-6 space-y-6">
2
+ <div class="bg-red-50 p-4 rounded-xl shadow">
3
+ <h2 class="text-xl font-semibold text-red-800">⚠️ <%= exception.dig(:error, :type) %> in <%= exception.dig(:error, :location) %></h2>
4
+ <p class="text-red-700 mt-2"><%= exception.dig(:error, :message) %></p>
5
+ <% if exception.dig(:error, :backtrace_line).present? %>
6
+ <p class="text-gray-700 mt-2 font-mono text-sm"><%= exception.dig(:error, :backtrace_line) %></p>
7
+ <% end %>
8
+ </div>
9
+
10
+ <% # Reorder the sections for better logical flow %>
11
+ <% [:request, :session, :data].each do |section| %>
12
+ <% if exception[section].present? %>
13
+ <div class="bg-white border border-gray-200 rounded-xl shadow p-4">
14
+ <h3 class="text-lg font-bold text-gray-800 capitalize"><%= section %></h3>
15
+ <dl class="mt-2 grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-3 text-sm">
16
+ <% exception[section].each do |k, v| %>
17
+ <div>
18
+ <dt class="text-gray-500"><%= k %></dt>
19
+ <dd class="text-gray-800 font-mono break-words"><%= v %></dd>
20
+ </div>
21
+ <% end %>
22
+ </dl>
23
+ </div>
24
+ <% end %>
25
+ <% end %>
26
+
27
+ <% if exception[:backtrace].present? %>
28
+ <div class="bg-gray-100 border border-gray-200 rounded-xl shadow p-4">
29
+ <h3 class="text-lg font-bold text-gray-800">Backtrace</h3>
30
+ <ul class="mt-2 list-disc list-inside text-sm text-gray-700 font-mono space-y-1">
31
+ <% exception[:backtrace].each do |line| %>
32
+ <li><%= line %></li>
33
+ <% end %>
34
+ </ul>
35
+ </div>
36
+ <% end %>
37
+
38
+ <% if exception[:environment].present? %>
39
+ <div class="bg-white border border-gray-200 rounded-xl shadow p-4">
40
+ <h3 class="text-lg font-bold text-gray-800">Environment</h3>
41
+ <details>
42
+ <summary class="cursor-pointer text-blue-600 hover:text-blue-800 mb-2">Show Environment Variables</summary>
43
+ <dl class="mt-2 grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-3 text-sm">
44
+ <% exception[:environment].each do |k, v| %>
45
+ <div>
46
+ <dt class="text-gray-500"><%= k %></dt>
47
+ <dd class="text-gray-800 font-mono break-words"><%= v %></dd>
48
+ </div>
49
+ <% end %>
50
+ </dl>
51
+ </details>
52
+ </div>
53
+ <% end %>
54
+ </div>
@@ -0,0 +1,5 @@
1
+ <div class="mt-3">
2
+ <%= sanitize(email.html_body,
3
+ tags: ActionView::Base.sanitized_allowed_tags + ['table', 'tbody', 'tr', 'td'],
4
+ attributes: ActionView::Base.sanitized_allowed_attributes + ['style']) %>
5
+ </div>
@@ -0,0 +1,3 @@
1
+ <div class="mt-3 bg-black text-white font-mono p-4 rounded">
2
+ <%= simple_format email.text_body %>
3
+ </div>
@@ -1,3 +1,5 @@
1
+ require "rails_mail/renderer"
2
+
1
3
  module RailsMail
2
4
  class Configuration
3
5
  attr_accessor :authentication_callback, :show_clear_all_button_callback,
@@ -10,6 +12,7 @@ module RailsMail
10
12
  @trim_emails_older_than = nil
11
13
  @trim_emails_max_count = nil
12
14
  @enqueue_trim_job = ->(email) { RailsMail::TrimEmailsJob.perform_later }
15
+ register_default_renderers
13
16
  end
14
17
 
15
18
  def authenticate(&callback)
@@ -25,5 +28,13 @@ module RailsMail
25
28
  end
26
29
  @show_clear_all_button_callback = callback
27
30
  end
31
+
32
+ private
33
+
34
+ def register_default_renderers
35
+ RailsMail::RendererRegistry.register(RailsMail::Renderer::HtmlRenderer)
36
+ RailsMail::RendererRegistry.register(RailsMail::Renderer::TextRenderer)
37
+ RailsMail::RendererRegistry.register(RailsMail::Renderer::ExceptionNotifierRenderer)
38
+ end
28
39
  end
29
40
  end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsMail
4
+ class ExceptionParser
5
+ attr_reader :raw
6
+
7
+ def initialize(raw)
8
+ @raw = raw
9
+ end
10
+
11
+ def valid_format?
12
+ return false unless @raw.present?
13
+ @raw.include?("occurred in")
14
+ end
15
+
16
+ def parse
17
+ return {} unless @raw
18
+
19
+ # Extract the error information from the first few lines
20
+ error_info = extract_error
21
+
22
+ # Extract sections using a more direct approach
23
+ sections = {
24
+ "Data" => extract_section("Data"),
25
+ "Request" => extract_section("Request"),
26
+ "Session" => extract_section("Session"),
27
+ "Backtrace" => extract_section("Backtrace"),
28
+ "Environment" => extract_section("Environment")
29
+ }
30
+
31
+ {
32
+ error: error_info,
33
+ data: parse_section_content(sections["Data"]),
34
+ request: parse_section_content(sections["Request"]),
35
+ session: parse_section_content(sections["Session"]),
36
+ backtrace: parse_backtrace(sections["Backtrace"]),
37
+ environment: parse_section_content(sections["Environment"])
38
+ }
39
+ end
40
+
41
+ def extract_error
42
+ # Look for the error line pattern - supporting both "A" and "An"
43
+ error_match = @raw.match(/^A(?:n)? ([\w:]+) occurred in ([^:]+):\r\n\r\n\s+(.*?)\r\n\s+(.+?)\r\n/m)
44
+ return {} unless error_match
45
+
46
+ {
47
+ type: error_match[1],
48
+ location: error_match[2],
49
+ message: error_match[3],
50
+ backtrace_line: error_match[4]
51
+ }
52
+ end
53
+
54
+ def extract_section(section_name)
55
+ # Match the section header and everything until the next section header or end of string
56
+ section_regex = /^-+\r\n#{Regexp.escape(section_name)}:\r\n-+\r\n(.*?)(?=\r\n-+\r\n|\z)/m
57
+ match = @raw.match(section_regex)
58
+ match ? match[1] : ""
59
+ end
60
+
61
+ def parse_section_content(content)
62
+ return {} unless content && !content.empty?
63
+
64
+ result = {}
65
+ content.split("\r\n").each do |line|
66
+ line = line.strip
67
+ # Handle lines that start with * and have indentation
68
+ if line.start_with?("*")
69
+ # Remove the asterisk and any leading whitespace
70
+ key_value = line.sub(/^\*\s*/, "").split(":", 2)
71
+ if key_value.length == 2
72
+ key = key_value[0].strip
73
+ value = key_value[1].strip
74
+ result[key] = value if key && !key.empty?
75
+ end
76
+ end
77
+ end
78
+ result
79
+ end
80
+
81
+ def parse_backtrace(content)
82
+ return [] unless content && !content.empty?
83
+
84
+ # Split by Windows-style line endings and extract non-empty lines
85
+ lines = content.split("\r\n").map(&:strip).reject(&:empty?)
86
+ # Filter out lines that start with * (which would be key-value pairs)
87
+ lines.reject { |line| line.start_with?("*") }
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,25 @@
1
+ module RailsMail
2
+ module Renderer
3
+ class Base
4
+ def self.handles?(email)
5
+ raise NotImplementedError, "#{self.name} must implement .handles?"
6
+ end
7
+
8
+ def self.partial_name
9
+ raise NotImplementedError, "#{self.name} must implement .partial_name"
10
+ end
11
+
12
+ def self.title
13
+ self.name.demodulize.sub("Renderer", "")
14
+ end
15
+
16
+ def self.priority
17
+ 0 # Lower numbers render first
18
+ end
19
+
20
+ def self.data(email)
21
+ {} # Override to provide additional data to the partial
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ module RailsMail
2
+ module Renderer
3
+ class ExceptionNotifierRenderer < Base
4
+ def self.handles?(email)
5
+ email.exception_parser.valid_format?
6
+ end
7
+
8
+ def self.partial_name
9
+ "rails_mail/emails/exception"
10
+ end
11
+
12
+ def self.title
13
+ "Exception"
14
+ end
15
+
16
+ def self.priority
17
+ 1 # After standard renderers
18
+ end
19
+
20
+ def self.data(email)
21
+ { exception: email.exception_parser.parse }
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,22 @@
1
+ module RailsMail
2
+ module Renderer
3
+ class HtmlRenderer < Base
4
+ def self.handles?(email)
5
+ email.content_type&.include?("text/html") ||
6
+ email.content_type&.include?("multipart/alternative")
7
+ end
8
+
9
+ def self.partial_name
10
+ "rails_mail/emails/html_content"
11
+ end
12
+
13
+ def self.title
14
+ "HTML"
15
+ end
16
+
17
+ def self.priority
18
+ 10 # Base priority for standard content
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ module RailsMail
2
+ module Renderer
3
+ class TextRenderer < Base
4
+ def self.handles?(email)
5
+ email.content_type&.include?("text/plain") ||
6
+ email.content_type&.include?("multipart/alternative")
7
+ end
8
+
9
+ def self.partial_name
10
+ "rails_mail/emails/text_content"
11
+ end
12
+
13
+ def self.title
14
+ "Text"
15
+ end
16
+
17
+ def self.priority
18
+ 20 # Just after HTML
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,10 @@
1
+ require "rails_mail/renderer/base"
2
+ require "rails_mail/renderer/html_renderer"
3
+ require "rails_mail/renderer/text_renderer"
4
+ require "rails_mail/renderer/exception_notifier_renderer"
5
+ require "rails_mail/renderer_registry"
6
+
7
+ module RailsMail
8
+ module Renderer
9
+ end
10
+ end
@@ -0,0 +1,19 @@
1
+ module RailsMail
2
+ class RendererRegistry
3
+ class << self
4
+ def register(renderer_class)
5
+ renderers << renderer_class
6
+ end
7
+
8
+ def renderers
9
+ @renderers ||= []
10
+ end
11
+
12
+ def matching_renderers(email)
13
+ renderers
14
+ .select { |renderer| renderer.handles?(email) }
15
+ .sort_by(&:priority)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -1,3 +1,3 @@
1
1
  module RailsMail
2
- VERSION = "0.10.1"
2
+ VERSION = "0.10.2"
3
3
  end
data/lib/rails_mail.rb CHANGED
@@ -2,6 +2,8 @@ require "rails_mail/version"
2
2
  require "rails_mail/engine"
3
3
  require "rails_mail/delivery_method"
4
4
  require "rails_mail/configuration"
5
+ require "rails_mail/exception_parser"
6
+ require "rails_mail/renderer"
5
7
 
6
8
  module RailsMail
7
9
  class << self
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_mail
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.1
4
+ version: 0.10.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter Philips
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-04-15 00:00:00.000000000 Z
11
+ date: 2025-04-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -87,7 +87,10 @@ files:
87
87
  - app/views/rails_mail/emails/_email.html.erb
88
88
  - app/views/rails_mail/emails/_email_tabs.html.erb
89
89
  - app/views/rails_mail/emails/_empty_state.html.erb
90
+ - app/views/rails_mail/emails/_exception.html.erb
90
91
  - app/views/rails_mail/emails/_form.html.erb
92
+ - app/views/rails_mail/emails/_html_content.html.erb
93
+ - app/views/rails_mail/emails/_text_content.html.erb
91
94
  - app/views/rails_mail/emails/destroy.turbo_stream.erb
92
95
  - app/views/rails_mail/emails/destroy_all.turbo_stream.erb
93
96
  - app/views/rails_mail/emails/edit.html.erb
@@ -105,6 +108,13 @@ files:
105
108
  - lib/rails_mail/configuration.rb
106
109
  - lib/rails_mail/delivery_method.rb
107
110
  - lib/rails_mail/engine.rb
111
+ - lib/rails_mail/exception_parser.rb
112
+ - lib/rails_mail/renderer.rb
113
+ - lib/rails_mail/renderer/base.rb
114
+ - lib/rails_mail/renderer/exception_notifier_renderer.rb
115
+ - lib/rails_mail/renderer/html_renderer.rb
116
+ - lib/rails_mail/renderer/text_renderer.rb
117
+ - lib/rails_mail/renderer_registry.rb
108
118
  - lib/rails_mail/version.rb
109
119
  - lib/tasks/rails_mail_tasks.rake
110
120
  homepage: https://github.com/synth/rails_mail