Explorar o código

Fix mail parser

- Add check for requireld mail headers found while parsing
  before add email
- Improve look and fill
Alexey Edelev %!s(int64=5) %!d(string=hai) anos
pai
achega
f8e27d77c1
Modificáronse 6 ficheiros con 265 adicións e 148 borrados
  1. 57 0
      css/index.css
  2. 47 13
      css/styles.css
  3. 143 118
      main.go
  4. 1 0
      templater.go
  5. 14 14
      templates/index.html
  6. 3 3
      templates/maillist.html

+ 57 - 0
css/index.css

@@ -0,0 +1,57 @@
+html, body {
+    height: 100%;
+    margin: 0px;
+}
+
+table {
+    border-collapse: collapse;
+    border-spacing: 0;
+}
+
+.contentBox {
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+
+    margin-left: 150pt!important;
+    margin-top: 5pt!important;
+    margin-right: 5pt!important;
+    margin-bottom: 30pt!important;
+
+    overflow-y: auto;
+    border-radius: 2pt;
+}
+
+.emailDetails {
+    background-color: white;
+}
+
+.foldersBox {
+    position: absolute;
+    top :0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+
+    margin-left: 5pt!important;
+    margin-top: 5pt!important;
+    margin-right: 5pt!important;
+    margin-bottom: 30pt!important;
+
+    overflow-y: auto;
+    width: 150pt;
+}
+
+.copyrights {
+    position: absolute;
+    right: 0;
+    bottom: 0;
+    margin-right: 5pt!important;
+    margin-bottom: 5pt!important;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    max-width: 80%;
+}

+ 47 - 13
css/styles.css

@@ -6,15 +6,9 @@ body {
     background-color: white;
 }
 
-table {
-    border-collapse: collapse;
-    border-spacing: 0;
-}
-
 table.mailList {
     width: 100%;
     table-layout: fixed;
-    border-radius: 2pt;
     overflow: hidden;
 }
 
@@ -27,15 +21,46 @@ table.mailList {
 }
 
 .mailList tr {
-    background-color: aliceblue;
-    border-bottom: 1px solid rgb(169, 198, 223);
+    background-color: #ffffff;
+    border-bottom: 1px solid #41cd52;
     cursor: pointer;
+    transition: background-color .3s;
 }
 
 .mailList tr:hover {
-    background-color: rgb(229, 239, 248);
+    background-color: #ebffee;
 }
 
