├── .github └── workflows │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Gemfile ├── Gemfile.lock ├── MIT-LICENSE ├── README.md ├── Rakefile ├── gemfiles ├── Gemfile-rails-5-2 ├── Gemfile-rails-6-0 ├── Gemfile-rails-6-1 ├── Gemfile-rails-7-0 └── Gemfile-rails-main ├── lib ├── generators │ └── rails │ │ ├── mail_form_generator.rb │ │ └── templates │ │ └── model.rb ├── mail_form.rb └── mail_form │ ├── base.rb │ ├── delivery.rb │ ├── notifier.rb │ ├── shim.rb │ ├── version.rb │ └── views │ └── mail_form │ └── contact.erb ├── mail_form.gemspec └── test ├── active_record_test.rb ├── mail_form_test.rb ├── resource_test.rb ├── test_file.txt ├── test_helper.rb └── views └── mail_form └── custom_template.erb /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | strategy: 6 | fail-fast: false 7 | matrix: 8 | gemfile: 9 | - Gemfile 10 | - gemfiles/Gemfile-rails-main 11 | - gemfiles/Gemfile-rails-7-0 12 | - gemfiles/Gemfile-rails-6-1 13 | - gemfiles/Gemfile-rails-6-0 14 | - gemfiles/Gemfile-rails-5-2 15 | ruby: 16 | - '3.3' 17 | - '3.2' 18 | - '3.1' 19 | - '3.0' 20 | - '2.7' 21 | - '2.6' 22 | - '2.5' 23 | exclude: 24 | - gemfile: Gemfile 25 | ruby: '2.6' 26 | - gemfile: Gemfile 27 | ruby: '2.5' 28 | - gemfile: gemfiles/Gemfile-rails-main 29 | ruby: '3.0' 30 | - gemfile: gemfiles/Gemfile-rails-main 31 | ruby: '2.7' 32 | - gemfile: gemfiles/Gemfile-rails-main 33 | ruby: '2.6' 34 | - gemfile: gemfiles/Gemfile-rails-main 35 | ruby: '2.5' 36 | - gemfile: gemfiles/Gemfile-rails-7-0 37 | ruby: '2.6' 38 | - gemfile: gemfiles/Gemfile-rails-7-0 39 | ruby: '2.5' 40 | - gemfile: gemfiles/Gemfile-rails-6-0 41 | ruby: '3.3' 42 | - gemfile: gemfiles/Gemfile-rails-6-0 43 | ruby: '3.2' 44 | - gemfile: gemfiles/Gemfile-rails-6-0 45 | ruby: '3.1' 46 | - gemfile: gemfiles/Gemfile-rails-5-2 47 | ruby: '3.3' 48 | - gemfile: gemfiles/Gemfile-rails-5-2 49 | ruby: '3.2' 50 | - gemfile: gemfiles/Gemfile-rails-5-2 51 | ruby: '3.1' 52 | - gemfile: gemfiles/Gemfile-rails-5-2 53 | ruby: '3.0' 54 | - gemfile: gemfiles/Gemfile-rails-5-2 55 | ruby: '2.7' 56 | runs-on: ubuntu-latest 57 | env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps 58 | BUNDLE_GEMFILE: ${{ matrix.gemfile }} 59 | steps: 60 | - uses: actions/checkout@v3 61 | - uses: ruby/setup-ruby@v1 62 | with: 63 | ruby-version: ${{ matrix.ruby }} 64 | bundler-cache: true # runs bundle install and caches installed gems automatically 65 | - run: bundle exec rake 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle/ 2 | pkg/ 3 | rdoc/ 4 | gemfiles/*.lock 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Unreleased 2 | 3 | * Add support for Ruby 3.3. (no changes required) 4 | 5 | # 1.10.1 6 | 7 | * Add support for Rails 7.1. (no changes required.) 8 | 9 | # 1.10.0 10 | 11 | * Add support for Rails 7.0 and Ruby 3.1/3.2 (no changes required) 12 | * Add support for multiple files through a single attachment. [#76, #78] 13 | * Remove test files from the gem package. 14 | 15 | # 1.9.0 16 | 17 | * Add support for Ruby 3.0, drop support for Ruby < 2.5. 18 | * Add support for Rails 6.1, drop support for Rails < 5.2. 19 | * Move CI to GitHub Actions. 20 | 21 | # 1.8.1 22 | 23 | * Fix Active Record integration when including `Mail::Delivery`. 24 | 25 | # 1.8.0 26 | 27 | * Added support for Rails 6.0. 28 | * Drop support for Rails < 5.0 and Ruby < 2.4. 29 | 30 | # 1.7.1 31 | 32 | * Added support for Rails 5.2. 33 | 34 | # 1.7.0 35 | 36 | * Added support for Rails 5.1. 37 | 38 | # 1.6.0 39 | 40 | * Support Rails 4.1 and 4.2. 41 | 42 | # Version 1.5.0 43 | 44 | * Support Rails 4. 45 | * Drop support to Rails < 3.2 and Ruby 1.8. 46 | 47 | # Version 1.4 48 | 49 | * Fixed bug that was causing all Active Record attributes be saved as nil 50 | * Avoid symbol injection on forms 51 | 52 | # Version 1.3 53 | 54 | * Removed deprecated methods in version 1.2 55 | * Added persisted? header and a generator 56 | 57 | # Version 1.2 58 | 59 | * No more class attributes, just define a headers method 60 | 61 | # Version 1.1 62 | 63 | * Rails 3 compatibility 64 | 65 | # Version 1.0 66 | 67 | * Rename to mail_form and launch Rails 2.3 branch 68 | 69 | # Version 0.4 70 | 71 | * Added support to template 72 | 73 | # Version 0.3 74 | 75 | * Added support to symbols on :sender, :subject and :recipients 76 | * Added support to symbols on :validate 77 | 78 | # Version 0.2 79 | 80 | * Added support to request objects and append DSL 81 | * Added support to :attachments (thanks to @andrewtimberlake) 82 | 83 | # Version 0.1 84 | 85 | * First release 86 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | 1. If you have any questions about Mail Form, search the 4 | [Wiki](https://github.com/plataformatec/mail_form/wiki) or 5 | or Stack Overflow. 6 | Do not post questions here. 7 | 8 | 2. If you find a security bug, **DO NOT** submit an issue here. 9 | Please send an e-mail to [opensource@plataformatec.com.br](mailto:opensource@plataformatec.com.br) 10 | instead. 11 | 12 | 3. Do a small search on the issues tracker before submitting your issue to 13 | see if it was already reported or fixed. In case it was not, create your report 14 | including Rails and Mail Form versions. If you are getting exceptions, please 15 | include the full backtrace. 16 | 17 | That's it! The more information you give, the more easy it becomes for us to 18 | track it down and fix it. Ideal scenario would be adding the issue to Mail Form 19 | test suite or to a sample application. 20 | 21 | Thanks! 22 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | gem 'rake' 6 | gem 'rdoc' 7 | 8 | gem 'actionmailer', '~> 7.1.0' 9 | gem 'activemodel', '~> 7.1.0' 10 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | mail_form (1.10.1) 5 | actionmailer (>= 5.2) 6 | activemodel (>= 5.2) 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | actionmailer (7.1.3.2) 12 | actionpack (= 7.1.3.2) 13 | actionview (= 7.1.3.2) 14 | activejob (= 7.1.3.2) 15 | activesupport (= 7.1.3.2) 16 | mail (~> 2.5, >= 2.5.4) 17 | net-imap 18 | net-pop 19 | net-smtp 20 | rails-dom-testing (~> 2.2) 21 | actionpack (7.1.3.2) 22 | actionview (= 7.1.3.2) 23 | activesupport (= 7.1.3.2) 24 | nokogiri (>= 1.8.5) 25 | racc 26 | rack (>= 2.2.4) 27 | rack-session (>= 1.0.1) 28 | rack-test (>= 0.6.3) 29 | rails-dom-testing (~> 2.2) 30 | rails-html-sanitizer (~> 1.6) 31 | actionview (7.1.3.2) 32 | activesupport (= 7.1.3.2) 33 | builder (~> 3.1) 34 | erubi (~> 1.11) 35 | rails-dom-testing (~> 2.2) 36 | rails-html-sanitizer (~> 1.6) 37 | activejob (7.1.3.2) 38 | activesupport (= 7.1.3.2) 39 | globalid (>= 0.3.6) 40 | activemodel (7.1.3.2) 41 | activesupport (= 7.1.3.2) 42 | activesupport (7.1.3.2) 43 | base64 44 | bigdecimal 45 | concurrent-ruby (~> 1.0, >= 1.0.2) 46 | connection_pool (>= 2.2.5) 47 | drb 48 | i18n (>= 1.6, < 2) 49 | minitest (>= 5.1) 50 | mutex_m 51 | tzinfo (~> 2.0) 52 | base64 (0.2.0) 53 | bigdecimal (3.1.7) 54 | builder (3.2.4) 55 | concurrent-ruby (1.2.3) 56 | connection_pool (2.4.1) 57 | crass (1.0.6) 58 | date (3.3.4) 59 | drb (2.2.1) 60 | erubi (1.12.0) 61 | globalid (1.2.1) 62 | activesupport (>= 6.1) 63 | i18n (1.14.4) 64 | concurrent-ruby (~> 1.0) 65 | loofah (2.22.0) 66 | crass (~> 1.0.2) 67 | nokogiri (>= 1.12.0) 68 | mail (2.8.1) 69 | mini_mime (>= 0.1.1) 70 | net-imap 71 | net-pop 72 | net-smtp 73 | mini_mime (1.1.5) 74 | mini_portile2 (2.8.5) 75 | minitest (5.22.3) 76 | mutex_m (0.2.0) 77 | net-imap (0.4.10) 78 | date 79 | net-protocol 80 | net-pop (0.1.2) 81 | net-protocol 82 | net-protocol (0.2.2) 83 | timeout 84 | net-smtp (0.5.0) 85 | net-protocol 86 | nokogiri (1.15.6) 87 | mini_portile2 (~> 2.8.2) 88 | racc (~> 1.4) 89 | psych (5.1.2) 90 | stringio 91 | racc (1.7.3) 92 | rack (3.0.10) 93 | rack-session (2.0.0) 94 | rack (>= 3.0.0) 95 | rack-test (2.1.0) 96 | rack (>= 1.3) 97 | rails-dom-testing (2.2.0) 98 | activesupport (>= 5.0.0) 99 | minitest 100 | nokogiri (>= 1.6) 101 | rails-html-sanitizer (1.6.0) 102 | loofah (~> 2.21) 103 | nokogiri (~> 1.14) 104 | rake (13.2.1) 105 | rdoc (6.6.3.1) 106 | psych (>= 4.0.0) 107 | stringio (3.1.0) 108 | timeout (0.4.1) 109 | tzinfo (2.0.6) 110 | concurrent-ruby (~> 1.0) 111 | 112 | PLATFORMS 113 | ruby 114 | 115 | DEPENDENCIES 116 | actionmailer (~> 7.1.0) 117 | activemodel (~> 7.1.0) 118 | mail_form! 119 | rake 120 | rdoc 121 | 122 | BUNDLED WITH 123 | 2.4.22 124 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020-2024 Rafael França, Carlos Antônio da Silva 2 | Copyright (c) 2009-2019 Plataformatec 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## MailForm 2 | 3 | [![Gem Version](https://fury-badge.herokuapp.com/rb/mail_form.svg)](http://badge.fury.io/rb/mail_form) 4 | 5 | ### Rails 5 6 | 7 | This gem was built on top of `ActiveModel` to showcase how you can pull in validations, naming 8 | and `i18n` from Rails to your models without the need to implement it all by yourself. 9 | 10 | This README refers to the **MailForm** gem to be used in Rails 5+. For instructions 11 | on how to use MailForm in older versions of Rails, please refer to the available branches. 12 | 13 | ### Description 14 | 15 | **MailForm** allows you to send an e-mail straight from a form. For instance, 16 | if you want to make a contact form just the following lines are needed (including the e-mail): 17 | 18 | ```ruby 19 | class ContactForm < MailForm::Base 20 | attribute :name, validate: true 21 | attribute :email, validate: /\A[^@\s]+@[^@\s]+\z/i 22 | attribute :file, attachment: true 23 | 24 | attribute :message 25 | attribute :nickname, captcha: true 26 | 27 | # Declare the e-mail headers. It accepts anything the mail method 28 | # in ActionMailer accepts. 29 | def headers 30 | { 31 | subject: "My Contact Form", 32 | to: "your.email@your.domain.com", 33 | from: %("#{name}" <#{email}>) 34 | } 35 | end 36 | end 37 | ``` 38 | 39 | Then you start a console with `rails console` and type: 40 | 41 | ```ruby 42 | >> c = ContactForm.new(name: 'José', email: 'jose@email.com', message: 'Cool!') 43 | >> c.deliver 44 | ``` 45 | 46 | Check your inbox and the e-mail will be there, with the sent fields (assuming that 47 | you configured your mailer delivery method properly). 48 | 49 | ### MailForm::Base 50 | 51 | When you inherit from `MailForm::Base`, it pulls down a set of stuff from `ActiveModel`, 52 | as `ActiveModel::Validation`, `ActiveModel::Translation` and `ActiveModel::Naming`. 53 | 54 | This brings `I18n`, error messages, validations and attributes handling like in 55 | `ActiveRecord` to **MailForm**, so **MailForm** can be used in your controllers and form builders without extra tweaks. This also means that instead of the following: 56 | 57 | ```ruby 58 | attribute :email, validate: /\A[^@\s]+@[^@\s]+\z/i 59 | ``` 60 | 61 | You could actually do this: 62 | 63 | ```ruby 64 | attribute :email 65 | validates_format_of :email, with: /\A[^@\s]+@[^@\s]+\z/i 66 | ``` 67 | 68 | Choose the one which pleases you the most. For more information on the API, please 69 | continue reading below. 70 | 71 | ### Playing together ORMs 72 | 73 | **MailForm** plays nice with ORMs as well. You just need to include `MailForm::Delivery` 74 | in your model and declare which attributes should be sent: 75 | 76 | ```ruby 77 | class User < ActiveRecord::Base 78 | include MailForm::Delivery 79 | 80 | append :remote_ip, :user_agent, :session 81 | attributes :name, :email, :created_at 82 | 83 | def headers 84 | { 85 | to: "your.email@your.domain.com", 86 | subject: "User created an account" 87 | } 88 | end 89 | end 90 | ``` 91 | 92 | The delivery will be triggered in an `after_create` hook. 93 | 94 | ## Installation 95 | 96 | Install **MailForm** is very easy. Just edit your Gemfile adding the following: 97 | 98 | ```ruby 99 | gem 'mail_form' 100 | ``` 101 | 102 | Then run `bundle install` to install **MailForm**. 103 | 104 | You can run `rails generate mail_form` to view help information on how to generate 105 | a basic form to get you started. 106 | 107 | ## API Overview 108 | 109 | ### attributes(*attributes) 110 | 111 | Declare your form attributes. All attributes declared here will be appended 112 | to the e-mail, except the ones :captcha is true. 113 | 114 | Options: 115 | 116 | * `:validate` - A hook to `validates_*_of`. When `true` is given, validates the 117 | presence of the attribute. When a regexp, validates format. When array, 118 | validates the inclusion of the attribute in the array. 119 | 120 | Whenever `:validate` is given, the presence is automatically checked. Give 121 | `allow_blank: true` to override. 122 | 123 | Finally, when `:validate` is a symbol, the method given as symbol will be 124 | called. Then you can add validations as you do in Active Record (`errors.add`). 125 | 126 | * `:attachment` - When given, expects a file to be sent and attaches 127 | it to the e-mail. Don't forget to set your form to multitype. 128 | It also accepts multiple files through a single attachment attribute, 129 | and will attach them individually to the e-mail. 130 | 131 | * `:captcha` - When true, validates the attributes must be blank. 132 | This is a simple way to avoid spam and the input should be hidden with CSS. 133 | 134 | Examples: 135 | 136 | ```ruby 137 | class ContactForm < MailForm::Base 138 | attributes :name, validate: true 139 | attributes :email, validate: /\A[^@\s]+@[^@\s]+\z/i 140 | attributes :type, validate: ["General", "Interface bug"] 141 | attributes :message 142 | attributes :screenshot, attachment: true, validate: :interface_bug? 143 | attributes :nickname, captcha: true 144 | 145 | def interface_bug? 146 | if type == 'Interface bug' && screenshot.nil? 147 | self.errors.add(:screenshot, "can't be blank on interface bugs") 148 | end 149 | end 150 | end 151 | 152 | c = ContactForm.new(nickname: 'not_blank', email: 'your@email.com', name: 'José') 153 | c.valid? #=> true 154 | c.spam? #=> true (raises an error in development, to remember you to hide it) 155 | c.deliver #=> false (just delivers if is not a spam and is valid, raises an error in development) 156 | 157 | c = ContactForm.new(email: 'invalid') 158 | c.valid? #=> false 159 | c.errors.inspect #=> { name: :blank, email: :invalid } 160 | c.errors.full_messages #=> [ "Name can't be blank", "Email is invalid" ] 161 | 162 | c = ContactForm.new(name: 'José', email: 'your@email.com') 163 | c.deliver 164 | ``` 165 | 166 | ### append(*methods) 167 | 168 | **MailForm** also makes easy to append request information from client to the sent 169 | mail. You just have to do: 170 | 171 | ```ruby 172 | class ContactForm < MailForm::Base 173 | append :remote_ip, :user_agent, :session 174 | # ... 175 | end 176 | ``` 177 | 178 | And in your controller: 179 | 180 | ```ruby 181 | @contact_form = ContactForm.new(params[:contact_form]) 182 | @contact_form.request = request 183 | ``` 184 | 185 | The remote ip, user agent and session will be sent in the e-mail in a 186 | request information session. You can give to append any method that the 187 | request object responds to. 188 | 189 | ## I18n 190 | 191 | I18n in **MailForm** works like in ActiveRecord, so all models, attributes and messages 192 | can be used with localized. Below is an I18n file example file: 193 | 194 | ```ruby 195 | mail_form: 196 | models: 197 | contact_form: "Your site contact form" 198 | attributes: 199 | contact_form: 200 | email: "E-mail" 201 | telephone: "Telephone number" 202 | message: "Sent message" 203 | request: 204 | title: "Technical information about the user" 205 | remote_ip: "IP Address" 206 | user_agent: "Browser" 207 | ``` 208 | 209 | ## Custom e-mail template 210 | 211 | To customize the e-mail template that is used create a file called `contact.erb` in `app/views/mail_form`. 212 | Take a look at `lib/mail_form/views/mail_form/contact.erb` in this repo to see how the default template works. 213 | 214 | ## Maintainers 215 | 216 | * José Valim - http://github.com/josevalim 217 | * Carlos Antonio - http://github.com/carlosantoniodasilva 218 | 219 | ## Contributors 220 | 221 | * Andrew Timberlake - http://github.com/andrewtimberlake 222 | 223 | ## Supported Ruby / Rails versions 224 | 225 | We intend to maintain support for all Ruby / Rails versions that haven't reached end-of-life. 226 | 227 | For more information about specific versions please check [Ruby](https://www.ruby-lang.org/en/downloads/branches/) 228 | and [Rails](https://guides.rubyonrails.org/maintenance_policy.html) maintenance policies, and our test matrix. 229 | 230 | ## Bugs and Feedback 231 | 232 | If you discover any bug, please use github issues tracker. 233 | 234 | ## License 235 | 236 | MIT License. Copyright 2020-2024 Rafael França, Carlos Antônio da Silva. Copyright 2009-2019 Plataformatec. 237 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require 'bundler' 4 | Bundler::GemHelper.install_tasks 5 | 6 | require 'rake/testtask' 7 | require 'rdoc/task' 8 | 9 | desc 'Default: run unit tests.' 10 | task default: :test 11 | 12 | desc 'Test the mail_form plugin.' 13 | Rake::TestTask.new(:test) do |t| 14 | t.libs << 'test' 15 | t.pattern = 'test/**/*_test.rb' 16 | t.verbose = true 17 | t.warning = true 18 | end 19 | 20 | desc 'Generate documentation for the mail_form plugin.' 21 | RDoc::Task.new(:rdoc) do |rdoc| 22 | rdoc.rdoc_dir = 'rdoc' 23 | rdoc.title = 'MailForm' 24 | rdoc.options << '--line-numbers' 25 | rdoc.rdoc_files.include('README.md') 26 | rdoc.rdoc_files.include('lib/**/*.rb') 27 | end 28 | -------------------------------------------------------------------------------- /gemfiles/Gemfile-rails-5-2: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec path: '..' 4 | 5 | gem 'rake' 6 | gem 'rdoc' 7 | 8 | gem 'actionmailer', '~> 5.2.0' 9 | gem 'activemodel', '~> 5.2.0' 10 | -------------------------------------------------------------------------------- /gemfiles/Gemfile-rails-6-0: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec path: '..' 4 | 5 | gem 'rake' 6 | gem 'rdoc' 7 | 8 | gem 'actionmailer', '~> 6.0.0' 9 | gem 'activemodel', '~> 6.0.0' 10 | -------------------------------------------------------------------------------- /gemfiles/Gemfile-rails-6-1: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec path: '..' 4 | 5 | gem 'rake' 6 | gem 'rdoc' 7 | 8 | gem 'actionmailer', '~> 6.1.0' 9 | gem 'activemodel', '~> 6.1.0' 10 | 11 | if RUBY_VERSION >= "3.1" 12 | # https://github.com/rails/rails/commit/180a315c39e750af6fd1f677cd8693771c140f35 13 | gem "net-smtp", require: false 14 | gem "net-imap", require: false 15 | gem "net-pop", require: false 16 | end 17 | -------------------------------------------------------------------------------- /gemfiles/Gemfile-rails-7-0: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec path: ".." 4 | 5 | gem 'rake' 6 | gem 'rdoc' 7 | 8 | gem 'actionmailer', '~> 7.0.0' 9 | gem 'activemodel', '~> 7.0.0' 10 | -------------------------------------------------------------------------------- /gemfiles/Gemfile-rails-main: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec path: '..' 4 | 5 | gem 'rake' 6 | gem 'rdoc' 7 | 8 | gem 'actionmailer', github: 'rails/rails', branch: 'main' 9 | gem 'activemodel', github: 'rails/rails', branch: 'main' 10 | -------------------------------------------------------------------------------- /lib/generators/rails/mail_form_generator.rb: -------------------------------------------------------------------------------- 1 | module Rails 2 | module Generators 3 | class MailFormGenerator < Rails::Generators::NamedBase 4 | def self.source_root 5 | @_mail_form_source_root ||= File.expand_path("../templates", __FILE__) 6 | end 7 | 8 | argument :attributes, type: :array, default: [], banner: "field:type field:type" 9 | 10 | check_class_collision 11 | 12 | def create_model_file 13 | template 'model.rb', File.join('app/models', class_path, "#{file_name}.rb") 14 | end 15 | end 16 | end 17 | end -------------------------------------------------------------------------------- /lib/generators/rails/templates/model.rb: -------------------------------------------------------------------------------- 1 | class <%= class_name %> < MailForm::Base 2 | <% attributes.each do |attribute| -%> 3 | attribute :<%= attribute.name %> 4 | <% end -%> 5 | 6 | def headers 7 | { to: "PLEASE-CHANGE-ME@example.org" } 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/mail_form.rb: -------------------------------------------------------------------------------- 1 | module MailForm 2 | autoload :Base, 'mail_form/base' 3 | autoload :Delivery, 'mail_form/delivery' 4 | autoload :Notifier, 'mail_form/notifier' 5 | autoload :Shim, 'mail_form/shim' 6 | end 7 | -------------------------------------------------------------------------------- /lib/mail_form/base.rb: -------------------------------------------------------------------------------- 1 | module MailForm 2 | class Base 3 | include MailForm::Shim 4 | include MailForm::Delivery 5 | 6 | def self.lookup_ancestors 7 | super - [MailForm::Base] 8 | end 9 | end 10 | end -------------------------------------------------------------------------------- /lib/mail_form/delivery.rb: -------------------------------------------------------------------------------- 1 | module MailForm 2 | module Delivery 3 | extend ActiveSupport::Concern 4 | 5 | included do 6 | class_attribute :mail_attributes 7 | self.mail_attributes = [] 8 | 9 | class_attribute :mail_captcha 10 | self.mail_captcha = [] 11 | 12 | class_attribute :mail_attachments 13 | self.mail_attachments = [] 14 | 15 | class_attribute :mail_appendable 16 | self.mail_appendable = [] 17 | 18 | if respond_to?(:before_deliver) && respond_to?(:after_deliver) 19 | before_deliver :check_not_spam 20 | after_deliver :deliver! 21 | else # For ActiveRecord compatibility 22 | before_create :check_not_spam 23 | after_create :deliver! 24 | alias :deliver :save 25 | end 26 | 27 | attr_accessor :request 28 | end 29 | 30 | module ClassMethods 31 | # Declare your form attributes. All attributes declared here will be appended 32 | # to the e-mail, except the ones captcha is true. 33 | # 34 | # == Options 35 | # 36 | # * :validate - A hook to validates_*_of. When true is given, validates the 37 | # presence of the attribute. When a regexp, validates format. When array, 38 | # validates the inclusion of the attribute in the array. 39 | # 40 | # Whenever :validate is given, the presence is automatically checked. Give 41 | # allow_blank: true to override. 42 | # 43 | # Finally, when :validate is a symbol, the method given as symbol will be 44 | # called. Then you can add validations as you do in ActiveRecord (errors.add). 45 | # 46 | # * :attachment - When given, expects a file to be sent and attaches 47 | # it to the e-mail. Don't forget to set your form to multitype. 48 | # 49 | # * :captcha - When true, validates the attributes must be blank 50 | # This is a simple way to avoid spam 51 | # 52 | # == Examples 53 | # 54 | # class ContactForm < MailForm 55 | # attributes :name, validate: true 56 | # attributes :email, validate: /\A[^@\s]+@[^@\s]+\z/i 57 | # attributes :type, validate: ["General", "Interface bug"] 58 | # attributes :message 59 | # attributes :screenshot, attachment: true, validate: :interface_bug? 60 | # attributes :nickname, captcha: true 61 | # 62 | # def interface_bug? 63 | # if type == 'Interface bug' && screenshot.nil? 64 | # self.errors.add(:screenshot, "can't be blank when you are reporting an interface bug") 65 | # end 66 | # end 67 | # end 68 | # 69 | def attribute(*accessors) 70 | options = accessors.extract_options! 71 | 72 | # TODO: make this not depend on column_names 73 | columns_methods = self.respond_to?(:column_names) ? column_names.map(&:to_sym) : [] 74 | attr_accessor(*(accessors - instance_methods.map(&:to_sym) - columns_methods)) 75 | 76 | if options[:attachment] 77 | self.mail_attachments += accessors 78 | elsif options[:captcha] 79 | self.mail_captcha += accessors 80 | else 81 | self.mail_attributes += accessors 82 | end 83 | 84 | validation = options.delete(:validate) 85 | return unless validation 86 | 87 | accessors.each do |accessor| 88 | case validation 89 | when Symbol, Class 90 | validate validation 91 | break 92 | when Regexp 93 | validates_format_of accessor, with: validation, allow_blank: true 94 | when Array 95 | validates_inclusion_of accessor, in: validation, allow_blank: true 96 | when Range 97 | validates_length_of accessor, within: validation, allow_blank: true 98 | end 99 | 100 | validates_presence_of accessor unless options[:allow_blank] == true 101 | end 102 | end 103 | alias :attributes :attribute 104 | 105 | # Values from request object to be appended to the contact form. 106 | # Whenever used, you have to send the request object when initializing the object: 107 | # 108 | # @contact_form = ContactForm.new(params[:contact_form], request) 109 | # 110 | # You can get the values to be appended from the AbstractRequest 111 | # documentation (http://api.rubyonrails.org/classes/ActionController/AbstractRequest.html) 112 | # 113 | # == Examples 114 | # 115 | # class ContactForm < MailForm 116 | # append :remote_ip, :user_agent, :session, :cookies 117 | # end 118 | # 119 | def append(*values) 120 | self.mail_appendable += values 121 | end 122 | end 123 | 124 | # In development, raises an error if the captcha field is not blank. This is 125 | # is good to remember that the field should be hidden with CSS and shown only 126 | # to robots. 127 | # 128 | # In test and in production, it returns true if all captcha fields are blank, 129 | # returns false otherwise. 130 | # 131 | def spam? 132 | self.class.mail_captcha.each do |field| 133 | next if send(field).blank? 134 | 135 | if defined?(Rails) && Rails.env.development? 136 | raise ScriptError, "The captcha field #{field} was supposed to be blank" 137 | else 138 | return true 139 | end 140 | end 141 | 142 | false 143 | end 144 | 145 | def not_spam? 146 | !spam? 147 | end 148 | 149 | def check_not_spam 150 | throw(:abort) if spam? 151 | end 152 | private :check_not_spam 153 | 154 | # Deliver the resource without running any validation. 155 | def deliver! 156 | mailer = MailForm::Notifier.contact(self) 157 | if mailer.respond_to?(:deliver_now) 158 | mailer.deliver_now 159 | else 160 | mailer.deliver 161 | end 162 | end 163 | 164 | # Returns a hash of attributes, according to the attributes existent in 165 | # self.class.mail_attributes. 166 | def mail_form_attributes 167 | self.class.mail_attributes.each_with_object({}) do |attr, hash| 168 | hash[attr.to_s] = send(attr) 169 | end 170 | end 171 | end 172 | end 173 | -------------------------------------------------------------------------------- /lib/mail_form/notifier.rb: -------------------------------------------------------------------------------- 1 | module MailForm 2 | class Notifier < ActionMailer::Base 3 | self.mailer_name = "mail_form" 4 | append_view_path File.expand_path('../views', __FILE__) 5 | 6 | def contact(resource) 7 | if resource.request.nil? && resource.class.mail_appendable.any? 8 | raise ScriptError, "You set :append values but forgot to give me the request object" 9 | end 10 | 11 | @resource = @form = resource 12 | 13 | resource.class.mail_attachments.each do |attribute| 14 | value = resource.send(attribute) 15 | if value.is_a?(Array) 16 | value.each { |attachment_file| add_attachment(attachment_file) } 17 | else 18 | add_attachment(value) 19 | end 20 | end 21 | 22 | headers = resource.headers 23 | headers[:from] ||= resource.email 24 | headers[:subject] ||= resource.class.model_name.human 25 | mail(headers) 26 | end 27 | 28 | private 29 | 30 | def add_attachment(attachment_file) 31 | return unless attachment_file.respond_to?(:read) 32 | attachments[attachment_file.original_filename] = attachment_file.read 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/mail_form/shim.rb: -------------------------------------------------------------------------------- 1 | require 'active_model' 2 | 3 | # This the module which makes any class behave like ActiveModel. 4 | module MailForm 5 | module Shim 6 | def self.included(base) 7 | base.class_eval do 8 | extend ActiveModel::Naming 9 | extend ActiveModel::Translation 10 | extend ActiveModel::Callbacks 11 | include ActiveModel::Validations 12 | include ActiveModel::Conversion 13 | 14 | extend MailForm::Shim::ClassMethods 15 | define_model_callbacks :deliver 16 | end 17 | end 18 | 19 | module ClassMethods 20 | def i18n_scope 21 | :mail_form 22 | end 23 | end 24 | 25 | # Initialize assigning the parameters given as hash. 26 | def initialize(params = {}) 27 | params.each_pair do |attr, value| 28 | send("#{attr}=", value) if respond_to?("#{attr}=", true) 29 | end unless params.blank? 30 | end 31 | 32 | # Always return true so when using form_for, the default method will be post. 33 | def new_record? 34 | true 35 | end 36 | 37 | def persisted? 38 | false 39 | end 40 | 41 | # Always return nil so when using form_for, the default method will be post. 42 | def id 43 | nil 44 | end 45 | 46 | # Create just check validity, and if so, trigger callbacks. 47 | def deliver 48 | if valid? 49 | run_callbacks :deliver 50 | else 51 | false 52 | end 53 | end 54 | alias :save :deliver 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/mail_form/version.rb: -------------------------------------------------------------------------------- 1 | module MailForm 2 | VERSION = "1.10.1".freeze 3 | end 4 | -------------------------------------------------------------------------------- /lib/mail_form/views/mail_form/contact.erb: -------------------------------------------------------------------------------- 1 |

