├── spec ├── dummy │ ├── log │ │ └── .keep │ ├── app │ │ ├── mailers │ │ │ ├── .keep │ │ │ ├── application_mailer.rb │ │ │ ├── newsletter_mailer.rb │ │ │ └── auth_mailer.rb │ │ ├── models │ │ │ ├── .keep │ │ │ └── concerns │ │ │ │ └── .keep │ │ ├── assets │ │ │ ├── images │ │ │ │ ├── .keep │ │ │ │ └── cat.png │ │ │ ├── config │ │ │ │ └── manifest.js │ │ │ ├── stylesheets │ │ │ │ └── application.css │ │ │ └── javascripts │ │ │ │ └── application.js │ │ ├── controllers │ │ │ ├── concerns │ │ │ │ └── .keep │ │ │ ├── admin_controller.rb │ │ │ └── application_controller.rb │ │ ├── helpers │ │ │ └── application_helper.rb │ │ ├── views │ │ │ ├── rails_email_preview │ │ │ │ └── _my_hook.html.erb │ │ │ ├── auth_mailer │ │ │ │ ├── password_reset.html.erb │ │ │ │ └── email_confirmation.html.erb │ │ │ ├── newsletter_mailer │ │ │ │ ├── monthly_newsletter.html.erb │ │ │ │ └── weekly_newsletter.html.erb │ │ │ └── layouts │ │ │ │ └── admin.html.erb │ │ └── mailer_previews │ │ │ ├── auth_mailer_preview.rb │ │ │ └── newsletter_mailer_preview.rb │ ├── lib │ │ └── assets │ │ │ └── .keep │ ├── public │ │ ├── favicon.ico │ │ ├── 500.html │ │ ├── 422.html │ │ └── 404.html │ ├── config │ │ ├── locales │ │ │ ├── es.yml │ │ │ ├── de.yml │ │ │ └── en.yml │ │ ├── routes.rb │ │ ├── boot.rb │ │ ├── initializers │ │ │ ├── session_store.rb │ │ │ ├── secret_token.rb │ │ │ ├── filter_parameter_logging.rb │ │ │ ├── mime_types.rb │ │ │ ├── backtrace_silencers.rb │ │ │ ├── rails_email_preview.rb │ │ │ ├── wrap_parameters.rb │ │ │ └── inflections.rb │ │ ├── environment.rb │ │ ├── application.rb │ │ └── environments │ │ │ ├── development.rb │ │ │ ├── test.rb │ │ │ └── production.rb │ ├── README.rdoc │ ├── bin │ │ ├── rake │ │ ├── bundle │ │ └── rails │ ├── config.ru │ └── Rakefile ├── gemfiles │ ├── i18n-tasks.gemfile │ ├── rails_7_1.gemfile │ ├── rails_7_0.gemfile │ └── rails_6_1.gemfile ├── support │ ├── with_layout.rb │ └── save_screenshots.rb ├── features │ ├── email_test_send_spec.rb │ ├── take_screenshots_spec.rb │ ├── emails_list_spec.rb │ └── email_show_spec.rb ├── update_previews_generator_spec.rb ├── spec_helper.rb └── preview_list_presenter_spec.rb ├── lib ├── rails_email_preview │ ├── version.rb │ ├── engine.rb │ ├── delivery_handler.rb │ ├── main_app_route_delegator.rb │ ├── view_hooks.rb │ └── integrations │ │ └── comfortable_mexica_sofa.rb ├── generators │ └── rails_email_preview │ │ ├── install_generator.rb │ │ └── update_previews_generator.rb └── rails_email_preview.rb ├── doc └── img │ ├── rep-nav.png │ ├── rep-show.png │ ├── rep-edit-sofa.png │ └── rep-show-default.png ├── .gitmodules ├── Gemfile ├── app ├── assets │ ├── images │ │ └── rails_email_preview │ │ │ └── favicon.png │ └── stylesheets │ │ └── rails_email_preview │ │ ├── bootstrap3.css │ │ └── application.css ├── views │ ├── rails_email_preview │ │ └── emails │ │ │ ├── _i18n_nav.html.erb │ │ │ ├── _format_nav.html.erb │ │ │ ├── _headers.html.erb │ │ │ ├── _headers_and_nav.html.erb │ │ │ ├── _email_iframe.html.erb │ │ │ ├── _nav.html.erb │ │ │ ├── _send_form.html.erb │ │ │ ├── show.html.erb │ │ │ ├── index.html.erb │ │ │ └── email_iframe.js │ ├── layouts │ │ └── rails_email_preview │ │ │ ├── _flash_notices.html.erb │ │ │ ├── application.html.erb │ │ │ └── email.html.erb │ └── integrations │ │ └── cms │ │ ├── _customize_cms_for_rails_email_preview.html.erb │ │ ├── comfy_v1_integration.js │ │ └── comfy_v2_integration.js ├── controllers │ └── rails_email_preview │ │ ├── application_controller.rb │ │ └── emails_controller.rb ├── presenters │ └── rails_email_preview │ │ └── preview_list_presenter.rb ├── models │ └── rails_email_preview │ │ └── preview.rb └── helpers │ └── rails_email_preview │ └── emails_helper.rb ├── .gitignore ├── config ├── i18n-tasks.yml ├── routes.rb ├── locales │ ├── en.yml │ ├── es.yml │ ├── de.yml │ └── ru.yml └── initializers │ └── rails_email_preview.rb ├── shared.gemfile ├── .simplecov ├── rails_email_preview.gemspec ├── MIT-LICENSE ├── Rakefile ├── .github └── workflows │ └── tests.yml ├── CHANGES.md └── README.md /spec/dummy/log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/app/mailers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/app/models/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/lib/assets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/images/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/config/locales/es.yml: -------------------------------------------------------------------------------- 1 | es: 2 | dummy: 3 | hello: Hola -------------------------------------------------------------------------------- /spec/dummy/README.rdoc: -------------------------------------------------------------------------------- 1 | == README 2 | 3 | This is a dummy app for testing REP -------------------------------------------------------------------------------- /spec/dummy/config/locales/de.yml: -------------------------------------------------------------------------------- 1 | de: 2 | dummy: 3 | hello: Hallo 4 | -------------------------------------------------------------------------------- /spec/dummy/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | dummy: 3 | hello: Hello 4 | -------------------------------------------------------------------------------- /spec/dummy/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /spec/gemfiles/i18n-tasks.gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'i18n-tasks' 4 | -------------------------------------------------------------------------------- /lib/rails_email_preview/version.rb: -------------------------------------------------------------------------------- 1 | module RailsEmailPreview 2 | VERSION = '2.2.3' 3 | end 4 | -------------------------------------------------------------------------------- /doc/img/rep-nav.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glebm/rails_email_preview/HEAD/doc/img/rep-nav.png -------------------------------------------------------------------------------- /doc/img/rep-show.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glebm/rails_email_preview/HEAD/doc/img/rep-show.png -------------------------------------------------------------------------------- /spec/dummy/app/controllers/admin_controller.rb: -------------------------------------------------------------------------------- 1 | class AdminController < ApplicationController 2 | 3 | end -------------------------------------------------------------------------------- /spec/dummy/app/views/rails_email_preview/_my_hook.html.erb: -------------------------------------------------------------------------------- 1 | Hook <%= pos %> 2 | -------------------------------------------------------------------------------- /doc/img/rep-edit-sofa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glebm/rails_email_preview/HEAD/doc/img/rep-edit-sofa.png -------------------------------------------------------------------------------- /doc/img/rep-show-default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glebm/rails_email_preview/HEAD/doc/img/rep-show-default.png -------------------------------------------------------------------------------- /spec/dummy/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/images/cat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glebm/rails_email_preview/HEAD/spec/dummy/app/assets/images/cat.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "spec/screenshots"] 2 | path = spec/screenshots 3 | url = https://github.com/glebm/rep_spec_screenshots.git 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | gem 'rails' 6 | gem 'i18n-tasks' 7 | 8 | eval_gemfile './shared.gemfile' 9 | -------------------------------------------------------------------------------- /spec/dummy/app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default :from => 'test@test.com' 3 | end 4 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../javascripts .js 3 | //= link_directory ../stylesheets .css 4 | -------------------------------------------------------------------------------- /spec/dummy/app/views/auth_mailer/password_reset.html.erb: -------------------------------------------------------------------------------- 1 |

Click below to reset your password:

2 | 3 |

Reset password

4 | -------------------------------------------------------------------------------- /app/assets/images/rails_email_preview/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glebm/rails_email_preview/HEAD/app/assets/images/rails_email_preview/favicon.png -------------------------------------------------------------------------------- /spec/dummy/bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /spec/dummy/app/views/newsletter_mailer/monthly_newsletter.html.erb: -------------------------------------------------------------------------------- 1 |

Hello user, here is your monthly newsletter

2 | 3 |
Lorem ipsum
4 | -------------------------------------------------------------------------------- /spec/dummy/app/views/newsletter_mailer/weekly_newsletter.html.erb: -------------------------------------------------------------------------------- 1 |

Hello user, here is your weekly newsletter

2 | 3 |
Lorem ipsum
4 | -------------------------------------------------------------------------------- /spec/dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.routes.draw do 2 | 3 | mount RailsEmailPreview::Engine, at: 'rep-emails' 4 | root to: redirect('/rep-emails') 5 | end 6 | -------------------------------------------------------------------------------- /spec/gemfiles/rails_7_1.gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec path: '../..' 4 | eval_gemfile '../../shared.gemfile' 5 | 6 | gem 'rails', '~> 7.1' 7 | 8 | -------------------------------------------------------------------------------- /spec/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | # Set up gems listed in the Gemfile. 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../../Gemfile', __FILE__) 3 | require 'bundler/setup' 4 | -------------------------------------------------------------------------------- /spec/gemfiles/rails_7_0.gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec path: '../..' 4 | eval_gemfile '../../shared.gemfile' 5 | 6 | gem 'rails', '~> 7.0.4' 7 | 8 | -------------------------------------------------------------------------------- /spec/dummy/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../../config/application', __FILE__) 3 | require_relative '../config/boot' 4 | require 'rails/commands' 5 | -------------------------------------------------------------------------------- /lib/rails_email_preview/engine.rb: -------------------------------------------------------------------------------- 1 | module ::RailsEmailPreview 2 | class Engine < Rails::Engine 3 | isolate_namespace RailsEmailPreview 4 | load_generators 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/dummy/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Rails.application 5 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Dummy::Application.config.session_store :cookie_store, key: '_dummy_session' 4 | -------------------------------------------------------------------------------- /spec/gemfiles/rails_6_1.gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec path: '../..' 4 | eval_gemfile '../../shared.gemfile' 5 | 6 | gem 'rails', '~> 6.1.7' 7 | gem 'puma', '~> 5.6.5' 8 | -------------------------------------------------------------------------------- /spec/dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the Rails application. 5 | Dummy::Application.initialize! 6 | -------------------------------------------------------------------------------- /spec/support/with_layout.rb: -------------------------------------------------------------------------------- 1 | module WithLayout 2 | def with_layout(layout) 3 | RailsEmailPreview.layout = layout 4 | yield 5 | ensure 6 | RailsEmailPreview.layout = nil 7 | end 8 | end -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.lock 2 | *.gem 3 | .idea/ 4 | .rvmrc 5 | .bundle/ 6 | .ruby-version 7 | .ruby-gemset 8 | log/*.log 9 | pkg/ 10 | spec/dummy/db/*.sqlite3 11 | spec/dummy/log/*.log 12 | spec/dummy/tmp/ 13 | coverage/ 14 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.config.secret_key_base = '4380f36fda304251bf48f12ad4474b6d11447f1f959bd5b77a5d56c92b97f4c403ee0ae13d31a85ed88058ff8795bf31ec17e70e5c229b3707a77a2ee7e81724' 2 | -------------------------------------------------------------------------------- /app/views/rails_email_preview/emails/_i18n_nav.html.erb: -------------------------------------------------------------------------------- 1 |

2 | <% @preview.locales.each do |locale| %><%= content_tag :a, locale_name(locale), change_locale_attr(locale) %><% end %> 3 |

4 | -------------------------------------------------------------------------------- /app/views/rails_email_preview/emails/_format_nav.html.erb: -------------------------------------------------------------------------------- 1 |

2 | <% @preview.formats.each do |format| %><%= content_tag :a, format_label(format), change_format_attr(format) -%><% end %> 3 |

