Christopher Warrington
Christopher Warrington

Reputation: 767

Devise not sending emails through ActionMailer

I have read through so many tutorials and answers online and am not finding my fix. I have setup my ActionMailer to send emails through Mailgun's service. I can verify that in production it does send emails through my online contact form without any issues. But, when I try to use one of Devise's restore templates (ie: Forgot Password) the email is not being sent.

Code

--> config/environments/production.rb

# ActionMailer Config
config.action_mailer.perform_caching = false

config.action_mailer.raise_delivery_errors = true

config.action_mailer.default_url_options = { :host => 'thechristianchain.org' }

config.action_mailer.delivery_method = :smtp
config.action_mailer.perform_deliveries = true

config.action_mailer.default :charset => "utf-8"

config.action_mailer.smtp_settings = {
  :authentication => :plain,
  :address => "smtp.mailgun.org",
  :port => '587',
  :domain => "mx.tccf.co",
  :user_name => ENV["MAILGUN_USERNAME"],
  :password => ENV["MAILGUN_PASSWORD"]
}

To try and debug I set raise_deliever_errors to true as seen in the code above.

When I look through the logs I see this (I removed the log timestamps for easier reading):

I, [2018-02-23T20:17:28.511441 #4]  INFO -- : [fb02bfdf-a5c3-4918-bae6-5c5d32ef6fba] Sent mail to [email protected] (1028.0ms)

D, [2018-02-23T20:17:28.511562 #4] DEBUG -- : [fb02bfdf-a5c3-4918-bae6-5c5d32ef6fba] Date: Fri, 23 Feb 2018 20:17:27 +0000

From: [email protected]
Reply-To: [email protected]
To: [email protected]
Message-ID: <5a9076d776379_4233fb2465758@2694484e-c3cb-44d0-8ece-832cd70adf2c.mail>
Subject: Reset password instructions
Mime-Version: 1.0
Content-Type: text/html;
 charset=UTF-8
Content-Transfer-Encoding: 7bit

<p>Hello [email protected]!</p>

<p>Someone has requested a link to change your password. You can do this through the link below.</p>

<p><a href="http://thechristianchain.org/password/edit?reset_password_token=1gtx9XHmTzUjf97YhJdG">Change my password</a></p>

<p>If you didn't request this, please ignore this email.</p>
<p>Your password won't change until you access the link above and create a new one.</p>

I, [2018-02-23T20:17:28.511950 #4] INFO -- : [fb02bfdf-a5c3-4918-bae6-5c5d32ef6fba] Completed 401 Unauthorized in 1044ms (ActiveRecord: 5.7ms)
F, [2018-02-23T20:17:28.513684 #4] FATAL -- : [fb02bfdf-a5c3-4918-bae6-5c5d32ef6fba]   
F, [2018-02-23T20:17:28.513764 #4] FATAL -- : [fb02bfdf-a5c3-4918-bae6-5c5d32ef6fba] Net::SMTPAuthenticationError (535 5.7.0 Mailgun is not loving your login or password
):

The key is in the last line where it fails because 401 Unauthorized and Net::SMTPAuthenticationError and basically stating that Mailgun did not like my login credentials.

QUESTION

Here's my question. Where is Devise pulling the information? When I send through the contact form, Mailgun accepts the login credentials that I submit; but when Devise sends, they are not accepted. This makes me think that there is a setting somewhere that is not pulling the information from production.rb.

Upvotes: 1

Views: 2004

Answers (1)

Christopher Warrington
Christopher Warrington

Reputation: 767

After 4 days of working on this issue, I was able to come up with a solution. Not sure if it is the best or most eloquent way of doing it, but just in case anyone else in the future has this same issue, here is what I did.

First, some explanation. I am not sure why updating the development.rb and production.rb files with my config.action_mailer.smtp_settings didn't work. By all of the documentation, it seems that Devise should work seamlessly with those settings. Now, the reason my contact form worked and Devise didn't though is because my contact form was utilizing contact_us_email_mailer.rb which had the code for sending email through Mailgun's API. (Thank you @kuwantum. Your comment helped me make the connection that I was sending the emails differently.)

So, my mentality was to figure out how Devise sends it's emails, where it calls the SMTP or API to email, and then enter my information there. I went to the Devise Wiki and found a "How To" for setting up a custom mailer file. Following these steps, I setup membership_mailer.rb file under app/mailers/ that looked like this:

class MembershipMailer < Devise::Mailer   

  helper :application
  include Devise::Controllers::UrlHelpers

end

And then changed my config/initializers/devise.rb file to point to this new mailer by changing the following line:

# Configure the class responsible to send e-mails.
# config.mailer = 'Devise::Mailer'
  config.mailer = 'MembershipMailer'

I then needed to grab the original Devise mailer code and enter it into this document. I got that code here.

devise/mailer.rb

def confirmation_instructions(record, token, opts={})
  @token = token
  devise_mail(record, :confirmation_instructions, opts)
end

def reset_password_instructions(record, token, opts={})
  @token = token
  devise_mail(record, :reset_password_instructions, opts)
end

def unlock_instructions(record, token, opts={})
  @token = token
  devise_mail(record, :unlock_instructions, opts)
end

def email_changed(record, opts={})
  devise_mail(record, :email_changed, opts)
end

def password_change(record, opts={})
  devise_mail(record, :password_change, opts)
end

Looking at this code, I see the devise_mail being called often, so I needed to find out what that code is. And I got to this page. Searching this site gave me the code that I needed.

def devise_mail(record, action, opts = {}, &block)
  initialize_from_record(record)
  mail headers_for(action, opts), &block
end

This code then showed a few more function calls that I searched for on the site mentioned above and the code that I needed to understand was the headers_for section. This is that code:

def headers_for(action, opts)
  headers = {
    subject: subject_for(action),
    to: resource.email,
    from: mailer_sender(devise_mapping),
    reply_to: mailer_reply_to(devise_mapping),
    template_path: template_paths,
    template_name: action
  }.merge(opts)

  @email = headers[:to]
  headers
end

Running byebug, I was able to call this function and get a hash of responses, and calling each gave me it's value. So, typing headers_for(action, opts)[:from] gave me the default email that I had setup in the devise.rb file. So, that works.

Now, for the part that took some trial and error. I was having difficulty with the SMTP settings for Mailgun but know that the API worked as it was working with my contact form. So, I needed to get the variables working. This is the code that worked for my contact form (note that the hidden key was being called from a .env file created with the 'dotenv-rails' gem and the require statement utilized the 'rest-client' gem.):

contact_us_email_mailer.rb

require 'rest-client'

def send_email(from, to, subject, text)
  RestClient.post ENV.fetch("MAILGUN_MX_URL"),
    :from => from,
    :to => to,
    :subject => subject,
    :text => text
end

Now, combining this with the devise_mail function I got this code:

def mail(record, action, opts = {}, block)
  initialize_from_record(record)

  RestClient.post ENV.fetch("MAILGUN_MX_URL"),
    :from => headers_for(action, opts)[:from],
    :to => headers_for(action, opts)[:to],  
    :subject => headers_for(action, opts)[:subject],
end

I then needed to figure out how to get the email templates created into the email. That is when I came across this StackOverflow answer that pointed me in the right direction and I came up with this code:

html_output = render_to_string(:action => action, :layout => false)
:html => html_output.to_str

Adding that to the previous code I got this:

def mail(record, action, opts = {}, block)
  initialize_from_record(record)
  html_output = render_to_string(:action => action, :layout => false)

  RestClient.post ENV.fetch("MAILGUN_MX_URL"),
    :from => headers_for(action, opts)[:from],
    :to => headers_for(action, opts)[:to],  
    :subject => headers_for(action, opts)[:subject],
    :html => html_output.to_str
end

The next issue I had to do was change the input of the 'action' from a symbol to a string. For instance :confirmation_instructions became 'confirmation_instructions'.

The last issue was that the system was trying to find all of the mailer templates in a membership_mailer folder instead of the devise/mailers/ folder. So, I just moved the devise templates into the new folder and that solved that problem.

And that did it. The new mailer was being called, sent correctly to the Mailgun API, calling the devise mailer templates from the new folder and sending the emails to the user. Hopefully this detailed explanation will help someone in the future.

Here is the final mailer code:

class MembershipMailer < Devise::Mailer   

  helper :application
  include Devise::Controllers::UrlHelpers
  require 'rest-client'

  def mail(record, action, opts = {}, block)
    initialize_from_record(record)
    html_output = render_to_string(:action => action, :layout => false)

    RestClient.post ENV.fetch("MAILGUN_MX_URL"),
      :from => headers_for(action, opts)[:from],
      :to => headers_for(action, opts)[:to],  
      :subject => headers_for(action, opts)[:subject],
      :html => html_output.to_str
  end

  def confirmation_instructions(record, token, opts={})
    @token = token
    mail(record, 'confirmation_instructions', opts)
  end

  def reset_password_instructions(record, token, opts={})
    @token = token
    mail(record, 'reset_password_instructions', opts)
  end

  def unlock_instructions(record, token, opts={})
    @token = token
    mail(record, 'unlock_instructions', opts)
  end

  def email_changed(record, opts={})
    mail(record, 'email_changed', opts)
  end

  def password_change(record, opts={})
    mail(record, 'password_change', opts)
  end
end

Upvotes: 2

Related Questions