Browse Source

Implement basic project structure

- Create scanner, web, bd, common, config packages
- Move related parts to apropriate package
Alexey Edelev 4 years ago
parent
commit
4174b9f7ad
14 changed files with 819 additions and 339 deletions
  1. 1 1
      LICENSE
  2. 1 1
      build.sh
  3. 221 0
      common/gostfix.pb.go
  4. 2 2
      common/gostfix.proto
  5. 30 0
      common/version.go
  6. 4 0
      config/main.ini.default
  7. 26 0
      db/db.go
  8. 0 69
      mailscanner.go
  9. 9 265
      main.go
  10. 91 0
      scanner/mailscanner.go
  11. 86 0
      utils/fileutils.go
  12. 118 0
      utils/regexp.go
  13. 229 0
      web/server.go
  14. 1 1
      web/templater.go

+ 1 - 1
LICENSE

@@ -1,5 +1,5 @@
 MIT License
-Copyright (c) <year> <copyright holders>
+Copyright (c) 2020 Alexey Edelev <semlanik@gmail.com>
 
 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:
 

+ 1 - 1
build.sh

@@ -1,7 +1,7 @@
 export GOPATH=$PWD
 export PATH=$PATH:$PWD/bin
 export GOBIN=$PWD/bin
-export RPC_PATH=$PWD
+export RPC_PATH=$PWD/common
 
 go get github.com/golang/protobuf/protoc-gen-go
 go install ./src/github.com/golang/protobuf/protoc-gen-go

+ 221 - 0
common/gostfix.pb.go

