From 99599c099f818ec7e0c41974f774d145170048a5 Mon Sep 17 00:00:00 2001
From: Peter Smit <peter@smitmail.eu>
Date: Wed, 17 Dec 2014 10:26:19 +0200
Subject: [PATCH 1/6] Add alternative email addresses to the model

A new struct is created named EmailAddress that contains alternative
email addresses for users. Also the email related methods; IsEmailUsed
and GetUserByEmail are updated.

DeleteUser deletes the extra email addresses and DeleteInactivateUsers
also deletes inactive accounts. This could be factored out, but should
do it for now.
---
 models/models.go |  2 +-
 models/user.go   | 49 ++++++++++++++++++++++++++++++++++++++++--------
 2 files changed, 42 insertions(+), 9 deletions(-)

diff --git a/models/models.go b/models/models.go
index 92849f585..677f9ba9d 100644
--- a/models/models.go
+++ b/models/models.go
@@ -45,7 +45,7 @@ func init() {
 		new(Issue), new(Comment), new(Attachment), new(IssueUser), new(Label), new(Milestone),
 		new(Mirror), new(Release), new(LoginSource), new(Webhook),
 		new(UpdateTask), new(HookTask), new(Team), new(OrgUser), new(TeamUser),
-		new(Notice))
+		new(Notice), new(EmailAddress))
 }
 
 func LoadModelsConfig() {
diff --git a/models/user.go b/models/user.go
index 7e6a6c824..08710763d 100644
--- a/models/user.go
+++ b/models/user.go
@@ -50,10 +50,11 @@ var (
 
 // User represents the object of individual and member of organization.
 type User struct {
-	Id          int64
-	LowerName   string `xorm:"UNIQUE NOT NULL"`
-	Name        string `xorm:"UNIQUE NOT NULL"`
-	FullName    string
+	Id        int64
+	LowerName string `xorm:"UNIQUE NOT NULL"`
+	Name      string `xorm:"UNIQUE NOT NULL"`
+	FullName  string
+	// Email is the primary email address (to be used for communication).
 	Email       string `xorm:"UNIQUE(s) NOT NULL"`
 	Passwd      string `xorm:"NOT NULL"`
 	LoginType   LoginType
@@ -93,6 +94,15 @@ type User struct {
 	Members     []*User `xorm:"-"`
 }
 
+// EmailAdresses is the list of all email addresses of a user. Can contain the
+// primary email address, but is not obligatory
+type EmailAddress struct {
+	Id          int64
+	OwnerId     int64  `xorm:"INDEX NOT NULL"`
+	Email       string `xorm:"UNIQUE NOT NULL"`
+	IsActivated bool
+}
+
 // DashboardLink returns the user dashboard page link.
 func (u *User) DashboardLink() string {
 	if u.IsOrganization() {
@@ -248,6 +258,9 @@ func IsEmailUsed(email string) (bool, error) {
 	if len(email) == 0 {
 		return false, nil
 	}
+	if used, err := x.Get(&EmailAddress{Email: email}); used || err != nil {
+		return used, err
+	}
 	return x.Get(&User{Email: email})
 }
 
@@ -488,6 +501,10 @@ func DeleteUser(u *User) error {
 	if _, err = x.Delete(&Access{UserName: u.LowerName}); err != nil {
 		return err
 	}
+	// Delete all alternative email addresses
+	if _, err = x.Delete(&EmailAddress{OwnerId: u.Id}); err != nil {
+		return err
+	}
 	// Delete all SSH keys.
 	keys := make([]*PublicKey, 0, 10)
 	if err = x.Find(&keys, &PublicKey{OwnerId: u.Id}); err != nil {
@@ -508,9 +525,12 @@ func DeleteUser(u *User) error {
 	return err
 }
 
-// DeleteInactivateUsers deletes all inactivate users.
+// DeleteInactivateUsers deletes all inactivate users and email addresses.
 func DeleteInactivateUsers() error {
 	_, err := x.Where("is_active=?", false).Delete(new(User))
+	if err == nil {
+		_, err = x.Delete(&EmailAddress{IsActivated: false})
+	}
 	return err
 }
 
@@ -629,14 +649,27 @@ func GetUserByEmail(email string) (*User, error) {
 	if len(email) == 0 {
 		return nil, ErrUserNotExist
 	}
+	// First try to find the user by primary email
 	user := &User{Email: strings.ToLower(email)}
 	has, err := x.Get(user)
 	if err != nil {
 		return nil, err
-	} else if !has {
-		return nil, ErrUserNotExist
 	}
-	return user, nil
+	if has {
+		return user, nil
+	}
+
+	// Otherwise, check in alternative list for activated email addresses
+	emailAddress := &EmailAddress{Email: strings.ToLower(email), IsActivated: true}
+	has, err = x.Get(emailAddress)
+	if err != nil {
+		return nil, err
+	}
+	if has {
+		return GetUserById(emailAddress.OwnerId)
+	}
+
+	return nil, ErrUserNotExist
 }
 
 // SearchUserByName returns given number of users whose name contains keyword.

From 6919c80f0bdaffbf4f39c42a12536daae7f4fe79 Mon Sep 17 00:00:00 2001
From: Peter <peter@smitmail.eu>
Date: Wed, 17 Dec 2014 17:40:10 +0200
Subject: [PATCH 2/6] Add function to the model for email address management
 (add/delete/activate)

---
 models/user.go | 133 +++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 133 insertions(+)

diff --git a/models/user.go b/models/user.go
index 08710763d..fd2d8dc70 100644
--- a/models/user.go
+++ b/models/user.go
@@ -42,6 +42,8 @@ var (
 	ErrUserNotExist          = errors.New("User does not exist")
 	ErrUserNotKeyOwner       = errors.New("User does not the owner of public key")
 	ErrEmailAlreadyUsed      = errors.New("E-mail already used")
+	ErrEmailNotExist         = errors.New("E-mail does not exist")
+	ErrEmailNotActivated     = errors.New("E-mail address has not been activated")
 	ErrUserNameIllegal       = errors.New("User name contains illegal characters")
 	ErrLoginSourceNotExist   = errors.New("Login source does not exist")
 	ErrLoginSourceNotActived = errors.New("Login source is not actived")
@@ -101,6 +103,7 @@ type EmailAddress struct {
 	OwnerId     int64  `xorm:"INDEX NOT NULL"`
 	Email       string `xorm:"UNIQUE NOT NULL"`
 	IsActivated bool
+	IsPrimary   bool `xorm:"-"`
 }
 
 // DashboardLink returns the user dashboard page link.
@@ -368,6 +371,25 @@ func VerifyUserActiveCode(code string) (user *User) {
 	return nil
 }
 
+// verify active code when active account
+func VerifyActiveEmailCode(code, email string) *EmailAddress {
+	minutes := setting.Service.ActiveCodeLives
+
+	if user := getVerifyUser(code); user != nil {
+		// time limit code
+		prefix := code[:base.TimeLimitCodeLength]
+		data := com.ToStr(user.Id) + email + user.LowerName + user.Passwd + user.Rands
+
+		if base.VerifyTimeLimitCode(data, minutes, prefix) {
+			emailAddress := &EmailAddress{Email: email}
+			if has, _ := x.Get(emailAddress); has {
+				return emailAddress
+			}
+		}
+	}
+	return nil
+}
+
 // ChangeUserName changes all corresponding setting from old user name to new one.
 func ChangeUserName(u *User, newUserName string) (err error) {
 	if !IsLegalName(newUserName) {
@@ -604,6 +626,117 @@ func GetUserIdsByNames(names []string) []int64 {
 	return ids
 }
 
+// Get all email addresses
+func GetEmailAddresses(uid int64) ([]*EmailAddress, error) {
+	emails := make([]*EmailAddress, 0, 5)
+	err := x.Where("owner_id=?", uid).Find(&emails)
+	if err != nil {
+		return nil, err
+	}
+
+	u, err := GetUserById(uid)
+	if err != nil {
+		return nil, err
+	}
+
+	primary_email_found := false
+
+	for _, email := range emails {
+		if email.Email == u.Email {
+			primary_email_found = true
+			email.IsPrimary = true
+		} else {
+			email.IsPrimary = false
+		}
+	}
+
+	// We alway want the primary email address displayed, even if it's not in
+	// the emailaddress table (yet)
+	if !primary_email_found {
+		emails = append(emails, &EmailAddress{Email: u.Email, IsActivated: true, IsPrimary: true})
+	}
+	return emails, nil
+}
+
+func AddEmailAddress(email *EmailAddress) error {
+	used, err := IsEmailUsed(email.Email)
+	if err != nil {
+		return err
+	} else if used {
+		return ErrEmailAlreadyUsed
+	}
+
+	_, err = x.Insert(email)
+	return err
+}
+
+func (email *EmailAddress) Activate() error {
+	email.IsActivated = true
+	if _, err := x.Id(email.Id).AllCols().Update(email); err != nil {
+		return err
+	}
+
+	if user, err := GetUserById(email.OwnerId); err != nil {
+		return err
+	} else {
+		user.Rands = GetUserSalt()
+		return UpdateUser(user)
+	}
+}
+
+func DeleteEmailAddress(email *EmailAddress) error {
+	has, err := x.Get(email)
+	if err != nil {
+		return err
+	} else if !has {
+		return ErrEmailNotExist
+	}
+
+	if _, err = x.Delete(email); err != nil {
+		return err
+	}
+
+	return nil
+
+}
+
+func MakeEmailPrimary(email *EmailAddress) error {
+	has, err := x.Get(email)
+	if err != nil {
+		return err
+	} else if !has {
+		return ErrEmailNotExist
+	}
+
+	if !email.IsActivated {
+		return ErrEmailNotActivated
+	}
+
+	user := &User{Id: email.OwnerId}
+	has, err = x.Get(user)
+	if err != nil {
+		return err
+	} else if !has {
+		return ErrUserNotExist
+	}
+
+	// Make sure the former primary email doesn't disappear
+	former_primary_email := &EmailAddress{Email: user.Email}
+	has, err = x.Get(former_primary_email)
+	if err != nil {
+		return err
+	} else if !has {
+		former_primary_email.OwnerId = user.Id
+		former_primary_email.IsActivated = user.IsActive
+		x.Insert(former_primary_email)
+	}
+
+	user.Email = email.Email
+	_, err = x.Id(user.Id).AllCols().Update(user)
+
+	return err
+}
+
 // UserCommit represents a commit with validation of user.
 type UserCommit struct {
 	User *User

From ec71d538fcb1f84050048a660a25258a5401828f Mon Sep 17 00:00:00 2001
From: Peter <peter@smitmail.eu>
Date: Wed, 17 Dec 2014 17:41:49 +0200
Subject: [PATCH 3/6] Method for activating email addresses through
 verification email

---
 modules/mailer/mail.go | 33 +++++++++++++++++++++++++++++++++
 routers/user/auth.go   | 21 +++++++++++++++++++++
 2 files changed, 54 insertions(+)

diff --git a/modules/mailer/mail.go b/modules/mailer/mail.go
index 6c73e7e58..21f33b17e 100644
--- a/modules/mailer/mail.go
+++ b/modules/mailer/mail.go
@@ -21,6 +21,7 @@ import (
 
 const (
 	AUTH_ACTIVE           base.TplName = "mail/auth/active"
+	AUTH_ACTIVATE_EMAIL   base.TplName = "mail/auth/activate_email"
 	AUTH_REGISTER_SUCCESS base.TplName = "mail/auth/register_success"
 	AUTH_RESET_PASSWORD   base.TplName = "mail/auth/reset_passwd"
 
@@ -64,6 +65,17 @@ func CreateUserActiveCode(u *models.User, startInf interface{}) string {
 	return code
 }
 
+// create a time limit code for user active
+func CreateUserEmailActivateCode(u *models.User, e *models.EmailAddress, startInf interface{}) string {
+	minutes := setting.Service.ActiveCodeLives
+	data := com.ToStr(u.Id) + e.Email + u.LowerName + u.Passwd + u.Rands
+	code := base.CreateTimeLimitCode(data, minutes, startInf)
+
+	// add tail hex username
+	code += hex.EncodeToString([]byte(u.LowerName))
+	return code
+}
+
 // Send user register mail with active code
 func SendRegisterMail(r macaron.Render, u *models.User) {
 	code := CreateUserActiveCode(u, nil)
@@ -103,6 +115,27 @@ func SendActiveMail(r macaron.Render, u *models.User) {
 	SendAsync(&msg)
 }
 
+// Send email to verify secondary email.
+func SendActivateEmail(r macaron.Render, user *models.User, email *models.EmailAddress) {
+	code := CreateUserEmailActivateCode(user, email, nil)
+
+	subject := "Verify your e-mail address"
+
+	data := GetMailTmplData(user)
+	data["Code"] = code
+	data["Email"] = email.Email
+	body, err := r.HTMLString(string(AUTH_ACTIVATE_EMAIL), data)
+	if err != nil {
+		log.Error(4, "mail.SendActiveMail(fail to render): %v", err)
+		return
+	}
+
+	msg := NewMailMessage([]string{email.Email}, subject, body)
+	msg.Info = fmt.Sprintf("UID: %d, send activate email to %s", user.Id, email.Email)
+
+	SendAsync(&msg)
+}
+
 // Send reset password email.
 func SendResetPasswdMail(r macaron.Render, u *models.User) {
 	code := CreateUserActiveCode(u, nil)
diff --git a/routers/user/auth.go b/routers/user/auth.go
index c695f929a..a5b27453e 100644
--- a/routers/user/auth.go
+++ b/routers/user/auth.go
@@ -343,6 +343,27 @@ func Activate(ctx *middleware.Context) {
 	ctx.HTML(200, ACTIVATE)
 }
 
+func ActivateEmail(ctx *middleware.Context) {
+	code := ctx.Query("code")
+	email_string := ctx.Query("email")
+
+	// Verify code.
+	if email := models.VerifyActiveEmailCode(code, email_string); email != nil {
+		err := email.Activate()
+		if err != nil {
+			ctx.Handle(500, "ActivateEmail", err)
+		}
+
+		log.Trace("Email activated: %s", email.Email)
+
+		ctx.Flash.Success(ctx.Tr("settings.activate_email_success"))
+
+	}
+
+	ctx.Redirect(setting.AppSubUrl + "/user/settings/email")
+	return
+}
+
 func ForgotPasswd(ctx *middleware.Context) {
 	ctx.Data["Title"] = ctx.Tr("auth.forgot_password")
 

From f34b04cfc09a3bf0d18c7c4d8950869a252f0a28 Mon Sep 17 00:00:00 2001
From: Peter <peter@smitmail.eu>
Date: Wed, 17 Dec 2014 17:42:31 +0200
Subject: [PATCH 4/6] Template for email activation email

---
 templates/mail/auth/activate_email.tmpl | 30 +++++++++++++++++++++++++
 1 file changed, 30 insertions(+)
 create mode 100644 templates/mail/auth/activate_email.tmpl

diff --git a/templates/mail/auth/activate_email.tmpl b/templates/mail/auth/activate_email.tmpl
new file mode 100644
index 000000000..24bbf89d0
--- /dev/null
+++ b/templates/mail/auth/activate_email.tmpl
@@ -0,0 +1,30 @@
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+<title>{{.User.Name}}, please activate your e-mail address</title>
+</head>
+<body style="background:#eee;">
+<div style="color:#333; font:12px/1.5 Tahoma,Arial,sans-serif;; text-shadow:1px 1px #fff; padding:0; margin:0;">
+    <div style="width:600px;margin:0 auto; padding:40px 0 20px;">
+        <div style="border:1px solid #d9d9d9;border-radius:3px; background:#fff; box-shadow: 0px 2px 5px rgba(0, 0, 0,.05); -webkit-box-shadow: 0px 2px 5px rgba(0, 0, 0,.05);">
+            <div style="padding: 20px 15px;">
+                <h1 style="font-size:20px; padding:10px 0 20px; margin:0; border-bottom:1px solid #ddd;"><img src="{{.AppUrl}}/img/favicon.png" style="height: 32px; margin-bottom: -10px;"> <a style="color:#333;text-decoration:none;" target="_blank" href="{{.AppUrl}}">{{.AppName}}</a></h1>
+                <div style="padding:40px 15px;">
+                    <div style="font-size:16px; padding-bottom:30px; font-weight:bold;">
+                        Hi <span style="color: #00BFFF;">{{.User.Name}}</span>,
+                    </div>
+                    <div style="font-size:14px; padding:0 15px;">
+						<p style="margin:0;padding:0 0 9px 0;">Please click the following link to verify your e-mail address within <b>{{.ActiveCodeLives}} hours</b>.</p>
+						<p style="margin:0;padding:0 0 9px 0;">
+							<a href="{{.AppUrl}}user/activate_email?code={{.Code}}&email={{.Email}}">{{.AppUrl}}user/activate_email?code={{.Code}}&email={{.Email}}</a>
+						</p>
+						<p style="margin:0;padding:0 0 9px 0;">Not working? Try copying and pasting it to your browser.</p>
+                    </div>
+                </div>
+            </div>
+        </div>
+        <div style="color:#aaa;padding:10px;text-align:center;">
+            © 2014 <a style="color:#888;text-decoration:none;" target="_blank" href="http://gogits.org">Gogs: Go Git Service</a>
+        </div>
+    </div>
+</div>
+</body>
+</html>
\ No newline at end of file

From b033f2f535ad498ed78da6599d446abe2fd5c27d Mon Sep 17 00:00:00 2001
From: Peter <peter@smitmail.eu>
Date: Wed, 17 Dec 2014 17:42:54 +0200
Subject: [PATCH 5/6] Finish method for having multiple emails/user.

All basics are implemented. Missing are the right (localized) strings
and the page markup could have a look at by a frontend guy.
---
 cmd/web.go                         |   3 +
 modules/auth/user_form.go          |   8 +++
 routers/user/setting.go            | 108 +++++++++++++++++++++++++++++
 templates/user/settings/email.tmpl |  58 ++++++++++++++++
 templates/user/settings/nav.tmpl   |   1 +
 5 files changed, 178 insertions(+)
 create mode 100644 templates/user/settings/email.tmpl

diff --git a/cmd/web.go b/cmd/web.go
index 38b802b68..eeb2f5d28 100644
--- a/cmd/web.go
+++ b/cmd/web.go
@@ -241,6 +241,8 @@ func runWeb(*cli.Context) {
 		m.Get("", user.Settings)
 		m.Post("", bindIgnErr(auth.UpdateProfileForm{}), user.SettingsPost)
 		m.Post("/avatar", binding.MultipartForm(auth.UploadAvatarForm{}), user.SettingsAvatar)
+		m.Get("/email", user.SettingsEmails)
+		m.Post("/email", bindIgnErr(auth.AddEmailForm{}), user.SettingsEmailPost)
 		m.Get("/password", user.SettingsPassword)
 		m.Post("/password", bindIgnErr(auth.ChangePasswordForm{}), user.SettingsPasswordPost)
 		m.Get("/ssh", user.SettingsSSHKeys)
@@ -252,6 +254,7 @@ func runWeb(*cli.Context) {
 	m.Group("/user", func() {
 		// r.Get("/feeds", binding.Bind(auth.FeedsForm{}), user.Feeds)
 		m.Any("/activate", user.Activate)
+		m.Any("/activate_email", user.ActivateEmail)
 		m.Get("/email2user", user.Email2User)
 		m.Get("/forget_password", user.ForgotPasswd)
 		m.Post("/forget_password", user.ForgotPasswdPost)
diff --git a/modules/auth/user_form.go b/modules/auth/user_form.go
index 4dfe2499f..becd5cbca 100644
--- a/modules/auth/user_form.go
+++ b/modules/auth/user_form.go
@@ -97,6 +97,14 @@ func (f *UploadAvatarForm) Validate(ctx *macaron.Context, errs binding.Errors) b
 	return validate(errs, ctx.Data, f, ctx.Locale)
 }
 
+type AddEmailForm struct {
+	Email string `form:"email" binding:"Required;Email;MaxSize(50)"`
+}
+
+func (f *AddEmailForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
+	return validate(errs, ctx.Data, f, ctx.Locale)
+}
+
 type ChangePasswordForm struct {
 	OldPassword string `form:"old_password" binding:"Required;MinSize(6);MaxSize(255)"`
 	Password    string `form:"password" binding:"Required;MinSize(6);MaxSize(255)"`
diff --git a/routers/user/setting.go b/routers/user/setting.go
index 304ddd362..7dca5d869 100644
--- a/routers/user/setting.go
+++ b/routers/user/setting.go
@@ -14,6 +14,7 @@ import (
 	"github.com/gogits/gogs/modules/auth"
 	"github.com/gogits/gogs/modules/base"
 	"github.com/gogits/gogs/modules/log"
+	"github.com/gogits/gogs/modules/mailer"
 	"github.com/gogits/gogs/modules/middleware"
 	"github.com/gogits/gogs/modules/setting"
 )
@@ -21,6 +22,7 @@ import (
 const (
 	SETTINGS_PROFILE      base.TplName = "user/settings/profile"
 	SETTINGS_PASSWORD     base.TplName = "user/settings/password"
+	SETTINGS_EMAILS       base.TplName = "user/settings/email"
 	SETTINGS_SSH_KEYS     base.TplName = "user/settings/sshkeys"
 	SETTINGS_SOCIAL       base.TplName = "user/settings/social"
 	SETTINGS_APPLICATIONS base.TplName = "user/settings/applications"
@@ -126,6 +128,112 @@ func SettingsAvatar(ctx *middleware.Context, form auth.UploadAvatarForm) {
 	ctx.Flash.Success(ctx.Tr("settings.update_avatar_success"))
 }
 
+func SettingsEmails(ctx *middleware.Context) {
+	ctx.Data["Title"] = ctx.Tr("settings")
+	ctx.Data["PageIsUserSettings"] = true
+	ctx.Data["PageIsSettingsEmails"] = true
+
+	var err error
+	ctx.Data["Emails"], err = models.GetEmailAddresses(ctx.User.Id)
+
+	if err != nil {
+		ctx.Handle(500, "email.GetEmailAddresses", err)
+		return
+	}
+
+	ctx.HTML(200, SETTINGS_EMAILS)
+}
+
+func SettingsEmailPost(ctx *middleware.Context, form auth.AddEmailForm) {
+	ctx.Data["Title"] = ctx.Tr("settings")
+	ctx.Data["PageIsUserSettings"] = true
+	ctx.Data["PageIsSettingsEmails"] = true
+
+	var err error
+	ctx.Data["Emails"], err = models.GetEmailAddresses(ctx.User.Id)
+	if err != nil {
+		ctx.Handle(500, "email.GetEmailAddresses", err)
+		return
+	}
+
+	// Delete Email address.
+	if ctx.Query("_method") == "DELETE" {
+		id := com.StrTo(ctx.Query("id")).MustInt64()
+		if id <= 0 {
+			return
+		}
+
+		if err = models.DeleteEmailAddress(&models.EmailAddress{Id: id}); err != nil {
+			ctx.Handle(500, "DeleteEmail", err)
+		} else {
+			log.Trace("Email address deleted: %s", ctx.User.Name)
+			ctx.Redirect(setting.AppSubUrl + "/user/settings/email")
+		}
+		return
+	}
+
+	// Make emailaddress primary.
+	if ctx.Query("_method") == "PRIMARY" {
+		id := com.StrTo(ctx.Query("id")).MustInt64()
+		if id <= 0 {
+			return
+		}
+
+		if err = models.MakeEmailPrimary(&models.EmailAddress{Id: id}); err != nil {
+			ctx.Handle(500, "MakeEmailPrimary", err)
+		} else {
+			log.Trace("Email made primary: %s", ctx.User.Name)
+			ctx.Redirect(setting.AppSubUrl + "/user/settings/email")
+		}
+		return
+	}
+
+	// Add Email address.
+	if ctx.Req.Method == "POST" {
+		if ctx.HasError() {
+			ctx.HTML(200, SETTINGS_EMAILS)
+			return
+		}
+
+		cleanEmail := strings.Replace(form.Email, "\n", "", -1)
+		e := &models.EmailAddress{
+			OwnerId:     ctx.User.Id,
+			Email:       cleanEmail,
+			IsActivated: !setting.Service.RegisterEmailConfirm,
+		}
+
+		if err := models.AddEmailAddress(e); err != nil {
+			if err == models.ErrEmailAlreadyUsed {
+				ctx.RenderWithErr(ctx.Tr("form.email_has_been_used"), SETTINGS_EMAILS, &form)
+				return
+			}
+			ctx.Handle(500, "email.AddEmailAddress", err)
+			return
+		} else {
+
+			// Send confirmation e-mail
+			if setting.Service.RegisterEmailConfirm {
+				mailer.SendActivateEmail(ctx.Render, ctx.User, e)
+
+				if err := ctx.Cache.Put("MailResendLimit_"+ctx.User.LowerName, ctx.User.LowerName, 180); err != nil {
+					log.Error(4, "Set cache(MailResendLimit) fail: %v", err)
+				}
+				ctx.Flash.Success(ctx.Tr("settings.add_email_success_confirmation_email_sent"))
+			} else {
+				ctx.Flash.Success(ctx.Tr("settings.add_email_success"))
+			}
+
+			log.Trace("Email address added: %s", e.Email)
+
+			ctx.Redirect(setting.AppSubUrl + "/user/settings/email")
+			return
+		}
+
+	}
+
+	ctx.HTML(200, SETTINGS_EMAILS)
+}
+
 func SettingsPassword(ctx *middleware.Context) {
 	ctx.Data["Title"] = ctx.Tr("settings")
 	ctx.Data["PageIsUserSettings"] = true
diff --git a/templates/user/settings/email.tmpl b/templates/user/settings/email.tmpl
new file mode 100644
index 000000000..6f2a85a3c
--- /dev/null
+++ b/templates/user/settings/email.tmpl
@@ -0,0 +1,58 @@
+{{template "ng/base/head" .}}
+{{template "ng/base/header" .}}
+<div id="setting-wrapper" class="main-wrapper">
+    <div id="user-profile-setting" class="container clear">
+        {{template "user/settings/nav" .}}
+        <div class="grid-4-5 left">
+            <div class="setting-content">
+                {{template "ng/base/alert" .}}
+                <div id="user-email-setting-content">
+                    <div id="user-email-panel" class="panel panel-radius">
+                        <div class="panel-header">
+                             <strong>{{.i18n.Tr "settings.manage_emails"}}</strong>
+                        </div>
+                        <ul class="panel-body setting-list">
+                            <li>{{.i18n.Tr "settings.email_desc"}}</li>
+                            {{range .Emails}}
+                            <li class="email clear">
+                                <div class="email-content left">
+									<p><strong>{{.Email}}</strong></p>
+							   </div>
+							   {{if not .IsPrimary}}
+							   {{if .IsActivated}}
+							   <form action="{{AppSubUrl}}/user/settings/email" method="post">
+                                    {{$.CsrfTokenHtml}}
+                                    <input name="_method" type="hidden" value="PRIMARY">
+                                    <input name="id" type="hidden" value="{{.Id}}">
+                                    <button class="right email-btn btn btn-green btn-radius btn-small">{{$.i18n.Tr "settings.primary_email"}}</button>
+                                </form>
+								{{end}}
+                                <form action="{{AppSubUrl}}/user/settings/email" method="post">
+                                    {{$.CsrfTokenHtml}}
+                                    <input name="_method" type="hidden" value="DELETE">
+                                    <input name="id" type="hidden" value="{{.Id}}">
+                                    <button class="right email-btn btn btn-red btn-radius btn-small">{{$.i18n.Tr "settings.delete_email"}}</button>
+                                </form>
+							   {{end}}
+                            </li>
+                            {{end}}
+                             <form action="{{AppSubUrl}}/user/settings/email" method="post">
+							{{.CsrfTokenHtml}}
+							<p class="panel-header"><strong>{{.i18n.Tr "settings.add_new_email"}}</strong></p>
+							<p class="field">
+                                <label class="req" for="email">{{.i18n.Tr "settings.email"}}</label>
+                                <input class="ipt ipt-radius" id="email" name="email" type="text" required />
+                            </p>
+                            <p class="field">
+                                <label></label>
+                                <button class="btn btn-green btn-radius" id="email-add-btn">{{.i18n.Tr "settings.add_email"}}</button>
+                            </p>
+							</form>
+                        </ul>
+                    </div>                    
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+{{template "ng/base/footer" .}}
\ No newline at end of file
diff --git a/templates/user/settings/nav.tmpl b/templates/user/settings/nav.tmpl
index 6204b85a7..780f1218a 100644
--- a/templates/user/settings/nav.tmpl
+++ b/templates/user/settings/nav.tmpl
@@ -4,6 +4,7 @@
         <ul class="menu menu-vertical switching-list grid-1-5 left">
             <li {{if .PageIsSettingsProfile}}class="current"{{end}}><a href="{{AppSubUrl}}/user/settings">{{.i18n.Tr "settings.profile"}}</a></li>
             <li {{if .PageIsSettingsPassword}}class="current"{{end}}><a href="{{AppSubUrl}}/user/settings/password">{{.i18n.Tr "settings.password"}}</a></li>
+            <li {{if .PageIsSettingsEmail}}class="current"{{end}}><a href="{{AppSubUrl}}/user/settings/email">{{.i18n.Tr "settings.emails"}}</a></li>
             <li {{if .PageIsSettingsSSHKeys}}class="current"{{end}}><a href="{{AppSubUrl}}/user/settings/ssh">{{.i18n.Tr "settings.ssh_keys"}}</a></li>
             <li {{if .PageIsSettingsSocial}}class="current"{{end}}><a href="{{AppSubUrl}}/user/settings/social">{{.i18n.Tr "settings.social"}}</a></li>
             <li {{if .PageIsSettingsApplications}}class="current"{{end}}><a href="{{AppSubUrl}}/user/settings/applications">{{.i18n.Tr "settings.applications"}}</a></li>

From 20b5c23a192b3a87b4fdac724c5be705588e461c Mon Sep 17 00:00:00 2001
From: Peter <peter@smitmail.eu>
Date: Sat, 20 Dec 2014 09:26:51 +0200
Subject: [PATCH 6/6] Small fixes to multiple_emails feature

---
 models/user.go          | 24 ++++++++++++------------
 routers/user/setting.go |  2 +-
 2 files changed, 13 insertions(+), 13 deletions(-)

diff --git a/models/user.go b/models/user.go
index fd2d8dc70..631a02747 100644
--- a/models/user.go
+++ b/models/user.go
@@ -100,7 +100,7 @@ type User struct {
 // primary email address, but is not obligatory
 type EmailAddress struct {
 	Id          int64
-	OwnerId     int64  `xorm:"INDEX NOT NULL"`
+	Uid         int64  `xorm:"INDEX NOT NULL"`
 	Email       string `xorm:"UNIQUE NOT NULL"`
 	IsActivated bool
 	IsPrimary   bool `xorm:"-"`
@@ -261,8 +261,8 @@ func IsEmailUsed(email string) (bool, error) {
 	if len(email) == 0 {
 		return false, nil
 	}
-	if used, err := x.Get(&EmailAddress{Email: email}); used || err != nil {
-		return used, err
+	if has, err := x.Get(&EmailAddress{Email: email}); has || err != nil {
+		return has, err
 	}
 	return x.Get(&User{Email: email})
 }
@@ -524,7 +524,7 @@ func DeleteUser(u *User) error {
 		return err
 	}
 	// Delete all alternative email addresses
-	if _, err = x.Delete(&EmailAddress{OwnerId: u.Id}); err != nil {
+	if _, err = x.Delete(&EmailAddress{Uid: u.Id}); err != nil {
 		return err
 	}
 	// Delete all SSH keys.
@@ -551,7 +551,7 @@ func DeleteUser(u *User) error {
 func DeleteInactivateUsers() error {
 	_, err := x.Where("is_active=?", false).Delete(new(User))
 	if err == nil {
-		_, err = x.Delete(&EmailAddress{IsActivated: false})
+		_, err = x.Where("is_activated=?", false).Delete(new(EmailAddress))
 	}
 	return err
 }
@@ -639,11 +639,11 @@ func GetEmailAddresses(uid int64) ([]*EmailAddress, error) {
 		return nil, err
 	}
 
-	primary_email_found := false
+	isPrimaryFound := false
 
 	for _, email := range emails {
 		if email.Email == u.Email {
-			primary_email_found = true
+			isPrimaryFound = true
 			email.IsPrimary = true
 		} else {
 			email.IsPrimary = false
@@ -652,7 +652,7 @@ func GetEmailAddresses(uid int64) ([]*EmailAddress, error) {
 
 	// We alway want the primary email address displayed, even if it's not in
 	// the emailaddress table (yet)
-	if !primary_email_found {
+	if !isPrimaryFound {
 		emails = append(emails, &EmailAddress{Email: u.Email, IsActivated: true, IsPrimary: true})
 	}
 	return emails, nil
@@ -676,7 +676,7 @@ func (email *EmailAddress) Activate() error {
 		return err
 	}
 
-	if user, err := GetUserById(email.OwnerId); err != nil {
+	if user, err := GetUserById(email.Uid); err != nil {
 		return err
 	} else {
 		user.Rands = GetUserSalt()
@@ -712,7 +712,7 @@ func MakeEmailPrimary(email *EmailAddress) error {
 		return ErrEmailNotActivated
 	}
 
-	user := &User{Id: email.OwnerId}
+	user := &User{Id: email.Uid}
 	has, err = x.Get(user)
 	if err != nil {
 		return err
@@ -726,7 +726,7 @@ func MakeEmailPrimary(email *EmailAddress) error {
 	if err != nil {
 		return err
 	} else if !has {
-		former_primary_email.OwnerId = user.Id
+		former_primary_email.Uid = user.Id
 		former_primary_email.IsActivated = user.IsActive
 		x.Insert(former_primary_email)
 	}
@@ -799,7 +799,7 @@ func GetUserByEmail(email string) (*User, error) {
 		return nil, err
 	}
 	if has {
-		return GetUserById(emailAddress.OwnerId)
+		return GetUserById(emailAddress.Uid)
 	}
 
 	return nil, ErrUserNotExist
diff --git a/routers/user/setting.go b/routers/user/setting.go
index 7dca5d869..419e84b39 100644
--- a/routers/user/setting.go
+++ b/routers/user/setting.go
@@ -197,7 +197,7 @@ func SettingsEmailPost(ctx *middleware.Context, form auth.AddEmailForm) {
 
 		cleanEmail := strings.Replace(form.Email, "\n", "", -1)
 		e := &models.EmailAddress{
-			OwnerId:     ctx.User.Id,
+			Uid:         ctx.User.Id,
 			Email:       cleanEmail,
 			IsActivated: !setting.Service.RegisterEmailConfirm,
 		}