+
+/* .mailList tr:before {
+    content: "";
+
+    position: absolute;
+    top: 50%;
+    left: 50%;
+
+    display: block;
+    width: 0;
+    padding-top: 0;
+
+    border-radius: 100%;
+
+    background-color: rgba(236, 240, 241, .3);
+
+    -webkit-transform: translate(-50%, -50%);
+    -moz-transform: translate(-50%, -50%);
+    -ms-transform: translate(-50%, -50%);
+    -o-transform: translate(-50%, -50%);
+    transform: translate(-50%, -50%);
+}
+
+.mailList tr:active:before {
+    width: 120%;
+    padding-top: 120%;
+
+    transition: width .2s ease-out, padding-top .2s ease-out;
+} */
+
 td.fromCol {
     width: 300pt;
     font-weight: bold;
@@ -61,7 +86,6 @@ td.dateCol {
     position: relative;
 
     display: block;
-    margin: 10pt auto;
     padding:8pt;
 
     overflow: hidden;
@@ -71,7 +95,7 @@ td.dateCol {
     border-radius: 2pt;
     box-shadow: 0 1pt 4pt rgba(0, 0, 0, .6);
 
-    background-color: #2ecc71;
+    background-color: #41cd52;
     color: #ecf0f1;
 
     font-size: 12pt;
@@ -80,7 +104,7 @@ td.dateCol {
 }
 
 .btn:hover, .btn:focus {
-    background-color: #27ae60;
+    background-color: #3ab849;
 }
 
 .btn > * {
@@ -139,10 +163,20 @@ td.dateCol {
 
 .materialLevel1 {
     box-shadow: 0 1pt 3pt rgba(0, 0, 0, .6);
-
+    margin-top: 1pt;
+    margin-bottom: 1pt;
+    margin-left: 4pt;
+    padding-right: 4pt;
 }
 
 .materialLevel2 {
     box-shadow: 0 1pt 4pt rgba(0, 0, 0, .6);
+    margin-top: 1pt;
+    margin-bottom: 1pt;
+    margin-left: 5pt;
+    padding-right: 5pt;
+}
 
+.copyrights {
+    color: #b8d4bc
 }

+ 143 - 118
main.go

@@ -44,6 +44,14 @@ const (
 	StateContentScan
 )
 
+const (
+	AtLeastOneHeaderMask = 1 << iota
+	FromHeaderMask
+	DateHeaderMask
+	ToHeaderMask
+	AllHeaderMask = 15
+)
+
 const (
 	HeaderRegExp        = "^([\x21-\x7E^:]+):(.*)"
 	FoldingRegExp       = "^\\s+(.*)"
@@ -53,6 +61,10 @@ const (
 	UserRegExp          = "^[a-zA-Z][\\w0-9\\._]*"
 )
 
+const (
+	Version = "0.1.0 alpha"
+)
+
 // type Email struct {
 // 	From        string
 // 	To          string
@@ -106,149 +118,162 @@ func (e *GofixEngine) Run() {
 
 func (e *GofixEngine) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	fmt.Println(r.URL.Path)
-	switch r.URL.Path {
-	case "/css/styles.css":
+	if strings.Index(r.URL.Path, "/css/") == 0 {
 		e.fileServer.ServeHTTP(w, r)
-	default:
-		{
-			user := r.URL.Query().Get("user")
-
-			if e.userChecker.FindString(user) != user || user == "" {
-				fmt.Print("Invalid user")
-				w.WriteHeader(http.StatusUnauthorized)
-				fmt.Fprint(w, "401 - Access denied")
-				return
-			}
-
-			state := StateHeaderScan
-			headerFinder, err := regexp.Compile(HeaderRegExp)
-			if err != nil {
-				log.Fatalf("Invalid regexp %s\n", err)
-			}
+	} else {
+		user := r.URL.Query().Get("user")
+
+		if e.userChecker.FindString(user) != user || user == "" {
+			fmt.Print("Invalid user")
+			w.WriteHeader(http.StatusUnauthorized)
+			fmt.Fprint(w, "401 - Access denied")
+			return
+		}
 
-			foldingFinder, err := regexp.Compile(FoldingRegExp)
-			if err != nil {
-				log.Fatalf("Invalid regexp %s\n", err)
-			}
+		state := StateHeaderScan
+		headerFinder, err := regexp.Compile(HeaderRegExp)
+		if err != nil {
+			log.Fatalf("Invalid regexp %s\n", err)
+		}
 
-			boundaryStartFinder, err := regexp.Compile(BoundaryStartRegExp)
-			if err != nil {
-				log.Fatalf("Invalid regexp %s\n", err)
-			}
+		foldingFinder, err := regexp.Compile(FoldingRegExp)
+		if err != nil {
+			log.Fatalf("Invalid regexp %s\n", err)
+		}
 
-			boundaryEndFinder, err := regexp.Compile(BoundaryEndRegExp)
-			if err != nil {
-				log.Fatalf("Invalid regexp %s\n", err)
-			}
+		// boundaryStartFinder, err := regexp.Compile(BoundaryStartRegExp)
+		// if err != nil {
+		// 	log.Fatalf("Invalid regexp %s\n", err)
+		// }
 
-			boundaryFinder, err := regexp.Compile(BoundaryRegExp)
+		boundaryEndFinder, err := regexp.Compile(BoundaryEndRegExp)
+		if err != nil {
+			log.Fatalf("Invalid regexp %s\n", err)
+		}
 
-			if !fileExists(e.mailPath + "/" + r.URL.Query().Get("user")) {
-				w.WriteHeader(http.StatusForbidden)
-				fmt.Fprint(w, "403 Unknown user")
-				return
-			}
+		boundaryFinder, err := regexp.Compile(BoundaryRegExp)
 
-			file, _ := os.Open(e.mailPath + "/" + r.URL.Query().Get("user"))
-			scanner := bufio.NewScanner(file)
-			activeBoundary := ""
-			var previousHeader *string = nil
-			var emails []*Mail
-			email := NewEmail()
-			for scanner.Scan() {
-				if scanner.Text() == "" {
-					if state == StateHeaderScan {
-						boundaryCapture := 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 !fileExists(e.mailPath + "/" + r.URL.Query().Get("user")) {
+			w.WriteHeader(http.StatusForbidden)
+			fmt.Fprint(w, "403 Unknown user")
+			return
+		}
 
+		file, _ := os.Open(e.mailPath + "/" + r.URL.Query().Get("user"))
+		scanner := bufio.NewScanner(file)
+		activeBoundary := ""
+		var previousHeader *string = nil
+		var emails []*Mail
+		mandatoryHeaders := 0
+		email := NewEmail()
+		for scanner.Scan() {
+			if scanner.Text() == "" {
+				if state == StateHeaderScan && mandatoryHeaders&AtLeastOneHeaderMask == AtLeastOneHeaderMask {
+					boundaryCapture := 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 = ""
-						emails = append(emails, email)
+						fmt.Printf("Actual headers: %d\n", mandatoryHeaders)
+						if mandatoryHeaders == AllHeaderMask {
+							emails = append(emails, email)
+						}
 						email = NewEmail()
 						state = StateHeaderScan
+						mandatoryHeaders = 0
 					} else {
-						// fmt.Printf("Empty line in state %d\n", state)
+						fmt.Printf("Still in body scan\n")
+						continue
 					}
+				} else {
+					fmt.Printf("Empty line in state %d\n", state)
 				}
+			}
 
-				if state == StateHeaderScan {
-					capture := 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])
-						switch header {
-						case "from":
-							previousHeader = &email.Header.From
-						case "to":
-							previousHeader = &email.Header.To
-						case "cc":
-							previousHeader = &email.Header.Cc
-						case "bcc":
-							previousHeader = &email.Header.Bcc
-						case "subject":
-							previousHeader = &email.Header.Subject
-						case "date":
-							previousHeader = &email.Header.Date
-						case "content-type":
-							previousHeader = &email.Body.ContentType
-						default:
-							previousHeader = nil
-						}
-						if previousHeader != nil {
-							*previousHeader += capture[2]
-						}
-						continue
+			if state == StateHeaderScan {
+				capture := 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
 					}
-
-					capture = foldingFinder.FindStringSubmatch(scanner.Text())
-					if len(capture) == 2 && previousHeader != nil {
-						*previousHeader += capture[1]
-						continue
+					if previousHeader != nil {
+						*previousHeader += capture[2]
 					}
-				} else {
-					// email.Body.Content += scanner.Text() + "\n"
-					if activeBoundary != "" {
-						capture := boundaryEndFinder.FindStringSubmatch(scanner.Text())
-						if len(capture) == 2 {
-							// fmt.Printf("capture Boundary End %s\n", capture[1])
-							if activeBoundary == capture[1] {
-								state = StateBodyScan
-							}
-
-							continue
-						}
-						capture = boundaryStartFinder.FindStringSubmatch(scanner.Text())
-						if len(capture) == 2 {
-							// fmt.Printf("capture Boundary Start %s\n", capture[1])
-							state = StateContentScan
-							continue
+					continue
+				}
+
+				capture = foldingFinder.FindStringSubmatch(scanner.Text())
+				if len(capture) == 2 && previousHeader != nil {
+					*previousHeader += capture[1]
+					continue
+				}
+			} else {
+				// email.Body.Content += scanner.Text() + "\n"
+				if activeBoundary != "" {
+					capture := 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 state == StateBodyScan { //Finalize if body read till EOF
-				// fmt.Printf("--------------------------Previous email-------------------------\n%v\n", email)
+		if state == StateBodyScan && mandatoryHeaders == AllHeaderMask { //Finalize if body read till EOF
+			// fmt.Printf("--------------------------Previous email-------------------------\n%v\n", email)
 
-				previousHeader = nil
-				activeBoundary = ""
-				emails = append(emails, email)
-				state = StateHeaderScan
-			}
-
-			fmt.Fprint(w, e.templater.ExecuteIndex(&Index{
-				MailList: template.HTML(e.templater.ExecuteMailList(emails)),
-				Folders:  "Folders",
-			}))
+			previousHeader = nil
+			activeBoundary = ""
+			emails = append(emails, email)
+			state = StateHeaderScan
 		}
+
+		fmt.Fprint(w, e.templater.ExecuteIndex(&Index{
+			MailList: template.HTML(e.templater.ExecuteMailList(emails)),
+			Folders:  "Folders",
+			Version:  Version,
+		}))
 	}
 }
 

+ 1 - 0
templater.go

@@ -45,6 +45,7 @@ type Templater struct {
 type Index struct {
 	Folders  template.HTML
 	MailList template.HTML
+	Version  template.HTML
 }
 
 func NewTemplater(templatesPath string) (t *Templater) {

+ 14 - 14
templates/index.html

@@ -3,23 +3,23 @@
     <head>
         <meta charset="utf-8"/>
         <link href="https://fonts.googleapis.com/css?family=Titillium+Web&display=swap" rel="stylesheet">
+        <link type="text/css" href="css/index.css" rel="stylesheet">
         <link type="text/css" href="css/styles.css" rel="stylesheet">
     </head>
     <body>
-        <table style="width: 100%;">
-            <tr>
-                <td style="width: 150pt; vertical-align:top">
-                    <div style="display: inline-block; overflow-y: auto; padding: 10pt; ">
-                        <button class="btn orange materialLevel1">New email</button>
-                        {{.Folders}}
-                    </div>
-                </td>
-                <td>
-                    <div style="display: inline-block; overflow-y: auto; padding: 10pt;">
-                        {{.MailList}}
-                    </div>
-                </td>
-            </tr>
+        <div>
+            <div class="foldersBox">
+                <button class="btn materialLevel1" style="width: 130pt">New email</button>
+                {{.Folders}}
+            </div>
+            <div class="materialLevel1 contentBox">
+                {{.MailList}}
+            </div>
+            <div id="details" class="materialLevel2 contentBox emailDetails" style="display: none;">
+                    <button class="btn materialLevel1" onclick="document.getElementById('details').style.display='none';">Back</button><br/>
+                    test email
+                </div>
+            <p class="copyrights">gostfix {{.Version}} Web interface. Copyright (c) 2020 Alexey Edelev &lt;semlanik@gmail.com&gt;</p>
         </div>
     </body>
 </html>

+ 3 - 3
templates/maillist.html

@@ -1,8 +1,8 @@
-<table class="mailList materialLevel1">
+<table class="mailList">
     <tbody>
         {{range .}}
-        <tr onclick="window.location='#';">
-            <td class="fromCol">{{.Header.From}}</a></td>
+        <tr onclick="document.getElementById('details').style.display='block';">
+            <td class="fromCol">{{.Header.From}}</td>
             <td class="subjCol">{{.Header.Subject}}</td>
             <td class="dateCol">{{.Header.Date}}</td>
         </tr>