├── .github └── workflows │ └── ruby.yml ├── .gitignore ├── .rspec ├── CHANGELOG.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── letter_opener.gemspec ├── lib ├── letter_opener.rb └── letter_opener │ ├── configuration.rb │ ├── delivery_method.rb │ ├── message.rb │ ├── railtie.rb │ ├── tasks │ └── letter_opener.rake │ └── templates │ ├── default.html.erb │ └── light.html.erb └── spec ├── letter_opener ├── delivery_method_spec.rb └── message_spec.rb └── spec_helper.rb /.github/workflows/ruby.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake 6 | # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby 7 | 8 | name: Ruby 9 | 10 | on: 11 | push: 12 | branches: [ master ] 13 | pull_request: 14 | branches: [ master ] 15 | 16 | jobs: 17 | test: 18 | runs-on: ubuntu-latest 19 | strategy: 20 | matrix: 21 | ruby-version: ['3.0', '3.1', '3.2', '3.3', 'truffleruby', 'jruby'] 22 | 23 | steps: 24 | - uses: actions/checkout@v4 25 | - name: Set up Ruby 26 | # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby, 27 | # change this to (see https://github.com/ruby/setup-ruby#versioning): 28 | uses: ruby/setup-ruby@v1 29 | with: 30 | ruby-version: ${{ matrix.ruby-version }} 31 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 32 | - name: Run tests 33 | run: bundle exec rake 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | Gemfile.lock 4 | pkg/* 5 | tmp/* 6 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --backtrace 3 | --order rand 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.10.0 ## 2 | * Allow Launchy 3.0+. 3 | 4 | ## 1.9.0 ## 5 | * Store mail location in `Mail::Message` object (thanks [Jonathan Chan](https://github.com/jonmchan)) 6 | * Drop Ruby 2 support. Support only Ruby 3.0+ 7 | * Remove `nkf` gem dependency. 8 | 9 | ## 1.8.1 ## 10 | * Fix duplication of Rails tasks caused by LetterOpener's rake task file. (thanks [zarqman](https://github.com/zarqman)) 11 | 12 | ## 1.8.0 ## 13 | * Allow configuration of the 'file///' part in Launchy.open path (thanks [ 14 | Regis Millet](https://github.com/Kulgar)) 15 | * Enhance 'tmp:clear' task to delete 'tmp/letter_opener' files (thanks [Joaquín Vicente](https://github.com/wacko)) 16 | * Convert mail's subject to UTF-8. (thanks [kuroponzu](https://github.com/kuroponzu)) 17 | * Use proper attachment's filename sanitization so we don't escape Japanese characters. 18 | 19 | ## 1.7.0 ## 20 | * Use default configuration in `Message::rendered_messages` (thanks [Krystan HuffMenne 21 | ](https://github.com/gitKrystan)) 22 | * Do not use `Rails.root` path if LetterOpener is used outside of Rails (thanks [centrevillage](https://github.com/centrevillage)) 23 | * Allow to set only `Mail#cc`/`Mail#bcc` without `Mail#to`. 24 | 25 | ## 1.6.0 ## 26 | * Do not depend on Mail gem to check delivery params. 27 | * Do not parse and escape url before passing it to Launchy. 28 | 29 | ## 1.5.0 ## 30 | * Use proper check for `Rails::Railties` (thanks [Florian Weingarten](https://github.com/fw42)) 31 | * Add a shim for the iFrame "srcdoc" attribute (make it work with IE). 32 | * Do not convert `-` to `_` in attachment file names. (thanks [Steven Harman](https://github.com/stevenharman)) 33 | * Drop Ruby 1.9 support. 34 | * Escape inline attachment names the same way they are stored in the attachments directory (thanks [Daniel Rikowski](https://github.com/daniel-rikowski)) 35 | * Increase timestamp precision in the mail filename. (thanks [Piotr Usewicz](https://github.com/pusewicz)) 36 | * Add ability to configure location of stored mails. (thanks [Ben](https://github.com/beejamin)) 37 | * Add light version of template for mails that doesn't render any additional styling. (thanks [Ben](https://github.com/beejamin)) 38 | 39 | ## 1.4.1 ## 40 | * Stop base tag appearing in plain-text previews. (thanks [Coby Chapple](https://github.com/cobyism)) 41 | 42 | ## 1.4.0 ## 43 | * Add base tag to the iframe so links work with X-Frame-Options set to SAMEORIGIN. (thanks [Jason Tokoph](https://github.com/jtokoph)) 44 | * Check delivery params before rendering an email to match SMTP behaviour. 45 | 46 | ## 1.3.0 ## 47 | 48 | * Fix message body encoding is observed correctly in QP CTE. (thanks [Mark Dodwell](https://github.com/mkdynamic)) 49 | * Remove fixed width on the mail content. (thanks [weexpectedTHIS](https://github.com/weexpectedTHIS)) 50 | * Render email content in the iframe. Fixes [#98](https://github.com/ryanb/letter_opener/issues/98). (thanks [Jacob Maine](https://github.com/mainej)) 51 | 52 | ## 1.2.0 ## 53 | 54 | * Fix auto_link() which in some cases would return an empty tag for plain text messages. (thanks [Kevin McPhillips](https://github.com/kmcphillips)) 55 | * Update styles. (thanks [Adam Doppelt](https://github.com/gurgeous)) 56 | 57 | ## 1.1.2 ## 58 | 59 | * Show formatted display names in html template (thanks [ClaireMcGinty](https://github.com/ClaireMcGinty)) 60 | * Use `file:///` uri scheme to fix Launchy on Windows. 61 | 62 | ## 1.1.1 ## 63 | 64 | * Handle cc and bcc as array of emails. (thanks [jordandcarter](https://github.com/jordandcarter)) 65 | * Use `file://` uri scheme since Launcy can't open escaped URL without it. (thanks [Adrian2112](https://github.com/Adrian2112)) 66 | * Update Launchy dependency to `~> 2.2` (thanks [JeanMertz](https://github.com/JeanMertz)) 67 | * Change all nonword chars in filename of attachment to underscore so 68 | it can be saved on all platforms. (thanks [phallstrom](https://github.com/phallstrom)) 69 | 70 | ## 1.1.0 ## 71 | 72 | * Update Launchy dependency to `~> 2.2.0` (thanks [webdevotion](https://github.com/webdevotion)) 73 | * Handle `sender` field (thanks [sjtipton](https://github.com/sjtipton)) 74 | * Show subject only if it's present (thanks [jadehyper](https://github.com/jadehyper)) 75 | * Show subject as title of web page (thanks [statique](https://github.com/statique)) 76 | 77 | ## 1.0.0 ## 78 | 79 | * Attachment Support (thanks [David Cornu](https://github.com/davidcornu)) 80 | * Escape HTML in subject and other fields 81 | * Raise an exception if the :location option is not present instead of using a default 82 | * Open rich version by default (thanks [Damir](https://github.com/sidonath)) 83 | * Override margin on dt and dd elements in CSS (thanks [Edgars Beigarts](https://github.com/ebeigarts)) 84 | * Autolink URLs in plain version (thanks [Matt Burke](https://github.com/spraints)) 85 | 86 | ## 0.1.0 ## 87 | 88 | * From and To show name and Email when specified 89 | * Fix bug when letter_opener couldn't open email in Windows 90 | * Handle spaces in the application path (thanks [Mike Boone](https://github.com/boone)) 91 | * As letter_opener doesn't work with Launchy < 2.0.4 let's depend on >= 2.0.4 (thanks [Samnang Chhun](https://github.com/samnang)) 92 | * Handle `reply_to` field (thanks [Wes Gibbs](https://github.com/wgibbs)) 93 | * Set the charset in email preview (thanks [Bruno Michel](https://github.com/nono)) 94 | 95 | ## 0.0.2 ## 96 | 97 | * Fixing launchy requirement (thanks [Bruno Michel](https://github.com/nono)) 98 | 99 | ## 0.0.1 ## 100 | 101 | * Initial release 102 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | gemspec 3 | 4 | if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.1") 5 | gem "net-imap", require: false 6 | gem "net-pop", require: false 7 | gem "net-smtp", require: false 8 | end 9 | 10 | gem "rake" 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Ryan Bates 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 | # Letter Opener [![Ruby](https://github.com/ryanb/letter_opener/actions/workflows/ruby.yml/badge.svg)](https://github.com/ryanb/letter_opener/actions/workflows/ruby.yml) 2 | 3 | Preview email in the default browser instead of sending it. This means you do not need to set up email delivery in your development environment, and you no longer need to worry about accidentally sending a test email to someone else's address. 4 | 5 | ## Rails Setup 6 | 7 | First add the gem to your development environment and run the `bundle` command to install it. 8 | 9 | ```rb 10 | gem "letter_opener", group: :development 11 | ``` 12 | 13 | Then set the delivery method in `config/environments/development.rb` 14 | 15 | ```rb 16 | config.action_mailer.delivery_method = :letter_opener 17 | config.action_mailer.perform_deliveries = true 18 | ``` 19 | 20 | Now any email will pop up in your browser instead of being sent. The messages are stored in `tmp/letter_opener`. 21 | If you want to change application that will be used to open your emails you should override `LAUNCHY_APPLICATION` environment variable or set `Launchy.application` in the initializer. 22 | 23 | ### Configuration 24 | 25 | ```rb 26 | LetterOpener.configure do |config| 27 | # To overrider the location for message storage. 28 | # Default value is `tmp/letter_opener` 29 | config.location = Rails.root.join('tmp', 'my_mails') 30 | 31 | # To render only the message body, without any metadata or extra containers or styling. 32 | # Default value is `:default` that renders styled message with showing useful metadata. 33 | config.message_template = :light 34 | 35 | # To change default file URI scheme you can provide `file_uri_scheme` config. 36 | # It might be useful when you use WSL (Windows Subsystem for Linux) and default 37 | # scheme doesn't work for you. 38 | # Default value is blank 39 | config.file_uri_scheme = 'file://///wsl$/Ubuntu-18.04' 40 | end 41 | ``` 42 | 43 | ## Non Rails Setup 44 | 45 | If you aren't using Rails, this can be easily set up with the Mail gem. Just set the delivery method when configuring Mail and specify a location. 46 | 47 | ```rb 48 | require "letter_opener" 49 | Mail.defaults do 50 | delivery_method LetterOpener::DeliveryMethod, location: File.expand_path('../tmp/letter_opener', __FILE__) 51 | end 52 | ``` 53 | 54 | The method is similar if you're using the Pony gem: 55 | 56 | ```rb 57 | require "letter_opener" 58 | Pony.options = { 59 | via: LetterOpener::DeliveryMethod, 60 | via_options: {location: File.expand_path('../tmp/letter_opener', __FILE__)} 61 | } 62 | ``` 63 | 64 | Alternatively, if you are using ActionMailer directly (without Rails) you will need to add the delivery method. 65 | 66 | ```rb 67 | require "letter_opener" 68 | ActionMailer::Base.add_delivery_method :letter_opener, LetterOpener::DeliveryMethod, :location => File.expand_path('../tmp/letter_opener', __FILE__) 69 | ActionMailer::Base.delivery_method = :letter_opener 70 | ``` 71 | 72 | ## Remote Alternatives 73 | 74 | Letter Opener uses [Launchy](https://github.com/copiousfreetime/launchy) to open sent mail in the browser. This assumes the Ruby process is running on the local development machine. If you are using a separate staging server or VM this will not work. In that case consider using [Mailtrap](http://mailtrap.io/) or [MailCatcher](http://mailcatcher.me/). 75 | 76 | If you are running your application within a Docker Container or VM and do not have a browser available to open emails received by Letter Opener, you may see the following error: 77 | 78 | ``` 79 | WARN: Launchy::CommandNotFoundError: Unable to find a browser command. If this is unexpected, Please rerun with environment variable LAUNCHY_DEBUG=true or the '-d' commandline option and file a bug at https://github.com/copiousfreetime/launchy/issues/new 80 | ``` 81 | 82 | To resolve this, simply set the following ENV variables: 83 | 84 | ``` 85 | LAUNCHY_DRY_RUN=true 86 | BROWSER=/dev/null 87 | ``` 88 | 89 | In order to keep this project simple, I don't have plans to turn it into a Rails engine with an interface for browsing the sent mail but there is a [gem you can use for that](https://github.com/fgrehm/letter_opener_web). 90 | 91 | 92 | ## Development & Feedback 93 | 94 | Questions or problems? Please use the [issue tracker](https://github.com/ryanb/letter_opener/issues). If you would like to contribute to this project, fork this repository and run `bundle` and `rake` to run the tests. Pull requests appreciated. 95 | 96 | Special thanks to the [mail_view](https://github.com/37signals/mail_view/) gem for inspiring this project and for their mail template. Also thanks to [Vasiliy Ermolovich](https://github.com/nashby) for helping manage this project. 97 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler::GemHelper.install_tasks 3 | require 'rspec/core/rake_task' 4 | 5 | desc "Run RSpec" 6 | RSpec::Core::RakeTask.new do |t| 7 | t.verbose = false 8 | end 9 | task :default => :spec 10 | -------------------------------------------------------------------------------- /letter_opener.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = "letter_opener" 3 | s.version = "1.10.0" 4 | s.author = "Ryan Bates" 5 | s.email = "ryan@railscasts.com" 6 | s.homepage = "https://github.com/ryanb/letter_opener" 7 | s.summary = "Preview mail in browser instead of sending." 8 | s.description = "When mail is sent from your application, Letter Opener will open a preview in the browser instead of sending." 9 | s.license = "MIT" 10 | 11 | s.files = Dir["{lib}/**/*", "[A-Z]*"] - ["Gemfile.lock"] 12 | s.require_path = "lib" 13 | 14 | s.add_dependency 'launchy', '>= 2.2', '< 4' 15 | 16 | s.add_development_dependency 'rspec', '~> 3.10.0' 17 | s.add_development_dependency 'mail', '~> 2.6.0' 18 | 19 | s.required_rubygems_version = ">= 1.3.4" 20 | 21 | if s.respond_to?(:metadata) 22 | s.metadata = { 23 | 'bug_tracker_uri' => 'https://github.com/ryanb/letter_opener/issues', 24 | 'changelog_uri' => 'https://github.com/ryanb/letter_opener/blob/master/CHANGELOG.md', 25 | 'documentation_uri' => 'http://www.rubydoc.info/gems/letter_opener/', 26 | 'homepage_uri' => 'https://github.com/ryanb/letter_opener', 27 | 'source_code_uri' => 'https://github.com/ryanb/letter_opener/', 28 | } 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/letter_opener.rb: -------------------------------------------------------------------------------- 1 | module LetterOpener 2 | autoload :Message, "letter_opener/message" 3 | autoload :DeliveryMethod, "letter_opener/delivery_method" 4 | autoload :Configuration, "letter_opener/configuration" 5 | 6 | def self.configuration 7 | @configuration ||= Configuration.new 8 | end 9 | 10 | def self.configure 11 | yield(configuration) 12 | end 13 | end 14 | 15 | require "letter_opener/railtie" if defined?(Rails::Railtie) 16 | -------------------------------------------------------------------------------- /lib/letter_opener/configuration.rb: -------------------------------------------------------------------------------- 1 | module LetterOpener 2 | class Configuration 3 | attr_accessor :location, :message_template, :file_uri_scheme 4 | 5 | def initialize 6 | @location = Rails.root.join('tmp', 'letter_opener') if defined?(Rails) && Rails.respond_to?(:root) && Rails.root 7 | @message_template = 'default' 8 | @file_uri_scheme = nil 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/letter_opener/delivery_method.rb: -------------------------------------------------------------------------------- 1 | require "digest/sha1" 2 | require "launchy" 3 | 4 | module LetterOpener 5 | class DeliveryMethod 6 | class InvalidOption < StandardError; end 7 | 8 | attr_accessor :settings 9 | 10 | def initialize(options = {}) 11 | options[:message_template] ||= LetterOpener.configuration.message_template 12 | options[:location] ||= LetterOpener.configuration.location 13 | options[:file_uri_scheme] ||= LetterOpener.configuration.file_uri_scheme 14 | 15 | raise InvalidOption, "A location option is required when using the Letter Opener delivery method" if options[:location].nil? 16 | 17 | self.settings = options 18 | end 19 | 20 | def deliver!(mail) 21 | validate_mail!(mail) 22 | location = File.join(settings[:location], "#{Time.now.to_f.to_s.tr('.', '_')}_#{Digest::SHA1.hexdigest(mail.encoded)[0..6]}") 23 | 24 | messages = Message.rendered_messages(mail, location: location, message_template: settings[:message_template]) 25 | ::Launchy.open("#{settings[:file_uri_scheme]}#{messages.first.filepath}") 26 | end 27 | 28 | private 29 | 30 | def validate_mail!(mail) 31 | if !mail.smtp_envelope_from || mail.smtp_envelope_from.empty? 32 | raise ArgumentError, "SMTP From address may not be blank" 33 | end 34 | 35 | if !mail.smtp_envelope_to || mail.smtp_envelope_to.empty? 36 | raise ArgumentError, "SMTP To address may not be blank" 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/letter_opener/message.rb: -------------------------------------------------------------------------------- 1 | require "cgi" 2 | require "erb" 3 | require "fileutils" 4 | require "uri" 5 | 6 | module LetterOpener 7 | class Message 8 | attr_reader :mail 9 | 10 | def self.rendered_messages(mail, options = {}) 11 | messages = [] 12 | messages << new(mail, options.merge(part: mail.html_part)) if mail.html_part 13 | messages << new(mail, options.merge(part: mail.text_part)) if mail.text_part 14 | messages << new(mail, options) if messages.empty? 15 | messages.each(&:render) 16 | messages.sort 17 | end 18 | 19 | ERROR_MSG = '%s or default configuration must be given'.freeze 20 | 21 | def initialize(mail, options = {}) 22 | @mail = mail 23 | @location = options[:location] || LetterOpener.configuration.location 24 | @part = options[:part] 25 | @template = options[:message_template] || LetterOpener.configuration.message_template 26 | @attachments = [] 27 | 28 | raise ArgumentError, ERROR_MSG % 'options[:location]' unless @location 29 | raise ArgumentError, ERROR_MSG % 'options[:message_template]' unless @template 30 | end 31 | 32 | def render 33 | FileUtils.mkdir_p(@location) 34 | 35 | if mail.attachments.any? 36 | attachments_dir = File.join(@location, 'attachments') 37 | FileUtils.mkdir_p(attachments_dir) 38 | mail.attachments.each do |attachment| 39 | filename = attachment_filename(attachment) 40 | path = File.join(attachments_dir, filename) 41 | 42 | unless File.exist?(path) # true if other parts have already been rendered 43 | File.open(path, 'wb') { |f| f.write(attachment.body.raw_source) } 44 | end 45 | 46 | @attachments << [attachment.filename, "attachments/#{filename}"] 47 | end 48 | end 49 | 50 | File.open(filepath, 'w') do |f| 51 | f.write ERB.new(template).result(binding) 52 | end 53 | 54 | mail["location_#{type}"] = filepath 55 | end 56 | 57 | def template 58 | File.read(File.expand_path("../templates/#{@template}.html.erb", __FILE__)) 59 | end 60 | 61 | def filepath 62 | File.join(@location, "#{type}.html") 63 | end 64 | 65 | def content_type 66 | @part && @part.content_type || @mail.content_type 67 | end 68 | 69 | def body 70 | @body ||= begin 71 | body = (@part || @mail).decoded 72 | 73 | mail.attachments.each do |attachment| 74 | body.gsub!(attachment.url, "attachments/#{attachment_filename(attachment)}") 75 | end 76 | 77 | body 78 | end 79 | end 80 | 81 | def from 82 | @from ||= Array(@mail['from']).join(", ") 83 | end 84 | 85 | def sender 86 | @sender ||= Array(@mail['sender']).join(", ") 87 | end 88 | 89 | def subject 90 | @subject ||= @mail.subject 91 | end 92 | 93 | def to 94 | @to ||= Array(@mail['to']).join(", ") 95 | end 96 | 97 | def cc 98 | @cc ||= Array(@mail['cc']).join(", ") 99 | end 100 | 101 | def bcc 102 | @bcc ||= Array(@mail['bcc']).join(", ") 103 | end 104 | 105 | def reply_to 106 | @reply_to ||= Array(@mail['reply-to']).join(", ") 107 | end 108 | 109 | def type 110 | content_type =~ /html/ ? "rich" : "plain" 111 | end 112 | 113 | def encoding 114 | body.respond_to?(:encoding) ? body.encoding : "utf-8" 115 | end 116 | 117 | def auto_link(text) 118 | text.gsub(URI::Parser.new.make_regexp(%W[https http])) do |link| 119 | "#{ link }" 120 | end 121 | end 122 | 123 | def h(content) 124 | CGI.escapeHTML(content) 125 | end 126 | 127 | def attachment_filename(attachment) 128 | # Copied from https://github.com/rails/rails/blob/6bfc637659248df5d6719a86d2981b52662d9b50/activestorage/app/models/active_storage/filename.rb#L57 129 | attachment.filename.encode( 130 | Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "�").strip.tr("\u{202E}%$|:;/\t\r\n\\", "-" 131 | ) 132 | end 133 | 134 | def <=>(other) 135 | order = %w[rich plain] 136 | order.index(type) <=> order.index(other.type) 137 | end 138 | end 139 | end 140 | -------------------------------------------------------------------------------- /lib/letter_opener/railtie.rb: -------------------------------------------------------------------------------- 1 | module LetterOpener 2 | class Railtie < Rails::Railtie 3 | initializer "letter_opener.add_delivery_method" do 4 | ActiveSupport.on_load :action_mailer do 5 | ActionMailer::Base.add_delivery_method( 6 | :letter_opener, 7 | LetterOpener::DeliveryMethod 8 | ) 9 | end 10 | end 11 | 12 | rake_tasks do 13 | load 'letter_opener/tasks/letter_opener.rake' 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/letter_opener/tasks/letter_opener.rake: -------------------------------------------------------------------------------- 1 | namespace :tmp do 2 | task :letter_opener do 3 | rm_rf Dir["tmp/letter_opener/[^.]*"], verbose: false 4 | end 5 | 6 | task clear: :letter_opener 7 | end 8 | -------------------------------------------------------------------------------- /lib/letter_opener/templates/default.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <% if mail.subject %> 5 | <%= h mail.subject %> 6 | <% end %> 7 | 8 | 62 | 63 | 64 |
65 |
66 |
67 |
From:
68 |
<%= h from %>
69 | 70 | <% unless sender.empty? %> 71 |
Sender:
72 |
<%= h sender %>
73 | <% end %> 74 | 75 | <% unless reply_to.empty? %> 76 |
Reply-To:
77 |
<%= h reply_to %>
78 | <% end %> 79 | 80 | <% if mail.subject %> 81 |
Subject:
82 |
<%= h mail.subject %>
83 | <% end %> 84 | 85 |
Date:
86 |
<%= Time.now.strftime("%b %e, %Y %I:%M:%S %p %Z") %>
87 | 88 | <% unless to.empty? %> 89 |
To:
90 |
<%= h to %>
91 | <% end %> 92 | 93 | <% unless cc.empty? %> 94 |
CC:
95 |
<%= h cc %>
96 | <% end %> 97 | 98 | <% unless bcc.empty? %> 99 |
BCC:
100 |
<%= h bcc %>
101 | <% end %> 102 | 103 | <% if @attachments.any? %> 104 |
Attachments:
105 |
106 | <% @attachments.each do |filename, path| %> 107 | <%= filename %>  108 | <% end %> 109 |
110 | <% end %> 111 |
112 | 113 | <% if mail.multipart? %> 114 |

