Browse Source

Refactor mail notifications

- Move notification logic to database
- Rework new mail notifications
- Add link to the project page into templates
Alexey Edelev 3 years ago
parent
commit
74e9777f1b

+ 1 - 1
README.md

@@ -36,7 +36,7 @@ gostfix only works on Linux-like operating systems
     }
 
     # Add web sockets proxy
-    location ~ ^/m[\d]+/notifierSubscribe$ {
+    location ~ ^/m/[\d]+/notifierSubscribe$ {
         proxy_pass http://localhost:65200;
         proxy_http_version 1.1;
         proxy_set_header Upgrade $http_upgrade;

+ 1 - 1
common/notifier.go

@@ -27,5 +27,5 @@ package common
 
 type Notifier interface {
 	NotifyMaiboxUpdate(email string)
-	NotifyNewMail(email string, m Mail)
+	NotifyNewMail(email string, m MailMetadata)
 }

+ 53 - 4
db/db.go

@@ -34,6 +34,7 @@ import (
 	"log"
 	"os"
 	"strings"
+	"sync"
 	"time"
 
 	common "git.semlanik.org/semlanik/gostfix/common"
@@ -49,6 +50,15 @@ import (
 	config "git.semlanik.org/semlanik/gostfix/config"
 )
 
+type StuctNotifiers struct {
+	notifiers     []common.Notifier
+	notifiersLock sync.Mutex
+}
+
+var notifiers StuctNotifiers = StuctNotifiers{
+	notifiers: []common.Notifier{},
+}
+
 type Storage struct {
 	db                  *mongo.Database
 	usersCollection     *mongo.Collection
@@ -263,14 +273,14 @@ func (s *Storage) RemoveEmail(user string, email string) error {
 }
 
 func (s *Storage) SaveMail(email, folder string, m *common.Mail, read bool) error {
-	result := &struct {
+	user := &struct {
 		User string
 	}{}
 
-	s.emailsCollection.FindOne(context.Background(), bson.M{"email": email}).Decode(result)
+	s.emailsCollection.FindOne(context.Background(), bson.M{"email": email}).Decode(user)
 
-	mailsCollection := s.db.Collection(qualifiedMailCollection(result.User))
-	mailsCollection.InsertOne(context.Background(), &struct {
+	mailsCollection := s.db.Collection(qualifiedMailCollection(user.User))
+	result, err := mailsCollection.InsertOne(context.Background(), &struct {
 		Email  string
 		Mail   *common.Mail
 		Folder string
@@ -283,6 +293,21 @@ func (s *Storage) SaveMail(email, folder string, m *common.Mail, read bool) erro
 		Read:   read,
 		Trash:  false,
 	}, options.InsertOne().SetBypassDocumentValidation(true))
+
+	if err != nil {
+		return err
+	}
+
+	mail := *m //deep copy for multithreading
+	s.notifyNewMail(email, common.MailMetadata{
+		Id:     result.InsertedID.(primitive.ObjectID).Hex(),
+		Read:   false,
+		Trash:  false,
+		Folder: folder,
+		User:   user.User,
+		Mail:   &mail,
+	})
+
 	return nil
 }
 
@@ -627,3 +652,27 @@ func (s *Storage) CheckAttachment(user, attachment string) bool {
 	result := mailsCollection.FindOne(context.Background(), bson.M{"mail.body.attachments.id": attachment})
 	return result.Err() == nil
 }
+
+func (s *Storage) RegisterNotifier(notifier common.Notifier) {
+	if notifier != nil {
+		notifiers.notifiersLock.Lock()
+		defer notifiers.notifiersLock.Unlock()
+		notifiers.notifiers = append(notifiers.notifiers, notifier)
+	}
+}
+
+func (s *Storage) notifyNewMail(email string, mail common.MailMetadata) {
+	notifiers.notifiersLock.Lock()
+	defer notifiers.notifiersLock.Unlock()
+	for _, notifier := range notifiers.notifiers {
+		notifier.NotifyNewMail(email, mail)
+	}
+}
+
+func (s *Storage) notifyMailboxUpdate(email string) {
+	notifiers.notifiersLock.Lock()
+	defer notifiers.notifiersLock.Unlock()
+	for _, notifier := range notifiers.notifiers {
+		notifier.NotifyMaiboxUpdate(email)
+	}
+}

+ 0 - 2
main.go

@@ -51,8 +51,6 @@ func NewGofixEngine() (e *GofixEngine) {
 		web:     web.NewServer(mailScanner),
 		sasl:    saslService,
 	}
-
-	e.scanner.RegisterNotifier(e.web.Notifier)
 	return
 }
 

+ 0 - 32
scanner/mailscanner.go

@@ -29,7 +29,6 @@ import (
 	"fmt"
 	"log"
 	"os"
-	"sync"
 
 	"git.semlanik.org/semlanik/gostfix/common"
 	config "git.semlanik.org/semlanik/gostfix/config"
@@ -47,8 +46,6 @@ type MailScanner struct {
 	emailMaps     map[string]string
 	storage       *db.Storage
 	signalChannel chan int
-	notifiers     []common.Notifier
-	notifiersLock sync.Mutex
 }
 
 func NewMailScanner() (ms *MailScanner) {
@@ -77,7 +74,6 @@ func NewMailScanner() (ms *MailScanner) {
 		watcher:       watcher,
 		storage:       storage,
 		signalChannel: make(chan int),
-		notifiers:     []common.Notifier{},
 	}
 
 	return
@@ -163,12 +159,8 @@ func (ms *MailScanner) Run() {
 
 					if mailbox != "" {
 						mails := ms.readMailFile(mailPath)
-						if len(mails) > 0 {
-							ms.notifyMailboxUpdate(mailbox)
-						}
 						for _, mail := range mails {
 							ms.storage.SaveMail(mailbox, common.Inbox, mail, false)
-							ms.notifyNewMail(mailbox, *mail)
 						}
 						log.Printf("New email for %s, emails read %d", mailPath, len(mails))
 					} else {
@@ -210,27 +202,3 @@ func (ms *MailScanner) readMailFile(mailPath string) (mails []*common.Mail) {
 
 	return mails
 }
-
-func (ms *MailScanner) RegisterNotifier(notifier common.Notifier) {
-	if notifier != nil {
-		ms.notifiersLock.Lock()
-		defer ms.notifiersLock.Unlock()
-		ms.notifiers = append(ms.notifiers, notifier)
-	}
-}
-
-func (ms *MailScanner) notifyNewMail(email string, mail common.Mail) {
-	ms.notifiersLock.Lock()
-	defer ms.notifiersLock.Unlock()
-	for _, notifier := range ms.notifiers {
-		notifier.NotifyNewMail(email, mail)
-	}
-}
-
-func (ms *MailScanner) notifyMailboxUpdate(email string) {
-	ms.notifiersLock.Lock()
-	defer ms.notifiersLock.Unlock()
-	for _, notifier := range ms.notifiers {
-		notifier.NotifyMaiboxUpdate(email)
-	}
-}

+ 0 - 1
scanner/parser.go

@@ -100,7 +100,6 @@ func parseFile(file *utils.LockedFile) []*common.Mail {
 				emails = append(emails, pd.email)
 			}
 			pd.reset()
-			fmt.Println("Found new email" + currentText)
 			continue
 		}
 

+ 7 - 2
web/css/styles.css

@@ -116,7 +116,12 @@ body {
 }
 
 #copyrightBox {
-    color: var(--secondary-dark-color)
+    color: var(--secondary-dark-color);
+}
+
+#copyrightBox a {
+    color: var(--secondary-dark-color);
+    text-decoration: underline;
 }
 
 .toEmail.valid {
@@ -140,4 +145,4 @@ body {
     box-shadow: var(--level2-shadow);
     border-bottom-left-radius: var(--default-radius);
     border-bottom-right-radius: var(--default-radius);
-}
+}

+ 11 - 4
web/js/index.js

@@ -564,11 +564,18 @@ function connectNotifier() {
         protocol = 'ws://';
     }
     notifierSocket = new WebSocket(protocol + window.location.host + '/m/' + mailbox + '/notifierSubscribe');
-    notifierSocket.onmessage = function (e) {
-        for (var i = 0; i < folders.length; i++) {
-            folderStat(folders[i]);
+    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]);
+                }
+            }
+            break;
         }
-        updateMailList(currentFolder, currentPage);
     }
 }
 

+ 1 - 1
web/mailbox.go

@@ -100,7 +100,7 @@ func (s *Server) handleMailboxRequest(w http.ResponseWriter, r *http.Request, us
 	case "sendNewMail":
 		s.handleNewMail(w, r, user, emails[mailbox])
 	case "notifierSubscribe":
-		s.Notifier.handleNotifierRequest(w, r, emails[mailbox])
+		s.notifier.handleNotifierRequest(w, r, emails[mailbox])
 	default:
 		http.Redirect(w, r, "/m/0", http.StatusTemporaryRedirect)
 	}

+ 5 - 4
web/server.go

@@ -64,7 +64,7 @@ type Server struct {
 	templater         *Templater
 	sessionStore      *sessions.CookieStore
 	storage           *db.Storage
-	Notifier          *webNotifier
+	notifier          *webNotifier
 	scanner           common.Scanner
 }
 
@@ -89,10 +89,13 @@ func NewServer(scanner common.Scanner) *Server {
 		attachmentsServer: http.StripPrefix("/attachment/", http.FileServer(http.Dir(config.ConfigInstance().AttachmentsPath))),
 		sessionStore:      sessions.NewCookieStore(make([]byte, 32)),
 		storage:           storage,
-		Notifier:          NewWebNotifier(),
+		notifier:          NewWebNotifier(),
 		scanner:           scanner,
 	}
 
+	s.notifier.server = s
+	s.storage.RegisterNotifier(s.notifier)
+
 	return s
 }
 
@@ -152,8 +155,6 @@ func (s *Server) handleSecure(w http.ResponseWriter, r *http.Request, urlParts [
 	case "mail":
 		if len(urlParts) == 2 {
 			s.handleMailRequest(w, r, user, urlParts[1])
-		} else {
-			//TODO: return mail list here
 		}
 	case "settings":
 		s.handleSettings(w, r, user)

+ 1 - 1
web/templates/error.html

@@ -23,7 +23,7 @@
                     </div>
                 </div>
             </div>
-            <div id="copyrightBox" class="elidedText"><img src="/assets/logo.svg" height="30px"/>gostfix {{.Version}} Web interface. Copyright (c) 2020 Alexey Edelev &lt;semlanik@gmail.com&gt;</div>
+            <div id="copyrightBox" class="elidedText"><img src="/assets/logo.svg" height="30px"/><a href="https://github.com/semlanik/gostfix" target="_blank">gostfix</a>&nbsp;{{.Version}} Web interface. Copyright (c) 2020 Alexey Edelev &lt;semlanik@gmail.com&gt;</div>
         </div>
     </body>
 </html>

+ 1 - 1
web/templates/index.html

@@ -57,7 +57,7 @@
                     </div>
                 </div>
             </div>
-            <div id="copyrightBox" class="elidedText"><img src="/assets/logo.svg" height="30px"/>gostfix {{.Version}} Web interface. Copyright (c) 2020 Alexey Edelev &lt;semlanik@gmail.com&gt;</div>
+            <div id="copyrightBox" class="elidedText"><img src="/assets/logo.svg" height="30px"/><a href="https://github.com/semlanik/gostfix" target="_blank">gostfix</a>&nbsp;{{.Version}} Web interface. Copyright (c) 2020 Alexey Edelev &lt;semlanik@gmail.com&gt;</div>
         </div>
     </body>
 </html>

+ 1 - 1
web/templates/login.html

@@ -33,7 +33,7 @@
                     {{.Signup}}
                 </div>
             </div>
-            <div id="copyrightBox" class="elidedText"><img src="/assets/logo.svg" height="30px"/>gostfix {{.Version}} Web interface. Copyright (c) 2020 Alexey Edelev &lt;semlanik@gmail.com&gt;</div>
+            <div id="copyrightBox" class="elidedText"><img src="/assets/logo.svg" height="30px"/><a href="https://github.com/semlanik/gostfix" target="_blank">gostfix</a>&nbsp;{{.Version}} Web interface. Copyright (c) 2020 Alexey Edelev &lt;semlanik@gmail.com&gt;</div>
         </div>
     </body>
 </html>

+ 1 - 1
web/templates/register.html

@@ -55,7 +55,7 @@
                     </form>
                 </div>
             </div>
-            <div id="copyrightBox" class="elidedText"><img src="/assets/logo.svg" height="30px"/>gostfix {{.Version}} Web interface. Copyright (c) 2020 Alexey Edelev &lt;semlanik@gmail.com&gt;</div>
+            <div id="copyrightBox" class="elidedText"><img src="/assets/logo.svg" height="30px"/><a href="https://github.com/semlanik/gostfix" target="_blank">gostfix</a>&nbsp;{{.Version}} Web interface. Copyright (c) 2020 Alexey Edelev &lt;semlanik@gmail.com&gt;</div>
         </div>
     </body>
 </html>

+ 1 - 1
web/templates/settings.html

@@ -80,7 +80,7 @@
                     </div>
                 </div>
             </div>
-            <div id="copyrightBox" class="elidedText"><img src="/assets/logo.svg" height="30px"/>gostfix {{.Version}} Web interface. Copyright (c) 2020 Alexey Edelev &lt;semlanik@gmail.com&gt;</div>
+            <div id="copyrightBox" class="elidedText"><img src="/assets/logo.svg" height="30px"/><a href="https://github.com/semlanik/gostfix" target="_blank">gostfix</a>&nbsp;{{.Version}} Web interface. Copyright (c) 2020 Alexey Edelev &lt;semlanik@gmail.com&gt;</div>
         </div>
     </body>
 </html>

+ 25 - 9
web/webnotifier.go

@@ -26,6 +26,7 @@
 package web
 
 import (
+	"encoding/json"
 	"fmt"
 	"log"
 	"net/http"
@@ -37,10 +38,11 @@ import (
 
 type websocketChannel struct {
 	connection *websocket.Conn
-	channel    chan *common.Mail
+	channel    chan *common.MailMetadata
 }
 
 type webNotifier struct {
+	server        *Server
 	notifiers     map[string]*websocketChannel
 	notifiersLock sync.Mutex
 }
@@ -53,14 +55,14 @@ func NewWebNotifier() *webNotifier {
 
 func (wn *webNotifier) NotifyMaiboxUpdate(email string) {
 	if channel, ok := wn.getNotifier(email); ok {
-		channel.channel <- &common.Mail{} //TODO: Dummy notificator for now, later need to make separate interface to handle this
+		channel.channel <- &common.MailMetadata{} //TODO: Dummy notificator for now, later need to make separate interface to handle this
 	}
 }
 
-func (wn *webNotifier) NotifyNewMail(email string, m common.Mail) {
-	// if channel, ok := wn.getNotifier(email); ok {
-	// 	channel.channel <- &m
-	// }
+func (wn *webNotifier) NotifyNewMail(email string, m common.MailMetadata) {
+	if channel, ok := wn.getNotifier(email); ok {
+		channel.channel <- &m
+	}
 	//TODO: this functionality needs JS support to create new mails from templates
 }
 
@@ -73,13 +75,14 @@ func (wn *webNotifier) handleNotifierRequest(w http.ResponseWriter, r *http.Requ
 	fmt.Printf("New web socket session start %s\n", email)
 	conn, err := upgrader.Upgrade(w, r, nil)
 	if err != nil {
+		log.Printf("Could not upgrade websocket %s\n", err)
 		http.Error(w, "Could not open websocket connection", http.StatusBadRequest)
 		return
 	}
 
 	c := &websocketChannel{
 		connection: conn,
-		channel:    make(chan *common.Mail, 10),
+		channel:    make(chan *common.MailMetadata, 10),
 	}
 	wn.addNotifier(email, c)
 
@@ -94,11 +97,24 @@ func (wn *webNotifier) handleNotifierRequest(w http.ResponseWriter, r *http.Requ
 }
 
 func (wn *webNotifier) handleNotifications(c *websocketChannel) {
-	//Do nothing for now
 	for {
 		select {
 		case newMail := <-c.channel:
-			err := c.connection.WriteJSON(newMail)
+			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)
 			if err != nil {
 				log.Println(err.Error())
 				return