Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Add mailer previews feature based on mail_view gem
  • Loading branch information
pixeltrix committed Dec 17, 2013
1 parent 1602a70 commit d6dec7f
Show file tree
Hide file tree
Showing 17 changed files with 736 additions and 6 deletions.
4 changes: 4 additions & 0 deletions actionmailer/CHANGELOG.md
@@ -1,3 +1,7 @@
* Add mailer previews feature based on 37 Signals mail_view gem

*Andrew White*

* Calling `mail()` without arguments serves as getter for the current mail
message and keeps previously set headers.

Expand Down
2 changes: 2 additions & 0 deletions actionmailer/lib/action_mailer.rb
Expand Up @@ -41,6 +41,8 @@ module ActionMailer
autoload :Base
autoload :DeliveryMethods
autoload :MailHelper
autoload :Preview
autoload :Previews, 'action_mailer/preview'
autoload :TestCase
autoload :TestHelper
end
20 changes: 20 additions & 0 deletions actionmailer/lib/action_mailer/base.rb
Expand Up @@ -308,6 +308,25 @@ module ActionMailer
# Note that unless you have a specific reason to do so, you should prefer using before_action
# rather than after_action in your ActionMailer classes so that headers are parsed properly.
#
# = Previewing emails
#
# You can preview your email templates visually by adding a mailer preview file to the
# <tt>ActionMailer::Base.preview_path</tt>. Since most emails do something interesting
# with database data, you'll need to write some scenarios to load messages with fake data:
#
# class NotifierPreview < ActionMailer::Preview
# def welcome
# Notifier.welcome(User.first)
# end
# end
#
# Methods must return a Mail::Message object which can be generated by calling the mailer
# method without the additional <tt>deliver</tt>. The location of the mailer previews
# directory can be configured using the <tt>preview_path</tt> option which has a default
# of <tt>test/mailers/previews</tt>:
#
# config.action_mailer.preview_path = "#{Rails.root}/lib/mailer_previews"
#
# = Configuration options
#
# These options are specified on the class level, like
Expand Down Expand Up @@ -362,6 +381,7 @@ module ActionMailer
# <tt>delivery_method :test</tt>. Most useful for unit and functional testing.
class Base < AbstractController::Base
include DeliveryMethods
include Previews

abstract!

Expand Down
67 changes: 67 additions & 0 deletions actionmailer/lib/action_mailer/preview.rb
@@ -0,0 +1,67 @@
require 'active_support/descendants_tracker'

module ActionMailer
module Previews #:nodoc:
extend ActiveSupport::Concern

included do
# Set the location of mailer previews through app configuration:
#
# config.action_mailer.preview_path = "#{Rails.root}/lib/mailer_previews"
#
class_attribute :preview_path, instance_writer: false
end
end

class Preview
extend ActiveSupport::DescendantsTracker

class << self
# Returns all mailer preview classes
def all
load_previews if descendants.empty?
descendants
end

# Returns the mail object for the given email name
def call(email)
preview = self.new
preview.public_send(email)
end

# Returns all of the available email previews
def emails
public_instance_methods(false).map(&:to_s).sort
end

# Returns true if the email exists
def email_exists?(email)
emails.include?(email)
end

# Returns true if the preview exists
def exists?(preview)
all.any?{ |p| p.preview_name == preview }
end

# Find a mailer preview by its underscored class name
def find(preview)
all.find{ |p| p.preview_name == preview }
end

# Returns the underscored name of the mailer preview without the suffix
def preview_name
name.sub(/Preview$/, '').underscore
end

protected
def load_previews #:nodoc:
Dir["#{preview_path}/**/*_preview.rb"].each{ |file| require_dependency file }
end

def preview_path #:nodoc:
Base.preview_path
end
end
end
end
8 changes: 8 additions & 0 deletions actionmailer/lib/action_mailer/railtie.rb
Expand Up @@ -40,5 +40,13 @@ class Railtie < Rails::Railtie # :nodoc:
config.compile_methods! if config.respond_to?(:compile_methods!)
end
end

initializer "action_mailer.configure_mailer_previews", before: :set_autoload_paths do |app|
if Rails.env.development?
options = app.config.action_mailer
options.preview_path ||= defined?(Rails.root) ? "#{Rails.root}/test/mailers/previews" : nil
app.config.autoload_paths << options.preview_path
end
end
end
end
2 changes: 1 addition & 1 deletion actionpack/lib/action_dispatch/routing/inspector.rb
Expand Up @@ -69,7 +69,7 @@ def action
end

