Why does my Rails 2 ActionMailer fail intermittently?

92 Views Asked by At

I'm relatively inexperienced and trying to maintain a web app built with Rails 2.3.X and Ruby 1.8.7. Users who create accounts with the app are supposed to receive an automated email with an "activation" link, which is how they confirm that they actually have access to the email address they want to register.

I have found that some, but not all, of my users are getting this activation email. When they don't get the email in their inbox, the email also does not appear in their spam folders.

My suspicion is that the problem has to do with the formatting of the email; maybe some email providers don't like the format and filter it automatically.

I am not sure what information I need to provide to investigate the cause, but here are some places that I thought made sense:

Web form (user can request a new activation link)

<h3>Resend activation link?</h3>
<form action="/send_activation" method="post">
    <input name="authenticity_token" type="hidden" value="<%= form_authenticity_token %>" />
    <p class="field">
    <label for="user_email">Email Address</label>
    <input class="login" id="user_email" name="user[email]" size="45" type="text" />
    </p>
    <div class="clear"></div>  
    <input name="commit" type="submit" value="submit" />
</form>

sessions_controller.rb

def send_activation
  return unless request.post?
  @title = "Send Activation Link"
  @user = User.find_by_email(params[:user][:email])
  if @user && @user.activated_at.blank?
      UserMailer.deliver_signup_notification(@user)
      redirect_to login_url
      flash[:notice] = "Activation link sent." 
  else
      flash[:notice] = "Could not find your email address. Try another." 
  end
end

user_mailer.rb

class UserMailer < ActionMailer::Base

def activation(user)
setup_email(user)
@subject    += 'Your account has been activated!'
@body[:url]  = "http://MYWEBSITE/"
end

integral_mailer.rb

module IntegralMailer
  def perform_delivery_integral_mailer(mail)
    destinations = mail.destinations
    mail.ready_to_send

    helo = smtp_settings[:helo] || "localhost.localdomain"

    ActionMailer::Base::INTEGRAL_MAILER_SERVER.send_mail(helo, mail.from, destinations, mail.encoded)
  end
end

integral_mailer_server.rb

require 'net/smtp'
require 'logger'
require 'fileutils'
require 'mx_lookup'

class IntegralMailerServer
  include FileUtils

  def self.reloadable?; false; end

  def initialize(opts={})
    @times_to_try_busy_host = opts[:times_to_try_busy_host] || 2
    initialize_log( (opts[:age_log_file] || 'daily'), opts[:log_path] )
  end

  def send_mail(helo_domain, from_email, destination_emails, data)
    t = Thread.new do
      destination_emails.each do |dest_email|

        mx_records = MXLookup.for_email(dest_email)

        mx_records.each do |mx_record|
          begin
            do_delivery(mx_record[:host], helo_domain, data, from_email, dest_email) and break
          rescue Net::SMTPFatalError => e
            log_fatal_delivery(dest_email, mx_record[:host], e.message) and break
          rescue Net::SMTPServerBusy => e
            log_delayed_delivery(dest_email, mx_record[:host], e.message)
            # there has to be a better method to use for this
            unless (mx_records.find_all { |r| r == mx_record }).size >= @times_to_try_busy_host
              mx_records << mx_record
            else
              log_fatal_delivery(dest_email, mx_record[:host], e.message)
            end
          rescue => e
            if mx_records.last == mx_record
              log_fatal_delivery(dest_email, mx_record[:host], e.message)
            else
              log_delayed_delivery(dest_email, mx_record[:host], e.message)
            end
          end
        end # mx_records.each

      end # destination_emails.each
    end # Thread.new 
  end # send_mail

  private

  def do_delivery(host, helo, data, from_email, dest_email)
    Net::SMTP.start(host, 25, helo) do |smtp|
      smtp.send_message data, from_email, dest_email
      log_successful_delivery(dest_email, host)
    end; true
  end

  def log_delayed_delivery(email, host, reason)
    msg =  "Message NOT sent to #{email} via #{host}\n  --> #{reason.strip}\n" +
           "  --> However, I'm not giving up just yet."
    @log.warn(msg); msg
  end

  def log_fatal_delivery(email, host, reason)
    msg = "Message NOT sent to #{email} via #{host}\n  --> #{reason.strip}\n" +
          "  --> I'm giving up delivery of this message."
    @log.fatal(msg); msg
  end

  def log_successful_delivery(email, host)
    msg = "Message sent to #{email} via #{host}"
    @log.info(msg); msg
  end

  def initialize_log(age_log_file='daily', log_path=nil)
    if log_path
      @log = Logger.new(log_path, age_log_file)
    else
      if defined?(RAILS_ROOT)
        @log = Logger.new("#{RAILS_ROOT}/log/integral_mailer.log", age_log_file) rescue Logger.new(STDOUT)
      else
        @log = Logger.new(STDOUT)
      end
    end
    @log.info "Initializing"
  end
end
1

There are 1 best solutions below

4
Steven Moffat On

Initially I would confirm the email is being sent each time. Do you have a copy of the LOG showing send success that are reported as failures by users ?

so some form of success count AFTER the send command to a variable, also CC the emails to yourself/ dummy account for inspection. To isolate receipt issues and inspect the headers retrospectivly.

break the procedure down to steps you can confirm instead of or as well as sending an email write a file to the server and inspect its content and formatting.

you need examples of success and examples of failure, testing emails is difficult at best unless you have a copy of the "lost" email.

I have had success using ActionMailer with an external delviery agent consider moving to this ? its simpler and more resilient once you've set up the agent.

Do you have record of the content of the email as well ?