summaryrefslogtreecommitdiffstats
path: root/app
diff options
context:
space:
mode:
authorJean-Philippe Lang <jp_lang@yahoo.fr>2015-01-17 14:14:12 +0000
committerJean-Philippe Lang <jp_lang@yahoo.fr>2015-01-17 14:14:12 +0000
commite3618bdbecd9b5d86eb6d2c8c256ba3fcdf05189 (patch)
tree3b54c10eecece6cc2674491a76a4e5e932d82d1e /app
parent7f29c2fd88f271ac59f1c10b90942fec57b35ae2 (diff)
downloadredmine-e3618bdbecd9b5d86eb6d2c8c256ba3fcdf05189.tar.gz
redmine-e3618bdbecd9b5d86eb6d2c8c256ba3fcdf05189.zip
Add support for multiple email addresses per user (#4244).
git-svn-id: http://svn.redmine.org/redmine/trunk@13886 e93f8b46-1217-0410-a6f0-8f06a7374b81
Diffstat (limited to 'app')
-rw-r--r--app/controllers/email_addresses_controller.rb105
-rw-r--r--app/controllers/users_controller.rb2
-rw-r--r--app/helpers/email_addresses_helper.rb38
-rw-r--r--app/helpers/users_helper.rb6
-rw-r--r--app/models/document.rb4
-rw-r--r--app/models/email_address.rb54
-rw-r--r--app/models/mail_handler.rb2
-rw-r--r--app/models/mailer.rb71
-rw-r--r--app/models/message.rb4
-rw-r--r--app/models/news.rb23
-rw-r--r--app/models/principal.rb12
-rw-r--r--app/models/user.rb54
-rw-r--r--app/models/wiki_content.rb8
-rw-r--r--app/views/email_addresses/_index.html.erb26
-rw-r--r--app/views/email_addresses/index.html.erb2
-rw-r--r--app/views/email_addresses/index.js.erb3
-rw-r--r--app/views/my/account.html.erb1
-rw-r--r--app/views/settings/_authentication.html.erb2
-rw-r--r--app/views/users/edit.html.erb1
19 files changed, 369 insertions, 49 deletions
diff --git a/app/controllers/email_addresses_controller.rb b/app/controllers/email_addresses_controller.rb
new file mode 100644
index 000000000..373be00a0
--- /dev/null
+++ b/app/controllers/email_addresses_controller.rb
@@ -0,0 +1,105 @@
+# Redmine - project management software
+# Copyright (C) 2006-2015 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class EmailAddressesController < ApplicationController
+ before_filter :find_user, :require_admin_or_current_user
+ before_filter :find_email_address, :only => [:update, :destroy]
+
+ def index
+ @addresses = @user.email_addresses.order(:id).where(:is_default => false).to_a
+ @address ||= EmailAddress.new
+ end
+
+ def create
+ saved = false
+ if @user.email_addresses.count <= Setting.max_additional_emails.to_i
+ @address = EmailAddress.new(:user => @user, :is_default => false)
+ attrs = params[:email_address]
+ if attrs.is_a?(Hash)
+ @address.address = attrs[:address].to_s
+ end
+ saved = @address.save
+ end
+
+ respond_to do |format|
+ format.html {
+ if saved
+ redirect_to user_email_addresses_path(@user)
+ else
+ index
+ render :action => 'index'
+ end
+ }
+ format.js {
+ @address = nil if saved
+ index
+ render :action => 'index'
+ }
+ end
+ end
+
+ def update
+ if params[:notify].present?
+ @address.notify = params[:notify].to_s
+ end
+ @address.save
+
+ respond_to do |format|
+ format.html {
+ redirect_to user_email_addresses_path(@user)
+ }
+ format.js {
+ @address = nil
+ index
+ render :action => 'index'
+ }
+ end
+ end
+
+ def destroy
+ @address.destroy
+
+ respond_to do |format|
+ format.html {
+ redirect_to user_email_addresses_path(@user)
+ }
+ format.js {
+ @address = nil
+ index
+ render :action => 'index'
+ }
+ end
+ end
+
+ private
+
+ def find_user
+ @user = User.find(params[:user_id])
+ end
+
+ def find_email_address
+ @address = @user.email_addresses.where(:is_default => false).find(params[:id])
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+
+ def require_admin_or_current_user
+ unless @user == User.current
+ require_admin
+ end
+ end
+end
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index b57ee8deb..5958e36f8 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -41,7 +41,7 @@ class UsersController < ApplicationController
@status = params[:status] || 1
- scope = User.logged.status(@status)
+ scope = User.logged.status(@status).preload(:email_address)
scope = scope.like(params[:name]) if params[:name].present?
scope = scope.in_group(params[:group_id]) if params[:group_id].present?
diff --git a/app/helpers/email_addresses_helper.rb b/app/helpers/email_addresses_helper.rb
new file mode 100644
index 000000000..f397e9907
--- /dev/null
+++ b/app/helpers/email_addresses_helper.rb
@@ -0,0 +1,38 @@
+# encoding: utf-8
+#
+# Redmine - project management software
+# Copyright (C) 2006-2015 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module EmailAddressesHelper
+
+ # Returns a link to enable or disable notifications for the address
+ def toggle_email_address_notify_link(address)
+ if address.notify?
+ link_to image_tag('email.png'),
+ user_email_address_path(address.user, address, :notify => '0'),
+ :method => :put,
+ :title => l(:label_disable_notifications),
+ :remote => true
+ else
+ link_to image_tag('email_disabled.png'),
+ user_email_address_path(address.user, address, :notify => '1'),
+ :method => :put,
+ :title => l(:label_enable_notifications),
+ :remote => true
+ end
+ end
+end
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index 1bd1d9a91..2ba41fa62 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -42,6 +42,12 @@ module UsersHelper
end
end
+ def additional_emails_link(user)
+ if user.email_addresses.count > 1 || Setting.max_additional_emails.to_i > 0
+ link_to l(:label_email_address_plural), user_email_addresses_path(@user), :class => 'icon icon-email-add', :remote => true
+ end
+ end
+
def user_settings_tabs
tabs = [{:name => 'general', :partial => 'users/general', :label => :label_general},
{:name => 'memberships', :partial => 'users/memberships', :label => :label_project_plural}
diff --git a/app/models/document.rb b/app/models/document.rb
index f73f8ed63..29f9031fc 100644
--- a/app/models/document.rb
+++ b/app/models/document.rb
@@ -60,6 +60,10 @@ class Document < ActiveRecord::Base
@updated_on
end
+ def notified_users
+ project.notified_users.reject {|user| !visible?(user)}
+ end
+
private
def send_notification
diff --git a/app/models/email_address.rb b/app/models/email_address.rb
new file mode 100644
index 000000000..01fd75b1a
--- /dev/null
+++ b/app/models/email_address.rb
@@ -0,0 +1,54 @@
+# Redmine - project management software
+# Copyright (C) 2006-2015 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class EmailAddress < ActiveRecord::Base
+ belongs_to :user
+ attr_protected :id
+
+ after_update :destroy_tokens
+ after_destroy :destroy_tokens
+
+ validates_presence_of :address
+ validates_format_of :address, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, :allow_blank => true
+ validates_length_of :address, :maximum => User::MAIL_LENGTH_LIMIT, :allow_nil => true
+ validates_uniqueness_of :address, :case_sensitive => false,
+ :if => Proc.new {|email| email.address_changed? && email.address.present?}
+
+ def address=(arg)
+ write_attribute(:address, arg.to_s.strip)
+ end
+
+ def destroy
+ if is_default?
+ false
+ else
+ super
+ end
+ end
+
+ private
+
+ # Delete all outstanding password reset tokens on email change.
+ # This helps to keep the account secure in case the associated email account
+ # was compromised.
+ def destroy_tokens
+ if address_changed? || destroyed?
+ tokens = ['recovery']
+ Token.where(:user_id => user_id, :action => tokens).delete_all
+ end
+ end
+end
diff --git a/app/models/mail_handler.rb b/app/models/mail_handler.rb
index cff50e6af..825fdf546 100644
--- a/app/models/mail_handler.rb
+++ b/app/models/mail_handler.rb
@@ -306,7 +306,7 @@ class MailHandler < ActionMailer::Base
if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
unless addresses.empty?
- User.active.where('LOWER(mail) IN (?)', addresses).each do |w|
+ User.active.having_mail(addresses).each do |w|
obj.add_watcher(w)
end
end
diff --git a/app/models/mailer.rb b/app/models/mailer.rb
index bc279b691..a859c039b 100644
--- a/app/models/mailer.rb
+++ b/app/models/mailer.rb
@@ -39,8 +39,8 @@ class Mailer < ActionMailer::Base
@issue = issue
@users = to_users + cc_users
@issue_url = url_for(:controller => 'issues', :action => 'show', :id => issue)
- mail :to => to_users.map(&:mail),
- :cc => cc_users.map(&:mail),
+ mail :to => to_users,
+ :cc => cc_users,
:subject => "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] (#{issue.status.name}) #{issue.subject}"
end
@@ -71,8 +71,8 @@ class Mailer < ActionMailer::Base
@journal = journal
@journal_details = journal.visible_details(@users.first)
@issue_url = url_for(:controller => 'issues', :action => 'show', :id => issue, :anchor => "change-#{journal.id}")
- mail :to => to_users.map(&:mail),
- :cc => cc_users.map(&:mail),
+ mail :to => to_users,
+ :cc => cc_users,
:subject => s
end
@@ -95,7 +95,7 @@ class Mailer < ActionMailer::Base
@issues_url = url_for(:controller => 'issues', :action => 'index',
:set_filter => 1, :assigned_to_id => user.id,
:sort => 'due_date:asc')
- mail :to => user.mail,
+ mail :to => user,
:subject => l(:mail_subject_reminder, :count => issues.size, :days => days)
end
@@ -109,7 +109,7 @@ class Mailer < ActionMailer::Base
@author = User.current
@document = document
@document_url = url_for(:controller => 'documents', :action => 'show', :id => document)
- mail :to => document.recipients,
+ mail :to => document.notified_users,
:subject => "[#{document.project.name}] #{l(:label_document_new)}: #{document.title}"
end
@@ -127,15 +127,15 @@ class Mailer < ActionMailer::Base
when 'Project'
added_to_url = url_for(:controller => 'files', :action => 'index', :project_id => container)
added_to = "#{l(:label_project)}: #{container}"
- recipients = container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}.collect {|u| u.mail}
+ recipients = container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}
when 'Version'
added_to_url = url_for(:controller => 'files', :action => 'index', :project_id => container.project)
added_to = "#{l(:label_version)}: #{container.name}"
- recipients = container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}.collect {|u| u.mail}
+ recipients = container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}
when 'Document'
added_to_url = url_for(:controller => 'documents', :action => 'show', :id => container.id)
added_to = "#{l(:label_document)}: #{container.title}"
- recipients = container.recipients
+ recipients = container.notified_users
end
redmine_headers 'Project' => container.project.identifier
@attachments = attachments
@@ -157,8 +157,8 @@ class Mailer < ActionMailer::Base
references news
@news = news
@news_url = url_for(:controller => 'news', :action => 'show', :id => news)
- mail :to => news.recipients,
- :cc => news.cc_for_added_news,
+ mail :to => news.notified_users,
+ :cc => news.notified_watchers_for_added_news,
:subject => "[#{news.project.name}] #{l(:label_news)}: #{news.title}"
end
@@ -176,8 +176,8 @@ class Mailer < ActionMailer::Base
@news = news
@comment = comment
@news_url = url_for(:controller => 'news', :action => 'show', :id => news)
- mail :to => news.recipients,
- :cc => news.watcher_recipients,
+ mail :to => news.notified_users,
+ :cc => news.notified_watchers,
:subject => "Re: [#{news.project.name}] #{l(:label_news)}: #{news.title}"
end
@@ -192,8 +192,8 @@ class Mailer < ActionMailer::Base
@author = message.author
message_id message
references message.root
- recipients = message.recipients
- cc = ((message.root.watcher_recipients + message.board.watcher_recipients).uniq - recipients)
+ recipients = message.notified_users
+ cc = ((message.root.notified_watchers + message.board.notified_watchers).uniq - recipients)
@message = message
@message_url = url_for(message.event_url)
mail :to => recipients,
@@ -211,8 +211,8 @@ class Mailer < ActionMailer::Base
'Wiki-Page-Id' => wiki_content.page.id
@author = wiki_content.author
message_id wiki_content
- recipients = wiki_content.recipients
- cc = wiki_content.page.wiki.watcher_recipients - recipients
+ recipients = wiki_content.notified_users
+ cc = wiki_content.page.wiki.notified_watchers - recipients
@wiki_content = wiki_content
@wiki_content_url = url_for(:controller => 'wiki', :action => 'show',
:project_id => wiki_content.project,
@@ -232,8 +232,8 @@ class Mailer < ActionMailer::Base
'Wiki-Page-Id' => wiki_content.page.id
@author = wiki_content.author
message_id wiki_content
- recipients = wiki_content.recipients
- cc = wiki_content.page.wiki.watcher_recipients + wiki_content.page.watcher_recipients - recipients
+ recipients = wiki_content.notified_users
+ cc = wiki_content.page.wiki.notified_watchers + wiki_content.page.notified_watchers - recipients
@wiki_content = wiki_content
@wiki_content_url = url_for(:controller => 'wiki', :action => 'show',
:project_id => wiki_content.project,
@@ -267,7 +267,7 @@ class Mailer < ActionMailer::Base
# Mailer.account_activation_request(user).deliver => sends an email to all active administrators
def account_activation_request(user)
# Send the email to all active administrators
- recipients = User.active.where(:admin => true).collect { |u| u.mail }.compact
+ recipients = User.active.where(:admin => true)
@user = user
@url = url_for(:controller => 'users', :action => 'index',
:status => User::STATUS_REGISTERED,
@@ -378,12 +378,20 @@ class Mailer < ActionMailer::Base
'From' => Setting.mail_from,
'List-Id' => "<#{Setting.mail_from.to_s.gsub('@', '.')}>"
+ # Replaces users with their email addresses
+ [:to, :cc, :bcc].each do |key|
+ if headers[key].present?
+ headers[key] = self.class.email_addresses(headers[key])
+ end
+ end
+
# Removes the author from the recipients and cc
# if the author does not want to receive notifications
# about what the author do
if @author && @author.logged? && @author.pref.no_self_notified
- headers[:to].delete(@author.mail) if headers[:to].is_a?(Array)
- headers[:cc].delete(@author.mail) if headers[:cc].is_a?(Array)
+ addresses = @author.mails
+ headers[:to] -= addresses if headers[:to].is_a?(Array)
+ headers[:cc] -= addresses if headers[:cc].is_a?(Array)
end
if @author && @author.logged?
@@ -447,6 +455,25 @@ class Mailer < ActionMailer::Base
end
end
+ # Returns an array of email addresses to notify by
+ # replacing users in arg with their notified email addresses
+ #
+ # Example:
+ # Mailer.email_addresses(users)
+ # => ["foo@example.net", "bar@example.net"]
+ def self.email_addresses(arg)
+ arr = Array.wrap(arg)
+ mails = arr.reject {|a| a.is_a? Principal}
+ users = arr - mails
+ if users.any?
+ mails += EmailAddress.
+ where(:user_id => users.map(&:id)).
+ where("is_default = ? OR notify = ?", true, true).
+ pluck(:address)
+ end
+ mails
+ end
+
private
# Appends a Redmine header field (name is prepended with 'X-Redmine-')
diff --git a/app/models/message.rb b/app/models/message.rb
index 5ca8d7ea7..de7ebaab5 100644
--- a/app/models/message.rb
+++ b/app/models/message.rb
@@ -103,6 +103,10 @@ class Message < ActiveRecord::Base
usr && usr.logged? && (usr.allowed_to?(:delete_messages, project) || (self.author == usr && usr.allowed_to?(:delete_own_messages, project)))
end
+ def notified_users
+ project.notified_users.reject {|user| !visible?(user)}
+ end
+
private
def add_author_as_watcher
diff --git a/app/models/news.rb b/app/models/news.rb
index e0793d9a8..b8bc35f39 100644
--- a/app/models/news.rb
+++ b/app/models/news.rb
@@ -54,20 +54,29 @@ class News < ActiveRecord::Base
user.allowed_to?(:comment_news, project)
end
+ def notified_users
+ project.users.select {|user| user.notify_about?(self) && user.allowed_to?(:view_news, project)}
+ end
+
def recipients
- project.users.select {|user| user.notify_about?(self) && user.allowed_to?(:view_news, project)}.map(&:mail)
+ notified_users.map(&:mail)
end
- # Returns the email addresses that should be cc'd when a new news is added
- def cc_for_added_news
- cc = []
+ # Returns the users that should be cc'd when a new news is added
+ def notified_watchers_for_added_news
+ watchers = []
if m = project.enabled_module('news')
- cc = m.notified_watchers
+ watchers = m.notified_watchers
unless project.is_public?
- cc = cc.select {|user| project.users.include?(user)}
+ watchers = watchers.select {|user| project.users.include?(user)}
end
end
- cc.map(&:mail)
+ watchers
+ end
+
+ # Returns the email addresses that should be cc'd when a new news is added
+ def cc_for_added_news
+ notified_watchers_for_added_news.map(&:mail)
end
# returns latest news for projects visible by user
diff --git a/app/models/principal.rb b/app/models/principal.rb
index 7e87091f3..355e8907e 100644
--- a/app/models/principal.rb
+++ b/app/models/principal.rb
@@ -68,7 +68,8 @@ class Principal < ActiveRecord::Base
where({})
else
pattern = "%#{q}%"
- sql = %w(login firstname lastname mail).map {|column| "LOWER(#{table_name}.#{column}) LIKE LOWER(:p)"}.join(" OR ")
+ sql = %w(login firstname lastname).map {|column| "LOWER(#{table_name}.#{column}) LIKE LOWER(:p)"}.join(" OR ")
+ sql << " OR #{table_name}.id IN (SELECT user_id FROM #{EmailAddress.table_name} WHERE LOWER(address) LIKE LOWER(:p))"
params = {:p => pattern}
if q =~ /^(.+)\s+(.+)$/
a, b = "#{$1}%", "#{$2}%"
@@ -108,6 +109,14 @@ class Principal < ActiveRecord::Base
to_s
end
+ def mail=(*args)
+ nil
+ end
+
+ def mail
+ nil
+ end
+
def visible?(user=User.current)
Principal.visible(user).where(:id => id).first == self
end
@@ -145,7 +154,6 @@ class Principal < ActiveRecord::Base
self.hashed_password ||= ''
self.firstname ||= ''
self.lastname ||= ''
- self.mail ||= ''
true
end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index b680fe52d..2175d0682 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -81,6 +81,8 @@ class User < Principal
has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
has_one :rss_token, lambda {where "action='feeds'"}, :class_name => 'Token'
has_one :api_token, lambda {where "action='api'"}, :class_name => 'Token'
+ has_one :email_address, lambda {where :is_default => true}, :autosave => true
+ has_many :email_addresses, :dependent => :delete_all
belongs_to :auth_source
scope :logged, lambda { where("#{User.table_name}.status <> #{STATUS_ANONYMOUS}") }
@@ -96,15 +98,12 @@ class User < Principal
LOGIN_LENGTH_LIMIT = 60
MAIL_LENGTH_LIMIT = 60
- validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
+ validates_presence_of :login, :firstname, :lastname, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
validates_uniqueness_of :login, :if => Proc.new { |user| user.login_changed? && user.login.present? }, :case_sensitive => false
- validates_uniqueness_of :mail, :if => Proc.new { |user| user.mail_changed? && user.mail.present? }, :case_sensitive => false
# Login must contain letters, numbers, underscores only
validates_format_of :login, :with => /\A[a-z0-9_\-@\.]*\z/i
validates_length_of :login, :maximum => LOGIN_LENGTH_LIMIT
validates_length_of :firstname, :lastname, :maximum => 30
- validates_format_of :mail, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, :allow_blank => true
- validates_length_of :mail, :maximum => MAIL_LENGTH_LIMIT, :allow_nil => true
validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
validate :validate_password_length
validate do
@@ -113,6 +112,7 @@ class User < Principal
end
end
+ before_validation :instantiate_email_address
before_create :set_mail_notification
before_save :generate_password_if_needed, :update_hashed_password
before_destroy :remove_references_before_destroy
@@ -127,6 +127,14 @@ class User < Principal
where("#{User.table_name}.id NOT IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id)
}
scope :sorted, lambda { order(*User.fields_for_order_statement)}
+ scope :having_mail, lambda {|arg|
+ addresses = Array.wrap(arg).map {|a| a.to_s.downcase}
+ if addresses.any?
+ joins(:email_addresses).where("LOWER(address) IN (?)", addresses).uniq
+ else
+ none
+ end
+ }
def set_mail_notification
self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
@@ -152,8 +160,21 @@ class User < Principal
base_reload(*args)
end
+ def mail
+ email_address.try(:address)
+ end
+
def mail=(arg)
- write_attribute(:mail, arg.to_s.strip)
+ email = email_address || build_email_address
+ email.address = arg
+ end
+
+ def mail_changed?
+ email_address.try(:address_changed?)
+ end
+
+ def mails
+ email_addresses.pluck(:address)
end
def self.find_or_initialize_by_identity_url(url)
@@ -421,7 +442,7 @@ class User < Principal
# Makes find_by_mail case-insensitive
def self.find_by_mail(mail)
- where("LOWER(mail) = ?", mail.to_s.downcase).first
+ having_mail(mail).first
end
# Returns true if the default admin account can no longer be used
@@ -669,7 +690,7 @@ class User < Principal
def self.anonymous
anonymous_user = AnonymousUser.first
if anonymous_user.nil?
- anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
+ anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :login => '', :status => 0)
raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
end
anonymous_user
@@ -699,6 +720,10 @@ class User < Principal
end
end
+ def instantiate_email_address
+ email_address || build_email_address
+ end
+
private
def generate_password_if_needed
@@ -708,16 +733,13 @@ class User < Principal
end
end
- # Delete all outstanding password reset tokens on password or email change.
+ # Delete all outstanding password reset tokens on password change.
# Delete the autologin tokens on password change to prohibit session leakage.
# This helps to keep the account secure in case the associated email account
# was compromised.
def destroy_tokens
- tokens = []
- tokens |= ['recovery', 'autologin'] if hashed_password_changed?
- tokens |= ['recovery'] if mail_changed?
-
- if tokens.any?
+ if hashed_password_changed?
+ tokens = ['recovery', 'autologin']
Token.where(:user_id => id, :action => tokens).delete_all
end
end
@@ -779,6 +801,7 @@ class AnonymousUser < User
def logged?; false end
def admin; false end
def name(*args); I18n.t(:label_user_anonymous) end
+ def mail=(*args); nil end
def mail; nil end
def time_zone; nil end
def rss_key; nil end
@@ -804,4 +827,9 @@ class AnonymousUser < User
def destroy
false
end
+
+ protected
+
+ def instantiate_email_address
+ end
end
diff --git a/app/models/wiki_content.rb b/app/models/wiki_content.rb
index 90947d589..992c7da34 100644
--- a/app/models/wiki_content.rb
+++ b/app/models/wiki_content.rb
@@ -41,11 +41,13 @@ class WikiContent < ActiveRecord::Base
page.nil? ? [] : page.attachments
end
+ def notified_users
+ project.notified_users.reject {|user| !visible?(user)}
+ end
+
# Returns the mail addresses of users that should be notified
def recipients
- notified = project.notified_users
- notified.reject! {|user| !visible?(user)}
- notified.collect(&:mail)
+ notified_users.collect(&:mail)
end
# Return true if the content is the current page content
diff --git a/app/views/email_addresses/_index.html.erb b/app/views/email_addresses/_index.html.erb
new file mode 100644
index 000000000..644cd759b
--- /dev/null
+++ b/app/views/email_addresses/_index.html.erb
@@ -0,0 +1,26 @@
+<% if @addresses.present? %>
+ <table class="list email_addresses">
+ <% @addresses.each do |address| %>
+ <tr class="<%= cycle("odd", "even") %>">
+ <td class="email"><%= address.address %></td>
+ <td class="buttons">
+ <%= toggle_email_address_notify_link(address) %>
+ <%= delete_link user_email_address_path(@user, address), :remote => true %>
+ </td>
+ </tr>
+ <% end %>
+ </table>
+<% end %>
+
+<% unless @addresses.size >= Setting.max_additional_emails.to_i %>
+ <div>
+ <%= form_for @address, :url => user_email_addresses_path(@user), :remote => true do |f| %>
+ <p><%= l(:label_email_address_add) %></p>
+ <%= error_messages_for @address %>
+ <p>
+ <%= f.text_field :address, :size => 40 %>
+ <%= submit_tag l(:button_add) %>
+ </p>
+ <% end %>
+ </div>
+<% end %>
diff --git a/app/views/email_addresses/index.html.erb b/app/views/email_addresses/index.html.erb
new file mode 100644
index 000000000..7de1d37d1
--- /dev/null
+++ b/app/views/email_addresses/index.html.erb
@@ -0,0 +1,2 @@
+<h2><%= @user.name %></h2>
+<%= render :partial => 'email_addresses/index' %>
diff --git a/app/views/email_addresses/index.js.erb b/app/views/email_addresses/index.js.erb
new file mode 100644
index 000000000..2a7147f36
--- /dev/null
+++ b/app/views/email_addresses/index.js.erb
@@ -0,0 +1,3 @@
+$('#ajax-modal').html('<%= escape_javascript(render :partial => 'email_addresses/index') %>');
+showModal('ajax-modal', '600px', '<%= escape_javascript l(:label_email_address_plural) %>');
+$('#email_address_address').focus();
diff --git a/app/views/my/account.html.erb b/app/views/my/account.html.erb
index 0ce223b0a..cadf0e830 100644
--- a/app/views/my/account.html.erb
+++ b/app/views/my/account.html.erb
@@ -1,4 +1,5 @@
<div class="contextual">
+<%= additional_emails_link(@user) %>
<%= link_to(l(:button_change_password), {:action => 'password'}, :class => 'icon icon-passwd') if @user.change_password_allowed? %>
<%= call_hook(:view_my_account_contextual, :user => @user)%>
</div>
diff --git a/app/views/settings/_authentication.html.erb b/app/views/settings/_authentication.html.erb
index d190fab66..77b5afced 100644
--- a/app/views/settings/_authentication.html.erb
+++ b/app/views/settings/_authentication.html.erb
@@ -16,6 +16,8 @@
<p><%= setting_check_box :lost_password, :label => :label_password_lost %></p>
+<p><%= setting_text_field :max_additional_emails, :size => 6 %></p>
+
<p><%= setting_check_box :openid, :disabled => !Object.const_defined?(:OpenID) %></p>
<p><%= setting_check_box :rest_api_enabled %></p>
diff --git a/app/views/users/edit.html.erb b/app/views/users/edit.html.erb
index d46521730..67b57f802 100644
--- a/app/views/users/edit.html.erb
+++ b/app/views/users/edit.html.erb
@@ -1,5 +1,6 @@
<div class="contextual">
<%= link_to l(:label_profile), user_path(@user), :class => 'icon icon-user' %>
+<%= additional_emails_link(@user) %>
<%= change_status_link(@user) %>
<%= delete_link user_path(@user) if User.current != @user %>
</div>