diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 5c82269ff29781b69d8f6d547d1c88b9222add69..2fce8c079a5635a71ed54100f4a2ef9b528c8476 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -48,6 +48,14 @@ class UsersController < ApplicationController else flash[:error] = I18n.t 'users.update.language_not_changed' end + elsif u[:email] + @user.unconfirmed_email = u[:email] + if @user.save + @user.mail_confirm_email + flash[:notice] = I18n.t 'users.update.unconfirmed_email_changed' + else + flash[:error] = I18n.t 'users.update.unconfirmed_email_not_changed' + end end end @@ -126,4 +134,13 @@ class UsersController < ApplicationController tar_path = PhotoMover::move_photos(current_user) send_data( File.open(tar_path).read, :filename => "#{current_user.id}.tar" ) end + + def confirm_email + if current_user.confirm_email(params[:token]) + flash[:notice] = I18n.t('users.confirm_email.email_confirmed', :email => current_user.email) + elsif current_user.unconfirmed_email.present? + flash[:error] = I18n.t('users.confirm_email.email_not_confirmed') + end + redirect_to edit_user_path + end end diff --git a/app/mailers/notifier.rb b/app/mailers/notifier.rb index 99d9e2b1e1a8d469c71b0723ec51e899b6c08beb..a1b58452a74d6b4559cae9e46c2d31849e6e8bda 100644 --- a/app/mailers/notifier.rb +++ b/app/mailers/notifier.rb @@ -113,6 +113,16 @@ class Notifier < ActionMailer::Base end end + def confirm_email(receiver_id) + @receiver = User.find_by_id(receiver_id) + + I18n.with_locale(@receiver.language) do + mail(:to => "\"#{@receiver.name}\" <#{@receiver.unconfirmed_email}>", + :subject => I18n.t('notifier.confirm_email.subject', :unconfirmed_email => @receiver.unconfirmed_email), + :host => AppConfig[:pod_uri].host) + end + end + private def log_mail recipient_id, sender_id, type log_string = "event=mail mail_type=#{type} recipient_id=#{recipient_id} sender_id=#{sender_id}" diff --git a/app/models/jobs/mail_confirm_email.rb b/app/models/jobs/mail_confirm_email.rb new file mode 100644 index 0000000000000000000000000000000000000000..6c8bc3248057994a68a37cd3b67b9c6db860edec --- /dev/null +++ b/app/models/jobs/mail_confirm_email.rb @@ -0,0 +1,8 @@ +module Job + class MailConfirmEmail < Base + @queue = :mail + def self.perform_delegate(user_id) + Notifier.confirm_email(user_id).deliver + end + end +end \ No newline at end of file diff --git a/app/models/user.rb b/app/models/user.rb index f8c2ecf47b5fc03f3aa4b0920b86452e2ed05b4f..d75b91f4d840e35da414616a86e812e52b6359d9 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -24,6 +24,7 @@ class User < ActiveRecord::Base validates_format_of :username, :with => /\A[A-Za-z0-9_]+\z/ validates_length_of :username, :maximum => 32 validates_inclusion_of :language, :in => AVAILABLE_LANGUAGE_CODES + validates_format_of :unconfirmed_email, :with => Devise.email_regexp, :allow_blank => true validates_presence_of :person, :unless => proc {|user| user.invitation_token.present?} validates_associated :person @@ -48,6 +49,7 @@ class User < ActiveRecord::Base before_save do person.save if person && person.changed? end + before_save :guard_unconfirmed_email attr_accessible :getting_started, :password, :password_confirmation, :language, :disable_mail @@ -101,6 +103,12 @@ class User < ActiveRecord::Base true end + def confirm_email(token) + return false if token.blank? || token != confirm_email_token + self.email = unconfirmed_email + save + end + ######### Aspects ###################### def move_contact(person, to_aspect, from_aspect) @@ -214,6 +222,12 @@ class User < ActiveRecord::Base end end + def mail_confirm_email + return false if unconfirmed_email.blank? + Resque.enqueue(Job::MailConfirmEmail, id) + true + end + ######### Posts and Such ############### def retract(target) if target.respond_to?(:relayable?) && target.relayable? @@ -365,4 +379,12 @@ class User < ActiveRecord::Base def remove_mentions Mention.where( :person_id => self.person.id).delete_all end + + def guard_unconfirmed_email + self.unconfirmed_email = nil if unconfirmed_email.blank? || unconfirmed_email == email + + if unconfirmed_email_changed? + self.confirm_email_token = unconfirmed_email ? ActiveSupport::SecureRandom.hex(15) : nil + end + end end diff --git a/app/views/notifier/confirm_email.html.haml b/app/views/notifier/confirm_email.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..eb4f905e2a6077de687476b0fbe189e34deb0258 --- /dev/null +++ b/app/views/notifier/confirm_email.html.haml @@ -0,0 +1,6 @@ +%p + = t('notifier.hello', :name => @receiver.profile.first_name) +%p + != t('notifier.confirm_email.click_link', :unconfirmed_email => @receiver.unconfirmed_email) + %br + = link_to confirm_email_url(:token => @receiver.confirm_email_token), confirm_email_url(:token => @receiver.confirm_email_token) diff --git a/app/views/notifier/confirm_email.text.haml b/app/views/notifier/confirm_email.text.haml new file mode 100644 index 0000000000000000000000000000000000000000..8ea9b8fef2d2dc03da454420c6fed73d9b056f85 --- /dev/null +++ b/app/views/notifier/confirm_email.text.haml @@ -0,0 +1,4 @@ +!= t('notifier.hello', :name => @receiver.profile.first_name) + +!= t('notifier.confirm_email.click_link', :unconfirmed_email => @receiver.unconfirmed_email) +!= confirm_email_url(:token => @receiver.confirm_email_token) diff --git a/app/views/users/edit.html.haml b/app/views/users/edit.html.haml index 4b0fee89a35e688fce08e6f1d6242ddf09582ca1..ee3f8ecc4d575e3c69926d137d78985d592fe3f5 100644 --- a/app/views/users/edit.html.haml +++ b/app/views/users/edit.html.haml @@ -19,8 +19,15 @@ .span-5.last %h3 = t('.your_email') - %p - = current_user.email + = form_for 'user', :url => user_path, :html => { :method => :put } do |f| + = f.error_messages + %p + = f.text_field :email, :value => @user.unconfirmed_email || @user.email + = f.submit t('.change_email') + %br + - if @user.unconfirmed_email.present? + %p= t('.email_awaiting_confirmation', :email => @user.email, :unconfirmed_email => @user.unconfirmed_email) + %br %br %br diff --git a/config/locales/diaspora/en.yml b/config/locales/diaspora/en.yml index cf1a843d48815bab7d86df9432dc9d8a84a8e335..b08d091269585713628a1e3f4f4e932a6755ace8 100644 --- a/config/locales/diaspora/en.yml +++ b/config/locales/diaspora/en.yml @@ -420,7 +420,9 @@ en: liked: liked: "%{name} just liked your post" view_post: "View post >" - + confirm_email: + subject: "Please activate your new e-mail address %{unconfirmed_email}" + click_link: "To activate your new e-mail address %{unconfirmed_email}, please click this link:" people: zero: "no people" one: "1 person" @@ -723,6 +725,7 @@ en: close_account: "Close Account" change_language: "Change Language" change_password: "Change Password" + change_email: "Change E-Mail" new_password: "New Password" current_password: "Current password" download_xml: "download my xml" @@ -738,6 +741,7 @@ en: private_message: "...you receive a private message?" liked: "...someone likes your post?" change: "Change" + email_awaiting_confirmation: "We have sent you an activation link to %{unconfirmed_email}. Till you follow this link and activate the new address, we will continue to use your original address %{email}." destroy: "Your account has been locked. It may take 20 minutes for us to finish closing your account. Thank you for trying Diaspora." getting_started: welcome: "Welcome to Diaspora!" @@ -761,8 +765,13 @@ en: language_changed: "Language Changed" language_not_changed: "Language Change Failed" email_notifications_changed: "Email notifications changed" + unconfirmed_email_changed: "E-Mail Changed. Needs activation." + unconfirmed_email_not_changed: "E-Mail Change Failed" public: does_not_exist: "User %{username} does not exist!" + confirm_email: + email_confirmed: "E-Mail %{email} activated" + email_not_confirmed: "E-Mail could not be activated. Wrong link?" webfinger: fetch_failed: "failed to fetch webfinger profile for %{profile_url}" diff --git a/config/routes.rb b/config/routes.rb index bca90dd5c596403cea5404243e31ad3ac3c46d13..9b792f21e4966bae7a3db079f019e6fa12ea6926 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -68,6 +68,7 @@ Diaspora::Application.routes.draw do get 'public/:username' => :public, :as => 'users_public' match 'getting_started' => :getting_started, :as => 'getting_started' get 'getting_started_completed' => :getting_started_completed + get 'confirm_email/:token' => :confirm_email, :as => 'confirm_email' end # This is a hack to overide a route created by devise. diff --git a/db/migrate/20110601083310_add_unconfirmed_email_to_users.rb b/db/migrate/20110601083310_add_unconfirmed_email_to_users.rb new file mode 100644 index 0000000000000000000000000000000000000000..9c295e557006d9b8d5d68bbbac3c9339002272db --- /dev/null +++ b/db/migrate/20110601083310_add_unconfirmed_email_to_users.rb @@ -0,0 +1,9 @@ +class AddUnconfirmedEmailToUsers < ActiveRecord::Migration + def self.up + add_column :users, :unconfirmed_email, :string, :default => nil, :null => true + end + + def self.down + remove_column :users, :unconfirmed_email + end +end diff --git a/db/migrate/20110601091059_add_confirm_email_token_to_users.rb b/db/migrate/20110601091059_add_confirm_email_token_to_users.rb new file mode 100644 index 0000000000000000000000000000000000000000..4c71e97b2368a77c5b315939b418a3f30baeca5c --- /dev/null +++ b/db/migrate/20110601091059_add_confirm_email_token_to_users.rb @@ -0,0 +1,9 @@ +class AddConfirmEmailTokenToUsers < ActiveRecord::Migration + def self.up + add_column :users, :confirm_email_token, :string, :limit => 30 + end + + def self.down + remove_column :users, :confirm_email_token + end +end diff --git a/db/schema.rb b/db/schema.rb index da2eb61fc25f4a53c388eefef8f10e0fafab5483..a8ddf606d9a5c91dc230d253915d623992d19898 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -391,6 +391,8 @@ ActiveRecord::Schema.define(:version => 20110707234802) do t.integer "invited_by_id" t.string "invited_by_type" t.string "authentication_token", :limit => 30 + t.string "unconfirmed_email" + t.string "confirm_email_token", :limit => 30 t.datetime "locked_at" end diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index 440809bfee2e320136e723521bc94e1763c245ee..9c4090967796810ee110c2f2b907ab41b2b764f0 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -89,6 +89,41 @@ describe UsersController do end end + describe 'email' do + before do + Resque.stub!(:enqueue) + end + + it 'allow the user to change his (unconfirmed) email' do + put(:update, :id => @user.id, :user => { :email => "my@newemail.com"}) + @user.reload + @user.unconfirmed_email.should eql("my@newemail.com") + end + + it 'informs the user about success' do + put(:update, :id => @user.id, :user => { :email => "my@newemail.com"}) + request.flash[:notice].should eql(I18n.t('users.update.unconfirmed_email_changed')) + request.flash[:error].should be_blank + end + + it 'informs the user about failure' do + put(:update, :id => @user.id, :user => { :email => "my@newemailcom"}) + request.flash[:error].should eql(I18n.t('users.update.unconfirmed_email_not_changed')) + request.flash[:notice].should be_blank + end + + it 'allow the user to change his (unconfirmed) email to blank (= abort confirmation)' do + put(:update, :id => @user.id, :user => { :email => ""}) + @user.reload + @user.unconfirmed_email.should eql(nil) + end + + it 'sends out activation email on success' do + Resque.should_receive(:enqueue).with(Job::MailConfirmEmail, @user.id).once + put(:update, :id => @user.id, :user => { :email => "my@newemail.com"}) + end + end + describe 'email settings' do it 'lets the user turn off mail' do par = {:id => @user.id, :user => {:email_preferences => {'mentioned' => 'true'}}} @@ -138,4 +173,31 @@ describe UsersController do alice.reload.access_locked?.should be_true end end + + describe '#confirm_email' do + before do + @user.update_attribute(:unconfirmed_email, 'my@newemail.com') + end + + it 'redirects to to the user edit page' do + get 'confirm_email', :token => @user.confirm_email_token + response.should redirect_to edit_user_path + end + + it 'confirms email' do + get 'confirm_email', :token => @user.confirm_email_token + @user.reload + @user.email.should eql('my@newemail.com') + request.flash[:notice].should eql(I18n.t('users.confirm_email.email_confirmed', :email => 'my@newemail.com')) + request.flash[:error].should be_blank + end + + it 'does NOT confirm email with wrong token' do + get 'confirm_email', :token => @user.confirm_email_token.reverse + @user.reload + @user.email.should_not eql('my@newemail.com') + request.flash[:error].should eql(I18n.t('users.confirm_email.email_not_confirmed')) + request.flash[:notice].should be_blank + end + end end diff --git a/spec/mailers/notifier_spec.rb b/spec/mailers/notifier_spec.rb index f585b7841bca013cf8e112a7a4a3e23eda815cf7..f11b67ea14478a428e59d8f7b3e5058f1c690f84 100644 --- a/spec/mailers/notifier_spec.rb +++ b/spec/mailers/notifier_spec.rb @@ -46,7 +46,7 @@ describe Notifier do end it 'has the layout' do - + mail = Notifier.single_admin("Welcome to bureaucracy!", user) mail.body.encoded.should match /change your notification settings/ end @@ -217,5 +217,33 @@ describe Notifier do end end end + + describe ".confirm_email" do + before do + user.update_attribute(:unconfirmed_email, "my@newemail.com") + end + + let!(:confirm_email) { Notifier.confirm_email(user.id) } + + it 'goes to the right person' do + confirm_email.to.should == [user.unconfirmed_email] + end + + it 'has the unconfirmed emil in the subject' do + confirm_email.subject.should include(user.unconfirmed_email) + end + + it 'has the unconfirmed emil in the body' do + confirm_email.body.encoded.should include(user.unconfirmed_email) + end + + it 'has the receivers name in the body' do + confirm_email.body.encoded.should include(user.person.profile.first_name) + end + + it 'has the activation link in the body' do + confirm_email.body.encoded.should include(confirm_email_url(:token => user.confirm_email_token)) + end + end end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index f95c68d01cc5726f3f643f56398585e66fbfd1d7..b34d92e3d44e42da0f721a7475fadd8c9e5a80f9 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -114,6 +114,31 @@ describe User do alice.email = eve.email alice.should_not be_valid end + + it "requires a vaild email address" do + alice.email = "somebody@anywhere" + alice.should_not be_valid + end + end + + describe "of unconfirmed_email" do + it "unconfirmed_email address can be nil/blank" do + alice.unconfirmed_email = nil + alice.should be_valid + alice.unconfirmed_email = "" + alice.should be_valid + end + + it "does NOT require a unique unconfirmed_email address" do + eve.update_attribute :unconfirmed_email, "new@email.com" + alice.unconfirmed_email = "new@email.com" + alice.should be_valid + end + + it "requires a vaild unconfirmed_email address" do + alice.unconfirmed_email = "somebody@anywhere" + alice.should_not be_valid + end end describe "of language" do @@ -595,4 +620,136 @@ describe User do end end end + + context 'change email' do + let(:user){ alice } + + describe "#unconfirmed_email" do + it "is nil by default" do + user.unconfirmed_email.should eql(nil) + end + + it "forces blank to nil" do + user.unconfirmed_email = "" + user.save! + user.unconfirmed_email.should eql(nil) + end + + it "is ignored if it equals email" do + user.unconfirmed_email = user.email + user.save! + user.unconfirmed_email.should eql(nil) + end + + it "allows change to valid new email" do + user.unconfirmed_email = "alice@newmail.com" + user.save! + user.unconfirmed_email.should eql("alice@newmail.com") + end + end + + describe "#confirm_email_token" do + it "is nil by default" do + user.confirm_email_token.should eql(nil) + end + + it "is autofilled when unconfirmed_email is set to new email" do + user.unconfirmed_email = "alice@newmail.com" + user.save! + user.confirm_email_token.should_not be_blank + user.confirm_email_token.size.should eql(30) + end + + it "is set back to nil when unconfirmed_email is empty" do + user.unconfirmed_email = "alice@newmail.com" + user.save! + user.confirm_email_token.should_not be_blank + user.unconfirmed_email = nil + user.save! + user.confirm_email_token.should eql(nil) + end + + it "generates new token on every new unconfirmed_email" do + user.unconfirmed_email = "alice@newmail.com" + user.save! + first_token = user.confirm_email_token + user.unconfirmed_email = "alice@andanotherone.com" + user.save! + user.confirm_email_token.should_not eql(first_token) + user.confirm_email_token.size.should eql(30) + end + end + + describe '#mail_confirm_email' do + it 'enqueues a mail job on user with unconfirmed email' do + user.update_attribute(:unconfirmed_email, "alice@newmail.com") + Resque.should_receive(:enqueue).with(Job::MailConfirmEmail, alice.id).once + alice.mail_confirm_email.should eql(true) + end + + it 'enqueues NO mail job on user without unconfirmed email' do + Resque.should_not_receive(:enqueue).with(Job::MailConfirmEmail, alice.id) + alice.mail_confirm_email.should eql(false) + end + end + + describe '#confirm_email' do + context 'on user with unconfirmed email' do + before do + user.update_attribute(:unconfirmed_email, "alice@newmail.com") + end + + it 'confirms email and set the unconfirmed_email to email on valid token' do + user.confirm_email(user.confirm_email_token).should eql(true) + user.email.should eql("alice@newmail.com") + user.unconfirmed_email.should eql(nil) + user.confirm_email_token.should eql(nil) + end + + it 'returns false and does not change anything on wrong token' do + user.confirm_email(user.confirm_email_token.reverse).should eql(false) + user.email.should_not eql("alice@newmail.com") + user.unconfirmed_email.should_not eql(nil) + user.confirm_email_token.should_not eql(nil) + end + + it 'returns false and does not change anything on blank token' do + user.confirm_email("").should eql(false) + user.email.should_not eql("alice@newmail.com") + user.unconfirmed_email.should_not eql(nil) + user.confirm_email_token.should_not eql(nil) + end + + it 'returns false and does not change anything on blank token' do + user.confirm_email(nil).should eql(false) + user.email.should_not eql("alice@newmail.com") + user.unconfirmed_email.should_not eql(nil) + user.confirm_email_token.should_not eql(nil) + end + end + + context 'on user without unconfirmed email' do + it 'returns false and does not change anything on any token' do + user.confirm_email("12345"*6).should eql(false) + user.email.should_not eql("alice@newmail.com") + user.unconfirmed_email.should eql(nil) + user.confirm_email_token.should eql(nil) + end + + it 'returns false and does not change anything on blank token' do + user.confirm_email("").should eql(false) + user.email.should_not eql("alice@newmail.com") + user.unconfirmed_email.should eql(nil) + user.confirm_email_token.should eql(nil) + end + + it 'returns false and does not change anything on blank token' do + user.confirm_email(nil).should eql(false) + user.email.should_not eql("alice@newmail.com") + user.unconfirmed_email.should eql(nil) + user.confirm_email_token.should eql(nil) + end + end + end + end end