@@ -0,0 +1,221 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// source: gostfix.proto
+
+package common
+
+import (
+	fmt "fmt"
+	proto "github.com/golang/protobuf/proto"
+	math "math"
+)
+
+// Reference imports to suppress errors if they are not otherwise used.
+var _ = proto.Marshal
+var _ = fmt.Errorf
+var _ = math.Inf
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the proto package it is being compiled against.
+// A compilation error at this line likely means your copy of the
+// proto package needs to be updated.
+const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
+
+type MailBody struct {
+	ContentType          string   `protobuf:"bytes,1,opt,name=contentType,proto3" json:"contentType,omitempty"`
+	Content              []byte   `protobuf:"bytes,2,opt,name=content,proto3" json:"content,omitempty"`
+	XXX_NoUnkeyedLiteral struct{} `json:"-"`
+	XXX_unrecognized     []byte   `json:"-"`
+	XXX_sizecache        int32    `json:"-"`
+}
+
+func (m *MailBody) Reset()         { *m = MailBody{} }
+func (m *MailBody) String() string { return proto.CompactTextString(m) }
+func (*MailBody) ProtoMessage()    {}
+func (*MailBody) Descriptor() ([]byte, []int) {
+	return fileDescriptor_0ab36b6dc6e1dcaa, []int{0}
+}
+
+func (m *MailBody) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_MailBody.Unmarshal(m, b)
+}
+func (m *MailBody) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_MailBody.Marshal(b, m, deterministic)
+}
+func (m *MailBody) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_MailBody.Merge(m, src)
+}
+func (m *MailBody) XXX_Size() int {
+	return xxx_messageInfo_MailBody.Size(m)
+}
+func (m *MailBody) XXX_DiscardUnknown() {
+	xxx_messageInfo_MailBody.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_MailBody proto.InternalMessageInfo
+
+func (m *MailBody) GetContentType() string {
+	if m != nil {
+		return m.ContentType
+	}
+	return ""
+}
+
+func (m *MailBody) GetContent() []byte {
+	if m != nil {
+		return m.Content
+	}
+	return nil
+}
+
+type MailHeader struct {
+	From                 string   `protobuf:"bytes,1,opt,name=from,proto3" json:"from,omitempty"`
+	To                   string   `protobuf:"bytes,2,opt,name=to,proto3" json:"to,omitempty"`
+	Cc                   string   `protobuf:"bytes,3,opt,name=cc,proto3" json:"cc,omitempty"`
+	Bcc                  string   `protobuf:"bytes,4,opt,name=bcc,proto3" json:"bcc,omitempty"`
+	Date                 string   `protobuf:"bytes,5,opt,name=date,proto3" json:"date,omitempty"`
+	Subject              string   `protobuf:"bytes,6,opt,name=subject,proto3" json:"subject,omitempty"`
+	XXX_NoUnkeyedLiteral struct{} `json:"-"`
+	XXX_unrecognized     []byte   `json:"-"`
+	XXX_sizecache        int32    `json:"-"`
+}
+
+func (m *MailHeader) Reset()         { *m = MailHeader{} }
+func (m *MailHeader) String() string { return proto.CompactTextString(m) }
+func (*MailHeader) ProtoMessage()    {}
+func (*MailHeader) Descriptor() ([]byte, []int) {
+	return fileDescriptor_0ab36b6dc6e1dcaa, []int{1}
+}
+
+func (m *MailHeader) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_MailHeader.Unmarshal(m, b)
+}
+func (m *MailHeader) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_MailHeader.Marshal(b, m, deterministic)
+}
+func (m *MailHeader) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_MailHeader.Merge(m, src)
+}
+func (m *MailHeader) XXX_Size() int {
+	return xxx_messageInfo_MailHeader.Size(m)
+}
+func (m *MailHeader) XXX_DiscardUnknown() {
+	xxx_messageInfo_MailHeader.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_MailHeader proto.InternalMessageInfo
+
+func (m *MailHeader) GetFrom() string {
+	if m != nil {
+		return m.From
+	}
+	return ""
+}
+
+func (m *MailHeader) GetTo() string {
+	if m != nil {
+		return m.To
+	}
+	return ""
+}
+
+func (m *MailHeader) GetCc() string {
+	if m != nil {
+		return m.Cc
+	}
+	return ""
+}
+
+func (m *MailHeader) GetBcc() string {
+	if m != nil {
+		return m.Bcc
+	}
+	return ""
+}
+
+func (m *MailHeader) GetDate() string {
+	if m != nil {
+		return m.Date
+	}
+	return ""
+}
+
+func (m *MailHeader) GetSubject() string {
+	if m != nil {
+		return m.Subject
+	}
+	return ""
+}
+
+type Mail struct {
+	Header               *MailHeader `protobuf:"bytes,1,opt,name=header,proto3" json:"header,omitempty"`
+	Body                 *MailBody   `protobuf:"bytes,2,opt,name=body,proto3" json:"body,omitempty"`
+	XXX_NoUnkeyedLiteral struct{}    `json:"-"`
+	XXX_unrecognized     []byte      `json:"-"`
+	XXX_sizecache        int32       `json:"-"`
+}
+
+func (m *Mail) Reset()         { *m = Mail{} }
+func (m *Mail) String() string { return proto.CompactTextString(m) }
+func (*Mail) ProtoMessage()    {}
+func (*Mail) Descriptor() ([]byte, []int) {
+	return fileDescriptor_0ab36b6dc6e1dcaa, []int{2}
+}
+
+func (m *Mail) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_Mail.Unmarshal(m, b)
+}
+func (m *Mail) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_Mail.Marshal(b, m, deterministic)
+}
+func (m *Mail) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_Mail.Merge(m, src)
+}
+func (m *Mail) XXX_Size() int {
+	return xxx_messageInfo_Mail.Size(m)
+}
+func (m *Mail) XXX_DiscardUnknown() {
+	xxx_messageInfo_Mail.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_Mail proto.InternalMessageInfo
+
+func (m *Mail) GetHeader() *MailHeader {
+	if m != nil {
+		return m.Header
+	}
+	return nil
+}
+
+func (m *Mail) GetBody() *MailBody {
+	if m != nil {
+		return m.Body
+	}
+	return nil
+}
+
+func init() {
+	proto.RegisterType((*MailBody)(nil), "common.MailBody")
+	proto.RegisterType((*MailHeader)(nil), "common.MailHeader")
+	proto.RegisterType((*Mail)(nil), "common.Mail")
+}
+
+func init() { proto.RegisterFile("gostfix.proto", fileDescriptor_0ab36b6dc6e1dcaa) }
+
+var fileDescriptor_0ab36b6dc6e1dcaa = []byte{
+	// 230 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x4c, 0x90, 0x31, 0x4f, 0xc3, 0x40,
+	0x0c, 0x85, 0x95, 0x34, 0x04, 0xea, 0x00, 0xaa, 0x3c, 0xdd, 0x18, 0x45, 0x0c, 0x15, 0x43, 0x86,
+	0xf2, 0x0f, 0x18, 0x10, 0x0b, 0xcb, 0x89, 0x81, 0x35, 0xf1, 0x5d, 0xa1, 0x88, 0xc4, 0x55, 0x6a,
+	0x24, 0xb2, 0xf1, 0xd3, 0x91, 0x9d, 0x54, 0x64, 0x7b, 0xef, 0x9d, 0xef, 0xdd, 0x77, 0x86, 0x9b,
+	0x77, 0x3e, 0xc9, 0xfe, 0xf0, 0x53, 0x1f, 0x07, 0x16, 0xc6, 0x9c, 0xb8, 0xeb, 0xb8, 0xaf, 0x9e,
+	0xe0, 0xea, 0xa5, 0x39, 0x7c, 0x3d, 0x72, 0x18, 0xb1, 0x84, 0x82, 0xb8, 0x97, 0xd8, 0xcb, 0xeb,
+	0x78, 0x8c, 0x2e, 0x29, 0x93, 0xed, 0xda, 0x2f, 0x23, 0x74, 0x70, 0x39, 0x5b, 0x97, 0x96, 0xc9,
+	0xf6, 0xda, 0x9f, 0x6d, 0xf5, 0x9b, 0x00, 0x68, 0xd1, 0x73, 0x6c, 0x42, 0x1c, 0x10, 0x21, 0xdb,
+	0x0f, 0xdc, 0xcd, 0x1d, 0xa6, 0xf1, 0x16, 0x52, 0x61, 0xbb, 0xb7, 0xf6, 0xa9, 0xb0, 0x7a, 0x22,
+	0xb7, 0x9a, 0x3c, 0x11, 0x6e, 0x60, 0xd5, 0x12, 0xb9, 0xcc, 0x02, 0x95, 0xda, 0x12, 0x1a, 0x89,
+	0xee, 0x62, 0x6a, 0x51, 0xad, 0x08, 0xa7, 0xef, 0xf6, 0x33, 0x92, 0xb8, 0xdc, 0xe2, 0xb3, 0xad,
+	0xde, 0x20, 0x53, 0x02, 0xbc, 0x87, 0xfc, 0xc3, 0x28, 0xec, 0xf5, 0x62, 0x87, 0xf5, 0xf4, 0xd7,
+	0xfa, 0x9f, 0xcf, 0xcf, 0x13, 0x78, 0x07, 0x59, 0xcb, 0x61, 0x34, 0xaa, 0x62, 0xb7, 0x59, 0x4e,
+	0xea, 0x4a, 0xbc, 0x9d, 0xb6, 0xb9, 0xed, 0xec, 0xe1, 0x2f, 0x00, 0x00, 0xff, 0xff, 0x81, 0xe6,
+	0x8d, 0x59, 0x44, 0x01, 0x00, 0x00,
+}

+ 2 - 2
gostfix.proto → common/gostfix.proto

@@ -1,6 +1,6 @@
 syntax = "proto3";
 
-package main;
+package common;
 
 message MailBody {
     string contentType = 1;
@@ -19,4 +19,4 @@ message MailHeader {
 message Mail {
     MailHeader header = 1;
     MailBody body = 2;
-}
+}

+ 30 - 0
common/version.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
+
+const (
+	Version = "0.1.0 alpha"
+)

+ 4 - 0
config/main.ini.default

@@ -0,0 +1,4 @@
+#Path to virtual maildir
+virtual_mailbox_base=
+#Virtual mailboxes maps
+virtual_mailbox_maps=

+ 26 - 0
db/db.go

@@ -0,0 +1,26 @@
+/*
+ * 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 db

+ 0 - 69
mailscanner.go

@@ -1,69 +0,0 @@
-package main
-
-import (
-	"fmt"
-	ioutil "io/ioutil"
-	"log"
-
-	fsnotify "github.com/fsnotify/fsnotify"
-)
-
-type MailScanner struct {
-	watcher *fsnotify.Watcher
-}
-
-// func fileExists(filename string) bool {
-// 	info, err := os.Stat(filename)
-// 	if os.IsNotExist(err) {
-// 		return false
-// 	}
-// 	return err == nil && !info.IsDir() && info != nil
-// }
-
-func NewMailScanner(mailPath string) (ms *MailScanner) {
-	fmt.Printf("Add mail folder %s for watching\n", mailPath)
-	watcher, err := fsnotify.NewWatcher()
-	if err != nil {
-		log.Fatal(err)
-	}
-
-	ms = &MailScanner{
-		watcher: watcher,
-	}
-
-	files, err := ioutil.ReadDir(mailPath)
-	if err != nil {
-		log.Fatal(err)
-	}
-
-	for _, f := range files {
-		fullPath := mailPath + "/" + f.Name()
-		if fileExists(fullPath) {
-			fmt.Printf("Add mail file %s for watching\n", fullPath)
-			watcher.Add(fullPath)
-		}
-	}
-
-	return
-}
-
-func (ms *MailScanner) Run() {
-	go func() {
-		for {
-			select {
-			case event, ok := <-ms.watcher.Events:
-				if !ok {
-					return
-				}
-				if event.Op&fsnotify.Write == fsnotify.Write {
-					log.Println("New email for", event.Name)
-				}
-			case err, ok := <-ms.watcher.Errors:
-				if !ok {
-					return
-				}
-				log.Println("error:", err)
-			}
-		}
-	}()
-}

+ 9 - 265
main.go

@@ -26,290 +26,34 @@
 package main
 
 import (
-	"bufio"
-	"fmt"
-	template "html/template"
-	"log"
-	"net/http"
 	"os"
-	"regexp"
-	"strings"
 
-	unix "golang.org/x/sys/unix"
+	scanner "./scanner"
+	web "./web"
 )
 
-const (
-	StateHeaderScan = iota
-	StateBodyScan
-	StateContentScan
-)
-
-const (
-	AtLeastOneHeaderMask = 1 << iota
-	FromHeaderMask
-	DateHeaderMask
-	ToHeaderMask
-	AllHeaderMask = 15
-)
-
-const (
-	HeaderRegExp        = "^([\x21-\x7E^:]+):(.*)"
-	FoldingRegExp       = "^\\s+(.*)"
-	BoundaryStartRegExp = "^--(.*)"
-	BoundaryEndRegExp   = "^--(.*)--$"
-	BoundaryRegExp      = "boundary=\"(.*)\""
-	UserRegExp          = "^[a-zA-Z][\\w0-9\\._]*"
-)
-
-const (
-	Version = "0.1.0 alpha"
-)
-
-// type Email struct {
-// 	From        string
-// 	To          string
-// 	Cc          string
-// 	Bcc         string
-// 	Date        string
-// 	Subject     string
-// 	ContentType string
-// 	Body        string
-// }
-
-func NewEmail() *Mail {
-	return &Mail{
-		Header: &MailHeader{},
-		Body: &MailBody{
-			ContentType: "plain/text",
-		},
-	}
-}
-
 type GofixEngine struct {
-	templater   *Templater
-	fileServer  http.Handler
-	userChecker *regexp.Regexp
-	scanner     *MailScanner
-	mailPath    string
+	scanner *scanner.MailScanner
+	web     *web.Server
 }
 
 func NewGofixEngine(mailPath string) (e *GofixEngine) {
 	e = &GofixEngine{
-		templater:  NewTemplater("templates"),
-		fileServer: http.FileServer(http.Dir("./")),
-		scanner:    NewMailScanner(mailPath),
-		mailPath:   mailPath,
+		scanner: scanner.NewMailScanner(mailPath),
+		web:     web.NewServer(mailPath),
 	}
 
-	var err error = nil
-	e.userChecker, err = regexp.Compile(UserRegExp)
-	if err != nil {
-		log.Fatal("Could not compile user checker regex")
-	}
 	return
 }
 
 func (e *GofixEngine) Run() {
-	defer e.scanner.watcher.Close()
+	defer e.scanner.Stop()
 	e.scanner.Run()
-	http.Handle("/", e)
-	log.Fatal(http.ListenAndServe(":65200", nil))
-}
-
-func (e *GofixEngine) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	fmt.Println(r.URL.Path)
-	if strings.Index(r.URL.Path, "/css/") == 0 || strings.Index(r.URL.Path, "/assets/") == 0 {
-		e.fileServer.ServeHTTP(w, r)
-	} 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
-		}
-
-		state := StateHeaderScan
-		headerFinder, err := regexp.Compile(HeaderRegExp)
-		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)
-		}
-
-		// boundaryStartFinder, err := regexp.Compile(BoundaryStartRegExp)
-		// 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)
-		}
-
-		boundaryFinder, err := regexp.Compile(BoundaryRegExp)
-
-		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 = ""
-						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)
-				}
-			}
-
-			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
-					}
-					if previousHeader != nil {
-						*previousHeader += capture[2]
-					}
-					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 && 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",
-			Version:  Version,
-		}))
-	}
-}
-
-func openAndLockMailFile() {
-	file, err := os.OpenFile("/home/vmail/semlanik.org/ci", os.O_RDWR, 0)
-	if err != nil {
-		log.Fatalf("Error to open /home/vmail/semlanik.org/ci %s", err)
-	}
-	defer file.Close()
-
-	lk := &unix.Flock_t{
-		Type: unix.F_WRLCK,
-	}
-	err = unix.FcntlFlock(file.Fd(), unix.F_SETLKW, lk)
-	lk.Type = unix.F_UNLCK
-
-	if err != nil {
-		log.Fatalf("Error to set lock %s", err)
-	}
-	defer unix.FcntlFlock(file.Fd(), unix.F_SETLKW, lk)
-
-	fmt.Printf("Succesfully locked PID: %d", lk.Pid)
-	input := bufio.NewScanner(os.Stdin)
-	input.Scan()
-}
-
-func fileExists(filename string) bool {
-	info, err := os.Stat(filename)
-	if os.IsNotExist(err) {
-		return false
-	}
-	return err == nil && !info.IsDir() && info != nil
+	e.web.Run()
 }
 
 func main() {
-	mailPath := "./"
+	mailPath := "."
 	if len(os.Args) >= 2 {
 		mailPath = os.Args[1]
 	}

+ 91 - 0
scanner/mailscanner.go

@@ -0,0 +1,91 @@
+/*
+ * 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 scanner
+
+import (
+	"fmt"
+	ioutil "io/ioutil"
+	"log"
+
+	utils "../utils"
+	fsnotify "github.com/fsnotify/fsnotify"
+)
+
+type MailScanner struct {
+	watcher *fsnotify.Watcher
+}
+
+func NewMailScanner(mailPath string) (ms *MailScanner) {
+	fmt.Printf("Add mail folder %s for watching\n", mailPath)
+	watcher, err := fsnotify.NewWatcher()
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	ms = &MailScanner{
+		watcher: watcher,
+	}
+
+	files, err := ioutil.ReadDir(mailPath)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	for _, f := range files {
+		fullPath := mailPath + "/" + f.Name()
+		if utils.FileExists(fullPath) {
+			fmt.Printf("Add mail file %s for watching\n", fullPath)
+			watcher.Add(fullPath)
+		}
+	}
+
+	return
+}
+
+func (ms *MailScanner) Run() {
+	go func() {
+		for {
+			select {
+			case event, ok := <-ms.watcher.Events:
+				if !ok {
+					return
+				}
+				if event.Op&fsnotify.Write == fsnotify.Write {
+					log.Println("New email for", event.Name)
+				}
+			case err, ok := <-ms.watcher.Errors:
+				if !ok {
+					return
+				}
+				log.Println("error:", err)
+			}
+		}
+	}()
+}
+
+func (ms *MailScanner) Stop() {
+	defer ms.watcher.Close()
+}

+ 86 - 0
utils/fileutils.go

@@ -0,0 +1,86 @@
+/*
+ * 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 utils
+
+import (
+	"os"
+
+	unix "golang.org/x/sys/unix"
+)
+
+func FileExists(filename string) bool {
+	info, err := os.Stat(filename)
+	if os.IsNotExist(err) {
+		return false
+	}
+	return err == nil && !info.IsDir() && info != nil
+}
+
+func DirectoryExists(filename string) bool {
+	info, err := os.Stat(filename)
+	if os.IsNotExist(err) {
+		return false
+	}
+	return err == nil && info.IsDir() && info != nil
+}
+
+type LockedFile struct {
+	file *os.File
+	lock *unix.Flock_t
+}
+
+func OpenAndLockWait(path string) (file *LockedFile, err error) {
+	file = &LockedFile{}
+	file.file, err = os.OpenFile(path, os.O_RDWR, 0)
+	if err != nil {
+		return nil, err
+	}
+
+	file.lock = &unix.Flock_t{
+		Type: unix.F_WRLCK,
+	}
+	err = unix.FcntlFlock(file.file.Fd(), unix.F_SETLKW, file.lock)
+	file.lock.Type = unix.F_UNLCK
+
+	if err != nil {
+		return nil, err
+	}
+
+	return
+}
+
+func (f *LockedFile) Read(p []byte) (n int, err error) {
+	return f.file.Read(p)
+}
+
+func (f *LockedFile) CloseAndUnlock() error {
+	err1 := unix.FcntlFlock(f.file.Fd(), unix.F_SETLKW, f.lock)
+	err2 := f.file.Close()
+	if err1 != nil {
+		return err1
+	}
+	return err2
+}

+ 118 - 0
utils/regexp.go

@@ -0,0 +1,118 @@
+/*
+ * 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 utils
+
+import (
+	"log"
+	"regexp"
+	"sync"
+)
+
+const (
+	HeaderRegExp        = "^([\x21-\x7E^:]+):(.*)"
+	FoldingRegExp       = "^\\s+(.*)"
+	BoundaryStartRegExp = "^--(.*)"
+	BoundaryEndRegExp   = "^--(.*)--$"
+	BoundaryRegExp      = "boundary=\"(.*)\""
+)
+
+const (
+	UserRegExp = "^[a-zA-Z][\\w0-9\\._]*"
+)
+
+type RegExpUtils regExpUtils
+
+var (
+	once     sync.Once
+	instance *regExpUtils
+)
+
+func RegExpUtilsInstance() *RegExpUtils {
+
+	once.Do(func() {
+		instance, _ = newRegExpUtils()
+	})
+
+	return (*RegExpUtils)(instance)
+}
+
+type regExpUtils struct {
+	UserChecker         *regexp.Regexp
+	HeaderFinder        *regexp.Regexp
+	FoldingFinder       *regexp.Regexp
+	BoundaryStartFinder *regexp.Regexp
+	BoundaryEndFinder   *regexp.Regexp
+	BoundaryFinder      *regexp.Regexp
+}
+
+func newRegExpUtils() (*regExpUtils, error) {
+	headerFinder, err := regexp.Compile(HeaderRegExp)
+	if err != nil {
+		log.Fatalf("Invalid regexp %s\n", err)
+		return nil, err
+	}
+
+	foldingFinder, err := regexp.Compile(FoldingRegExp)
+	if err != nil {
+		log.Fatalf("Invalid regexp %s\n", err)
+		return nil, err
+	}
+
+	boundaryStartFinder, err := regexp.Compile(BoundaryStartRegExp)
+	if err != nil {
+		log.Fatalf("Invalid regexp %s\n", err)
+		return nil, err
+	}
+
+	boundaryEndFinder, err := regexp.Compile(BoundaryEndRegExp)
+	if err != nil {
+		log.Fatalf("Invalid regexp %s\n", err)
+		return nil, err
+	}
+
+	boundaryFinder, err := regexp.Compile(BoundaryRegExp)
+	if err != nil {
+		log.Fatalf("Invalid regexp %s\n", err)
+		return nil, err
+	}
+
+	userChecker, err := regexp.Compile(UserRegExp)
+	if err != nil {
+		log.Fatalf("Invalid regexp %s\n", err)
+		return nil, err
+	}
+
+	ru := &regExpUtils{
+		UserChecker:         userChecker,
+		HeaderFinder:        headerFinder,
+		FoldingFinder:       foldingFinder,
+		BoundaryStartFinder: boundaryStartFinder,
+		BoundaryEndFinder:   boundaryEndFinder,
+		BoundaryFinder:      boundaryFinder,
+	}
+
+	return ru, nil
+}

+ 229 - 0
web/server.go

@@ -0,0 +1,229 @@
+/*
+ * 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 web
+
+import (
+	"bufio"
+	"fmt"
+	template "html/template"
+	"log"
+	"net/http"
+	"strings"
+
+	common "../common"
+	utils "../utils"
+)
+
+const (
+	StateHeaderScan = iota
+	StateBodyScan
+	StateContentScan
+)
+
+const (
+	AtLeastOneHeaderMask = 1 << iota
+	FromHeaderMask
+	DateHeaderMask
+	ToHeaderMask
+	AllHeaderMask = 15
+)
+
+func NewEmail() *common.Mail {
+	return &common.Mail{
+		Header: &common.MailHeader{},
+		Body: &common.MailBody{
+			ContentType: "plain/text",
+		},
+	}
+}
+
+type Server struct {
+	fileServer http.Handler
+	templater  *Templater
+	mailPath   string
+}
+
+func NewServer(mailPath string) *Server {
+	return &Server{
+		templater:  NewTemplater("templates"),
+		fileServer: http.FileServer(http.Dir(".")),
+		mailPath:   mailPath,
+	}
+}
+
+func (s *Server) Run() {
+	http.Handle("/", s)
+	log.Fatal(http.ListenAndServe(":65200", nil))
+}
+
+func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	fmt.Println(r.URL.Path)
+	if strings.Index(r.URL.Path, "/css/") == 0 || strings.Index(r.URL.Path, "/assets/") == 0 {
+		s.fileServer.ServeHTTP(w, r)
+	} else {
+		user := r.URL.Query().Get("user")
+
+		if utils.RegExpUtilsInstance().UserChecker.FindString(user) != user || user == "" {
+			fmt.Print("Invalid user")
+			w.WriteHeader(http.StatusUnauthorized)
+			fmt.Fprint(w, "401 - Access denied")
+			return
+		}
+
+		// mailPath = config.mailPath + "/" + r.URL.Query().Get("user")
+		mailPath := "tmp" + "/" + r.URL.Query().Get("user")
+		if !utils.FileExists(mailPath) {
+			w.WriteHeader(http.StatusForbidden)
+			fmt.Fprint(w, "403 Unknown user")
+			return
+		}
+
+		file, err := utils.OpenAndLockWait(mailPath)
+		if err != nil {
+			w.WriteHeader(http.StatusInternalServerError)
+			fmt.Fprint(w, "500 Internal server error")
+			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
+					}
+				} else {
+					fmt.Printf("Empty line in state %d\n", state)
+				}
+			}
+
+			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]
+					}
+					continue
+				}
+
+				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 = ""
+						}
+
+						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)
+
+			previousHeader = nil
+			activeBoundary = ""
+			emails = append(emails, email)
+			state = StateHeaderScan
+		}
+
+		fmt.Fprint(w, s.templater.ExecuteIndex(&Index{
+			MailList: template.HTML(s.templater.ExecuteMailList(emails)),
+			Folders:  "Folders",
+			Version:  common.Version,
+		}))
+	}
+}

+ 1 - 1
templater.go → web/templater.go

@@ -23,7 +23,7 @@
  * DEALINGS IN THE SOFTWARE.
  */
 
-package main
+package web
 
 import (
 	"bytes"