There are still a few issues when using those wonderful plugins.
1. Emailing activation code using a gmail account doesn't work explicitly with the current Rails version 2.2.2. In order to make it work, place the following class named smtp_tsl.rb into the lib folder (This is not my code, I found it in some news group. I don't remember where, sorry.)
require "openssl"
require "net/smtp"
class Net::SMTP
class << self
send :remove_method, :start
attr_accessor :use_tls
end
@use_tls = true
def self.start( address, port = nil,
helo = 'localhost.localdomain',
user = nil, secret = nil, authtype = nil, use_tls = ::Net::SMTP.use_tls,
&block) # :yield: smtp
new(address, port).start(helo, user, secret, authtype, use_tls, &block)
end
alias tls_old_start start
def start( helo = 'localhost.localdomain',
user = nil, secret = nil, authtype = nil, use_tls = ::Net::SMTP.use_tls ) # :yield: smtp
start_method = use_tls ? :do_tls_start : :do_start
if block_given?
begin
send start_method, helo, user, secret, authtype
return yield(self)
ensure
do_finish
end
else
send start_method, helo, user, secret, authtype
return self
end
end
private
def do_tls_start(helodomain, user, secret, authtype)
raise IOError, 'SMTP session already started' if @started
check_auth_args user, secret, authtype if user or secret
sock = timeout(@open_timeout) { TCPSocket.open(@address, @port) }
@socket = Net::InternetMessageIO.new(sock)
@socket.read_timeout = 60 #@read_timeout
@socket.debug_output = @debug_output
check_response(critical { recv_response() })
do_helo(helodomain)
raise 'openssl library not installed' unless defined?(OpenSSL)
starttls
ssl = OpenSSL::SSL::SSLSocket.new(sock)
ssl.sync_close = true
ssl.connect
@socket = Net::InternetMessageIO.new(ssl)
@socket.read_timeout = 60 #@read_timeout
@socket.debug_output = @debug_output
do_helo(helodomain)
authenticate user, secret, authtype if user
@started = true
ensure
unless @started
# authentication failed, cancel connection.
@socket.close if not @started and @socket and not @socket.closed?
@socket = nil
end
end
def do_helo(helodomain)
begin
if @esmtp
ehlo helodomain
else
helo helodomain
end
rescue Net::ProtocolError
if @esmtp
@esmtp = false
@error_occured = false
retry
end
raise
end
end
def starttls
getok('STARTTLS')
end
alias tls_old_quit quit
def quit
begin
getok('QUIT')
rescue EOFError
end
end
end unless Net::SMTP.private_method_defined? :do_tls_start or
Net::SMTP.method_defined? :tls?
Now, in order to configure your ActionMailer, create a file email.rb in your config/initializers and place the following code into it:
require 'smtp_tls'
ActionMailer::Base.delivery_method = :smtp
ActionMailer::Base.smtp_settings = {
:address => "smtp.gmail.com",
:port => 587,
:domain => "gmail.com",
:authentication => :plain,
:user_name => "<mygmailaccount>@gmail.com",
:password => "<mypassword>"
}
ActionMailer::Base.perform_deliveries = true
ActionMailer::Base.raise_delivery_errors = true
ActionMailer::Base.default_charset = "utf-8"
2. acts_as_state_machine assigns the activation code twice. As a result, the activation code saved in the database is not the same as the activation code sent to the user... Logically, the initial state of the state machine should be "passive", not "pending" and then the activation code is assigned once... except the machine doesn't change the state to "pending" for some reason. The hack I found on the internet is to reload the object from the database before sending an email... except, it nukes the object associations. My own hack is provided below, replace make_activation_code function in the model with the following one: def make_activation_code
unless self.state == 'pending'
self.deleted_at = nil
self.activation_code = Digest::SHA1.hexdigest( Time.now.to_s.split(//).sort_by {rand}.join )
end
end
3. I have implemented a couple of functions of my own:
a. resend activation code
This was trivial
b. resend password if forgotten
The user account in this case should be active, i.e. the state machine makes a self-transition from the active state to the same. In order to be able to execute code on transitions, I used the following enhancement of the acts_as_state_machine plugin (big thanks to this guy).
Also, I created 2 extra fields in the model - passreset_code and passreset_at. Once the user requests to resend the password, passreset_code is created and sent to them. Once the user clicks on the link with this code, they're presented with a form to reset the password. Once they update the password, the code is cleared.
In order to implement this, add the following transitions to the model:
event :resetpassword do
transitions :from => :active, :to => :active, :on_transition => :make_passreset_code
end
event :updatepassword do
transitions :from => :active, :to => :active, :on_transition => :do_update_password
end
and the following protected methods:
def make_passreset_code
self.passreset_code = Digest::SHA1.hexdigest( Time.now.to_s.split(//).sort_by {rand}.join )
end
def do_update_password
self.passreset_at = Time.now.utc
self.passreset_code = nil
end