Browse Source

Implement ondemand folder stats update

- Add notifier functionality to update folder statistics
- Remove deprecated folder statistics request
- Rework basic folder fetch mechanism
Alexey Edelev 3 years ago
parent
commit
f24e26eec3
8 changed files with 256 additions and 144 deletions
  1. 85 27
      common/gostfix.pb.go
  2. 6 0
      common/gostfix.proto
  3. 1 0
      common/mailutils.go
  4. 1 1
      common/notifier.go
  5. 79 55
      db/db.go
  6. 33 28
      web/js/index.js
  7. 13 11
      web/mailbox.go
  8. 38 22
      web/webnotifier.go

+ 85 - 27
common/gostfix.pb.go

@@ -444,6 +444,61 @@ func (m *Folder) GetCustom() bool {
 	return false
 }
 
+type FolderStat struct {
+	Folder               string   `protobuf:"bytes,1,opt,name=folder,proto3" json:"folder,omitempty"`
+	Total                uint32   `protobuf:"varint,2,opt,name=total,proto3" json:"total,omitempty"`
+	Unread               uint32   `protobuf:"varint,3,opt,name=unread,proto3" json:"unread,omitempty"`
+	XXX_NoUnkeyedLiteral struct{} `json:"-" bson:"-"`
+	XXX_unrecognized     []byte   `json:"-" bson:"-"`
+	XXX_sizecache        int32    `json:"-" bson:"-"`
+}
+
+func (m *FolderStat) Reset()         { *m = FolderStat{} }
+func (m *FolderStat) String() string { return proto.CompactTextString(m) }
+func (*FolderStat) ProtoMessage()    {}
+func (*FolderStat) Descriptor() ([]byte, []int) {
+	return fileDescriptor_0ab36b6dc6e1dcaa, []int{8}
+}
+
+func (m *FolderStat) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_FolderStat.Unmarshal(m, b)
+}
+func (m *FolderStat) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_FolderStat.Marshal(b, m, deterministic)
+}
+func (m *FolderStat) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_FolderStat.Merge(m, src)
+}
+func (m *FolderStat) XXX_Size() int {
+	return xxx_messageInfo_FolderStat.Size(m)
+}
+func (m *FolderStat) XXX_DiscardUnknown() {
+	xxx_messageInfo_FolderStat.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_FolderStat proto.InternalMessageInfo
+
+func (m *FolderStat) GetFolder() string {
+	if m != nil {
+		return m.Folder
+	}
+	return ""
+}
+
+func (m *FolderStat) GetTotal() uint32 {
+	if m != nil {
+		return m.Total
+	}
+	return 0
+}
+
+func (m *FolderStat) GetUnread() uint32 {
+	if m != nil {
+		return m.Unread
+	}
+	return 0
+}
+
 func init() {
 	proto.RegisterType((*MailBody)(nil), "common.MailBody")
 	proto.RegisterType((*MailHeader)(nil), "common.MailHeader")
@@ -453,36 +508,39 @@ func init() {
 	proto.RegisterType((*UserInfo)(nil), "common.UserInfo")
 	proto.RegisterType((*Frame)(nil), "common.Frame")
 	proto.RegisterType((*Folder)(nil), "common.Folder")
+	proto.RegisterType((*FolderStat)(nil), "common.FolderStat")
 }
 
 func init() { proto.RegisterFile("gostfix.proto", fileDescriptor_0ab36b6dc6e1dcaa) }
 
 var fileDescriptor_0ab36b6dc6e1dcaa = []byte{
-	// 403 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x74, 0x52, 0x4d, 0x8f, 0xd3, 0x30,
-	0x10, 0x55, 0x3e, 0x1a, 0xd2, 0x09, 0xa0, 0xee, 0x08, 0x21, 0x0b, 0x71, 0x88, 0x22, 0x0e, 0x15,
-	0x87, 0x0a, 0x0a, 0xa7, 0xbd, 0xc1, 0x61, 0x05, 0x07, 0x38, 0x58, 0x8b, 0xc4, 0x11, 0xc7, 0x71,
-	0xa8, 0x21, 0x89, 0xa3, 0xc4, 0x91, 0xb6, 0x37, 0x7e, 0x3a, 0xf2, 0xc4, 0x69, 0x4b, 0x25, 0x6e,
-	0xef, 0x79, 0xc6, 0xf3, 0xde, 0x3c, 0x1b, 0x9e, 0xfc, 0x34, 0xa3, 0xad, 0xf5, 0xc3, 0xae, 0x1f,
-	0x8c, 0x35, 0x98, 0x48, 0xd3, 0xb6, 0xa6, 0x2b, 0xfe, 0x04, 0x90, 0x7e, 0x11, 0xba, 0xf9, 0x68,
-	0xaa, 0x23, 0xbe, 0x84, 0x75, 0xdf, 0x08, 0xdd, 0xdd, 0xab, 0x07, 0xcb, 0x82, 0x3c, 0xd8, 0xae,
-	0xf9, 0xf9, 0x00, 0x5f, 0x40, 0x3a, 0x68, 0x79, 0xa0, 0x62, 0x48, 0xc5, 0x13, 0xc7, 0x5b, 0xc8,
-	0x84, 0xb5, 0x42, 0x1e, 0x5a, 0xd5, 0xd9, 0x91, 0x45, 0x79, 0xb4, 0xcd, 0xf6, 0x6c, 0x37, 0x8b,
-	0xec, 0x3e, 0x9c, 0x4a, 0x9f, 0x94, 0xa8, 0xd4, 0xc0, 0x2f, 0x9b, 0x9d, 0x05, 0x70, 0x16, 0xe6,
-	0x1a, 0x22, 0xc4, 0xf5, 0x60, 0x5a, 0xaf, 0x4f, 0x18, 0x9f, 0x42, 0x68, 0x8d, 0x17, 0x0d, 0xad,
-	0x71, 0x5c, 0x4a, 0x16, 0xcd, 0x5c, 0x4a, 0xdc, 0x40, 0x54, 0x4a, 0xc9, 0x62, 0x3a, 0x70, 0xd0,
-	0x4d, 0xa9, 0x84, 0x55, 0x6c, 0x95, 0x07, 0x5b, 0xe4, 0x84, 0x91, 0xc1, 0xa3, 0x71, 0x2a, 0x7f,
-	0x29, 0x69, 0x59, 0x42, 0x9d, 0x0b, 0x2d, 0xbe, 0x43, 0xec, 0x1c, 0xe0, 0x6b, 0x48, 0x0e, 0xe4,
-	0x82, 0xd4, 0xb3, 0x3d, 0x2e, 0x1b, 0x9c, 0xfd, 0x71, 0xdf, 0x81, 0xaf, 0x20, 0x2e, 0x4d, 0x75,
-	0x24, 0x57, 0xd9, 0x7e, 0x73, 0xd9, 0xe9, 0xc2, 0xe4, 0x54, 0x2d, 0x38, 0xc0, 0x79, 0x7b, 0x7c,
-	0x73, 0x35, 0xff, 0xff, 0x09, 0x2d, 0x2a, 0xf3, 0x1e, 0x82, 0x54, 0x1e, 0xd3, 0x1e, 0xa2, 0xf8,
-	0x01, 0x9b, 0xeb, 0x7e, 0x97, 0x88, 0xae, 0x7c, 0x66, 0xa1, 0xae, 0xdc, 0x63, 0xd5, 0xba, 0x51,
-	0x5f, 0x45, 0xab, 0x96, 0xc7, 0x5a, 0x38, 0xe6, 0x90, 0x49, 0xd3, 0x59, 0xd5, 0xd9, 0xfb, 0x63,
-	0xaf, 0x7c, 0x8c, 0x97, 0x47, 0xc5, 0x2d, 0xa4, 0xdf, 0x46, 0x35, 0x7c, 0xee, 0x6a, 0xe3, 0x1c,
-	0x4c, 0xa3, 0x77, 0xbc, 0xe6, 0x84, 0x69, 0xfa, 0xd4, 0x34, 0xff, 0x4c, 0xf7, 0xbc, 0x78, 0x0b,
-	0xab, 0xbb, 0xc1, 0xc9, 0x20, 0xc4, 0xe3, 0x6f, 0xdd, 0xd3, 0xc5, 0x1b, 0x4e, 0x18, 0x9f, 0xc1,
-	0xaa, 0xd1, 0xad, 0x9e, 0x3f, 0xd0, 0x0d, 0x9f, 0x49, 0xf1, 0x1e, 0x92, 0x3b, 0xd3, 0xf8, 0x75,
-	0x3b, 0x37, 0xd4, 0x8b, 0x39, 0x8c, 0xcf, 0x21, 0x91, 0xd3, 0x68, 0x4d, 0x4b, 0x97, 0x52, 0xee,
-	0x59, 0x99, 0xd0, 0x4f, 0x7e, 0xf7, 0x37, 0x00, 0x00, 0xff, 0xff, 0xe9, 0x88, 0x74, 0xc2, 0xda,
-	0x02, 0x00, 0x00,
+	// 437 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x74, 0x53, 0xc1, 0x8e, 0xd3, 0x40,
+	0x0c, 0x55, 0xdb, 0x34, 0xb4, 0x0e, 0x45, 0x5d, 0x0b, 0xa1, 0x08, 0x71, 0xa8, 0x22, 0x0e, 0x15,
+	0x87, 0x0a, 0x0a, 0xa7, 0xbd, 0xc1, 0x61, 0x05, 0x07, 0x38, 0x0c, 0x8b, 0xc4, 0x91, 0xe9, 0x64,
+	0x42, 0x07, 0x92, 0x4c, 0x94, 0x38, 0xd2, 0xf6, 0xc6, 0xa7, 0x23, 0x3b, 0x93, 0x6d, 0x59, 0x89,
+	0x9b, 0x9f, 0xfd, 0xec, 0xf7, 0xec, 0x49, 0x60, 0xf5, 0xd3, 0x77, 0x54, 0xb8, 0xbb, 0x5d, 0xd3,
+	0x7a, 0xf2, 0x18, 0x1b, 0x5f, 0x55, 0xbe, 0xce, 0xfe, 0x4c, 0x60, 0xf1, 0x59, 0xbb, 0xf2, 0x83,
+	0xcf, 0x4f, 0xf8, 0x02, 0x96, 0x4d, 0xa9, 0x5d, 0x7d, 0x6b, 0xef, 0x28, 0x9d, 0x6c, 0x26, 0xdb,
+	0xa5, 0x3a, 0x27, 0xf0, 0x39, 0x2c, 0x5a, 0x67, 0x8e, 0x52, 0x9c, 0x4a, 0xf1, 0x1e, 0xe3, 0x35,
+	0x24, 0x9a, 0x48, 0x9b, 0x63, 0x65, 0x6b, 0xea, 0xd2, 0xd9, 0x66, 0xb6, 0x4d, 0xf6, 0xe9, 0x6e,
+	0x10, 0xd9, 0xbd, 0xbf, 0x2f, 0x7d, 0xb4, 0x3a, 0xb7, 0xad, 0xba, 0x24, 0xb3, 0x05, 0x60, 0x0b,
+	0x43, 0x0d, 0x11, 0xa2, 0xa2, 0xf5, 0x55, 0xd0, 0x97, 0x18, 0x9f, 0xc0, 0x94, 0x7c, 0x10, 0x9d,
+	0x92, 0x67, 0x6c, 0x4c, 0x3a, 0x1b, 0xb0, 0x31, 0xb8, 0x86, 0xd9, 0xc1, 0x98, 0x34, 0x92, 0x04,
+	0x87, 0x3c, 0x25, 0xd7, 0x64, 0xd3, 0xf9, 0x66, 0xb2, 0x45, 0x25, 0x31, 0xa6, 0xf0, 0xa8, 0xeb,
+	0x0f, 0xbf, 0xac, 0xa1, 0x34, 0x16, 0xe6, 0x08, 0xb3, 0xef, 0x10, 0xb1, 0x03, 0x7c, 0x05, 0xf1,
+	0x51, 0x5c, 0x88, 0x7a, 0xb2, 0xc7, 0x71, 0x83, 0xb3, 0x3f, 0x15, 0x18, 0xf8, 0x12, 0xa2, 0x83,
+	0xcf, 0x4f, 0xe2, 0x2a, 0xd9, 0xaf, 0x2f, 0x99, 0x7c, 0x4c, 0x25, 0xd5, 0x4c, 0x01, 0x9c, 0xb7,
+	0xc7, 0xd7, 0x0f, 0xe6, 0xff, 0xff, 0x42, 0xa3, 0xca, 0xb0, 0x87, 0x16, 0x95, 0xc7, 0xb2, 0x87,
+	0xce, 0x7e, 0xc0, 0xfa, 0x21, 0x9f, 0x2f, 0xe2, 0xf2, 0x70, 0xb3, 0xa9, 0xcb, 0xf9, 0xb1, 0x0a,
+	0x57, 0xda, 0x2f, 0xba, 0xb2, 0xe3, 0x63, 0x8d, 0x18, 0x37, 0x90, 0x18, 0x5f, 0x93, 0xad, 0xe9,
+	0xf6, 0xd4, 0xd8, 0x70, 0xc6, 0xcb, 0x54, 0x76, 0x0d, 0x8b, 0x6f, 0x9d, 0x6d, 0x3f, 0xd5, 0x85,
+	0x67, 0x07, 0x7d, 0x17, 0x1c, 0x2f, 0x95, 0xc4, 0x32, 0xbd, 0x2f, 0xcb, 0x7f, 0xa6, 0x07, 0x9c,
+	0xbd, 0x81, 0xf9, 0x4d, 0xcb, 0x32, 0x08, 0x51, 0xf7, 0xdb, 0x35, 0xd2, 0x78, 0xa5, 0x24, 0xc6,
+	0xa7, 0x30, 0x2f, 0x5d, 0xe5, 0x86, 0x0f, 0xe8, 0x4a, 0x0d, 0x20, 0x7b, 0x07, 0xf1, 0x8d, 0x2f,
+	0xc3, 0xba, 0x35, 0x0f, 0x0d, 0x62, 0x1c, 0xe3, 0x33, 0x88, 0x4d, 0xdf, 0x91, 0xaf, 0xa4, 0x69,
+	0xa1, 0x02, 0xe2, 0xd3, 0x0e, 0x5d, 0x5f, 0x49, 0x13, 0xb3, 0x0a, 0x41, 0xa1, 0x37, 0x20, 0x56,
+	0x24, 0x4f, 0xba, 0x94, 0xe6, 0x95, 0x1a, 0x00, 0xb3, 0xfb, 0xba, 0xb5, 0x3a, 0x97, 0xed, 0x57,
+	0x2a, 0xa0, 0x43, 0x2c, 0x7f, 0xc7, 0xdb, 0xbf, 0x01, 0x00, 0x00, 0xff, 0xff, 0xef, 0x23, 0xbe,
+	0x5d, 0x2e, 0x03, 0x00, 0x00,
 }

+ 6 - 0
common/gostfix.proto

@@ -46,4 +46,10 @@ message Frame {
 message Folder {
 	string name = 1;
 	bool custom = 2;
+}
+
+message FolderStat {
+	string folder = 1;
+	uint32 total = 2;
+	uint32 unread = 3;
 }

+ 1 - 0
common/mailutils.go

@@ -34,6 +34,7 @@ func NewMail() *Mail {
 
 type MailMetadata struct {
 	Id     string `bson:"_id"`
+	Email  string
 	User   string
 	Mail   *Mail
 	Read   bool

+ 1 - 1
common/notifier.go

@@ -26,6 +26,6 @@
 package common
 
 type Notifier interface {
-	NotifyMaiboxUpdate(email string)
+	NotifyMaiboxUpdate(email string, stats []FolderStat)
 	NotifyNewMail(email string, m MailMetadata)
 }

+ 79 - 55
db/db.go

@@ -308,41 +308,12 @@ func (s *Storage) SaveMail(email, folder string, m *common.Mail, read bool) erro
 		Mail:   &mail,
 	})
 
-	return nil
-}
-
-func (s *Storage) MoveMail(user string, mailId string, folder string) error {
-	mailsCollection := s.db.Collection(qualifiedMailCollection(user))
-
-	oId, err := primitive.ObjectIDFromHex(mailId)
-	if err != nil {
-		return err
-	}
-
-	if folder == common.Trash {
-		_, err = mailsCollection.UpdateOne(context.Background(), bson.M{"_id": oId}, bson.M{"$set": bson.M{"trash": true}})
-	} else {
-		_, err = mailsCollection.UpdateOne(context.Background(), bson.M{"_id": oId}, bson.M{"$set": bson.M{"folder": folder, "trash": false}})
-	}
-	return err
-}
-
-func (s *Storage) RestoreMail(user string, mailId string) error {
-	mailsCollection := s.db.Collection(qualifiedMailCollection(user))
-
-	oId, err := primitive.ObjectIDFromHex(mailId)
-	if err != nil {
-		return err
-	}
-
-	//TODO: Legacy for old databases remove soon
-	metadata, err := s.GetMail(user, mailId)
-	if metadata.Folder == common.Trash {
-		_, err = mailsCollection.UpdateOne(context.Background(), bson.M{"_id": oId}, bson.M{"$set": bson.M{"folder": common.Inbox}})
+	stats, err := s.GetEmailStats(user.User, email, folder)
+	if err == nil {
+		s.notifyMailboxUpdate(email, []common.FolderStat{stats})
 	}
 
-	_, err = mailsCollection.UpdateOne(context.Background(), bson.M{"_id": oId}, bson.M{"$set": bson.M{"trash": false}})
-	return err
+	return nil
 }
 
 func (s *Storage) DeleteMail(user string, mailId string) error {
@@ -364,6 +335,12 @@ func (s *Storage) DeleteMail(user string, mailId string) error {
 	}
 
 	_, err = mailsCollection.DeleteOne(context.Background(), bson.M{"_id": oId})
+
+	stats, errTemp := s.GetEmailStats(user, result.Email, common.Trash)
+	if errTemp == nil {
+		s.notifyMailboxUpdate(result.Email, []common.FolderStat{stats})
+	}
+
 	return err
 }
 
@@ -428,12 +405,12 @@ func (s *Storage) GetUserInfo(user string) (*common.UserInfo, error) {
 	return result, err
 }
 
-func (s *Storage) GetEmailStats(user string, email string, folder string) (unread, total int, err error) {
+func (s *Storage) GetEmailStats(user string, email string, folder string) (stat common.FolderStat, err error) {
+	stat = common.FolderStat{
+		Folder: folder,
+	}
+
 	mailsCollection := s.db.Collection(qualifiedMailCollection(user))
-	result := &struct {
-		Total  int
-		Unread int
-	}{}
 
 	matchFilter := bson.M{"email": email}
 	if folder == common.Trash {
@@ -449,24 +426,22 @@ func (s *Storage) GetEmailStats(user string, email string, folder string) (unrea
 		}
 	}
 
-	unreadMatchFilter := matchFilter
-	unreadMatchFilter["read"] = false
-
-	cur, err := mailsCollection.Aggregate(context.Background(), bson.A{bson.M{"$match": unreadMatchFilter}, bson.M{"$count": "unread"}})
+	cur, err := mailsCollection.Aggregate(context.Background(), bson.A{bson.M{"$match": matchFilter}, bson.M{"$count": "total"}})
 	if err == nil && cur.Next(context.Background()) {
-		cur.Decode(result)
+		cur.Decode(&stat)
 	} else {
-		return 0, 0, err
+		return
 	}
 
-	cur, err = mailsCollection.Aggregate(context.Background(), bson.A{bson.M{"$match": matchFilter}, bson.M{"$count": "total"}})
+	matchFilter["read"] = false
+
+	cur, err = mailsCollection.Aggregate(context.Background(), bson.A{bson.M{"$match": matchFilter}, bson.M{"$count": "unread"}})
 	if err == nil && cur.Next(context.Background()) {
-		cur.Decode(result)
+		cur.Decode(&stat)
 	} else {
-		return 0, 0, err
+		return
 	}
-
-	return result.Unread, result.Total, err
+	return
 }
 
 func (s *Storage) GetMail(user string, id string) (metadata *common.MailMetadata, err error) {
@@ -496,6 +471,9 @@ func (s *Storage) SetRead(user string, id string, read bool) error {
 		return err
 	}
 	_, err = mailsCollection.UpdateOne(context.Background(), bson.M{"_id": oId}, bson.M{"$set": bson.M{"read": read}})
+
+	s.notifyMailboxUpdateForMail(user, id)
+
 	return err
 }
 
@@ -506,7 +484,23 @@ func (s *Storage) UpdateMail(user string, id string, mailMap interface{}) error
 	if err != nil {
 		return err
 	}
+
+	fromFolder := ""
+	metadata, err := s.GetMail(user, id)
+	if err == nil {
+		if metadata.Trash {
+			fromFolder = common.Trash
+		} else {
+			fromFolder = metadata.Folder
+		}
+	} else {
+		log.Printf("Unable to get mail info to update folder statistics %s", err)
+	}
+
 	_, err = mailsCollection.UpdateOne(context.Background(), bson.M{"_id": oId}, bson.M{"$set": mailMap})
+
+	s.notifyMailboxUpdateForMail(user, id, fromFolder)
+
 	return err
 }
 
@@ -546,10 +540,10 @@ func (s *Storage) CheckEmailExists(email string) bool {
 
 func (s *Storage) GetFolders(email string) (folders []*common.Folder) {
 	folders = []*common.Folder{
-		&common.Folder{Name: common.Inbox, Custom: false},
-		&common.Folder{Name: common.Sent, Custom: false},
-		&common.Folder{Name: common.Trash, Custom: false},
-		&common.Folder{Name: common.Spam, Custom: false},
+		{Name: common.Inbox, Custom: false},
+		{Name: common.Sent, Custom: false},
+		{Name: common.Trash, Custom: false},
+		{Name: common.Spam, Custom: false},
 	}
 	return
 }
@@ -669,10 +663,40 @@ func (s *Storage) notifyNewMail(email string, mail common.MailMetadata) {
 	}
 }
 
-func (s *Storage) notifyMailboxUpdate(email string) {
+func (s *Storage) notifyMailboxUpdate(email string, stats []common.FolderStat) {
 	notifiers.notifiersLock.Lock()
 	defer notifiers.notifiersLock.Unlock()
 	for _, notifier := range notifiers.notifiers {
-		notifier.NotifyMaiboxUpdate(email)
+		notifier.NotifyMaiboxUpdate(email, stats)
+	}
+}
+
+func (s *Storage) notifyMailboxUpdateForMail(user, id string, folders ...string) {
+	metadata, err := s.GetMail(user, id)
+
+	if err != nil {
+		log.Printf("Unable to get mail metadata to update mailbox stat %v\n", err)
+		return
+	}
+
+	if metadata.Trash {
+		folders = append(folders, common.Trash)
+	}
+
+	var stats []common.FolderStat
+	stat, err := s.GetEmailStats(user, metadata.Email, metadata.Folder)
+	stats = append(stats, stat)
+	for _, folder := range folders {
+		if folder == metadata.Folder {
+			continue
+		}
+
+		stat, err = s.GetEmailStats(user, metadata.Email, folder)
+		if err == nil {
+			stats = append(stats, stat)
+		} else {
+			log.Printf("Unable to update mailbox stat %v\n", err)
+		}
 	}
+	s.notifyMailboxUpdate(metadata.Email, stats)
 }

+ 33 - 28
web/js/index.js

@@ -205,7 +205,6 @@ function requestMail(mailId) {
                 $('#mail'+mailId).addClass('read');
                 $('#mailDetails').html(result);
                 setDetailsVisible(true);
-                folderStat(currentFolder);//TODO: receive statistic from websocket
                 checkMailUnread();
             },
             error: function(jqXHR, textStatus, errorThrown) {
@@ -217,6 +216,17 @@ function requestMail(mailId) {
     }
 }
 
+function updateFolderStat(stat) {
+    var folder = stat.folder
+    if (stat.unread > 0) {
+        $('#folderStats'+folder).text(stat.unread);
+        $('#folder'+folder).addClass('unread');
+    } else {
+        $('#folder'+folder).removeClass('unread');
+        $('#folderStats'+folder).text("");
+    }
+}
+
 function loadFolders() {
     if (mailbox === null) {
         return
@@ -225,12 +235,13 @@ function loadFolders() {
     $.ajax({
         url: '/m/' + mailbox + '/folders',
         success: function(result) {
-            folderList = jQuery.parseJSON(result);
+            var folderList = jQuery.parseJSON(result);
+            $('#folders').html(folderList.html);
             for(var i = 0; i < folderList.folders.length; i++) {
-                folders.push(folderList.folders[i].name);
-                folderStat(folderList.folders[i].name);
+                var folder = folderList.folders[i].name;
+                folders.push(folder);
+                updateFolderStat(folderList.stats[i])
             }
-            $('#folders').html(folderList.html);
         },
         error: function(jqXHR, textStatus, errorThrown) {
             showToast(Severity.Critical, 'Unable to update folder list: ' + errorThrown + ' ' + textStatus);
@@ -249,14 +260,8 @@ function folderStat(folder) {
             folder: folder
         },
         success: function(result) {
-            var stats = jQuery.parseJSON(result);
-            if (stats.unread > 0) {
-                $('#folderStats'+folder).text(stats.unread);
-                $('#folder'+folder).addClass('unread');
-            } else {
-                $('#folder'+folder).removeClass('unread');
-                $('#folderStats'+folder).text("");
-            }
+            var stat = jQuery.parseJSON(result);
+            updateFolderStat(stat)
         },
         error: function(jqXHR, textStatus, errorThrown) {
             showToast(Severity.Critical, 'Unable to update folder list: ' + errorThrown + ' ' + textStatus);
@@ -332,7 +337,6 @@ function setRead(mailId, read) {
                 $('#mail'+mailId).removeClass('read');
                 $('#mail'+mailId).addClass('unread');
             }
-            folderStat(currentFolder);//TODO: receive statistic from websocket
             checkMailUnread();
         },
         error: function(jqXHR, textStatus, errorThrown) {
@@ -362,8 +366,6 @@ function removeMail(mailId, callback) {
             if (callback) {
                 callback(mailId);
             }
-            folderStat(currentFolder);//TODO: receive statistic from websocket
-            folderStat('Trash');//TODO: receive statistic from websocket
         },
         error: function(jqXHR, textStatus, errorThrown) {
             showToast(Severity.Critical, 'Unable to remove mail: ' + errorThrown + ' ' + textStatus);
@@ -384,9 +386,6 @@ function restoreMail(mailId, callback) {
             if (callback) {
                 callback();
             }
-            for (var i = 0; i < folders.length; i++) {
-                folderStat(folders[i]);
-            }
         },
         error: function(jqXHR, textStatus, errorThrown) {
             showToast(Severity.Critical, 'Unable to restore mail: ' + errorThrown + ' ' + textStatus);
@@ -559,22 +558,28 @@ function connectNotifier() {
         return;
     }
 
-    var protocol = 'wss://';
-    if (window.location.protocol  !== 'https:') {
-        protocol = 'ws://';
-    }
+    var protocol = window.location.protocol  !== 'https:' ? 'ws://' : 'wss://';
     notifierSocket = new WebSocket(protocol + window.location.host + '/m/' + mailbox + '/notifierSubscribe');
     notifierSocket.onmessage = function (ev) {
         jsonData = JSON.parse(ev.data);
         switch (jsonData.type) {
         case 'mail':
-            $('#mailList').prepend(jsonData.data.html);
-            for (var i = 0; i < folders.length; i++) {
-                if (folders[i] == jsonData.data.folder) {
-                    folderStat(folders[i]);
-                }
+            if (currentFolder == jsonData.data.folder) {
+                $('#mailList').prepend(jsonData.data.html);
             }
             break;
+        case 'stats':
+            for (var i = 0; i < jsonData.data.length; i++) {
+                var folder = jsonData.data[i].folder
+                var unread = jsonData.data[i].unread
+                if (unread > 0) {
+                    $('#folderStats'+folder).text(unread);
+                    $('#folder'+folder).addClass('unread');
+                } else {
+                    $('#folder'+folder).removeClass('unread');
+                    $('#folderStats'+folder).text("");
+                }
+            }
         }
     }
 }

+ 13 - 11
web/mailbox.go

@@ -109,12 +109,20 @@ func (s *Server) handleMailboxRequest(w http.ResponseWriter, r *http.Request, us
 func (s *Server) handleFolders(w http.ResponseWriter, user, email string) {
 	folders := s.storage.GetFolders(email)
 
+	var stats []interface{}
+	for _, folder := range folders {
+		stat, _ := s.storage.GetEmailStats(user, email, folder.Name)
+		stats = append(stats, stat)
+	}
+
 	out, err := json.Marshal(&struct {
 		Folders []*common.Folder `json:"folders"`
 		Html    string           `json:"html"`
+		Stats   []interface{}    `json:"stats"`
 	}{
 		Folders: folders,
 		Html:    s.templater.ExecuteFolders(s.storage.GetFolders(email)),
+		Stats:   stats,
 	})
 
 	if err != nil {
@@ -125,19 +133,13 @@ func (s *Server) handleFolders(w http.ResponseWriter, user, email string) {
 }
 
 func (s *Server) handleFolderStat(w http.ResponseWriter, r *http.Request, user, email string) {
-	unread, total, err := s.storage.GetEmailStats(user, email, s.extractFolder(email, r))
+	stat, err := s.storage.GetEmailStats(user, email, s.extractFolder(email, r))
 	if err != nil {
 		s.error(http.StatusInternalServerError, "Couldn't read mailbox stat", w)
 		return
 	}
 
-	out, err := json.Marshal(&struct {
-		Total  int `json:"total"`
-		Unread int `json:"unread"`
-	}{
-		Total:  total,
-		Unread: unread,
-	})
+	out, err := json.Marshal(stat)
 
 	if err != nil {
 		s.error(http.StatusInternalServerError, "Couldn't parse mailbox stat", w)
@@ -155,7 +157,7 @@ func (s *Server) handleMailList(w http.ResponseWriter, r *http.Request, user, em
 		page = 0
 	}
 
-	_, total, err := s.storage.GetEmailStats(user, email, folder)
+	stat, err := s.storage.GetEmailStats(user, email, folder)
 	if err != nil {
 		s.error(http.StatusInternalServerError, "Couldn't read email database", w)
 		return
@@ -169,10 +171,10 @@ func (s *Server) handleMailList(w http.ResponseWriter, r *http.Request, user, em
 	}
 
 	out, err := json.Marshal(&struct {
-		Total int    `json:"total"`
+		Total uint32 `json:"total"`
 		Html  string `json:"html"`
 	}{
-		Total: total,
+		Total: stat.Total,
 		Html:  s.templater.ExecuteMailList(mailList),
 	})
 	if err != nil {

+ 38 - 22
web/webnotifier.go

@@ -36,9 +36,14 @@ import (
 	"github.com/gorilla/websocket"
 )
 
+type webNotification struct {
+	Type string      `json:"type"`
+	Data interface{} `json:"data"`
+}
+
 type websocketChannel struct {
 	connection *websocket.Conn
-	channel    chan *common.MailMetadata
+	channel    chan interface{}
 }
 
 type webNotifier struct {
@@ -53,9 +58,9 @@ func NewWebNotifier() *webNotifier {
 	}
 }
 
-func (wn *webNotifier) NotifyMaiboxUpdate(email string) {
+func (wn *webNotifier) NotifyMaiboxUpdate(email string, stats []common.FolderStat) {
 	if channel, ok := wn.getNotifier(email); ok {
-		channel.channel <- &common.MailMetadata{} //TODO: Dummy notificator for now, later need to make separate interface to handle this
+		channel.channel <- stats
 	}
 }
 
@@ -82,7 +87,7 @@ func (wn *webNotifier) handleNotifierRequest(w http.ResponseWriter, r *http.Requ
 
 	c := &websocketChannel{
 		connection: conn,
-		channel:    make(chan *common.MailMetadata, 10),
+		channel:    make(chan interface{}, 10),
 	}
 	wn.addNotifier(email, c)
 
@@ -99,25 +104,36 @@ func (wn *webNotifier) handleNotifierRequest(w http.ResponseWriter, r *http.Requ
 func (wn *webNotifier) handleNotifications(c *websocketChannel) {
 	for {
 		select {
-		case newMail := <-c.channel:
-			out, err := json.Marshal(&struct {
-				Type string      `json:"type"`
-				Data interface{} `json:"data"`
-			}{
-				Type: "mail",
-				Data: &struct {
-					Folder string `json:"folder"`
-					HTML   string `json:"html"`
-				}{
-					Folder: newMail.Folder,
-					HTML:   wn.server.templater.ExecuteMailList([]*common.MailMetadata{newMail}),
-				},
-			})
-
-			err = c.connection.WriteMessage(websocket.TextMessage, out)
+		case data := <-c.channel:
+			var err error = nil
+			var out []byte
+			if newMail, ok := data.(*common.MailMetadata); ok {
+				out, err = json.Marshal(&webNotification{
+					Type: "mail",
+					Data: &struct {
+						Folder string `json:"folder"`
+						HTML   string `json:"html"`
+					}{
+						Folder: newMail.Folder,
+						HTML:   wn.server.templater.ExecuteMailList([]*common.MailMetadata{newMail}),
+					},
+				})
+
+			} else if stats, ok := data.([]common.FolderStat); ok {
+				out, err = json.Marshal(&webNotification{
+					Type: "stats",
+					Data: stats,
+				})
+			}
+
 			if err != nil {
-				log.Println(err.Error())
-				return
+				log.Printf("Unable to marshal notification data %v\n", err)
+			} else {
+				err = c.connection.WriteMessage(websocket.TextMessage, out)
+				if err != nil {
+					log.Println(err.Error())
+					return
+				}
 			}
 		}
 	}