func (a *API) createResource(c *gin.Context) { // Get token and account info from the context var ( account = c.MustGet("account").(*models.Account) token = c.MustGet("token").(*models.Token) ) // Check the scope if !models.InScope(token.Scope, []string{"resources:create"}) { c.JSON(403, &gin.H{ "code": 0, "error": "Your token has insufficient scope", }) return } // Decode the input var input struct { Meta map[string]interface{} `json:"meta"` Body []byte `json:"body"` Tags []string `json:"tags"` } if err := c.Bind(&input); err != nil { c.JSON(422, &gin.H{ "code": 0, "message": err.Error(), }) return } // Save it into database resource := &models.Resource{ ID: uniuri.NewLen(uniuri.UUIDLen), DateCreated: time.Now(), DateModified: time.Now(), Owner: account.ID, Meta: input.Meta, Body: input.Body, Tags: input.Tags, } // Insert it into database if err := r.Table("resources").Insert(resource).Exec(a.Rethink); err != nil { c.JSON(500, &gin.H{ "code": 0, "message": err.Error(), }) return } c.JSON(201, resource) }
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) HandleDelivery(next func(conn *smtpd.Connection)) func(conn *smtpd.Connection) { return func(conn *smtpd.Connection) { // Context variables var ( isSpam = false ctxID = uniuri.NewLen(uniuri.UUIDLen) ) // Check for spam spamReply, err := m.Spam.Report(string(conn.Envelope.Data)) if err != nil { m.Log.WithFields(logrus.Fields{ "ctx_id": ctxID, "err": err, }).Error("Unable to check an email in spamd") } if spamReply != nil && spamReply.Code == spamc.EX_OK { if spam, ok := spamReply.Vars["isSpam"]; ok && spam.(bool) { isSpam = true } } // Trim the spaces from the input conn.Envelope.Data = bytes.TrimSpace(conn.Envelope.Data) // First run the analysis algorithm to generate an email description node := &models.EmailNode{} if err := analyzeEmail(node, conn.Envelope.Data); err != nil { m.Error(conn, err) return } // Calculate message ID messageID := node.Headers.Get("Message-ID") x1i := strings.Index(messageID, "<") if x1i != -1 { x2i := strings.Index(messageID[x1i+1:], ">") if x2i != -1 { messageID = messageID[x1i+1 : x1i+x2i+1] } } if messageID == "" { messageID = uniuri.NewLen(uniuri.UUIDLen) + "@invalid-incoming.pgp.st" } // Generate the members field fromHeader, err := mail.ParseAddress(node.Headers.Get("From")) if err != nil { m.Error(conn, err) return } members := []string{fromHeader.Address} toHeader, err := node.Headers.AddressList("From") if err != nil { m.Error(conn, err) return } for _, to := range toHeader { members = append(members, to.Address) } if x := node.Headers.Get("CC"); x != "" { ccHeader, err := node.Headers.AddressList("CC") if err != nil { m.Error(conn, err) return } for _, cc := range ccHeader { members = append(members, cc.Address) } } // Get the recipients list from the scope recipients := conn.Environment["recipients"].([]recipient) for _, recipient := range recipients { // Parse the found key keyring, err := openpgp.ReadKeyRing(bytes.NewReader(recipient.Key.Body)) if err != nil { m.Error(conn, err) return } // Prepare a new email object email := &models.Email{ ID: uniuri.NewLen(uniuri.UUIDLen), DateCreated: time.Now(), DateModified: time.Now(), Owner: recipient.Account.ID, MessageID: messageID, Status: "received", } // Generate a new key key := make([]byte, 32) if _, err := io.ReadFull(rand.Reader, key); err != nil { m.Error(conn, err) return } akey := [32]byte{} copy(akey[:], key) // Generate a new nonce nonce := make([]byte, 8) if _, err = rand.Read(nonce); err != nil { m.Error(conn, err) return } // Set up a new stream stream, err := chacha20.New(key, nonce) if err != nil { m.Error(conn, err) return } // Prepare output ciphertext := make( []byte, len(conn.Envelope.Data)+ chacha20.NonceSize+ (len(conn.Envelope.Data)/1024+1)*16, ) // First write the nonce copy(ciphertext[:chacha20.NonceSize], nonce) // Then write the authenticated encrypted stream oi := chacha20.NonceSize for i := 0; i < len(conn.Envelope.Data); i += 1024 { // Get the chunk max := i + 1024 if max > len(conn.Envelope.Data) { max = len(conn.Envelope.Data) } chunk := conn.Envelope.Data[i:max] // Encrypt the block stream.XORKeyStream(ciphertext[oi:oi+len(chunk)], chunk) // Authenticate it var out [16]byte poly1305.Sum(&out, ciphertext[oi:oi+len(chunk)], &akey) // Write it into the result copy(ciphertext[oi+len(chunk):oi+len(chunk)+poly1305.TagSize], out[:]) // Increase the counter oi += 1024 + poly1305.TagSize } // Save it to the body email.Body = ciphertext // Create a manifest and encrypt it manifest, err := json.Marshal(models.Manifest{ Key: key, Nonce: nonce, Description: node, }) if err != nil { m.Error(conn, err) return } encryptedManifest, err := utils.PGPEncrypt(manifest, keyring) if err != nil { m.Error(conn, err) return } email.Manifest = encryptedManifest // Match the thread var thread *models.Thread // Get the References header var references string if x := node.Headers.Get("In-Reply-To"); x != "" { references = x } else if x := node.Headers.Get("References"); x != "" { references = x } // Match by Message-ID if references != "" { // As specified in http://www.jwz.org/doc/threading.html, first thing <> is the msg id // We support both <message-id> and message-id format. x1i := strings.Index(references, "<") if x1i != -1 { x2i := strings.Index(references[x1i+1:], ">") if x2i != -1 { references = references[x1i+1 : x1i+x2i+1] } } // Look up the message ID in the database cursor, err := r.Table("emails").GetAllByIndex("messageIDOwner", []interface{}{ references, recipient.Account.ID, }).CoerceTo("array").Do(func(emails r.Term) r.Term { return r.Branch( emails.Count().Eq(1), r.Table("threads").Get(emails.Nth(0).Field("thread")).Default(map[string]interface{}{}), map[string]interface{}{}, ) }).Run(m.Rethink) if err != nil { m.Error(conn, err) return } defer cursor.Close() if err := cursor.One(&thread); err != nil { m.Error(conn, err) return } // Check if we've found it, clear it if it's invalid if thread.ID == "" { thread = nil } } // We can't match it by subject, so proceed to create a new thread if thread == nil { var secure string if findEncrypted(node) { secure = "all" } else { secure = "none" } labels := []string{recipient.Labels.Inbox} if isSpam { labels = append(labels, recipient.Labels.Spam) } thread = &models.Thread{ ID: uniuri.NewLen(uniuri.UUIDLen), DateCreated: time.Now(), DateModified: time.Now(), Owner: recipient.Account.ID, Labels: labels, Members: members, Secure: secure, } if err := r.Table("threads").Insert(thread).Exec(m.Rethink); err != nil { m.Error(conn, err) return } } else { // Modify the existing thread foundInbox := false foundSpam := false for _, label := range thread.Labels { if label == recipient.Labels.Inbox { foundInbox = true } if isSpam { if label == recipient.Labels.Spam { foundSpam = true } if foundInbox && foundSpam { break } } else { if foundInbox { break } } } // Append to thread.Labels if !foundInbox { thread.Labels = append(thread.Labels, recipient.Labels.Inbox) } if !foundSpam && isSpam { thread.Labels = append(thread.Labels, recipient.Labels.Spam) } // Members update membersHash := map[string]struct{}{} for _, member := range thread.Members { membersHash[member] = struct{}{} } for _, member := range members { if _, ok := membersHash[member]; !ok { membersHash[member] = struct{}{} } } thread.Members = []string{} for member := range membersHash { thread.Members = append(thread.Members, member) } update := map[string]interface{}{ "date_modified": time.Now(), "is_read": false, "labels": thread.Labels, "members": thread.Members, } secure := findEncrypted(node) if (thread.Secure == "all" && !secure) || (thread.Secure == "none" && secure) { thread.Secure = "some" } if err := r.Table("threads").Get(thread.ID).Update(update).Exec(m.Rethink); err != nil { m.Error(conn, err) return } } email.Thread = thread.ID if err := r.Table("emails").Insert(email).Exec(m.Rethink); err != nil { m.Error(conn, err) return } m.Log.WithFields(logrus.Fields{ "address": recipient.Address.ID, "account": recipient.Account.MainAddress, }).Info("Email received") } next(conn) } }
func applicationsAdd(c *cli.Context) int { // Connect to RethinkDB _, session, connected := connectToRethinkDB(c) if !connected { return 1 } // Input struct var input struct { Owner string `json:"owner"` Callback string `json:"callback"` Homepage string `json:"homepage"` Name string `json:"name"` Description string `json:"description"` } // 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, "Owner's ID: ") input.Owner, err = rd.ReadString('\n') if err != nil { writeError(c, err) return 1 } input.Owner = strings.TrimSpace(input.Owner) fmt.Fprint(c.App.Writer, "Application's name: ") input.Name, err = rd.ReadString('\n') if err != nil { writeError(c, err) return 1 } input.Name = strings.TrimSpace(input.Name) fmt.Fprint(c.App.Writer, "Homepage URL: ") input.Homepage, err = rd.ReadString('\n') if err != nil { writeError(c, err) return 1 } input.Homepage = strings.TrimSpace(input.Homepage) fmt.Fprint(c.App.Writer, "Description: ") input.Description, err = rd.ReadString('\n') if err != nil { writeError(c, err) return 1 } input.Description = strings.TrimSpace(input.Description) fmt.Fprint(c.App.Writer, "Callback URL: ") input.Callback, err = rd.ReadString('\n') if err != nil { writeError(c, err) return 1 } input.Callback = strings.TrimSpace(input.Callback) } // Validate the input // Check if account ID exists cursor, err := r.Table("accounts").Get(input.Owner).Ne(nil).Run(session) if err != nil { writeError(c, err) return 1 } 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.Owner)) return 1 } // Homepage URL should be a URL if !govalidator.IsURL(input.Homepage) { writeError(c, fmt.Errorf("%s is not a URL", input.Homepage)) return 1 } // Callback URL should be a URL if !govalidator.IsURL(input.Callback) { writeError(c, fmt.Errorf("%s is not a URL", input.Callback)) return 1 } // Insert into database application := &models.Application{ ID: uniuri.NewLen(uniuri.UUIDLen), DateCreated: time.Now(), DateModified: time.Now(), Owner: input.Owner, Secret: uniuri.NewLen(32), Callback: input.Callback, Homepage: input.Homepage, Name: input.Name, Description: input.Description, } if !c.GlobalBool("dry") { if err := r.Table("applications").Insert(application).Exec(session); err != nil { writeError(c, err) return 1 } } // Write a success message fmt.Fprintf(c.App.Writer, "Created a new application with ID %s\n", application.ID) return 0 }
func tokensAdd(c *cli.Context) int { // Connect to RethinkDB _, session, connected := connectToRethinkDB(c) if !connected { return 1 } // Input struct var input struct { Owner string `json:"owner"` ExpiryDate time.Time `json:"expiry_date"` Type string `json:"type"` Scope []string `json:"scope"` ClientID string `json:"client_id"` } // 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, "Owner's ID: ") input.Owner, err = rd.ReadString('\n') if err != nil { writeError(c, err) return 1 } input.Owner = strings.TrimSpace(input.Owner) fmt.Fprint(c.App.Writer, "Type [auth/activate/code]: ") input.Type, err = rd.ReadString('\n') if err != nil { writeError(c, err) return 1 } input.Type = strings.TrimSpace(input.Type) fmt.Fprint(c.App.Writer, "Expiry date [2006-01-02T15:04:05Z07:00/empty]: ") expiryDate, err := rd.ReadString('\n') if err != nil { writeError(c, err) return 1 } expiryDate = strings.TrimSpace(expiryDate) if expiryDate != "" { input.ExpiryDate, err = time.Parse(time.RFC3339, expiryDate) if err != nil { writeError(c, err) return 1 } } if input.Type == "auth" || input.Type == "code" { fmt.Fprint(c.App.Writer, "Client ID: ") input.ClientID, err = rd.ReadString('\n') if err != nil { writeError(c, err) return 1 } input.ClientID = strings.TrimSpace(input.ClientID) fmt.Fprint(c.App.Writer, "Scope (seperated by commas): ") scope, err := rd.ReadString('\n') if err != nil { writeError(c, err) return 1 } scope = strings.TrimSpace(scope) input.Scope = strings.Split(scope, ",") } } // Validate the input // Type has to be either auth or activate if input.Type != "auth" && input.Type != "activate" && input.Type != "code" { writeError(c, fmt.Errorf("Token type must be either auth or activate. Got %s.", input.Type)) return 1 } // Scopes must exist if input.Scope != nil && len(input.Scope) > 0 { for _, scope := range input.Scope { if _, ok := models.Scopes[scope]; !ok { writeError(c, fmt.Errorf("Scope %s doesn't exist", scope)) return 1 } } } // Owner must exist 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.Owner)) return 1 } // Application must exist if input.ClientID != "" { cursor, err = r.Table("applications").Get(input.ClientID).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("Application %s doesn't exist", input.ClientID)) return 1 } } // Insert into database token := &models.Token{ ID: uniuri.NewLen(uniuri.UUIDLen), DateCreated: time.Now(), DateModified: time.Now(), Owner: input.Owner, ExpiryDate: input.ExpiryDate, Type: input.Type, Scope: input.Scope, ClientID: input.ClientID, } if !c.GlobalBool("dry") { if err := r.Table("tokens").Insert(token).Exec(session); err != nil { writeError(c, err) return 1 } } // Write a success message fmt.Fprintf(c.App.Writer, "Created a new %s token with ID %s\n", token.Type, token.ID) return 0 }
func (a *API) createToken(c *gin.Context) { // Get token and account info from the context var ( account = c.MustGet("account").(*models.Account) currentToken = c.MustGet("token").(*models.Token) ) if !models.InScope(currentToken.Scope, []string{"tokens:oauth"}) { c.JSON(403, &gin.H{ "code": 0, "error": "Your token has insufficient scope", }) return } // Decode the input var input struct { Type string `json:"type"` ClientID string `json:"client_id"` Scope []string `json:"scope"` ExpiryTime int64 `json:"expiry_time"` } if err := c.Bind(&input); err != nil { c.JSON(422, &gin.H{ "code": 0, "message": err.Error(), }) return } // Run the validation errors := []string{} // Type has to be code if input.Type != "code" && input.Type != "auth" { errors = append(errors, "Only \"code\" token can be created using this endpoint.") } // Scope must contain proper scopes sm := map[string]struct{}{} for _, scope := range input.Scope { if _, ok := models.Scopes[scope]; !ok { errors = append(errors, "Scope \""+scope+"\" does not exist.") } else { sm[scope] = struct{}{} } } if _, ok := sm["password_grant"]; ok { errors = append(errors, "You can not request the password grant scope.") } if _, ok := sm["admin"]; ok && account.Subscription != "admin" { errors = append(errors, "You can not request the admin scope.") } // Expiry time must be valid if input.ExpiryTime == 0 { input.ExpiryTime = 86400 } else if input.ExpiryTime < 0 { errors = append(errors, "Invalid expiry time.") } // Client ID has to be an application ID var application *models.Application if input.ClientID == "" { errors = append(errors, "Client ID is missing.") } else { cursor, err := r.Table("applications").Get(input.ClientID).Default(map[string]interface{}{}).Run(a.Rethink) if err != nil { c.JSON(500, &gin.H{ "code": 0, "message": err.Error(), }) return } defer cursor.Close() if err := cursor.One(&application); err != nil { c.JSON(500, &gin.H{ "code": 0, "message": err.Error(), }) return } if application.ID == "" { errors = append(errors, "There is no such application.") } } // Abort the request if there are errors if len(errors) > 0 { c.JSON(422, &gin.H{ "code": 0, "message": "Validation failed.", "errors": errors, }) return } // Create a new token token := &models.Token{ ID: uniuri.NewLen(uniuri.UUIDLen), DateCreated: time.Now(), DateModified: time.Now(), Owner: account.ID, ExpiryDate: time.Now().Add(time.Duration(input.ExpiryTime) * time.Second), Type: input.Type, Scope: input.Scope, ClientID: input.ClientID, } // Insert it into database if err := r.Table("tokens").Insert(token).Exec(a.Rethink); err != nil { c.JSON(500, &gin.H{ "code": 0, "message": err.Error(), }) return } // Write the token into the response c.JSON(201, token) return }
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 }