git.sophuwu.com > mailboxxer   
              389
            
             package web

import (
	"context"
	_ "embed"
	"errors"
	"fmt"
	"github.com/microcosm-cc/bluemonday"
	"golang.org/x/sys/unix"
	"html"
	"html/template"
	"net/http"
	"net/mail"
	"os"
	"os/signal"
	"path/filepath"
	"strings"

	"git.sophuwu.com/mailboxxer/db"
)

//go:embed templates/index.html
var htmlTemplate string

//go:embed templates/style.css
var cssText string

//go:embed templates/script.js
var jsText string

var t *template.Template

const DefaultAddr = "127.0.1.69:3141"

var sigchan = make(chan os.Signal)

func stopServer() {
	sigchan <- unix.SIGTERM
}

func load() {
	fmt.Printf("starting %s...\n", filepath.Base(os.Args[0]))
	fmt.Printf("\tPID: %d\n", os.Getpid())
	ld := func() {
		// systemd reload signal
	}
	ld()
	ch := make(chan os.Signal)
	signal.Notify(ch, unix.SIGHUP)
	for {
		<-ch
		ld()
	}
}

func ServeHttp(addr string) {
	// go load()

	t = template.Must(template.New("index").Parse(htmlTemplate))
	signal.Notify(sigchan, unix.SIGTERM, unix.SIGINT, unix.SIGQUIT, unix.SIGKILL, unix.SIGSTOP, unix.SIGABRT)

	server := http.Server{
		Addr:    addr,
		Handler: HttpHandle{http.FileServer(http.Dir(db.SAVEPATH))},
	}

	go func() {
		fmt.Printf("starting http server on: %s\n", addr)
		err := server.ListenAndServe()
		if err != nil && !errors.Is(err, http.ErrServerClosed) {
			fmt.Fprintln(os.Stderr, "error: server:", err)
			stopServer()
		}
		fmt.Println("stopped")
	}()
	sig := <-sigchan
	fmt.Printf("\rgot %s\nstopping server...\n", sig.String())
	_ = server.Shutdown(context.Background())
}

func E(s ...string) []any {
	a := make([]any, len(s))
	for i, v := range s {
		a[i] = html.EscapeString(v)
	}
	return a
}

type HtmlEM struct {
	Id       string
	Date     string
	Subject  string
	ToName   string
	ToAddr   string
	FromName string
	FromAddr string
}

func ParseInt(s string) int {
	n := 0
	for _, r := range s {
		if r < '0' || r > '9' {
			break
		}
		n = n*10 + int(r-'0')
	}
	return n
}

func TempErr(w http.ResponseWriter, code int) {
	msg := "An error occurred"
	switch code {
	case 404:
		msg = "Not found"
	case 500:
		msg = "Internal server error"
	}
	var dat = map[string]any{
		"Error":     msg,
		"ErrorCode": code,
	}
	t.Execute(w, dat)
}

type HttpHandle struct {
	fs http.Handler
}

