Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 7 additions & 8 deletions db/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,14 @@ var (

// Indexes of tables in the database
var tableIndexes = map[string][]string{
"tokens": []string{"user", "user_id"},
"accounts": []string{"name"},
"emails": []string{"user_id"},
"drafts": []string{"user_id"},
"contacts": []string{},
"threads": []string{"user_id"},
"labels": []string{},
"keys": []string{},
"reservations": []string{},
"contacts": []string{"owner"},
"emails": []string{"owner", "label_ids"},
"keys": []string{"owner", "key_id"},
"labels": []string{"owner"},
"reservations": []string{"email", "name"},
"threads": []string{"owner"},
"tokens": []string{"owner"},
}

// List of names of databases
Expand Down
85 changes: 81 additions & 4 deletions db/table_emails.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,31 @@ func (e *EmailsTable) List(
sort []string,
offset int,
limit int,
label string,
) ([]*models.Email, error) {
// Filter by owner's ID
term := e.GetTable().Filter(map[string]interface{}{
"owner": owner,
})

var term gorethink.Term

if owner != "" && label != "" {
term = e.GetTable().Filter(func(row gorethink.Term) gorethink.Term {
return gorethink.And(
row.Field("owner").Eq(owner),
row.Field("label_ids").Contains(label),
)
})
}

if owner != "" && label == "" {
term = e.GetTable().Filter(map[string]interface{}{
"owner": owner,
})
}

if owner == "" && label != "" {
term = e.GetTable().Filter(func(row gorethink.Term) gorethink.Term {
return row.Field("label_ids").Contains(label)
})
}

// If sort array has contents, parse them and add to the term
if sort != nil && len(sort) > 0 {
Expand Down Expand Up @@ -102,3 +122,60 @@ func (e *EmailsTable) List(

return resp, nil
}

func (e *EmailsTable) GetByLabel(label string) ([]*models.Email, error) {
var result []*models.Email

cursor, err := e.GetTable().Filter(func(row gorethink.Term) gorethink.Term {
return row.Field("label_ids").Contains(label)
}).GetAll().Run(e.GetSession())
if err != nil {
return nil, err
}

err = cursor.All(&result)
if err != nil {
return nil, err
}

return result, nil
}

func (e *EmailsTable) CountByLabel(label string) (int, error) {
var result int

cursor, err := e.GetTable().Filter(func(row gorethink.Term) gorethink.Term {
return row.Field("label_ids").Contains(label)
}).Count().Run(e.GetSession())
if err != nil {
return 0, err
}

err = cursor.One(&result)
if err != nil {
return 0, err
}

return result, nil
}

func (e *EmailsTable) CountByLabelUnread(label string) (int, error) {
var result int

cursor, err := e.GetTable().Filter(func(row gorethink.Term) gorethink.Term {
return gorethink.And(
row.Field("label_ids").Contains(label),
row.Field("is_read").Eq(false),
)
}).Count().Run(e.GetSession())
if err != nil {
return 0, err
}

err = cursor.One(&result)
if err != nil {
return 0, err
}

return result, nil
}
159 changes: 159 additions & 0 deletions db/table_labels.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package db

import (
"time"

"github.com/dancannon/gorethink"

"github.com/lavab/api/cache"
"github.com/lavab/api/models"
)

type LabelsTable struct {
RethinkCRUD
Emails *EmailsTable
Cache cache.Cache
Expires time.Duration
}

func (l *LabelsTable) Insert(data interface{}) error {
if err := l.RethinkCRUD.Insert(data); err != nil {
return err
}

label, ok := data.(*models.Token)
if !ok {
return nil
}

return l.Cache.Set(l.RethinkCRUD.GetTableName()+":"+label.ID, label, l.Expires)
}

// Update clears all updated keys
func (l *LabelsTable) Update(data interface{}) error {
if err := l.RethinkCRUD.Update(data); err != nil {
return err
}

return l.Cache.DeleteMask(l.RethinkCRUD.GetTableName() + ":*")
}

// UpdateID updates the specified label and updates cache
func (l *LabelsTable) UpdateID(id string, data interface{}) error {
if err := l.RethinkCRUD.UpdateID(id, data); err != nil {
return err
}

label, err := l.GetLabel(id)
if err != nil {
return err
}

return l.Cache.Set(l.RethinkCRUD.GetTableName()+":"+id, label, l.Expires)
}

// Delete removes from db and cache using filter
func (l *LabelsTable) Delete(cond interface{}) error {
result, err := l.GetTable().Filter(cond).Delete(gorethink.DeleteOpts{
ReturnChanges: true,
}).RunWrite(l.GetSession())
if err != nil {
return err
}

var ids []interface{}
for _, change := range result.Changes {
ids = append(ids, l.RethinkCRUD.GetTableName()+":"+change.OldValue.(map[string]interface{})["id"].(string))
}

return l.Cache.DeleteMulti(ids...)
}

// DeleteID removes from db and cache using id query
func (l *LabelsTable) DeleteID(id string) error {
label, err := l.GetLabel(id)
if err != nil {
return err
}

if err := l.RethinkCRUD.DeleteID(l.RethinkCRUD.GetTableName() + ":" + id); err != nil {
return err
}

l.Cache.Delete(l.RethinkCRUD.GetTableName() + ":" + id)
l.Cache.Delete(l.RethinkCRUD.GetTableName() + ":owner:" + label.Owner)

return nil
}

func (l *LabelsTable) GetLabel(id string) (*models.Label, error) {
var result models.Label

if err := l.Cache.Get(l.RethinkCRUD.GetTableName()+":"+id, &result); err == nil {
return &result, nil
}

if err := l.FindFetchOne(id, &result); err != nil {
return nil, err
}

totalCount, err := l.Emails.CountByLabel(result.ID)
if err != nil {
return nil, err
}

result.EmailsTotal = totalCount

unreadCount, err := l.Emails.CountByLabelUnread(result.ID)
if err != nil {
return nil, err
}

result.EmailsUnread = unreadCount

err = l.Cache.Set(l.RethinkCRUD.GetTableName()+":"+id, result, l.Expires)
if err != nil {
return nil, err
}

return &result, nil
}

// GetOwnedBy returns all labels owned by id
func (l *LabelsTable) GetOwnedBy(id string) ([]*models.Label, error) {
var result []*models.Label

if err := l.Cache.Get(l.RethinkCRUD.GetTableName()+":owner:"+id, &result); err == nil {
return result, nil
}

err := l.WhereAndFetch(map[string]interface{}{
"owner": id,
}, &result)
if err != nil {
return nil, err
}

for i := range result {
totalCount, err := l.Emails.CountByLabel(result[i].ID)
if err != nil {
return nil, err
}

result[i].EmailsTotal = totalCount

unreadCount, err := l.Emails.CountByLabelUnread(result[i].ID)
if err != nil {
return nil, err
}

result[i].EmailsTotal = unreadCount
}

err = l.Cache.Set(l.RethinkCRUD.GetTableName()+":owner:"+id, result, l.Expires)
if err != nil {
return nil, err
}

return result, nil
}
2 changes: 1 addition & 1 deletion db/table_tokens.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ func (t *TokensTable) DeleteID(id string) error {

// FindFetchOne tries cache and then tries using DefaultCRUD's fetch operation
func (t *TokensTable) FindFetchOne(id string, value interface{}) error {
if err := t.Cache.Get(id, value); err == nil {
if err := t.Cache.Get(t.RethinkCRUD.GetTableName()+":"+id, value); err == nil {
return nil
}

Expand Down
2 changes: 2 additions & 0 deletions env/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ var (
Reservations *db.ReservationsTable
// Emails is the global instance of EmailsTable
Emails *db.EmailsTable
// Labels is the global instance of LabelsTable
Labels *db.LabelsTable
// Factors contains all currently registered factors
Factors map[string]factor.Factor
// NATS is the encoded connection to the NATS queue
Expand Down
2 changes: 2 additions & 0 deletions models/email.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,6 @@ type Email struct {
ThreadID string `json:"thread_id" gorethink:"thread_id"`

Status string `json:"status" gorethink:"status"`

IsRead string `json:"is_read" gorethink:"is_read"`
}
9 changes: 2 additions & 7 deletions models/label.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,6 @@ type Label struct {
// Examples: inbox, trash, spam, drafts, starred, etc.
Builtin bool `json:"builtin" gorethink:"builtin"`

// EmailsUnread is the number of unread emails that have a particular label applied.
// Storing this for each label eliminates the need of db lookups for this commonly needed information.
EmailsUnread int `json:"emails_unread" gorethink:"emails_unread"`

// EmailsTotal is the number of emails that have a particular label applied.
// Storing this for each label eliminates the need of db lookups for this commonly needed information.
EmailsTotal int `json:"emails_total" gorethink:"emails_total"`
EmailsUnread int `json:"emails_unread" gorethink:"-"`
EmailsTotal int `json:"emails_total" gorethink:"-"`
}
2 changes: 2 additions & 0 deletions models/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ type Token struct {

// Type describes the token's purpose: auth, invite, confirm, upgrade.
Type string `json:"type" gorethink:"type"`

Email string `json:"email,omitempty" gorethink:"email"`
}

// MakeToken creates a generic token.
Expand Down
47 changes: 40 additions & 7 deletions routes/accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,13 @@ func AccountsCreate(w http.ResponseWriter, r *http.Request) {
return
}

// Both username and password are filled, so we can create a new account.
account := &models.Account{
Resource: models.MakeResource("", input.Username),
Type: "beta",
AltEmail: input.AltEmail,
}

// Check "invited" for token validity
if requestType == "invited" {
// Fetch the token from the database
Expand Down Expand Up @@ -216,17 +223,12 @@ func AccountsCreate(w http.ResponseWriter, r *http.Request) {
})
return
}

account.AltEmail = token.Email
}

// TODO: sanitize user name (i.e. remove caps, periods)

// Both username and password are filled, so we can create a new account.
account := &models.Account{
Resource: models.MakeResource("", input.Username),
Type: "beta",
AltEmail: input.AltEmail,
}

// Set the password
err = account.SetPassword(input.Password)
if err != nil {
Expand Down Expand Up @@ -264,6 +266,37 @@ func AccountsCreate(w http.ResponseWriter, r *http.Request) {
return
}

// Create labels
err = env.Labels.Insert([]*models.Label{
&models.Label{
Resource: models.MakeResource(account.ID, "Inbox"),
Builtin: true,
},
&models.Label{
Resource: models.MakeResource(account.ID, "Trash"),
Builtin: true,
},
&models.Label{
Resource: models.MakeResource(account.ID, "Spam"),
Builtin: true,
},
&models.Label{
Resource: models.MakeResource(account.ID, "Starred"),
Builtin: true,
},
})
if err != nil {
utils.JSONResponse(w, 500, &AccountsCreateResponse{
Success: false,
Message: "Internal server error - AC/CR/03",
})

env.Log.WithFields(logrus.Fields{
"error": err.Error(),
}).Error("Could not insert labels into the database")
return
}

// Send the email if classic and return a response
if requestType == "classic" {
// TODO: Send emails
Expand Down
Loading