├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .rspec ├── Appraisals ├── CHANGELOG.md ├── Gemfile ├── MIT-LICENSE ├── README.md ├── Rakefile ├── app ├── assets │ └── images │ │ └── maily │ │ ├── icons │ │ ├── globe.svg │ │ └── paperclip.svg │ │ └── logo.png ├── controllers │ └── maily │ │ ├── application_controller.rb │ │ └── emails_controller.rb ├── helpers │ └── maily │ │ ├── application_helper.rb │ │ └── emails_helper.rb └── views │ ├── layouts │ └── maily │ │ └── application.html.erb │ └── maily │ ├── emails │ ├── edit.html.erb │ ├── index.html.erb │ └── show.html.erb │ └── shared │ ├── _flash_messages.html.erb │ ├── _footer.html.erb │ ├── _header.html.erb │ ├── _javascript.html.erb │ ├── _sidebar.html.erb │ └── _stylesheet.html.erb ├── config └── routes.rb ├── gemfiles ├── rails_5.2.gemfile ├── rails_6.0.gemfile ├── rails_6.1.gemfile ├── rails_7.0.gemfile └── rails_7.1.gemfile ├── lib ├── generators │ ├── maily │ │ └── install_generator.rb │ └── templates │ │ └── initializer.rb ├── maily.rb └── maily │ ├── email.rb │ ├── engine.rb │ ├── generator.rb │ ├── mailer.rb │ └── version.rb ├── maily.gemspec ├── spec ├── controllers_spec.rb ├── dummy │ ├── README.md │ ├── Rakefile │ ├── app │ │ ├── controllers │ │ │ └── application_controller.rb │ │ ├── mailers │ │ │ ├── application_mailer.rb │ │ │ └── notifier.rb │ │ └── views │ │ │ ├── notifications │ │ │ └── custom_template_path.html.erb │ │ │ └── notifier │ │ │ ├── custom_template.html.erb │ │ │ ├── multipart.html.erb │ │ │ ├── multipart.text.erb │ │ │ ├── no_arguments.html.erb │ │ │ ├── only_text.text.erb │ │ │ ├── version.html.erb │ │ │ ├── with_arguments.html.erb │ │ │ ├── with_array_arguments.html.erb │ │ │ ├── with_description.html.erb │ │ │ ├── with_inline_attachments.html.erb │ │ │ ├── with_params.html.erb │ │ │ └── with_slim_template.html.slim │ ├── bin │ │ ├── bundle │ │ ├── rails │ │ └── rake │ ├── config.ru │ ├── config │ │ ├── application.rb │ │ ├── boot.rb │ │ ├── environment.rb │ │ ├── environments │ │ │ ├── development.rb │ │ │ ├── production.rb │ │ │ └── test.rb │ │ ├── initializers │ │ │ ├── backtrace_silencers.rb │ │ │ ├── filter_parameter_logging.rb │ │ │ ├── inflections.rb │ │ │ ├── maily.rb │ │ │ ├── mime_types.rb │ │ │ ├── secret_token.rb │ │ │ ├── session_store.rb │ │ │ └── wrap_parameters.rb │ │ ├── locales │ │ │ └── en.yml │ │ └── routes.rb │ ├── lib │ │ └── maily_hooks.rb │ ├── log │ │ └── .keep │ └── public │ │ ├── 404.html │ │ ├── 422.html │ │ ├── 500.html │ │ └── favicon.ico ├── email_spec.rb ├── generator_spec.rb ├── mailer_spec.rb ├── maily_spec.rb └── spec_helper.rb └── support └── images ├── logo.png └── screenshot.png /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | name: CI 8 | runs-on: ubuntu-latest 9 | env: 10 | BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}.gemfile 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | ruby: ["3.0", "3.1", "3.2", "3.3"] 15 | gemfile: [rails_6.1, rails_7.0, rails_7.1] 16 | include: 17 | - ruby: "2.7" 18 | gemfile: rails_5.2 19 | - ruby: "3.0" 20 | gemfile: rails_6.0 21 | - ruby: head 22 | gemfile: rails_7.1 23 | steps: 24 | - uses: actions/checkout@v3 25 | - uses: ruby/setup-ruby@v1 26 | with: 27 | ruby-version: ${{ matrix.ruby }} 28 | bundler-cache: true 29 | - run: bundle exec rspec 30 | continue-on-error: ${{ endsWith(matrix.ruby, 'head') }} 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle/ 2 | pkg/ 3 | Gemfile.lock 4 | *.gemfile.lock 5 | spec/dummy/log/*.log 6 | spec/dummy/tmp/ 7 | .byebug_history 8 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | %w( 2 | 7.1 3 | 7.0 4 | 6.1 5 | 6.0 6 | 5.2 7 | ).each do |version| 8 | appraise "rails-#{version}" do 9 | gem "rails", "~> #{version}.0" 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [2.1.0] 6 | 7 | - Use `ActionMailer::InlinePreviewInterceptor` to facilitate integration with other gems like premailer (#55) 8 | - Move CI from Travis to GitHub Actions 9 | 10 | ## [2.0.2] 11 | 12 | - UI: Hide delivery form if `allow_delivery` is false (#54) 13 | 14 | ## [2.0.1] 15 | 16 | - UI: Don't show empty mailer classes in sidebar (#51) 17 | 18 | ## [2.0.0] 19 | 20 | - Email versions (#49) 21 | - Drop official support for EOL Rails versions: 5.1, 5.0 and 4.2 22 | - Drop official support for EOL Rubies: 2.4 23 | 24 | ## [1.0.0] 25 | 26 | - Avoid the dependency on Sprockets (#48) 27 | 28 | ## [0.12.3] 29 | 30 | - Styles: fix input placeholder color in Firefox 31 | 32 | ## [0.12.2] 33 | 34 | - Refine logo and UI (#45) 35 | 36 | ## [0.12.1] 37 | 38 | - Sprockets v4 fixes (#44) 39 | 40 | ## [0.12.0] 41 | 42 | - Support inline attachments (#30) 43 | 44 | ## [0.11.0] 45 | 46 | - Support arrays in hooks (#42) 47 | - Drop Ruby 2.3 (EOL) from CI 48 | 49 | ## [0.10.1] 50 | 51 | - UI: fix iframe (onload) resize 52 | 53 | ## [0.10.0] 54 | 55 | - Support `ActionMailer::Parameterized` (#39) 56 | - UI tweaks 57 | 58 | ## [0.9.1] 59 | 60 | - Properly display text parts by respecting break lines 61 | - CI: official Rails 6 support 62 | - Reduce gem size by including only necessary files 63 | 64 | ## [0.9.0] 65 | 66 | - Allow to register external mailers, for example, from a gem 67 | - CI Rubies: add 2.6 and drop 2.2 (EOL) 68 | 69 | ## [0.8.2] 70 | 71 | - Fix incompatibility with last gem `mail` (v2.7.1) release (#32) 72 | 73 | ## [0.8.1] 74 | 75 | - Serve fonts from Google Fonts API 76 | 77 | ## [0.8.0] 78 | 79 | - Inherited emails support (#28) 80 | - `Maily.available_locales` defaults to Rails available_locales `config.i18n.available_locales` (#27) 81 | 82 | ## [0.7.2] 83 | 84 | - Definitive fix for newer apps using `webpacker` and without `sass-rails` (#22) 85 | - Fix regression in generator, from v0.7.0 (#24) 86 | - Allow to edit emails with different templates engines: Slim, Haml, ... (#25) 87 | 88 | ## [0.7.1] 89 | 90 | - Fix assets pipeline integration for applications using `webpacker` instead of `sprockets` (#22) 91 | 92 | ## [0.7.0] 93 | 94 | - Allow to hide emails (#11) 95 | - Allow to override `template_name` 96 | - New project logo 97 | - Internal classes refactor 98 | - Front-end cleanup and simplification (remove font icons, update normalize.css, CSS/HTML refactor, better page titles, better flash messages) 99 | - Drop Ruby 2.1 (EOL) from CI 100 | - CI against Rails 5.2 101 | 102 | ## [0.6.3] 103 | 104 | - Add support for `reply_to` 105 | 106 | ## [0.6.2] 107 | 108 | - Rails 5.1 fixes (#19, #20) 109 | - Hooks: improve arguments validation (#20) 110 | 111 | ## [0.6.1] 112 | 113 | - Fix Rails automatic generator (#18) 114 | - UI: display better error messages (#17) 115 | - UI: alphabetically sort mailers and emails in sidebar (#17) 116 | 117 | ## [0.6.0] 118 | 119 | - Lazy hooks (#14) 120 | - Better capture and dispatch internal exceptions (#13) 121 | - Allow to define a description per email (#10) 122 | - Remove HTML5 Shiv (front-end dependency) 123 | - Fix assets pipeline integration 124 | - Lots of front-end tweaks 125 | - Basic dashboard with customizable welcome message 126 | - CI suite: officially supported Rails versions: 4.2, 5.0 and 5.1 127 | 128 | ## [0.5.0] 129 | 130 | - Appraisals integration: Rails 3.2, 4.X and 5.0 131 | - Modernize CI Rubies: add 2.2, 2.3 and 2.4 (remove 1.9.3 and 2.0) 132 | - Fix Ruby warnings (#12) 133 | - Docs typos (#8) 134 | 135 | ## [0.4.0] 136 | 137 | - Allow to setup HTTP basic authentication 138 | 139 | ## [0.3.5] 140 | 141 | - CI: run tests against ruby 2.1 142 | - Properly define dependencies 143 | 144 | ## [0.3.4] 145 | 146 | - Fix syntax error introduced in v0.3.3 :( 147 | 148 | ## [0.3.3] 149 | 150 | - Disallow templates edition in production mode 151 | 152 | ## [0.3.2] 153 | 154 | - Fix #3: email container not displayed properly in Firefox 155 | 156 | ## [0.3.1] 157 | 158 | - Add basic specs 159 | - CI integration 160 | 161 | ## [0.3.0] 162 | 163 | - First real usable release :tada: 164 | 165 | [2.1.0]: https://github.com/markets/maily/compare/v2.0.2...v2.1.0 166 | [2.0.2]: https://github.com/markets/maily/compare/v2.0.1...v2.0.2 167 | [2.0.1]: https://github.com/markets/maily/compare/v2.0.0...v2.0.1 168 | [2.0.0]: https://github.com/markets/maily/compare/v1.0.0...v2.0.0 169 | [1.0.0]: https://github.com/markets/maily/compare/v0.12.3...v1.0.0 170 | [0.12.3]: https://github.com/markets/maily/compare/v0.12.2...v0.12.3 171 | [0.12.2]: https://github.com/markets/maily/compare/v0.12.1...v0.12.2 172 | [0.12.1]: https://github.com/markets/maily/compare/v0.12.0...v0.12.1 173 | [0.12.0]: https://github.com/markets/maily/compare/v0.11.0...v0.12.0 174 | [0.11.0]: https://github.com/markets/maily/compare/v0.10.1...v0.11.0 175 | [0.10.1]: https://github.com/markets/maily/compare/v0.10.0...v0.10.1 176 | [0.10.0]: https://github.com/markets/maily/compare/v0.9.1...v0.10.0 177 | [0.9.1]: https://github.com/markets/maily/compare/v0.9.0...v0.9.1 178 | [0.9.0]: https://github.com/markets/maily/compare/v0.8.2...v0.9.0 179 | [0.8.2]: https://github.com/markets/maily/compare/v0.8.1...v0.8.2 180 | [0.8.1]: https://github.com/markets/maily/compare/v0.8.0...v0.8.1 181 | [0.8.0]: https://github.com/markets/maily/compare/v0.7.2...v0.8.0 182 | [0.7.2]: https://github.com/markets/maily/compare/v0.7.1...v0.7.2 183 | [0.7.1]: https://github.com/markets/maily/compare/v0.7.0...v0.7.1 184 | [0.7.0]: https://github.com/markets/maily/compare/v0.6.3...v0.7.0 185 | [0.6.3]: https://github.com/markets/maily/compare/v0.6.2...v0.6.3 186 | [0.6.2]: https://github.com/markets/maily/compare/v0.6.1...v0.6.2 187 | [0.6.1]: https://github.com/markets/maily/compare/v0.6.0...v0.6.1 188 | [0.6.0]: https://github.com/markets/maily/compare/v0.5.0...v0.6.0 189 | [0.5.0]: https://github.com/markets/maily/compare/v0.4.0...v0.5.0 190 | [0.4.0]: https://github.com/markets/maily/compare/v0.3.5...v0.4.0 191 | [0.3.5]: https://github.com/markets/maily/compare/v0.3.4...v0.3.5 192 | [0.3.4]: https://github.com/markets/maily/compare/v0.3.3...v0.3.4 193 | [0.3.3]: https://github.com/markets/maily/compare/v0.3.2...v0.3.3 194 | [0.3.2]: https://github.com/markets/maily/compare/v0.3.1...v0.3.2 195 | [0.3.1]: https://github.com/markets/maily/compare/v0.3.0...v0.3.1 196 | [0.3.0]: https://github.com/markets/maily/compare/v0.1.0...v0.3.0 197 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem 'byebug', group: [:development, :test] 6 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2024 Marc Anguera Insa 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | 4 |

5 |

6 | 7 | 8 | 9 | GitHub 10 |

11 |
12 | 13 | Maily is a Rails Engine to manage, test and navigate through all your email templates of your app, being able to preview them directly in your browser. 14 | 15 | Maily automatically picks up all your emails and make them accessible from a kind of dashboard. 16 | 17 | ## Features: 18 | 19 | * Mountable engine 20 | * Visual preview in the browser (attachments as well) 21 | * Template edition (only in development) 22 | * Email delivery 23 | * Easy way (aka `Hooks`) to define and customize data for emails 24 | * Email versions 25 | * Flexible authorization system 26 | * Minimalistic and clean interface 27 | * Generator to handle a comfortable installation 28 | 29 | ![](support/images/screenshot.png) 30 | 31 | ## Installation 32 | 33 | Add this line to your `Gemfile` and then run `bundle install`: 34 | 35 | ```ruby 36 | gem 'maily' 37 | ``` 38 | 39 | Run generator: 40 | 41 | ``` 42 | > rails g maily:install 43 | ``` 44 | 45 | This generator runs some tasks for you: 46 | 47 | * Mounts the engine (to `/maily` by default) in your routes 48 | * Adds an initializer (into `config/initializers/maily.rb`) to customize some settings 49 | * Adds a file (into `lib/maily_hooks.rb`) to define hooks 50 | 51 | ## Initialization and configuration 52 | 53 | You should use the `setup` method to configure and customize `Maily` settings: 54 | 55 | ```ruby 56 | # config/initializers/maily.rb 57 | 58 | Maily.setup do |config| 59 | # On/off engine 60 | # config.enabled = !Rails.env.production? 61 | 62 | # Allow templates edition 63 | # config.allow_edition = !Rails.env.production? 64 | 65 | # Allow deliveries 66 | # config.allow_delivery = !Rails.env.production? 67 | 68 | # Your application available_locales (or I18n.available_locales) by default 69 | # config.available_locales = [:en, :es, :pt, :fr] 70 | 71 | # Run maily under different controller ('ActionController::Base' by default) 72 | # config.base_controller = '::AdminController' 73 | 74 | # Configure hooks path 75 | # config.hooks_path = 'lib/maily_hooks.rb' 76 | 77 | # Http basic authentication (nil by default) 78 | # config.http_authorization = { username: 'admin', password: 'secret' } 79 | 80 | # Customize welcome message 81 | # config.welcome_message = "Welcome to our email testing platform. If you have any problem, please contact support team at support@example.com." 82 | end 83 | ``` 84 | 85 | You can use the following format too: 86 | 87 | ```ruby 88 | Maily.enabled = ENV['MAILY_ENABLED'] 89 | Maily.allow_edition = false 90 | ``` 91 | 92 | ### Templates edition (`allow_edition` option) 93 | 94 | This feature was designed for the `development` environment. Since it's based on just a file edition, and while running in `production` mode, code is not reloaded between requests, Rails doesn't take into account your changes (without restarting the server). Actually, allowing arbitrary Ruby code evaluation is potentially dangerous, and that's not a good idea in `production`. 95 | 96 | So, template edition is not allowed outside of the `development` environment. 97 | 98 | ## Hooks 99 | 100 | Most of emails need to populate some data to consume it and do interesting things. Hooks are used to define this data via a little DSL. Hooks also accept "callable" objects to *lazy* load variables/data, so each email will evaluate its hooks on demand. Example: 101 | 102 | ```ruby 103 | # lib/maily_hooks.rb 104 | 105 | user = User.new(email: 'user@example.com') 106 | lazy_user = -> { User.with_comments.first } # callable object, lazy evaluation 107 | comment = Struct.new(:body).new('Lorem ipsum') # stub way 108 | service = FactoryGirl.create(:service) # using fixtures with FactoryGirl 109 | 110 | Maily.hooks_for('Notifier') do |mailer| 111 | mailer.register_hook(:welcome, user, template_path: 'users') 112 | mailer.register_hook(:new_comment, lazy_user, comment) 113 | end 114 | 115 | Maily.hooks_for('PaymentNotifier') do |mailer| 116 | mailer.register_hook(:invoice, user, service) 117 | end 118 | ``` 119 | 120 | Note that you are able to override the `template_path` and the `template_name` like can be done in Rails. You must pass these options as a hash and last argument: 121 | 122 | ```ruby 123 | Maily.hooks_for('YourMailerClass') do |mailer| 124 | mailer.register_hook(:a_random_email, template_path: 'notifications') 125 | mailer.register_hook(:another_email, template_name: 'email_base') 126 | end 127 | ``` 128 | 129 | ### Email versions 130 | 131 | You can add versions for special emails. This is useful in some cases where template content depends on the parameters you provide, for instance, a welcome message for trial accounts and gold accounts. 132 | 133 | ```ruby 134 | free_trial_account = -> { Account.free_trial.first } 135 | gold_account = -> { Account.gold.first } 136 | 137 | Maily.hooks_for('Notifier') do |mailer| 138 | mailer.register_hook(:welcome, free_trial_account, version: 'Free trial account') 139 | mailer.register_hook(:welcome, gold_account, version: 'Gold account') 140 | end 141 | ``` 142 | 143 | ### Email description 144 | 145 | You can add a description to any email and it will be displayed along with its preview. This is useful in some cases like: someone from another team, for example, a marketing specialist, visiting Maily to review some texts and images; they can easily understand when this email is sent by the system. 146 | 147 | ```ruby 148 | Maily.hooks_for('BookingNotifier') do |mailer| 149 | mailer.register_hook(:accepted, description: "This email is sent when a reservation has been accepted by the system." ) 150 | end 151 | ``` 152 | 153 | ### Hide emails 154 | 155 | You are also able to hide emails: 156 | 157 | ```ruby 158 | Maily.hooks_for('Notifier') do |mailer| 159 | mailer.hide_email(:sensible_email, :secret_email) 160 | end 161 | ``` 162 | 163 | ### Use `params` 164 | 165 | Support for [`ActionMailer::Parameterized`](https://api.rubyonrails.org/classes/ActionMailer/Parameterized.html) is possible via `with_params` hash: 166 | 167 | ```ruby 168 | message = -> { 'Hello!' } 169 | 170 | Maily.hooks_for('Notifier') do |mailer| 171 | mailer.register_hook(:new_message, with_params: { message: message }) 172 | end 173 | ``` 174 | 175 | ### External emails 176 | 177 | If you want to register and display in the UI, emails from external sources, like for example a gem, you can do it by adding a hook. Example: 178 | 179 | ```ruby 180 | Maily.hooks_for('Devise::Mailer') do |mailer| 181 | mailer.hide_email(:unlock_instructions) 182 | 183 | mailer.register_hook(:reset_password_instructions, user, 'random_token') 184 | mailer.register_hook(:confirmation_instructions, user, 'random_token') 185 | mailer.register_hook(:password_change, user) 186 | end 187 | ``` 188 | 189 | ## Authorization 190 | 191 | Basically, you have 2 ways to restrict access to the `Maily` section. You can even combine both. 192 | 193 | ### Custom base controller 194 | 195 | By default `Maily` runs under `ActionController::Base`, but you are able to customize that parent controller (`Maily.base_controller` option) in order to achieve (using, for example, `before_action` blocks) a kind of access control system. For example, set a different base controller: 196 | 197 | ```ruby 198 | Maily.base_controller = '::SuperAdminController' 199 | ``` 200 | 201 | And then write your own authorization rules in this defined controller: 202 | 203 | ```ruby 204 | class SuperAdminController < ActionController::Base 205 | before_action :maily_authorized? 206 | 207 | private 208 | 209 | def maily_authorized? 210 | current_user.admin? || raise("You don't have access to this section!") 211 | end 212 | end 213 | ``` 214 | 215 | ### HTTP basic authentication 216 | 217 | You can also authorize yours users via HTTP basic authentication, simply use this option: 218 | 219 | ```ruby 220 | Maily.http_authorization = { username: 'admin', password: 'secret' } 221 | ``` 222 | 223 | ## Notes 224 | 225 | Rails 4.1 introduced a built-in mechanism to preview the application emails. It is in fact, a port of [basecamp/mail_view](https://github.com/basecamp/mail_view) gem to the core. 226 | 227 | Alternatively, there are more gems out there to get a similar functionality, but with different approaches and features. Like for example: [ryanb/letter_opener](https://github.com/ryanb/letter_opener), [sj26/mailcatcher](https://github.com/sj26/mailcatcher) or [glebm/rails_email_preview](https://github.com/glebm/rails_email_preview). 228 | 229 | ## Development 230 | 231 | Any kind of feedback, bug report, idea or enhancement are really appreciated :tada: 232 | 233 | To contribute, just fork the repo, hack on it and send a pull request. Don't forget to add tests for behaviour changes and run the test suite: 234 | 235 | ``` 236 | > bundle exec rspec 237 | ``` 238 | 239 | Run the test suite against all supported versions: 240 | 241 | ``` 242 | > bundle exec appraisal install 243 | > bundle exec appraisal rspec 244 | ``` 245 | 246 | Run specs against specific version: 247 | 248 | ``` 249 | > bundle exec appraisal rails-6.0 rspec 250 | ``` 251 | 252 | ### Demo 253 | 254 | Start a sample Rails app ([source code](spec/dummy)) with `Maily` integrated: 255 | 256 | ``` 257 | > bundle exec rake web # PORT=4000 (default: 3000) 258 | ``` 259 | 260 | ## License 261 | 262 | Copyright (c) Marc Anguera. Maily is released under the [MIT](MIT-LICENSE) License. 263 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | 3 | desc 'Start development Rails app' 4 | task :web do 5 | app_path = 'spec/dummy' 6 | port = ENV['PORT'] || 3000 7 | 8 | puts "Starting application in http://localhost:#{port} ... \n" 9 | 10 | Dir.chdir(app_path) 11 | exec("rails s -p #{port}") 12 | end 13 | -------------------------------------------------------------------------------- /app/assets/images/maily/icons/globe.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Globe 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/assets/images/maily/icons/paperclip.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Paperclip 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/assets/images/maily/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markets/maily/a68c23139055516480758448b5eeaae5e0a0ad50/app/assets/images/maily/logo.png -------------------------------------------------------------------------------- /app/controllers/maily/application_controller.rb: -------------------------------------------------------------------------------- 1 | module Maily 2 | class ApplicationController < Maily.base_controller.constantize 3 | before_action :maily_enabled?, :http_authorization 4 | 5 | layout 'maily/application' 6 | 7 | def maily_params 8 | _params = {} 9 | 10 | [:mailer, :email, :version, :part, :locale, :version].each do |key| 11 | _params[key] = params[key] if params[key].present? 12 | end 13 | 14 | _params 15 | end 16 | helper_method :maily_params 17 | 18 | private 19 | 20 | def maily_enabled? 21 | Maily.enabled || head(404, message: "Maily disabled") 22 | end 23 | 24 | def http_authorization 25 | if auth_hash = Maily.http_authorization 26 | authenticate_or_request_with_http_basic do |username, password| 27 | username == auth_hash[:username] && password == auth_hash[:password] 28 | end 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /app/controllers/maily/emails_controller.rb: -------------------------------------------------------------------------------- 1 | module Maily 2 | class EmailsController < Maily::ApplicationController 3 | before_action :allowed_action?, only: [:edit, :update, :deliver] 4 | before_action :load_mailers, only: [:index, :show, :edit] 5 | before_action :load_mailer_and_email, except: [:index] 6 | around_action :perform_with_locale, only: [:show, :raw, :deliver] 7 | 8 | def index 9 | end 10 | 11 | def show 12 | valid, message = @maily_email.validate_arguments 13 | 14 | unless valid 15 | redirect_to(root_path, alert: message) 16 | end 17 | end 18 | 19 | def raw 20 | content = if @email.multipart? 21 | params[:part] == 'text' ? @email.text_part.body : @email.html_part.body 22 | else 23 | @email.body 24 | end 25 | 26 | content = content.raw_source 27 | content = view_context.simple_format(content) if @email.sub_type == 'plain' || params[:part] == 'text' 28 | 29 | render html: content.html_safe, layout: false 30 | end 31 | 32 | def attachment 33 | attachment = @email.attachments.find { |elem| elem.filename == params[:attachment] } 34 | 35 | send_data attachment.body, filename: attachment.filename, type: attachment.content_type 36 | end 37 | 38 | def edit 39 | @template = @maily_email.template(params[:part]) 40 | end 41 | 42 | def update 43 | @maily_email.update_template(params[:body], params[:part]) 44 | 45 | redirect_to maily_email_path(maily_params), notice: 'Template updated!' 46 | end 47 | 48 | def deliver 49 | @email.to = params[:to] 50 | 51 | @email.deliver 52 | 53 | redirect_to maily_email_path(maily_params), notice: "Email sent to #{params[:to]}!" 54 | end 55 | 56 | private 57 | 58 | def allowed_action? 59 | Maily.allowed_action?(action_name) || redirect_to(root_path, alert: "Action #{action_name} not allowed!") 60 | end 61 | 62 | def load_mailers 63 | @mailers = Maily::Mailer.list 64 | end 65 | 66 | def load_mailer_and_email 67 | mailer = Maily::Mailer.find(params[:mailer]) 68 | @maily_email = mailer.find_email(params[:email], params[:version]) 69 | 70 | if @maily_email 71 | @email = @maily_email.call 72 | else 73 | redirect_to(root_path, alert: "Email not found!") 74 | end 75 | end 76 | 77 | def perform_with_locale 78 | I18n.with_locale(params[:locale]) do 79 | yield 80 | end 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /app/helpers/maily/application_helper.rb: -------------------------------------------------------------------------------- 1 | module Maily 2 | module ApplicationHelper 3 | def title 4 | _title = 'Maily' 5 | 6 | if params[:mailer] && params[:email] 7 | _title << " - #{params[:mailer].humanize} | #{params[:email].humanize}" 8 | end 9 | 10 | _title 11 | end 12 | 13 | def sidebar_class(mailer, email) 14 | 'selected_mail' if mailer.name == params[:mailer] && email.name == params[:email] 15 | end 16 | 17 | def version_list_class(email) 18 | 'selected_mail' if email.name == params[:email] && email.version == params[:version] 19 | end 20 | 21 | def logo 22 | image_tag(file_to_base64('logo.png', 'image/png')) 23 | end 24 | 25 | def icon(name) 26 | image_tag(file_to_base64("icons/#{name}.svg", 'image/svg+xml'), class: :icon) 27 | end 28 | 29 | private 30 | 31 | def file_to_base64(path, mime_type) 32 | file = Maily::Engine.root.join('app/assets/images/maily').join(path) 33 | base64_contents = Base64.strict_encode64(file.read) 34 | 35 | "data:#{mime_type};base64,#{base64_contents}" 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /app/helpers/maily/emails_helper.rb: -------------------------------------------------------------------------------- 1 | module Maily 2 | module EmailsHelper 3 | def total_emails(mailers) 4 | mailers.map { |mailer| mailer.total_emails }.sum 5 | end 6 | 7 | def email_description(email) 8 | return unless email.description 9 | 10 | tag.div(class: 'mail_description') do 11 | concat tag.strong('Description ') 12 | concat email.description 13 | end 14 | end 15 | 16 | def part_class(part) 17 | 'format_selected' if part == params[:part] || (part == 'html' && !params[:part]) 18 | end 19 | 20 | def uniq_emails(email_list) 21 | email_list.inject([]) do |memo, email| 22 | memo << email unless memo.map(&:name).include?(email.name) 23 | memo 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /app/views/layouts/maily/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= title %> 5 | <%= csrf_meta_tags %> 6 | <%= render 'maily/shared/stylesheet' %> 7 | <%= render 'maily/shared/javascript' %> 8 | 9 | 10 | <%= render 'maily/shared/header' %> 11 | 12 |
13 | <%= render 'maily/shared/sidebar' %> 14 | 15 |
16 | <%= render 'maily/shared/flash_messages' %> 17 | 18 | <%= yield %> 19 |
20 |
21 | 22 | <%= render 'maily/shared/footer' %> 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/views/maily/emails/edit.html.erb: -------------------------------------------------------------------------------- 1 | <%= email_description(@maily_email) %> 2 | 3 | <%= form_tag(update_maily_email_path(maily_params), method: :put) do %> 4 | 15 | 16 |
17 | <%= text_area_tag :body, @template, class: 'mail_updatearea' %> 18 |
19 | <% end %> 20 | -------------------------------------------------------------------------------- /app/views/maily/emails/index.html.erb: -------------------------------------------------------------------------------- 1 |

<%= total_emails(@mailers) %> emails found

2 |

Use the menu on the left hand side of the screen to navigate through the different email templates.

3 | 4 |
5 | <%= Maily.welcome_message.to_s.html_safe %> 6 |
7 | -------------------------------------------------------------------------------- /app/views/maily/emails/show.html.erb: -------------------------------------------------------------------------------- 1 | <%= email_description(@maily_email) %> 2 | 3 | 35 | 36 | <% if @maily_email.has_versions? %> 37 |
38 | Versions 39 | 44 |
45 | <% end %> 46 | 47 |
48 | 56 | 57 | <% if @email.html_part && @email.text_part %> 58 | 62 | <% end %> 63 | 64 | <% if @email.html_part || @email.text_part || @email.body.present? %> 65 | 66 | <% end %> 67 | 68 | <% if @email.has_attachments? %> 69 | 82 | <% end %> 83 |
84 | -------------------------------------------------------------------------------- /app/views/maily/shared/_flash_messages.html.erb: -------------------------------------------------------------------------------- 1 | <% if flash[:alert].present? %> 2 |
<%= flash[:alert].html_safe %>
3 | <% end %> 4 | 5 | <% if flash[:notice].present? %> 6 |
<%= flash[:notice].html_safe %>
7 | <% end %> 8 | -------------------------------------------------------------------------------- /app/views/maily/shared/_footer.html.erb: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /app/views/maily/shared/_header.html.erb: -------------------------------------------------------------------------------- 1 |
2 | 3 | <%= logo %> 4 | 5 | 6 | 7 | Back to app 8 | 9 |
10 | -------------------------------------------------------------------------------- /app/views/maily/shared/_javascript.html.erb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/maily/shared/_sidebar.html.erb: -------------------------------------------------------------------------------- 1 | 22 | -------------------------------------------------------------------------------- /app/views/maily/shared/_stylesheet.html.erb: -------------------------------------------------------------------------------- 1 | <% 2 | blue = "#59abc6" 3 | grey = "#cccccc" 4 | dark_blue = "#2f738a" 5 | dark_grey = "#666666" 6 | text_color = "#333333" 7 | link_color = blue 8 | hover_color = dark_blue 9 | border_radius = "3px" 10 | %> 11 | 12 | 284 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Maily::Engine.routes.draw do 2 | get ':mailer/:email' => 'emails#show', as: :maily_email 3 | get ':mailer/:email/raw' => 'emails#raw', as: :raw_maily_email 4 | get ':mailer/:email/edit' => 'emails#edit', as: :edit_maily_email 5 | put ':mailer/:email' => 'emails#update', as: :update_maily_email 6 | post ':mailer/:email/deliver' => 'emails#deliver', as: :deliver_maily_email 7 | get ':mailer/:email/attachment' => 'emails#attachment', as: :attachment_maily_email 8 | 9 | root :to => 'emails#index' 10 | end 11 | -------------------------------------------------------------------------------- /gemfiles/rails_5.2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "byebug", group: [:development, :test] 6 | gem "rails", "~> 5.2.0" 7 | 8 | gemspec path: "../" 9 | -------------------------------------------------------------------------------- /gemfiles/rails_6.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "byebug", group: [:development, :test] 6 | gem "rails", "~> 6.0.0" 7 | 8 | gemspec path: "../" 9 | -------------------------------------------------------------------------------- /gemfiles/rails_6.1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "byebug", group: [:development, :test] 6 | gem "rails", "~> 6.1.0" 7 | 8 | gemspec path: "../" 9 | -------------------------------------------------------------------------------- /gemfiles/rails_7.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "byebug", group: [:development, :test] 6 | gem "rails", "~> 7.0.0" 7 | 8 | gemspec path: "../" 9 | -------------------------------------------------------------------------------- /gemfiles/rails_7.1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "byebug", group: [:development, :test] 6 | gem "rails", "~> 7.1.0" 7 | 8 | gemspec path: "../" 9 | -------------------------------------------------------------------------------- /lib/generators/maily/install_generator.rb: -------------------------------------------------------------------------------- 1 | module Maily 2 | module Generators 3 | class InstallGenerator < Rails::Generators::Base 4 | desc 'Maily installation: route and initializer' 5 | source_root File.expand_path("../../templates", __FILE__) 6 | 7 | def install 8 | puts "==> Installing Maily components ..." 9 | generate_routing 10 | copy_initializer 11 | build_hooks 12 | puts "Ready! You can now access Maily at /maily" 13 | end 14 | 15 | private 16 | 17 | def generate_routing 18 | route "mount Maily::Engine, at: '/maily'" 19 | end 20 | 21 | def copy_initializer 22 | template 'initializer.rb', 'config/initializers/maily.rb' 23 | end 24 | 25 | def build_hooks 26 | create_file "lib/maily_hooks.rb" do 27 | Maily::Generator.run 28 | end 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/generators/templates/initializer.rb: -------------------------------------------------------------------------------- 1 | Maily.setup do |config| 2 | # On/off engine 3 | # config.enabled = !Rails.env.production? 4 | 5 | # Allow templates edition 6 | # config.allow_edition = !Rails.env.production? 7 | 8 | # Allow deliveries 9 | # config.allow_delivery = !Rails.env.production? 10 | 11 | # Your application available_locales (or I18n.available_locales) by default 12 | # config.available_locales = [:en, :es, :pt, :fr] 13 | 14 | # Run maily under different controller ('ActionController::Base' by default) 15 | # config.base_controller = '::AdminController' 16 | 17 | # Configure hooks path 18 | # config.hooks_path = 'lib/maily_hooks.rb' 19 | 20 | # Http basic authentication (nil by default) 21 | # config.http_authorization = { username: 'admin', password: 'secret' } 22 | 23 | # Customize welcome message 24 | # config.welcome_message = "Welcome to our email testing platform. If you have any problem, please contact support team at support@example.com." 25 | end 26 | -------------------------------------------------------------------------------- /lib/maily.rb: -------------------------------------------------------------------------------- 1 | require 'maily/version' 2 | require 'maily/engine' 3 | require 'maily/mailer' 4 | require 'maily/email' 5 | require 'maily/generator' 6 | 7 | module Maily 8 | class << self 9 | attr_accessor :enabled, :allow_edition, :allow_delivery, :available_locales, 10 | :base_controller, :http_authorization, :hooks_path, :welcome_message 11 | 12 | def init! 13 | self.enabled = !Rails.env.production? 14 | self.allow_edition = !Rails.env.production? 15 | self.allow_delivery = !Rails.env.production? 16 | self.available_locales = Rails.application.config.i18n.available_locales || I18n.available_locales 17 | self.base_controller = 'ActionController::Base' 18 | self.http_authorization = nil 19 | self.hooks_path = "lib/maily_hooks.rb" 20 | self.welcome_message = nil 21 | end 22 | 23 | def load_emails_and_hooks 24 | # Load emails from file system 25 | Dir[Rails.root + 'app/mailers/*.rb'].each do |mailer| 26 | klass_name = File.basename(mailer, '.rb') 27 | Maily::Mailer.new(klass_name) 28 | end 29 | 30 | # Load hooks 31 | hooks_file_path = File.join(Rails.root, hooks_path) 32 | require hooks_file_path if File.exist?(hooks_file_path) 33 | end 34 | 35 | def hooks_for(mailer_name) 36 | mailer_name = mailer_name.underscore 37 | mailer = Maily::Mailer.find(mailer_name) || Maily::Mailer.new(mailer_name) 38 | 39 | yield(mailer) if block_given? 40 | end 41 | 42 | def setup 43 | init! 44 | yield(self) if block_given? 45 | end 46 | 47 | def allowed_action?(action) 48 | case action.to_sym 49 | when :edit 50 | allow_edition 51 | when :update 52 | allow_edition && !Rails.env.production? 53 | when :deliver 54 | allow_delivery 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/maily/email.rb: -------------------------------------------------------------------------------- 1 | module Maily 2 | class Email 3 | DEFAULT_VERSION = 'default'.freeze 4 | 5 | attr_accessor :name, :mailer, :arguments, :template_path, :template_name, :description, :with_params, :version 6 | 7 | class << self 8 | def name_with_version(name, version = nil) 9 | _version = formatted_version(version) 10 | [name, _version].join(':') 11 | end 12 | 13 | def formatted_version(version) 14 | _version = version.presence || DEFAULT_VERSION 15 | _version&.parameterize&.underscore 16 | end 17 | end 18 | 19 | def initialize(name, mailer) 20 | self.name = name 21 | self.mailer = mailer 22 | self.arguments = nil 23 | self.with_params = nil 24 | self.template_path = mailer.name 25 | self.template_name = name 26 | self.description = nil 27 | end 28 | 29 | def mailer_klass 30 | mailer.klass 31 | end 32 | 33 | def parameterized_mailer_klass 34 | params = with_params && with_params.transform_values { |param| param.respond_to?(:call) ? param.call : param } 35 | mailer_klass.with(params) 36 | end 37 | 38 | def parameters 39 | mailer_klass.instance_method(name).parameters 40 | end 41 | 42 | def require_hook? 43 | parameters.any? 44 | end 45 | 46 | def required_arguments 47 | parameters.select { |param| param.first == :req }.map(&:last) 48 | end 49 | 50 | def optional_arguments 51 | parameters.select { |param| param.first == :opt }.map(&:last) 52 | end 53 | 54 | def validate_arguments 55 | from = required_arguments.size 56 | to = from + optional_arguments.size 57 | passed_by_hook = arguments && arguments.size || 0 58 | 59 | if passed_by_hook < from 60 | [false, "#{name} email requires at least #{from} arguments, passed #{passed_by_hook}"] 61 | elsif passed_by_hook > to 62 | [false, "#{name} email requires at the most #{to} arguments, passed #{passed_by_hook}"] 63 | else 64 | [true, nil] 65 | end 66 | end 67 | 68 | def register_hook(*args) 69 | if args.last.is_a?(Hash) 70 | self.description = args.last.delete(:description) 71 | self.with_params = args.last.delete(:with_params) 72 | self.version = Maily::Email.formatted_version(args.last.delete(:version)) 73 | 74 | if tpl_path = args.last.delete(:template_path) 75 | self.template_path = tpl_path 76 | end 77 | 78 | if tpl_name = args.last.delete(:template_name) 79 | self.template_name = tpl_name 80 | end 81 | 82 | args.pop 83 | end 84 | 85 | self.arguments = args 86 | end 87 | 88 | def call 89 | *args = arguments && arguments.map { |arg| arg.respond_to?(:call) ? arg.call : arg } 90 | 91 | message = if args == [nil] 92 | parameterized_mailer_klass.public_send(name) 93 | else 94 | parameterized_mailer_klass.public_send(name, *args) 95 | end 96 | 97 | ActionMailer::Base.preview_interceptors.each do |interceptor| 98 | interceptor.previewing_email(message) 99 | end 100 | 101 | message 102 | end 103 | 104 | def base_path(part) 105 | Dir["#{Rails.root}/app/views/#{template_path}/#{template_name}.#{part}.*"].first 106 | end 107 | 108 | def path(part = nil) 109 | return base_path(part) if part 110 | 111 | html_part = base_path('html') 112 | if html_part && File.exist?(html_part) 113 | html_part 114 | else 115 | base_path('text') 116 | end 117 | end 118 | 119 | def template(part = nil) 120 | File.read(path(part)) 121 | end 122 | 123 | def update_template(new_content, part = nil) 124 | File.open(path(part), 'w') do |f| 125 | f.write(new_content) 126 | end 127 | end 128 | 129 | def versions 130 | regexp = Regexp.new("^#{self.name}:") 131 | 132 | mailer.emails.select do |email_key, _email| 133 | email_key.match?(regexp) 134 | end 135 | end 136 | 137 | def has_versions? 138 | versions.count > 1 139 | end 140 | end 141 | end 142 | -------------------------------------------------------------------------------- /lib/maily/engine.rb: -------------------------------------------------------------------------------- 1 | module Maily 2 | class Engine < ::Rails::Engine 3 | isolate_namespace Maily 4 | load_generators 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/maily/generator.rb: -------------------------------------------------------------------------------- 1 | module Maily 2 | module Generator 3 | def self.run 4 | Maily.init! 5 | 6 | fixtures = [] 7 | hooks = [] 8 | 9 | Maily::Mailer.list.each do |mailer| 10 | _hooks = [] 11 | 12 | mailer.emails_list.each do |email| 13 | if email.require_hook? 14 | fixtures << email.required_arguments 15 | _hooks << " mailer.register_hook(:#{email.name}, #{email.required_arguments.join(', ')})" 16 | end 17 | end 18 | 19 | if _hooks.present? 20 | hooks << "\nMaily.hooks_for('#{mailer.name.classify}') do |mailer|" 21 | hooks << _hooks 22 | hooks << "end" 23 | end 24 | end 25 | 26 | fixtures = fixtures.flatten.uniq.map do |fixture| 27 | argument = fixture.to_s 28 | value = argument.pluralize == argument ? '[]' : "''" 29 | 30 | [argument, value].join(' = ') 31 | end.join("\n") 32 | 33 | fixtures + "\n" + hooks.join("\n") + "\n" 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/maily/mailer.rb: -------------------------------------------------------------------------------- 1 | module Maily 2 | class Mailer 3 | cattr_accessor :collection 4 | attr_accessor :name, :emails, :klass 5 | 6 | def initialize(name) 7 | self.name = name 8 | self.klass = name.camelize.constantize 9 | self.emails = {} 10 | 11 | parse_emails 12 | 13 | self.class.collection ||= {} 14 | self.class.collection[name] = self 15 | end 16 | 17 | def self.all 18 | Maily.load_emails_and_hooks if collection.nil? 19 | collection 20 | end 21 | 22 | def self.list 23 | all.values.sort_by(&:name) 24 | end 25 | 26 | def self.find(mailer_name) 27 | all[mailer_name] 28 | end 29 | 30 | def find_email(email_name, version = nil) 31 | key = Maily::Email.name_with_version(email_name, version) 32 | emails[key] 33 | end 34 | 35 | def emails_list 36 | emails.values.sort_by(&:name) 37 | end 38 | 39 | def total_emails 40 | emails.size 41 | end 42 | 43 | def register_hook(email_name, *args) 44 | version = get_version(*args) 45 | email = find_email(email_name, version) || add_email(email_name, version) 46 | email && email.register_hook(*args) 47 | end 48 | 49 | def hide_email(*email_names) 50 | email_names.each do |email_name| 51 | _email_name = Maily::Email.name_with_version(email_name.to_s) 52 | emails.delete(_email_name) 53 | end 54 | end 55 | 56 | private 57 | 58 | def parse_emails 59 | _emails = klass.send(:public_instance_methods, false) 60 | 61 | _emails.each do |email| 62 | add_email(email) 63 | end 64 | end 65 | 66 | def add_email(email_name, version = nil) 67 | hide_email(email_name) if version 68 | email = Maily::Email.new(email_name.to_s, self) 69 | key = Maily::Email.name_with_version(email_name, version) 70 | emails[key] = email 71 | end 72 | 73 | def get_version(*args) 74 | return unless args.last.is_a?(Hash) 75 | 76 | Maily::Email.formatted_version(args.last[:version]) 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/maily/version.rb: -------------------------------------------------------------------------------- 1 | module Maily 2 | VERSION = "2.1.0" 3 | end 4 | -------------------------------------------------------------------------------- /maily.gemspec: -------------------------------------------------------------------------------- 1 | require "./lib/maily/version" 2 | 3 | Gem::Specification.new do |s| 4 | s.name = "maily" 5 | s.version = Maily::VERSION 6 | s.authors = ["markets"] 7 | s.email = ["srmarc.ai@gmail.com"] 8 | s.homepage = "https://github.com/markets/maily" 9 | s.summary = "Rails Engine to preview emails in the browser." 10 | s.description = "Maily is a Rails Engine to manage, test and navigate through all your email templates of your app, being able to preview them directly in your browser." 11 | s.license = "MIT" 12 | 13 | s.files = Dir["{app,config,lib}/**/*"] + %w[README.md CHANGELOG.md MIT-LICENSE] 14 | s.require_paths = ["lib"] 15 | 16 | s.add_dependency "rails", ">= 4.2" 17 | 18 | s.add_development_dependency "rspec-rails" 19 | s.add_development_dependency "appraisal" 20 | end 21 | -------------------------------------------------------------------------------- /spec/controllers_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe Maily::EmailsController, type: :controller do 2 | render_views 3 | 4 | routes { Maily::Engine.routes } 5 | 6 | before(:each) do 7 | Maily.init! 8 | end 9 | 10 | it 'responds ok if enabled' do 11 | expect { get :index }.not_to raise_error 12 | end 13 | 14 | it 'raises 404 if disabled' do 15 | Maily.enabled = false 16 | 17 | get :index 18 | 19 | expect(response.status).to eq(404) 20 | end 21 | 22 | it 'responds with 401 if http authorization fails' do 23 | Maily.http_authorization = { username: 'admin', password: 'admin' } 24 | 25 | get :index 26 | 27 | expect(response.status).to eq(401) 28 | end 29 | 30 | it 'responds ok with valid http authorization' do 31 | Maily.http_authorization = { username: 'admin', password: 'admin' } 32 | request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials('admin', 'admin') 33 | 34 | get :index 35 | 36 | expect(response.status).to eq(200) 37 | end 38 | 39 | describe 'non-existent emails' do 40 | it 'email not in the system -> 302' do 41 | get :show, params: { mailer: 'notifier', email: 'non_existent_email' } 42 | 43 | expect(response.status).to eq(302) 44 | expect(flash.alert).to eq("Email not found!") 45 | end 46 | 47 | it 'hidden emails -> 302' do 48 | get :show, params: { mailer: 'notifier', email: 'hidden' } 49 | 50 | expect(response.status).to eq(302) 51 | expect(flash.alert).to eq("Email not found!") 52 | end 53 | end 54 | 55 | describe 'GET #raw' do 56 | it 'renders the template (HTML part)' do 57 | get :raw, params: { mailer: 'notifier', email: 'with_arguments' } 58 | 59 | expect(response.body).to match("

