main.go 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. /*
  2. * MIT License
  3. *
  4. * Copyright (c) 2020 Alexey Edelev <semlanik@gmail.com>
  5. *
  6. * This file is part of gostfix project https://git.semlanik.org/semlanik/gostfix
  7. *
  8. * Permission is hereby granted, free of charge, to any person obtaining a copy of this
  9. * software and associated documentation files (the "Software"), to deal in the Software
  10. * without restriction, including without limitation the rights to use, copy, modify,
  11. * merge, publish, distribute, sublicense, and/or sell copies of the Software, and
  12. * to permit persons to whom the Software is furnished to do so, subject to the following
  13. * conditions:
  14. *
  15. * The above copyright notice and this permission notice shall be included in all copies
  16. * or substantial portions of the Software.
  17. *
  18. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
  19. * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
  20. * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
  21. * FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
  22. * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  23. * DEALINGS IN THE SOFTWARE.
  24. */
  25. package main
  26. import (
  27. "bufio"
  28. "fmt"
  29. template "html/template"
  30. "log"
  31. "net/http"
  32. "os"
  33. "regexp"
  34. "strings"
  35. unix "golang.org/x/sys/unix"
  36. )
  37. const (
  38. StateHeaderScan = iota
  39. StateBodyScan
  40. StateContentScan
  41. )
  42. const (
  43. AtLeastOneHeaderMask = 1 << iota
  44. FromHeaderMask
  45. DateHeaderMask
  46. ToHeaderMask
  47. AllHeaderMask = 15
  48. )
  49. const (
  50. HeaderRegExp = "^([\x21-\x7E^:]+):(.*)"
  51. FoldingRegExp = "^\\s+(.*)"
  52. BoundaryStartRegExp = "^--(.*)"
  53. BoundaryEndRegExp = "^--(.*)--$"
  54. BoundaryRegExp = "boundary=\"(.*)\""
  55. UserRegExp = "^[a-zA-Z][\\w0-9\\._]*"
  56. )
  57. const (
  58. Version = "0.1.0 alpha"
  59. )
  60. // type Email struct {
  61. // From string
  62. // To string
  63. // Cc string
  64. // Bcc string
  65. // Date string
  66. // Subject string
  67. // ContentType string
  68. // Body string
  69. // }
  70. func NewEmail() *Mail {
  71. return &Mail{
  72. Header: &MailHeader{},
  73. Body: &MailBody{
  74. ContentType: "plain/text",
  75. },
  76. }
  77. }
  78. type GofixEngine struct {
  79. templater *Templater
  80. fileServer http.Handler
  81. userChecker *regexp.Regexp
  82. scanner *MailScanner
  83. mailPath string
  84. }
  85. func NewGofixEngine(mailPath string) (e *GofixEngine) {
  86. e = &GofixEngine{
  87. templater: NewTemplater("templates"),
  88. fileServer: http.FileServer(http.Dir("./")),
  89. scanner: NewMailScanner(mailPath),
  90. mailPath: mailPath,
  91. }
  92. var err error = nil
  93. e.userChecker, err = regexp.Compile(UserRegExp)
  94. if err != nil {
  95. log.Fatal("Could not compile user checker regex")
  96. }
  97. return
  98. }
  99. func (e *GofixEngine) Run() {
  100. defer e.scanner.watcher.Close()
  101. e.scanner.Run()
  102. http.Handle("/", e)
  103. log.Fatal(http.ListenAndServe(":65200", nil))
  104. }
  105. func (e *GofixEngine) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  106. fmt.Println(r.URL.Path)
  107. if strings.Index(r.URL.Path, "/css/") == 0 || strings.Index(r.URL.Path, "/assets/") == 0 {
  108. e.fileServer.ServeHTTP(w, r)
  109. } else {
  110. user := r.URL.Query().Get("user")
  111. if e.userChecker.FindString(user) != user || user == "" {
  112. fmt.Print("Invalid user")
  113. w.WriteHeader(http.StatusUnauthorized)
  114. fmt.Fprint(w, "401 - Access denied")
  115. return
  116. }
  117. state := StateHeaderScan
  118. headerFinder, err := regexp.Compile(HeaderRegExp)
  119. if err != nil {
  120. log.Fatalf("Invalid regexp %s\n", err)
  121. }
  122. foldingFinder, err := regexp.Compile(FoldingRegExp)
  123. if err != nil {
  124. log.Fatalf("Invalid regexp %s\n", err)
  125. }
  126. // boundaryStartFinder, err := regexp.Compile(BoundaryStartRegExp)
  127. // if err != nil {
  128. // log.Fatalf("Invalid regexp %s\n", err)
  129. // }
  130. boundaryEndFinder, err := regexp.Compile(BoundaryEndRegExp)
  131. if err != nil {
  132. log.Fatalf("Invalid regexp %s\n", err)
  133. }
  134. boundaryFinder, err := regexp.Compile(BoundaryRegExp)
  135. if !fileExists(e.mailPath + "/" + r.URL.Query().Get("user")) {
  136. w.WriteHeader(http.StatusForbidden)
  137. fmt.Fprint(w, "403 Unknown user")
  138. return
  139. }
  140. file, _ := os.Open(e.mailPath + "/" + r.URL.Query().Get("user"))
  141. scanner := bufio.NewScanner(file)
  142. activeBoundary := ""
  143. var previousHeader *string = nil
  144. var emails []*Mail
  145. mandatoryHeaders := 0
  146. email := NewEmail()
  147. for scanner.Scan() {
  148. if scanner.Text() == "" {
  149. if state == StateHeaderScan && mandatoryHeaders&AtLeastOneHeaderMask == AtLeastOneHeaderMask {
  150. boundaryCapture := boundaryFinder.FindStringSubmatch(email.Body.ContentType)
  151. if len(boundaryCapture) == 2 {
  152. activeBoundary = boundaryCapture[1]
  153. } else {
  154. activeBoundary = ""
  155. }
  156. state = StateBodyScan
  157. // fmt.Printf("--------------------------Start body scan content type:%s boundary: %s -------------------------\n", email.Body.ContentType, activeBoundary)
  158. } else if state == StateBodyScan {
  159. // fmt.Printf("--------------------------Previous email-------------------------\n%v\n", email)
  160. if activeBoundary == "" {
  161. previousHeader = nil
  162. activeBoundary = ""
  163. fmt.Printf("Actual headers: %d\n", mandatoryHeaders)
  164. if mandatoryHeaders == AllHeaderMask {
  165. emails = append(emails, email)
  166. }
  167. email = NewEmail()
  168. state = StateHeaderScan
  169. mandatoryHeaders = 0
  170. } else {
  171. fmt.Printf("Still in body scan\n")
  172. continue
  173. }
  174. } else {
  175. fmt.Printf("Empty line in state %d\n", state)
  176. }
  177. }
  178. if state == StateHeaderScan {
  179. capture := headerFinder.FindStringSubmatch(scanner.Text())
  180. if len(capture) == 3 {
  181. // fmt.Printf("capture Header %s : %s\n", strings.ToLower(capture[0]), strings.ToLower(capture[1]))
  182. header := strings.ToLower(capture[1])
  183. mandatoryHeaders |= AtLeastOneHeaderMask
  184. switch header {
  185. case "from":
  186. previousHeader = &email.Header.From
  187. mandatoryHeaders |= FromHeaderMask
  188. case "to":
  189. previousHeader = &email.Header.To
  190. mandatoryHeaders |= ToHeaderMask
  191. case "cc":
  192. previousHeader = &email.Header.Cc
  193. case "bcc":
  194. previousHeader = &email.Header.Bcc
  195. mandatoryHeaders |= ToHeaderMask
  196. case "subject":
  197. previousHeader = &email.Header.Subject
  198. case "date":
  199. previousHeader = &email.Header.Date
  200. mandatoryHeaders |= DateHeaderMask
  201. case "content-type":
  202. previousHeader = &email.Body.ContentType
  203. default:
  204. previousHeader = nil
  205. }
  206. if previousHeader != nil {
  207. *previousHeader += capture[2]
  208. }
  209. continue
  210. }
  211. capture = foldingFinder.FindStringSubmatch(scanner.Text())
  212. if len(capture) == 2 && previousHeader != nil {
  213. *previousHeader += capture[1]
  214. continue
  215. }
  216. } else {
  217. // email.Body.Content += scanner.Text() + "\n"
  218. if activeBoundary != "" {
  219. capture := boundaryEndFinder.FindStringSubmatch(scanner.Text())
  220. if len(capture) == 2 {
  221. // fmt.Printf("capture Boundary End %s\n", capture[1])
  222. if activeBoundary == capture[1] {
  223. state = StateBodyScan
  224. activeBoundary = ""
  225. }
  226. continue
  227. }
  228. // capture = boundaryStartFinder.FindStringSubmatch(scanner.Text())
  229. // if len(capture) == 2 && activeBoundary == capture[1] {
  230. // // fmt.Printf("capture Boundary Start %s\n", capture[1])
  231. // state = StateContentScan
  232. // continue
  233. // }
  234. }
  235. }
  236. }
  237. if state == StateBodyScan && mandatoryHeaders == AllHeaderMask { //Finalize if body read till EOF
  238. // fmt.Printf("--------------------------Previous email-------------------------\n%v\n", email)
  239. previousHeader = nil
  240. activeBoundary = ""
  241. emails = append(emails, email)
  242. state = StateHeaderScan
  243. }
  244. fmt.Fprint(w, e.templater.ExecuteIndex(&Index{
  245. MailList: template.HTML(e.templater.ExecuteMailList(emails)),
  246. Folders: "Folders",
  247. Version: Version,
  248. }))
  249. }
  250. }
  251. func openAndLockMailFile() {
  252. file, err := os.OpenFile("/home/vmail/semlanik.org/ci", os.O_RDWR, 0)
  253. if err != nil {
  254. log.Fatalf("Error to open /home/vmail/semlanik.org/ci %s", err)
  255. }
  256. defer file.Close()
  257. lk := &unix.Flock_t{
  258. Type: unix.F_WRLCK,
  259. }
  260. err = unix.FcntlFlock(file.Fd(), unix.F_SETLKW, lk)
  261. lk.Type = unix.F_UNLCK
  262. if err != nil {
  263. log.Fatalf("Error to set lock %s", err)
  264. }
  265. defer unix.FcntlFlock(file.Fd(), unix.F_SETLKW, lk)
  266. fmt.Printf("Succesfully locked PID: %d", lk.Pid)
  267. input := bufio.NewScanner(os.Stdin)
  268. input.Scan()
  269. }
  270. func fileExists(filename string) bool {
  271. info, err := os.Stat(filename)
  272. if os.IsNotExist(err) {
  273. return false
  274. }
  275. return err == nil && !info.IsDir() && info != nil
  276. }
  277. func main() {
  278. mailPath := "./"
  279. if len(os.Args) >= 2 {
  280. mailPath = os.Args[1]
  281. }
  282. engine := NewGofixEngine(mailPath)
  283. engine.Run()
  284. }