├── 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 |
Hello user, here is your monthly newsletter
2 | 3 |Lorem ipsum4 | -------------------------------------------------------------------------------- /spec/dummy/app/views/newsletter_mailer/weekly_newsletter.html.erb: -------------------------------------------------------------------------------- 1 |
Hello user, here is your weekly newsletter
2 | 3 |Lorem ipsum4 | -------------------------------------------------------------------------------- /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 |<%= 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 |Planned features:
25 | 26 |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 |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 |Maybe you tried to change something you didn't have access to.
55 |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 |You may have mistyped the address or the page may have moved.
55 |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 | | 77 | #{cms_edit_email_snippet_link(edit_path)} 78 | |