115 | <% if type == "plain" && mail.html_part %> 116 | View HTML version 117 | <% elsif type == "rich" && mail.text_part %> 118 | View plain text version 119 | <% end %> 120 |

121 | <% end %> 122 |
123 | 124 | <% if type == "plain" %> 125 |
<%= auto_link(h(body)) %>
126 | <% else %> 127 | 128 | <% end %> 129 |
130 | 131 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /lib/letter_opener/templates/light.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <% if mail.subject %> 5 | <%= h mail.subject %> 6 | <% end %> 7 | 8 | 9 | <%= body %> 10 | 11 | 12 | -------------------------------------------------------------------------------- /spec/letter_opener/delivery_method_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe LetterOpener::DeliveryMethod do 4 | let(:location) { File.expand_path('../../../tmp/letter_opener', __FILE__) } 5 | let(:file_uri_scheme) { nil } 6 | 7 | let(:plain_file) { Dir["#{location}/*/plain.html"].first } 8 | let(:plain) { CGI.unescape_html(File.read(plain_file)) } 9 | 10 | before do 11 | allow(Launchy).to receive(:open) 12 | FileUtils.rm_rf(location) 13 | context = self 14 | 15 | Mail.defaults do 16 | delivery_method LetterOpener::DeliveryMethod, location: context.location, file_uri_scheme: context.file_uri_scheme 17 | end 18 | end 19 | 20 | it 'raises an exception if no location passed' do 21 | expect { LetterOpener::DeliveryMethod.new }.to raise_exception(LetterOpener::DeliveryMethod::InvalidOption) 22 | expect { LetterOpener::DeliveryMethod.new(location: "foo") }.to_not raise_exception 23 | end 24 | 25 | context 'integration' do 26 | before do 27 | expect(Launchy).to receive(:open).and_call_original 28 | ENV['LAUNCHY_DRY_RUN'] = 'true' 29 | end 30 | 31 | context 'normal location path' do 32 | it 'opens email' do 33 | expect($stdout).to receive(:puts) 34 | expect { 35 | Mail.deliver do 36 | to 'Bar bar@example.com' 37 | from 'Foo foo@example.com' 38 | body 'World! http://example.com' 39 | end 40 | }.not_to raise_error 41 | end 42 | end 43 | 44 | context 'with spaces in location path' do 45 | let(:location) { File.expand_path('../../../tmp/letter_opener with space', __FILE__) } 46 | 47 | it 'opens email' do 48 | expect($stdout).to receive(:puts) 49 | expect { 50 | Mail.deliver do 51 | to 'Bar bar@example.com' 52 | from 'Foo foo@example.com' 53 | body 'World! http://example.com' 54 | end 55 | }.not_to raise_error 56 | end 57 | end 58 | end 59 | 60 | context 'content' do 61 | context 'plain' do 62 | before do 63 | expect(Launchy).to receive(:open) 64 | 65 | Mail.deliver do 66 | from 'Foo ' 67 | sender 'Baz ' 68 | reply_to 'No Reply ' 69 | to 'Bar ' 70 | cc 'Qux ' 71 | bcc 'Qux ' 72 | subject 'Hello' 73 | body 'World! http://example.com' 74 | end 75 | end 76 | 77 | it 'creates plain html document' do 78 | expect(File.exist?(plain_file)).to be_truthy 79 | end 80 | 81 | it 'saves From field' do 82 | expect(plain).to include("Foo ") 83 | end 84 | 85 | it 'saves Sender field' do 86 | expect(plain).to include("Baz ") 87 | end 88 | 89 | it 'saves Reply-to field' do 90 | expect(plain).to include("No Reply ") 91 | end 92 | 93 | it 'saves To field' do 94 | expect(plain).to include("Bar ") 95 | end 96 | 97 | it 'saves Subject field' do 98 | expect(plain).to include("Hello") 99 | end 100 | 101 | it 'saves Body with autolink' do 102 | expect(plain).to include('World! http://example.com') 103 | end 104 | end 105 | 106 | context 'multipart' do 107 | let(:rich_file) { Dir["#{location}/*/rich.html"].first } 108 | let(:rich) { CGI.unescape_html(File.read(rich_file)) } 109 | 110 | before do 111 | expect(Launchy).to receive(:open) 112 | 113 | Mail.deliver do 114 | from 'foo@example.com' 115 | to 'bar@example.com' 116 | subject 'Many parts with ' 117 | text_part do 118 | body 'This is text' 119 | end 120 | html_part do 121 | content_type 'text/html; charset=UTF-8' 122 | body '

