├── .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 | [](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 |
<%= @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 attribute, scope: [ :mail_form, :request ], default: attribute.to_s.humanize %>: 35 | <%= value.include?("\n") ? simple_format(value) : value %>
36 | <% end %> 37 |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[