diff --git a/actions.go b/actions.go
index 596350a2b82e6c0a9be8adef1cb525dfa61e2059..934d163647a5b54752f417e0cdc1516868e081c2 100644
--- a/actions.go
+++ b/actions.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"crypto/rand"
 	"encoding/base64"
+	"errors"
 	"fmt"
 
 	"git.autistici.org/ai3/go-common/pwhash"
@@ -162,7 +163,7 @@ func (r *PrivilegedRequestBase) Authorize(rctx *RequestContext) error {
 	}
 	// TODO: call out to the auth-server
 	if !pwhash.ComparePassword(rctx.User.Password, r.CurPassword) {
-		return fmt.Errorf("bad password (%s vs %s)", rctx.User.Password, r.CurPassword)
+		return errors.New("bad password")
 	}
 	return nil
 }
diff --git a/actions_create.go b/actions_create.go
index 8ca2d64bd8b9780b311d048276e2bce231feaefa..5898304880c35551bf9ac03ae01c4ed043d1457d 100644
--- a/actions_create.go
+++ b/actions_create.go
@@ -118,7 +118,7 @@ func (r *CreateUserRequest) applyTemplate(rctx *RequestContext) error {
 	r.User.Has2FA = false
 	r.User.HasOTP = false
 	r.User.HasEncryptionKeys = true // set to true so that resetPassword will create keys.
-	r.User.PasswordRecoveryHint = ""
+	r.User.AccountRecoveryHint = ""
 	r.User.AppSpecificPasswords = nil
 	r.User.U2FRegistrations = nil
 	if r.User.Lang == "" {
diff --git a/actions_test.go b/actions_test.go
index 964dbe77e942f9b03bd17f14946080c6354b0dd0..85b4b89aa2b081bbd7d3ab42fc017edb8a61ccf0 100644
--- a/actions_test.go
+++ b/actions_test.go
@@ -73,14 +73,14 @@ func (b *fakeBackend) SetUserPassword(_ context.Context, user *User, password st
 	return nil
 }
 
-func (b *fakeBackend) SetPasswordRecoveryHint(_ context.Context, user *User, hint, response string) error {
-	b.users[user.Name].PasswordRecoveryHint = hint
+func (b *fakeBackend) SetAccountRecoveryHint(_ context.Context, user *User, hint, response string) error {
+	b.users[user.Name].AccountRecoveryHint = hint
 	b.recoveryPasswords[user.Name] = response
 	return nil
 }
 
-func (b *fakeBackend) DeletePasswordRecoveryHint(_ context.Context, user *User) error {
-	b.users[user.Name].PasswordRecoveryHint = ""
+func (b *fakeBackend) DeleteAccountRecoveryHint(_ context.Context, user *User) error {
+	b.users[user.Name].AccountRecoveryHint = ""
 	delete(b.recoveryPasswords, user.Name)
 	return nil
 }
@@ -667,7 +667,7 @@ func TestService_ResetResourcePassword(t *testing.T) {
 // 	svc := testService("")
 
 // 	// Bad recovery response.
-// 	_, err := svc.RecoverPassword(context.Background(), tx, &PasswordRecoveryRequest{
+// 	_, err := svc.RecoverPassword(context.Background(), tx, &AccountRecoveryRequest{
 // 		Username:         "testuser",
 // 		RecoveryPassword: "BADPASS",
 // 		Password:         "new_password",
@@ -677,7 +677,7 @@ func TestService_ResetResourcePassword(t *testing.T) {
 // 	}
 
 // 	// Successful account recovery.
-// 	_, err = svc.RecoverPassword(context.Background(), tx, &PasswordRecoveryRequest{
+// 	_, err = svc.RecoverPassword(context.Background(), tx, &AccountRecoveryRequest{
 // 		Username:         "testuser",
 // 		RecoveryPassword: "recoverypassword",
 // 		Password:         "new_password",
diff --git a/actions_user.go b/actions_user.go
index f1c16a459402456cbdadfb8463310c7402b1d4bc..5d9ee7e11ded547f21c043e64c8a1fa7b49fbbd7 100644
--- a/actions_user.go
+++ b/actions_user.go
@@ -49,25 +49,25 @@ func (r *ChangeUserPasswordRequest) Serve(rctx *RequestContext) (interface{}, er
 	return nil, err
 }
 
-// PasswordRecoveryRequest lets users reset their password by providing
+// AccountRecoveryRequest lets users reset their password by providing
 // secondary credentials, which we authenticate ourselves. It is not
 // authenticated with SSO.
 //
 // Two-factor authentication is disabled on successful recovery.
-type PasswordRecoveryRequest struct {
+type AccountRecoveryRequest struct {
 	Username         string `json:"username"`
 	RecoveryPassword string `json:"recovery_password"`
 	Password         string `json:"password"`
 	// TODO: add DeviceInfo
 }
 
-// PasswordRecoveryResponse is the response type for PasswordRecoveryRequest.
-type PasswordRecoveryResponse struct {
+// AccountRecoveryResponse is the response type for AccountRecoveryRequest.
+type AccountRecoveryResponse struct {
 	Hint string `json:"hint,optional"`
 }
 
 // Sanitize the request.
-func (r *PasswordRecoveryRequest) Sanitize() {
+func (r *AccountRecoveryRequest) Sanitize() {
 	if r.RecoveryPassword != "" {
 		r.RecoveryPassword = sanitizedValue
 	}
@@ -77,16 +77,19 @@ func (r *PasswordRecoveryRequest) Sanitize() {
 }
 
 // Validate the request.
-func (r *PasswordRecoveryRequest) Validate(rctx *RequestContext) error {
-	if err := rctx.fieldValidators.password(rctx.Context, r.Password); err != nil {
-		return newValidationError(nil, "password", err.Error())
+func (r *AccountRecoveryRequest) Validate(rctx *RequestContext) error {
+	// Only validate the password if attempting recovery.
+	if r.RecoveryPassword != "" {
+		if err := rctx.fieldValidators.password(rctx.Context, r.Password); err != nil {
+			return newValidationError(nil, "password", err.Error())
+		}
 	}
 	return nil
 }
 
 // PopulateContext extracts information from the request and stores it
 // into the RequestContext.
-func (r *PasswordRecoveryRequest) PopulateContext(rctx *RequestContext) error {
+func (r *AccountRecoveryRequest) PopulateContext(rctx *RequestContext) error {
 	user, err := getUserOrDie(rctx.Context, rctx.TX, r.Username)
 	if err != nil {
 		return err
@@ -96,7 +99,7 @@ func (r *PasswordRecoveryRequest) PopulateContext(rctx *RequestContext) error {
 }
 
 // Authorize the request.
-func (r *PasswordRecoveryRequest) Authorize(rctx *RequestContext) error {
+func (r *AccountRecoveryRequest) Authorize(rctx *RequestContext) error {
 	// Anyone can request the hint (rate-limit above this layer).
 	if r.RecoveryPassword == "" {
 		return nil
@@ -112,9 +115,9 @@ func (r *PasswordRecoveryRequest) Authorize(rctx *RequestContext) error {
 }
 
 // Serve the request.
-func (r *PasswordRecoveryRequest) Serve(rctx *RequestContext) (interface{}, error) {
-	resp := PasswordRecoveryResponse{
-		Hint: rctx.User.PasswordRecoveryHint,
+func (r *AccountRecoveryRequest) Serve(rctx *RequestContext) (interface{}, error) {
+	resp := AccountRecoveryResponse{
+		Hint: rctx.User.AccountRecoveryHint,
 	}
 
 	// Only attempt to authenticate if the recovery password is
@@ -172,16 +175,16 @@ func (r *ResetPasswordRequest) Serve(rctx *RequestContext) (interface{}, error)
 	return nil, nil
 }
 
-// SetPasswordRecoveryHintRequest lets users set the password recovery hint
+// SetAccountRecoveryHintRequest lets users set the password recovery hint
 // and expected response (secondary password).
-type SetPasswordRecoveryHintRequest struct {
+type SetAccountRecoveryHintRequest struct {
 	PrivilegedRequestBase
 	Hint     string `json:"recovery_hint"`
 	Response string `json:"recovery_response"`
 }
 
 // Validate the request.
-func (r *SetPasswordRecoveryHintRequest) Validate(rctx *RequestContext) error {
+func (r *SetAccountRecoveryHintRequest) Validate(rctx *RequestContext) error {
 	var err *validationError
 	if r.Hint == "" {
 		err = newValidationError(err, "recovery_hint", "mandatory field")
@@ -189,12 +192,12 @@ func (r *SetPasswordRecoveryHintRequest) Validate(rctx *RequestContext) error {
 	if verr := rctx.fieldValidators.password(rctx.Context, r.Response); verr != nil {
 		err = newValidationError(err, "recovery_response", verr.Error())
 	}
-	return err
+	return err.orNil()
 }
 
 // Serve the request.
-func (r *SetPasswordRecoveryHintRequest) Serve(rctx *RequestContext) (interface{}, error) {
-	return nil, rctx.User.setPasswordRecoveryHint(rctx.Context, rctx.TX, r.CurPassword, r.Hint, r.Response)
+func (r *SetAccountRecoveryHintRequest) Serve(rctx *RequestContext) (interface{}, error) {
+	return nil, rctx.User.setAccountRecoveryHint(rctx.Context, rctx.TX, r.CurPassword, r.Hint, r.Response)
 }
 
 // CreateApplicationSpecificPasswordRequest lets users create their own ASPs.
diff --git a/backend/model.go b/backend/model.go
index f3e9d73afc414811cb92cc928491146584aefa02..8add2f31817bed60c68f92b23deab8834643f1af 100644
--- a/backend/model.go
+++ b/backend/model.go
@@ -120,16 +120,17 @@ func newUser(entry *ldap.Entry) (*as.RawUser, error) {
 	//
 	// The case of password recovery attributes is more complex:
 	// the current schema has those on email=, but we'd like to
-	// move them to uid=, so we currently have to support both.
+	// move them to uid=, so we currently have to support both
+	// (but the uid= one takes precedence).
 	uidNumber, _ := strconv.Atoi(entry.GetAttributeValue(uidNumberLDAPAttr)) // nolint
 	user := &as.RawUser{
 		User: as.User{
-			Name:                 entry.GetAttributeValue("uid"),
-			Lang:                 entry.GetAttributeValue(preferredLanguageLDAPAttr),
-			UID:                  uidNumber,
-			PasswordRecoveryHint: entry.GetAttributeValue(recoveryHintLDAPAttr),
-			U2FRegistrations:     decodeU2FRegistrations(entry.GetAttributeValues(u2fRegistrationsLDAPAttr)),
-			HasOTP:               entry.GetAttributeValue(totpSecretLDAPAttr) != "",
+			Name:                entry.GetAttributeValue("uid"),
+			Lang:                entry.GetAttributeValue(preferredLanguageLDAPAttr),
+			UID:                 uidNumber,
+			AccountRecoveryHint: entry.GetAttributeValue(recoveryHintLDAPAttr),
+			U2FRegistrations:    decodeU2FRegistrations(entry.GetAttributeValues(u2fRegistrationsLDAPAttr)),
+			HasOTP:              entry.GetAttributeValue(totpSecretLDAPAttr) != "",
 		},
 		// Remove the legacy LDAP {crypt} prefix on old passwords.
 		Password:         strings.TrimPrefix(entry.GetAttributeValue(passwordLDAPAttr), "{crypt}"),
@@ -261,13 +262,19 @@ func (tx *backendTX) GetUser(ctx context.Context, username string) (*as.RawUser,
 		}
 
 		for _, entry := range result.Entries {
-			// Some user-level attributes are actually stored on the email
-			// object, a shortcoming of the legacy A/I database model. Set
-			// them on the main User object.
+			// Some user-level attributes are actually stored on
+			// the email object, which is desired in some cases,
+			// but in others is a shortcoming of the legacy A/I
+			// database model. Set them on the main User
+			// object. For the latter, attributes on the main User
+			// object take precedence.
 			if isObjectClass(entry, "virtualMailUser") {
-				if s := entry.GetAttributeValue(recoveryHintLDAPAttr); s != "" {
-					user.PasswordRecoveryHint = s
+				if user.AccountRecoveryHint == "" {
+					if s := entry.GetAttributeValue(recoveryHintLDAPAttr); s != "" {
+						user.AccountRecoveryHint = s
+					}
 				}
+
 				user.AppSpecificPasswords = getASPInfo(decodeAppSpecificPasswords(entry.GetAttributeValues(aspLDAPAttr)))
 				user.Keys = decodeUserEncryptionKeys(
 					entry.GetAttributeValues(storagePrivateKeyLDAPAttr))
@@ -297,7 +304,7 @@ func (tx *backendTX) SetUserPassword(ctx context.Context, user *as.User, encrypt
 	return
 }
 
-func (tx *backendTX) SetPasswordRecoveryHint(ctx context.Context, user *as.User, hint, response string) error {
+func (tx *backendTX) SetAccountRecoveryHint(ctx context.Context, user *as.User, hint, response string) error {
 	// Write the password recovery attributes on the uid= object,
 	// as per the new schema.
 	dn := tx.getUserDN(user)
@@ -306,7 +313,7 @@ func (tx *backendTX) SetPasswordRecoveryHint(ctx context.Context, user *as.User,
 	return nil
 }
 
-func (tx *backendTX) DeletePasswordRecoveryHint(ctx context.Context, user *as.User) error {
+func (tx *backendTX) DeleteAccountRecoveryHint(ctx context.Context, user *as.User) error {
 	// Write the password recovery attributes on the uid= object,
 	// as per the new schema.
 	dn := tx.getUserDN(user)
diff --git a/errors.go b/errors.go
index 7ff6ffbceb1c3bae9e059a844d190c8a66fbf60a..691a209d0b34f8332a5f04b83656ba1864b3913d 100644
--- a/errors.go
+++ b/errors.go
@@ -85,8 +85,12 @@ func newValidationError(err *validationError, field, msg string) *validationErro
 
 func (v *validationError) Error() string {
 	var p []string
-	for f, l := range v.fields {
-		p = append(p, fmt.Sprintf("%s: %s", f, strings.Join(l, ", ")))
+	if v.fields != nil {
+		for f, l := range v.fields {
+			p = append(p, fmt.Sprintf("%s: %s", f, strings.Join(l, ", ")))
+		}
+	} else {
+		p = append(p, "INVALID ERROR")
 	}
 	return fmt.Sprintf("request validation error: %s", strings.Join(p, "; "))
 }
@@ -95,3 +99,10 @@ func (v *validationError) JSON() []byte {
 	data, _ := json.Marshal(v.fields) // nolint
 	return data
 }
+
+func (v *validationError) orNil() error {
+	if v == nil {
+		return nil
+	}
+	return v
+}
diff --git a/integrationtest/integration_test.go b/integrationtest/integration_test.go
index 9884f59ce4bf92c6a3d5ecbe4281d793aa0c2c78..355d39d895ad671dda9bdd06e73bd9bf05ab7942 100644
--- a/integrationtest/integration_test.go
+++ b/integrationtest/integration_test.go
@@ -2,6 +2,7 @@ package integrationtest
 
 import (
 	"bytes"
+	"context"
 	"encoding/json"
 	"errors"
 	"fmt"
@@ -17,6 +18,8 @@ import (
 	"git.autistici.org/ai3/accountserver/backend"
 	"git.autistici.org/ai3/accountserver/ldaptest"
 	"git.autistici.org/ai3/accountserver/server"
+	"git.autistici.org/ai3/go-common/pwhash"
+	"git.autistici.org/ai3/go-common/userenckey"
 	"git.autistici.org/id/go-sso"
 	"golang.org/x/crypto/ed25519"
 )
@@ -99,12 +102,16 @@ func (c *testClient) request(uri string, req, out interface{}) error {
 	return json.Unmarshal(data, out)
 }
 
-func startService(t testing.TB) (func(), *testClient) {
+func startService(t testing.TB) (func(), as.Backend, *testClient) {
 	stop := ldaptest.StartServer(t, &ldaptest.Config{
-		Dir:   "../ldaptest",
-		Port:  testLDAPPort,
-		Base:  "dc=example,dc=com",
-		LDIFs: []string{"testdata/base.ldif", "testdata/test1.ldif"},
+		Dir:  "../ldaptest",
+		Port: testLDAPPort,
+		Base: "dc=example,dc=com",
+		LDIFs: []string{
+			"testdata/base.ldif",
+			"testdata/test1.ldif",
+			"testdata/test2.ldif",
+		},
 	})
 
 	be, err := backend.NewLDAPBackend(testLDAPAddr, "cn=manager,dc=example,dc=com", "password", "dc=example,dc=com")
@@ -151,14 +158,14 @@ func startService(t testing.TB) (func(), *testClient) {
 		stop()
 		srv.Close()
 		ssoStop()
-	}, c
+	}, be, c
 }
 
 // Verify that authentication on GetUser works as expected:
 // - users can fetch their own data but not other users'
 // - admins can read everything.
 func TestIntegration_GetUser_Auth(t *testing.T) {
-	stop, c := startService(t)
+	stop, _, c := startService(t)
 	defer stop()
 
 	testdata := []struct {
@@ -194,16 +201,16 @@ func TestIntegration_GetUser_Auth(t *testing.T) {
 	}
 }
 
-// Verify that a user can change their password.
-func TestIntegration_ChangeUserPassword(t *testing.T) {
-	stop, c := startService(t)
+// Verify that a user can't change someone else's password.
+func TestIntegration_ChangeUserPassword_AuthFail(t *testing.T) {
+	stop, _, c := startService(t)
 	defer stop()
 
 	err := c.request("/api/user/change_password", &as.ChangeUserPasswordRequest{
 		PrivilegedRequestBase: as.PrivilegedRequestBase{
 			UserRequestBase: as.UserRequestBase{
 				RequestBase: as.RequestBase{
-					SSO: c.ssoTicket("uno@investici.org"),
+					SSO: c.ssoTicket("due@investici.org"),
 				},
 				Username: "uno@investici.org",
 			},
@@ -212,14 +219,23 @@ func TestIntegration_ChangeUserPassword(t *testing.T) {
 		Password: "new_password",
 	}, nil)
 
-	if err != nil {
-		t.Fatal("ChangePassword", err)
+	if err == nil {
+		t.Fatal("ChangePassword for another user succeeded")
 	}
 }
 
-// Verify that changing the password sets user encryption keys.
-func TestIntegration_ChangeUserPassword_SetsEncryptionKeys(t *testing.T) {
-	stop, c := startService(t)
+// Verify various attempts at changing the password (user has no encryption keys).
+func TestIntegration_ChangeUserPassword(t *testing.T) {
+	runChangeUserPasswordTest(t, "uno@investici.org")
+}
+
+// Verify various attempts at changing the password (user with encryption keys).
+func TestIntegration_ChangeUserPassword_WithEncryptionKeys(t *testing.T) {
+	runChangeUserPasswordTest(t, "due@investici.org")
+}
+
+func runChangeUserPasswordTest(t *testing.T, username string) {
+	stop, _, c := startService(t)
 	defer stop()
 
 	testdata := []struct {
@@ -239,9 +255,9 @@ func TestIntegration_ChangeUserPassword_SetsEncryptionKeys(t *testing.T) {
 			PrivilegedRequestBase: as.PrivilegedRequestBase{
 				UserRequestBase: as.UserRequestBase{
 					RequestBase: as.RequestBase{
-						SSO: c.ssoTicket("uno@investici.org"),
+						SSO: c.ssoTicket(username),
 					},
-					Username: "uno@investici.org",
+					Username: username,
 				},
 				CurPassword: td.password,
 			},
@@ -256,7 +272,7 @@ func TestIntegration_ChangeUserPassword_SetsEncryptionKeys(t *testing.T) {
 }
 
 func TestIntegration_CreateResource(t *testing.T) {
-	stop, c := startService(t)
+	stop, _, c := startService(t)
 	defer stop()
 
 	testdata := []struct {
@@ -376,7 +392,7 @@ func TestIntegration_CreateResource(t *testing.T) {
 }
 
 func TestIntegration_CreateMultipleResources_WithTemplate(t *testing.T) {
-	stop, c := startService(t)
+	stop, _, c := startService(t)
 	defer stop()
 
 	// The create request is very bare, most values will be filled
@@ -407,3 +423,130 @@ func TestIntegration_CreateMultipleResources_WithTemplate(t *testing.T) {
 		t.Errorf("CreateResources failed: %v", err)
 	}
 }
+
+func TestIntegration_CreateUser(t *testing.T) {
+	stop, be, c := startService(t)
+	defer stop()
+
+	// The create request is very bare, most values will be filled
+	// in by the server using resource templates.
+	var resp as.CreateUserResponse
+	err := c.request("/api/user/create", &as.CreateUserRequest{
+		AdminRequestBase: as.AdminRequestBase{
+			RequestBase: as.RequestBase{
+				SSO: c.ssoTicket(testAdminUser),
+			},
+		},
+		User: &as.User{
+			Name: "newuser@example.com",
+			Resources: []*as.Resource{
+				&as.Resource{
+					ID:   as.NewResourceID(as.ResourceTypeEmail, "newuser@example.com", "newuser@example.com"),
+					Name: "newuser@example.com",
+				},
+			},
+		},
+	}, &resp)
+	if err != nil {
+		t.Fatalf("CreateUser failed: %v", err)
+	}
+
+	if resp.Password == "" {
+		t.Fatalf("no password in response (%v)", resp)
+	}
+
+	// Verify that the new password works.
+	checkUserInvariants(t, be, "newuser@example.com", resp.Password)
+}
+
+func TestIntegration_AccountRecovery(t *testing.T) {
+	runAccountRecoveryTest(t, "uno@investici.org")
+}
+
+func TestIntegration_AccountRecovery_WithEncryptionKeys(t *testing.T) {
+	user := runAccountRecoveryTest(t, "due@investici.org")
+	if !user.HasEncryptionKeys {
+		t.Fatalf("encryption keys not enabled after account recovery")
+	}
+}
+
+func runAccountRecoveryTest(t *testing.T, username string) *as.RawUser {
+	stop, be, c := startService(t)
+	defer stop()
+
+	hint := "secret code?"
+	secondaryPw := "open sesame!"
+	err := c.request("/api/user/set_account_recovery_hint", &as.SetAccountRecoveryHintRequest{
+		PrivilegedRequestBase: as.PrivilegedRequestBase{
+			UserRequestBase: as.UserRequestBase{
+				RequestBase: as.RequestBase{
+					SSO: c.ssoTicket(username),
+				},
+				Username: username,
+			},
+			CurPassword: "password",
+		},
+		Hint:     hint,
+		Response: secondaryPw,
+	}, nil)
+	if err != nil {
+		t.Fatalf("SetAccountRecoveryHint failed: %v", err)
+	}
+
+	// The first request just fetches the recovery hint.
+	var resp as.AccountRecoveryResponse
+	err = c.request("/api/recover_account", &as.AccountRecoveryRequest{
+		Username: username,
+	}, &resp)
+	if err != nil {
+		t.Fatalf("AccountRecovery (hint only) failed: %v", err)
+	}
+	if resp.Hint != hint {
+		t.Fatalf("bad AccountRecovery hint, got '%s' expected '%s'", resp.Hint, hint)
+	}
+
+	// Now recover the account and set a new password.
+	newPw := "new password"
+	err = c.request("/api/recover_account", &as.AccountRecoveryRequest{
+		Username:         username,
+		RecoveryPassword: secondaryPw,
+		Password:         newPw,
+	}, &resp)
+	if err != nil {
+		t.Fatalf("AccountRecovery failed: %v", err)
+	}
+
+	return checkUserInvariants(t, be, username, newPw)
+}
+
+// Verify that some user authentication invariants are true. Returns
+// the RawUser for further checks.
+func checkUserInvariants(t *testing.T, be as.Backend, username, primaryPassword string) *as.RawUser {
+	tx, _ := be.NewTransaction()
+	user, err := tx.GetUser(context.Background(), username)
+	if err != nil {
+		t.Fatalf("GetUser(%s): %v", username, err)
+	}
+
+	// Verify that the password is correct.
+	if !pwhash.ComparePassword(user.Password, primaryPassword) {
+		t.Fatalf("password for user %s is not %s", username, primaryPassword)
+	}
+
+	// Verify that we can successfully encrypt keys.
+	if user.HasEncryptionKeys {
+		if _, err := userenckey.Decrypt(keysToBytes(user.Keys), []byte(primaryPassword)); err != nil {
+			t.Fatalf("password for user %s can't decrypt keys", username)
+		}
+	}
+
+	return user
+}
+
+func keysToBytes(keys []*as.UserEncryptionKey) [][]byte {
+	var rawKeys [][]byte
+	for _, k := range keys {
+		rawKeys = append(rawKeys, k.Key)
+	}
+	return rawKeys
+}
diff --git a/integrationtest/testdata/test2.ldif b/integrationtest/testdata/test2.ldif
new file mode 100644
index 0000000000000000000000000000000000000000..989b55e75c39711162eafeda065147a039e91b89
--- /dev/null
+++ b/integrationtest/testdata/test2.ldif
@@ -0,0 +1,87 @@
+dn: uid=due@investici.org,ou=People,dc=example,dc=com
+cn: due@investici.org
+gecos: due@investici.org
+gidNumber: 2000
+givenName: Private
+homeDirectory: /var/empty
+loginShell: /bin/false
+objectClass: top
+objectClass: person
+objectClass: posixAccount
+objectClass: shadowAccount
+objectClass: organizationalPerson
+objectClass: inetOrgPerson
+shadowLastChange: 12345
+shadowMax: 99999
+shadowWarning: 7
+sn: due@investici.org
+uid: due@investici.org
+uidNumber: 256799
+userPassword:: JGEyJDQkMzI3NjgkMSQwZDgyMzU1YjQ0Mzg0M2NmZDY4MjU1MzE4ZTVjYTdiZSRmNTQ0ODkxOTFiNWZlYzk2MDRlNWQ2ODZjMDQxZjJkNTFmOTgxOGY4ZTFmM2E4MDYzY2U3ZTEwMTE3OTc2OGI0
+
+dn: mail=due@investici.org,uid=due@investici.org,ou=People,dc=example,dc=com
+creationDate: 01-08-2013
+gidNumber: 2000
+host: host2
+mail: due@investici.org
+mailMessageStore: investici.org/due/
+objectClass: top
+objectClass: virtualMailUser
+originalHost: host2
+status: active
+uidNumber: 256799
+userPassword:: JGEyJDQkMzI3NjgkMSQwZDgyMzU1YjQ0Mzg0M2NmZDY4MjU1MzE4ZTVjYTdiZSRmNTQ0ODkxOTFiNWZlYzk2MDRlNWQ2ODZjMDQxZjJkNTFmOTgxOGY4ZTFmM2E4MDYzY2U3ZTEwMTE3OTc2OGI0
+storagePublicKey:: LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUU0d0VBWUhLb1pJemowQ0FRWUZLNEVFQUNFRE9nQUVKaDFSdW1MTkt3M1dBTXNSMFFpMVRrc2pJSC9udCtwaApGRjZjdmhPZFN3anhsOVhFVVovVzlONWdDcUlqbGl0eFk2VHdSZWlzZThrPQotLS0tLUVORCBQVUJMSUMgS0VZLS0tLS0K
+storageEncryptedSecretKey:: bWFpbjoBAQAAAAAQAAAEIKLsDnzqCxHAmVXiSi3WT77YqNDY5sW0les/rC0owl0MegC3frvzAGG7vr6PhYNozksn2Fw4tQbj7G52HeQr3V8R58J3F2CHLdiwLGDMKCNy1hjyCN6rXCp1OQqg4VWtEovumogA4FaJtZS74WnCP2YGcxJy/0Am3U2TlFmx0e0jzuCk9lZ8piX+YKR6c8Qh/bv5vjq2gZ9AO2nh5Q==
+
+dn: ftpname=due,uid=due@investici.org,ou=People,dc=example,dc=com
+status: active
+givenName: Private
+cn: due
+objectClass: top
+objectClass: person
+objectClass: posixAccount
+objectClass: shadowAccount
+objectClass: organizationalPerson
+objectClass: inetOrgPerson
+objectClass: ftpAccount
+loginShell: /bin/false
+userPassword:: JGEyJDQkMzI3NjgkMSQwZDgyMzU1YjQ0Mzg0M2NmZDY4MjU1MzE4ZTVjYTdiZSRmNTQ0ODkxOTFiNWZlYzk2MDRlNWQ2ODZjMDQxZjJkNTFmOTgxOGY4ZTFmM2E4MDYzY2U3ZTEwMTE3OTc2OGI0
+shadowWarning: 7
+uidNumber: 256799
+host: host2
+shadowMax: 99999
+ftpname: due
+gidNumber: 33
+gecos: FTP Account for due@investici.org
+sn: Private
+homeDirectory: /home/users/investici.org/due
+uid: due
+creationDate: 01-08-2013
+shadowLastChange: 12345
+originalHost: host2
+
+dn: alias=due,uid=due@investici.org,ou=People,dc=example,dc=com
+status: active
+parentSite: autistici.org
+objectClass: top
+objectClass: subSite
+alias: due
+host: host2
+documentRoot: /home/users/investici.org/due/html-due
+creationDate: 01-08-2013
+originalHost: host2
+statsId: 2193
+option: php
+
+dn: dbname=due,alias=due,uid=due@investici.org,ou=People,dc=example,dc=com
+clearPassword: tae1tei8eir7wae1OZaeXXX
+dbname: due
+dbuser: due
+host: host2
+originalHost: host2
+mysqlPassword: *7DD66AA8CD1E7687B993732C3A87CFFA43B95E27
+objectClass: top
+objectClass: dbMysql
+status: active
+
diff --git a/server/server.go b/server/server.go
index e36de4158220a1beb5ba0256b8aca2bbeb61211d..3cb32aaf7e6eee7ed86e14e9e6a7c854e8d473c2 100644
--- a/server/server.go
+++ b/server/server.go
@@ -34,15 +34,15 @@ func (r *actionRegistry) newRequest(path string) (as.Request, bool) {
 	return reflect.New(h).Interface().(as.Request), true
 }
 
-// AccountServer is the HTTP API interface to AccountService.
-type AccountServer struct {
+// APIServer is the HTTP API interface to AccountService.
+type APIServer struct {
 	*actionRegistry
 	service *as.AccountService
 }
 
-// New creates a new AccountServer.
-func New(service *as.AccountService, backend as.Backend) *AccountServer {
-	s := &AccountServer{
+// New creates a new APIServer.
+func New(service *as.AccountService, backend as.Backend) *APIServer {
+	s := &APIServer{
 		actionRegistry: newActionRegistry(),
 		service:        service,
 	}
@@ -51,7 +51,7 @@ func New(service *as.AccountService, backend as.Backend) *AccountServer {
 	s.Register("/api/user/create", &as.CreateUserRequest{})
 	s.Register("/api/user/update", &as.UpdateUserRequest{})
 	s.Register("/api/user/change_password", &as.ChangeUserPasswordRequest{})
-	s.Register("/api/user/set_password_recovery_hint", &as.SetPasswordRecoveryHintRequest{})
+	s.Register("/api/user/set_account_recovery_hint", &as.SetAccountRecoveryHintRequest{})
 	s.Register("/api/user/enable_otp", &as.EnableOTPRequest{})
 	s.Register("/api/user/disable_otp", &as.DisableOTPRequest{})
 	s.Register("/api/user/create_app_specific_password", &as.CreateApplicationSpecificPasswordRequest{})
@@ -63,14 +63,14 @@ func New(service *as.AccountService, backend as.Backend) *AccountServer {
 	s.Register("/api/resource/reset_password", &as.ResetPasswordRequest{})
 	s.Register("/api/resource/email/add_alias", &as.AddEmailAliasRequest{})
 	s.Register("/api/resource/email/delete_alias", &as.DeleteEmailAliasRequest{})
-	s.Register("/api/recover_password", &as.PasswordRecoveryRequest{})
+	s.Register("/api/recover_account", &as.AccountRecoveryRequest{})
 
 	return s
 }
 
 var emptyResponse struct{}
 
-func (s *AccountServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+func (s *APIServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 	// Create a new empty request object based on the request
 	// path, then decode the HTTP request JSON body onto it.
 	r, ok := s.newRequest(req.URL.Path)
diff --git a/service.go b/service.go
index 0350dd8d4d3b380406167a6ae9c654fe1d759846..837f2bebf02d49c794fb377b74201b4215c20531 100644
--- a/service.go
+++ b/service.go
@@ -47,8 +47,8 @@ type TX interface {
 	UpdateUser(context.Context, *User) error
 	CreateUser(context.Context, *User) error
 	SetUserPassword(context.Context, *User, string) error
-	SetPasswordRecoveryHint(context.Context, *User, string, string) error
-	DeletePasswordRecoveryHint(context.Context, *User) error
+	SetAccountRecoveryHint(context.Context, *User, string, string) error
+	DeleteAccountRecoveryHint(context.Context, *User) error
 	SetUserEncryptionKeys(context.Context, *User, []*UserEncryptionKey) error
 	SetUserEncryptionPublicKey(context.Context, *User, []byte) error
 	SetApplicationSpecificPassword(context.Context, *User, *AppSpecificPasswordInfo, string) error
diff --git a/types.go b/types.go
index e6bac78331b1b7ac7a12092425f1299a507773de..5e5ed5fcef66abaf6d88c07a710fce19c1f968ea 100644
--- a/types.go
+++ b/types.go
@@ -39,7 +39,7 @@ type User struct {
 	// this user.  TODO: consider disabling it.
 	HasEncryptionKeys bool `json:"has_encryption_keys"`
 
-	PasswordRecoveryHint string `json:"password_recovery_hint"`
+	AccountRecoveryHint string `json:"account_recovery_hint"`
 
 	AppSpecificPasswords []*AppSpecificPasswordInfo `json:"app_specific_passwords,omitempty"`
 
@@ -203,7 +203,7 @@ func (u *RawUser) setPrimaryPassword(ctx context.Context, tx TX, unlockPassword,
 
 // Set the password recovery hint for the user. When encryption keys are
 // present, requires a valid unlockPassword.
-func (u *RawUser) setPasswordRecoveryHint(ctx context.Context, tx TX, unlockPassword, hint, response string) error {
+func (u *RawUser) setAccountRecoveryHint(ctx context.Context, tx TX, unlockPassword, hint, response string) error {
 	if u.HasEncryptionKeys {
 		l, err := u.Keys.add(UserEncryptionKeyRecoveryID, unlockPassword, response)
 		if err != nil {
@@ -217,7 +217,7 @@ func (u *RawUser) setPasswordRecoveryHint(ctx context.Context, tx TX, unlockPass
 
 	enc := pwhash.Encrypt(response)
 	u.RecoveryPassword = enc
-	return tx.SetPasswordRecoveryHint(ctx, &u.User, hint, enc)
+	return tx.SetAccountRecoveryHint(ctx, &u.User, hint, enc)
 }
 
 // Initialize encryption keys for this user, given the primary authentication
@@ -232,11 +232,11 @@ func (u *RawUser) initEncryption(ctx context.Context, tx TX, password string) er
 			return err
 		}
 	}
-	if u.PasswordRecoveryHint != "" {
-		if err := tx.DeletePasswordRecoveryHint(ctx, &u.User); err != nil {
+	if u.AccountRecoveryHint != "" {
+		if err := tx.DeleteAccountRecoveryHint(ctx, &u.User); err != nil {
 			return err
 		}
-		u.PasswordRecoveryHint = ""
+		u.AccountRecoveryHint = ""
 	}
 
 	// Initialize a new key storage with the given primary password.