This is HTML

' 123 | end 124 | end 125 | end 126 | 127 | it 'creates plain html document' do 128 | expect(File.exist?(plain_file)).to be_truthy 129 | end 130 | 131 | it 'creates rich html document' do 132 | expect(File.exist?(rich_file)).to be_truthy 133 | end 134 | 135 | it 'shows link to rich html version' do 136 | expect(plain).to include("View HTML version") 137 | end 138 | 139 | it 'saves text part' do 140 | expect(plain).to include("This is text") 141 | end 142 | 143 | it 'saves html part' do 144 | expect(rich).to include("

This is HTML

") 145 | end 146 | 147 | it 'saves escaped Subject field' do 148 | expect(plain).to include("Many parts with ") 149 | end 150 | 151 | it 'shows subject as title' do 152 | expect(rich).to include("Many parts with <html>") 153 | end 154 | end 155 | end 156 | 157 | context 'document with spaces in name' do 158 | let(:location) { File.expand_path('../../../tmp/letter_opener with space', __FILE__) } 159 | 160 | before do 161 | expect(Launchy).to receive(:open) 162 | 163 | Mail.deliver do 164 | from 'Foo ' 165 | to 'bar@example.com' 166 | subject 'Hello' 167 | body 'World!' 168 | end 169 | end 170 | 171 | it 'creates plain html document' do 172 | File.exist?(plain_file) 173 | end 174 | 175 | it 'saves From filed' do 176 | expect(plain).to include("Foo ") 177 | end 178 | end 179 | 180 | context 'using deliver! method' do 181 | before do 182 | expect(Launchy).to receive(:open) 183 | Mail.new do 184 | from 'foo@example.com' 185 | to 'bar@example.com' 186 | subject 'Hello' 187 | body 'World!' 188 | end.deliver! 189 | end 190 | 191 | it 'creates plain html document' do 192 | expect(File.exist?(plain_file)).to be_truthy 193 | end 194 | 195 | it 'saves From field' do 196 | expect(plain).to include("foo@example.com") 197 | end 198 | 199 | it 'saves To field' do 200 | expect(plain).to include("bar@example.com") 201 | end 202 | 203 | it 'saves Subject field' do 204 | expect(plain).to include("Hello") 205 | end 206 | 207 | it 'saves Body field' do 208 | expect(plain).to include("World!") 209 | end 210 | end 211 | 212 | context 'attachments in plain text mail' do 213 | before do 214 | Mail.deliver do 215 | from 'foo@example.com' 216 | to 'bar@example.com' 217 | subject 'With attachments' 218 | text_part do 219 | body 'This is text' 220 | end 221 | attachments[File.basename(__FILE__)] = File.read(__FILE__) 222 | end 223 | end 224 | 225 | it 'creates attachments dir with attachment' do 226 | attachment = Dir["#{location}/*/attachments/#{File.basename(__FILE__)}"].first 227 | expect(File.exist?(attachment)).to be_truthy 228 | end 229 | 230 | it 'saves attachment name' do 231 | plain = File.read(Dir["#{location}/*/plain.html"].first) 232 | expect(plain).to include(File.basename(__FILE__)) 233 | end 234 | end 235 | 236 | context 'attachments in rich mail' do 237 | let(:url) { mail.attachments[0].url } 238 | 239 | let!(:mail) do 240 | Mail.deliver do 241 | from 'foo@example.com' 242 | to 'bar@example.com' 243 | subject 'With attachments' 244 | attachments[File.basename(__FILE__)] = File.read(__FILE__) 245 | url = attachments[0].url 246 | html_part do 247 | content_type 'text/html; charset=UTF-8' 248 | body "Here's an image: " 249 | end 250 | end 251 | end 252 | 253 | it 'creates attachments dir with attachment' do 254 | attachment = Dir["#{location}/*/attachments/#{File.basename(__FILE__)}"].first 255 | expect(File.exist?(attachment)).to be_truthy 256 | end 257 | 258 | it 'replaces inline attachment urls' do 259 | text = File.read(Dir["#{location}/*/rich.html"].first) 260 | expect(mail.parts[0].body).to include(url) 261 | expect(text).to_not include(url) 262 | expect(text).to include("attachments/#{File.basename(__FILE__)}") 263 | end 264 | end 265 | 266 | context 'attachments with non-word characters in the filename' do 267 | before do 268 | Mail.deliver do 269 | from 'foo@example.com' 270 | to 'bar@example.com' 271 | subject 'With attachments' 272 | text_part do 273 | body 'This is text' 274 | end 275 | attachments['non word:chars/used,01-02.txt'] = File.read(__FILE__) 276 | 277 | url = attachments[0].url 278 | html_part do 279 | content_type 'text/html; charset=UTF-8' 280 | body "This is an image: " 281 | end 282 | end 283 | end 284 | 285 | it 'creates attachments dir with attachment' do 286 | attachment = Dir["#{location}/*/attachments/non word-chars-used,01-02.txt"].first 287 | expect(File.exist?(attachment)).to be_truthy 288 | end 289 | 290 | it 'saves attachment name' do 291 | plain = File.read(Dir["#{location}/*/plain.html"].first) 292 | expect(plain).to include('non word-chars-used,01-02.txt') 293 | end 294 | 295 | it 'replaces inline attachment names' do 296 | text = File.read(Dir["#{location}/*/rich.html"].first) 297 | expect(text).to include('attachments/non word-chars-used,01-02.txt') 298 | end 299 | end 300 | 301 | context 'subjectless mail' do 302 | before do 303 | expect(Launchy).to receive(:open) 304 | 305 | Mail.deliver do 306 | from 'Foo foo@example.com' 307 | reply_to 'No Reply no-reply@example.com' 308 | to 'Bar bar@example.com' 309 | body 'World! http://example.com' 310 | end 311 | end 312 | 313 | it 'creates plain html document' do 314 | expect(File.exist?(plain_file)).to be_truthy 315 | end 316 | end 317 | 318 | context 'delivery params' do 319 | it 'raises an exception if there is no SMTP Envelope To value' do 320 | expect(Launchy).not_to receive(:open) 321 | 322 | expect { 323 | Mail.deliver do 324 | from 'Foo foo@example.com' 325 | reply_to 'No Reply no-reply@example.com' 326 | body 'World! http://example.com' 327 | end 328 | }.to raise_exception(ArgumentError) 329 | end 330 | 331 | it 'does not raise an exception if there is at least one SMTP Envelope To value' do 332 | expect(Launchy).to receive(:open) 333 | 334 | expect { 335 | Mail.deliver do 336 | from 'Foo foo@example.com' 337 | cc 'Bar bar@example.com' 338 | reply_to 'No Reply no-reply@example.com' 339 | body 'World! http://example.com' 340 | end 341 | }.not_to raise_exception 342 | end 343 | end 344 | 345 | context 'light template' do 346 | before do 347 | expect(Launchy).to receive(:open) 348 | 349 | LetterOpener.configure do |config| 350 | config.message_template = :light 351 | end 352 | 353 | Mail.defaults do 354 | delivery_method LetterOpener::DeliveryMethod, :location => File.expand_path('../../../tmp/letter_opener', __FILE__) 355 | end 356 | 357 | Mail.deliver do 358 | subject 'Foo subject' 359 | from 'Foo foo@example.com' 360 | reply_to 'No Reply no-reply@example.com' 361 | to 'Bar bar@example.com' 362 | body 'World! http://example.com' 363 | end 364 | end 365 | 366 | after do 367 | LetterOpener.configure do |config| 368 | config.message_template = :default 369 | end 370 | end 371 | 372 | it 'creates plain html document' do 373 | expect(File.exist?(plain_file)).to be_truthy 374 | end 375 | end 376 | 377 | context 'specifying custom file_uri_scheme configuration option' do 378 | after do 379 | Mail.defaults do 380 | delivery_method LetterOpener::DeliveryMethod, location: File.expand_path('../../../tmp/letter_opener', __FILE__) 381 | end 382 | 383 | Mail.deliver do 384 | subject 'Foo subject' 385 | from 'Foo foo@example.com' 386 | reply_to 'No Reply no-reply@example.com' 387 | to 'Bar bar@example.com' 388 | body 'World! http://example.com' 389 | end 390 | 391 | LetterOpener.configure do |config| 392 | config.file_uri_scheme = file_uri_scheme 393 | end 394 | end 395 | 396 | context 'file_uri_scheme is not set in configuration' do 397 | it "sends the path to Launchy with the 'file://' prefix by default" do 398 | allow(Launchy).to receive(:open) do |path| 399 | expect(path).not_to match(/^file:\/\//) 400 | end 401 | end 402 | end 403 | 404 | context 'file_uri_scheme is set in configuration' do 405 | it "sends the path to Launchy with the 'file://///wsl$/Ubuntu-18.04' prefix" do 406 | allow(Launchy).to receive(:open) do |path| 407 | expect(path).to match(/^file:\/\/\/\/\/wsl\$\/Ubuntu-18.04/) 408 | end 409 | 410 | LetterOpener.configure do |config| 411 | config.file_uri_scheme = 'file://///wsl$/Ubuntu-18.04' 412 | end 413 | end 414 | end 415 | end 416 | end 417 | -------------------------------------------------------------------------------- /spec/letter_opener/message_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'spec_helper' 3 | 4 | describe LetterOpener::Message do 5 | let(:location) { File.expand_path('../../../tmp/letter_opener', __FILE__) } 6 | 7 | def mail(options={}, &blk) 8 | Mail.new(options, &blk) 9 | end 10 | 11 | describe '#reply_to' do 12 | it 'handles one email as a string' do 13 | mail = mail(:reply_to => 'test@example.com') 14 | message = described_class.new(mail, location: location) 15 | expect(message.reply_to).to eq('test@example.com') 16 | end 17 | 18 | it 'handles one email with display names' do 19 | mail = mail(:reply_to => 'test ') 20 | message = described_class.new(mail, location: location) 21 | expect(message.reply_to).to eq('test ') 22 | end 23 | 24 | it 'handles array of emails' do 25 | mail = mail(:reply_to => ['test1@example.com', 'test2@example.com']) 26 | message = described_class.new(mail, location: location) 27 | expect(message.reply_to).to eq('test1@example.com, test2@example.com') 28 | end 29 | 30 | it 'handles array of emails with display names' do 31 | mail = mail(:reply_to => ['test1 ', 'test2 ']) 32 | message = described_class.new(mail, location: location) 33 | expect(message.reply_to).to eq('test1 , test2 ') 34 | end 35 | 36 | end 37 | 38 | describe '#subject' do 39 | it 'handles UTF-8 charset subject' do 40 | mail = mail(:subject => 'test_mail') 41 | message = described_class.new(mail, location: location) 42 | expect(message.subject).to eq('test_mail') 43 | end 44 | 45 | it 'handles encode ISO-2022-JP charset subject' do 46 | mail = mail(:subject => '=?iso-2022-jp?B?GyRCJUYlOSVIJWEhPCVrGyhC?=') 47 | message = described_class.new(mail, location: location) 48 | expect(message.subject).to eq('テストメール') 49 | end 50 | end 51 | 52 | describe '#to' do 53 | it 'handles one email as a string' do 54 | mail = mail(:to => 'test@example.com') 55 | message = described_class.new(mail, location: location) 56 | expect(message.to).to eq('test@example.com') 57 | end 58 | 59 | it 'handles one email with display names' do 60 | mail = mail(:to => 'test ') 61 | message = described_class.new(mail, location: location) 62 | expect(message.to).to eq('test ') 63 | end 64 | 65 | it 'handles array of emails' do 66 | mail = mail(:to => ['test1@example.com', 'test2@example.com']) 67 | message = described_class.new(mail, location: location) 68 | expect(message.to).to eq('test1@example.com, test2@example.com') 69 | end 70 | 71 | it 'handles array of emails with display names' do 72 | mail = mail(:to => ['test1 ', 'test2 ']) 73 | message = described_class.new(mail, location: location) 74 | expect(message.to).to eq('test1 , test2 ') 75 | end 76 | 77 | end 78 | 79 | describe '#cc' do 80 | it 'handles one cc email as a string' do 81 | mail = mail(:cc => 'test@example.com') 82 | message = described_class.new(mail, location: location) 83 | expect(message.cc).to eq('test@example.com') 84 | end 85 | 86 | it 'handles one cc email with display name' do 87 | mail = mail(:cc => ['test ', 'test2 ']) 88 | message = described_class.new(mail, location: location) 89 | expect(message.cc).to eq('test , test2 ') 90 | end 91 | 92 | it 'handles array of cc emails' do 93 | mail = mail(:cc => ['test1@example.com', 'test2@example.com']) 94 | message = described_class.new(mail, location: location) 95 | expect(message.cc).to eq('test1@example.com, test2@example.com') 96 | end 97 | 98 | it 'handles array of cc emails with display names' do 99 | mail = mail(:cc => ['test ', 'test2 ']) 100 | message = described_class.new(mail, location: location) 101 | expect(message.cc).to eq('test , test2 ') 102 | end 103 | 104 | end 105 | 106 | describe '#bcc' do 107 | it 'handles one bcc email as a string' do 108 | mail = mail(:bcc => 'test@example.com') 109 | message = described_class.new(mail, location: location) 110 | expect(message.bcc).to eq('test@example.com') 111 | end 112 | 113 | it 'handles one bcc email with display name' do 114 | mail = mail(:bcc => ['test ', 'test2 ']) 115 | message = described_class.new(mail, location: location) 116 | expect(message.bcc).to eq('test , test2 ') 117 | end 118 | 119 | it 'handles array of bcc emails' do 120 | mail = mail(:bcc => ['test1@example.com', 'test2@example.com']) 121 | message = described_class.new(mail, location: location) 122 | expect(message.bcc).to eq('test1@example.com, test2@example.com') 123 | end 124 | 125 | it 'handles array of bcc emails with display names' do 126 | mail = mail(:bcc => ['test ', 'test2 ']) 127 | message = described_class.new(mail, location: location) 128 | expect(message.bcc).to eq('test , test2 ') 129 | end 130 | 131 | end 132 | 133 | describe '#sender' do 134 | it 'handles one email as a string' do 135 | mail = mail(:sender => 'sender@example.com') 136 | message = described_class.new(mail, location: location) 137 | expect(message.sender).to eq('sender@example.com') 138 | end 139 | 140 | it 'handles one email as a string with display name' do 141 | mail = mail(:sender => 'test ') 142 | message = described_class.new(mail, location: location) 143 | expect(message.sender).to eq('test ') 144 | end 145 | 146 | it 'handles array of emails' do 147 | mail = mail(:sender => ['sender1@example.com', 'sender2@example.com']) 148 | message = described_class.new(mail, location: location) 149 | expect(message.sender).to eq('sender1@example.com, sender2@example.com') 150 | end 151 | 152 | it 'handles array of emails with display names' do 153 | mail = mail(:sender => ['test ', 'test2 ']) 154 | message = described_class.new(mail, location: location) 155 | expect(message.sender).to eq('test , test2 ') 156 | end 157 | 158 | end 159 | 160 | describe '#<=>' do 161 | it 'sorts rich type before plain type' do 162 | plain = described_class.new(double(content_type: 'text/plain'), location: location) 163 | rich = described_class.new(double(content_type: 'text/html'), location: location) 164 | expect([plain, rich].sort).to eq([rich, plain]) 165 | end 166 | end 167 | 168 | describe '#auto_link' do 169 | let(:message){ described_class.new(mail, location: location) } 170 | 171 | it 'does not modify unlinkable text' do 172 | text = 'the quick brown fox jumped over the lazy dog' 173 | expect(message.auto_link(text)).to eq(text) 174 | end 175 | 176 | it 'adds links for http' do 177 | raw = "Link to http://localhost:3000/example/path path" 178 | linked = "Link to http://localhost:3000/example/path path" 179 | expect(message.auto_link(raw)).to eq(linked) 180 | end 181 | end 182 | 183 | describe '#body' do 184 | it 'handles UTF-8 charset body correctly, with QP CTE, for a non-multipart message' do 185 | mail = mail(:sender => 'sender@example.com') do 186 | content_type "text/html; charset=UTF-8" 187 | content_transfer_encoding 'quoted-printable' 188 | body "☃" 189 | end 190 | message = message = described_class.new(mail, location: location) 191 | expect(message.body.encoding.name).to eq('UTF-8') 192 | end 193 | 194 | it 'handles UTF-8 charset HTML part body correctly, with QP CTE, for a multipart message' do 195 | mail = mail(:sender => 'sender@example.com') do 196 | html_part do 197 | content_type "text/html; charset=UTF-8" 198 | content_transfer_encoding 'quoted-printable' 199 | body "☃" 200 | end 201 | end 202 | message = described_class.new(mail, location: location, part: mail.html_part) 203 | expect(message.body.encoding.name).to eq('UTF-8') 204 | end 205 | 206 | it 'handles UTF-8 charset text part body correctly, with QP CTE, for a multipart message' do 207 | mail = mail(:sender => 'sender@example.com') do 208 | text_part do 209 | content_type "text/plain; charset=UTF-8" 210 | content_transfer_encoding 'quoted-printable' 211 | body "☃" 212 | end 213 | end 214 | message = described_class.new(mail, location: location, part: mail.text_part) 215 | expect(message.body.encoding.name).to eq('UTF-8') 216 | end 217 | end 218 | 219 | describe '#render' do 220 | it 'records the saved email path for plain content type' do 221 | mail = mail(:subject => 'test_mail') 222 | message = described_class.new(mail, location: location) 223 | message.render 224 | expect(mail['location_plain'].value).to end_with('tmp/letter_opener/plain.html') 225 | end 226 | 227 | 228 | it 'records the saved email path for rich content type' do 229 | mail = mail(:content_type => 'text/html', :subject => 'test_mail') 230 | message = described_class.new(mail, location: location) 231 | message.render 232 | expect(mail['location_rich'].value).to end_with('tmp/letter_opener/rich.html') 233 | end 234 | end 235 | 236 | describe '.rendered_messages' do 237 | it 'uses configured default template if options not given' do 238 | allow(LetterOpener.configuration).to receive(:location) { location } 239 | messages = described_class.rendered_messages(mail) 240 | expect(messages.first.template).not_to be_nil 241 | end 242 | 243 | it 'fails if necessary defaults are not provided' do 244 | allow(LetterOpener.configuration).to receive(:location) { nil } 245 | expect { described_class.rendered_messages(mail) }.to raise_error(ArgumentError) 246 | end 247 | 248 | it 'fails if necessary defaults are not provided' do 249 | allow(LetterOpener.configuration).to receive(:message_template) { nil } 250 | expect { described_class.rendered_messages(mail) }.to raise_error(ArgumentError) 251 | end 252 | end 253 | end 254 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | Bundler.require(:default) 4 | 5 | require "mail" 6 | 7 | RSpec.configure do |config| 8 | config.run_all_when_everything_filtered = true 9 | end 10 | --------------------------------------------------------------------------------