summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--cmd/web.go3
-rw-r--r--models/models.go2
-rw-r--r--models/user.go182
-rw-r--r--modules/auth/user_form.go8
-rw-r--r--modules/mailer/mail.go33
-rw-r--r--routers/user/auth.go21
-rw-r--r--routers/user/setting.go108
-rw-r--r--templates/mail/auth/activate_email.tmpl30
-rw-r--r--templates/user/settings/email.tmpl58
-rw-r--r--templates/user/settings/nav.tmpl1
10 files changed, 437 insertions, 9 deletions
diff --git a/cmd/web.go b/cmd/web.go
index 27879e4a39..df7c96b123 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/models/models.go b/models/models.go
index 92849f5856..677f9ba9d7 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 729f2c0c7e..2537a3e692 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")
@@ -50,10 +52,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 +96,16 @@ 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
+ Uid int64 `xorm:"INDEX NOT NULL"`
+ Email string `xorm:"UNIQUE NOT NULL"`
+ IsActivated bool
+ IsPrimary bool `xorm:"-"`
+}
+
// DashboardLink returns the user dashboard page link.
func (u *User) DashboardLink() string {
if u.IsOrganization() {
@@ -248,6 +261,9 @@ func IsEmailUsed(email string) (bool, error) {
if len(email) == 0 {
return false, nil
}
+ if has, err := x.Get(&EmailAddress{Email: email}); has || err != nil {
+ return has, err
+ }
return x.Get(&User{Email: email})
}
@@ -355,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) {
@@ -488,6 +523,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{Uid: 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 +547,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.Where("is_activated=?", false).Delete(new(EmailAddress))
+ }
return err
}
@@ -584,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
+ }
+
+ isPrimaryFound := false
+
+ for _, email := range emails {
+ if email.Email == u.Email {
+ isPrimaryFound = 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 !isPrimaryFound {
+ 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.Uid); 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.Uid}
+ 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.Uid = 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
@@ -629,14 +782,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.Uid)
+ }
+
+ return nil, ErrUserNotExist
}
// SearchUserByName returns given number of users whose name contains keyword.
diff --git a/modules/auth/user_form.go b/modules/auth/user_form.go
index 4dfe2499f1..becd5cbca8 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/modules/mailer/mail.go b/modules/mailer/mail.go
index 6c73e7e58f..21f33b17e8 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 e576e6af9c..9ed44e353c 100644
--- a/routers/user/auth.go
+++ b/routers/user/auth.go
@@ -345,6 +345,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")
diff --git a/routers/user/setting.go b/routers/user/setting.go
index 304ddd362b..419e84b395 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{
+ Uid: 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/mail/auth/activate_email.tmpl b/templates/mail/auth/activate_email.tmpl
new file mode 100644
index 0000000000..24bbf89d05
--- /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
diff --git a/templates/user/settings/email.tmpl b/templates/user/settings/email.tmpl
new file mode 100644
index 0000000000..6f2a85a3c5
--- /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 6204b85a75..780f1218a1 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>