With arguments

") 60 | end 61 | 62 | it 'renders the template (TEXT part)' do 63 | get :raw, params: { mailer: 'notifier', email: 'only_text' } 64 | 65 | expect(response.body).to match("

Text part\n
with break lines

") 66 | end 67 | 68 | it 'renders inline attachments' do 69 | get :raw, params: { mailer: 'notifier', email: 'with_inline_attachments' } 70 | 71 | expect(response.body).to match("data:image/jpeg;base64") 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /spec/dummy/README.md: -------------------------------------------------------------------------------- 1 | # Dummy App 2 | 3 | Dummy Rails application to test the `Maily` engine. 4 | 5 | Run the suite (from `Maily` root path): 6 | 7 | ``` 8 | > bundle exec rake 9 | ``` 10 | 11 | It's also used as a demo application to show `Maily` in action and for development purposes. You can run the app locally by using the following command: 12 | 13 | ``` 14 | > bundle exec rake web 15 | ``` 16 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: 'foo@foo.com', to: 'bar@bar.com' 3 | 4 | def from_other_class 5 | mail template_path: 'notifier', template_name: 'custom_template' 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/dummy/app/mailers/notifier.rb: -------------------------------------------------------------------------------- 1 | class Notifier < ApplicationMailer 2 | def no_arguments 3 | mail 4 | end 5 | 6 | def missing_arguments(email) 7 | mail 8 | end 9 | 10 | def with_arguments(email, opt_arg = nil) 11 | mail 12 | end 13 | 14 | def with_array_arguments(emails) 15 | mail 16 | end 17 | 18 | def with_description 19 | mail 20 | end 21 | 22 | def with_params 23 | @message = params[:message] 24 | mail 25 | end 26 | 27 | def custom_template_path 28 | mail template_path: 'notifications' 29 | end 30 | 31 | def custom_template_name 32 | mail template_name: 'custom_template' 33 | end 34 | 35 | def hidden 36 | mail 37 | end 38 | 39 | def multipart 40 | mail 41 | end 42 | 43 | def only_text 44 | mail 45 | end 46 | 47 | def with_slim_template 48 | mail 49 | end 50 | 51 | def with_inline_attachments 52 | attachments.inline['image.jpg'] = File.read(Rails.root.join("public/favicon.ico")) 53 | mail 54 | end 55 | 56 | def version(account) 57 | @account = account 58 | mail 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/dummy/app/views/notifications/custom_template_path.html.erb: -------------------------------------------------------------------------------- 1 |

