405
package web
import (
"context"
_ "embed"
"errors"
"fmt"
"github.com/microcosm-cc/bluemonday"
"golang.org/x/sys/unix"
"html"
"html/template"
"mime"
"net/http"
"net/mail"
"net/url"
"os"
"os/signal"
"path/filepath"
"strings"
"git.sophuwu.com/mailboxxer/db"
)
//go:embed templates/index.html
var htmlTemplate string
//go:embed templates/send.html
var sendTemplate 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
}
if r.URL.Path == "/send.html" {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprint(w, sendTemplate)
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 style="text-wrap: wrap;">%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")
p.AllowRelativeURLs(true)
p.RewriteSrc(func(src *url.URL) {
if _, err = os.Stat(filepath.Join(db.SAVEPATH, id, src.Path)); err == nil && strings.HasPrefix(mime.TypeByExtension(filepath.Ext(src.Path)), "image") {
src.Path = filepath.Join("/" + id + "/" + src.Path)
}
})
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)
}