Send and receive emails in rails
Email providers
There is nice table of main providers
Check SMTP
If you need to check if smtp configuration is working you can use free service https://debugmail.io/ , just use port 9025 instead 25 since ISP is blocking
- Copy configuration from their site and put in to your config.
For cli command line you can use swaks
like
swaks --to [email protected] --server $SERVER --port $PORT --auth-user $AUTH_USER --auth-password $AUTH_PASSWORD --auth-plaintext --auth-hide-password
so in autput you can see all telnet communications:
# generate base64 encoding
# special characters need to have \ in front
perl -MMIME::Base64 -e 'print encode_base64("duleorlovic\@gmx.com");'
perl -MMIME::Base64 -e 'print encode_base64("password");'
telnet debugmail.io 9025
EHLO main
AUTH LOGIN
<paste encoded username>
<paste encoded password>
ctrl + ]
ctrl + d
If you want to inspect how rails action_mailer sends and receive tcp messages
than put byebug in net smtp class on line 940 get_response
recv_response
/home/orlovic/.rvm/rubies/ruby-2.3.3/lib/ruby/2.3.0/net/smtp.rb
GMX
rails credentials:edit
smtp_server: mail.gmx.com
smtp_username: dule****@gmx.com
smtp_password: *****
# config/application.rb
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
address: Rails.application.credentials.smtp_username,
port: 587,
authentication: 'plain',
enable_starttls_auto: true,
user_name: Rails.application.credentials.smtp_username,
password: Rails.application.credentials.smtp_password,
}
Gmail
Gmail smtp is the most easiest way to start
# config/application.rb
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
address: 'smtp.gmail.com',
port: 587,
domain: 'gmail.com',
authentication: 'plain',
enable_starttls_auto: true,
user_name: Rails.application.secrets.smtp_username,
password: Rails.application.secrets.smtp_password
}
# config/secrets.yml
smtp_username: <%= ENV["SMTP_USERNAME"] %>
smtp_password: <%= ENV["SMTP_PASSWORD"] %>
If you receive error SocketError: getaddrinfo: Name or service not known
than
you probably miss the address
field.
If there is error with EOFError: end of file reached
than you need to change
domain
field (should not be localhost
, but the domain part of the sender
email, for example gmail.com
).
The best way is to enable 2 step verification and create App Password https://support.google.com/accounts/answer/185833 App password can be used instead of password and does not require enable less secure apps 3th party apps. You can enable multiple two step verification with one single mobile phone number.
If you see error in logs:
2018-06-18T09:13:29.371621+00:00 app[web.1]: An error occurred when sending a notification using 'email' notifier. Net::SMTPAuthenticationError: 534-5.7.14 <https://accounts.google.com/signin/continue?sarp=1&scc=1&plt=AKgnsbu5
Username and Password not accepted. Learn more
Note that google does not allow less secure app any more https://support.google.com/accounts/answer/6010255 You need to Allow less secure apps https://support.google.com/accounts/answer/6010255
gmail send email as sometimes stops since google disable allow less secure app. port 587 and Secured connection using TLS (this is default recommended) but enable https://myaccount.google.com/u/7/lesssecureapps? or you will get error
Authentication failed. Please check your username/password and Less Secure Apps access for
Sometimes you can send from your IP but not from Heroku IP address.
For error Errno::ECONNREFUSED (Connection refused - connect(2) for "localhost" port 25
the problem occurs when you in initializers (for example config/initializers/devise.rb or config/initializers/exception_notification.rb) use ApplicationMailer::MAILER_SENDER or some other constant from Rails classes Note that this occurs only on production. So use only constants from initializers.
Rails.application.config.action_mailer.smtp_settings
# it is the same as
Rails.configuration.action_mailer.smtp_settings
=> {:address=>"smtp.gmail.com", :port=>587, :authentication=>"plain", :enable_starttls_auto=>true, :user_name=>...
Rails.configuration.action_mailer.delivery_method
=> :smtp
AWS Workmail
# config/application.rb
config.action_mailer.smtp_settings = {
address: 'smtp.mail.us-east-1.awsapps.com',
port: 465,
domain: 'mydomain.com',
user_name: Rails.application.credentials.smtp_username,
password: Rails.application.credentials.smtp_password,
authentication: 'login',
enable_starttls_auto: false,
tls: true,
ssl: true,
}
config.action_mailer.delivery_method = :smtp
aws ses simple email service https://www.sitepoint.com/deliver-the-mail-with-amazon-ses-and-rails/
# config/application.rb
config.action_mailer.smtp_settings = {
address: "email-smtp.us-east-1.amazonaws.com",
port: 587,
user_name: Rails.application.credentials.smtp_username,
password: Rails.application.credentials.smtp_password,
authentication: :login,
enable_starttls_auto: true,
}
config.action_mailer.delivery_method = :smtp
Sendgrid
Sendgrid is simple to start on heroku. Just add new add-on free plan with
commands heroku addons:create sendgrid
and that will set up env keys.
heroku config
you can find the keys and copy them to heroku config:set
SMTP_USERNAME=asdasdasd SMTP_PASSWORD=asdasdasd
. It allows sending with from
field any domain, but in gmail it shows that message is from: My Company
[email protected] via sendgrid.me
.
# do not use Rails.application.config.action_mailer.smtp_settings
cat > config/initializers/smtp.rb << \HERE_DOC
ActionMailer::Base.smtp_settings = {
:user_name => Rails.application.secrets.smtp_username,
:password => Rails.application.secrets.smtp_password,
:domain => 'yourdomain.com',
:address => 'smtp.sendgrid.net',
:port => 587,
:authentication => :plain,
:enable_starttls_auto => true
}
HERE_DOC
Another way is to use API
Mandrill
Mandrill is better than Sendgrid, since Sendgrid can not automatically convert html to txt mails. Also mandrill has nice API so you do not need background job to send a lot of emails quickly. To setup sending using API just run:
# Gemfile
gem 'mandrill_dm'
# config/application.rb
config.action_mailer.delivery_method = :mandrill
# config/initializers/mandrill.rb
MandrillDm.configure do |config|
config.api_key = Rails.application.secrets.mandrill_api_key
end
# config/secrets.yml
development:
mandrill_api_key: <%= ENV["MANDRILL_API_KEY"] %>
If you are using mandril_delivery
for ExceptionNotification than emails will
look scrambled, because generated html version will join all lines. Note that it
will trigger any webhooks that you have set up.
Sparkpost
Sparkpost offer a lot of free usage (mandrill requires subscription) so
currently it is my best option. You need first to validate your domain, so you
can send with from
field with that domain. You need also to
echo "gem 'sparkpost_rails'" >> Gemfile
sed -i config/environments/production.rb -e '/^end$/i \
config.action_mailer.delivery_method = :sparkpost'
cat > config/initializers/sparkpostrails.rb << HERE_DOC
# https://github.com/the-refinery/sparkpost_rails#additional-configuration
SparkPostRails.configure do |c|
c.api_key = Rails.application.secrets.sparkpost_api_key
end
HERE_DOC
sed -i config/secrets.yml -e '/^test:/i \
# email provider\
sparkpost_api_key: <%= ENV["SPARKPOST_API_KEY"] %>'
vi config/secrets.yml # update mailer_sender to match your domain
You can also use smtp with SPARK_POST but it is two times slower
# config/application.rb
config.action_mailer.smtp_settings = {
address: 'smtp.sparkpostmail.com',
port: 587,
enable_starttls_auto: true,
user_name: 'SMTP_Injection',
password: Rails.application.secrets.sparkpost_api_key,
}
config.action_mailer.delivery_method = :smtp # sparkpost
# check local configuration
rails runner "puts Rails.application.config.action_mailer.smtp_settings"
time rails runner 'UserMailer.signup.deliver_now!' # ~5sec with smtp
time rails runner 'UserMailer.signup.deliver_now!' # ~2.5sec with sparkpost
Letter opener for local preview
sed -i '/group :development do/a \
# open emails in browser\
gem "letter_opener"' Gemfile
sed -i '/^end$/i \ config.action_mailer.delivery_method = :letter_opener' config/environments/development.rb
# also in config/initializers/exception_notification.rb
Note that email letter opener does not work when you run with rake jobs:work
,
but works when bin/delayed_job run
(Launchy works in both cases, this
difference is only for mailer).
Interceptor
When you need to check production emails localy, than you can set up interceptor so you receive all emails (and not real customer emails).
# config/initializers/interceptor.rb
class DevelopmentMailInterceptor
def self.delivering_email(message)
message.subject = "#{message.to} #{message.subject}"
message.to = Rails.application.secrets.mail_interceptor_email
end
end
if Rails.env.development?
ActionMailer::Base.register_interceptor(DevelopmentMailInterceptor)
end
# config/secrets.yml
mail_interceptop_email: <%= ENV["MAIL_INTERCEPTOR_EMAIL"] %>
When you need to preview a lot of emails, its faster to use letter_opener gem.
Just put in your Gemfile under development gem "letter_opener"
and in
config/environments/development.rb config.action_mailer.delivery_method =
:letter_opener
. Works when email is sent (even from ajax response or console).
Style
Official gmail styles supports
<style>
in head and media queries but when you forward email than css styles
will be gone. Better is to use gem which will copy and duplicate all styles from
head to inline styles and that will support more clients (not just gmail).
For easier styling, you should use roadie gem that will generate all inline style from your head styles.
# Attach css classes to emails
gem 'roadie'
gem 'roadie-rails'
You need to include mixing to each mailer (including in ApplicationMailer does not help)
# app/mailers/my_mailer.rb
class MyMailer < ActionMailer::Base
include Roadie::Rails::Automatic
end
# or include in ApplicaitionMailer and use roadie_mail
class ApplicationMailer < ActionMailer::Base
include Roadie::Rails::Mailer
end
class MyMailer < ActionMailer::Base
def welcome(user)
roadie_mail to: user.email
end
end
You can change template with mail to: '[email protected]', template_name:
'contact_form'
To send without template you can
mail to: '[email protected]' do |format|
format.html { render text: 'a' }
end
Another solution is gem 'premailer-rails'
https://github.com/fphilipe/premailer-rails which can also generate text part
so you do not need to maintain it. Just add the gem and you are good to go.
I notice that in test I need to replace
mail = ActionMailer::Base.deliveries.last
# instead of using: mail.body use
mail.body.encoded
# or
mail.to_s
https://github.com/fphilipe/premailer-rails#how-it-works You can use external styles (from public or from cdn) and it will be converted to inline. Can not use font awesome since it requires custom font which is not supported in gmail https://stackoverflow.com/questions/40030954/how-to-use-custom-font-in-email-template
Gmail Android App will also parse media queries and apply that to inline styles (it will override inline styles). Gmail in the browser will parse styles (but not media queries) and apply to the elements when presenting to the user.
To preview emails use generated preview files in
test/mailers/previews/my_mailer_preview.rb
or create new file:
# app/mailer_previews/application_mailer_preview.rb
class ApplicationMailerPreview < ActionMailer::Preview
def new_message_from_client
message = Message.first || FactoryBot.create :message
ApplicationMailer.new_message_from_client message
end
end
add a line config.action_mailer.preview_path =
"#{Rails.root}/app/mailer_previews"
to config/environments/development.rb and
go to rails/mailers.
If you are using catch all route than add those lines
# config/routes.rb
# https://stackoverflow.com/questions/26130130/what-are-the-routes-i-need-to-set-up-to-preview-emails-using-rails-4-1-actionmai
get '/rails/mailers' => "rails/mailers#index"
get '/rails/mailers/*path' => "rails/mailers#preview"
# https://stackoverflow.com/a/6047561/287166
match '*a', to: 'home#routing_error', via: [:get, :post]
Add authentication to mailer preview
# config/initializers/mailer_preview.rb
class Rails::MailersController
before_filter :_authenticate_admin!
def _authenticate_admin!
redirect_to root_path, alert: 'Only admin' unless current_admin_user.present?
end
end
Another gem to preview emails https://github.com/markets/maily
Here is example of style:
# app/mailers/applicaion_mailer.rb
class ApplicationMailer < ActionMailer::Base
default from: '[email protected]'
layout 'mailer'
add_template_helper MailerHelper
end
# app/views/layouts/mailer.html.erb
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style>
.email-container {
max-width: 500px;
}
.pre-header {
display: none;
}
.bordered {
border: 2px solid #ccc;
border-radius: 5px;
padding: 5px;
background: #e5f1ff;
}
</style>
</head>
<body>
<div class="email-container">
<div class="pre-header">
<%= yield :subject_line %>
</div>
<%= yield %>
</div>
</body>
</html>
# app/helpers/mailer_helper.rb
module MailerHelper
def subject_line(message)
content_for :subject_line, message
end
end
# app/mailers/user_mailer/contact.html.erb
<%
subject_line @user.name
%>
<h1><%= t "user_mailer.landing_signup.title", name: @user.email %></h1>
To change layout for devise mailer you can use https://github.com/heartcombo/devise/wiki/How-To:-Create-custom-layouts#application–devise-config
Devise::Mailer.layout "email"
or better is to change parent email
# config/initializers/devise.rb
config.parent_mailer = 'ApplicationMailer'
Receiving emails
When you want to receive, use mandrill-rails.
echo '
# receiving emails and webhooks
gem "mandrill-rails" ' >> Gemfile
sed
resource :inbox, :controller => 'inbox', :only => [:show,:create]
config/routes.rb
echo 'class InboxController < ApplicationController
include Mandrill::Rails::WebHookProcessor
def handle_inbound(event_payload)
# do something with payload
end
end ' > app/controllers/inbox_controller.rb
Mandrill:
- create api key for prod and test
- validate inbound domains for prod and test
- create routes for validated domains (this will create one webhook)
- create webhooks
- create rules that match api and hooktype and send it to webhook
authentication
You should add an IP address or domain from which you will send email so receiving server can check if you authorized them to deliver https://www.cloudflare.com/en-gb/learning/dns/dns-records/dns-spf-record/
v=spf1 include:_spf.google.com ~all
Feedback Loop is in a header and some clients enable them http://www.list-unsubscribe.com/
Internal Notification
Those are usefull admin or devops notifications
# config/secrets.yml
shared:
mailer_sender: <%= ENV["MAILER_SENDER"] || "My Company <[email protected]>" %>
internal_notification_email: <%= ENV["INTERNAL_NOTIFICATION_EMAIL"] || "[email protected]" %>
# mailers/application_mailer.rb
class ApplicationMailer < ActionMailer::Base
layout 'mailer'
default from: Rails.application.secrets.mailer_sender
INTERNAL_NOTIFICATION_EMAIL = Rails.application.secrets.internal_notification_email
def internal_notification(subject, item = {})
return unless INTERNAL_NOTIFICATION_EMAIL
email_subject = "[MyApp#{' staging' if Rails.application.secrets.is_staging}] #{subject}"
email_body = "<h1>#{subject}</h1><strong>Details:</strong>" +
item.inspect.
gsub(', ', ",<br>").
gsub('{', '<br>{<br>').
gsub('}', '<br>}<br>')
mail to: INTERNAL_NOTIFICATION_EMAIL,
subject: email_subject,
body: email_body,
content_type: "text/html"
end
end
You can send notification in any class. Note that first param is string, and ohers are hash.
# app/models/user.rb
after_save :send_notification_geocode_failed
def send_notification_geocode_failed
if address_changed? && !city.present?
ApplicationMailer.internal_notification(
"geocode city is not present #{name}",
name: name,
url: Rails.application.routes.url_helpers.menu_url(link),
address: address,
).deliver_now
end
end
Note that you should not send email in before blocks since when validation fails it will rollback and even background job is rollbacked.
ActionMailer
Here is what we can do with ActionMailer:
- headers
- attachments
You can use before_action
and after_action
and access to params
http://guides.rubyonrails.org/action_mailer_basics.html#action-mailer-callbacks
For example prevent_delivery_to_guests
class UserMailer < ApplicationMailer
before_action { @business, @user = params[:business], params[:user] }
after_action :prevent_delivery_to_guests
def feedback_message
end
def prevent_delivery_to_guests
if @user && @user.guest?
mail.perform_deliveries = false
end
end
end
Dynamic smtp settings at runtime
class DeviseMailer < Devise::Mailer
after_action :set_smtp
def set_smtp
# determine smtp settings form @receiver or other
if isp.use_my_smtp_server && @_mail_was_called # spam could ignore mail
mail.from = "#{receiver.smtp_from_name} <#{receiver.smtp_from_email}>"
mail.reply_to = "#{receiver.smtp_from_name} <#{receiver.smtp_from_email}>"
mail.delivery_method.settings.merge!(
address: receiver.smtp_host,
port: (receiver.smtp_port.present? ? receiver.smtp_port : 587),
user_name: receiver.smtp_username,
password: receiver.smtp_password,
)
end
end
Save emails in database
# app/mailers/application_mailer.rb
class ApplicationMailer < ActionMailer::Base
default from: Rails.application.credentials.mailer_sender
layout 'mailer'
after_action :save_email
def save_email
return if @user.blank?
# here we have access to `mail` object
@user.member_profile.emails.create(
to: @user.email,
subject: mail.subject,
body: mail.body,
)
end
end
Test
# test/mailers/application_mailer_test.rb
require 'test_helper'
class ApplicationMailerTest < ActionMailer::TestCase
test '#save_email' do
user = users(:user)
assert_difference 'Email.count', 1 do
AdminMailer.add_photos(user).deliver_now
end
email = Email.last
assert_equal 'Add your photo to improve responses', email.subject
end
end
Interesting
You can include small giff that looks like screencast. Image should be less than 1MB and included inline.
Email gems http://awesome-ruby.com/#-email and gmail
You can use img tags and css background image, but if it is run in background (it does not know on which request.host) than you need to set asset host (look in common rails bootstrap snippets
# app/views/layouts/mailer.html.erb
background-image: url('<%= asset_url 'cute-small.jpg' %>');
<%= image_tag 'premesti_se.gif' %>
# config/application.rb
config.action_mailer.asset_host = "http://my_host"
Gmail Go To Action
Using some header json you can set button in gmail subject line “Quick Actions”. https://stackoverflow.com/questions/22318432/how-do-i-add-go-to-action-in-gmail-subject-line-using-schema-org
Spam detection
You can disable registering specific email domains using this list https://github.com/FGRibreau/mailchecker Using this gem https://github.com/rubygarage/truemail you can check if actual email account exists on smtp server. Fake emails are detected using: whitelist/blacklist, regex, mx validation, smtp validation
email_address = EmailAddress.new '[email protected]'
email_address.valid?
=> false
email_address.error
=> "Domain name not registered"
Format of emails can be validated using https://github.com/afair/email_address
Email
Prevent spam
https://support.google.com/mail/thread/3973530?hl=en
Testing emails
https://www.engineyard.com/blog/testing-async-emails-rails-42
# test/support/mailer_helpers.rb
module MailerHelpers
def clear_mails
ActionMailer::Base.deliveries = []
end
# if you deliver_now you can
# assert_difference 'all_mails.count', 1 do
# and for background deliver_later you need to assert perform or enqueue
# inherit from ActiveJob::TestCase
# or include ActiveJob::TestHelper
# assert_performed_jobs 1, only: ActionMailer::MailDeliveryJob do
def all_mails
ActionMailer::Base.deliveries
end
# last_email is renamed to last_mail
def last_mail
raise 'you_should_use_give_me_last_mail_and_clear_mails'
# ActionMailer::Base.deliveries.last
end
# some usage is like
# mail = give_me_last_mail_and_clear_mails
# assert_equal [email], mail.to
# assert_match t('user_mailer.landing_signup.confirmation_text'), mail.html_part.decoded # mail.body.to_s when it is not multipart (devise) when there is not txt.erb template
# confirmation_link = mail.html_part.decoded.match(
# /(http:.*)">#{t("confirm_email")}/
# )[1]
# visit confirmation_link
def give_me_last_mail_and_clear_mails
mail = ActionMailer::Base.deliveries.last
clear_mails
mail
end
end
class ActiveSupport::TestCase
include MailerHelpers
# for assert_performed_jobs
include ActiveJob::TestHelper
end
class ActionDispatch::IntegrationTest
include MailerHelpers
# for assert_performed_jobs
include ActiveJob::TestHelper
end
Click link tracking in emails
https://github.com/ankane/ahoy_email
- gmail shows download button for images, but you can prevent that by wrapping
the image with link, or use style:
img + div { display:none; }
- get local configuration development ActiveRecord