<%= message.subject %>

2 | 3 | <% @resource.mail_form_attributes.each do |attribute, value| 4 | next if value.blank? %> 5 | 6 |

<%= @resource.class.human_attribute_name(attribute) %>: 7 | <%= case value 8 | when /\n/ 9 | raw(simple_format(h(value))) 10 | when Time, DateTime, Date 11 | I18n.l(value) 12 | else 13 | value 14 | end 15 | %>

16 | <% end %> 17 | 18 | <% unless @resource.class.mail_appendable.blank? %> 19 |

<%= I18n.t :title, scope: [ :mail_form, :request ], default: 'Request information' %>

20 | 21 | <% @resource.class.mail_appendable.each do |attribute| 22 | value = @resource.request.send(attribute) 23 | 24 | value = if value.is_a?(Hash) && !value.empty? 25 | list = value.to_a.map{ |k,v| content_tag(:li, h("#{k}: #{v.inspect}")) }.join("\n") 26 | content_tag(:ul, raw(list), style: "list-style:none;") 27 | elsif value.is_a?(String) 28 | value 29 | else 30 | value.inspect 31 | end 32 | %> 33 | 34 |

<%= I18n.t attribute, scope: [ :mail_form, :request ], default: attribute.to_s.humanize %>: 35 | <%= value.include?("\n") ? simple_format(value) : value %>