Custom template path

2 | -------------------------------------------------------------------------------- /spec/dummy/app/views/notifier/custom_template.html.erb: -------------------------------------------------------------------------------- 1 |

Custom template

2 | -------------------------------------------------------------------------------- /spec/dummy/app/views/notifier/multipart.html.erb: -------------------------------------------------------------------------------- 1 |

Multipart HTML

2 | -------------------------------------------------------------------------------- /spec/dummy/app/views/notifier/multipart.text.erb: -------------------------------------------------------------------------------- 1 | Multipart plain text 2 | -------------------------------------------------------------------------------- /spec/dummy/app/views/notifier/no_arguments.html.erb: -------------------------------------------------------------------------------- 1 |

No arguments

2 | -------------------------------------------------------------------------------- /spec/dummy/app/views/notifier/only_text.text.erb: -------------------------------------------------------------------------------- 1 | Text part 2 | with break lines -------------------------------------------------------------------------------- /spec/dummy/app/views/notifier/version.html.erb: -------------------------------------------------------------------------------- 1 |

Welcome to version email <%= @account.name %>

2 | -------------------------------------------------------------------------------- /spec/dummy/app/views/notifier/with_arguments.html.erb: -------------------------------------------------------------------------------- 1 |

With arguments

2 | -------------------------------------------------------------------------------- /spec/dummy/app/views/notifier/with_array_arguments.html.erb: -------------------------------------------------------------------------------- 1 |

