Browse Source

Refactor login functionality

- Move verification and authentication to Authernticator
- Rework url handling in web server
- Add data files install rules
Alexey Edelev 4 years ago
parent
commit
4940e01823
6 changed files with 301 additions and 223 deletions
  1. 1 0
      .gitignore
  2. 60 3
      auth/authenticator.go
  3. 11 1
      build.sh
  4. 201 207
      web/server.go
  5. 27 11
      web/templater.go
  6. 1 1
      web/templates/error.html

+ 1 - 0
.gitignore

@@ -24,3 +24,4 @@ _testmain.go
 *.test
 *.prof
 
+data/

+ 60 - 3
auth/authenticator.go

@@ -26,13 +26,70 @@
 package auth
 
 import (
+	"bufio"
+	"log"
+	"os"
+	"strings"
+
+	config "../config"
 	utils "../utils"
 )
 
-func Authenticate(email string, password string) bool {
-	if !utils.RegExpUtilsInstance().EmailChecker.MatchString(email) {
+type Authenticator struct {
+	mailMaps map[string]string //TODO: temporary here. Later should be part of mailscanner and never accessed from here
+}
+
+func NewAuthenticator() (a *Authenticator) {
+	a = &Authenticator{
+		mailMaps: readMailMaps(), //TODO: temporary here. Later should be part of mailscanner and never accessed from here
+	}
+	return
+}
+
+func (a *Authenticator) Authenticate(user, password string) (string, bool) {
+	if !utils.RegExpUtilsInstance().EmailChecker.MatchString(user) {
+		return "", false
+	}
+	_, ok := a.mailMaps[user]
+
+	return "", ok
+}
+
+func (a *Authenticator) Verify(user, token string) bool {
+	if !utils.RegExpUtilsInstance().EmailChecker.MatchString(user) {
 		return false
 	}
+	_, ok := a.mailMaps[user]
+
+	return ok
+}
+
+func (a *Authenticator) MailPath(user string) string { //TODO: temporary here. Later should be part of mailscanner and never accessed from here
+	return a.mailMaps[user]
+}
+
+func readMailMaps() map[string]string { //TODO: temporary here. Later should be part of mailscanner and never accessed from here
+	mailMaps := make(map[string]string)
+	mapsFile := config.ConfigInstance().VMailboxMaps
+	if !utils.FileExists(mapsFile) {
+		return mailMaps
+	}
+
+	file, err := os.Open(mapsFile)
+	if err != nil {
+		log.Fatalf("Unable to open virtual mailbox maps %s\n", mapsFile)
+	}
+
+	scanner := bufio.NewScanner(file)
+
+	for scanner.Scan() {
+		mailPathPair := strings.Split(scanner.Text(), " ")
+		if len(mailPathPair) != 2 {
+			log.Printf("Invalid record in virtual mailbox maps %s", scanner.Text())
+			continue
+		}
+		mailMaps[mailPathPair[0]] = mailPathPair[1]
+	}
 
-	return false
+	return mailMaps
 }

+ 11 - 1
build.sh

@@ -10,6 +10,16 @@ go install ./src/github.com/golang/protobuf/protoc-gen-go
 rm -f $RPC_PATH/*.pb.go
 protoc -I$RPC_PATH --go_out=plugins=grpc:$RPC_PATH $RPC_PATH/gostfix.proto
 
+echo "Installing data"
+rm -rf data
+mkdir data
+cp -a main.ini data/
+cp -a main.cf data/
+cp -a vmailbox data/
+cp -a web/assets data/
+cp -a web/css data/
+cp -a web/js data/
+cp -a web/templates data/
+
 go get -v
 go build -o $GOBIN/gostfix
-

+ 201 - 207
web/server.go

@@ -29,12 +29,11 @@ import (
 	"bufio"
 	"fmt"
 	template "html/template"
-	ioutil "io/ioutil"
 	"log"
 	"net/http"
-	"os"
 	"strings"
 
+	auth "../auth"
 	common "../common"
 	config "../config"
 	utils "../utils"
@@ -55,6 +54,10 @@ const (
 	AllHeaderMask = 15
 )
 
+const (
+	CookieSessionToken = "gostfix_session"
+)
+
 func NewEmail() *common.Mail {
 	return &common.Mail{
 		Header: &common.MailHeader{},
@@ -65,18 +68,18 @@ func NewEmail() *common.Mail {
 }
 
 type Server struct {
-	mailMaps     map[string]string //TODO: Temporary
-	fileServer   http.Handler
-	templater    *Templater
-	sessionStore *sessions.CookieStore
+	authenticator *auth.Authenticator
+	fileServer    http.Handler
+	templater     *Templater
+	sessionStore  *sessions.CookieStore
 }
 
 func NewServer() *Server {
 	return &Server{
-		mailMaps:     readMailMaps(), //TODO: Temporary
-		templater:    NewTemplater("./data/templates"),
-		fileServer:   http.FileServer(http.Dir("./data")),
-		sessionStore: sessions.NewCookieStore(make([]byte, 32)),
+		authenticator: auth.NewAuthenticator(),
+		templater:     NewTemplater("./data/templates"),
+		fileServer:    http.FileServer(http.Dir("./data")),
+		sessionStore:  sessions.NewCookieStore(make([]byte, 32)),
 	}
 }
 
@@ -91,233 +94,224 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		utils.StartsWith(r.URL.Path, "/assets/") ||
 		utils.StartsWith(r.URL.Path, "/js/") {
 		s.fileServer.ServeHTTP(w, r)
-	} else if r.URL.Path == "/login" {
-		session, _ := s.sessionStore.Get(r, "token")
-		user, ok := session.Values["user"].(string)
-		if !ok || user == "" {
-			user = ""
-			if err := r.ParseForm(); err == nil {
-				user = r.FormValue("user")
-			}
+	} else {
+		switch r.URL.Path {
+		case "/login":
+			s.handleLogin(w, r)
+		case "/logout":
+			s.handleLogout(w, r)
+		case "/messageDetails":
+			s.handleMessageDetails(w, r)
+		default:
+			s.handleMailbox(w, r)
 		}
+	}
+}
 
-		_, ok = s.mailMaps[user]
-		if utils.RegExpUtilsInstance().EmailChecker.MatchString(user) &&
-			user != "" && ok {
-			session.Values["user"] = user
-			session.Save(r, w)
-			http.Redirect(w, r, "/mailbox", http.StatusTemporaryRedirect)
+func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
+	if err := r.ParseForm(); err == nil {
+		user := r.FormValue("user")
+		password := r.FormValue("password")
+		token, ok := s.authenticator.Authenticate(user, password)
+		if ok {
+			s.Login(user, token, w, r)
 			return
 		}
+	}
 
-		session.Values["user"] = ""
-		session.Save(r, w)
-
-		fmt.Printf("Actual user: %s, Actual map: %v\n", user, s.mailMaps)
-		data, _ := ioutil.ReadFile("./data/templates/login.html")
-		w.Write(data)
-	} else if r.URL.Path == "/logout" {
-		fmt.Printf("logout")
-		session, _ := s.sessionStore.Get(r, "token")
-		session.Values["user"] = ""
-		session.Save(r, w)
-		fmt.Printf("Reset user session: %s\n", session.Values["user"])
-		http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
-	} else if r.URL.Path == "/messageDetails" {
-		fmt.Fprint(w, s.templater.ExecuteDetails(""))
-	} else {
-		session, _ := s.sessionStore.Get(r, "token")
-		user, ok := session.Values["user"].(string)
+	if s.authenticator.Verify(s.extractAuth(w, r)) {
+		http.Redirect(w, r, "/mailbox", http.StatusTemporaryRedirect)
+		return
+	}
 
-		fmt.Printf("User session: %s\n", user)
+	s.Logout(w, r)
+	fmt.Fprint(w, s.templater.ExecuteLogin(&LoginTemplateData{
+		common.Version,
+	}))
+}
 
-		if !ok || utils.RegExpUtilsInstance().EmailChecker.FindString(user) != user || user == "" {
-			fmt.Print("Invalid user")
-			http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
-			return
-		}
+func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) {
+	s.Logout(w, r)
+	http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
+}
 
-		mailRelPath, ok := s.mailMaps[user]
-		if !ok {
-			fmt.Print("Invalid user")
-			http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
-			return
-		}
+func (s *Server) handleMessageDetails(w http.ResponseWriter, r *http.Request) {
+	//TODO: Not implemented yet. Need database mail storage implemented first
+	fmt.Fprint(w, s.templater.ExecuteDetails(""))
+}
 
-		mailPath := config.ConfigInstance().VMailboxBase + "/" + mailRelPath
-		if !utils.FileExists(mailPath) {
-			fmt.Printf("logout")
-			session, _ := s.sessionStore.Get(r, "token")
-			session.Values["user"] = ""
-			session.Save(r, w)
-			fmt.Printf("Reset user session: %s\n", session.Values["user"])
-
-			w.WriteHeader(http.StatusInternalServerError)
-			fmt.Fprint(w, s.templater.ExecuteError(&Error{
-				Code:   http.StatusInternalServerError,
-				String: "Unable to access your mailbox. Please contact Administrator.",
-			}))
-			return
-		}
+func (s *Server) handleMailbox(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)
+	}
 
-		file, err := utils.OpenAndLockWait(mailPath)
-		if err != nil {
-			fmt.Printf("logout")
-			session, _ := s.sessionStore.Get(r, "token")
-			session.Values["user"] = ""
-			session.Save(r, w)
-			fmt.Printf("Reset user session: %s\n", session.Values["user"])
-
-			w.WriteHeader(http.StatusInternalServerError)
-			fmt.Fprint(w, s.templater.ExecuteError(&Error{
-				Code:   http.StatusInternalServerError,
-				String: "Unable to access your mailbox. Please contact Administrator.",
-			}))
-			return
-		}
+	mailPath := config.ConfigInstance().VMailboxBase + "/" + s.authenticator.MailPath(user)
+	if !utils.FileExists(mailPath) {
+		s.Logout(w, r)
+		s.Error(http.StatusInternalServerError, "Unable to access your mailbox. Please contact Administrator.", w, r)
+		return
+	}
 
-		defer file.CloseAndUnlock()
-
-		scanner := bufio.NewScanner(file)
-		activeBoundary := ""
-		var previousHeader *string = nil
-		var emails []*common.Mail
-		mandatoryHeaders := 0
-		email := NewEmail()
-		state := StateHeaderScan
-		for scanner.Scan() {
-			if scanner.Text() == "" {
-				if state == StateHeaderScan && mandatoryHeaders&AtLeastOneHeaderMask == AtLeastOneHeaderMask {
-					boundaryCapture := utils.RegExpUtilsInstance().BoundaryFinder.FindStringSubmatch(email.Body.ContentType)
-					if len(boundaryCapture) == 2 {
-						activeBoundary = boundaryCapture[1]
-					} else {
-						activeBoundary = ""
-					}
-					state = StateBodyScan
-					// fmt.Printf("--------------------------Start body scan content type:%s boundary: %s -------------------------\n", email.Body.ContentType, activeBoundary)
-				} else if state == StateBodyScan {
-					// fmt.Printf("--------------------------Previous email-------------------------\n%v\n", email)
-					if activeBoundary == "" {
-						previousHeader = nil
-						activeBoundary = ""
-						// fmt.Printf("Actual headers: %d\n", mandatoryHeaders)
-						if mandatoryHeaders == AllHeaderMask {
-							emails = append(emails, email)
-						}
-						email = NewEmail()
-						state = StateHeaderScan
-						mandatoryHeaders = 0
-					} else {
-						// fmt.Printf("Still in body scan\n")
-						continue
-					}
+	file, err := utils.OpenAndLockWait(mailPath)
+	if err != nil {
+		s.Logout(w, r)
+		s.Error(http.StatusInternalServerError, "Unable to access your mailbox. Please contact Administrator.", w, r)
+		return
+	}
+	defer file.CloseAndUnlock()
+
+	scanner := bufio.NewScanner(file)
+	activeBoundary := ""
+	var previousHeader *string = nil
+	var emails []*common.Mail
+	mandatoryHeaders := 0
+	email := NewEmail()
+	state := StateHeaderScan
+	for scanner.Scan() {
+		if scanner.Text() == "" {
+			if state == StateHeaderScan && mandatoryHeaders&AtLeastOneHeaderMask == AtLeastOneHeaderMask {
+				boundaryCapture := utils.RegExpUtilsInstance().BoundaryFinder.FindStringSubmatch(email.Body.ContentType)
+				if len(boundaryCapture) == 2 {
+					activeBoundary = boundaryCapture[1]
 				} else {
-					// fmt.Printf("Empty line in state %d\n", state)
+					activeBoundary = ""
 				}
-			}
-
-			if state == StateHeaderScan {
-				capture := utils.RegExpUtilsInstance().HeaderFinder.FindStringSubmatch(scanner.Text())
-				if len(capture) == 3 {
-					// fmt.Printf("capture Header %s : %s\n", strings.ToLower(capture[0]), strings.ToLower(capture[1]))
-					header := strings.ToLower(capture[1])
-					mandatoryHeaders |= AtLeastOneHeaderMask
-					switch header {
-					case "from":
-						previousHeader = &email.Header.From
-						mandatoryHeaders |= FromHeaderMask
-					case "to":
-						previousHeader = &email.Header.To
-						mandatoryHeaders |= ToHeaderMask
-					case "cc":
-						previousHeader = &email.Header.Cc
-					case "bcc":
-						previousHeader = &email.Header.Bcc
-						mandatoryHeaders |= ToHeaderMask
-					case "subject":
-						previousHeader = &email.Header.Subject
-					case "date":
-						previousHeader = &email.Header.Date
-						mandatoryHeaders |= DateHeaderMask
-					case "content-type":
-						previousHeader = &email.Body.ContentType
-					default:
-						previousHeader = nil
-					}
-					if previousHeader != nil {
-						*previousHeader += capture[2]
+				state = StateBodyScan
+				// fmt.Printf("--------------------------Start body scan content type:%s boundary: %s -------------------------\n", email.Body.ContentType, activeBoundary)
+			} else if state == StateBodyScan {
+				// fmt.Printf("--------------------------Previous email-------------------------\n%v\n", email)
+				if activeBoundary == "" {
+					previousHeader = nil
+					activeBoundary = ""
+					// fmt.Printf("Actual headers: %d\n", mandatoryHeaders)
+					if mandatoryHeaders == AllHeaderMask {
+						emails = append(emails, email)
 					}
+					email = NewEmail()
+					state = StateHeaderScan
+					mandatoryHeaders = 0
+				} else {
+					// fmt.Printf("Still in body scan\n")
 					continue
 				}
+			} else {
+				// fmt.Printf("Empty line in state %d\n", state)
+			}
+		}
 
-				capture = utils.RegExpUtilsInstance().FoldingFinder.FindStringSubmatch(scanner.Text())
-				if len(capture) == 2 && previousHeader != nil {
-					*previousHeader += capture[1]
-					continue
+		if state == StateHeaderScan {
+			capture := utils.RegExpUtilsInstance().HeaderFinder.FindStringSubmatch(scanner.Text())
+			if len(capture) == 3 {
+				// fmt.Printf("capture Header %s : %s\n", strings.ToLower(capture[0]), strings.ToLower(capture[1]))
+				header := strings.ToLower(capture[1])
+				mandatoryHeaders |= AtLeastOneHeaderMask
+				switch header {
+				case "from":
+					previousHeader = &email.Header.From
+					mandatoryHeaders |= FromHeaderMask
+				case "to":
+					previousHeader = &email.Header.To
+					mandatoryHeaders |= ToHeaderMask
+				case "cc":
+					previousHeader = &email.Header.Cc
+				case "bcc":
+					previousHeader = &email.Header.Bcc
+					mandatoryHeaders |= ToHeaderMask
+				case "subject":
+					previousHeader = &email.Header.Subject
+				case "date":
+					previousHeader = &email.Header.Date
+					mandatoryHeaders |= DateHeaderMask
+				case "content-type":
+					previousHeader = &email.Body.ContentType
+				default:
+					previousHeader = nil
 				}
-			} else {
-				// email.Body.Content += scanner.Text() + "\n"
-				if activeBoundary != "" {
-					capture := utils.RegExpUtilsInstance().BoundaryEndFinder.FindStringSubmatch(scanner.Text())
-					if len(capture) == 2 {
-						// fmt.Printf("capture Boundary End %s\n", capture[1])
-						if activeBoundary == capture[1] {
-							state = StateBodyScan
-							activeBoundary = ""
-						}
-
-						continue
-					}
-					// capture = boundaryStartFinder.FindStringSubmatch(scanner.Text())
-					// if len(capture) == 2 && activeBoundary == capture[1] {
-					// 	// fmt.Printf("capture Boundary Start %s\n", capture[1])
-					// 	state = StateContentScan
-					// 	continue
-					// }
+				if previousHeader != nil {
+					*previousHeader += capture[2]
 				}
+				continue
 			}
-		}
 
-		if state == StateBodyScan && mandatoryHeaders == AllHeaderMask { //Finalize if body read till EOF
-			// fmt.Printf("--------------------------Previous email-------------------------\n%v\n", email)
+			capture = utils.RegExpUtilsInstance().FoldingFinder.FindStringSubmatch(scanner.Text())
+			if len(capture) == 2 && previousHeader != nil {
+				*previousHeader += capture[1]
+				continue
+			}
+		} else {
+			// email.Body.Content += scanner.Text() + "\n"
+			if activeBoundary != "" {
+				capture := utils.RegExpUtilsInstance().BoundaryEndFinder.FindStringSubmatch(scanner.Text())
+				if len(capture) == 2 {
+					// fmt.Printf("capture Boundary End %s\n", capture[1])
+					if activeBoundary == capture[1] {
+						state = StateBodyScan
+						activeBoundary = ""
+					}
 
-			previousHeader = nil
-			activeBoundary = ""
-			emails = append(emails, email)
-			state = StateHeaderScan
+					continue
+				}
+				// capture = boundaryStartFinder.FindStringSubmatch(scanner.Text())
+				// if len(capture) == 2 && activeBoundary == capture[1] {
+				// 	// fmt.Printf("capture Boundary Start %s\n", capture[1])
+				// 	state = StateContentScan
+				// 	continue
+				// }
+			}
 		}
+	}
+
+	if state == StateBodyScan && mandatoryHeaders == AllHeaderMask { //Finalize if body read till EOF
+		// fmt.Printf("--------------------------Previous email-------------------------\n%v\n", email)
 
-		fmt.Fprint(w, s.templater.ExecuteIndex(&Index{
-			MailList: template.HTML(s.templater.ExecuteMailList(emails)),
-			Folders:  "Folders",
-			Version:  common.Version,
-		}))
+		previousHeader = nil
+		activeBoundary = ""
+		emails = append(emails, email)
+		state = StateHeaderScan
 	}
+
+	fmt.Fprint(w, s.templater.ExecuteIndex(&IndexTemplateData{
+		MailList: template.HTML(s.templater.ExecuteMailList(emails)),
+		Folders:  "Folders",
+		Version:  common.Version,
+	}))
 }
 
-func readMailMaps() map[string]string { //TODO: Temporary
-	mailMaps := make(map[string]string)
-	mapsFile := config.ConfigInstance().VMailboxMaps
-	if !utils.FileExists(mapsFile) {
-		return mailMaps
-	}
+func (s *Server) Logout(w http.ResponseWriter, r *http.Request) {
+	fmt.Println("logout")
 
-	file, err := os.Open(mapsFile)
-	if err != nil {
-		log.Fatalf("Unable to open virtual mailbox maps %s\n", mapsFile)
-	}
+	session, _ := s.sessionStore.Get(r, CookieSessionToken)
+	session.Values["user"] = ""
+	session.Values["token"] = ""
+	session.Save(r, w)
+}
 
-	scanner := bufio.NewScanner(file)
+func (s *Server) Login(user, token string, w http.ResponseWriter, r *http.Request) {
+	session, _ := s.sessionStore.Get(r, CookieSessionToken)
+	session.Values["user"] = user
+	session.Values["token"] = token
+	session.Save(r, w)
+	http.Redirect(w, r, "/mailbox", http.StatusTemporaryRedirect)
+}
 
-	for scanner.Scan() {
-		mailPathPair := strings.Split(scanner.Text(), " ")
-		if len(mailPathPair) != 2 {
-			log.Printf("Invalid record in virtual mailbox maps %s", scanner.Text())
-			continue
-		}
-		mailMaps[mailPathPair[0]] = mailPathPair[1]
+func (s *Server) Error(code int, text string, w http.ResponseWriter, r *http.Request) {
+	w.WriteHeader(http.StatusInternalServerError)
+	fmt.Fprint(w, s.templater.ExecuteError(&ErrorTemplateData{
+		Code: code,
+		Text: "Unable to access your mailbox. Please contact Administrator.",
+	}))
+}
+
+func (s *Server) extractAuth(w http.ResponseWriter, r *http.Request) (user, token string) {
+	session, err := s.sessionStore.Get(r, CookieSessionToken)
+	if err != nil {
+		log.Printf("Unable to read user session %s\n", err)
+		return
 	}
+	user, _ = session.Values["user"].(string)
+	token, _ = session.Values["token"].(string)
 
-	return mailMaps
+	return
 }

+ 27 - 11
web/templater.go

@@ -37,6 +37,7 @@ const (
 	MailListTemplateName = "maillist.html"
 	DetailsTemplateName  = "details.html"
 	ErrorTemplateName    = "error.html"
+	LoginTemplateName    = "login.html"
 )
 
 type Templater struct {
@@ -44,17 +45,22 @@ type Templater struct {
 	mailListTemplate *template.Template
 	detailsTemplate  *template.Template
 	errorTemplate    *template.Template
+	loginTemplate    *template.Template
 }
 
-type Index struct {
+type IndexTemplateData struct {
 	Folders  template.HTML
 	MailList template.HTML
 	Version  template.HTML
 }
 
-type Error struct {
+type ErrorTemplateData struct {
 	Code    int
-	String  string
+	Text    string
+	Version string
+}
+
+type LoginTemplateData struct {
 	Version string
 }
 
@@ -80,11 +86,17 @@ func NewTemplater(templatesPath string) (t *Templater) {
 		log.Fatal(err)
 	}
 
+	login, err := parseTemplate(templatesPath + "/" + LoginTemplateName)
+	if err != nil {
+		log.Fatal(err)
+	}
+
 	t = &Templater{
 		indexTemplate:    index,
 		mailListTemplate: maillist,
 		detailsTemplate:  details,
 		errorTemplate:    errors,
+		loginTemplate:    login,
 	}
 	return
 }
@@ -98,20 +110,24 @@ func parseTemplate(path string) (*template.Template, error) {
 	return template.New("Index").Parse(string(content))
 }
 
-func (t *Templater) ExecuteIndex(content interface{}) string {
-	return executeTemplateCommon(t.indexTemplate, content)
+func (t *Templater) ExecuteIndex(data interface{}) string {
+	return executeTemplateCommon(t.indexTemplate, data)
+}
+
+func (t *Templater) ExecuteMailList(data interface{}) string {
+	return executeTemplateCommon(t.mailListTemplate, data)
 }
 
-func (t *Templater) ExecuteMailList(mailList interface{}) string {
-	return executeTemplateCommon(t.mailListTemplate, mailList)
+func (t *Templater) ExecuteDetails(data interface{}) string {
+	return executeTemplateCommon(t.detailsTemplate, data)
 }
 
-func (t *Templater) ExecuteDetails(details interface{}) string {
-	return executeTemplateCommon(t.detailsTemplate, details)
+func (t *Templater) ExecuteError(data interface{}) string {
+	return executeTemplateCommon(t.errorTemplate, data)
 }
 
-func (t *Templater) ExecuteError(err interface{}) string {
-	return executeTemplateCommon(t.errorTemplate, err)
+func (t *Templater) ExecuteLogin(data interface{}) string {
+	return executeTemplateCommon(t.loginTemplate, data)
 }
 
 func executeTemplateCommon(t *template.Template, values interface{}) string {

+ 1 - 1
web/templates/error.html

@@ -14,7 +14,7 @@
             <img src="assets/error.svg" height="100pt"/>
             <div style="display: block;">
                 <span style="font-size: 24pt;">{{.Code}}</span></br>
-                <span style="font-size: 14pt;">{{.String}}</span>
+                <span style="font-size: 14pt;">{{.Text}}</span>
             </div>
         </div>
         <p class="copyrights">gostfix {{.Version}} Web interface. Copyright (c) 2020 Alexey Edelev &lt;semlanik@gmail.com&gt;</p>