def internal?
controller.to_s =~ %r{\Arails/(info|welcome)} || path =~ %r{\A#{Rails.application.config.assets.prefix}}
controller.to_s =~ %r{\Arails/(info|mailers|welcome)} || path =~ %r{\A#{Rails.application.config.assets.prefix}}
end

def engine?
Expand Down
1 change: 1 addition & 0 deletions railties/lib/rails.rb
Expand Up @@ -25,6 +25,7 @@ module Rails

autoload :Info
autoload :InfoController
autoload :MailersController
autoload :WelcomeController

class << self
Expand Down
2 changes: 2 additions & 0 deletions railties/lib/rails/application/finisher.rb
Expand Up @@ -22,6 +22,8 @@ module Finisher
initializer :add_builtin_route do |app|
if Rails.env.development?
app.routes.append do
get '/rails/mailers' => "rails/mailers#index"
get '/rails/mailers/*path' => "rails/mailers#preview"
get '/rails/info/properties' => "rails/info#properties"
get '/rails/info/routes' => "rails/info#routes"
get '/rails/info' => "rails/info#index"
Expand Down
Expand Up @@ -4,11 +4,18 @@ module TestUnit # :nodoc:
module Generators # :nodoc:
class MailerGenerator < Base # :nodoc:
argument :actions, type: :array, default: [], banner: "method method"
check_class_collision suffix: "Test"

def check_class_collision
class_collisions "#{class_name}Test", "#{class_name}Preview"
end

def create_test_files
template "functional_test.rb", File.join('test/mailers', class_path, "#{file_name}_test.rb")
end

def create_preview_files
template "preview.rb", File.join('test/mailers/previews', class_path, "#{file_name}_preview.rb")
end
end
end
end
@@ -0,0 +1,11 @@
<% module_namespacing do -%>
class <%= class_name %>Preview < ActionMailer::Preview
<% actions.each do |action| -%>
def <%= action %>
<%= class_name %>.<%= action %>
end
<% end -%>
end
<% end -%>
73 changes: 73 additions & 0 deletions railties/lib/rails/mailers_controller.rb
@@ -0,0 +1,73 @@
require 'rails/application_controller'

class Rails::MailersController < Rails::ApplicationController # :nodoc:
prepend_view_path ActionDispatch::DebugExceptions::RESCUES_TEMPLATE_PATH

before_filter :require_local!
before_filter :find_preview, only: :preview

def index
@previews = ActionMailer::Preview.all
@page_title = "Mailer Previews"
end

def preview
if params[:path] == @preview.preview_name
@page_title = "Mailer Previews for #{@preview.preview_name}"
render action: 'mailer'
else
email = File.basename(params[:path])

if @preview.email_exists?(email)
@email = @preview.call(email)

if params[:part]
part_type = Mime::Type.lookup(params[:part])

if part = find_part(part_type)
response.content_type = part_type
render text: part.respond_to?(:decoded) ? part.decoded : part
else
raise AbstractController::ActionNotFound, "Email part '#{part_type}' not found in #{@preview.name}##{email}"
end
else
@part = find_preferred_part(request.format, Mime::HTML, Mime::TEXT)
render action: 'email', layout: false, formats: %w[html]
end
else
raise AbstractController::ActionNotFound, "Email '#{email}' not found in #{@preview.name}"
end
end
end

protected
def find_preview
candidates = []
params[:path].to_s.scan(%r{/|$}){ candidates << $` }
preview = candidates.detect{ |candidate| ActionMailer::Preview.exists?(candidate) }

if preview
@preview = ActionMailer::Preview.find(preview)
else
raise AbstractController::ActionNotFound, "Mailer preview '#{params[:path]}' not found"
end
end

def find_preferred_part(*formats)
if @email.multipart?
formats.each do |format|
return find_part(format) if @email.parts.any?{ |p| p.mime_type == format }
end
else
@email
end
end

def find_part(format)
if @email.multipart?
@email.parts.find{ |p| p.mime_type == format }
elsif @email.mime_type == format
@email
end
end
end
7 changes: 6 additions & 1 deletion railties/lib/rails/templates/layouts/application.html.erb
Expand Up @@ -29,7 +29,12 @@
</style>
</head>
<body>
<h2>Your App: <%= link_to 'properties', '/rails/info/properties' %> | <%= link_to 'routes', '/rails/info/routes' %></h2>
<h2>
Your App:
<%= link_to 'mailers', '/rails/mailers' %> |
<%= link_to 'properties', '/rails/info/properties' %> |
<%= link_to 'routes', '/rails/info/routes' %>
</h2>
<%= yield %>

</body>
Expand Down
98 changes: 98 additions & 0 deletions railties/lib/rails/templates/rails/mailers/email.html.erb
@@ -0,0 +1,98 @@
<!DOCTYPE html>
<html><head>
<meta name="viewport" content="width=device-width" />
<style type="text/css">
header {
width: 100%;
padding: 10px 0 0 0;
margin: 0;
background: white;
font: 12px "Lucida Grande", sans-serif;
border-bottom: 1px solid #dedede;
overflow: hidden;
}

dl {
margin: 0 0 10px 0;
padding: 0;
}

dt {
width: 80px;
padding: 1px;
float: left;
clear: left;
text-align: right;
color: #7f7f7f;
}

dd {
margin-left: 90px; /* 80px + 10px */
padding: 1px;
}

iframe {
border: 0;
width: 100%;
height: 800px;
}
</style>
</head>

<body>
<header>
<dl>
<% if @email.respond_to?(:smtp_envelope_from) && Array(@email.from) != Array(@email.smtp_envelope_from) %>
<dt>SMTP-From:</dt>
<dd><%= @email.smtp_envelope_from %></dd>
<% end %>
<% if @email.respond_to?(:smtp_envelope_to) && @email.to != @email.smtp_envelope_to %>
<dt>SMTP-To:</dt>
<dd><%= @email.smtp_envelope_to %></dd>
<% end %>

<dt>From:</dt>
<dd><%= @email.header['from'] %></dd>

<% if @email.reply_to %>
<dt>Reply-To:</dt>
<dd><%= @email.header['reply-to'] %></dd>
<% end %>

<dt>To:</dt>
<dd><%= @email.header['to'] %></dd>

<% if @email.cc %>
<dt>CC:</dt>
<dd><%= @email.header['cc'] %></dd>
<% end %>

<dt>Date:</dt>
<dd><%= Time.current.rfc2822 %></dd>

<dt>Subject:</dt>
<dd><strong><%= @email.subject %></strong></dd>

<% unless @email.attachments.nil? || @email.attachments.empty? %>
<dt>Attachments:</dt>
<dd>
<%= @email.attachments.map { |a| a.respond_to?(:original_filename) ? a.original_filename : a.filename }.inspect %>
</dd>
<% end %>
<% if @email.multipart? %>
<dd>
<select onchange="document.getElementsByName('messageBody')[0].src=this.options[this.selectedIndex].value;">
<option <%= request.format == Mime::HTML ? 'selected' : '' %> value="?part=text%2Fhtml">View as HTML email</option>
<option <%= request.format == Mime::TEXT ? 'selected' : '' %> value="?part=text%2Fplain">View as plain-text email</option>
</select>
</dd>
<% end %>
</dl>
</header>

<iframe seamless name="messageBody" src="?part=<%= Rack::Utils.escape(@part.mime_type) %>"></iframe>

</body>
</html>
8 changes: 8 additions & 0 deletions railties/lib/rails/templates/rails/mailers/index.html.erb
@@ -0,0 +1,8 @@
<% @previews.each do |preview| %>
<h3><%= link_to preview.preview_name.titleize, "/rails/mailers/#{preview.preview_name}" %></h3>
<ul>
<% preview.emails.each do |email| %>
<li><%= link_to email, "/rails/mailers/#{preview.preview_name}/#{email}" %></li>
<% end %>
</ul>
<% end %>
6 changes: 6 additions & 0 deletions railties/lib/rails/templates/rails/mailers/mailer.html.erb
@@ -0,0 +1,6 @@
<h3><%= @preview.preview_name.titleize %></h3>
<ul>
<% @preview.emails.each do |email| %>
<li><%= link_to email, "/rails/mailers/#{@preview.preview_name}/#{email}" %></li>
<% end %>
</ul>

0 comments on commit d6dec7f

Please sign in to comment.