func (hh HttpHandle) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// w.Header().Add("Content-Security-Policy", `default-src 'none'; style-src 'self'; img-src https: data: cid:; font-src 'self';`)
	// w.Header().Add("X-Content-Type-Options", "nosniff")
	// w.Header().Add("X-Frame-Options", "DENY")
	w.Header().Add("Referrer-Policy", "no-referrer")
	// w.Header().Add("Cross-Origin-Opener-Policy", "same-origin")
	// w.Header().Add("Cross-Origin-Embedder-Policy", "same-origin")
	// w.Header().Add("Cross-Origin-Resource-Policy", "same-origin")

	if r.URL.Path == "/style.css" {
		w.Header().Set("Content-Type", "text/css; charset=utf-8")
		fmt.Fprint(w, cssText)
		return
	}
	if r.URL.Path == "/script.js" {
		w.Header().Set("Content-Type", "application/javascript; charset=utf-8")
		fmt.Fprint(w, jsText)
		return
	}
	h := NewHandle(w, r)
	r.ParseForm()
	if r.URL.Path == "/" || r.URL.Path == "/api" {
		h.SearchHandler()
		return
	}
	if r.URL.Path == "/open" {
		w.Header().Set("Content-Type", "text/html; charset=utf-8")

		id := r.Form.Get("id")
		if len(id) != 40 {
			TempErr(w, 404)
			return
		}
		b, err := os.ReadFile(filepath.Join(db.SAVEPATH, id, "body.html"))
		var html string
		if err != nil {
			b, err = os.ReadFile(filepath.Join(db.SAVEPATH, id, "body.txt"))
			if err != nil {
				TempErr(w, 404)
				return
			}
			html = fmt.Sprintf(`<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"></head><body style="background-color: #262855; color: white;"><pre>%s</pre></body></html>`, string(b))
		} else {
			html = string(b)

			p := bluemonday.UGCPolicy()
			p.AllowDataURIImages()
			p.AllowIFrames()
			p.RequireSandboxOnIFrame()
			p.AllowStyling()
			p.AllowImages()
			p.AllowComments()
			p.AllowRelativeURLs(true)
			p.AllowURLSchemes("http", "https", "cid", "data")
			p.AllowStandardAttributes()
			p.AllowAttrs("style", "class").Globally()
			p.AllowAttrs("width", "height", "src", "rel").OnElements("img", "iframe", "video", "audio")
			p.AllowAttrs("href", "target").OnElements("a")
			p.AllowTables()
			p.AllowLists()
			p.AllowElements("table", "thead", "tbody", "tfoot", "th", "tr", "td", "ul", "ol", "li", "dl", "dt", "dd", "style", "a", "img", "iframe", "video", "audio")
			// p.AllowAttrs("type").OnElements("style")
			p.AllowElements("div", "section", "span", "p", "br", "hr", "b", "i", "u", "strong", "em", "h1", "h2", "h3", "h4", "h5", "h6", "pre", "code", "blockquote")
			// p.SkipElementsContent("script")
			html = p.Sanitize(html)

			if !strings.Contains(html, "</html>") && !strings.Contains(html, "</body>") {
				html = fmt.Sprintf(`<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"></head><body style="background-color: #262855; color: white;">%s</body></html>`, html)
			}
		}
		html = strings.Replace(html, `<head>`, `<head><style>*{color: white !important; background-color: #262833 !important;}</style>`, 1)
		fmt.Fprint(w, html)
		return
	}
	hh.fs.ServeHTTP(w, r)
}

type Handle struct {
	qu *db.Query
	w  http.ResponseWriter
	r  *http.Request
}

func NewHandle(w http.ResponseWriter, r *http.Request) *Handle {
	qu, errr := db.NewQuery(30)
	if errr != nil {
		fmt.Fprintln(os.Stderr, "Error creating query:", errr)
		db.Close()
		os.Exit(1)
	}
	return &Handle{
		qu: qu,
		w:  w,
		r:  r,
	}
}

func Http() http.HandlerFunc {

	return func(w http.ResponseWriter, r *http.Request) {

		// 		if filepath.Base(r.URL.Path) == "html.html" && len(strings.Split(r.URL.Path[1:], "/")) == 2 {
		// 			id := filepath.Base(filepath.Dir(r.URL.Path))
		// 			s, _ := os.ReadFile(filepath.Join(db.SAVEPATH, id, "header.txt"))
		// 			b, _ := os.ReadFile(filepath.Join(db.SAVEPATH, id, "body.txt"))
		// 			fmt.Fprintf(w, `<html><head><title>%s</title></head>
		// <body style="display: flex; flex-direction: row;">
		// <div style="width: 50%%;display:inline;height:100%%;overflow:scroll;"><pre>%s</pre><br><pre>%s</pre></div>
		// <iframe style="width: 50%%;display:inline;height:100%%;overflow:scroll;" src="%s"></iframe>
		// </body></html>`, id, string(s), string(b), "/"+id)
		// 			return
		// 		}
		if func(w http.ResponseWriter, r *http.Request) bool {
			var file string
			var id string
			l := strings.SplitN(r.URL.Path[1:], "/", 2)
			if len(l) == 0 {
				return false
			}
			if len(l) >= 1 {
				id = l[0]
			}
			if len(l) >= 2 {
				file = l[1]
			}
			if file != "" && filepath.Ext(file) != ".txt" {
				return false
			}
			de, err := os.ReadDir(filepath.Join(db.SAVEPATH, id))
			if err != nil {
				return false
			}

			darkMode := r.URL.Query().Get("dark")
			if darkMode == "" || darkMode == "0" || darkMode == "false" {
				darkMode = "0"
			} else {
				darkMode = "1"
			}
			dat := map[string]any{
				"DarkMode": darkMode,
				"Style":    `style="overflow: auto;"`,
			}

			var ss string
			var f os.DirEntry
			if file != "" {
				var b []byte
				for _, f = range de {
					if f.Name() == file && f.Type().IsRegular() {
						b, err = os.ReadFile(filepath.Join(db.SAVEPATH, id, file))
						break
					}
				}
				if err != nil || len(b) == 0 {
					return false
				}
				ss = string(b)
				if filepath.Ext(f.Name()) == ".txt" {
					dat["Text"] = ss
				} else {
					return false
				}
			} else {
				for _, f = range de {
					ss += fmt.Sprintf(`<a href="/%s`, filepath.Join(id, f.Name()))
					if filepath.Ext(f.Name()) == ".txt" {
						ss += fmt.Sprintf(`?dark=%s`, darkMode)
					}
					ss += fmt.Sprintf(`">%s</a><br>%c`, html.EscapeString(f.Name()), '\n')
				}
				ss = `<main ` + fmt.Sprint(dat["Style"]) + `>` + ss + `</main>`
				dat["Html"] = template.HTML(ss)
			}
			w.Header().Set("Content-Type", "text/html; charset=utf-8")
			t.Execute(w, dat)
			return true
		}(w, r) {
			return
		}
		http.FileServer(http.Dir(db.SAVEPATH)).ServeHTTP(w, r)
	}
}