With array arguments

2 | -------------------------------------------------------------------------------- /spec/dummy/app/views/notifier/with_description.html.erb: -------------------------------------------------------------------------------- 1 |

This email has a description

2 | -------------------------------------------------------------------------------- /spec/dummy/app/views/notifier/with_inline_attachments.html.erb: -------------------------------------------------------------------------------- 1 |

Inline Attachments

2 | 3 |

4 | <%= image_tag attachments['image.jpg'].url %> 5 |

6 | -------------------------------------------------------------------------------- /spec/dummy/app/views/notifier/with_params.html.erb: -------------------------------------------------------------------------------- 1 |

With params

2 | <%= @message %> 3 | -------------------------------------------------------------------------------- /spec/dummy/app/views/notifier/with_slim_template.html.slim: -------------------------------------------------------------------------------- 1 | p Hi from a Slim template 2 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /spec/dummy/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /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/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require 'action_controller/railtie' 4 | require 'action_view/railtie' 5 | require 'action_mailer/railtie' 6 | 7 | Bundler.require(*Rails.groups) 8 | 9 | require 'maily' 10 | 11 | module Dummy 12 | class Application < Rails::Application 13 | # Settings in config/environments/* take precedence over those specified here. 14 | # Application configuration should go into files in config/initializers 15 | # -- all .rb files in that directory are automatically loaded. 16 | 17 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 18 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 19 | # config.time_zone = 'Central Time (US & Canada)' 20 | 21 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 22 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 23 | # config.i18n.default_locale = :de 24 | config.i18n.available_locales = [:en, :es, :pt, :fr] 25 | end 26 | end 27 | 28 | -------------------------------------------------------------------------------- /spec/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 2 | 3 | require 'bundler/setup' # Set up gems listed in the Gemfile. 4 | -------------------------------------------------------------------------------- /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/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 | # Raise an error on page load if there are pending migrations 23 | # config.active_record.migration_error = :page_load 24 | end 25 | -------------------------------------------------------------------------------- /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_assets = 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 | # Precompile additional assets. 48 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 49 | # config.assets.precompile += %w( search.js ) 50 | 51 | # Ignore bad email addresses and do not raise email delivery errors. 52 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 53 | # config.action_mailer.raise_delivery_errors = false 54 | 55 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 56 | # the I18n.default_locale when a translation can not be found). 57 | config.i18n.fallbacks = true 58 | 59 | # Send deprecation notices to registered listeners. 60 | config.active_support.deprecation = :notify 61 | 62 | # Disable automatic flushing of the log to improve performance. 63 | # config.autoflush_log = false 64 | 65 | # Use default logging formatter so that PID and timestamp are not suppressed. 66 | config.log_formatter = ::Logger::Formatter.new 67 | end 68 | -------------------------------------------------------------------------------- /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 | config.serve_static_assets = true 17 | config.static_cache_control = "public, max-age=3600" 18 | 19 | # Show full error reports and disable caching. 20 | config.consider_all_requests_local = true 21 | config.action_controller.perform_caching = false 22 | 23 | # Raise exceptions instead of rendering exception templates. 24 | config.action_dispatch.show_exceptions = false 25 | 26 | # Disable request forgery protection in test environment. 27 | config.action_controller.allow_forgery_protection = false 28 | 29 | # Tell Action Mailer not to deliver emails to the real world. 30 | # The :test delivery method accumulates sent emails in the 31 | # ActionMailer::Base.deliveries array. 32 | config.action_mailer.delivery_method = :test 33 | 34 | # Print deprecation notices to the stderr. 35 | config.active_support.deprecation = :stderr 36 | end 37 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/maily.rb: -------------------------------------------------------------------------------- 1 | Maily.setup do |config| 2 | config.enabled = true 3 | config.allow_edition = true 4 | config.allow_delivery = true 5 | end 6 | -------------------------------------------------------------------------------- /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/config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rake secret` to generate a secure secret key. 9 | 10 | # Make sure your secret_key_base is kept private 11 | # if you're sharing your code publicly. 12 | Dummy::Application.config.secret_key_base = '2a1193cd2b7a71bb336cc1939bb8e651b330fe43d4792794b0aef2f61a72e3599fa780d0189c303865f8df19785b33b8f3291201299b382766d7dac7953a5983' 13 | -------------------------------------------------------------------------------- /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/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/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # To learn more, please read the Rails Internationalization guide 20 | # available at http://guides.rubyonrails.org/i18n.html. 21 | 22 | en: 23 | hello: "Hello world" 24 | -------------------------------------------------------------------------------- /spec/dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.routes.draw do 2 | mount Maily::Engine, at: '/maily' 3 | 4 | root to: redirect('/maily') 5 | end 6 | -------------------------------------------------------------------------------- /spec/dummy/lib/maily_hooks.rb: -------------------------------------------------------------------------------- 1 | email = -> { 'foo@foo.com' } 2 | emails = ['bar@foo.com'] 3 | message = -> { 'Hello!' } 4 | free_trial_account = -> { OpenStruct.new({ name: 'Nicolas Cage' }) } 5 | gold_account = -> { OpenStruct.new({ name: 'John Doe' }) } 6 | 7 | Maily.hooks_for('Notifier') do |mailer| 8 | mailer.register_hook(:with_arguments, email) 9 | mailer.register_hook(:with_array_arguments, emails) 10 | mailer.register_hook(:with_description, description: 'description') 11 | mailer.register_hook(:with_params, with_params: { message: message }) 12 | mailer.register_hook(:custom_template_path, template_path: 'notifications') 13 | mailer.register_hook(:custom_template_name, template_name: 'custom_template') 14 | mailer.register_hook(:from_other_class) 15 | mailer.register_hook(:version, free_trial_account, version: 'Free trial account') 16 | mailer.register_hook(:version, gold_account, version: 'Gold account') 17 | 18 | mailer.hide_email(:hidden) 19 | end 20 | -------------------------------------------------------------------------------- /spec/dummy/log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markets/maily/a68c23139055516480758448b5eeaae5e0a0ad50/spec/dummy/log/.keep -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/dummy/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markets/maily/a68c23139055516480758448b5eeaae5e0a0ad50/spec/dummy/public/favicon.ico -------------------------------------------------------------------------------- /spec/email_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe Maily::Email do 2 | let(:mailer) { Maily::Mailer.find('notifier') } 3 | 4 | context "with no arguments" do 5 | let (:email) { mailer.find_email('no_arguments') } 6 | 7 | it "should not require hook" do 8 | expect(email.required_arguments).to be_blank 9 | expect(email.require_hook?).to be false 10 | end 11 | 12 | it ".call" do 13 | expect { email.call }.to_not raise_error 14 | end 15 | end 16 | 17 | context "with arguments" do 18 | let (:email) { mailer.find_email('with_arguments') } 19 | 20 | it "should require hook" do 21 | expect(email.required_arguments).to be_present 22 | expect(email.require_hook?).to be true 23 | end 24 | 25 | it 'should handle lazy arguments successfully' do 26 | expect(email.arguments).to be_present 27 | expect(email.arguments.size).to eq(email.required_arguments.size) 28 | end 29 | 30 | it 'should handle not lazy arguments successfully' do 31 | allow(email).to receive(:email).and_return('foo@foo.com') 32 | 33 | expect(email.arguments).to be_present 34 | expect(email.arguments.size).to eq(email.required_arguments.size) 35 | end 36 | 37 | it 'should handle array type arguments' do 38 | email = mailer.find_email('with_array_arguments') 39 | 40 | expect(email.arguments.first).to be_an(Array) 41 | expect(email.arguments.size).to eq(email.required_arguments.size) 42 | end 43 | 44 | it ".call" do 45 | expect { email.call }.to_not raise_error 46 | end 47 | end 48 | 49 | context "with params" do 50 | let (:email) { mailer.find_email('with_params') } 51 | 52 | it "should not require hook" do 53 | expect(email.required_arguments).to be_blank 54 | expect(email.require_hook?).to be false 55 | end 56 | 57 | it 'should handle lazy params successfully' do 58 | expect(email.with_params).to be_present 59 | expect { email.call }.to_not raise_error 60 | end 61 | 62 | it 'should handle not lazy arguments successfully' do 63 | allow(email).to receive(:message).and_return('Hello!') 64 | 65 | expect(email.with_params).to be_present 66 | expect { email.call }.to_not raise_error 67 | end 68 | end 69 | 70 | it "should handle template_path via hook" do 71 | email = mailer.find_email('custom_template_path') 72 | 73 | expect(email.template_path).to eq('notifications') 74 | end 75 | 76 | it "should handle template_name via hook" do 77 | email = mailer.find_email('custom_template_name') 78 | 79 | expect(email.template_name).to eq('custom_template') 80 | end 81 | 82 | it "should handle description via hook" do 83 | email = mailer.find_email('with_description') 84 | 85 | expect(email.description).to eq('description') 86 | end 87 | 88 | describe '#validate_arguments' do 89 | it 'emails with no arguments required' do 90 | email = mailer.find_email('no_arguments') 91 | expect(email.validate_arguments).to eq [true, nil] 92 | 93 | email.arguments = ["asd"] 94 | expect(email.validate_arguments[1]).to match(/email requires at the most 0 arguments, passed 1/) 95 | 96 | email.arguments = nil # reset arguments for future calls 97 | end 98 | 99 | it 'emails with arguments required' do 100 | email = mailer.find_email('with_arguments') 101 | expect(email.validate_arguments).to eq [true, nil] 102 | 103 | email = mailer.find_email('missing_arguments') 104 | expect(email.validate_arguments[1]).to match(/email requires at least 1 arguments, passed 0/) 105 | end 106 | end 107 | 108 | describe '#path' do 109 | it 'with a multipart email defaults to html' do 110 | email = mailer.find_email('multipart') 111 | 112 | expect(email.path).to include('multipart.html.erb') 113 | expect(email.path('text')).to include('multipart.text.erb') 114 | expect(email.path('foo')).to eq nil 115 | end 116 | 117 | it 'with other template engines (e.g. Slim)' do 118 | email = mailer.find_email('with_slim_template') 119 | 120 | expect(email.path).to include('with_slim_template.html.slim') 121 | end 122 | end 123 | 124 | context 'class methods' do 125 | describe '#name_with_version' do 126 | it 'without version' do 127 | expect(Maily::Email.name_with_version('welcome', nil)).to eq("welcome:#{Maily::Email::DEFAULT_VERSION}") 128 | end 129 | 130 | it 'with version' do 131 | expect(Maily::Email.name_with_version('welcome', 'first_version')).to eq('welcome:first_version') 132 | end 133 | end 134 | 135 | describe '#formatted_version' do 136 | it 'without version' do 137 | expect(Maily::Email.formatted_version(nil)).to eq(Maily::Email.formatted_version(Maily::Email::DEFAULT_VERSION)) 138 | end 139 | 140 | it 'with version' do 141 | expect(Maily::Email.formatted_version('First Version')).to eq('first_version') 142 | end 143 | end 144 | end 145 | end 146 | -------------------------------------------------------------------------------- /spec/generator_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe Maily::Generator do 2 | it '.run generates valid fixtures and hooks for current application' do 3 | expect(Maily::Generator.run).to eq <<-HOOKS.strip_heredoc 4 | email = '' 5 | account = '' 6 | emails = [] 7 | 8 | Maily.hooks_for('Notifier') do |mailer| 9 | mailer.register_hook(:missing_arguments, email) 10 | mailer.register_hook(:version, account) 11 | mailer.register_hook(:version, account) 12 | mailer.register_hook(:with_arguments, email) 13 | mailer.register_hook(:with_array_arguments, emails) 14 | end 15 | HOOKS 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/mailer_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe Maily::Mailer do 2 | let(:mailer) { Maily::Mailer.find('notifier') } 3 | 4 | it "should load mailers" do 5 | expect(Maily::Mailer.all.keys).to include('application_mailer', 'notifier') 6 | end 7 | 8 | it "should build emails" do 9 | expect(mailer.emails.size).to eq(15) 10 | end 11 | 12 | it "should find mailers by name" do 13 | expect(Maily::Mailer.find('notifier').name).to eq('notifier') 14 | end 15 | 16 | it "should find emails by name" do 17 | expect(mailer.find_email('no_arguments').name).to eq('no_arguments') 18 | end 19 | 20 | it "should find emails by name and version" do 21 | email_name = 'version' 22 | version = Maily::Email.formatted_version('Gold account') 23 | email = mailer.find_email(email_name, version) 24 | expect(email.name).to eq(email_name) 25 | expect(email.version).to eq(version) 26 | end 27 | 28 | it "allow to add inherited emails via a hook" do 29 | expect(mailer.find_email('from_other_class').name).to eq('from_other_class') 30 | end 31 | 32 | it "allows to hide email" do 33 | expect(mailer.find_email('hidden')).to be nil 34 | end 35 | 36 | it ".list returns an array with all mailers" do 37 | list = Maily::Mailer.list 38 | 39 | expect(list).to be_an_instance_of(Array) 40 | expect(list.sample).to be_an_instance_of(Maily::Mailer) 41 | end 42 | 43 | it "#emails_list returns an array with all emails" do 44 | expect(mailer.emails_list).to be_an_instance_of(Array) 45 | expect(mailer.emails_list.sample).to be_an_instance_of(Maily::Email) 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/maily_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe Maily do 2 | it "#setup should initialize with some defaults if no block is provided" do 3 | Maily.setup 4 | 5 | expect(Maily.enabled).to be true 6 | expect(Maily.allow_edition).to be true 7 | expect(Maily.allow_delivery).to be true 8 | expect(Maily.available_locales).to eq([:en, :es, :pt, :fr]) 9 | expect(Maily.base_controller).to eq('ActionController::Base') 10 | expect(Maily.http_authorization).to be nil 11 | expect(Maily.welcome_message).to be nil 12 | end 13 | 14 | describe '#allowed_action?' do 15 | it "should not allow edition if edition is disabled" do 16 | Maily.allow_edition = false 17 | 18 | expect(Maily.allowed_action?(:edit)).to be false 19 | expect(Maily.allowed_action?(:update)).to be false 20 | end 21 | 22 | it "should not allow delivery if delivery is disabled" do 23 | Maily.allow_delivery = false 24 | 25 | expect(Maily.allowed_action?(:deliver)).to be false 26 | end 27 | end 28 | 29 | describe '#hooks_for' do 30 | it "allows to register external hooks" do 31 | class ExternalMailer < ActionMailer::Base 32 | def external_email 33 | end 34 | end 35 | 36 | Maily.hooks_for('ExternalMailer') 37 | 38 | expect(Maily::Mailer.find('external_mailer').emails.count).to eq 1 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | ENV['RAILS_ENV'] = 'test' 2 | 3 | require File.expand_path("../dummy/config/environment.rb", __FILE__) 4 | require 'rspec/rails' 5 | require 'maily' 6 | 7 | RSpec.configure do |config| 8 | config.mock_with :rspec 9 | config.order = 'random' 10 | config.disable_monkey_patching! 11 | end 12 | -------------------------------------------------------------------------------- /support/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markets/maily/a68c23139055516480758448b5eeaae5e0a0ad50/support/images/logo.png -------------------------------------------------------------------------------- /support/images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markets/maily/a68c23139055516480758448b5eeaae5e0a0ad50/support/images/screenshot.png --------------------------------------------------------------------------------