Browse Source

Implement basic mail read workflow

- Add complete message pasing and attachment store
- Add delete functionality
- Add read/unread functionality
- Add autoupdate
- Improve look and feel
TODO: Dates are not working
Alexey Edelev 5 years ago
parent
commit
b76ab7b146

+ 35 - 27
common/gostfix.pb.go

@@ -249,8 +249,9 @@ func (m *Attachment) GetData() []byte {
 }
 
 type AttachmentHeader struct {
-	FileName             string   `protobuf:"bytes,1,opt,name=fileName,proto3" json:"fileName,omitempty"`
-	ContentType          string   `protobuf:"bytes,2,opt,name=contentType,proto3" json:"contentType,omitempty"`
+	Id                   string   `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
+	FileName             string   `protobuf:"bytes,2,opt,name=fileName,proto3" json:"fileName,omitempty"`
+	ContentType          string   `protobuf:"bytes,3,opt,name=contentType,proto3" json:"contentType,omitempty"`
 	XXX_NoUnkeyedLiteral struct{} `json:"-" bson:"-"`
 	XXX_unrecognized     []byte   `json:"-" bson:"-"`
 	XXX_sizecache        int32    `json:"-" bson:"-"`
@@ -281,6 +282,13 @@ func (m *AttachmentHeader) XXX_DiscardUnknown() {
 
 var xxx_messageInfo_AttachmentHeader proto.InternalMessageInfo
 
+func (m *AttachmentHeader) GetId() string {
+	if m != nil {
+		return m.Id
+	}
+	return ""
+}
+
 func (m *AttachmentHeader) GetFileName() string {
 	if m != nil {
 		return m.FileName
@@ -404,29 +412,29 @@ func init() {
 }
 
 var fileDescriptor_0ab36b6dc6e1dcaa = []byte{
-	// 373 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x74, 0x52, 0xc1, 0x6a, 0xdb, 0x40,
-	0x10, 0x45, 0x96, 0xac, 0xda, 0xa3, 0xb6, 0xd8, 0x43, 0x0f, 0x4b, 0xe9, 0x41, 0x88, 0x1e, 0x4c,
-	0x0f, 0xa6, 0x75, 0x6f, 0xbe, 0x25, 0x87, 0x90, 0x1c, 0x12, 0xc2, 0xe2, 0x40, 0xae, 0xab, 0xd5,
-	0x2a, 0xde, 0x44, 0xd2, 0x0a, 0x69, 0x0d, 0xf6, 0x2d, 0x9f, 0x1e, 0x76, 0x24, 0xd9, 0xc6, 0x90,
-	0xdb, 0xbc, 0x79, 0x4f, 0xf3, 0xde, 0x8c, 0x16, 0xbe, 0xbd, 0x98, 0xd6, 0xe6, 0x7a, 0xbf, 0xac,
-	0x1b, 0x63, 0x0d, 0x86, 0xd2, 0x94, 0xa5, 0xa9, 0x92, 0x77, 0x0f, 0x26, 0xf7, 0x42, 0x17, 0xd7,
-	0x26, 0x3b, 0xe0, 0x2f, 0x98, 0xd6, 0x85, 0xd0, 0xd5, 0x46, 0xed, 0x2d, 0xf3, 0x62, 0x6f, 0x31,
-	0xe5, 0xa7, 0x06, 0xfe, 0x84, 0x49, 0xa3, 0xe5, 0x96, 0xc8, 0x11, 0x91, 0x47, 0x8c, 0x6b, 0x88,
-	0x84, 0xb5, 0x42, 0x6e, 0x4b, 0x55, 0xd9, 0x96, 0xf9, 0xb1, 0xbf, 0x88, 0x56, 0x6c, 0xd9, 0x99,
-	0x2c, 0xaf, 0x8e, 0xd4, 0xad, 0x12, 0x99, 0x6a, 0xf8, 0xb9, 0xd8, 0x45, 0x00, 0x17, 0xa1, 0xe3,
-	0x10, 0x21, 0xc8, 0x1b, 0x53, 0xf6, 0xfe, 0x54, 0xe3, 0x77, 0x18, 0x59, 0xd3, 0x9b, 0x8e, 0xac,
-	0x71, 0x58, 0x4a, 0xe6, 0x77, 0x58, 0x4a, 0x9c, 0x81, 0x9f, 0x4a, 0xc9, 0x02, 0x6a, 0xb8, 0xd2,
-	0x4d, 0xc9, 0x84, 0x55, 0x6c, 0x1c, 0x7b, 0x0b, 0xe4, 0x54, 0x23, 0x83, 0x2f, 0xed, 0x2e, 0x7d,
-	0x55, 0xd2, 0xb2, 0x90, 0x94, 0x03, 0x4c, 0x9e, 0x21, 0x70, 0x09, 0xf0, 0x0f, 0x84, 0x5b, 0x4a,
-	0x41, 0xee, 0xd1, 0x0a, 0x87, 0x0d, 0x4e, 0xf9, 0x78, 0xaf, 0xc0, 0xdf, 0x10, 0xa4, 0x26, 0x3b,
-	0x50, 0xaa, 0x68, 0x35, 0x3b, 0x57, 0xba, 0x63, 0x72, 0x62, 0x13, 0x0e, 0x70, 0xda, 0x1e, 0xff,
-	0x5e, 0xcc, 0xff, 0xfc, 0x42, 0x83, 0x4b, 0xb7, 0x87, 0x20, 0x97, 0xaf, 0xb4, 0x87, 0x48, 0x1e,
-	0x61, 0x76, 0xa9, 0x77, 0x3f, 0x27, 0xd7, 0x85, 0x7a, 0x10, 0xa5, 0xea, 0x2f, 0x77, 0xc4, 0x18,
-	0x43, 0x24, 0x4d, 0x65, 0x55, 0x65, 0x37, 0x87, 0x5a, 0xf5, 0x67, 0x3c, 0x6f, 0x25, 0x6b, 0x98,
-	0x3c, 0xb5, 0xaa, 0xb9, 0xab, 0x72, 0xe3, 0x1c, 0x77, 0x6d, 0x9f, 0x70, 0xca, 0xa9, 0xa6, 0xe9,
-	0xbb, 0xa2, 0xa0, 0xe9, 0xfd, 0xaf, 0x1f, 0x70, 0xf2, 0x0f, 0xc6, 0x37, 0x8d, 0xb3, 0x41, 0x08,
-	0xda, 0x37, 0x5d, 0xd3, 0x87, 0x73, 0x4e, 0x35, 0xfe, 0x80, 0x71, 0xa1, 0x4b, 0xdd, 0x3d, 0x98,
-	0x39, 0xef, 0x40, 0x1a, 0xd2, 0x1b, 0xfc, 0xff, 0x11, 0x00, 0x00, 0xff, 0xff, 0x58, 0xe1, 0xb1,
-	0x5c, 0x94, 0x02, 0x00, 0x00,
+	// 380 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x74, 0x52, 0x3d, 0x6f, 0xdb, 0x30,
+	0x10, 0x85, 0x3e, 0xac, 0xda, 0xa7, 0xb6, 0xb0, 0x0f, 0x1d, 0x88, 0xa2, 0x83, 0x20, 0x74, 0x30,
+	0x3a, 0x18, 0xad, 0xbb, 0x79, 0x6b, 0x87, 0xa2, 0x1d, 0x9a, 0x81, 0x70, 0x80, 0x8c, 0xa1, 0x28,
+	0x2a, 0x66, 0x22, 0x89, 0x82, 0x44, 0x03, 0xf6, 0x96, 0x9f, 0x1e, 0xf0, 0x24, 0xd9, 0x8e, 0x81,
+	0x6c, 0xf7, 0xee, 0x9e, 0xee, 0xbd, 0x7b, 0x22, 0x7c, 0x78, 0x30, 0x9d, 0x2d, 0xf4, 0x61, 0xd5,
+	0xb4, 0xc6, 0x1a, 0x8c, 0xa4, 0xa9, 0x2a, 0x53, 0xa7, 0xcf, 0x1e, 0x4c, 0xff, 0x0b, 0x5d, 0xfe,
+	0x36, 0xf9, 0x11, 0xbf, 0xc0, 0xac, 0x29, 0x85, 0xae, 0xb7, 0xea, 0x60, 0x99, 0x97, 0x78, 0xcb,
+	0x19, 0x3f, 0x37, 0xf0, 0x33, 0x4c, 0x5b, 0x2d, 0x77, 0x34, 0xf4, 0x69, 0x78, 0xc2, 0xb8, 0x81,
+	0x58, 0x58, 0x2b, 0xe4, 0xae, 0x52, 0xb5, 0xed, 0x58, 0x90, 0x04, 0xcb, 0x78, 0xcd, 0x56, 0xbd,
+	0xc8, 0xea, 0xd7, 0x69, 0xf4, 0x57, 0x89, 0x5c, 0xb5, 0xfc, 0x92, 0xec, 0x2c, 0x80, 0xb3, 0xd0,
+	0xcf, 0x10, 0x21, 0x2c, 0x5a, 0x53, 0x0d, 0xfa, 0x54, 0xe3, 0x47, 0xf0, 0xad, 0x19, 0x44, 0x7d,
+	0x6b, 0x1c, 0x96, 0x92, 0x05, 0x3d, 0x96, 0x12, 0xe7, 0x10, 0x64, 0x52, 0xb2, 0x90, 0x1a, 0xae,
+	0x74, 0x5b, 0x72, 0x61, 0x15, 0x9b, 0x24, 0xde, 0x12, 0x39, 0xd5, 0xc8, 0xe0, 0x5d, 0xb7, 0xcf,
+	0x1e, 0x95, 0xb4, 0x2c, 0x22, 0xe6, 0x08, 0xd3, 0x3b, 0x08, 0x9d, 0x03, 0xfc, 0x06, 0xd1, 0x8e,
+	0x5c, 0x90, 0x7a, 0xbc, 0xc6, 0xf1, 0x82, 0xb3, 0x3f, 0x3e, 0x30, 0xf0, 0x2b, 0x84, 0x99, 0xc9,
+	0x8f, 0xe4, 0x2a, 0x5e, 0xcf, 0x2f, 0x99, 0x2e, 0x4c, 0x4e, 0xd3, 0x94, 0x03, 0x9c, 0xaf, 0xc7,
+	0xef, 0x57, 0xfb, 0xdf, 0x4e, 0x68, 0x54, 0xe9, 0xef, 0x10, 0xa4, 0xf2, 0x9e, 0xee, 0x10, 0xe9,
+	0x3d, 0xcc, 0xaf, 0xf9, 0x2e, 0x11, 0x9d, 0x0f, 0x99, 0xf9, 0x3a, 0x77, 0x3f, 0xab, 0xd0, 0xa5,
+	0xba, 0x11, 0x95, 0x1a, 0x7f, 0xd6, 0x88, 0x31, 0x81, 0x58, 0x9a, 0xda, 0xaa, 0xda, 0x6e, 0x8f,
+	0x8d, 0x1a, 0x62, 0xbc, 0x6c, 0xa5, 0x1b, 0x98, 0xde, 0x76, 0xaa, 0xfd, 0x57, 0x17, 0xc6, 0x39,
+	0xd8, 0x77, 0x83, 0xe3, 0x19, 0xa7, 0x9a, 0xb6, 0xef, 0xcb, 0xf2, 0xd5, 0xf6, 0x01, 0xa7, 0x3f,
+	0x60, 0xf2, 0xa7, 0x75, 0x32, 0x08, 0x61, 0xf7, 0xa4, 0x1b, 0xfa, 0x70, 0xc1, 0xa9, 0xc6, 0x4f,
+	0x30, 0x29, 0x75, 0xa5, 0xfb, 0x07, 0xb4, 0xe0, 0x3d, 0xc8, 0x22, 0x7a, 0x93, 0x3f, 0x5f, 0x02,
+	0x00, 0x00, 0xff, 0xff, 0x17, 0x2e, 0x20, 0xce, 0xa4, 0x02, 0x00, 0x00,
 }

+ 3 - 2
common/gostfix.proto

@@ -28,8 +28,9 @@ message Attachment {
 }
 
 message AttachmentHeader {
-	string fileName = 1;
-	string contentType = 2;
+	string id = 1;
+	string fileName = 2;
+	string contentType = 3;
 }
 
 message UserInfo {

+ 15 - 4
config/config.go

@@ -37,10 +37,13 @@ import (
 const configPath = "data/main.ini"
 
 const (
-	KeyPostfixConfig = "postfix_config"
-	KeyMongoAddress  = "mongo_address"
-	KeyMongoUser     = "mongo_user"
-	KeyMongoPassword = "mongo_password"
+	KeyPostfixConfig       = "postfix_config"
+	KeyMongoAddress        = "mongo_address"
+	KeyMongoUser           = "mongo_user"
+	KeyMongoPassword       = "mongo_password"
+	KeyAttachmentsPath     = "attachments_path"
+	KeyAttachmentsUser     = "attachments_user"
+	KeyAttachmentsPassword = "attachments_password"
 )
 
 const (
@@ -72,6 +75,7 @@ type gostfixConfig struct {
 	MongoUser       string
 	MongoPassword   string
 	MongoAddress    string
+	AttachmentsPath string
 }
 
 func newConfig() (config *gostfixConfig, err error) {
@@ -139,6 +143,12 @@ func newConfig() (config *gostfixConfig, err error) {
 		mongoAddress = "localhost:27017"
 	}
 
+	attachmentsPath := cfg.Section("").Key(KeyAttachmentsPath).String()
+
+	if attachmentsPath == "" {
+		attachmentsPath = "attachments"
+	}
+
 	config = &gostfixConfig{
 		VMailboxBase:    baseDir,
 		VMailboxMaps:    mapsList[1],
@@ -146,6 +156,7 @@ func newConfig() (config *gostfixConfig, err error) {
 		MongoUser:       mongoUser,
 		MongoPassword:   mongoPassword,
 		MongoAddress:    mongoAddress,
+		AttachmentsPath: attachmentsPath,
 	}
 	return
 }

+ 35 - 5
db/db.go

@@ -50,7 +50,6 @@ type Storage struct {
 	tokensCollection    *mongo.Collection
 	emailsCollection    *mongo.Collection
 	allEmailsCollection *mongo.Collection
-	mailsCollection     *mongo.Collection
 }
 
 func qualifiedMailCollection(user string) string {
@@ -98,7 +97,6 @@ func NewStorage() (s *Storage, err error) {
 		tokensCollection:    db.Collection("tokens"),
 		emailsCollection:    db.Collection("emails"),
 		allEmailsCollection: db.Collection("allEmails"),
-		mailsCollection:     db.Collection("mails"),
 	}
 
 	//Initial database setup
@@ -274,8 +272,16 @@ func (s *Storage) SaveMail(email, folder string, m *common.Mail) error {
 	return nil
 }
 
-func (s *Storage) RemoveMail(user string, m *common.Mail) error {
-	return nil
+func (s *Storage) RemoveMail(user string, messageId string) error {
+	mailsCollection := s.db.Collection(qualifiedMailCollection(user))
+
+	oId, err := primitive.ObjectIDFromHex(messageId)
+	if err != nil {
+		return err
+	}
+
+	_, err = mailsCollection.DeleteOne(context.Background(), bson.M{"_id": oId})
+	return err
 }
 
 func (s *Storage) MailList(user, email, folder string, frame common.Frame) ([]*common.MailMetadata, error) {
@@ -322,6 +328,30 @@ func (s *Storage) GetUserInfo(user string) (*common.UserInfo, error) {
 	return result, err
 }
 
+func (s *Storage) GetEmailStats(user string, email string) (unread, total int, err error) {
+	mailsCollection := s.db.Collection(qualifiedMailCollection(user))
+	result := &struct {
+		Total  int
+		Unread int
+	}{}
+
+	cur, err := mailsCollection.Aggregate(context.Background(), bson.A{bson.M{"$match": bson.M{"email": email, "read": false}}, bson.M{"$count": "unread"}})
+	if err == nil && cur.Next(context.Background()) {
+		cur.Decode(result)
+	} else {
+		return 0, 0, err
+	}
+
+	cur, err = mailsCollection.Aggregate(context.Background(), bson.A{bson.M{"$match": bson.M{"email": email}}, bson.M{"$count": "total"}})
+	if err == nil && cur.Next(context.Background()) {
+		cur.Decode(result)
+	} else {
+		return 0, 0, err
+	}
+
+	return result.Unread, result.Total, err
+}
+
 func (s *Storage) GetMail(user string, id string) (m *common.Mail, err error) {
 	mailsCollection := s.db.Collection(qualifiedMailCollection(user))
 
@@ -350,7 +380,7 @@ func (s *Storage) SetRead(user string, id string, read bool) error {
 	if err != nil {
 		return err
 	}
-	_, err = mailsCollection.UpdateOne(context.Background(), bson.M{"_id": oId}, bson.M{"$set": bson.M{"read": true}})
+	_, err = mailsCollection.UpdateOne(context.Background(), bson.M{"_id": oId}, bson.M{"$set": bson.M{"read": read}})
 	return err
 }
 

+ 1 - 0
go.mod

@@ -9,6 +9,7 @@ require (
 	github.com/golang/protobuf v1.3.4
 	github.com/google/uuid v1.1.1
 	github.com/gorilla/sessions v1.2.0
+	github.com/jhillyerd/enmime v0.8.0
 	github.com/lyft/protoc-gen-star v0.4.14 // indirect
 	github.com/spf13/afero v1.2.2 // indirect
 	go.mongodb.org/mongo-driver v1.3.0

+ 19 - 0
go.sum

@@ -1,6 +1,8 @@
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/amsokol/protoc-gen-gotag v0.2.1 h1:N3ovy0PyxxUBXz+jkuJJUA1iNlQ+7a/QVSW361XF0kc=
 github.com/amsokol/protoc-gen-gotag v0.2.1/go.mod h1:VGStdb9DuXf/2T+bgrIqBouu8Hl81egNzVxD/DyEPAg=
+github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI=
+github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -10,6 +12,8 @@ github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw=
+github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
 github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0=
 github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY=
 github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg=
@@ -34,6 +38,8 @@ github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWe
 github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ=
 github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0=
 github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw=
+github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561 h1:aBzukfDxQlCTVS0NBUjI5YA3iVeaZ9Tb5PxNrrIP1xs=
+github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
 github.com/golang/protobuf v1.3.4 h1:87PNWwrRvUSnqS4dlcBU/ftvOIBep4sYuBLlh6rX2wk=
 github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
 github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
@@ -47,6 +53,10 @@ github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+
 github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ=
 github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/jaytaylor/html2text v0.0.0-20190408195923-01ec452cbe43 h1:jTkyeF7NZ5oIr0ESmcrpiDgAfoidCBF4F5kJhjtaRwE=
+github.com/jaytaylor/html2text v0.0.0-20190408195923-01ec452cbe43/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
+github.com/jhillyerd/enmime v0.8.0 h1:PHc/2LXtnDmCDm0V4+5NlBx+MoubmufhuNXwpKSV2o8=
+github.com/jhillyerd/enmime v0.8.0/go.mod h1:MBHs3ugk03NGjMM6PuRynlKf+HA5eSillZ+TRCm73AE=
 github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
 github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4=
 github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
@@ -62,7 +72,11 @@ github.com/lyft/protoc-gen-star v0.4.14 h1:HUkD4H4dYFIgu3Bns/3N6J5GmKHCEGnhYBwNu
 github.com/lyft/protoc-gen-star v0.4.14/go.mod h1:mE8fbna26u7aEA2QCVvvfBU/ZrPgocG1206xAFPcs94=
 github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
 github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
+github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
+github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
 github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
+github.com/olekukonko/tablewriter v0.0.1 h1:b3iUnf1v+ppJiOfNX4yxxqfWKMQPZR5yoh8urCTFX88=
+github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
 github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
@@ -72,6 +86,7 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
 github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
 github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
 github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
@@ -79,6 +94,8 @@ github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=
 github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
 github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
 github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
+github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
@@ -100,6 +117,8 @@ golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d h1:1ZiEyfaQIg3Qh0EoqpwAak
 golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk=
+golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=

+ 40 - 29
scanner/mailscanner.go

@@ -39,6 +39,10 @@ import (
 	fsnotify "github.com/fsnotify/fsnotify"
 )
 
+const (
+	SignalReconfigure = iota
+)
+
 func NewEmail() *common.Mail {
 	return &common.Mail{
 		Header: &common.MailHeader{},
@@ -47,9 +51,10 @@ func NewEmail() *common.Mail {
 }
 
 type MailScanner struct {
-	watcher   *fsnotify.Watcher
-	emailMaps map[string]string
-	storage   *db.Storage
+	watcher       *fsnotify.Watcher
+	emailMaps     map[string]string
+	storage       *db.Storage
+	signalChannel chan int
 }
 
 func NewMailScanner() (ms *MailScanner) {
@@ -67,8 +72,9 @@ func NewMailScanner() (ms *MailScanner) {
 	}
 
 	ms = &MailScanner{
-		watcher: watcher,
-		storage: storage,
+		watcher:       watcher,
+		storage:       storage,
+		signalChannel: make(chan int),
 	}
 
 	return
@@ -140,38 +146,43 @@ func (ms *MailScanner) readEmailMaps() {
 		}
 	}
 	ms.emailMaps = emailMaps
-}
 
-func (ms *MailScanner) Run() {
-	go func() {
-		ms.readEmailMaps()
-
-		for mailbox, mailPath := range ms.emailMaps {
-			if !utils.FileExists(mailPath) {
-				file, err := os.Create(mailPath)
-				if err != nil {
-					fmt.Printf("Unable to create mailbox for watching %s\n", err)
-					continue
-				}
-				file.Close()
+	for mailbox, mailPath := range ms.emailMaps {
+		if !utils.FileExists(mailPath) {
+			file, err := os.Create(mailPath)
+			if err != nil {
+				fmt.Printf("Unable to create mailbox for watching %s\n", err)
+				continue
 			}
+			file.Close()
+		}
 
-			mails := ms.readMailFile(mailPath)
-			for _, mail := range mails {
-				ms.storage.SaveMail(mailbox, "Inbox", mail)
-			}
-			log.Printf("New email for %s, emails read %d", mailPath, len(mails))
+		mails := ms.readMailFile(mailPath)
+		for _, mail := range mails {
+			ms.storage.SaveMail(mailbox, "Inbox", mail)
+		}
+		log.Printf("New email for %s, emails read %d", mailPath, len(mails))
 
-			err := ms.watcher.Add(mailPath)
-			if err != nil {
-				fmt.Printf("Unable to add mailbox for watching\n")
-			} else {
-				fmt.Printf("Add mail file %s for watching\n", mailPath)
-			}
+		err := ms.watcher.Add(mailPath)
+		if err != nil {
+			fmt.Printf("Unable to add mailbox for watching\n")
+		} else {
+			fmt.Printf("Add mail file %s for watching\n", mailPath)
 		}
+	}
+}
+
+func (ms *MailScanner) Run() {
+	go func() {
+		ms.readEmailMaps()
 
 		for {
 			select {
+			case signal := <-ms.signalChannel:
+				switch signal {
+				case SignalReconfigure:
+					ms.readEmailMaps()
+				}
 			case event, ok := <-ms.watcher.Events:
 				if !ok {
 					return

+ 38 - 1
scanner/parser.go

@@ -27,12 +27,17 @@ package scanner
 
 import (
 	"bufio"
+	"bytes"
+	"encoding/hex"
 	"log"
+	"os"
 	"strings"
 	"time"
 
 	"git.semlanik.org/semlanik/gostfix/common"
 	utils "git.semlanik.org/semlanik/gostfix/utils"
+	"github.com/google/uuid"
+	enmime "github.com/jhillyerd/enmime"
 )
 
 const (
@@ -98,6 +103,7 @@ func parseFile(file *utils.LockedFile) []*common.Mail {
 			if scanner.Text() == "" {
 				if pd.state == StateBodyScan && pd.activeBoundary == "" {
 					if pd.mandatoryHeaders == AllHeaderMask {
+						pd.parseBody()
 						emails = append(emails, pd.email)
 					}
 					pd.reset()
@@ -118,6 +124,7 @@ func parseFile(file *utils.LockedFile) []*common.Mail {
 
 	if pd.state == StateBodyScan {
 		if pd.mandatoryHeaders == AllHeaderMask {
+			pd.parseBody()
 			emails = append(emails, pd.email)
 		}
 		pd.reset()
@@ -161,7 +168,7 @@ func (pd *parseData) parseHeader(headerRaw string) {
 		}
 
 		if pd.previousHeader != nil {
-			*pd.previousHeader = capture[2]
+			*pd.previousHeader = strings.Trim(capture[2], " \t")
 		}
 		return
 	}
@@ -172,3 +179,33 @@ func (pd *parseData) parseHeader(headerRaw string) {
 		*pd.previousHeader += capture[1]
 	}
 }
+
+func (pd *parseData) parseBody() {
+	buffer := bytes.NewBufferString("content-type:" + pd.bodyContentType + "\n\n" + pd.bodyData)
+	en, err := enmime.ReadEnvelope(buffer)
+	if err != nil {
+		log.Printf("Unable to read mail body %s\n\nBody content: %s\n\n", err, pd.bodyData)
+		return
+	}
+
+	pd.email.Body = &common.MailBody{}
+
+	pd.email.Body.PlainText = en.Text
+	pd.email.Body.RichText = en.HTML
+
+	for _, attachment := range en.Attachments {
+		uuid := uuid.New()
+		fileName := hex.EncodeToString(uuid[:])
+		attachmentFile, err := os.Create(fileName)
+		log.Printf("Attachment found %s\n", fileName)
+		if err != nil {
+			continue
+		}
+		pd.email.Body.Attachments = append(pd.email.Body.Attachments, &common.AttachmentHeader{
+			Id:          fileName,
+			FileName:    attachment.FileName,
+			ContentType: attachment.ContentType,
+		})
+		attachmentFile.Write(attachment.Content)
+	}
+}

File diff suppressed because it is too large
+ 0 - 0
web/assets/attachments.svg


File diff suppressed because it is too large
+ 0 - 0
web/assets/back.svg


File diff suppressed because it is too large
+ 0 - 0
web/assets/read.svg


File diff suppressed because it is too large
+ 0 - 0
web/assets/remove.svg


File diff suppressed because it is too large
+ 0 - 0
web/assets/settings.svg


File diff suppressed because it is too large
+ 0 - 0
web/assets/unread.svg


+ 4 - 0
web/css/controls.css

@@ -71,6 +71,7 @@
 .scrollable {
     width: 100%;
     height: 100%;
+    overflow-x: hidden;
     overflow-y: auto;
 }
 
@@ -208,3 +209,6 @@
 
 }
 
+.iconBtn:hover, .iconBtn:focus {
+    cursor: pointer;
+}

+ 2 - 1
web/css/index.css

@@ -37,6 +37,7 @@ table {
     margin-top: 13pt!important;
     margin-bottom: -8pt!important;
     margin-right: 8pt!important;
+    padding: 10pt;
 }
 
 .foldersBox {
@@ -74,4 +75,4 @@ table {
     text-overflow: ellipsis;
     white-space: nowrap;
     max-width: 80%;
-}
+}

+ 19 - 0
web/css/styles.css

@@ -76,4 +76,23 @@ tr.unread {
 
 .copyrights {
     color: #b8d4bc
+}
+
+.primaryText {
+    color: black;
+    font-weight: bold;
+    font-size: 12pt;
+}
+
+.secondaryText {
+    color: #777777;
+    font-weight: normal;
+    font-size: 10pt;
+}
+
+.messageBody {
+    border-radius: 3pt;
+    border-width: 1pt;
+    border-style: solid;
+    border-color: #41cd52;
 }

+ 88 - 9
web/js/index.js

@@ -24,7 +24,8 @@
  */
 
 var detailsUrl = "details/"
-
+var updateTimerId = null
+var updateInterval = 5000
 $(document).ready(function(){
     $.ajaxSetup({
         global: false,
@@ -33,6 +34,9 @@ $(document).ready(function(){
     $(window).bind('hashchange', requestDetails)
     requestDetails()
     loadStatusLine()
+    clearInterval(updateTimerId)
+    // updateMessageList()
+    updateTimerId = setInterval(updateMessageList, updateInterval)
 })
 
 function openEmail(id) {
@@ -48,20 +52,19 @@ function requestDetails() {
                 url: "/messageDetails",
                 data: {messageId: messageId},
                 success: function(result) {
+                    $("#mail"+messageId).removeClass("unread")
+                    $("#mail"+messageId).addClass("read")
                     $("#details").html(result);
-                    $("#maillist").css({pointerEvents: "none"})
-                    $("#details").show()
+                    setDetailsVisible(true);
                 },
                 error: function(jqXHR, textStatus, errorThrown) {
-                    $("#details").html(result);
-                    $("#maillist").css({pointerEvents: "none"})
-                    $("#details").show()
+                    $("#details").html(textStatus)
+                    setDetailsVisible(true)
                 }
             })
         }
     } else {
-        $("#details").hide()
-        $("#maillist").css({pointerEvents: "auto"})
+        setDetailsVisible(false)
     }
 }
 
@@ -96,5 +99,81 @@ function localDate(timestamp) {
     } else {
         dateString = date.toLocaleDateString("en-US")
     }
-    document.write(dateString)
+
+    return dateString
+}
+
+function setRead(messageId, read) {
+    $.ajax({
+        url: "/setRead",
+        data: {messageId: messageId,
+               read: read},
+        success: function(result) {
+            if (read) {
+                if ($("#readIcon"+messageId)) {
+                    $("#readIcon"+messageId).attr("src", "/assets/read.svg")
+                }
+                $("#mail"+messageId).removeClass("unread")
+                $("#mail"+messageId).addClass("read")
+            } else {
+                if ($("#readIcon"+messageId)) {
+                    $("#readIcon"+messageId).attr("src", "/assets/unread.svg")
+                }
+                $("#mail"+messageId).removeClass("read")
+                $("#mail"+messageId).addClass("unread")
+            }
+        },
+        error: function(jqXHR, textStatus, errorThrown) {
+        }
+    })
+}
+
+function toggleRead(messageId) {
+    if ($("#readIcon"+messageId)) {
+        setRead(messageId, $("#readIcon"+messageId).attr("src") == "/assets/unread.svg")
+    }
+}
+
+function removeMail(messageId) {
+    $.ajax({
+        url: "/remove",
+        data: {messageId: messageId},
+        success: function(result) {
+            $("#mail"+messageId).remove();
+            closeDetails()
+        },
+        error: function(jqXHR, textStatus, errorThrown) {
+        }
+    })
+}
+
+function setDetailsVisible(visible) {
+    if (visible) {
+        $("#details").show()
+        $("#messageList").css({pointerEvents: "none"})
+        clearInterval(updateTimerId)
+    } else {
+        $("#details").hide()
+        $("#details").html("")
+        $("#messageList").css({pointerEvents: "auto"})
+        updateTimerId = setInterval(updateMessageList, updateInterval)
+        updateMessageList()
+    }
+}
+
+function updateMessageList() {
+    $.ajax({
+        url: "/messageList",
+        success: function(result) {
+            if($("#messageList")) {
+                // console.log("result: " + result)
+                $("#messageList").html(result)
+            }
+        },
+        error: function(jqXHR, textStatus, errorThrown) {
+            if($("#messageList")) {
+                $("#messageList").html(textStatus)
+            }
+        }
+    })
 }

+ 89 - 7
web/server.go

@@ -113,8 +113,18 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 			s.handleMessageDetails(w, r)
 		case "/statusLine":
 			s.handleStatusLine(w, r)
-		default:
+		case "/":
+			s.handleMailbox(w, r)
+		case "/mailbox":
 			s.handleMailbox(w, r)
+		case "/setRead":
+			s.handleSetRead(w, r)
+		case "/remove":
+			s.handleRemove(w, r)
+		case "/messageList":
+			s.handleMessageList(w, r)
+		default:
+			s.error(http.StatusBadRequest, "Invalid request", w, r)
 		}
 	}
 }
@@ -166,7 +176,20 @@ func (s *Server) handleMessageDetails(w http.ResponseWriter, r *http.Request) {
 	}
 
 	s.storage.SetRead(user, messageId, true)
-	fmt.Fprint(w, s.templater.ExecuteDetails(mail))
+	fmt.Fprint(w, s.templater.ExecuteDetails(&struct {
+		From      string
+		To        string
+		Subject   string
+		Text      template.HTML
+		MessageId string
+		Read      bool
+	}{
+		From:      mail.Header.From,
+		To:        mail.Header.To,
+		Subject:   mail.Header.Subject,
+		Text:      template.HTML(mail.Body.RichText),
+		MessageId: messageId,
+	}))
 }
 
 func (s *Server) handleStatusLine(w http.ResponseWriter, r *http.Request) {
@@ -177,14 +200,26 @@ func (s *Server) handleStatusLine(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	info, err := s.storage.GetUserInfo(user)
+	if err != nil {
+		s.error(http.StatusInternalServerError, "Could not read user info", w, r)
+		return
+	}
+
+	unread, total, err := s.storage.GetEmailStats(user, user)
+	if err != nil {
+		s.error(http.StatusInternalServerError, "Could not read user stats", w, r)
+		return
+	}
+
 	fmt.Fprint(w, s.templater.ExecuteStatusLine(&struct {
 		Name   string
-		Read   int
 		Unread int
+		Total  int
 	}{
-		Name:   "No name", //TODO: read from database
-		Read:   0,         //TODO: read from database
-		Unread: 0,         //TODO: read from database
+		Name:   info.FullName,
+		Unread: unread,
+		Total:  total,
 	}))
 }
 
@@ -214,6 +249,53 @@ func (s *Server) handleMailbox(w http.ResponseWriter, r *http.Request) {
 	}))
 }
 
+func (s *Server) handleSetRead(w http.ResponseWriter, r *http.Request) {
+	user, token := s.extractAuth(w, r)
+	if !s.authenticator.Verify(user, token) {
+		s.logout(w, r)
+		s.error(http.StatusUnauthorized, "Unknown user credentials", w, r)
+		return
+	}
+
+	read := r.FormValue("read") == "true"
+	messageId := r.FormValue("messageId")
+	fmt.Printf("SetRead %s, %s, %v\n", user, messageId, read)
+	s.storage.SetRead(user, messageId, read)
+	w.WriteHeader(http.StatusOK)
+	fmt.Fprint(w, "Test")
+}
+
+func (s *Server) handleRemove(w http.ResponseWriter, r *http.Request) {
+	user, token := s.extractAuth(w, r)
+	if !s.authenticator.Verify(user, token) {
+		s.logout(w, r)
+		s.error(http.StatusUnauthorized, "Unknown user credentials", w, r)
+		return
+	}
+
+	messageId := r.FormValue("messageId")
+
+	s.storage.RemoveMail(user, messageId)
+}
+
+func (s *Server) handleMessageList(w http.ResponseWriter, r *http.Request) {
+	user, token := s.extractAuth(w, r)
+	if !s.authenticator.Verify(user, token) {
+		s.logout(w, r)
+		http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
+		return
+	}
+
+	mailList, err := s.storage.MailList(user, user, "Inbox", common.Frame{Skip: 0, Limit: 0})
+
+	if err != nil {
+		s.error(http.StatusInternalServerError, "Couldn't read email database", w, r)
+		return
+	}
+
+	fmt.Fprint(w, s.templater.ExecuteMailList(mailList))
+}
+
 func (s *Server) logout(w http.ResponseWriter, r *http.Request) {
 	fmt.Println("logout")
 
@@ -239,7 +321,7 @@ func (s *Server) error(code int, text string, w http.ResponseWriter, r *http.Req
 		Version string
 	}{
 		Code:    code,
-		Text:    "Unable to access your mailbox. Please contact Administrator.",
+		Text:    text,
 		Version: common.Version,
 	}))
 }

+ 16 - 5
web/templates/details.html

@@ -1,6 +1,17 @@
-<div class="btn materialLevel1" style="width: 40pt;" onclick="closeDetails();">
-    <img src="assets/back.svg" style="width: 20pt"/>
-</div>
-<div id="emailDetails">
-    {{.Header}}
+<div style="height: 100%; width: 100%; display:flex; flex-flow: column;">
+    <div style="width: 100%; display: flex; flex-flow: row;">
+        <div style="overflow-x: hidden; text-overflow: ellipsis; white-space: nowrap; flex-grow: 1;">
+            <span class="primaryText">From: {{.From}}</span></br>
+            <span class="secondaryText">To: {{.To}}</span></br>
+            <span class="primaryText">Subject: {{.Subject}}</span></br>
+        </div>
+        <img id="readIcon{{.MessageId}}" class="iconBtn" style="width: 20pt; margin-right: 10pt;" onclick="toggleRead({{.MessageId}});" src="assets/read.svg"/>
+        <img id="deleteIcon" class="iconBtn" style="width: 20pt; margin-right: 10pt;" onclick="removeMail({{.MessageId}});" src="assets/remove.svg"/>
+        <div class="btn materialLevel1" style="width: 40pt; right: 0pt; top: 0pt; margin: auto;" onclick="closeDetails();">
+            <img src="assets/back.svg" style="width: 20pt"/>
+        </div>
+    </div>
+    <div class="scrollable messageBody" style="margin-top: 10pt;">
+        <div style="padding: 5pt;">{{.Text}}</div>
+    </div>
 </div>

+ 1 - 3
web/templates/index.html

@@ -16,9 +16,7 @@
                 {{.Folders}}
             </div>
             <div id="statusLine"></div>
-            <div class="materialLevel1 contentBox">
-                {{.MailList}}
-            </div>
+            <div id="messageList" class="materialLevel1 contentBox"></div>
             <div id="details" class="materialLevel2 contentBox emailDetails" style="display: none;">
             </div>
         </div>

+ 14 - 14
web/templates/maillist.html

@@ -1,17 +1,17 @@
-<div id="maillist" class="scrollable">
+<div class="scrollable">
     <div class="fadeIn" style="position: absolute; top: 5pt; left: 0; right: 0; height: 10pt"></div>
-        <table class="mailList" >
-            <tbody>
-                {{range .}}
-                <tr id="mail{{.Id}}" class="{{if .Read}}read{{else}}unread{{end}}" onclick="openEmail('{{.Id}}');">
-                    <td class="fromCol">{{.Mail.Header.From}}</td>
-                    <td class="subjCol">{{.Mail.Header.Subject}}</td>
-                    <td class="dateCol"><script type="text/javascript">localDate({{.Mail.Header.Date}})</script></td>
-                </tr>
-                {{else}}
-                <tr><td><b>Mail folder is empty</b></td></tr>
-                {{end}}
-            </tbody>
-        </table>
+    <table class="mailList">
+        <tbody>
+            {{range .}}
+            <tr id="mail{{.Id}}" class="{{if .Read}}read{{else}}unread{{end}}" onclick="openEmail('{{.Id}}');">
+                <td class="fromCol">{{.Mail.Header.From}}</td>
+                <td class="subjCol">{{.Mail.Header.Subject}}</td>
+                <td id="mailDate{{.Id}}" class="dateCol" onload="$('#mailDate{{.Id}}').html(localDate({{.Mail.Header.Date}}))"></td>
+            </tr>
+            {{else}}
+            <tr><td><b>Mail folder is empty</b></td></tr>
+            {{end}}
+        </tbody>
+    </table>
     <div class="fadeOut" style="position: absolute; bottom: 5pt; left: 0; right: 0; height:10pt"></div>
 </div>

+ 1 - 1
web/templates/statusline.html

@@ -1 +1 @@
-Welcome {{.Name}}, you have {{.Read}}({{.Unread}}) messages
+Welcome {{.Name}}, you have {{.Unread}}({{.Total}}) messages

Some files were not shown because too many files changed in this diff