36 | <% end %> 37 |
38 | <% end %> 39 | -------------------------------------------------------------------------------- /mail_form.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "mail_form/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "mail_form" 7 | s.version = MailForm::VERSION.dup 8 | s.platform = Gem::Platform::RUBY 9 | s.summary = "Send e-mail straight from forms in Rails with I18n, validations, attachments and request information." 10 | s.email = "contact@plataformatec.com.br" 11 | s.homepage = "https://github.com/plataformatec/mail_form" 12 | s.description = "Send e-mail straight from forms in Rails with I18n, validations, attachments and request information." 13 | s.authors = ['José Valim', 'Carlos Antônio'] 14 | s.license = 'MIT' 15 | s.metadata = { 16 | "homepage_uri" => "https://github.com/heartcombo/mail_form", 17 | "changelog_uri" => "https://github.com/heartcombo/mail_form/blob/main/CHANGELOG.md", 18 | "source_code_uri" => "https://github.com/heartcombo/mail_form", 19 | "bug_tracker_uri" => "https://github.com/heartcombo/mail_form/issues", 20 | "wiki_uri" => "https://github.com/heartcombo/mail_form/wiki" 21 | } 22 | 23 | s.files = Dir["CHANGELOG", "MIT-LICENSE", "README.md", "lib/**/*"] 24 | s.require_paths = ["lib"] 25 | 26 | s.required_ruby_version = '>= 2.5.0' 27 | 28 | s.add_dependency('actionmailer', '>= 5.2') 29 | s.add_dependency('activemodel', '>= 5.2') 30 | end 31 | -------------------------------------------------------------------------------- /test/active_record_test.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | require 'test_helper' 4 | 5 | class ActiveRecordTest < ActiveSupport::TestCase 6 | def setup 7 | ActionMailer::Base.deliveries = [] 8 | end 9 | 10 | def test_save_is_false_when_is_a_spam 11 | form = ActiveRecordForm.new(name: 'Carlos', email: 'is.valid@email.com', nickname: 'not_blank') 12 | assert form.valid? 13 | assert form.spam? 14 | assert !form.save 15 | assert_empty ActionMailer::Base.deliveries 16 | end 17 | 18 | def test_save_is_false_when_is_invalid 19 | form = ActiveRecordForm.new(name: 'Carlos', email: 'is.com') 20 | assert form.invalid? 21 | assert form.not_spam? 22 | assert !form.save 23 | assert_empty ActionMailer::Base.deliveries 24 | end 25 | 26 | def test_save_is_true_when_is_not_spam_and_valid 27 | form = ActiveRecordForm.new(name: 'Carlos', email: 'is.valid@email.com') 28 | assert form.valid? 29 | assert form.not_spam? 30 | assert form.save 31 | assert_equal 1, ActionMailer::Base.deliveries.size 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test/mail_form_test.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | require 'test_helper' 4 | 5 | class MailFormNotifierTest < ActiveSupport::TestCase 6 | def setup 7 | @form = ContactForm.new(name: 'José', email: 'my.email@my.domain.com', message: 'Cool') 8 | 9 | valid_attributes = { name: 'José', email: 'my.email@my.domain.com', message: "Cool\nno?" } 10 | @advanced = AdvancedForm.new(valid_attributes) 11 | @advanced.request = ActionController::TestRequest.create(Class.new) 12 | 13 | @test_file = Rack::Test::UploadedFile.new(File.join(File.dirname(__FILE__), 'test_file.txt')) 14 | @with_file = FileForm.new(name: 'José', email: 'my.email@my.domain.com', message: "Cool", file: @test_file) 15 | 16 | ActionMailer::Base.deliveries = [] 17 | end 18 | 19 | def test_email_is_sent 20 | @form.deliver 21 | assert_equal 1, ActionMailer::Base.deliveries.size 22 | end 23 | 24 | def test_subject_defaults_to_human_class_name 25 | @form.deliver 26 | assert_equal 'Contact form', first_delivery.subject 27 | end 28 | 29 | def test_body_contains_subject 30 | @form.deliver 31 | assert_match %r[Contact form], first_delivery.body.to_s 32 | end 33 | 34 | def test_body_contains_attributes_values 35 | @form.deliver 36 | assert_match %r[José], first_delivery.body.to_s 37 | assert_match %r[my.email@my.domain.com], first_delivery.body.to_s 38 | assert_match %r[Cool], first_delivery.body.to_s 39 | end 40 | 41 | def test_body_contains_attributes_names 42 | @form.deliver 43 | assert_match %r[Name:], first_delivery.body.to_s 44 | assert_match %r[Email:], first_delivery.body.to_s 45 | assert_match %r[Message:], first_delivery.body.to_s 46 | end 47 | 48 | def test_body_contains_localized_attributes_names 49 | I18n.backend.store_translations(:en, mail_form: { attributes: { contact_form: { message: 'Sent message' } } }) 50 | @form.deliver 51 | assert_match %r[Sent message:], first_delivery.body.to_s 52 | assert_no_match %r[Message:], first_delivery.body.to_s 53 | end 54 | 55 | def test_body_mail_format_messages_with_break_line 56 | @form.deliver 57 | assert_no_match %r[

