Browse Source

Sasl playground

- Add simple sasl authenticator
- Migrate to postfix based new mail indicator
- Fix mail ordering

TODO: Sasl is not working yet
Alexey Edelev 5 years ago
parent
commit
043ee7fba7
6 changed files with 256 additions and 33 deletions
  1. 1 1
      db/db.go
  2. 13 10
      main.go
  3. 183 0
      sasl/sasl.go
  4. 44 21
      scanner/parser.go
  5. 9 0
      utils/regexp.go
  6. 6 1
      web/mail.go

+ 1 - 1
db/db.go

@@ -302,7 +302,7 @@ func (s *Storage) MailList(user, email, folder string, frame common.Frame) ([]*c
 
 	request := bson.A{
 		bson.M{"$match": bson.M{"email": email, "folder": folder}},
-		bson.M{"$sort": bson.M{"mail.header.date": 1}},
+		bson.M{"$sort": bson.M{"mail.header.date": -1}},
 	}
 
 	if frame.Skip > 0 {

+ 13 - 10
main.go

@@ -26,7 +26,7 @@
 package main
 
 import (
-	"git.semlanik.org/semlanik/gostfix/db"
+	sasl "git.semlanik.org/semlanik/gostfix/sasl"
 	scanner "git.semlanik.org/semlanik/gostfix/scanner"
 	web "git.semlanik.org/semlanik/gostfix/web"
 )
@@ -34,12 +34,14 @@ import (
 type GofixEngine struct {
 	scanner *scanner.MailScanner
 	web     *web.Server
+	sasl    *sasl.SaslServer
 }
 
 func NewGofixEngine() (e *GofixEngine) {
 	e = &GofixEngine{
 		scanner: scanner.NewMailScanner(),
 		web:     web.NewServer(),
+		sasl:    sasl.NewSaslServer(),
 	}
 
 	return
@@ -47,21 +49,22 @@ func NewGofixEngine() (e *GofixEngine) {
 
 func (e *GofixEngine) Run() {
 	defer e.scanner.Stop()
+	e.sasl.Run()
 	e.scanner.Run()
 	e.web.Run()
 }
 
 func main() {
 	//Bad
-	storage, _ := db.NewStorage()
-	storage.AddUser("semlanik@semlanik.org", "test", "Alexey Edelev")
-	storage.AddUser("junkmail@semlanik.org", "test", "Alexey Edelev")
-	storage.AddUser("git@semlanik.org", "test", "Alexey Edelev")
-	storage.AddEmail("semlanik@semlanik.org", "ci@semlanik.org")
-	storage.AddEmail("semlanik@semlanik.org", "shopping@semlanik.org")
-	storage.AddEmail("semlanik@semlanik.org", "junkmail@semlanik.org")
-	storage.AddEmail("junkmail@semlanik.org", "qqqqq@semlanik.org")
-	storage.AddEmail("junkmail@semlanik.org", "main@semlanik.org")
+	// storage, _ := db.NewStorage()
+	// storage.AddUser("semlanik@semlanik.org", "test", "Alexey Edelev")
+	// storage.AddUser("junkmail@semlanik.org", "test", "Alexey Edelev")
+	// storage.AddUser("git@semlanik.org", "test", "Alexey Edelev")
+	// storage.AddEmail("semlanik@semlanik.org", "ci@semlanik.org")
+	// storage.AddEmail("semlanik@semlanik.org", "shopping@semlanik.org")
+	// storage.AddEmail("semlanik@semlanik.org", "junkmail@semlanik.org")
+	// storage.AddEmail("junkmail@semlanik.org", "qqqqq@semlanik.org")
+	// storage.AddEmail("junkmail@semlanik.org", "main@semlanik.org")
 	engine := NewGofixEngine()
 	engine.Run()
 }

+ 183 - 0
sasl/sasl.go

@@ -0,0 +1,183 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020 Alexey Edelev <semlanik@gmail.com>
+ *
+ * This file is part of gostfix project https://git.semlanik.org/semlanik/gostfix
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this
+ * software and associated documentation files (the "Software"), to deal in the Software
+ * without restriction, including without limitation the rights to use, copy, modify,
+ * merge, publish, distribute, sublicense, and/or sell copies of the Software, and
+ * to permit persons to whom the Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies
+ * or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+ * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
+ * FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+ * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+package sasl
+
+import (
+	"bufio"
+	"bytes"
+	"encoding/base64"
+	"encoding/hex"
+	"fmt"
+	"io"
+	"log"
+	"net"
+	"os"
+	"strconv"
+	"strings"
+
+	"github.com/google/uuid"
+)
+
+type SaslServer struct {
+	pid  int
+	cuid int
+}
+
+const (
+	Version = "VERSION"
+	CPid    = "CPID"
+	SPid    = "SPID"
+	Cuid    = "CUID"
+	Cookie  = "COOKIE"
+	Mech    = "MECH"
+	Done    = "DONE"
+	Auth    = "AUTH"
+	Fail    = "FAIL"
+	Cont    = "CONT"
+	Ok      = "OK"
+)
+
+const (
+	ContinueStateNone = iota
+	ContinueStateCredentials
+)
+
+func NewSaslServer() *SaslServer {
+	return &SaslServer{
+		pid:  os.Getpid(),
+		cuid: 0,
+	}
+}
+
+func (s *SaslServer) Run() {
+	go func() {
+		l, err := net.Listen("tcp", "127.0.0.1:65201")
+		if err != nil {
+			log.Fatalf("Coulf not start SASL server: %s\n", err)
+			return
+		}
+		defer l.Close()
+
+		log.Printf("Listen sasl on: %s\n", l.Addr().String())
+
+		for {
+			conn, err := l.Accept()
+			s.cuid++
+			if err != nil {
+				log.Println("Error accepting: ", err.Error())
+				continue
+			}
+			go s.handleRequest(conn)
+		}
+	}()
+}
+
+func (s *SaslServer) handleRequest(conn net.Conn) {
+	connectionReader := bufio.NewReader(conn)
+	continueState := ContinueStateNone
+	for {
+		fullbuf, err := connectionReader.ReadString('\n')
+
+		if err == io.EOF {
+			continue
+		}
+
+		if err != nil {
+			fmt.Printf("Read error %s\n", err)
+		}
+
+		currentMessage := fullbuf
+		if strings.Index(currentMessage, Version) == 0 {
+			versionIds := strings.Split(currentMessage, "\t")
+
+			if len(versionIds) < 3 {
+				break
+			}
+
+			if major, err := strconv.Atoi(versionIds[1]); err != nil || major != 1 {
+				break
+			}
+
+			cookieUuid := uuid.New()
+			fmt.Fprintf(conn, "%s\t%d\t%d\n", Version, 1, 2)
+			fmt.Fprintf(conn, "%s\t%s\t%s\n", Mech, "PLAIN", "plaintext")
+			fmt.Fprintf(conn, "%s\t%s\t%s\n", Mech, "LOGIN", "plaintext")
+
+			fmt.Fprintf(conn, "%s\t%d\n", SPid, s.pid)
+			fmt.Fprintf(conn, "%s\t%d\n", Cuid, s.cuid)
+
+			fmt.Fprintf(conn, "%s\t%s\n", Cookie, hex.EncodeToString(cookieUuid[:]))
+			fmt.Fprintf(conn, "%s\n", Done)
+		} else if strings.Index(currentMessage, Auth) == 0 {
+			authIds := strings.Split(currentMessage, "\t")
+			if len(authIds) < 2 {
+				break
+			}
+			fmt.Fprintf(conn, "%s\t%s\t%s\n", Cont, authIds[1], base64.StdEncoding.EncodeToString([]byte("Username:")))
+			continueState = ContinueStateCredentials
+		} else if strings.Index(currentMessage, Cont) == 0 {
+			contIds := strings.Split(currentMessage, "\t")
+			if len(contIds) < 2 {
+				break
+			}
+
+			if continueState == ContinueStateCredentials {
+				if len(contIds) < 3 {
+					fmt.Fprintf(conn, "%s\t%s\treason=%s\n", Fail, contIds[1], "invalid base64 data")
+					return
+				}
+
+				credentials, err := base64.StdEncoding.DecodeString(contIds[2])
+				if err != nil {
+					fmt.Fprintf(conn, "%s\t%s\treason=%s\n", Fail, contIds[1], "invalid base64 data")
+					return
+				}
+
+				credentialList := bytes.Split(credentials, []byte{0})
+				if len(credentialList) < 3 {
+					fmt.Fprintf(conn, "%s\t%s\treason=%s\n", Fail, contIds[1], "invalid user or password")
+					return
+				}
+
+				// identity := credentialList[0]
+				login := credentialList[1]
+				// password := credentialList[2]
+				//TODO: Use auth here
+				// if login != "semlanik@semlanik.org" || password != "test" {
+				if true {
+					fmt.Fprintf(conn, "%s\t%s\treason=%s\n", Fail, contIds[1], "invalid user or password")
+					return
+				}
+
+				fmt.Fprintf(conn, "%s\t%s\tuser=%s\n", Ok, contIds[1], login)
+				continueState = ContinueStateNone
+			} else {
+				fmt.Fprintf(conn, "%s\t%s\treason=%s\n", Fail, contIds[1], "invalid user or password")
+			}
+		}
+	}
+	conn.Close()
+}

+ 44 - 21
scanner/parser.go

@@ -30,6 +30,7 @@ import (
 	"bytes"
 	"encoding/hex"
 	"errors"
+	"fmt"
 	"log"
 	"os"
 	"strings"
@@ -85,9 +86,20 @@ func parseFile(file *utils.LockedFile) []*common.Mail {
 
 	scanner := bufio.NewScanner(file)
 	for scanner.Scan() {
+		currentText := scanner.Text()
+		if utils.RegExpUtilsInstance().MailIndicator.MatchString(currentText) {
+			if pd.mandatoryHeaders == AllHeaderMask {
+				pd.parseBody()
+				emails = append(emails, pd.email)
+			}
+			pd.reset()
+			fmt.Println("Found new email" + currentText)
+			continue
+		}
+
 		switch pd.state {
 		case StateHeaderScan:
-			if scanner.Text() == "" {
+			if currentText == "" {
 				if pd.mandatoryHeaders&AtLeastOneHeaderMask == AtLeastOneHeaderMask { //Cause we read at least one header
 					pd.previousHeader = nil
 					boundaryCapture := utils.RegExpUtilsInstance().BoundaryFinder.FindStringSubmatch(pd.bodyContentType)
@@ -99,28 +111,28 @@ func parseFile(file *utils.LockedFile) []*common.Mail {
 					pd.state = StateBodyScan
 				}
 			} else {
-				pd.parseHeader(scanner.Text())
+				pd.parseHeader(currentText)
 			}
 		case StateBodyScan:
-			if scanner.Text() == "" {
-				if pd.state == StateBodyScan && pd.activeBoundary == "" {
-					if pd.mandatoryHeaders == AllHeaderMask {
-						pd.parseBody()
-						emails = append(emails, pd.email)
-					}
-					pd.reset()
-					continue
-				}
-			}
-
-			if pd.activeBoundary != "" {
-				pd.bodyData += scanner.Text() + "\n"
-				capture := utils.RegExpUtilsInstance().BoundaryEndFinder.FindStringSubmatch(scanner.Text())
-				if len(capture) == 2 && pd.activeBoundary == capture[1] {
-					pd.state = StateBodyScan
-					pd.activeBoundary = ""
-				}
+			// if currentText == "" {
+			// 	if pd.state == StateBodyScan && pd.activeBoundary == "" {
+			// 		if pd.mandatoryHeaders == AllHeaderMask {
+			// 			pd.parseBody()
+			// 			emails = append(emails, pd.email)
+			// 		}
+			// 		pd.reset()
+			// 		continue
+			// 	}
+			// }
+
+			// if pd.activeBoundary != "" {
+			pd.bodyData += currentText + "\n"
+			capture := utils.RegExpUtilsInstance().BoundaryEndFinder.FindStringSubmatch(currentText)
+			if len(capture) == 2 && pd.activeBoundary == capture[1] {
+				pd.state = StateBodyScan
+				pd.activeBoundary = ""
 			}
+			// }
 		}
 	}
 
@@ -148,6 +160,11 @@ func (pd *parseData) parseHeader(headerRaw string) {
 		case "to":
 			pd.previousHeader = &pd.email.Header.To
 			pd.mandatoryHeaders |= ToHeaderMask
+		case "x-original-to":
+			if pd.email.Header.To == "" {
+				pd.previousHeader = &pd.email.Header.To
+				pd.mandatoryHeaders |= ToHeaderMask
+			}
 		case "cc":
 			pd.previousHeader = &pd.email.Header.Cc
 		case "bcc":
@@ -215,7 +232,13 @@ func (pd *parseData) parseBody() {
 }
 
 func parseDate(stringDate string) (int64, error) {
-	formatsToTest := []string{"Mon, _2 Jan 2006 15:04:05 -0700", time.RFC1123Z, time.RFC1123, time.UnixDate}
+	formatsToTest := []string{
+		"Mon, _2 Jan 2006 15:04:05 -0700",
+		time.RFC1123Z,
+		time.RFC1123,
+		time.UnixDate,
+		"Mon,  _2 Jan 2006 15:04:05 -0700 (MST)",
+		"Mon, _2 Jan 2006 15:04:05 -0700 (MST)"}
 	var err error
 	for _, format := range formatsToTest {
 		dateTime, err := time.Parse(format, stringDate)

+ 9 - 0
utils/regexp.go

@@ -32,6 +32,7 @@ import (
 )
 
 const (
+	NewMailIndicator    = "^(From\\s).*"
 	HeaderRegExp        = "^([\x21-\x7E^:]+):(.*)"
 	FoldingRegExp       = "^\\s+(.*)"
 	BoundaryStartRegExp = "^--(.*)"
@@ -62,6 +63,7 @@ func RegExpUtilsInstance() *RegExpUtils {
 }
 
 type regExpUtils struct {
+	MailIndicator       *regexp.Regexp
 	DomainChecker       *regexp.Regexp
 	EmailChecker        *regexp.Regexp
 	HeaderFinder        *regexp.Regexp
@@ -73,6 +75,12 @@ type regExpUtils struct {
 }
 
 func newRegExpUtils() (*regExpUtils, error) {
+	mailIndicator, err := regexp.Compile(NewMailIndicator)
+	if err != nil {
+		log.Fatalf("Invalid regexp %s\n", err)
+		return nil, err
+	}
+
 	headerFinder, err := regexp.Compile(HeaderRegExp)
 	if err != nil {
 		log.Fatalf("Invalid regexp %s\n", err)
@@ -122,6 +130,7 @@ func newRegExpUtils() (*regExpUtils, error) {
 	}
 
 	ru := &regExpUtils{
+		MailIndicator:       mailIndicator,
 		EmailChecker:        emailChecker,
 		HeaderFinder:        headerFinder,
 		FoldingFinder:       foldingFinder,

+ 6 - 1
web/mail.go

@@ -67,6 +67,11 @@ func (s *Server) handleMailDetails(w http.ResponseWriter, user, mailId string) {
 		return
 	}
 
+	text := mail.Body.RichText
+	if text == "" {
+		text = mail.Body.PlainText
+	}
+
 	s.storage.SetRead(user, mailId, true)
 	fmt.Fprint(w, s.templater.ExecuteDetails(&struct {
 		From    string
@@ -79,7 +84,7 @@ func (s *Server) handleMailDetails(w http.ResponseWriter, user, mailId string) {
 		From:    mail.Header.From,
 		To:      mail.Header.To,
 		Subject: mail.Header.Subject,
-		Text:    template.HTML(mail.Body.RichText),
+		Text:    template.HTML(text),
 		MailId:  mailId,
 	}))
 }