4 | 5 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | # Mime::Type.register_alias "text/html", :iphone 6 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | # Prevent CSRF attacks by raising an exception. 3 | # For APIs, you may want to use :null_session instead. 4 | protect_from_forgery with: :exception 5 | end 6 | -------------------------------------------------------------------------------- /spec/dummy/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path('../config/application', __FILE__) 5 | 6 | Dummy::Application.load_tasks 7 | -------------------------------------------------------------------------------- /config/i18n-tasks.yml: -------------------------------------------------------------------------------- 1 | base_locale: en 2 | locales: [en, de, ru, es] 3 | search: 4 | paths: 5 | - app/ 6 | - lib/ 7 | data: 8 | yaml: 9 | write: 10 | line_width: -1 11 | ignore_inconsistent_interpolations: 12 | - 'rep.base.email.one' 13 | - 'rep.base.mailer.one' 14 | -------------------------------------------------------------------------------- /spec/dummy/app/views/layouts/admin.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dummy Admin 5 | <%= stylesheet_link_tag "application", media: "all" %> 6 | <%= csrf_meta_tags %> 7 | 8 | 9 | 10 | <%= yield %> 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /spec/dummy/app/mailer_previews/auth_mailer_preview.rb: -------------------------------------------------------------------------------- 1 | class AuthMailerPreview 2 | def email_confirmation 3 | AuthMailer.email_confirmation 'test-user@test.com', @token || '73570k3n' 4 | end 5 | 6 | def password_reset 7 | AuthMailer.password_reset 'test-user@test.com' 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/dummy/app/mailers/newsletter_mailer.rb: -------------------------------------------------------------------------------- 1 | class NewsletterMailer < ApplicationMailer 2 | def weekly_newsletter(email) 3 | mail to: email 4 | end 5 | def monthly_newsletter(email) 6 | mail to: email 7 | end 8 | def quarterly_newsletter(email) 9 | mail to: email 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/dummy/app/mailer_previews/newsletter_mailer_preview.rb: -------------------------------------------------------------------------------- 1 | class NewsletterMailerPreview 2 | def weekly_newsletter 3 | NewsletterMailer.weekly_newsletter 'test-user@test.com' 4 | end 5 | 6 | def monthly_newsletter 7 | NewsletterMailer.monthly_newsletter 'test-user@test.com' 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/views/rails_email_preview/emails/_headers.html.erb: -------------------------------------------------------------------------------- 1 | <%= with_show_hook :headers do %> 2 |
3 | <%= with_show_hook :headers_content do %> 4 | <% human_headers mail do |name, value| %> 5 |
<%= name %>
6 |
<%= value %>
7 | <% end %> 8 | <% end %> 9 |
10 | <% end %> 11 | -------------------------------------------------------------------------------- /lib/rails_email_preview/delivery_handler.rb: -------------------------------------------------------------------------------- 1 | module RailsEmailPreview 2 | class DeliveryHandler 3 | def initialize(mail, headers) 4 | @mail = mail 5 | @mail.headers(headers) 6 | @mail.delivery_handler = self 7 | end 8 | 9 | attr_accessor :mail 10 | 11 | def deliver_mail(mail) 12 | yield 13 | end 14 | end 15 | end -------------------------------------------------------------------------------- /spec/dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require 'action_controller/railtie' 4 | require 'action_mailer/railtie' 5 | 6 | Bundler.require(*Rails.groups) 7 | require 'rails_email_preview' 8 | 9 | module Dummy 10 | class Application < Rails::Application 11 | config.i18n.available_locales = [:es, :en, :de, :ru] 12 | config.i18n.default_locale = :en 13 | end 14 | end 15 | 16 | -------------------------------------------------------------------------------- /shared.gemfile: -------------------------------------------------------------------------------- 1 | if defined?(JRUBY_VERSION) 2 | # JRuby attempts to install v0.9.2 even though jruby's reported required version is too low for it. 3 | gem 'i18n-tasks', '~> 0.8.7' 4 | end 5 | 6 | if !ENV['CI'] 7 | group :test, :development do 8 | gem 'byebug', platform: [:mri], require: false 9 | end 10 | end 11 | 12 | if ENV['COVERAGE'] 13 | group :test do 14 | gem 'simplecov', require: false 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/features/email_test_send_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'email test send', :type => :feature do 4 | let(:url_args) { {preview_id: 'auth_mailer_preview-email_confirmation'} } 5 | it 'shows email' do 6 | page.driver.post rails_email_preview.rep_test_deliver_path(url_args), {recipient_email: 'test@test.com'} 7 | expect(page.driver.response.location).to eq rails_email_preview.rep_email_url(url_args) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /spec/support/save_screenshots.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | module SaveScreenshots 3 | def screenshot!(name) 4 | return unless [:selenium, :webkit, :cuprite].include?(Capybara.current_driver) 5 | dir = File.expand_path('../screenshots', File.dirname(__FILE__)) 6 | name = "#{name}.png" unless name =~ /\.png$/ 7 | FileUtils.mkpath(dir) unless File.directory?(dir) 8 | puts "saving screenshot: #{name}" 9 | save_screenshot File.join(dir, name), full: true 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/views/layouts/rails_email_preview/_flash_notices.html.erb: -------------------------------------------------------------------------------- 1 | <% flash.each do |name, msg| %> 2 | <% next unless msg.is_a?(String) %> 3 |
4 | 5 | <%= content_tag :div, name.to_s.end_with?('_html') ? msg.html_safe : h(msg).gsub("\n", "
").html_safe, id: "flash_#{name.to_s.sub(/_html$/, '')}" %> 6 |
7 | <% end %> 8 | -------------------------------------------------------------------------------- /spec/dummy/app/mailers/auth_mailer.rb: -------------------------------------------------------------------------------- 1 | class AuthMailer < ApplicationMailer 2 | def email_confirmation(email, token) 3 | @token = token 4 | attachments['token.txt'] = @token 5 | attachments.inline['cat.png'] = File.read(Rails.application.root.join('app/assets/images/cat.png').to_s, mode: 'rb') 6 | mail reply_to: 'support@site.com', to: email, subject: 'Dummy Email Confirmation' 7 | end 8 | def password_reset(email) 9 | mail reply_to: 'support@site.com', to: email 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/rails_email_preview.rb: -------------------------------------------------------------------------------- 1 | require 'rails_email_preview' 2 | 3 | RailsEmailPreview.view_hooks.add_render :headers_and_nav, :before, partial: 'rails_email_preview/my_hook', locals: {pos: 'before headers_and_nav'} 4 | RailsEmailPreview.view_hooks.add_render :headers_content, :after, partial: 'rails_email_preview/my_hook', locals: {pos: 'after headers_content'} 5 | 6 | Rails.application.config.to_prepare do 7 | RailsEmailPreview.preview_classes = RailsEmailPreview.find_preview_classes(Rails.root.join 'app/mailer_previews') 8 | end 9 | -------------------------------------------------------------------------------- /app/views/rails_email_preview/emails/_headers_and_nav.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | 8 | 11 |
12 |
13 | -------------------------------------------------------------------------------- /lib/rails_email_preview/main_app_route_delegator.rb: -------------------------------------------------------------------------------- 1 | module RailsEmailPreview::MainAppRouteDelegator 2 | # delegate url helpers to main_app 3 | def method_missing(method, *args, &block) 4 | if main_app_route_method?(method) 5 | main_app.send(method, *args) 6 | else 7 | super 8 | end 9 | end 10 | 11 | def respond_to_missing?(method) 12 | super || main_app_route_method?(method) 13 | end 14 | 15 | private 16 | def main_app_route_method?(method) 17 | method.to_s =~ /_(?:path|url)$/ && main_app.respond_to?(method) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /.simplecov: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | SimpleCov.start do 3 | add_filter '/spec/' 4 | add_group 'Commands', 'app/commands' 5 | add_group 'Controllers', 'app/controllers' 6 | add_group 'Forms', 'app/forms' 7 | add_group 'Helpers', 'app/helpers' 8 | add_group 'Jobs', 'app/jobs' 9 | add_group 'Mailers', %w(app/mailers app/mailer_previews) 10 | add_group 'Models', 'app/models' 11 | add_group 'Policies', 'app/policies' 12 | add_group 'View models', 'app/view_models' 13 | add_group 'Lib', 'lib/' 14 | formatter SimpleCov::Formatter::HTMLFormatter unless ENV['CI'] 15 | end 16 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] if respond_to?(:wrap_parameters) 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the top of the 9 | * compiled file, but it's generally better to create a new file per style scope. 10 | * 11 | *= require_self 12 | *= require_tree . 13 | */ 14 | -------------------------------------------------------------------------------- /app/views/rails_email_preview/emails/_email_iframe.html.erb: -------------------------------------------------------------------------------- 1 |
2 | 8 | 9 | 10 | <%= javascript_tag render(template: 'rails_email_preview/emails/email_iframe', formats: [:js]), 11 | **(RailsEmailPreview.rails_supports_csp_nonce? ? {nonce: true} : {}) %> 12 |
13 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. 9 | // 10 | // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require_tree . 14 | -------------------------------------------------------------------------------- /app/views/rails_email_preview/emails/_nav.html.erb: -------------------------------------------------------------------------------- 1 | <%# Locale, format and actions %> 2 | <%= with_show_hook :nav do %> 3 |
4 | <% if I18n.available_locales.length > 1 %> 5 | <%= with_show_hook :nav_i18n do %> 6 | <%= render 'rails_email_preview/emails/i18n_nav' %> 7 | <% end %> 8 | <% end %> 9 | <%= with_show_hook :nav_format do %> 10 | <%= render 'rails_email_preview/emails/format_nav' %> 11 | <% end %> 12 | <% if RailsEmailPreview.enable_send_email %> 13 | <%= with_show_hook :nav_send do %> 14 | <%= render 'rails_email_preview/emails/send_form' %> 15 | <% end %> 16 | <% end %> 17 |
18 | <% end %> 19 | -------------------------------------------------------------------------------- /spec/features/take_screenshots_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | unless ENV['TRAVIS'] 3 | describe 'Take screenshots', type: :feature, js: true do 4 | it 'list page' do 5 | visit rails_email_preview.rep_root_path 6 | screenshot! 'list' 7 | end 8 | 9 | it 'list page in de' do 10 | begin 11 | RailsEmailPreview.locale = :de 12 | visit rails_email_preview.rep_root_path 13 | screenshot! 'list-de' 14 | ensure 15 | RailsEmailPreview.locale = nil 16 | end 17 | end 18 | 19 | it 'show email page' do 20 | visit rails_email_preview.rep_email_path(preview_id: 'auth_mailer_preview-email_confirmation') 21 | screenshot! 'show' 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/controllers/rails_email_preview/application_controller.rb: -------------------------------------------------------------------------------- 1 | module RailsEmailPreview 2 | class ApplicationController < ::RailsEmailPreview.parent_controller.constantize 3 | layout 'rails_email_preview/application' 4 | 5 | protected 6 | 7 | def prevent_browser_caching 8 | # Prevent back-button browser caching: 9 | # HTTP/1.1 10 | response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate' 11 | # Date in the past 12 | response.headers['Expires'] = 'Mon, 26 Jul 1997 05:00:00 GMT' 13 | # Always modified 14 | response.headers['Last-Modified'] = Time.now.httpdate 15 | # HTTP/1.0 16 | response.headers['Pragma'] = 'no-cache' 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /lib/generators/rails_email_preview/install_generator.rb: -------------------------------------------------------------------------------- 1 | module RailsEmailPreview 2 | module Generators 3 | class InstallGenerator < Rails::Generators::Base 4 | desc "creates an initializer file at config/initializers/rails_email_preview.rb and adds REP route to config/routes.rb" 5 | source_root File.expand_path('../../../..', __FILE__) 6 | 7 | def generate_initialization 8 | copy_file 'config/initializers/rails_email_preview.rb', 'config/initializers/rails_email_preview.rb' 9 | end 10 | 11 | def generate_routing 12 | route "mount RailsEmailPreview::Engine, at: 'emails'" 13 | log "# You can access REP urls like this: rails_email_preview.rep_emails_url #=> '/emails'" 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/views/rails_email_preview/emails/_send_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_tag rails_email_preview.rep_test_deliver_url(params: preview_params), method: :post, 2 | class: 'rep--send-to-form' do %> 3 | <%= hidden_field_tag :email_locale, @email_locale %> 4 | <%# Indentation ensures the lack of a space in between the button and the field: %> 5 | <%= content_tag :button, t('.send_btn'), type: 'submit', 6 | class: rep_style[:btn_danger_class], 7 | 'data-confirm' => t('.send_are_you_sure') -%> 8 | <%= text_field_tag :recipient_email, '', type: :email, required: true, 9 | class: rep_style[:form_control_class], 10 | placeholder: t('.send_recipient_placeholder') -%> 11 | <% end %> 12 | -------------------------------------------------------------------------------- /app/views/layouts/rails_email_preview/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= t('.head_title') %> 5 | <%= csrf_meta_tag %> 6 | <%= csp_meta_tag if RailsEmailPreview.rails_supports_csp_nonce? %> 7 | <%= tag :link, rel: 'icon', type: 'image/png', href: "data:image/png;base64,#{Base64.strict_encode64(File.binread(RailsEmailPreview::Engine.root.join("app/assets/images/rails_email_preview/favicon.png")))}" %> 8 | <%= content_tag :style, File.read(RailsEmailPreview::Engine.root.join("app/assets/stylesheets/rails_email_preview/application.css")).html_safe, **(RailsEmailPreview.rails_supports_csp_nonce? ? {nonce: content_security_policy_nonce} : {}) %> 9 | <%= yield(:head) %> 10 | 11 | 12 |
13 | <%= render 'layouts/rails_email_preview/flash_notices' %> 14 | <%= yield %> 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports and disable caching. 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send. 17 | config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger. 20 | config.active_support.deprecation = :log 21 | 22 | config.active_support.deprecation = :stderr 23 | end 24 | -------------------------------------------------------------------------------- /app/views/layouts/rails_email_preview/email.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= content_tag(:style, **(RailsEmailPreview.rails_supports_csp_nonce? ? {nonce: content_security_policy_nonce} : {})) do %> 5 | #message_body, #raw_message { 6 | font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 7 | word-break: break-word; 8 | } 9 | 10 | #error { 11 | padding: 15px; 12 | margin-bottom: 20px; 13 | border: 1px solid transparent; 14 | border-radius: 4px; 15 | background-color: #f2dede; 16 | border-color: #ebccd1; 17 | color: #a94442; 18 | } 19 | 20 | body { 21 | background-color: white; 22 | } 23 | <% end %> 24 | <%= javascript_tag **(RailsEmailPreview.rails_supports_csp_nonce? ? {nonce: true} : {}) do %> 25 | document.addEventListener('DOMContentLoaded', window.parent.rep.iframeOnDOMContentLoaded, true); 26 | <% end %> 27 | 28 | 29 |
30 | <%= yield %>
31 | 32 | 33 | -------------------------------------------------------------------------------- /spec/features/emails_list_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'emails list', :type => :feature do 4 | it 'shows emails' do 5 | visit rails_email_preview.rep_root_path 6 | [I18n.t('rails_email_preview.emails.index.list_title'), 'Auth', 'Email confirmation', 'Newsletter', 'Weekly newsletter', '4 emails in 2 mailers'].each do |text| 7 | expect(page).to have_content text 8 | end 9 | end 10 | 11 | it 'uses REP template by default' do 12 | visit rails_email_preview.rep_root_path 13 | expect(page).to have_title I18n.t('layouts.rails_email_preview.application.head_title') 14 | 15 | favicon = find 'head > link[rel=icon]', visible: :all 16 | expect(favicon[:href]).to start_with 'data:image/png;base64,' 17 | 18 | style = find 'head > style', visible: :all 19 | expect(style.text(:all)).to include '#rep-src-iframe-container' 20 | end 21 | 22 | it 'uses app template when specified' do 23 | with_layout 'admin' do 24 | visit rails_email_preview.rep_root_path 25 | expect(page).to have_title 'Dummy Admin' 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /app/presenters/rails_email_preview/preview_list_presenter.rb: -------------------------------------------------------------------------------- 1 | module RailsEmailPreview 2 | class PreviewListPresenter 3 | attr_reader :previews 4 | 5 | def initialize(previews) 6 | @previews = previews 7 | end 8 | 9 | def columns(&block) 10 | return to_enum(:columns) unless block_given? 11 | split_in_halves(groups) { |_k, v| v.length }.each do |column_groups| 12 | block.yield(column_groups) 13 | end 14 | end 15 | 16 | def groups 17 | @groups ||= by_class_name.inject({}) do |h, (_class_name, previews)| 18 | h.update previews.first.group_name => previews 19 | end 20 | end 21 | 22 | private 23 | 24 | def split_in_halves(xs, &weight) 25 | xs = xs.to_a 26 | ws = xs.map(&weight) 27 | col_w = (ws.sum + 1) / 2 28 | cur_w = 0 29 | mid = ws.find_index { |w| (cur_w += w) >= col_w + w } || [ws.length - 1, 0].max 30 | [xs.first(mid), xs.from(mid)] 31 | end 32 | 33 | def by_class_name 34 | @by_class_name ||= previews.group_by(&:preview_class_name) 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /rails_email_preview.gemspec: -------------------------------------------------------------------------------- 1 | require File.expand_path('../lib/rails_email_preview/version', __FILE__) 2 | 3 | 4 | Gem::Specification.new do |s| 5 | s.name = 'rails_email_preview' 6 | s.author = 'Gleb Mazovetskiy' 7 | s.email = 'glex.spb@gmail.com' 8 | s.homepage = 'https://github.com/glebm/rails_email_preview' 9 | s.license = 'MIT' 10 | 11 | s.summary = 'Preview emails in browser (rails engine)' 12 | s.description = 'A Rails Engine to preview plain text and html email in your browser' 13 | 14 | s.files = Dir['{app,lib,config}/**/*'] + ['MIT-LICENSE', 'Rakefile', 'Gemfile', 'README.md'] 15 | 16 | if s.respond_to?(:metadata=) 17 | s.metadata = { 'issue_tracker' => 'https://github.com/glebm/rails_email_preview' } 18 | end 19 | 20 | s.add_dependency 'rails', '>= 6.1' 21 | s.add_dependency 'request_store' 22 | 23 | s.add_development_dependency 'capybara', '>= 3.1.1' 24 | s.add_development_dependency 'cuprite', '>= 0.10' 25 | s.add_development_dependency 'rspec-rails', '>= 3.8.0' 26 | s.add_development_dependency 'puma', '>= 3.9.1' 27 | 28 | s.version = RailsEmailPreview::VERSION 29 | end 30 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2012-2013 Gleb Mazovetskiy 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | RailsEmailPreview::Engine.routes.draw do 2 | controller :emails do 3 | scope path: '(:email_locale)', 4 | # This constraint resolves ambiguity with :preview_id, allowing locale to be optional 5 | constraints: {email_locale: /#{I18n.available_locales.map(&Regexp.method(:escape)) * '|'}/}, 6 | defaults: {email_locale: I18n.default_locale.to_s} do 7 | get '/' => :index, as: :rep_emails 8 | scope path: ':preview_id', constraints: {preview_id: /\w+-\w+/} do 9 | scope '(:part_type)', 10 | constraints: {part_type: /html|plain|raw/}, 11 | defaults: {part_type: 'html'} do 12 | get '' => :show, as: :rep_email 13 | get 'body' => :show_body, as: :rep_raw_email 14 | end 15 | post 'deliver' => :test_deliver, as: :rep_test_deliver 16 | get 'attachments/:filename' => :show_attachment, as: :rep_raw_email_attachment 17 | get 'headers' => :show_headers, as: :rep_email_headers 18 | end 19 | # alias rep_emails_url to its stable api name 20 | get '/' => :index, as: :rep_root 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/views/rails_email_preview/emails/show.html.erb: -------------------------------------------------------------------------------- 1 | <%- # Using ERB for this template to have precise control of tag indentation for correct rendering of raw and text. -%> 2 |
3 |
4 | <%= with_show_hook :breadcrumb do %> 5 |
    6 | <%= with_show_hook :breadcrumb_content do %> 7 |
  1. 8 | <%= t '.breadcrumb_list' %> 9 |
  2. <%= @preview.name %>
  3. 10 | <% end %> 11 |
12 | <% end %> 13 | 14 | <%= with_show_hook :headers_and_nav do %> 15 | <%= render 'rails_email_preview/emails/headers_and_nav' %> 16 | <% end %> 17 | 18 | <%= with_show_hook :email_body do %> 19 | <%- # actual email content, rendered in an iframe to prevent browser styles from interfering -%> 20 | <%= render 'rails_email_preview/emails/email_iframe' %> 21 | <% end %> 22 |
23 |
24 | -------------------------------------------------------------------------------- /spec/dummy/app/views/auth_mailer/email_confirmation.html.erb: -------------------------------------------------------------------------------- 1 | <%= image_tag self.attachments['cat.png'].url, width: '200px', style: 'float: right' %> 2 |

<%= t 'dummy.hello' %> user, here is your confirmation token: <%= @token %>

3 | 4 |
5 | Confirm email address 6 |
7 | 8 |

Thredded is a Rails 4.1+ forum/messageboard engine. Its goal is to be as simple and feature rich as possible.

9 | 10 |

Some of the features currently in Thredded:

11 | 12 | 23 | 24 |

Planned features:

25 | 26 | 29 | 30 |

Thanks for registering! Enjoy!

31 | -------------------------------------------------------------------------------- /app/views/integrations/cms/_customize_cms_for_rails_email_preview.html.erb: -------------------------------------------------------------------------------- 1 | <% 2 | # When rendering inside rails_email_preview hide nav, and hide irrelevant things from the form 3 | adapter = ::RailsEmailPreview::Integrations::ComfortableMexicanSofa 4 | snippet = @snippet || (adapter.cms_snippet_class === @record && @record) 5 | if snippet && (p = adapter.rep_email_params_from_snippet(snippet)) 6 | show_url = rails_email_preview.rep_raw_email_url(p) 7 | end 8 | customize_form = show_url.present? || snippet && !snippet.persisted? 9 | %> 10 | <% if customize_form %> 11 | 12 |
16 | <% if adapter.cms_v2_plus? %> 17 | <%= javascript_tag render(template: 'integrations/cms/comfy_v2_integration.js'), 18 | **(RailsEmailPreview.rails_supports_csp_nonce? ? {nonce: true} : {}) %> 19 | <% else # CMS v1 %> 20 | <%= javascript_tag render(template: 'integrations/cms/comfy_v1_integration.js'), 21 | **(RailsEmailPreview.rails_supports_csp_nonce? ? {nonce: true} : {}) %> 22 | <% end %> 23 | <% end %> 24 | -------------------------------------------------------------------------------- /app/views/rails_email_preview/emails/index.html.erb: -------------------------------------------------------------------------------- 1 | <% 2 | list = @list 3 | previews = @previews 4 | %> 5 | 6 |
7 |

<%= t '.list_title' %>

8 | 9 | <%= with_index_hook :list do %> 10 |
11 | <% list.columns do |groups| %> 12 | 25 | <% end %> 26 |
27 | <% end %> 28 |
29 | 36 |
37 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | --- 2 | en: 3 | integrations: 4 | cms: 5 | customize_cms_for_rails_email_preview: 6 | edit_email: Editing email 7 | view_link: View 8 | errors: 9 | site_missing: Please create a CMS site for %{locale} first. When using multiple locales the site should be Mirrored. 10 | layouts: 11 | rails_email_preview: 12 | application: 13 | head_title: Emails - REP 14 | rails_email_preview: 15 | emails: 16 | index: 17 | list_title: Application Emails 18 | send_form: 19 | send_are_you_sure: This will actually send this email. Are you sure? 20 | send_btn: Send to 21 | send_recipient_placeholder: Email 22 | show: 23 | breadcrumb_list: Emails 24 | rep: 25 | base: 26 | email: 27 | one: 1 email 28 | other: "%{count} emails" 29 | in: in 30 | loading: Loading... 31 | mailer: 32 | one: 1 mailer 33 | other: "%{count} mailers" 34 | errors: 35 | email_missing_format: Format missing 36 | headers: 37 | attachments: Attachments 38 | bcc: BCC 39 | cc: CC 40 | from: From 41 | reply_to: Reply to 42 | subject: Subject 43 | to: To 44 | test_deliver: 45 | no_delivery_method: Please set 'config.action_mailer.delivery_method' to send emails in '%{environment}' environment 46 | provide_email: Send to which address? 47 | sent_notice: Sent to %{address} via %{delivery_method} 48 | -------------------------------------------------------------------------------- /config/locales/es.yml: -------------------------------------------------------------------------------- 1 | --- 2 | es: 3 | integrations: 4 | cms: 5 | customize_cms_for_rails_email_preview: 6 | edit_email: Editando el email 7 | view_link: Ver 8 | errors: 9 | site_missing: Por favor, primero crea un CMS para %{locale}. Cuando estes usando varios idiomas el site debería estar 10 | layouts: 11 | rails_email_preview: 12 | application: 13 | head_title: Emails - REP 14 | rails_email_preview: 15 | emails: 16 | index: 17 | list_title: Emails de la aplicación 18 | send_form: 19 | send_are_you_sure: Esto enviara el email. ¿Estás seguro? 20 | send_btn: Enviar a 21 | send_recipient_placeholder: Email 22 | show: 23 | breadcrumb_list: Emails 24 | rep: 25 | base: 26 | email: 27 | one: 1 email 28 | other: "%{count} emails" 29 | in: en 30 | loading: Cargando... 31 | mailer: 32 | one: 1 mailer 33 | other: "%{count} mailers" 34 | errors: 35 | email_missing_format: Falta formato 36 | headers: 37 | attachments: Adjuntos 38 | bcc: BCC 39 | cc: CC 40 | from: De 41 | reply_to: Responder a 42 | subject: Asunto 43 | to: A 44 | test_deliver: 45 | no_delivery_method: Por favor configura 'config.action_mailer.delivery_method' para enviar correos en el entorno de '%{environment}' 46 | provide_email: "¿A que dirección?" 47 | sent_notice: Enviado a %{address} vía %{delivery_method} 48 | -------------------------------------------------------------------------------- /spec/dummy/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 48 | 49 | 50 | 51 | 52 |
53 |

We're sorry, but something went wrong.

54 |
55 |

If you are the application owner check the logs for more information.

56 | 57 | 58 | -------------------------------------------------------------------------------- /spec/update_previews_generator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'rails g rails_email_preview:update_previews' do 4 | context 'new mailer class' do 5 | let(:test_mailer_path) { 'app/mailers/new_mailer.rb' } 6 | let(:test_mailer_src) { <<-RUBY } 7 | class NewMailer < ApplicationMailer 8 | def notification(user) 9 | mail to: user.email 10 | end 11 | end 12 | RUBY 13 | let(:expected_preview_path) { 'app/mailer_previews/new_mailer_preview.rb' } 14 | let(:expected_preview_src) { <<-RUBY } 15 | class NewMailerPreview 16 | def notification 17 | NewMailer.notification user 18 | end 19 | end 20 | RUBY 21 | before do 22 | File.open(test_mailer_path, 'w') { |f| f.write test_mailer_src } 23 | end 24 | after do 25 | [expected_preview_path, test_mailer_path].each do |f| 26 | FileUtils.rm(f) if File.exist?(f) 27 | Object.send(:remove_const, :NewMailer) if defined?(NewMailer) 28 | Object.send(:remove_const, :NewMailerPreview) if defined?(NewMailerPreview) 29 | end 30 | end 31 | it 'creates a stub preview class' do 32 | if Rails.respond_to?(:autoloaders) && Rails.autoloaders.respond_to?(:zeitwerk_enabled?) 33 | require Rails.root.join(test_mailer_path) 34 | end 35 | Rails::Generators.invoke('rails_email_preview:update_previews', []) 36 | path = Rails.root.join(expected_preview_path) 37 | expect(File).to exist(path) 38 | expect(File.read(path)).to( 39 | eq(expected_preview_src) 40 | ) 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /config/initializers/rails_email_preview.rb: -------------------------------------------------------------------------------- 1 | require 'rails_email_preview' 2 | 3 | #= REP hooks and config 4 | #RailsEmailPreview.setup do |config| 5 | # 6 | # # hook before rendering preview: 7 | # config.before_render do |message, preview_class_name, mailer_action| 8 | # # Use roadie-rails: 9 | # Roadie::Rails::MailInliner.new(message, message.roadie_options).execute 10 | # # Use premailer-rails: 11 | # Premailer::Rails::Hook.delivering_email(message) 12 | # # Use actionmailer-inline-css: 13 | # ActionMailer::InlineCssHook.delivering_email(message) 14 | # end 15 | # 16 | # # do not show Send Email button 17 | # config.enable_send_email = false 18 | # 19 | # # You can specify a controller for RailsEmailPreview::ApplicationController to inherit from: 20 | # config.parent_controller = 'Admin::ApplicationController' # default: '::ApplicationController' 21 | #end 22 | 23 | #= REP + Comfortable Mexican Sofa integration 24 | # 25 | # # enable comfortable_mexican_sofa integration: 26 | # require 'rails_email_preview/integrations/comfortable_mexica_sofa' 27 | 28 | Rails.application.config.to_prepare do 29 | # Render REP inside a custom layout (set to 'application' to use app layout, default is REP's own layout) 30 | # This will also make application routes accessible from within REP: 31 | # RailsEmailPreview.layout = 'admin' 32 | 33 | # Set UI locale to something other than :en 34 | # RailsEmailPreview.locale = :de 35 | 36 | # Auto-load preview classes from: 37 | RailsEmailPreview.preview_classes = RailsEmailPreview.find_preview_classes('app/mailer_previews') 38 | end 39 | -------------------------------------------------------------------------------- /config/locales/de.yml: -------------------------------------------------------------------------------- 1 | --- 2 | de: 3 | integrations: 4 | cms: 5 | customize_cms_for_rails_email_preview: 6 | edit_email: E-Mail bearbeiten 7 | view_link: Anzeigen 8 | errors: 9 | site_missing: Bitte erstellen Sie eine CMS-Website für %{locale} zuerst. Bei der Verwendung von mehreren Gebietsschemas sollte der Standort gespiegelt werden. 10 | layouts: 11 | rails_email_preview: 12 | application: 13 | head_title: E-Mails - REP 14 | rails_email_preview: 15 | emails: 16 | index: 17 | list_title: Anwendungs E-Mails 18 | send_form: 19 | send_are_you_sure: Diese Aktion wird diese E-Mail wirklich versenden. Bist du sicher? 20 | send_btn: Senden an 21 | send_recipient_placeholder: E-Mails 22 | show: 23 | breadcrumb_list: E-Mails 24 | rep: 25 | base: 26 | email: 27 | one: 1 E-Mail 28 | other: "%{count} E-Mails" 29 | in: in 30 | loading: Lade... 31 | mailer: 32 | one: 1 Mailer 33 | other: "%{count} Mailer" 34 | errors: 35 | email_missing_format: Format fehlenden 36 | headers: 37 | attachments: Anhänge 38 | bcc: BCC 39 | cc: CC 40 | from: Aus 41 | reply_to: Beantworten 42 | subject: Betreff 43 | to: Zu 44 | test_deliver: 45 | no_delivery_method: Bitte setze 'config.action_mailer.delivery_method' um E-Mails in der „%{environment}“ Umgebung zu senden. 46 | provide_email: An welche Adresse senden? 47 | sent_notice: Senden an %{address} mit %{delivery_method} 48 | -------------------------------------------------------------------------------- /spec/dummy/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 48 | 49 | 50 | 51 | 52 |
53 |

The change you wanted was rejected.

54 |

Maybe you tried to change something you didn't have access to.

55 |
56 |

If you are the application owner check the logs for more information.

57 | 58 | 59 | -------------------------------------------------------------------------------- /lib/generators/rails_email_preview/update_previews_generator.rb: -------------------------------------------------------------------------------- 1 | module RailsEmailPreview 2 | module Generators 3 | class UpdatePreviewsGenerator < Rails::Generators::Base 4 | desc "creates app/mailer_previews/NEW_MAILER_preview.rb for each new mailer" 5 | def generate_mailer_previews 6 | previews_dir = 'app/mailer_previews/' 7 | empty_directory previews_dir 8 | Dir['app/mailers/*.rb'].each do |p| 9 | basename = File.basename(p, '.rb') 10 | if basename == 'application_mailer' || File.read(p) !~ /\bdef\s/ 11 | shell.say_status :skip, basename, :blue 12 | next 13 | end 14 | preview_path = File.join(previews_dir, "#{basename}_preview.rb") 15 | if File.exist?(preview_path) 16 | shell.say_status :exist, preview_path, :blue 17 | next 18 | end 19 | create_file preview_path, mailer_class_body(basename.camelize) 20 | end 21 | end 22 | 23 | private 24 | 25 | def mailer_class_body(mailer_class_name) 26 | <<-RUBY 27 | class #{mailer_class_name}Preview 28 | #{(mailer_methods(mailer_class_name) * "\n\n").chomp} 29 | end 30 | RUBY 31 | end 32 | 33 | def mailer_methods(mailer_class_name) 34 | mailer_class = mailer_class_name.constantize 35 | ::RailsEmailPreview::Preview.mail_methods(mailer_class).map do |m| 36 | <<-RUBY 37 | def #{m} 38 | #{mailer_class_name}.#{m.to_s} #{mailer_class.instance_method(m).parameters.map(&:second) * ', '} 39 | end 40 | RUBY 41 | end 42 | end 43 | 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/dummy/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 48 | 49 | 50 | 51 | 52 |
53 |

The page you were looking for doesn't exist.

54 |

You may have mistyped the address or the page may have moved.

55 |
56 |

If you are the application owner check the logs for more information.

57 | 58 | 59 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # Configure Rails Environment 2 | ENV['RAILS_ENV'] = ENV['RACK_ENV'] = 'test' 3 | if ENV['COVERAGE'] && !%w(rbx jruby).include?(RUBY_ENGINE) && !ENV['MIGRATION_SPEC'] 4 | require 'simplecov' 5 | SimpleCov.command_name 'RSpec' 6 | end 7 | 8 | require File.expand_path('../dummy/config/environment.rb', __FILE__) 9 | 10 | require 'rspec/rails' 11 | require 'capybara/rails' 12 | require 'capybara/rspec' 13 | require 'fileutils' 14 | 15 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } 16 | 17 | require 'capybara/cuprite' 18 | 19 | browser_path = ENV['CHROMIUM_BIN'] || %w[ 20 | /usr/bin/chromium-browser 21 | /snap/bin/chromium 22 | /Applications/Chromium.app/Contents/MacOS/Chromium 23 | /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome 24 | ].find { |path| File.executable?(path) } 25 | 26 | Capybara.register_driver :cuprite do |app| 27 | options = { 28 | window_size: [800, 800], 29 | timeout: 15, 30 | } 31 | options[:browser_path] = browser_path if browser_path 32 | Capybara::Cuprite::Driver.new(app, options) 33 | end 34 | 35 | Capybara.javascript_driver = ENV['CAPYBARA_JS_DRIVER']&.to_sym || :cuprite 36 | Capybara.asset_host = ENV['CAPYBARA_ASSET_HOST'] if ENV['CAPYBARA_ASSET_HOST'] 37 | Capybara.configure do |config| 38 | config.run_server = true 39 | config.server_port = 7000 40 | config.default_max_wait_time = 10 41 | end 42 | 43 | RSpec.configure do |config| 44 | config.include SaveScreenshots 45 | config.include WithLayout 46 | 47 | config.around(:each) do |ex| 48 | Dir.chdir(Rails.root) { ex.run } 49 | end 50 | end 51 | 52 | Rails.backtrace_cleaner.remove_silencers! 53 | -------------------------------------------------------------------------------- /config/locales/ru.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ru: 3 | integrations: 4 | cms: 5 | customize_cms_for_rails_email_preview: 6 | edit_email: Редактировать 7 | view_link: Показать 8 | errors: 9 | site_missing: Сначала создайте сайт для %{locale} в CMS (отметьте опцию "Mirrored"). 10 | layouts: 11 | rails_email_preview: 12 | application: 13 | head_title: Письма - REP 14 | rails_email_preview: 15 | emails: 16 | index: 17 | list_title: Письма от приложения 18 | send_form: 19 | send_are_you_sure: Письмо будет по-настоящему отправлено. Продолжить? 20 | send_btn: Отправить на 21 | send_recipient_placeholder: Адрес эл. почты 22 | show: 23 | breadcrumb_list: Письма 24 | rep: 25 | base: 26 | email: 27 | few: "%{count} письма" 28 | many: "%{count} писем" 29 | one: "%{count} письмо" 30 | other: "%{count} письма" 31 | in: в 32 | loading: Письмо загружается... 33 | mailer: 34 | few: "%{count}-х коллекциях" 35 | many: "%{count}-и коллекциях" 36 | one: "%{count}-й коллекции" 37 | other: "%{count} коллекциях" 38 | errors: 39 | email_missing_format: Формат отсутствует 40 | headers: 41 | attachments: Вложения 42 | bcc: Скрытая копия 43 | cc: Копия 44 | from: Отправитель 45 | reply_to: Ответ на 46 | subject: Тема 47 | to: Получатель 48 | test_deliver: 49 | no_delivery_method: Настройте 'config.action_mailer.delivery_method', чтобы отправлять письма в среде '%{environment}' 50 | provide_email: На какой адрес отправить? 51 | sent_notice: Оправлено на %{address} через %{delivery_method} 52 | -------------------------------------------------------------------------------- /spec/features/email_show_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'email show', :type => :feature do 4 | let(:url_args) { {preview_id: 'auth_mailer_preview-email_confirmation'} } 5 | it 'shows email' do 6 | visit rails_email_preview.rep_email_path(url_args) 7 | expect(page).to have_content('Dummy Email Confirmation') 8 | expect(page).to have_content I18n.t('rails_email_preview.emails.show.breadcrumb_list', locale: :en) 9 | expect(page).to have_content 'Hook before headers_and_nav' 10 | expect(page).to have_content 'Hook after headers_content' 11 | end 12 | 13 | it 'shows email in de' do 14 | begin 15 | RailsEmailPreview.locale = :de 16 | visit rails_email_preview.rep_email_path(url_args) 17 | expect(page).to have_content('Dummy Email Confirmation') 18 | expect(page).to have_content I18n.t('rails_email_preview.emails.show.breadcrumb_list', 19 | locale: :de) 20 | ensure 21 | RailsEmailPreview.locale = nil 22 | end 23 | end 24 | 25 | it 'falls back to en on unknown locale' do 26 | begin 27 | RailsEmailPreview.locale = :fr 28 | visit rails_email_preview.rep_email_path(url_args) 29 | expect(page).to have_content 'Dummy Email Confirmation' 30 | expect(page).to have_content I18n.t('rails_email_preview.emails.show.breadcrumb_list', 31 | locale: :en) 32 | ensure 33 | RailsEmailPreview.locale = nil 34 | end 35 | end 36 | 37 | it 'shows locale links' do 38 | visit rails_email_preview.rep_email_path(url_args) 39 | %w(en es).each do |locale| 40 | rails_email_preview.rep_email_path(url_args.merge(email_locale: locale)) 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /app/views/integrations/cms/comfy_v1_integration.js: -------------------------------------------------------------------------------- 1 | if (window.parent && window.parent.rep && (new RegExp(document.querySelector('meta[name="rep-root-path"]').content)).test(parent.location.href)) { 2 | jQuery(function($) { 3 | // Hide nav: 4 | $('.left-column,.right-column').hide(); 5 | $('.center-column').css('margin', 0); 6 | $('#comfy').css('backgroundColor', 'white'); 7 | 8 | // Replace header: 9 | const repData = document.querySelector('#rep-cms-integration-data').dataset; 10 | let showUrl = repData.showUrl; 11 | if (showUrl) { 12 | const parentParams = parent.location.search; 13 | if (!/\?/.test(showUrl)) showUrl += '?'; 14 | showUrl = showUrl.replace(/\?.*$/, parentParams); 15 | document.querySelector('.page-header h2').innerHTML = 16 | `${repData.editEmailLabel} ${repData.viewLinkLabel}`; 17 | document.querySelector('.form-actions a').href = showUrl; 18 | } 19 | 20 | // Snippet form: 21 | var control = function(name) { 22 | return $('[name^="snippet[' + name + ']"]').closest('.form-group,.control-group'); 23 | }; 24 | 25 | // retext labels 26 | control('label').find('.control-label').text("Subject"); 27 | control('content').find('.control-label').text("Body"); 28 | 29 | // hide identifier and categories 30 | control('identifier').hide(); 31 | control('category_ids').hide(); 32 | 33 | // Do not mess with identifier 34 | $('[data-slug]').removeAttr('data-slug'); 35 | }); 36 | 37 | // Schedule headers view refresh on next load 38 | jQuery(window).on('load', function() { 39 | setTimeout(function() { 40 | window.parent.rep.fetchHeadersOnNextLoad = true; 41 | }) 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /spec/preview_list_presenter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'PreviewListPresenter' do 4 | Presenter = RailsEmailPreview::PreviewListPresenter 5 | Preview = RailsEmailPreview::Preview 6 | context 'columns' do 7 | it 'are balanced equally when possible' do 8 | previews = [ 9 | a = Preview.new(preview_class_name: 'A', preview_method: 'x'), 10 | b = Preview.new(preview_class_name: 'B', preview_method: 'x') 11 | ] 12 | expect(Presenter.new(previews).columns.to_a).to eq([[['A', [a]]], [['B', [b]]]]) 13 | end 14 | context 'when impossible to balance equally' do 15 | it 'the first column has more previews if possible' do 16 | previews = [ 17 | a = Preview.new(preview_class_name: 'A', preview_method: 'x'), 18 | b = Preview.new(preview_class_name: 'B', preview_method: 'x'), 19 | c = Preview.new(preview_class_name: 'C', preview_method: 'x') 20 | ] 21 | expect(Presenter.new(previews).columns.to_a).to eq([[['A', [a]], ['B', [b]]], [['C', [c]]]]) 22 | end 23 | it 'two columns even if the first column has fewer previews' do 24 | previews = [ 25 | a = Preview.new(preview_class_name: 'A', preview_method: 'x'), 26 | b1 = Preview.new(preview_class_name: 'B', preview_method: 'x'), 27 | b2 = Preview.new(preview_class_name: 'B', preview_method: 'y'), 28 | b3 = Preview.new(preview_class_name: 'B', preview_method: 'z'), 29 | b4 = Preview.new(preview_class_name: 'B', preview_method: 't') 30 | ] 31 | expect(Presenter.new(previews).columns.to_a).to eq([[['A', [a]]], [['B', [b1, b2, b3, b4]]]]) 32 | end 33 | end 34 | it 'does not fail with no previews' do 35 | expect(Presenter.new([]).columns.to_a).to eq([[], []]) 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure static asset server for tests with Cache-Control for performance. 16 | if Rails::VERSION::MAJOR >= 5 17 | config.public_file_server.enabled = false 18 | config.public_file_server.headers = { 'Cache-Control' => 'public, max-age=31536000' } 19 | else 20 | config.serve_static_files = true 21 | config.static_cache_control = 'public, max-age=3600' 22 | end 23 | 24 | # Show full error reports and disable caching. 25 | config.consider_all_requests_local = true 26 | config.action_controller.perform_caching = false 27 | 28 | # Raise exceptions instead of rendering exception templates. 29 | config.action_dispatch.show_exceptions = false 30 | 31 | # Disable request forgery protection in test environment. 32 | config.action_controller.allow_forgery_protection = false 33 | 34 | # Tell Action Mailer not to deliver emails to the real world. 35 | # The :test delivery method accumulates sent emails in the 36 | # ActionMailer::Base.deliveries array. 37 | config.action_mailer.delivery_method = :test 38 | 39 | # Print deprecation notices to the stderr. 40 | config.active_support.deprecation = :stderr 41 | end 42 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | begin 3 | require 'bundler/setup' 4 | rescue LoadError 5 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks' 6 | end 7 | 8 | # Load dummy app tasks 9 | APP_RAKEFILE = File.expand_path('spec/dummy/Rakefile', __dir__) 10 | load 'rails/tasks/engine.rake' 11 | 12 | Bundler::GemHelper.install_tasks 13 | 14 | require 'rspec/core/rake_task' 15 | RSpec::Core::RakeTask.new(:spec) 16 | 17 | task default: :spec 18 | 19 | desc 'Start development web server' 20 | task :dev do 21 | require 'rackup' 22 | host = '0.0.0.0' 23 | port = ENV['PORT'] || 9292 24 | ENV['RACK_ENV'] = ENV['RAILS_ENV'] = 'development' 25 | Dir.chdir 'spec/dummy' 26 | 27 | Rackup::Server.start( 28 | environment: 'development', 29 | Host: host, 30 | Port: port, 31 | config: 'config.ru' 32 | ) 33 | end 34 | 35 | desc 'Test all Gemfiles from spec/*.gemfile' 36 | task :test_all_gemfiles do 37 | require 'pty' 38 | require 'shellwords' 39 | cmd = 'bundle install --quiet && bundle exec rake --trace' 40 | statuses = Dir.glob('./spec/gemfiles/*{[!.lock]}').map do |gemfile| 41 | Bundler.with_clean_env do 42 | env = {'BUNDLE_GEMFILE' => gemfile} 43 | $stderr.puts "Testing #{File.basename(gemfile)}:\n export #{env.map { |k, v| "#{k}=#{Shellwords.escape v}" } * ' '}; #{cmd}" 44 | PTY.spawn(env, cmd) do |r, _w, pid| 45 | begin 46 | r.each_line { |l| puts l } 47 | rescue Errno::EIO 48 | # Errno:EIO error means that the process has finished giving output. 49 | ensure 50 | ::Process.wait pid 51 | end 52 | end 53 | [$? && $?.exitstatus == 0, gemfile] 54 | end 55 | end 56 | failed_gemfiles = statuses.reject(&:first).map { |(_status, gemfile)| gemfile } 57 | if failed_gemfiles.empty? 58 | $stderr.puts "✓ Tests pass with all #{statuses.size} gemfiles" 59 | else 60 | $stderr.puts "❌ FAILING (#{failed_gemfiles.size} / #{statuses.size})\n#{failed_gemfiles * "\n"}" 61 | exit 1 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /app/views/integrations/cms/comfy_v2_integration.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | if (!(window.parent && window.parent.rep && 3 | (new RegExp(document.querySelector('meta[name="rep-root-path"]').content)).test(parent.location.href))) return; 4 | document.querySelector('#cms-left').style.display = 'none'; 5 | document.addEventListener('DOMContentLoaded', () => { 6 | // Hide nav 7 | document.querySelector('#cms-right').style.display = 'none'; 8 | const main = document.querySelector('#cms-main'); 9 | main.classList.remove('col-lg-8'); 10 | main.classList.add('col-lg-12'); 11 | 12 | // Replace header: 13 | const repData = document.querySelector('#rep-cms-integration-data').dataset; 14 | let showUrl = repData.showUrl; 15 | if (showUrl) { 16 | const parentParams = parent.location.search; 17 | if (!/\?/.test(showUrl)) showUrl += '?'; 18 | showUrl = showUrl.replace(/\?.*$/, parentParams); 19 | main.querySelector('.page-header h2').innerHTML = 20 | `${repData.editEmailLabel} ${repData.viewLinkLabel}`; 21 | main.querySelector('.form-actions a').href = showUrl; 22 | } 23 | 24 | const control = (name) => { 25 | const input = main.querySelector(`[name^="snippet[${name}]"]:not([type="hidden"])`); 26 | let parent = input.parentElement; 27 | while (!parent.classList.contains('form-group')) parent = parent.parentElement; 28 | return parent; 29 | }; 30 | 31 | // Retext labels: 32 | control('label').querySelector('label').innerText = 'Subject'; 33 | control('content').querySelector('label').innerText = 'Body'; 34 | 35 | // Hide identifiers and categories: 36 | control('identifier').style.display = 'none'; 37 | control('category_ids').style.display = 'none'; 38 | 39 | // Do not mess with the identifier 40 | document.querySelector('[data-slug]').removeAttribute('data-slug'); 41 | }); 42 | window.parent.rep.fetchHeadersOnNextLoad = true; 43 | window.parent.rep.iframeOnDOMContentLoaded(); 44 | })(); 45 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [ main ] 4 | pull_request: 5 | types: [ opened, synchronize ] 6 | jobs: 7 | test: 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | include: 12 | - ruby_version: '3.3' 13 | gemfile: rails_7_1 14 | upload_coverage: true 15 | - ruby_version: '3.2' 16 | gemfile: rails_7_1 17 | # - ruby_version: '3.1' 18 | # gemfile: rails_7_1 19 | - ruby_version: '3.3' 20 | gemfile: rails_7_0 21 | - ruby_version: '3.3' 22 | gemfile: rails_6_1 23 | env: 24 | CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} 25 | BUNDLE_GEMFILE: ${{ github.workspace }}/spec/gemfiles/${{ matrix.gemfile }}.gemfile 26 | steps: 27 | - name: "Determine whether to upload coverage" 28 | if: ${{ env.CC_TEST_REPORTER_ID && matrix.upload_coverage }} 29 | run: echo COVERAGE=1 >> $GITHUB_ENV 30 | - uses: actions/checkout@v4 31 | - name: Set up Ruby ${{ matrix.ruby_version }} and ${{ matrix.gemfile }}.gemfile 32 | uses: ruby/setup-ruby@v1 33 | with: 34 | ruby-version: ${{ matrix.ruby_version }} 35 | bundler: ${{ matrix.bundler || 'Gemfile.lock' }} 36 | bundler-cache: true 37 | cache-version: 1000 38 | - name: Run tests 39 | if: ${{ !env.COVERAGE }} 40 | run: bundle exec rspec --format d 41 | - name: Run tests and upload coverage 42 | uses: paambaati/codeclimate-action@v3.0.0 43 | if: ${{ env.COVERAGE }} 44 | with: 45 | coverageCommand: bundle exec rspec --format d 46 | i18n-tasks: 47 | runs-on: ubuntu-latest 48 | env: 49 | BUNDLE_GEMFILE: ${{ github.workspace }}/spec/gemfiles/i18n-tasks.gemfile 50 | steps: 51 | - uses: actions/checkout@v4 52 | - name: Set up Ruby and i18n-tasks.gemfile 53 | uses: ruby/setup-ruby@v1 54 | with: 55 | ruby-version: 3.3 56 | bundler-cache: true 57 | - name: Run i18n-tasks 58 | run: bundle exec i18n-tasks health 59 | -------------------------------------------------------------------------------- /app/models/rails_email_preview/preview.rb: -------------------------------------------------------------------------------- 1 | module RailsEmailPreview 2 | # Preview for one mailer method 3 | class Preview 4 | attr_accessor :id, :preview_class_name, :preview_method 5 | 6 | def initialize(attr = {}) 7 | attr.each { |k, v| self.send "#{k}=", v } 8 | end 9 | 10 | def locales 11 | I18n.available_locales 12 | end 13 | 14 | def formats 15 | %w(html plain raw) 16 | end 17 | 18 | def preview_mail(run_hooks = false, search_query_params = {}) 19 | preview_instance = preview_class_name.constantize.new 20 | setup_instance_variables(preview_instance, search_query_params) 21 | 22 | preview_instance.send(preview_method).tap do |mail| 23 | RailsEmailPreview.run_before_render(mail, self) if run_hooks 24 | end 25 | end 26 | 27 | def name 28 | @name ||= "#{group_name}: #{humanized_method_name}" 29 | end 30 | 31 | def humanized_method_name 32 | @action_name ||= preview_method.to_s.humanize 33 | end 34 | 35 | # @deprecated {#method_name} is deprecated and will be removed in v3 36 | alias_method :method_name, :humanized_method_name 37 | 38 | def group_name 39 | @group_name ||= preview_class_name.to_s.underscore.gsub('/', ': ').sub(/(_mailer)?_preview$/, '').humanize 40 | end 41 | 42 | class << self 43 | def find(email_id) 44 | @by_id[email_id] 45 | end 46 | 47 | alias_method :[], :find 48 | 49 | attr_reader :all 50 | 51 | def mail_methods(mailer) 52 | mailer.public_instance_methods(false).map(&:to_s) 53 | end 54 | 55 | def load_all(class_names) 56 | @all = [] 57 | @by_id = {} 58 | class_names.each do |preview_class_name| 59 | preview_class = preview_class_name.constantize 60 | 61 | mail_methods(preview_class).sort.each do |preview_method| 62 | mailer_method = preview_method 63 | id = "#{preview_class_name.underscore.gsub('/', '__')}-#{mailer_method}" 64 | 65 | email = new( 66 | id: id, 67 | preview_class_name: preview_class_name, 68 | preview_method: preview_method 69 | ) 70 | @all << email 71 | @by_id[id] = email 72 | end 73 | end 74 | @all.sort_by!(&:name) 75 | end 76 | end 77 | 78 | private 79 | 80 | def setup_instance_variables(object, params) 81 | unless params.empty? 82 | params.each { |k,v| object.instance_variable_set("@#{k}", v) } 83 | end 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /app/views/rails_email_preview/emails/email_iframe.js: -------------------------------------------------------------------------------- 1 | (function (doc) { 2 | var rep = window['rep'] || (window['rep'] = { resizeAttached: false }); 3 | rep.loaded = false; 4 | 5 | function findIframe() { 6 | return doc.getElementById('rep-src-iframe'); 7 | } 8 | 9 | function resizeIframe() { 10 | var el = findIframe(); 11 | if (!el) { 12 | rep.loaded = false; 13 | return; 14 | } 15 | var iframeBody = el.contentWindow.document.body; 16 | if (iframeBody) { 17 | el.style.height = (getBodyHeight(iframeBody)) + "px"; 18 | } 19 | } 20 | 21 | function getBodyHeight(body) { 22 | var boundingRect = body.getBoundingClientRect(); 23 | var style = body.ownerDocument.defaultView.getComputedStyle(body); 24 | var marginY = parseInt(style['margin-bottom'], 10) + 25 | parseInt(style['margin-top'], 10); 26 | // There may be a horizontal scrollbar adding to the height. 27 | var scrollbarHeight = 17; 28 | return scrollbarHeight + marginY + Math.max( 29 | body.scrollHeight, body.offsetHeight, body.clientHeight, 30 | boundingRect.height + boundingRect.top) + 31 | // no idea why these 4px are needed: 32 | 4; 33 | } 34 | 35 | function fetchHeaders() { 36 | var headersView = doc.getElementById('email-headers'), 37 | xhr = new XMLHttpRequest(); 38 | xhr.open('GET', headersView.getAttribute('data-url'), true); 39 | xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); 40 | xhr.send(null); 41 | xhr.onreadystatechange = function () { 42 | if (xhr.readyState === 4) { 43 | headersView.innerHTML = xhr.responseText; 44 | } 45 | } 46 | } 47 | 48 | // Called from the iframe via window.parent 49 | rep.iframeOnDOMContentLoaded = function () { 50 | rep.loaded = true; 51 | resizeIframe(); 52 | // CMS refresh headers hook 53 | if (rep.fetchHeadersOnNextLoad) { 54 | rep.fetchHeadersOnNextLoad = false; 55 | fetchHeaders(); 56 | } 57 | }; 58 | 59 | // This is only called back once the iframe has finished loading everything, including images 60 | rep.iframeOnLoad = resizeIframe; 61 | 62 | // Resize on window resize 63 | if (!rep.resizeAttached) { 64 | window.addEventListener('resize', function () { 65 | if (rep.loaded) resizeIframe(); 66 | }, true); 67 | rep.resizeAttached = true 68 | } 69 | 70 | // Only show progress bar after some time to avoid flashing 71 | setTimeout(function () { 72 | doc.getElementById('email-progress-bar').style.display = 'block'; 73 | }, 350); 74 | 75 | findIframe().addEventListener('load', resizeIframe); 76 | resizeIframe(); 77 | })(document); 78 | -------------------------------------------------------------------------------- /app/helpers/rails_email_preview/emails_helper.rb: -------------------------------------------------------------------------------- 1 | module RailsEmailPreview::EmailsHelper 2 | 3 | FORMAT_LABELS = { 'html' => 'HTML', 'plain' => 'Text', 'raw' => 'Raw'} 4 | 5 | def format_label(mime_type) 6 | FORMAT_LABELS[mime_type] 7 | end 8 | 9 | def change_locale_attr(locale) 10 | {href: rails_email_preview.rep_email_path(preview_params.merge(part_type: @part_type, email_locale: locale)), 11 | class: rep_btn_class(@email_locale == locale.to_s)} 12 | end 13 | 14 | def change_format_attr(format) 15 | {href: rails_email_preview.rep_email_path(preview_params.merge(part_type: format)), 16 | class: rep_btn_class(@part_type == format)} 17 | end 18 | 19 | def locale_name(locale) 20 | if defined?(TwitterCldr) 21 | TwitterCldr::Shared::LanguageCodes.to_language(locale.to_s, :bcp_47) 22 | else 23 | locale.to_s 24 | end 25 | end 26 | 27 | def human_headers(mail, &block) 28 | {t('rep.headers.subject') => mail.subject || '(no subject)', 29 | t('rep.headers.from') => mail.from, 30 | t('rep.headers.reply_to') => mail.reply_to, 31 | t('rep.headers.to') => mail.to, 32 | t('rep.headers.cc') => mail.cc, 33 | t('rep.headers.bcc') => mail.bcc, 34 | t('rep.headers.attachments') => attachment_links(mail) 35 | }.each do |name, value| 36 | block.call(name, format_header_value(value)) unless value.blank? 37 | end 38 | end 39 | 40 | def attachment_links(mail) 41 | mail.attachments.map do |attachment| 42 | url = rails_email_preview.rep_raw_email_attachment_path(preview_params.merge(filename: attachment.filename)) 43 | link_to(attachment.filename, url, title: attachment.header.to_s) 44 | end.to_sentence.html_safe 45 | end 46 | 47 | def format_header_value(value) 48 | if value.is_a?(Array) 49 | value.map(&:to_s) * ', ' 50 | else 51 | value.to_s 52 | end 53 | end 54 | 55 | # style 56 | def rep_style 57 | RailsEmailPreview.style 58 | end 59 | 60 | def rep_btn_class(active = false) 61 | [rep_style[:btn_default_class], (rep_style[:btn_active_class_modifier] if active)].compact * ' ' 62 | end 63 | 64 | def rep_btn_group_class 65 | rep_style[:btn_group_class] 66 | end 67 | 68 | def with_index_hook(key, &block) 69 | render_hook key, list: @list, previews: @previews, &block 70 | end 71 | 72 | def with_show_hook(key, &block) 73 | render_hook key, mail: @mail, preview: @preview, &block 74 | end 75 | 76 | def render_hook(key, args, &block) 77 | view_hooks.render(key, args, self, &block) 78 | end 79 | 80 | def hook?(key) 81 | view_hooks.for?(key) 82 | end 83 | 84 | def view_hooks 85 | RailsEmailPreview.view_hooks 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both thread web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = true 16 | 17 | # Enable Rack::Cache to put a simple HTTP cache in front of your application 18 | # Add `rack-cache` to your Gemfile before enabling this. 19 | # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid. 20 | # config.action_dispatch.rack_cache = true 21 | 22 | # Disable Rails's static asset server (Apache or nginx will already do this). 23 | config.serve_static_files = false 24 | 25 | # Specifies the header that your server uses for sending files. 26 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache 27 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx 28 | 29 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 30 | # config.force_ssl = true 31 | 32 | # Set to :debug to see everything in the log. 33 | config.log_level = :info 34 | 35 | # Prepend all log lines with the following tags. 36 | # config.log_tags = [ :subdomain, :uuid ] 37 | 38 | # Use a different logger for distributed setups. 39 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 40 | 41 | # Use a different cache store in production. 42 | # config.cache_store = :mem_cache_store 43 | 44 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 45 | # config.action_controller.asset_host = "http://assets.example.com" 46 | 47 | # Ignore bad email addresses and do not raise email delivery errors. 48 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 49 | # config.action_mailer.raise_delivery_errors = false 50 | 51 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 52 | # the I18n.default_locale when a translation can not be found). 53 | config.i18n.fallbacks = true 54 | 55 | # Send deprecation notices to registered listeners. 56 | config.active_support.deprecation = :notify 57 | 58 | # Disable automatic flushing of the log to improve performance. 59 | # config.autoflush_log = false 60 | 61 | # Use default logging formatter so that PID and timestamp are not suppressed. 62 | config.log_formatter = ::Logger::Formatter.new 63 | end 64 | -------------------------------------------------------------------------------- /lib/rails_email_preview/view_hooks.rb: -------------------------------------------------------------------------------- 1 | module RailsEmailPreview 2 | # Add hooks before, after, or replacing a UI element 3 | class ViewHooks 4 | args = { 5 | index: [:list, :previews].freeze, 6 | show: [:mail, :preview].freeze 7 | } 8 | # All valid hooks and their argument names 9 | SCHEMA = { 10 | list: args[:index], 11 | breadcrumb: args[:show], 12 | breadcrumb_content: args[:show], 13 | headers_and_nav: args[:show], 14 | headers: args[:show], 15 | headers_content: args[:show], 16 | nav: args[:show], 17 | nav_i18n: args[:show], 18 | nav_format: args[:show], 19 | nav_send: args[:show], 20 | email_body: args[:show], 21 | } 22 | POSITIONS = [:before, :replace, :after].freeze 23 | 24 | def initialize 25 | @hooks = Hooks.new 26 | end 27 | 28 | # @param [Symbol] id 29 | # @param [:before, :replace, :after] pos 30 | # @example 31 | # view_hooks.add_render :list, :before, partial: 'shared/hello' 32 | def add_render(id, pos, *render_args) 33 | render_args = render_args.dup 34 | render_opts = render_args.extract_options!.dup 35 | add id, pos do |locals = {}| 36 | render *render_args, render_opts.merge( 37 | locals: (render_opts[:locals] || {}).merge(locals)) 38 | end 39 | end 40 | 41 | # @param [Symbol] id 42 | # @param [:before, :replace, :after] pos 43 | # @example 44 | # view_hooks.add :headers_content, :after do |mail:, preview:| 45 | # raw "ID: #{h mail.header['X-APP-EMAIL-ID']}" 46 | # end 47 | def add(id, pos, &block) 48 | @hooks[id][pos] << block 49 | end 50 | 51 | def render(hook_id, locals, template, &content) 52 | at = @hooks[hook_id] 53 | validate_locals! hook_id, locals 54 | parts = [ 55 | render_providers(at[:before], locals, template), 56 | if at[:replace].present? 57 | render_providers(at[:replace], locals, template) 58 | else 59 | template.capture { content.call(locals) } 60 | end, 61 | render_providers(at[:after], locals, template) 62 | ] 63 | template.safe_join(parts, '') 64 | end 65 | 66 | private 67 | 68 | def render_providers(providers, locals, template) 69 | template.safe_join providers.map { |provider| template.instance_exec(locals, &provider) }, '' 70 | end 71 | 72 | def validate_locals!(hook_id, locals) 73 | if locals.keys.sort != SCHEMA[hook_id].sort 74 | raise ArgumentError.new("Invalid arguments #{locals.keys}. Valid: #{SCHEMA[hook_id]}") 75 | end 76 | end 77 | 78 | class Hooks < DelegateClass(Hash) 79 | def initialize 80 | super Hash.new { |h, id| 81 | validate_id! id 82 | h[id] = Hash.new { |hh, pos| 83 | validate_pos! pos 84 | hh[pos] = [] 85 | } 86 | } 87 | end 88 | 89 | private 90 | 91 | def validate_id!(id) 92 | raise ArgumentError.new('hook id must be a symbol') unless Symbol === id 93 | raise ArgumentError.new("Invalid hook #{id}. Valid: #{SCHEMA.keys * ', '}.") unless SCHEMA.key?(id) 94 | end 95 | 96 | def validate_pos!(pos) 97 | raise ArgumentError.new("Invalid position #{pos}. Valid: #{POSITIONS * ', '}") unless POSITIONS.include?(pos) 98 | end 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /lib/rails_email_preview.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'rails_email_preview/engine' 3 | require 'rails_email_preview/main_app_route_delegator' 4 | require 'rails_email_preview/version' 5 | require 'rails_email_preview/delivery_handler' 6 | require 'rails_email_preview/view_hooks' 7 | 8 | require 'request_store' 9 | require 'pathname' 10 | 11 | module RailsEmailPreview 12 | 13 | mattr_accessor :parent_controller 14 | self.parent_controller = '::ApplicationController' 15 | 16 | # preview class names 17 | mattr_accessor :preview_classes 18 | 19 | # UI locale 20 | mattr_accessor :locale 21 | 22 | # default email locale 23 | mattr_accessor :default_email_locale 24 | 25 | # send email button 26 | mattr_accessor :enable_send_email 27 | self.enable_send_email = true 28 | 29 | # some easy visual settings 30 | mattr_accessor :style 31 | self.style = { 32 | btn_active_class_modifier: 'rep--btn-active', 33 | btn_danger_class: 'rep--btn rep--btn-danger', 34 | btn_default_class: 'rep--btn rep--btn-default', 35 | btn_group_class: 'rep--btn-group', 36 | btn_primary_class: 'rep--btn rep--btn-primary', 37 | form_control_class: 'rep--form-control', 38 | list_group_class: 'rep--list-group', 39 | list_group_item_class: 'rep--list-group__item', 40 | row_class: 'rep--row', 41 | } 42 | 43 | @view_hooks = RailsEmailPreview::ViewHooks.new 44 | class << self 45 | # @return [RailsEmailPreview::ViewHooks] 46 | attr_reader :view_hooks 47 | 48 | def preview_classes=(classes) 49 | @preview_classes = classes 50 | RailsEmailPreview::Preview.load_all(classes) 51 | end 52 | 53 | def find_preview_classes(dir) 54 | return [] unless File.directory?(dir) 55 | Dir.chdir(dir) { Dir['**/*_preview.rb'].map { |p| p.sub(/\.rb$/, '').camelize } } 56 | end 57 | 58 | def layout=(layout) 59 | [::RailsEmailPreview::ApplicationController, ::RailsEmailPreview::EmailsController].each { |ctrl| ctrl.layout layout } 60 | if layout && layout !~ %r(^rails_email_preview/) 61 | # inline application routes if using an app layout 62 | inline_main_app_routes! 63 | end 64 | end 65 | 66 | def run_before_render(mail, preview) 67 | (defined?(@hooks) && @hooks[:before_render] || []).each do |block| 68 | block.call(mail, preview) 69 | end 70 | end 71 | 72 | def before_render(&block) 73 | ((@hooks ||= {})[:before_render] ||= []) << block 74 | end 75 | 76 | def inline_main_app_routes! 77 | unless ::RailsEmailPreview::EmailsController.instance_variable_get(:@inlined_routes) 78 | ::RailsEmailPreview::EmailsController.helper ::RailsEmailPreview::MainAppRouteDelegator 79 | ::RailsEmailPreview::EmailsController.instance_variable_set(:@inlined_routes, true) 80 | end 81 | end 82 | 83 | def setup 84 | yield self 85 | end 86 | 87 | # @api private 88 | def rails_supports_csp_nonce? 89 | @rails_supports_csp_nonce = (Rails.gem_version >= Gem::Version.new('5.2.0')) if @rails_supports_csp_nonce.nil? 90 | @rails_supports_csp_nonce 91 | end 92 | end 93 | 94 | # = Editing settings 95 | # edit link is rendered inside an iframe, so these options are provided for simple styling 96 | mattr_accessor :edit_link_text 97 | self.edit_link_text = '✎ Edit Text' 98 | mattr_accessor :edit_link_style 99 | self.edit_link_style = <<-CSS.strip.gsub(/\n+/m, ' ') 100 | display: block; 101 | font-family: Monaco, Helvetica, sans-serif; 102 | color: #7a4b8a; 103 | border: 2px dashed #7a4b8a; 104 | font-size: 20px; 105 | padding: 8px 12px; 106 | margin-top: 0.6em; 107 | margin-bottom: 0.6em; 108 | CSS 109 | end 110 | -------------------------------------------------------------------------------- /app/assets/stylesheets/rails_email_preview/bootstrap3.css: -------------------------------------------------------------------------------- 1 | #rep-src-iframe-container { 2 | min-height: 450px; 3 | position: relative; 4 | } 5 | #rep-src-iframe-container > .progress-bar { 6 | width: 100%; 7 | } 8 | 9 | #rep-src-iframe { 10 | width: 100%; 11 | border: none; 12 | padding: 0; 13 | margin: 0; 14 | height: 0; 15 | } 16 | 17 | .rep--email-headers { 18 | position: relative; 19 | min-height: 1px; 20 | padding-right: 15px; 21 | padding-left: 15px; 22 | } 23 | @media (min-width: 992px) { 24 | .rep--email-headers { 25 | float: left; 26 | width: 66.6666666667%; 27 | } 28 | } 29 | 30 | .rep--email-nav-container { 31 | position: relative; 32 | min-height: 1px; 33 | padding-right: 15px; 34 | padding-left: 15px; 35 | } 36 | @media (min-width: 992px) { 37 | .rep--email-nav-container { 38 | float: left; 39 | width: 33.3333333333%; 40 | } 41 | } 42 | 43 | .rep--email-list-half { 44 | position: relative; 45 | min-height: 1px; 46 | padding-right: 15px; 47 | padding-left: 15px; 48 | } 49 | @media (min-width: 992px) { 50 | .rep--email-list-half { 51 | float: left; 52 | width: 50%; 53 | } 54 | } 55 | 56 | .rep--headers-list dt { 57 | color: #333333; 58 | } 59 | @media (min-width: 768px) { 60 | .rep--headers-list dt { 61 | float: left; 62 | clear: left; 63 | margin-right: 0.45em; 64 | text-align: right; 65 | width: 100px; 66 | } 67 | .rep--headers-list dt::after { 68 | content: ":"; 69 | } 70 | } 71 | .rep--headers-list dd { 72 | width: auto; 73 | margin-left: 0; 74 | margin-bottom: 0.3em; 75 | } 76 | 77 | .rep--email-options { 78 | text-align: right; 79 | } 80 | 81 | .rep--breadcrumbs { 82 | padding: 8px 15px; 83 | margin-bottom: 20px; 84 | list-style: none; 85 | background-color: #f5f5f5; 86 | border-radius: 4px; 87 | } 88 | .rep--breadcrumbs > li { 89 | display: inline-block; 90 | } 91 | .rep--breadcrumbs > li + li:before { 92 | padding: 0 5px; 93 | color: #ccc; 94 | content: "/ "; 95 | } 96 | .rep--breadcrumbs > .active { 97 | color: rgb(119.085, 119.085, 119.085); 98 | } 99 | 100 | .rep--breadcrumbs__breadcrumb-active { 101 | color: rgb(119.085, 119.085, 119.085); 102 | } 103 | 104 | .rep--panel { 105 | box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.125); 106 | padding: 15px; 107 | margin-bottom: 20px; 108 | } 109 | 110 | .rep--send-to-form { 111 | display: block; 112 | white-space: nowrap; 113 | padding-bottom: 4px; 114 | } 115 | .rep--send-to-form button, .rep--send-to-form [type=email] { 116 | display: inline-block; 117 | } 118 | .rep--send-to-form button:focus, .rep--send-to-form button:active, .rep--send-to-form [type=email]:focus, .rep--send-to-form [type=email]:active { 119 | z-index: 2; 120 | } 121 | .rep--send-to-form button { 122 | line-height: 1.3; 123 | border-top-right-radius: 0; 124 | border-bottom-right-radius: 0; 125 | } 126 | .rep--send-to-form [type=email] { 127 | max-width: 140px; 128 | border-top-left-radius: 0; 129 | border-bottom-left-radius: 0; 130 | } 131 | 132 | .rep--footer { 133 | text-align: center; 134 | color: #31708f; 135 | } 136 | 137 | .rep--email-headers { 138 | position: relative; 139 | min-height: 1px; 140 | padding-right: 15px; 141 | padding-left: 15px; 142 | } 143 | @media (min-width: 768px) { 144 | .rep--email-headers { 145 | float: left; 146 | width: 66.6666666667%; 147 | } 148 | } 149 | 150 | .rep--email-nav-container { 151 | position: relative; 152 | min-height: 1px; 153 | padding-right: 15px; 154 | padding-left: 15px; 155 | } 156 | @media (min-width: 768px) { 157 | .rep--email-nav-container { 158 | float: left; 159 | width: 33.3333333333%; 160 | } 161 | } 162 | 163 | .rep--email-list-half { 164 | position: relative; 165 | min-height: 1px; 166 | padding-right: 15px; 167 | padding-left: 15px; 168 | } 169 | @media (min-width: 768px) { 170 | .rep--email-list-half { 171 | float: left; 172 | width: 50%; 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /app/controllers/rails_email_preview/emails_controller.rb: -------------------------------------------------------------------------------- 1 | module RailsEmailPreview 2 | class EmailsController < ::RailsEmailPreview::ApplicationController 3 | include ERB::Util 4 | before_action :load_preview, except: :index 5 | around_action :set_locale 6 | before_action :set_email_preview_locale 7 | helper_method :with_email_locale 8 | helper_method :preview_params 9 | 10 | # List of emails 11 | def index 12 | @previews = Preview.all 13 | @list = PreviewListPresenter.new(@previews) 14 | end 15 | 16 | # Preview an email 17 | def show 18 | prevent_browser_caching 19 | cms_edit_links! 20 | with_email_locale do 21 | if @preview.respond_to?(:preview_mail) 22 | @mail, body = mail_and_body 23 | @mail_body_html = render_to_string(html: body, layout: 'rails_email_preview/email') 24 | else 25 | raise ArgumentError.new("#{@preview} is not a preview class, does not respond_to?(:preview_mail)") 26 | end 27 | end 28 | end 29 | 30 | # Really deliver an email 31 | def test_deliver 32 | redirect_url = rails_email_preview.rep_email_url(preview_params.except(:recipient_email)) 33 | if (address = params[:recipient_email]).blank? || address !~ /@/ 34 | redirect_to redirect_url, alert: t('rep.test_deliver.provide_email') 35 | return 36 | end 37 | with_email_locale do 38 | delivery_handler = RailsEmailPreview::DeliveryHandler.new(preview_mail, to: address, cc: nil, bcc: nil) 39 | deliver_email!(delivery_handler.mail) 40 | end 41 | delivery_method = Rails.application.config.action_mailer.delivery_method 42 | if delivery_method 43 | redirect_to redirect_url, notice: t('rep.test_deliver.sent_notice', address: address, delivery_method: delivery_method) 44 | else 45 | redirect_to redirect_url, alert: t('rep.test_deliver.no_delivery_method', environment: Rails.env) 46 | end 47 | end 48 | 49 | # Download attachment 50 | def show_attachment 51 | with_email_locale do 52 | filename = "#{params[:filename]}.#{params[:format]}" 53 | attachment = preview_mail(false).attachments.find { |a| a.filename == filename } 54 | send_data attachment.body.raw_source, filename: filename 55 | end 56 | end 57 | 58 | # Render headers partial. Used by the CMS integration to refetch headers after editing. 59 | def show_headers 60 | mail = with_email_locale { mail_and_body.first } 61 | render partial: 'rails_email_preview/emails/headers', locals: {mail: mail} 62 | end 63 | 64 | # Render email body iframe HTML. Used by the CMS integration to provide a link back to Show from Edit. 65 | def show_body 66 | prevent_browser_caching 67 | cms_edit_links! 68 | with_email_locale do 69 | _, body = mail_and_body 70 | render html: body, layout: 'rails_email_preview/email' 71 | end 72 | end 73 | 74 | private 75 | 76 | def preview_params 77 | if Rails::VERSION::MAJOR >= 5 78 | params.to_unsafe_h.except(*(request.path_parameters.keys - [:email_locale])) 79 | else 80 | params.except(*(request.path_parameters.keys - [:email_locale])) 81 | end 82 | end 83 | 84 | def deliver_email!(mail) 85 | if mail.respond_to?(:deliver_now!) 86 | # ActiveJob 87 | mail.deliver_now! 88 | elsif mail.respond_to?(:deliver!) 89 | # support deliver! if present (resque-mailer etc) 90 | mail.deliver! 91 | else 92 | mail.deliver 93 | end 94 | end 95 | 96 | # Load mail and its body for preview 97 | # @return [[Mail, String]] the mail object and its body 98 | def mail_and_body 99 | mail = preview_mail 100 | body = mail_body_content(mail, @part_type) 101 | [mail, body] 102 | end 103 | 104 | # @param [Boolean] run_handlers whether to run the registered handlers for Mail object 105 | # @return [Mail] 106 | def preview_mail(run_handlers = true) 107 | @preview.preview_mail(run_handlers, preview_params) 108 | end 109 | 110 | # @param [Mail] mail 111 | # @param ['html', 'plain', 'raw'] 112 | # @return [String] version of the email for HTML 113 | def mail_body_content(mail, part_type) 114 | return "
#{html_escape(mail.to_s)}
".html_safe if part_type == 'raw' 115 | 116 | body_part = if mail.multipart? 117 | (part_type =~ /html/ ? mail.html_part : mail.text_part) 118 | else 119 | mail 120 | end 121 | return "
#{html_escape(t('rep.errors.email_missing_format', locale: @ui_locale))}
".html_safe if !body_part 122 | if body_part.content_type =~ /plain/ 123 | "
#{html_escape(body_part.body.to_s)}
".html_safe 124 | else 125 | body_content = body_part.body.to_s 126 | 127 | mail.attachments.each do |attachment| 128 | web_url = rails_email_preview.rep_raw_email_attachment_url(params[:preview_id], attachment.filename) 129 | body_content.gsub!(attachment.url, web_url) 130 | end 131 | 132 | body_content.html_safe 133 | end 134 | end 135 | 136 | def with_email_locale(&block) 137 | I18n.with_locale @email_locale, &block 138 | end 139 | 140 | # Email content locale 141 | def set_email_preview_locale 142 | @email_locale = (params[:email_locale] || RailsEmailPreview.default_email_locale || I18n.default_locale).to_s 143 | end 144 | 145 | # UI locale 146 | def set_locale 147 | @ui_locale = RailsEmailPreview.locale 148 | if !I18n.available_locales.map(&:to_s).include?(@ui_locale.to_s) 149 | @ui_locale = :en 150 | end 151 | begin 152 | locale_was = I18n.locale 153 | I18n.locale = @ui_locale 154 | yield if block_given? 155 | ensure 156 | I18n.locale = locale_was 157 | end 158 | end 159 | 160 | # Let REP's `cms_email_snippet` know to render an Edit link 161 | # Todo: Refactoring is especially welcome here 162 | def cms_edit_links! 163 | RequestStore.store[:rep_edit_links] = (@part_type == 'html') 164 | end 165 | 166 | def load_preview 167 | @preview = ::RailsEmailPreview::Preview[params[:preview_id]] or raise ActionController::RoutingError.new('Not Found') 168 | @part_type = params[:part_type] || 'html' 169 | end 170 | end 171 | end 172 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | * Remove explicit dependency on `sassc-rails`. Allow the use of this gem with either: 2 | `dartsass-sprockets`, `sassc-rails`, `dartsass-rails`, or `cssbundling-rails` 3 | * Drop support for EOL ruby and rails versions (rails >6.1, ruby >3.1) 4 | 5 | ## v2.2.3 6 | 7 | * Fixes Rails 7 compatibility. 8 | [#88](https://github.com/glebm/rails_email_preview/pull/88) 9 | [#90](https://github.com/glebm/rails_email_preview/issues/90) 10 | 11 | ## v2.2.2 12 | 13 | 1. Fixes deprecation warnings on Rails 6. 14 | 2. Fixes unintentional processing of email HTML as ERB. 15 | RCE vulnerability if preview body contains user input. 16 | [#82](https://github.com/glebm/rails_email_preview/issues/82) 17 | 18 | ## v2.2.1 19 | 20 | Fixes support for Rails <5.2 (regression introduced in v2.2.0). 21 | 22 | ## v2.2.0 23 | 24 | Adds CSP nonce to inline script tags if CSP is enabled on Rails v5.2+. 25 | 26 | ## v2.1.0 27 | 28 | Use `sassc-rails` instead of `sass-rails`. 29 | 30 | ## v2.0.6 31 | 32 | CMS integration now supports Comfy v2. 33 | 34 | ## v2.0.4 35 | 36 | Depend on `sass` instead of `sass-rails`. 37 | 38 | ## v2.0.3 39 | 40 | Fix a URL generation issue in the CMS integration on Rails 5. 41 | 42 | ## v2.0.2 43 | 44 | * Document roadie-rails support. 45 | * Fix body iframe height calculation. 46 | 47 | ## v2.0.1 48 | 49 | Drop support for all versions of Rails below 4.2. 50 | Fix Rails 5 deprecation warnings. 51 | 52 | ## v1.0.3 53 | 54 | Rails 5 support. 55 | 56 | ## v1.0.2 57 | 58 | Added a couple of variables for further default theme customization. 59 | 60 | ## v1.0.1 61 | 62 | Added `RailsEmailPreview.find_preview_classes(dir)` that also finds classes in subdirectories, and changed the default 63 | initializer to load classes like this: 64 | 65 | ```ruby 66 | RailsEmailPreview.preview_classes = RailsEmailPreview.find_preview_classes('app/mailer_previews') 67 | ``` 68 | 69 | ## v1.0.0 70 | 71 | **Breaking**: REP now uses a lightweight default theme with no dependencies by default. 72 | 73 | If you are using REP with the Bootstrap 3 theme, here are the configuration changes you need to make: 74 | 75 | * `@import "rails_email_preview/bootstrap3"` instead of `rails_email_preview/application`. 76 | * Add the following styles configuration to your REP initializer: 77 | 78 | ```ruby 79 | config.style.merge!( 80 | btn_active_class_modifier: 'active', 81 | btn_danger_class: 'btn btn-danger', 82 | btn_default_class: 'btn btn-default', 83 | btn_group_class: 'btn-group btn-group-sm', 84 | btn_primary_class: 'btn btn-primary', 85 | form_control_class: 'form-control', 86 | list_group_class: 'list-group', 87 | list_group_item_class: 'list-group-item', 88 | row_class: 'row', 89 | ) 90 | ``` 91 | 92 | The following REP internal class names have changed: 93 | 94 | * `.rep-email-options` is now `.rep--email-options`. 95 | * `.rep-headers-list` is now `.rep--headers-list`. 96 | * `.rep-email-show` is now `.rep--email-show`. 97 | * `.breadcrumb` is now `.rep--breadcrumbs`. 98 | * `.breadcrumb .active` is now `.rep--breadcrumbs__active`. 99 | * `.rep-send-to-wrapper` is gone, but now there is `.rep--send-to-form`. 100 | 101 | All REP views are now wrapped in a `div` with the `rep--main-container` class. 102 | 103 | REP no longer depends on slim and slim-rails. 104 | 105 | Fixed minor email locale handling bugs in navigation and the CMS integration. 106 | 107 | ## v0.2.31 108 | 109 | * Compatibility with namespaced email classes in the CMS. 110 | 111 | ## v0.2.30 112 | 113 | * Compatibility with namespaced email classes. 114 | * Change Sass stylesheets extensions from `.sass` to `.css.sass`. [#61](https://github.com/glebm/rails_email_preview/issues/61). 115 | * Spanish translation. Thanks, @epergo! 116 | 117 | ## v0.2.29 118 | 119 | * Latest CMS compatibility 120 | * Rails 4.2: avoid deprecation warnings 121 | 122 | ## v0.2.28 123 | 124 | * CMS beta compatibility 125 | 126 | ## v0.2.27 127 | 128 | * Improve CMS compatibility 129 | * New hook: breadcrumb 130 | 131 | ## v0.2.26 132 | 133 | * Fix an issue with preview list [#47](https://github.com/glebm/rails_email_preview/issues/47). 134 | * Fix a number of minor issues. 135 | 136 | ## v0.2.25 137 | 138 | * Show attachment headers in the link's hover text (HTML title). 139 | * Faster loading via `DOMContentLoaded` on the iframe as opposed to `load`. 140 | 141 | ## v0.2.24 142 | 143 | * Fix regression: Rails 3 support. 144 | 145 | ## v0.2.23 146 | 147 | * **View hooks** to inject or replace UI selectively. 148 | * Fix regression in attachments caused by having a controller action named `headers` (name conflict). 149 | 150 | ## v0.2.22 151 | 152 | * **Preview params** set from URL query. Thank you, @OlgaGr! 153 | * Routes now include locale and part type as segments (with defaults). 154 | * Faster loading using **srcdoc** iframe attribute; new progress bar. 155 | * New language: Russian. 156 | * Minor bugfixes. 157 | 158 | ## v0.2.21 159 | 160 | * **Attachments**. Thanks, @rzane! 161 | * CMS: 1.12 compatibility, better error messages. 162 | 163 | ## v0.2.20 164 | 165 | * REP will fall back to :en if its set locale is not in the list of available locales 166 | 167 | ## v0.2.19 168 | 169 | * Fixes for CMS integration 170 | 171 | ## v0.2.18 172 | 173 | * UI language is now set to :en by default, to avoid #32 174 | * Rails 3 compatibility issues fixed 175 | 176 | ## v0.2.17 177 | 178 | * Fix preview generator 179 | 180 | ## v0.2.15 .. v0.2.16 181 | 182 | * minor bugfixes 183 | * UI improvements 184 | 185 | ## v0.2.13 .. v0.2.14 186 | 187 | * clean up dependencies 188 | * squell compatibility 189 | 190 | ## v0.2.11 .. v0.2.12 191 | 192 | * german translation thanks to @baschtl 193 | * email iframe resizes on window resize 194 | * bugfixes 195 | 196 | ## v0.2.10 197 | 198 | * simplified setting custom layout with `layout=` 199 | * bugfixes 200 | 201 | ## v0.2.9 202 | 203 | * updated bootstrap, turbolinks 204 | * internal: tests + screenshots in spec/screenshots/ after each test run 205 | 206 | ## v0.2.8 207 | 208 | bugs fixed, looks improved 209 | 210 | ## v0.2.7 211 | 212 | * config.style to customize classes in REP views 213 | 214 | ## v0.2.4 .. 0.2.6 215 | 216 | * UI enhancements 217 | * CMS integration bug fixes 218 | * Send email bug fixes 219 | 220 | ## v0.2.3 221 | 222 | * Send Email from REP 223 | 224 | ## v0.2.0 225 | 226 | * inline_main_app_routes! (enables easy layout switching) 227 | * parent_controller (enables easy authorization integration) 228 | * Backwards incompatible: root_url is now rep_root_url, internal routes are prefixed too. 229 | -------------------------------------------------------------------------------- /lib/rails_email_preview/integrations/comfortable_mexica_sofa.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # simply require this file to enable Comfortable Mexican Sofa integration 3 | # read more https://github.com/glebm/rails_email_preview/wiki/Edit-Emails-with-Comfortable-Mexican-Sofa 4 | 5 | module RailsEmailPreview 6 | module Integrations 7 | module ComfortableMexicanSofa 8 | # @return [String] CMS identifier for the current email 9 | # ModerationMailer#approve -> "moderation_mailer-approve" 10 | def cms_email_id 11 | mailer = respond_to?(:controller) ? controller : self 12 | "#{mailer.class.name.underscore.gsub('/', '__')}-#{action_name}" 13 | end 14 | 15 | # @param [Hash] interpolation subject interpolation values 16 | # @return [String] Snippet title interpolated with passed variables 17 | # 18 | # For a snippet with title "Welcome, %{name}!" 19 | # cms_email_subject(name: "Alice") #=> "Welcome, Alice!" 20 | def cms_email_subject(interpolation = {}) 21 | snippet_id = "email-#{cms_email_id}" 22 | return '(no subject)' unless cms_snippet_class.where(identifier: snippet_id).exists? 23 | [I18n.locale, I18n.default_locale].compact.each do |locale| 24 | site = cms_site_class.find_by_locale(locale.to_s) 25 | unless site 26 | raise "rails_email_preview: #{t 'integrations.cms.errors.site_missing', locale: locale}" 27 | end 28 | snippet = site.snippets.find_by_identifier(snippet_id) 29 | next unless snippet.try(:content).present? 30 | 31 | # interpolate even if keys/values are missing 32 | title = snippet.label.to_s 33 | interpolation = interpolation.stringify_keys 34 | # set all missing values to '' 35 | title.scan(/%{([^}]+)}/) { |m| interpolation[m[0]] ||= '' } 36 | # remove all missing keys 37 | subject = title % interpolation.symbolize_keys.delete_if { |k, v| title !~ /%{#{k}}/ } 38 | 39 | return subject if subject.present? 40 | end 41 | '(no subject)' 42 | end 43 | 44 | # show edit link? 45 | def cms_email_edit_link? 46 | RequestStore.store[:rep_edit_links] 47 | end 48 | 49 | def cms_email_edit_link(site, default_site, snippet_id) 50 | snippet = site.snippets.find_by_identifier(snippet_id) || cms_snippet_class.new( 51 | label: "#{snippet_id.sub('-', ' / ').humanize}", 52 | identifier: snippet_id, 53 | site: site 54 | ) 55 | p = {site_id: site.id} 56 | edit_path = if snippet.persisted? 57 | p[:id] = snippet.id 58 | if snippet.content.blank? && default_site && (default_snippet = default_site.snippets.find_by_identifier(snippet_id)) 59 | p[:snippet] = { 60 | content: default_snippet.content 61 | } 62 | p[:snippet][:label] = default_snippet.label unless snippet.label.present? 63 | end 64 | send :"edit_#{cms_admin_site_snippet_route}_url", 65 | p.merge(only_path: true) 66 | else 67 | p[:snippet] = { 68 | label: snippet.label, 69 | identifier: snippet.identifier, 70 | category_ids: [site.categories.find_by_label('email').try(:id)] 71 | } 72 | send :"new_#{cms_admin_site_snippet_route}_url", 73 | p.merge(only_path: true) 74 | end 75 | <<-HTML.strip.html_safe 76 | 79 | HTML 80 | end 81 | 82 | # @return [String] Snippet content, passed through Kramdown. 83 | # Also renders an "✎ Edit" link inside the email when called from preview 84 | def cms_email_snippet(snippet_id = self.cms_email_id) 85 | snippet_id = "email-#{snippet_id}" 86 | site = cms_site_class.find_by_locale(I18n.locale.to_s) 87 | default_site = cms_site_class.find_by_locale(I18n.default_locale.to_s) 88 | 89 | if cms_snippet_class.where(identifier: snippet_id).exists? 90 | # Prefill from default locale if no content 91 | content = send(cms_snippet_render_method, snippet_id, site) 92 | result = (content.presence || send(cms_snippet_render_method, snippet_id, default_site)).to_s 93 | else 94 | result = '' 95 | end 96 | 97 | # If rendering in preview from admin, add edit/create link 98 | if cms_email_edit_link? 99 | result = safe_join [cms_email_edit_link(site, default_site, snippet_id), result], "\n\n" 100 | end 101 | result 102 | end 103 | 104 | def cms_edit_email_snippet_link(path) 105 | link_to(RailsEmailPreview.edit_link_text, path, style: RailsEmailPreview.edit_link_style.html_safe) 106 | end 107 | 108 | def self.rep_email_params_from_snippet(snippet) 109 | id_prefix = 'email-' 110 | return unless snippet && snippet.identifier && snippet.identifier.starts_with?(id_prefix) 111 | mailer_cl, act = snippet.identifier[id_prefix.length..-1].split('-') 112 | {preview_id: "#{mailer_cl}_preview-#{act}", 113 | email_locale: snippet.site.locale} 114 | end 115 | 116 | module CmsVersionsCompatibility 117 | def cms_admin_site_snippet_route 118 | if cms_version_gte?('1.11.0') 119 | :comfy_admin_cms_site_snippet 120 | else 121 | :admin_cms_site_snippet 122 | end 123 | end 124 | 125 | def cms_snippet_class 126 | if cms_version_gte?('1.12.0') 127 | ::Comfy::Cms::Snippet 128 | else 129 | ::Cms::Snippet 130 | end 131 | end 132 | 133 | def cms_site_class 134 | if cms_version_gte?('1.12.0') 135 | ::Comfy::Cms::Site 136 | else 137 | ::Cms::Site 138 | end 139 | end 140 | 141 | def cms_snippet_render_method 142 | if cms_version_gte?('1.12.0') 143 | :cms_snippet_render 144 | else 145 | :cms_snippet_content 146 | end 147 | end 148 | 149 | def cms_v2_plus? 150 | cms_version_gte? '2.0.0' 151 | end 152 | 153 | private 154 | def cms_version_gte?(version) 155 | (::ComfortableMexicanSofa::VERSION.split('.').map(&:to_i) <=> version.split('.').map(&:to_i)) >= 0 156 | end 157 | end 158 | 159 | extend CmsVersionsCompatibility 160 | include CmsVersionsCompatibility 161 | include ::Comfy::CmsHelper if cms_version_gte?('1.12.4') 162 | end 163 | end 164 | end 165 | 166 | ActionMailer::Base.module_eval do 167 | include ::RailsEmailPreview::Integrations::ComfortableMexicanSofa 168 | helper ::RailsEmailPreview::Integrations::ComfortableMexicanSofa 169 | end 170 | 171 | require 'comfortable_mexican_sofa' 172 | ComfortableMexicanSofa::ViewHooks.add :navigation, 'integrations/cms/customize_cms_for_rails_email_preview' 173 | -------------------------------------------------------------------------------- /app/assets/stylesheets/rails_email_preview/application.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --rep-brand-color: #4a90e2; 3 | --rep-brand-border-color: rgb(52.0214285714, 130.5214285714, 222.4785714286); 4 | --rep-brand-hover-bg-color: rgb(34.3857142857, 117.3857142857, 214.6142857143); 5 | --rep-brand-hover-border-color: rgb(29.4557142857, 100.5557142857, 183.8442857143); 6 | --rep-brand-focus-bg-color: var(--rep-brand-hover-border-color); 7 | --rep-brand-focus-border-color: rgb(20.3, 69.3, 126.7); 8 | --rep-brand-form-focus-border-color: rgb(161.9142857143, 197.9142857143, 240.0857142857); 9 | 10 | --rep-link-color: var(--rep-brand-color); 11 | --rep-link-focus-color: rgb(30.8642857143, 105.3642857143, 192.6357142857); 12 | 13 | --rep-gray: #818a91; 14 | 15 | --rep-gray-light: #dedede; 16 | 17 | --rep-gray-lighter: #f1f3f4; 18 | --rep-gray-lighter-border-color: rgb(226.72, 230.76, 232.78); 19 | --rep-gray-lighter-hover-bg-color: rgb(212.44, 218.52, 221.56); 20 | --rep-gray-lighter-hover-border-color: rgb(192.448, 201.384, 205.852); 21 | --rep-gray-lighter-focus-bg-color: var(--rep-gray-lighter-hover-border-color); 22 | --rep-gray-lighter-focus-border-color: rgb(155.32, 169.56, 176.68); 23 | 24 | --rep-danger-color: #d9534f; 25 | --rep-danger-border-color: rgb(212.4719626168, 62.5046728972, 58.0280373832); 26 | --rep-danger-hover-bg-color: rgb(201.4953271028, 48.0841121495, 43.5046728972); 27 | --rep-danger-hover-border-color: rgb(172.1345794393, 41.0775700935, 37.1654205607); 28 | --rep-danger-focus-bg-color: var(--rep-danger-hover-border-color); 29 | --rep-danger-focus-border-color: rgb(117.6074766355, 28.0654205607, 25.3925233645); 30 | 31 | --rep-breadcrumb-bg: var(--rep-gray-lighter); 32 | 33 | --rep-grid-gutter: 30px; 34 | --rep-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 35 | --rep-font-size-nav: 1.15em; 36 | --rep-text-color: rgba(black, 0.87); 37 | --rep-text-color-secondary: #263238; 38 | } 39 | 40 | #rep-src-iframe-container { 41 | min-height: 450px; 42 | position: relative; 43 | } 44 | #rep-src-iframe-container > .progress-bar { 45 | width: 100%; 46 | } 47 | 48 | #rep-src-iframe { 49 | width: 100%; 50 | border: none; 51 | padding: 0; 52 | margin: 0; 53 | height: 0; 54 | } 55 | 56 | .rep--main-container a { 57 | color: var(--rep-link-color); 58 | text-decoration: none; 59 | transition: color 0.1s linear; 60 | } 61 | .rep--main-container a:active, .rep--main-container a:focus, .rep--main-container a:hover { 62 | color: var(--rep-link-focus-color); 63 | } 64 | .rep--main-container a:active { 65 | outline: none; 66 | } 67 | .rep--main-container a:focus { 68 | outline: thin dotted; 69 | outline: 5px auto -webkit-focus-ring-color; 70 | outline-offset: -2px; 71 | } 72 | 73 | .rep--main-container hr { 74 | margin-top: 20px; 75 | margin-bottom: 20px; 76 | border: 0; 77 | border-top: 1px solid var(--rep-gray-lighter); 78 | } 79 | 80 | .rep--main-container { 81 | box-sizing: border-box; 82 | color: var(--rep-text-color); 83 | font-family: var(--rep-font-family); 84 | } 85 | .rep--main-container *, .rep--main-container *::before, .rep--main-container *::after { 86 | box-sizing: inherit; 87 | } 88 | .rep--row { 89 | margin-left: calc(var(--rep-grid-gutter) / -2); 90 | margin-right: calc(var(--rep-grid-gutter) / -2); 91 | } 92 | .rep--row::after { 93 | content: ""; 94 | display: table; 95 | clear: both; 96 | } 97 | 98 | .rep--email-headers, .rep--email-list-half, .rep--email-nav-container { 99 | position: relative; 100 | min-height: 1px; 101 | padding-right: calc(var(--rep-grid-gutter) / 2); 102 | padding-left: calc(var(--rep-grid-gutter) / 2); 103 | } 104 | 105 | @media (min-width: 768px) { 106 | .rep--email-nav-container { 107 | float: left; 108 | width: 33.3333333333%; 109 | } 110 | 111 | .rep--email-list-half { 112 | float: left; 113 | width: 50%; 114 | } 115 | 116 | .rep--email-headers { 117 | float: left; 118 | width: 66.6666666667%; 119 | } 120 | } 121 | 122 | .rep--breadcrumbs { 123 | padding: 0.75rem 1rem; 124 | margin-bottom: 1rem; 125 | font-size: var(--rep-font-size-nav); 126 | list-style: none; 127 | background-color: var(--rep-breadcrumb-bg); 128 | } 129 | 130 | .rep--breadcrumbs__breadcrumb { 131 | display: inline-block; 132 | } 133 | .rep--breadcrumbs__breadcrumb + ::before { 134 | display: inline-block; 135 | padding: 0 0.5em; 136 | color: var(--rep-gray-light); 137 | content: "/"; 138 | } 139 | 140 | .rep--breadcrumbs__breadcrumb-active { 141 | display: inline-block; 142 | } 143 | 144 | .rep--headers-list { 145 | margin: 0; 146 | } 147 | .rep--headers-list dt { 148 | color: var(--rep-text-color-secondary); 149 | font-weight: bold; 150 | } 151 | @media (min-width: 768px) { 152 | .rep--headers-list dt { 153 | float: left; 154 | clear: left; 155 | margin-right: 0.4em; 156 | text-align: right; 157 | width: 6em; 158 | } 159 | } 160 | .rep--headers-list dd { 161 | width: auto; 162 | margin-left: 0; 163 | margin-bottom: 0.2em; 164 | } 165 | 166 | .rep--btn { 167 | appearance: none; 168 | -webkit-font-smoothing: antialiased; 169 | border: 1px solid; 170 | cursor: pointer; 171 | display: inline-block; 172 | font-weight: 600; 173 | padding: 0.75em 1em; 174 | text-decoration: none; 175 | user-select: none; 176 | vertical-align: middle; 177 | text-align: center; 178 | white-space: nowrap; 179 | transition: all 0.2s ease-in-out; 180 | } 181 | .rep--btn:active, .rep--btn.rep--btn-active { 182 | background-image: none; 183 | outline: 0; 184 | box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); 185 | } 186 | .rep--btn:focus { 187 | outline: none; 188 | } 189 | 190 | .rep--btn-default { 191 | color: #111; 192 | background: var(--rep-gray-lighter); 193 | border-color: var(--rep-gray-lighter-border-color); 194 | } 195 | .rep--btn-default:hover { 196 | background: var(--rep-gray-lighter-hover-bg-color); 197 | border-color: var(--rep-gray-lighter-hover-border-color); 198 | } 199 | .rep--btn-default:focus { 200 | box-shadow: 0 0 3px var(--rep-gray-lighter); 201 | background-color: var(--rep-gray-lighter-focus-bg-color); 202 | border-color: var(--rep-gray-lighter-focus-border-color); 203 | } 204 | 205 | .rep--btn-primary { 206 | color: #fff; 207 | background: var(--rep-brand-color); 208 | border-color: var(--rep-brand-border-color); 209 | } 210 | .rep--btn-primary:hover { 211 | background: var(--rep-brand-hover-bg-color); 212 | border-color: var(--rep-brand-hover-border-color); 213 | } 214 | .rep--btn-primary:focus { 215 | box-shadow: 0 0 3px var(--rep-brand-color); 216 | background-color: var(--rep-brand-focus-bg-color); 217 | border-color: var(--rep-brand-focus-border-color); 218 | } 219 | 220 | .rep--btn-danger { 221 | color: #fff; 222 | background: var(--rep-danger-color); 223 | border-color: var(--rep-danger-border-color); 224 | } 225 | .rep--btn-danger:hover { 226 | background: var(--rep-danger-hover-bg-color); 227 | border-color: var(--rep-danger-hover-border-color); 228 | } 229 | .rep--btn-danger:focus { 230 | box-shadow: 0 0 3px var(--rep-danger-color); 231 | background-color: var(--rep-danger-focus-bg-color); 232 | border-color: var(--rep-danger-focus-border-color); 233 | } 234 | 235 | .rep--btn-group .rep--btn + .rep--btn { 236 | margin-left: -1px; 237 | } 238 | .rep--btn-group .rep--btn { 239 | z-index: 1; 240 | } 241 | .rep--btn-group .rep--btn.rep--btn-active { 242 | z-index: 2; 243 | } 244 | .rep--btn-group .rep--btn:focus, .rep--btn-group .rep--btn:hover { 245 | z-index: 3; 246 | } 247 | 248 | .rep--list-group { 249 | font-size: var(--rep-font-size-nav); 250 | } 251 | 252 | .rep--list-group__item { 253 | display: block; 254 | padding: 1rem 1.5rem; 255 | } 256 | .rep--list-group__item:hover { 257 | background-color: var(--rep-gray-lighter); 258 | transition: background-color ease-in-out 0.15s, background-color ease-in-out 0.15s; 259 | } 260 | .rep--list-group__item + .rep--list-group__item { 261 | border-top: 1px solid var(--rep-gray-light); 262 | } 263 | 264 | .rep--panel { 265 | box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.125); 266 | padding: 1.25rem; 267 | margin-bottom: 1rem; 268 | } 269 | 270 | .rep--email-options { 271 | font-size: 0.875rem; 272 | text-align: left; 273 | } 274 | .rep--email-options p { 275 | margin-top: 0; 276 | } 277 | @media (min-width: 768px) { 278 | .rep--email-options { 279 | text-align: right; 280 | } 281 | } 282 | 283 | .rep--send-to-form { 284 | display: block; 285 | white-space: nowrap; 286 | padding-bottom: 4px; 287 | } 288 | .rep--send-to-form button, .rep--send-to-form [type=email] { 289 | display: inline-block; 290 | } 291 | .rep--send-to-form button:focus, .rep--send-to-form button:active, .rep--send-to-form [type=email]:focus, .rep--send-to-form [type=email]:active { 292 | z-index: 2; 293 | } 294 | .rep--send-to-form button { 295 | line-height: 1.25; 296 | padding: 0.5rem; 297 | } 298 | .rep--send-to-form [type=email] { 299 | max-width: 135px; 300 | padding: 0.5rem; 301 | border-top-left-radius: 0; 302 | border-bottom-left-radius: 0; 303 | } 304 | 305 | .rep--form-control { 306 | padding: 0.5rem 0.75rem; 307 | line-height: 1.25; 308 | background-image: none; 309 | background-clip: padding-box; 310 | border: 1px solid rgba(0, 0, 0, 0.15); 311 | border-radius: 0.25rem; 312 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); 313 | transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; 314 | } 315 | .rep--form-control::placeholder { 316 | color: #999; 317 | opacity: 1; 318 | } 319 | .rep--form-control:focus { 320 | border-color: var(--rep-brand-form-focus-border-color); 321 | outline: none; 322 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6); 323 | } 324 | 325 | .rep--footer { 326 | text-align: center; 327 | color: var(--rep-gray); 328 | } 329 | 330 | body { 331 | margin: 0; 332 | padding: 0; 333 | } 334 | 335 | .rep-standalone-container { 336 | padding: 0 15px; 337 | } 338 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rails Email Preview [![Build Status][badge-ci]][ci] [![Test Coverage][coverage-badge]][coverage] [![Code Climate][codeclimate-badge]][codeclimate] [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/glebm/rails_email_preview?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 2 | 3 | Preview email in the browser with this Rails engine. Compatible with Rails 6.1+. 4 | 5 | An email review: 6 | 7 | ![screenshot][rep-show-screenshot] 8 | 9 | The list of all email previews: 10 | 11 | ![screenshot][rep-nav-screenshot] 12 | 13 | REP comes with two themes: a simple standalone theme, and a theme that uses [Bootstrap 3][rep-show-default-screenshot]. 14 | 15 | ## Installation 16 | 17 | Add [![Gem Version][gem-badge]][gem] to Gemfile: 18 | 19 | ```ruby 20 | gem 'rails_email_preview', '~> 2.2.3' 21 | ``` 22 | 23 | Add an initializer and the routes: 24 | 25 | ```console 26 | $ rails g rails_email_preview:install 27 | ``` 28 | 29 | Generate preview classes and method stubs in app/mailer_previews/ 30 | 31 | ```console 32 | $ rails g rails_email_preview:update_previews 33 | ``` 34 | 35 | ## Usage 36 | 37 | The last generator above will add a stub for each of your emails, then you populate the stubs with mock data: 38 | 39 | ```ruby 40 | # app/mailer_previews/user_mailer_preview.rb: 41 | class UserMailerPreview 42 | # preview methods should return Mail objects, e.g.: 43 | def invitation 44 | UserMailer.invitation mock_user('Alice'), mock_user('Bob') 45 | end 46 | 47 | def welcome 48 | UserMailer.welcome mock_user 49 | end 50 | 51 | private 52 | # You can put all your mock helpers in a module 53 | # or you can use your factories / fabricators, just make sure you are not creating anything 54 | def mock_user(name = 'Bill Gates') 55 | fake_id User.new(name: name, email: "user#{rand 100}@test.com") 56 | end 57 | 58 | def fake_id(obj) 59 | # overrides the method on just this object 60 | obj.define_singleton_method(:id) { 123 + rand(100) } 61 | obj 62 | end 63 | end 64 | ``` 65 | 66 | ### Parameters as instance variables 67 | 68 | All parameters in the search query will be available to the preview class as instance variables. 69 | For example, if URL to mailer preview looks like: 70 | 71 | /emails/user_mailer_preview-welcome?**user_id=1** 72 | 73 | The method `welcome` in `UserMailerPreview` have a `@user_id` instance variable defined: 74 | 75 | ```ruby 76 | class UserMailerPreview 77 | def welcome 78 | user = @user_id ? User.find(@user_id) : mock_user 79 | UserMailer.welcome(user) 80 | end 81 | end 82 | ``` 83 | 84 | Now you can preview or send the welcome email to a specific user. 85 | 86 | ### Routing 87 | 88 | You can access REP urls like this: 89 | 90 | ```ruby 91 | # engine root: 92 | rails_email_preview.rep_root_url 93 | # list of emails (same as root): 94 | rails_email_preview.rep_emails_url 95 | # email show: 96 | rails_email_preview.rep_email_url('user_mailer-welcome') 97 | ``` 98 | 99 | ### Sending Emails 100 | 101 | You can send emails via REP. This is especially useful when testing with limited clients (Blackberry, Outlook, etc.). 102 | This will use the environment's mailer settings, but the handler will `perform_deliveries`. 103 | Uncomment this line in the initializer to disable sending test emails: 104 | 105 | ```ruby 106 | config.enable_send_email = false 107 | ``` 108 | 109 | ### Editing Emails 110 | 111 | Emails can be stored in the database and edited in the browser. 112 | REP works with [Comfortable Mexican Sofa CMS](https://github.com/comfy/comfortable-mexican-sofa) to achieve this -- see the [CMS Guide](https://github.com/glebm/rails_email_preview/wiki/Edit-Emails-with-Comfortable-Mexican-Sofa) to learn more. 113 | 114 | [![screenshot](https://raw.github.com/glebm/rails_email_preview/master/doc/img/rep-edit-sofa.png)](https://github.com/glebm/rails_email_preview/wiki/Edit-Emails-with-Comfortable-Mexican-Sofa) 115 | 116 | ### CSS inlining 117 | 118 | For CSS inlining, REP supports [Roadie](https://github.com/Mange/roadie) and 119 | [Premailer](https://github.com/alexdunae/premailer). 120 | Both of these automatically translate CSS rules into inline styles and turn 121 | relative URLs into absolute ones. 122 | 123 | Roadie additionally extracts styles that cannot be inlined into a separate 124 | `