Cool], first_delivery.body.to_s 58 | 59 | @advanced.deliver 60 | assert_match %r[

Cool], last_delivery.body.to_s 61 | end 62 | 63 | def test_body_mail_format_dates_with_i18n 64 | @form.deliver 65 | assert_no_match %r[I18n.l(Date.today)], first_delivery.body.to_s 66 | end 67 | 68 | def test_body_does_not_append_request_if_append_is_not_called 69 | @form.deliver 70 | assert_no_match %r[Request information], first_delivery.body.to_s 71 | end 72 | 73 | def test_body_does_append_request_if_append_is_called 74 | @advanced.deliver 75 | assert_match %r[Request information], last_delivery.body.to_s 76 | end 77 | 78 | def test_request_title_is_localized 79 | I18n.backend.store_translations(:en, mail_form: { request: { title: 'Information about the request' } }) 80 | @advanced.deliver 81 | assert_no_match %r[Request information], last_delivery.body.to_s 82 | assert_match %r[Information about the request], last_delivery.body.to_s 83 | end 84 | 85 | def test_request_info_attributes_are_printed 86 | @advanced.deliver 87 | assert_match %r[Remote ip], last_delivery.body.to_s 88 | assert_match %r[User agent], last_delivery.body.to_s 89 | end 90 | 91 | def test_request_info_attributes_are_localized 92 | I18n.backend.store_translations(:en, mail_form: { request: { remote_ip: 'IP Address' } }) 93 | @advanced.deliver 94 | assert_match %r[IP Address], last_delivery.body.to_s 95 | assert_no_match %r[Remote ip], last_delivery.body.to_s 96 | end 97 | 98 | def test_request_info_values_are_printed 99 | @advanced.deliver 100 | assert_match %r[0\.0\.0\.0], last_delivery.body.to_s 101 | assert_match %r[Rails Testing], last_delivery.body.to_s 102 | end 103 | 104 | def test_request_info_hashes_are_print_inside_lists 105 | @advanced.request.session = { my: :session, user: "data" } 106 | @advanced.deliver 107 | assert_match %r[my: :session<\/li>], last_delivery.body.to_s 109 | assert_match %r[

  • user: \S+data\S+<\/li>], last_delivery.body.to_s 110 | end 111 | 112 | def test_error_is_raised_when_append_is_given_but_no_request_is_given 113 | assert_raise ScriptError do 114 | @advanced.request = nil 115 | @advanced.deliver 116 | end 117 | end 118 | 119 | def test_form_with_file_includes_an_attachment 120 | @with_file.deliver 121 | assert_equal 1, first_delivery.attachments.size 122 | end 123 | 124 | def test_form_with_file_does_not_output_attachment_as_attribute 125 | @with_file.deliver 126 | assert_no_match %r[File:], first_delivery.body.to_s 127 | end 128 | 129 | def test_form_with_multiple_files_includes_attachments 130 | @with_file.file = [@test_file, @test_file, @test_file] 131 | @with_file.deliver 132 | assert_equal 3, first_delivery.attachments.size 133 | end 134 | 135 | protected 136 | 137 | def first_delivery 138 | ActionMailer::Base.deliveries.first 139 | end 140 | 141 | def last_delivery 142 | ActionMailer::Base.deliveries.last 143 | end 144 | 145 | def teardown 146 | I18n.reload! 147 | end 148 | end 149 | -------------------------------------------------------------------------------- /test/resource_test.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | require 'test_helper' 4 | 5 | class MailFormBaseTest < ActiveSupport::TestCase 6 | 7 | def setup 8 | ActionMailer::Base.deliveries = [] 9 | end 10 | 11 | def test_id_is_nil 12 | assert_nil ContactForm.new.id 13 | end 14 | 15 | def test_is_always_a_new_record 16 | assert ContactForm.new.new_record? 17 | end 18 | 19 | def test_initialize_with_options 20 | form = ContactForm.new(name: 'José', email: 'jose@my.email.com') 21 | assert_equal 'José', form.name 22 | assert_equal 'jose@my.email.com', form.email 23 | end 24 | 25 | def test_spam_is_true_when_captcha_field_is_set 26 | form = ContactForm.new(nickname: 'not_blank') 27 | assert form.spam? 28 | assert !form.not_spam? 29 | end 30 | 31 | def test_spam_is_false_when_captcha_field_is_not_set 32 | form = ContactForm.new 33 | assert !form.spam? 34 | assert form.not_spam? 35 | end 36 | 37 | def test_is_not_valid_when_validatable_attributes_are_blank 38 | form = ContactForm.new 39 | assert !form.valid? 40 | assert form.invalid? 41 | 42 | assert_equal 2, form.errors.count 43 | assert_equal ["can't be blank"], form.errors[:email] 44 | assert_equal ["can't be blank"], form.errors[:name] 45 | end 46 | 47 | def test_is_not_valid_when_validatable_regexp_does_not_match 48 | form = ContactForm.new(name: 'Jose', email: 'not_valid') 49 | assert !form.valid? 50 | assert form.invalid? 51 | 52 | assert_equal(1, form.errors.count) 53 | assert_equal ["is invalid"], form.errors[:email] 54 | end 55 | 56 | def test_is_valid_when_validatable_attributes_are_valid 57 | form = ContactForm.new(name: 'Jose', email: 'is.valid@email.com') 58 | assert form.valid? 59 | assert !form.invalid? 60 | end 61 | 62 | def test_symbols_given_to_validate_are_called 63 | form = ContactForm.new 64 | assert !form.callback_run? 65 | form.valid? 66 | assert form.callback_run? 67 | end 68 | 69 | def test_deliver_is_false_when_is_a_spam 70 | form = ContactForm.new(name: 'Jose', email: 'is.valid@email.com', nickname: 'not_blank') 71 | assert form.valid? 72 | assert form.spam? 73 | assert !form.deliver 74 | assert_empty ActionMailer::Base.deliveries 75 | end 76 | 77 | def test_deliver_is_false_when_is_invalid 78 | form = ContactForm.new(name: 'Jose', email: 'is.com') 79 | assert form.invalid? 80 | assert form.not_spam? 81 | assert !form.deliver 82 | assert_empty ActionMailer::Base.deliveries 83 | end 84 | 85 | def test_deliver_is_true_when_is_not_spam_and_valid 86 | form = ContactForm.new(name: 'Jose', email: 'is.valid@email.com') 87 | assert form.valid? 88 | assert form.not_spam? 89 | assert form.deliver 90 | assert_equal 1, ActionMailer::Base.deliveries.size 91 | end 92 | 93 | def test_human_name_returns_a_humanized_name 94 | assert_equal 'Contact form', ContactForm.model_name.human 95 | end 96 | 97 | def test_human_name_can_be_localized 98 | I18n.backend.store_translations(:en, mail_form: { models: { contact_form: 'Formulário de contato' } }) 99 | assert_equal 'Formulário de contato', ContactForm.model_name.human 100 | end 101 | 102 | def test_human_attribute_name_returns_a_humanized_attribute 103 | assert_equal 'Message', ContactForm.human_attribute_name(:message) 104 | end 105 | 106 | def test_human_attribute_name_can_be_localized 107 | I18n.backend.store_translations(:en, mail_form: { attributes: { contact_form: { message: 'Mensagem' } } }) 108 | assert_equal 'Mensagem', ContactForm.human_attribute_name(:message) 109 | end 110 | 111 | def test_activemodel_linked_errors 112 | form = ContactForm.new(email: 'not_valid', category: "invalid") 113 | form.valid? 114 | assert_equal ["can't be blank"], form.errors[:name] 115 | assert_equal ["is invalid"], form.errors[:email] 116 | assert_equal ["is not included in the list"], form.errors[:category] 117 | assert_equal [], form.errors[:message] 118 | end 119 | 120 | def test_activemodel_errors_lookups_model_keys 121 | I18n.backend.store_translations(:en, mail_form: { errors: { models: { contact_form: 122 | { attributes: { email: { invalid: 'fill in the email' }, name: { blank: 'fill in the name' } } } 123 | }}}) 124 | 125 | form = ContactForm.new(email: 'not_valid') 126 | form.valid? 127 | 128 | assert_equal ["fill in the name"], form.errors[:name] 129 | assert_equal ["fill in the email"], form.errors[:email] 130 | end 131 | 132 | def teardown 133 | I18n.reload! 134 | end 135 | 136 | end 137 | -------------------------------------------------------------------------------- /test/test_file.txt: -------------------------------------------------------------------------------- 1 | A test file 2 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | 3 | RAILS_ENV = ENV["RAILS_ENV"] = "test" 4 | 5 | $:.unshift File.dirname(__FILE__) + '/../lib' 6 | require 'mail_form' 7 | 8 | require 'active_support' 9 | require 'active_support/test_case' 10 | require 'active_support/string_inquirer' 11 | require 'action_controller' 12 | require 'action_controller/test_case' 13 | require 'action_mailer' 14 | 15 | if ActiveSupport::TestCase.respond_to?(:test_order=) 16 | ActiveSupport::TestCase.test_order = :random 17 | end 18 | 19 | ActionMailer::Base.delivery_method = :test 20 | I18n.enforce_available_locales = false 21 | 22 | module Rails 23 | def self.env 24 | @env ||= ActiveSupport::StringInquirer.new('test') 25 | end 26 | end 27 | 28 | class ContactForm < MailForm::Base 29 | attribute :name, validate: true 30 | attribute :email, validate: /\A[^@\s]+@[^@\s]+\z/i 31 | attribute :category, validate: ["Interface bug", "General"], allow_blank: true 32 | attribute :nickname, captcha: true 33 | 34 | attributes :created_at, :message, validate: :callback 35 | 36 | def headers 37 | { to: 'my.email@my.domain.com' } 38 | end 39 | 40 | def initialize(*) 41 | super 42 | @_callback_run = false 43 | end 44 | 45 | def callback 46 | @_callback_run = true 47 | end 48 | 49 | def callback_run? 50 | @_callback_run 51 | end 52 | end 53 | 54 | class AdvancedForm < ContactForm 55 | append :remote_ip, :user_agent, :session 56 | 57 | def headers 58 | { to: [ 'my.first@email.com', 'my.second@email.com' ], 59 | subject: "My Advanced Form", 60 | from: %{"#{name}" <#{email}>}, 61 | "return-path" => "mypath" 62 | } 63 | end 64 | end 65 | 66 | class FileForm < ContactForm 67 | attribute :file, attachment: true, validate: true 68 | 69 | def headers 70 | to = if file 71 | "contact_file@my.domain.com" 72 | else 73 | "contact@my.domain.com" 74 | end 75 | { to: to } 76 | end 77 | end 78 | 79 | class ActiveRecordForm 80 | include ActiveModel::Model 81 | 82 | # Simulate Active Record `*_create` callbacks. 83 | extend ActiveModel::Callbacks 84 | define_model_callbacks :create 85 | 86 | def save 87 | if valid? 88 | run_callbacks(:create) { true } 89 | else 90 | false 91 | end 92 | end 93 | 94 | include MailForm::Delivery 95 | 96 | attribute :name, validate: true 97 | attribute :email, validate: /\A[^@\s]+@[^@\s]+\z/i 98 | attribute :nickname, captcha: true 99 | 100 | def headers 101 | { 102 | to: "your.email@your.domain.com", 103 | subject: "User created an account" 104 | } 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /test/views/mail_form/custom_template.erb: -------------------------------------------------------------------------------- 1 | Hello from my custom template! 2 | --------------------------------------------------------------------------------