func (h *Handle) SearchHandler() {
	var q []string
	if h.r.Form.Get("to") != "" {
		q = append(q, fmt.Sprintf(` toaddr LIKE '%%%s%%'`, h.r.Form.Get("to")))
	}
	if h.r.Form.Get("from") != "" {
		q = append(q, fmt.Sprintf(` fromaddr LIKE '%%%s%%'`, h.r.Form.Get("from")))
	}
	if h.r.Form.Get("subject") != "" {
		q = append(q, fmt.Sprintf(` subject LIKE '%%%s%%'`, h.r.Form.Get("subject")))
	}
	if h.r.Form.Get("date") != "" {
		q = append(q, fmt.Sprintf(` date LIKE '%%%s%%'`, h.r.Form.Get("date")))
	}
	where := func() string {
		if len(q) == 0 {
			return ""
		}
		return strings.Join(q, " AND ")
	}()
	var err error
	if where != h.qu.GetWhere() {
		err = h.qu.SetWhere(where)
		if err != nil {
			TempErr(h.w, 500)
			return
		}
	}
	err = h.qu.SetPage(ParseInt(h.r.Form.Get("page")) - 1)
	if err != nil {
		TempErr(h.w, 500)
		return
	}
	if h.qu.TotalRows() == 0 {
		TempErr(h.w, 404)
		return
	}
	var htmlMetas []HtmlEM
	var from *mail.Address
	var to []*mail.Address
	var addrlist []string
	var htmlMeta HtmlEM
	for _, em := range h.qu.Rows() {
		htmlMeta = HtmlEM{
			Id:      em.Id,
			Date:    db.TimeStr(em.Date),
			Subject: em.Subject,
		}
		if from, err = mail.ParseAddress(em.From); err != nil {
			htmlMeta.FromAddr = em.From
		} else {
			htmlMeta.FromName, htmlMeta.FromAddr = from.Name, from.Address
		}
		to, err = mail.ParseAddressList(em.To)
		if err != nil || len(to) == 0 {
			addrlist = strings.Split(em.To, ", ")
			if len(addrlist) == 0 {
				htmlMeta.ToAddr = em.To
			} else {
				htmlMeta.ToAddr = addrlist[0]
			}
		} else {
			htmlMeta.ToName, htmlMeta.ToAddr = to[0].Name, to[0].Address
		}
		htmlMetas = append(htmlMetas, htmlMeta)
	}
	dat := map[string]any{
		"ToAddr":     h.r.Form.Get("to"),
		"FromAddr":   h.r.Form.Get("from"),
		"Subject":    h.r.Form.Get("subject"),
		"Date":       h.r.Form.Get("date"),
		"Page":       h.qu.Page(),
		"TotalPages": h.qu.TotalPages(),
		"HtmlMetas":  htmlMetas,
	}
	t.Execute(h.w, dat)
}