Explorar o código

Complete registration functionality

- Implement registration backend
- Update mailbox mappings after registration complete
- Add registration fields validators
Alexey Edelev %!s(int64=5) %!d(string=hai) anos
pai
achega
b55613e1be
Modificáronse 9 ficheiros con 229 adicións e 33 borrados
  1. 30 0
      common/scanner.go
  2. 30 1
      db/db.go
  3. 3 12
      main.go
  4. 4 0
      scanner/mailscanner.go
  5. 9 0
      utils/regexp.go
  6. 2 2
      web/js/index.js
  7. 44 6
      web/server.go
  8. 2 2
      web/templates/login.html
  9. 105 10
      web/templates/register.html

+ 30 - 0
common/scanner.go

@@ -0,0 +1,30 @@
+/*
+ * 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 common
+
+type Scanner interface {
+	Reconfigure()
+}

+ 30 - 1
db/db.go

@@ -32,6 +32,9 @@ import (
 	"errors"
 	"fmt"
 	"log"
+	"os"
+	"os/exec"
+	"strings"
 	"time"
 
 	common "git.semlanik.org/semlanik/gostfix/common"
@@ -161,12 +164,33 @@ func (s *Storage) addEmail(user string, email string, upsert bool) error {
 		}
 	}
 
+	file, err := os.OpenFile(config.ConfigInstance().VMailboxMaps, os.O_APPEND|os.O_WRONLY, 0664)
+	if err != nil {
+		return errors.New("Unable to add email to maps" + err.Error())
+	}
+
+	emailParts := strings.Split(email, "@")
+
+	if len(emailParts) != 2 {
+		return errors.New("Invalid email format")
+	}
+
+	_, err = file.WriteString(email + " " + emailParts[1] + "/" + emailParts[0] + "\n")
+	if err != nil {
+		return errors.New("Unable to add email to maps" + err.Error())
+	}
+
+	cmd := exec.Command("postmap", config.ConfigInstance().VMailboxMaps)
+	err = cmd.Run()
+	if err != nil {
+		return errors.New("Unable to execute postmap")
+	}
+
 	_, err = s.emailsCollection.UpdateOne(context.Background(),
 		bson.M{"user": user},
 		bson.M{"$addToSet": bson.M{"email": email}},
 		options.Update().SetUpsert(upsert))
 
-	//TODO: Update postfix virtual map here
 	return err
 }
 
@@ -537,6 +561,11 @@ func (s *Storage) GetAllEmails() (emails []string, err error) {
 	return nil, err
 }
 
+func (s *Storage) CheckEmailExists(email string) bool {
+	result := s.allEmailsCollection.FindOne(context.Background(), bson.M{"emails": email})
+	return result.Err() == nil
+}
+
 func (s *Storage) GetFolders(email string) (folders []*common.Folder) {
 	folders = []*common.Folder{
 		&common.Folder{Name: common.Inbox, Custom: false},

+ 3 - 12
main.go

@@ -39,9 +39,10 @@ type GofixEngine struct {
 }
 
 func NewGofixEngine() (e *GofixEngine) {
+	mailScanner := scanner.NewMailScanner()
 	e = &GofixEngine{
-		scanner: scanner.NewMailScanner(),
-		web:     web.NewServer(),
+		scanner: mailScanner,
+		web:     web.NewServer(mailScanner),
 		sasl:    sasl.NewSaslServer(),
 	}
 
@@ -57,16 +58,6 @@ func (e *GofixEngine) 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")
 	defer profile.Start().Stop()
 	engine := NewGofixEngine()
 	engine.Run()

+ 4 - 0
scanner/mailscanner.go

@@ -85,6 +85,10 @@ func NewMailScanner() (ms *MailScanner) {
 	return
 }
 
+func (ms *MailScanner) Reconfigure() {
+	ms.signalChannel <- SignalReconfigure
+}
+
 func (ms *MailScanner) checkEmailRegistred(email string) bool {
 	emails, err := ms.storage.GetAllEmails()
 

+ 9 - 0
utils/regexp.go

@@ -39,6 +39,7 @@ const (
 	BoundaryEndRegExp   = "^--(.*)--$"
 	BoundaryRegExp      = "boundary=\"(.*)\""
 	MailboxRegExp       = "^/m(\\d+)/?(.*)"
+	FullnameRegExp      = "^[\\w ]*$"
 )
 
 const (
@@ -72,6 +73,7 @@ type regExpUtils struct {
 	BoundaryEndFinder   *regexp.Regexp
 	BoundaryFinder      *regexp.Regexp
 	MailboxFinder       *regexp.Regexp
+	FullnameChecker     *regexp.Regexp
 }
 
 func newRegExpUtils() (*regExpUtils, error) {
@@ -129,6 +131,12 @@ func newRegExpUtils() (*regExpUtils, error) {
 		return nil, err
 	}
 
+	fullnameChecker, err := regexp.Compile(FullnameRegExp)
+	if err != nil {
+		log.Fatalf("Invalid regexp %s\n", err)
+		return nil, err
+	}
+
 	ru := &regExpUtils{
 		MailIndicator:       mailIndicator,
 		EmailChecker:        emailChecker,
@@ -139,6 +147,7 @@ func newRegExpUtils() (*regExpUtils, error) {
 		BoundaryFinder:      boundaryFinder,
 		DomainChecker:       domainChecker,
 		MailboxFinder:       mailboxFinder,
+		FullnameChecker:     fullnameChecker,
 	}
 
 	return ru, nil

+ 2 - 2
web/js/index.js

@@ -104,7 +104,7 @@ function toEmailFieldChanged(e) {
     }
 
     var lastChar = actualText[selectionPosition]
-    if (lastChar.match(emailEndRegex)) {
+    if (emailEndRegex.test(lastChar)) {
         addToEmail(actualText.slice(0, selectionPosition))
         $("#toEmailField").val(actualText.slice(selectionPosition + 1, actualText.length))
     }
@@ -114,7 +114,7 @@ function addToEmail(toEmail) {
     if (toEmail.length <= 0) {
         return
     }
-    var style = toEmail.match(emailRegex) ? "valid" : "invalid"
+    var style = emailRegex.test(toEmail) ? "valid" : "invalid"
     $("<div class=\""+ style + " toEmail\" id=\"toEmail" + toEmailIndex + "\">" + toEmail + "<img class=\"iconBtn\" style=\"height: 12px; margin-left:10px; margin: auto;\" onclick=\"removeToEmail('toEmail" + toEmailIndex + "', '" + toEmail + "');\" src=\"/assets/cross.svg\"/></div>").insertBefore("#toEmailField")
     toEmailIndex++
     toEmailList.push(toEmail)

+ 44 - 6
web/server.go

@@ -66,9 +66,10 @@ type Server struct {
 	sessionStore  *sessions.CookieStore
 	storage       *db.Storage
 	Notifier      *webNotifier
+	scanner       common.Scanner
 }
 
-func NewServer() *Server {
+func NewServer(scanner common.Scanner) *Server {
 
 	storage, err := db.NewStorage()
 
@@ -84,6 +85,7 @@ func NewServer() *Server {
 		sessionStore:  sessions.NewCookieStore(make([]byte, 32)),
 		storage:       storage,
 		Notifier:      NewWebNotifier(),
+		scanner:       scanner,
 	}
 
 	return s
@@ -125,6 +127,8 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 			s.handleLogout(w, r)
 		case "/register":
 			s.handleRegister(w, r)
+		case "/checkEmail":
+			s.handleCheckEmail(w, r)
 		case "/mail":
 			fallthrough
 		case "/setRead":
@@ -148,15 +152,31 @@ func (s *Server) handleRegister(w http.ResponseWriter, r *http.Request) {
 	}
 
 	if err := r.ParseForm(); err == nil {
-		// user := r.FormValue("user")
-		// password := r.FormValue("password")
-		// fullname := r.FormValue("fullname")
+		user := r.FormValue("user")
+		password := r.FormValue("password")
+		fullname := r.FormValue("fullname")
+		if user != "" && password != "" && fullname != "" {
+			ok, email := s.checkEmail(user)
+			if ok && len(password) < 128 && len(fullname) < 128 && utils.RegExpUtilsInstance().FullnameChecker.MatchString(fullname) {
+				err := s.storage.AddUser(email, password, fullname)
+				if err != nil {
+					log.Println(err.Error())
+					s.error(http.StatusInternalServerError, "Unable to create user", w)
+					return
+				}
+
+				s.scanner.Reconfigure()
+				token, _ := s.authenticator.Authenticate(email, password)
+				s.login(email, token, w, r)
+				return
+			}
+		}
 	}
 
 	fmt.Fprint(w, s.templater.ExecuteRegister(&struct {
 		Version string
-	}{common.Version}))
-
+		Domain  string
+	}{common.Version, config.ConfigInstance().MyDomain}))
 }
 
 func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
@@ -197,6 +217,24 @@ func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) {
 	http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
 }
 
+func (s *Server) handleCheckEmail(w http.ResponseWriter, r *http.Request) {
+	if err := r.ParseForm(); err == nil {
+		if ok, _ := s.checkEmail(r.FormValue("user")); ok {
+			w.Write([]byte{0})
+			return
+		}
+		s.error(http.StatusNotAcceptable, "Email exists", w)
+		return
+	}
+	s.error(http.StatusBadRequest, "Invalid arguments", w)
+	return
+}
+
+func (s *Server) checkEmail(user string) (bool, string) {
+	email := user + "@" + config.ConfigInstance().MyDomain
+	return utils.RegExpUtilsInstance().EmailChecker.MatchString(email) && !s.storage.CheckEmailExists(email), email
+}
+
 func (s *Server) logout(w http.ResponseWriter, r *http.Request) {
 	fmt.Println("logout")
 

+ 2 - 2
web/templates/login.html

@@ -16,13 +16,13 @@
                 <div style="display: flex; flex-direction: column; width: 100%; height: 100%; justify-content: center;">
                     <form method="POST" action="/login" style="margin: 0 auto;">
                         <div class="inpt">
-                            <input name="user" type="text" required>
+                            <input name="user" type="text" required autocomplete="off">
                             <span class="highlight"></span>
                             <span class="bar"></span>
                             <label>Email</label>
                         </div>
                         <div class="inpt">
-                            <input name="password" type="password" required>
+                            <input name="password" type="password" required autocomplete="off">
                             <span class="highlight"></span>
                             <span class="bar"></span>
                             <label>Password</label>

+ 105 - 10
web/templates/register.html

@@ -9,16 +9,111 @@
         <link type="text/css" href="/css/controls.css" rel="stylesheet">
         <script src="/js/jquery-3.4.1.min.js"></script>
         <script>
+            const emailRegex = /^[a-zA-Z]+[\w\d\._-]*$/
+            const passwordRegex = /[A-Z0-6!"\#$%&'()*+,\-./:;<=>?@\[\\\]^_‘{|}~]/
+            const fullnameRegex = /^[\w ]*$/
+            var emailOk = false
+
             $(document).ready(function() {
-                $("#showPasswordButton").mousedown(function() {
-                    $("#passwordField").attr("type", "text")
+                $('#showPasswordButton').mousedown(function() {
+                    $('#passwordField').attr('type', 'text')
                 })
 
-                $("#showPasswordButton").mouseup(function() {
-                    $("#passwordField").attr("type", "password")
+                $('#showPasswordButton').mouseup(function() {
+                    $('#passwordField').attr('type', 'password')
                 })
 
+                validateEmail($('#userField'))
+                $('#fullnameField').on('input', validateFields)
+                $('#userField').on('input', function(e) { validateEmail($(e.target)) })
+                $('#passwordField').on('input', validateFields)
             })
+
+            function submitChanges() {
+                if (emailOk && validatePassword($('#passwordField')) && validateFullname($('#fullnameField'))) {
+                    $('#registerForm').submit()
+                }
+            }
+
+            var emailInputTimer = null
+            function validateEmail(element) {
+                emailOk = false
+                validateFields()
+                var fieldDiv = element.parent()
+                if (emailRegex.test(element.val())) {
+                    clearTimeout(emailInputTimer)
+                    emailInputTimer = setTimeout(emailCheck, 200)
+                    return
+                } else {
+                    fieldDiv.addClass("bad")
+                }
+            }
+
+            function emailCheck() {
+                var element = $('#userField')
+                var fieldDiv = element.parent()
+
+                $.ajax({
+                    url: "/checkEmail",
+                    data: {
+                        user: element.val()
+                    },
+                    success: function(result) {
+                        emailOk = true
+                        validateFields()
+                        fieldDiv.removeClass("bad")
+                    },
+                    error: function(jqXHR, textStatus, errorThrown) {
+                        emailOk = false
+                        validateFields()
+                        fieldDiv.addClass("bad")
+                    }
+                })
+            }
+
+            function validatePassword(element) {
+                var fieldDiv = element.parent()
+                if (validateField(element)) {
+                    if (element.val().length < 8 || !passwordRegex.test(element.val())) {
+                        fieldDiv.addClass("weak")
+                    } else {
+                        fieldDiv.removeClass("weak")
+                    }
+                    return true
+                }
+
+                return false
+            }
+
+            function validateFullname(element) {
+                var fieldDiv = element.parent()
+                if (fullnameRegex.test(element.val())) {
+                    fieldDiv.removeClass("bad")
+                    return true
+                }
+                fieldDiv.addClass("bad")
+                return false
+            }
+
+            function validateField(element) {
+                var fieldDiv = element.parent()
+                if (element.val() != '') {
+                    fieldDiv.removeClass('bad')
+                    return true
+                }
+                fieldDiv.addClass('bad')
+                return false
+            }
+
+            function validateFields() {
+                var passwordOk = validatePassword($('#passwordField'))
+                var fullnameOk = validateFullname($('#fullnameField'))
+                if (emailOk && passwordOk && fullnameOk) {
+                    $('#registerButton').removeClass('disabled')
+                } else {
+                    $('#registerButton').addClass('disabled')
+                }
+            }
         </script>
         <title>Gostfix mail {{.Version}}</title>
     </head>
@@ -26,27 +121,27 @@
         <div id="main">
             <div class="horizontalPaddingBox">
                 <div style="display: flex; flex-direction: column; width: 100%; height: 100%; justify-content: center;">
-                    <form method="POST" action="/register" style="margin: 0 auto;">
+                    <form id="registerForm" method="POST" action="/register" style="margin: 0 auto;">
                         <div class="inpt bad">
-                            <input name="fullname" type="text" required>
+                            <input id="fullnameField" name="fullname" type="text" required maxlength="128" autocomplete="off">
                             <span class="highlight"></span>
                             <span class="bar"></span>
                             <label>Full name</label>
                         </div>
                         <div class="inpt bad">
-                            <input name="user" type="text" required>
+                            <input id="userField" name="user" type="text" required maxlength="64" autocomplete="off">
                             <span class="highlight"></span>
                             <span class="bar"></span>
-                            <label>Email</label>
+                            <label>User @{{.Domain}}</label>
                         </div>
                         <div class="inpt bad">
-                            <input id="passwordField" name="password" type="password" required>
+                            <input id="passwordField" name="password" type="password" required maxlength="128" autocomplete="off">
                             <span class="highlight"></span>
                             <span class="bar"></span>
                             <label>Password</label>
                             <img id="showPasswordButton" src="/assets/eye.svg"/>
                         </div>
-                        <input type="submit" style="visibility: hidden;" />
+                        <div id="registerButton" class="btn materialLevel1 disabled" style="margin-bottom: 15px;" onclick="submitChanges();">Register</div>
                     </form>
                 </div>
             </div>