func TestUsernameFormat(t *testing.T) { Convey("Given a funky-spelled email address", t, func() { address := "*****@*****.**" Convey("NormalizeAddress should simplify it", func() { address = utils.NormalizeAddress(address) So(address, ShouldEqual, "*****@*****.**") Convey("And RemoveDots should remove all dots from the username part", func() { address = utils.RemoveDots(address) So(address, ShouldEqual, "*****@*****.**") }) }) }) Convey("Given a string with dots", t, func() { input := "he.....llo." Convey("RemoveDots should remove all dots", func() { input = utils.RemoveDots(input) So(input, ShouldEqual, "hello") }) }) }
func (a *API) oauthToken(c *gin.Context) { // Decode the input var input struct { GrantType string `json:"grant_type"` Code string `json:"code"` ClientID string `json:"client_id"` ClientSecret string `json:"client_secret"` Address string `json:"address"` Password string `json:"password"` ExpiryTime int64 `json:"expiry_time"` } if err := c.Bind(&input); err != nil { c.JSON(422, &gin.H{ "code": CodeGeneralInvalidInput, "message": err.Error(), }) return } // Switch the action switch input.GrantType { case "authorization_code": // Parameters: // - code - authorization code from the app // - client_id - id of the client app // - client_secret - secret of the client app // Fetch the application from database cursor, err := r.Table("applications").Get(input.ClientID).Default(map[string]interface{}{}).Run(a.Rethink) if err != nil { c.JSON(500, &gin.H{ "code": CodeGeneralDatabaseError, "message": err.Error(), }) return } defer cursor.Close() var application *models.Application if err := cursor.One(&application); err != nil { c.JSON(500, &gin.H{ "code": CodeGeneralDatabaseError, "message": err.Error(), }) return } if application.ID == "" { c.JSON(422, &gin.H{ "code": CodeOAuthInvalidApplication, "message": "No such client ID.", }) return } if application.Secret != input.ClientSecret { c.JSON(422, &gin.H{ "code": CodeOAuthInvalidSecret, "message": "Invalid client secret.", }) return } // Fetch the code from the database cursor, err = r.Table("tokens").Get(input.Code).Default(map[string]interface{}{}).Run(a.Rethink) if err != nil { c.JSON(500, &gin.H{ "code": CodeGeneralDatabaseError, "message": err.Error(), }) return } defer cursor.Close() var codeToken *models.Token if err := cursor.One(&codeToken); err != nil { c.JSON(500, &gin.H{ "code": CodeGeneralDatabaseError, "message": err.Error(), }) return } // Ensure token type and matching client id if codeToken.ID == "" || codeToken.Type != "code" || codeToken.ClientID != input.ClientID { c.JSON(422, &gin.H{ "code": CodeOAuthInvalidCode, "message": "Invalid code", }) return } // Create a new authentication code token := &models.Token{ ID: uniuri.NewLen(uniuri.UUIDLen), DateCreated: time.Now(), DateModified: time.Now(), Owner: codeToken.Owner, ExpiryDate: codeToken.ExpiryDate, Type: "auth", Scope: codeToken.Scope, ClientID: input.ClientID, } // Remove code token if err := r.Table("tokens").Get(codeToken.ID).Delete().Exec(a.Rethink); err != nil { c.JSON(500, &gin.H{ "code": CodeGeneralDatabaseError, "message": err.Error(), }) return } // Insert it into database if err := r.Table("tokens").Insert(token).Exec(a.Rethink); err != nil { c.JSON(500, &gin.H{ "code": CodeGeneralDatabaseError, "message": err.Error(), }) return } // Write the token into the response c.JSON(201, token) return case "password": // Parameters: // - address - address in the system // - password - sha256 of the account's password // - client_id - id of the client app used for stats // - expiry_time - seconds until token expires // If there's no domain, append default domain if strings.Index(input.Address, "@") == -1 { input.Address += "@" + a.Options.DefaultDomain } // Normalize the username na := utils.RemoveDots(utils.NormalizeAddress(input.Address)) // Validate input errors := []string{} if !govalidator.IsEmail(na) { errors = append(errors, "Invalid address format.") } var dp []byte if len(input.Password) != 64 { errors = append(errors, "Invalid password length.") } else { var err error dp, err = hex.DecodeString(input.Password) if err != nil { errors = append(errors, "Invalid password format.") } } if input.ExpiryTime == 0 { input.ExpiryTime = 86400 // 24 hours } else if input.ExpiryTime < 0 { errors = append(errors, "Invalid expiry time.") } if input.ClientID == "" { errors = append(errors, "Missing client ID.") } else { cursor, err := r.Table("applications").Get(input.ClientID).Ne(nil).Run(a.Rethink) if err != nil { c.JSON(500, &gin.H{ "code": CodeGeneralDatabaseError, "message": err.Error(), }) return } defer cursor.Close() var appExists bool if err := cursor.One(&appExists); err != nil { c.JSON(500, &gin.H{ "code": CodeGeneralDatabaseError, "message": err.Error(), }) return } if !appExists { errors = append(errors, "Invalid client ID.") } } if len(errors) > 0 { c.JSON(422, &gin.H{ "code": CodeOAuthValidationFailed, "message": "Validation failed.", "errors": errors, }) return } // Fetch the address from the database cursor, err := r.Table("addresses").Get(na).Default(map[string]interface{}{}).Do(func(address r.Term) map[string]interface{} { return map[string]interface{}{ "address": address, "account": r.Branch( address.HasFields("id"), r.Table("accounts").Get(address.Field("owner")), nil, ), } }).Run(a.Rethink) if err != nil { c.JSON(500, &gin.H{ "code": CodeGeneralDatabaseError, "message": err.Error(), }) return } defer cursor.Close() var result struct { Address *models.Address `gorethink:"address"` Account *models.Account `gorethink:"account"` } if err := cursor.One(&result); err != nil { c.JSON(500, &gin.H{ "code": CodeGeneralDatabaseError, "message": err.Error(), }) return } // Verify that both address and account exist if result.Address == nil || result.Address.ID == "" || result.Account == nil || result.Account.ID == "" { c.JSON(401, &gin.H{ "code": CodeOAuthInvalidAddress, "message": "No such address exists", }) return } // Verify the password valid, update, err := result.Account.VerifyPassword(dp) if err != nil { c.JSON(500, &gin.H{ "code": CodeOAuthInvalidPassword, "message": err.Error(), }) return } if update { result.Account.DateModified = time.Now() if err := r.Table("accounts").Get(result.Account.ID).Update(result.Account).Exec(a.Rethink); err != nil { c.JSON(500, &gin.H{ "code": CodeGeneralDatabaseError, "message": err.Error(), }) return } } if !valid { c.JSON(401, &gin.H{ "code": CodeOAuthInvalidPassword, "message": "Invalid password", }) return } // Create a new token token := &models.Token{ ID: uniuri.NewLen(uniuri.UUIDLen), DateCreated: time.Now(), DateModified: time.Now(), Owner: result.Account.ID, ExpiryDate: time.Now().Add(time.Duration(input.ExpiryTime) * time.Second), Type: "auth", Scope: []string{"password_grant"}, ClientID: input.ClientID, } if result.Account.Subscription == "admin" { token.Scope = append(token.Scope, "admin") } // Insert it into database if err := r.Table("tokens").Insert(token).Exec(a.Rethink); err != nil { c.JSON(500, &gin.H{ "code": CodeGeneralDatabaseError, "message": err.Error(), }) return } // Write the token into the response c.JSON(201, token) return case "client_credentials": // Parameters: // - client_id - id of the application // - client_secret - secret of the application c.JSON(501, &gin.H{ "code": CodeGeneralUnimplemented, "message": "Client credentials flow is not implemented.", }) } // Same as default in the switch c.JSON(422, &gin.H{ "code": CodeGeneralInvalidAction, "message": "Validation failed", "errors": []string{"Invalid action"}, }) return }
func (m *Mailer) HandleRecipient(next func(conn *smtpd.Connection)) func(conn *smtpd.Connection) { return func(conn *smtpd.Connection) { // Prepare the context if conn.Environment == nil { conn.Environment = map[string]interface{}{} } if _, ok := conn.Environment["recipients"]; !ok { conn.Environment["recipients"] = []recipient{} } recipients := conn.Environment["recipients"].([]recipient) // Get the most recently added recipient and parse it addr, err := mail.ParseAddress( conn.Envelope.Recipients[len(conn.Envelope.Recipients)-1], ) if err != nil { m.Error(conn, err) return } // Normalize the address addr.Address = utils.RemoveDots(utils.NormalizeAddress(addr.Address)) // Fetch the address and account from database cursor, err := r.Table("addresses").Get(addr.Address).Default(map[string]interface{}{}).Do(func(address r.Term) map[string]interface{} { return map[string]interface{}{ "address": address, "account": r.Branch( address.HasFields("id"), r.Table("accounts").Get(address.Field("owner")), nil, ), "key": r.Branch( address.HasFields("public_key").And(address.Field("public_key").Ne("")), r.Table("keys").Get(address.Field("public_key")).Without("identities"), r.Branch( address.HasFields("id"), r.Table("keys").GetAllByIndex("owner", address.Field("owner")).OrderBy("date_created").CoerceTo("array").Do(func(keys r.Term) r.Term { return r.Branch( keys.Count().Gt(0), keys.Nth(-1).Without("identities"), nil, ) }), nil, ), ), } }).Do(func(data r.Term) r.Term { return data.Merge(map[string]interface{}{ "labels": r.Branch( data.Field("account").HasFields("id"), r.Table("labels").GetAllByIndex("nameOwnerSystem", []interface{}{ "Inbox", data.Field("account").Field("id"), true, }, []interface{}{ "Spam", data.Field("account").Field("id"), true, }).CoerceTo("array").Do(func(result r.Term) r.Term { return r.Branch( result.Count().Eq(2), map[string]interface{}{ "inbox": result.Nth(0).Field("id"), "spam": result.Nth(1).Field("id"), }, nil, ) }), nil, ), }) }).Run(m.Rethink) if err != nil { m.Error(conn, err) return } defer cursor.Close() var result recipient if err := cursor.One(&result); err != nil { m.Error(conn, err) return } // Check if anything got matched if result.Address == nil || result.Address.ID == "" || result.Account == nil || result.Account.ID == "" { conn.Error(errors.New("No such address")) return } if result.Key == nil || result.Labels.Inbox == "" || result.Labels.Spam == "" { conn.Error(errors.New("Account is not configured")) return } // Append the result to the recipients conn.Environment["recipients"] = append(recipients, result) // Run the next handler next(conn) } }
func addressesAdd(c *cli.Context) int { // Connect to RethinkDB _, session, connected := connectToRethinkDB(c) if !connected { return 1 } // Input struct var input struct { ID string `json:"id"` Owner string `json:"owner"` } // Read JSON from stdin if c.Bool("json") { if err := json.NewDecoder(c.App.Env["reader"].(io.Reader)).Decode(&input); err != nil { writeError(c, err) return 1 } } else { // Buffer stdin rd := bufio.NewReader(c.App.Env["reader"].(io.Reader)) var err error // Acquire from interactive input fmt.Fprintf(c.App.Writer, "Address: ") input.ID, err = rd.ReadString('\n') if err != nil { writeError(c, err) return 1 } input.ID = strings.TrimSpace(input.ID) fmt.Fprintf(c.App.Writer, "Owner ID: ") input.Owner, err = rd.ReadString('\n') if err != nil { writeError(c, err) return 1 } input.Owner = strings.TrimSpace(input.Owner) } // First of all, the address. Append domain if it has no such suffix. if strings.Index(input.ID, "@") == -1 { input.ID += "@" + c.GlobalString("default_domain") } // And format it styledID := utils.NormalizeAddress(input.ID) input.ID = utils.RemoveDots(styledID) // Then check if it's taken. cursor, err := r.Table("addresses").Get(input.ID).Ne(nil).Run(session) if err != nil { writeError(c, err) return 1 } defer cursor.Close() var taken bool if err := cursor.One(&taken); err != nil { writeError(c, err) return 1 } if taken { writeError(c, fmt.Errorf("Address %s is already taken", input.ID)) return 1 } // Check if account ID exists cursor, err = r.Table("accounts").Get(input.Owner).Ne(nil).Run(session) if err != nil { writeError(c, err) } defer cursor.Close() var exists bool if err := cursor.One(&exists); err != nil { writeError(c, err) return 1 } if !exists { writeError(c, fmt.Errorf("Account %s doesn't exist", input.ID)) return 1 } // Insert the address into the database address := &models.Address{ ID: input.ID, StyledID: styledID, DateCreated: time.Now(), DateModified: time.Now(), Owner: input.Owner, } if !c.GlobalBool("dry") { if err := r.Table("addresses").Insert(address).Exec(session); err != nil { writeError(c, err) return 1 } } // Write a success message fmt.Fprintf(c.App.Writer, "Created a new address - %s\n", address.StyledID) return 0 }
func accountsAdd(c *cli.Context) int { // Connect to RethinkDB _, session, connected := connectToRethinkDB(c) if !connected { return 1 } // Input struct var input struct { MainAddress string `json:"main_address"` Password string `json:"password"` Subscription string `json:"subscription"` AltEmail string `json:"alt_email"` Status string `json:"status"` } // Read JSON from stdin if c.Bool("json") { if err := json.NewDecoder(c.App.Env["reader"].(io.Reader)).Decode(&input); err != nil { writeError(c, err) return 1 } } else { // Buffer stdin rd := bufio.NewReader(c.App.Env["reader"].(io.Reader)) var err error // Acquire from interactive input fmt.Fprint(c.App.Writer, "Main address: ") input.MainAddress, err = rd.ReadString('\n') if err != nil { writeError(c, err) return 1 } input.MainAddress = strings.TrimSpace(input.MainAddress) fmt.Fprint(c.App.Writer, "Password: "******"Password: "******"Subscription [beta/admin]: ") input.Subscription, err = rd.ReadString('\n') if err != nil { writeError(c, err) return 1 } input.Subscription = strings.TrimSpace(input.Subscription) fmt.Fprint(c.App.Writer, "Alternative address: ") input.AltEmail, err = rd.ReadString('\n') if err != nil { writeError(c, err) return 1 } input.AltEmail = strings.TrimSpace(input.AltEmail) fmt.Fprint(c.App.Writer, "Status [inactive/active/suspended]: ") input.Status, err = rd.ReadString('\n') if err != nil { writeError(c, err) return 1 } input.Status = strings.TrimSpace(input.Status) } // Analyze the input // First of all, the address. Append domain if it has no such suffix. if strings.Index(input.MainAddress, "@") == -1 { input.MainAddress += "@" + c.GlobalString("default_domain") } // And format it styledID := utils.NormalizeAddress(input.MainAddress) input.MainAddress = utils.RemoveDots(styledID) // Then check if it's taken. cursor, err := r.Table("addresses").Get(input.MainAddress).Ne(nil).Run(session) if err != nil { writeError(c, err) return 1 } defer cursor.Close() var taken bool if err := cursor.One(&taken); err != nil { writeError(c, err) return 1 } if taken { writeError(c, fmt.Errorf("Address %s is already taken", input.MainAddress)) return 1 } // If the password isn't 64 characters long, then hash it. var password []byte if len(input.Password) != 64 { hash := sha256.Sum256([]byte(input.Password)) password = hash[:] } else { password, err = hex.DecodeString(input.Password) if err != nil { writeError(c, err) return 1 } } // Subscription has to be beta or admin if input.Subscription != "beta" && input.Subscription != "admin" { writeError(c, fmt.Errorf("Subscription has to be either beta or admin. Got %s.", input.Subscription)) return 1 } // AltEmail must be an email if !govalidator.IsEmail(input.AltEmail) { writeError(c, fmt.Errorf("Email %s has an incorrect format", input.AltEmail)) return 1 } // Status has to be inactive/active/suspended if input.Status != "inactive" && input.Status != "active" && input.Status != "suspended" { writeError(c, fmt.Errorf("Status has to be either inactive, active or suspended. Got %s.", input.Status)) return 1 } // Prepare structs to insert account := &models.Account{ ID: uniuri.NewLen(uniuri.UUIDLen), DateCreated: time.Now(), DateModified: time.Now(), MainAddress: input.MainAddress, Subscription: input.Subscription, AltEmail: input.AltEmail, Status: input.Status, } if err := account.SetPassword([]byte(password)); err != nil { writeError(c, err) return 1 } address := &models.Address{ ID: input.MainAddress, StyledID: styledID, DateCreated: time.Now(), DateModified: time.Now(), Owner: account.ID, } var labels []*models.Label if account.Status != "inactive" { labels = []*models.Label{ &models.Label{ ID: uniuri.NewLen(uniuri.UUIDLen), DateCreated: time.Now(), DateModified: time.Now(), Owner: account.ID, Name: "Inbox", System: true, }, &models.Label{ ID: uniuri.NewLen(uniuri.UUIDLen), DateCreated: time.Now(), DateModified: time.Now(), Owner: account.ID, Name: "Spam", System: true, }, &models.Label{ ID: uniuri.NewLen(uniuri.UUIDLen), DateCreated: time.Now(), DateModified: time.Now(), Owner: account.ID, Name: "Sent", System: true, }, &models.Label{ ID: uniuri.NewLen(uniuri.UUIDLen), DateCreated: time.Now(), DateModified: time.Now(), Owner: account.ID, Name: "Starred", System: true, }, } } // Insert them into database if !c.Bool("dry") { if err := r.Table("addresses").Insert(address).Exec(session); err != nil { writeError(c, err) return 1 } if err := r.Table("accounts").Insert(account).Exec(session); err != nil { writeError(c, err) return 1 } if labels != nil { if err := r.Table("labels").Insert(labels).Exec(session); err != nil { writeError(c, err) return 1 } } } // Write a success message fmt.Fprintf(c.App.Writer, "Created a new account with ID %s\n", account.ID) return 0 }
func (a *API) createAccount(c *gin.Context) { // Decode the input var input struct { Action string `json:"action"` Username string `json:"username"` AltEmail string `json:"alt_email"` Token string `json:"token"` Password string `json:"password"` Address string `json:"address"` } if err := c.Bind(&input); err != nil { c.JSON(422, &gin.H{ "code": 0, "message": err.Error(), }) return } // Switch the action switch input.Action { case "reserve": // Parameters: // - username - desired username // - alt_email - desired email // Normalize the username styledID := utils.NormalizeUsername(input.Username) nu := utils.RemoveDots(styledID) // Validate input: // - len(username) >= 3 && len(username) <= 32 // - email.match(alt_email) errors := []string{} if len(nu) < 3 { errors = append(errors, "Username too short. It must be 3-32 characters long.") } if len(nu) > 32 { errors = append(errors, "Username too long. It must be 3-32 characters long.") } if !govalidator.IsEmail(input.AltEmail) { errors = append(errors, "Invalid alternative e-mail format.") } if len(errors) > 0 { c.JSON(422, &gin.H{ "code": 0, "message": "Validation failed.", "errors": errors, }) return } // Check in the database whether you can register such account cursor, err := r.Table("addresses").Get(nu + "@pgp.st").Ne(nil).Do(func(left r.Term) map[string]interface{} { return map[string]interface{}{ "username": left, "alt_email": r.Table("accounts").GetAllByIndex("alt_email", input.AltEmail).Count().Eq(1), } }).Run(a.Rethink) if err != nil { c.JSON(500, &gin.H{ "code": 0, "message": err.Error(), }) return } defer cursor.Close() var result struct { Username bool `gorethink:"username"` AltEmail bool `gorethink:"alt_email"` } if err := cursor.One(&result); err != nil { c.JSON(500, &gin.H{ "code": 0, "message": err.Error(), }) return } if result.Username || result.AltEmail { errors := []string{} if result.Username { errors = append(errors, "This username is taken.") } if result.AltEmail { errors = append(errors, "This email address is used.") } c.JSON(422, &gin.H{ "code": 0, "message": "Naming conflict", "errors": errors, }) return } // Create an account and an address address := &models.Address{ ID: nu + "@pgp.st", StyledID: styledID + "@pgp.st", DateCreated: time.Now(), Owner: "", // we set it later } account := &models.Account{ ID: uniuri.NewLen(uniuri.UUIDLen), DateCreated: time.Now(), MainAddress: address.ID, Subscription: "beta", AltEmail: input.AltEmail, Status: "inactive", } address.Owner = account.ID // Insert them into the database if err := r.Table("addresses").Insert(address).Exec(a.Rethink); err != nil { c.JSON(500, &gin.H{ "code": 0, "message": err.Error(), }) return } if err := r.Table("accounts").Insert(account).Exec(a.Rethink); err != nil { c.JSON(500, &gin.H{ "code": 0, "message": err.Error(), }) return } // Write a response c.JSON(201, account) return case "activate": // Parameters: // - address - expected address // - token - relevant token for address errors := []string{} if !govalidator.IsEmail(input.Address) { errors = append(errors, "Invalid address format") } if input.Token == "" { errors = append(errors, "Token is missing") } if len(errors) > 0 { c.JSON(422, &gin.H{ "code": 0, "message": "Validation failed", "errors": errors, }) return } // Normalise input.Address input_address := utils.RemoveDots(utils.NormalizeAddress(input.Address)) // Check in the database whether these both exist cursor, err := r.Table("tokens").Get(input.Token).Do(func(left r.Term) r.Term { return r.Branch( left.Ne(nil).And(left.Field("type").Eq("activate")), map[string]interface{}{ "token": left, "account": r.Table("accounts").Get(left.Field("owner")), }, map[string]interface{}{ "token": nil, "account": nil, }, ) }).Run(a.Rethink) if err != nil { c.JSON(500, &gin.H{ "code": 0, "message": err.Error(), }) return } defer cursor.Close() var result struct { Token *models.Token `gorethink:"token"` Account *models.Account `gorethink:"account"` } if err := cursor.One(&result); err != nil { c.JSON(500, &gin.H{ "code": 0, "message": err.Error(), }) return } if result.Token == nil { c.JSON(422, &gin.H{ "code": 0, "message": "Activation failed", "errors": []string{"Invalid token"}, }) return } if result.Account.MainAddress != input_address { c.JSON(422, &gin.H{ "code": 0, "message": "Activation failed", "errors": []string{"Address does not match token"}, }) return } // Everything seems okay, let's go ahead and delete the token if err := r.Table("tokens").Get(result.Token.ID).Delete().Exec(a.Rethink); err != nil { c.JSON(500, &gin.H{ "code": 0, "message": err.Error(), }) return } // token has been deleted now, let's do some post deletion checks if result.Token.IsExpired() { errors = append(errors, "Token expired") } if result.Account.Status != "inactive" { errors = append(errors, "Account already active") } if len(errors) > 0 { c.JSON(422, &gin.H{ "code": 0, "message": "Activation failed", "errors": errors, }) return } // make the account active, welcome to pgp.st, new guy! if err := r.Table("accounts").Get(result.Account.ID).Update(map[string]interface{}{ "status": "active", "date_modified": time.Now(), }).Exec(a.Rethink); err != nil { c.JSON(500, &gin.H{ "code": 0, "message": err.Error(), }) return } // Create labels for that user if err := r.Table("labels").Insert([]*models.Label{ &models.Label{ ID: uniuri.NewLen(uniuri.UUIDLen), DateCreated: time.Now(), DateModified: time.Now(), Owner: result.Account.ID, Name: "Inbox", System: true, }, &models.Label{ ID: uniuri.NewLen(uniuri.UUIDLen), DateCreated: time.Now(), DateModified: time.Now(), Owner: result.Account.ID, Name: "Spam", System: true, }, &models.Label{ ID: uniuri.NewLen(uniuri.UUIDLen), DateCreated: time.Now(), DateModified: time.Now(), Owner: result.Account.ID, Name: "Sent", System: true, }, &models.Label{ ID: uniuri.NewLen(uniuri.UUIDLen), DateCreated: time.Now(), DateModified: time.Now(), Owner: result.Account.ID, Name: "Starred", System: true, }, }).Exec(a.Rethink); err != nil { c.JSON(500, &gin.H{ "code": 0, "message": err.Error(), }) return } // Temporary response... c.JSON(201, &gin.H{ "id": result.Account.ID, "message": "Activation successful", }) return } // Same as default in the switch c.JSON(422, &gin.H{ "code": 0, "message": "Validation failed", "errors": []string{"Invalid action"}, }) return }