git.sophuwu.com > mailboxxer
made some changes to http
parent

9b5fe28b407c896d4ecb54e0d774cbfc03beb510

M db/parse.godb/parse.go

@@ -63,7 +63,7 @@ }

for _, f := range dir { err = newEntry(f) if err != nil { - return err + return fmt.Errorf("error processing file %s: %w", f.Name(), err) } } return nil

@@ -173,11 +173,11 @@ }

n := time.Now().Local() return strings.ReplaceAll(func() string { if t.Year() != n.Year() { - return t.Format("Jan 02th 2006") + return t.Format("02th Jan 2006") } d := time.Since(t) if d.Hours() > 24*6 { - return t.Format("Jan 02th") + return t.Format("02th Jan") } if d.Hours() > 24 { return t.Format("Mon 15:04")
M go.modgo.mod

@@ -13,9 +13,12 @@ golang.org/x/sys v0.34.0

) require ( + github.com/aymerick/douceur v0.2.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/google/uuid v1.5.0 // indirect + github.com/gorilla/css v1.0.1 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect golang.org/x/term v0.33.0 // indirect modernc.org/libc v1.37.6 // indirect
M go.sumgo.sum

@@ -1,5 +1,7 @@

git.sophuwu.com/gophuwu v0.0.0-20250728114940-b336dded4177 h1:FiGpg3/ceTwB5WNOuVACi2YlPB2nRLJlJNlQgi/xNxA= git.sophuwu.com/gophuwu v0.0.0-20250728114940-b336dded4177/go.mod h1:2j1SAWD5STcFV5oKUm4vChACQ1peXCKpfJLbgE/sD00= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=

@@ -8,8 +10,12 @@ github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=

github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= +github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= +github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
M main.gomain.go

@@ -1,15 +1,16 @@

package main import ( - "os" "fmt" "git.sophuwu.com/gophuwu/flags" + "os" + "git.sophuwu.com/mailboxxer/db" "git.sophuwu.com/mailboxxer/web" ) func init() { - newFlag := flags.NewNewFlagWithHandler(func(err error){ + newFlag := flags.NewNewFlagWithHandler(func(err error) { fmt.Fprintln(os.Stderr, err) os.Exit(1) })

@@ -21,22 +22,25 @@ db.ChkErr(flags.ParseArgs())

} func main() { - db.Open() - defer db.Close() isWeb, err := flags.GetBoolFlag("web") if err != nil { fmt.Fprintln(os.Stderr, err) - return + os.Exit(69) } var addr string if isWeb { addr, err = flags.GetStringFlag("listen") if err != nil { fmt.Fprintln(os.Stderr, err) - return + os.Exit(69) } + db.Open() web.ServeHttp(addr) - return + db.Close() + os.Exit(0) } + db.Open() CLI() + db.Close() + os.Exit(0) }
M web/templates/index.htmlweb/templates/index.html

@@ -1,280 +1,58 @@

{{ define "index" }} <!DOCTYPE html> -<html><head><title>Emails</title> -<style> -:root { - @media (max-width: 1100px) { - .recpt { - display: none; - } - --subwidth: 25%; - } -{{ if .DarkMode }} - --dark-light: {{ .DarkMode }}; -{{ end }} - - - --cont: calc(var(--dark-light, 0) * 100%); - --fg-color: rgb(var(--cont),var(--cont),var(--cont)); - - --bg-color: rgb(from var(--fg-color) calc(255 - r) calc(255 - g) calc(255 - b)); - --accent: color-mix(in lab, var(--accent1,#fcaeff), var(--bg-color) 50%); - --border-color: color-mix(in srgb, var(--fg-color), var(--bg-color) 80%); - --accent-light: color-mix(in srgb, var(--accent), var(--bg-color) 70%); -} -html { - color: var(--fg-color); - background-color: var(--bg-color); -} -section { - display: flex; - flex-direction: row; - cursor: pointer; - border-bottom: 1px solid var(--border-color); - width: 100%; -} -section > *{ - display: inline-block; - padding: 5px; -} -section > div:last-child { - border-right: none!important; -} -section > div { - border-right: 1px solid var(--border-color); -} - -main > section:hover{ - background-color: var(--accent-light); -} -.addr { - font-weight: bold; -} -.addr:hover > span { - display: none; -} -.addr > span { - display: inline; -} -div { - margin: 0 auto 0 auto; - overflow: clip; - text-overflow: ellipsis; - text-wrap: nowrap; -} -.time { - width: 13ch; -} -.sub { - width: calc(100% - 13ch - var(--subwidth, 40%)); -} -.addr { - width: calc( 20% - 4ch ); - font-size: 0.8em; -} -.pageform { - display: flex; - align-items: center; - justify-content: space-between; -} -.buttholder { - margin: 1ch 1lh; -} -button { - border: 1px solid var(--border-color); - border-radius: 0.5ch; -} -button:hover { - cursor: pointer; - background-color: var(--accent); -} -body { - margin: 0.5lh 1ch; - display: flex; - flex-direction: column; - position: fixed; - width: calc(100% - 2ch); - height: calc(100% - 1lh); - padding: 0; -} -header { - margin: 0; - padding: 0; -} -.frameholder { - z-index: 4; - position: absolute; - width: calc(100% - 2em); - height: calc(100% - 2em); - display: none; - left: 50%; - top: 50%; - transform: translate(-50%,-50%); - border: 2em solid transparent; - background-color: color-mix(in lab, var(--fg-color), transparent 40%); -} -.header { - background: var(--accent); -} -.frameholder > header { - display: flex; - flex-direction: row; - justify-content: space-between; - height: 3lh; - align-items: center; - padding: 0 3rlh 0 0.5rlh; -} -.frameholder > #iframe { - width: 100%; - height: calc(100% - 3lh); - border: none; - background: var(--bg-color); -} -.frameholder > header > button { - height: 2rlh; - width: 2rlh; - background-color: #b42032; - font-weight: bold; - color: var(--bg-color); - border-radius: 5px; - border: none; - position: absolute; - right: 0.5rlh; - font-size: 1.5rlh; -} -.frameholder > header > button:hover { - background-color: #ef2b30; -} -main { - overflow-y: auto; - width: 100%; - height: 100%; - margin: 0; - display: flex; - flex-direction: column; -} -h1 { - margin: 0; -} +<html lang="en"> +<head> + <meta charset="UTF-8"> + <title>Samael</title> + <link rel="stylesheet" href="/style.css"> + <script src="/script.js"></script> +</head> +<body> + <div class="container"> + <div class="header sidebyside"> + <div class="nomargin nopadding sidebyside" style="width: 200px;"><h1>Samael</h1><h6>some<br>email</h6></div> + <div class="nomargin nopadding" style="width: calc(100% - 400px);"> + <center><input autocomplete="off" style="width: 100%; max-width: 800px;" id="q" name="q" type="text" onchange="SearchInbox()" placeholder="Search Mail"></center> + </div> + <div class="nomargin nopadding sidebyside rightdiv" style="width: 200px;"> + <button onclick="ToggleSend()">Compose</button> + </div> + </div> + <div class="main sidebyside" style="width: 100%; height: 100%;"> + <div id="inboxframe" class="divframe"> + {{ range .HtmlMetas }} + <div className="inbox-entry" onClick="OpenMail('{{ .Id }}')"> -</style> -<style class="uwustyle"></style> -<script> - var frame; - var iframe; - document.addEventListener("DOMContentLoaded", (function() { - frame = document.getElementById("frame"); - iframe = document.getElementById("iframe"); - frame.querySelector("header > button").addEventListener("click", function() { - iframe.src = ""; - frame.style.display = "none"; - }); - loadDarkMode(); - })); - function pageUpOrDown(e) { - let pf = document.getElementById("pageform"); - let v = parseInt(pf.value); - if (e.innerHTML === "&lt;" || e.innerText === "<") { - pf.value = (v-1).toString(); - } else if (e.innerHTML === "&gt;" || e.innerText === ">") { - pf.value = (v+1).toString(); - } else { - return; - } - pf.form.submit(); - } - function setIframeSrc(elem) { - let id = elem.id; - let to = elem.querySelector(".to-addr").title; - let from = elem.querySelector(".from-addr").title; - let subject = elem.querySelector(".subject").innerText; - let date = elem.querySelector(".date-str").innerText; - iframe.src = "/"+id+"?dark=" + (localStorage.getItem("darkmode") === "true" ? "1" : "0"); - frame.style.display = "block"; - } - function loadDarkMode() { - let dark = localStorage.getItem("darkmode") === "true"; - let v = "0"; - if (dark) v = "1"; - let style = document.querySelector(".uwustyle"); - style.innerHTML = ":root { --dark-light: " + v + "; }"; - let color = localStorage.getItem("accent-color"); - if (color) { - style.innerHTML += ":root { --accent1: " + color + "; }"; - } - } - function toggleDarkMode() { - let dark = localStorage.getItem("darkmode") === "true"; - dark = !dark; - localStorage.setItem("darkmode", dark.toString()); - loadDarkMode() - } - function setAccentColor(color) { - if (!/^#[0-9a-fA-F]{6}$/.test(color)) { - console.error("Invalid color format. Use hex format like #ff0000."); - return; - } - localStorage.setItem("accent-color", color); - loadDarkMode(); - } -</script> -</head><body> -{{ if .Error }} -<h1>Error {{ .ErrorCode }}</h1> -<p>{{ .Error }}</p> -{{ end }} -{{ if .HtmlMetas }} -<header> - <form action="." class="pageform" method="get"> - <h1>Emails</h1> - <div class="buttholder"> - <button onclick="pageUpOrDown(this);">&lt;</button> - <span>Page {{ .Page }} of {{ .TotalPages }}</span> - <button onclick="pageUpOrDown(this);">&gt;</button> + <div className="sidebyside"> + <p className="inbox-from" title="{{ .FromAddr }}"> + {{ if .FromName }} + {{ .FromName }} + {{ else }} + {{ .FromAddr }} + {{ end }} + </p> + <p className="inbox-to" title="{{ .ToAddr }}"> + {{ if .ToName }} + {{ .ToName }} + {{ else }} + {{ .ToAddr }} + {{ end }} + </p> + <p className="inbox-date"> + {{ .Date }} + </p> + </div> + <p className="inbox-subject"> + {{ .Subject }} + </p> + <hr/> + </div> + {{ end }} + </div> + <iframe referrerpolicy="no-referrer" loading="lazy" sandbox id="contentframe" src="" style="visibility: hidden; width: 0;"></iframe> </div> - <input type="hidden" id="pageform" name="page" value="{{ .Page }}" /> - <input type="hidden" name="to" value="{{ .ToAddr }}" /> - <input type="hidden" name="from" value="{{ .FromAddr }}" /> - <input type="hidden" name="subject" value="{{ .Subject }}" /> - <input type="hidden" name="date" value="{{ .Date }}"/> - </form> - <section class="header"> - <div class="addr" title="Full Address">Sender</div> - <div class="sub">Subject</div> - <div class="addr recpt" title="Full Address">Recipient</div> - <div class="time">Date</div> - </section> -</header> -<main style="overflow-y: auto;width: 100%;height: 100%;"> -{{ range .HtmlMetas }} -<section id="{{ .Id }}" onclick="setIframeSrc(this);"> - <div class="addr from-addr" title="{{ .FromAddr }}">{{ .FromName }}</div> - <div class="sub subject">{{ .Subject }}</div> - <div class="addr recpt to-addr" title="{{ .ToAddr }}">{{ .ToName }}</div> - <div class="time date-str">{{ .Date }}</div> -</section> -{{ end }} -</main> -<article id="frame" class="frameholder hidden"> - <header class="header"> - <span> - Subject - </span> - <span> - Date - </span> - <button onclick="setIframeSrc('');">X</button> - </header> - <iframe id="iframe"> - </iframe> -</article> -{{ end }} -{{ if .Html }} -{{ .Html }} -{{ else }} -{{ if .Text }} -<pre style="margin:0; width: 100%; height: 100%; overflow: auto;">{{ .Text }}</pre> -{{ end }} -{{ end }} -</body></html> -{{ end }} + <iframe sandbox="" id="sendframe" src="/send.html"></iframe> + </div> +</body> +</html> +{{ end }}
A web/templates/index2.html

@@ -0,0 +1,280 @@

+{{ define "index" }} +<!DOCTYPE html> +<html><head><title>Emails</title> +<style> +:root { + @media (max-width: 1100px) { + .recpt { + display: none; + } + --subwidth: 25%; + } +{{ if .DarkMode }} + --dark-light: {{ .DarkMode }}; +{{ end }} + + + --cont: calc(var(--dark-light, 0) * 100%); + --fg-color: rgb(var(--cont),var(--cont),var(--cont)); + + --bg-color: rgb(from var(--fg-color) calc(255 - r) calc(255 - g) calc(255 - b)); + --accent: color-mix(in lab, var(--accent1,#fcaeff), var(--bg-color) 50%); + --border-color: color-mix(in srgb, var(--fg-color), var(--bg-color) 80%); + --accent-light: color-mix(in srgb, var(--accent), var(--bg-color) 70%); +} +html { + color: var(--fg-color); + background-color: var(--bg-color); +} +section { + display: flex; + flex-direction: row; + cursor: pointer; + border-bottom: 1px solid var(--border-color); + width: 100%; +} +section > *{ + display: inline-block; + padding: 5px; +} +section > div:last-child { + border-right: none!important; +} +section > div { + border-right: 1px solid var(--border-color); +} + +main > section:hover{ + background-color: var(--accent-light); +} +.addr { + font-weight: bold; +} +.addr:hover > span { + display: none; +} +.addr > span { + display: inline; +} +div { + margin: 0 auto 0 auto; + overflow: clip; + text-overflow: ellipsis; + text-wrap: nowrap; +} +.time { + width: 13ch; +} +.sub { + width: calc(100% - 13ch - var(--subwidth, 40%)); +} +.addr { + width: calc( 20% - 4ch ); + font-size: 0.8em; +} +.pageform { + display: flex; + align-items: center; + justify-content: space-between; +} +.buttholder { + margin: 1ch 1lh; +} +button { + border: 1px solid var(--border-color); + border-radius: 0.5ch; +} +button:hover { + cursor: pointer; + background-color: var(--accent); +} +body { + margin: 0.5lh 1ch; + display: flex; + flex-direction: column; + position: fixed; + width: calc(100% - 2ch); + height: calc(100% - 1lh); + padding: 0; +} +header { + margin: 0; + padding: 0; +} +.frameholder { + z-index: 4; + position: absolute; + width: calc(100% - 2em); + height: calc(100% - 2em); + display: none; + left: 50%; + top: 50%; + transform: translate(-50%,-50%); + border: 2em solid transparent; + background-color: color-mix(in lab, var(--fg-color), transparent 40%); +} +.header { + background: var(--accent); +} +.frameholder > header { + display: flex; + flex-direction: row; + justify-content: space-between; + height: 3lh; + align-items: center; + padding: 0 3rlh 0 0.5rlh; +} +.frameholder > #iframe { + width: 100%; + height: calc(100% - 3lh); + border: none; + background: var(--bg-color); +} +.frameholder > header > button { + height: 2rlh; + width: 2rlh; + background-color: #b42032; + font-weight: bold; + color: var(--bg-color); + border-radius: 5px; + border: none; + position: absolute; + right: 0.5rlh; + font-size: 1.5rlh; +} +.frameholder > header > button:hover { + background-color: #ef2b30; +} +main { + overflow-y: auto; + width: 100%; + height: 100%; + margin: 0; + display: flex; + flex-direction: column; +} +h1 { + margin: 0; +} + +</style> +<style class="uwustyle"></style> +<script> + var frame; + var iframe; + document.addEventListener("DOMContentLoaded", (function() { + frame = document.getElementById("frame"); + iframe = document.getElementById("iframe"); + frame.querySelector("header > button").addEventListener("click", function() { + iframe.src = ""; + frame.style.display = "none"; + }); + loadDarkMode(); + })); + function pageUpOrDown(e) { + let pf = document.getElementById("pageform"); + let v = parseInt(pf.value); + if (e.innerHTML === "&lt;" || e.innerText === "<") { + pf.value = (v-1).toString(); + } else if (e.innerHTML === "&gt;" || e.innerText === ">") { + pf.value = (v+1).toString(); + } else { + return; + } + pf.form.submit(); + } + function setIframeSrc(elem) { + let id = elem.id; + let to = elem.querySelector(".to-addr").title; + let from = elem.querySelector(".from-addr").title; + let subject = elem.querySelector(".subject").innerText; + let date = elem.querySelector(".date-str").innerText; + iframe.src = "/"+id+"?dark=" + (localStorage.getItem("darkmode") === "true" ? "1" : "0"); + frame.style.display = "block"; + } + function loadDarkMode() { + let dark = localStorage.getItem("darkmode") === "true"; + let v = "0"; + if (dark) v = "1"; + let style = document.querySelector(".uwustyle"); + style.innerHTML = ":root { --dark-light: " + v + "; }"; + let color = localStorage.getItem("accent-color"); + if (color) { + style.innerHTML += ":root { --accent1: " + color + "; }"; + } + } + function toggleDarkMode() { + let dark = localStorage.getItem("darkmode") === "true"; + dark = !dark; + localStorage.setItem("darkmode", dark.toString()); + loadDarkMode() + } + function setAccentColor(color) { + if (!/^#[0-9a-fA-F]{6}$/.test(color)) { + console.error("Invalid color format. Use hex format like #ff0000."); + return; + } + localStorage.setItem("accent-color", color); + loadDarkMode(); + } +</script> +</head><body> +{{ if .Error }} +<h1>Error {{ .ErrorCode }}</h1> +<p>{{ .Error }}</p> +{{ end }} +{{ if .HtmlMetas }} +<header> + <form action="." class="pageform" method="get"> + <h1>Emails</h1> + <div class="buttholder"> + <button onclick="pageUpOrDown(this);">&lt;</button> + <span>Page {{ .Page }} of {{ .TotalPages }}</span> + <button onclick="pageUpOrDown(this);">&gt;</button> + </div> + <input type="hidden" id="pageform" name="page" value="{{ .Page }}" /> + <input type="hidden" name="to" value="{{ .ToAddr }}" /> + <input type="hidden" name="from" value="{{ .FromAddr }}" /> + <input type="hidden" name="subject" value="{{ .Subject }}" /> + <input type="hidden" name="date" value="{{ .Date }}"/> + </form> + <section class="header"> + <div class="addr" title="Full Address">Sender</div> + <div class="sub">Subject</div> + <div class="addr recpt" title="Full Address">Recipient</div> + <div class="time">Date</div> + </section> +</header> +<main style="overflow-y: auto;width: 100%;height: 100%;"> +{{ range .HtmlMetas }} +<section id="{{ .Id }}" onclick="setIframeSrc(this);"> + <div class="addr from-addr" title="{{ .FromAddr }}">{{ .FromName }}</div> + <div class="sub subject">{{ .Subject }}</div> + <div class="addr recpt to-addr" title="{{ .ToAddr }}">{{ .ToName }}</div> + <div class="time date-str">{{ .Date }}</div> +</section> +{{ end }} +</main> +<article id="frame" class="frameholder hidden"> + <header class="header"> + <span> + Subject + </span> + <span> + Date + </span> + <button onclick="setIframeSrc('');">X</button> + </header> + <iframe id="iframe"> + </iframe> +</article> +{{ end }} +{{ if .Html }} +{{ .Html }} +{{ else }} +{{ if .Text }} +<pre style="margin:0; width: 100%; height: 100%; overflow: auto;">{{ .Text }}</pre> +{{ end }} +{{ end }} +</body></html> +{{ end }}
A web/templates/script.js

@@ -0,0 +1,78 @@

+ function ToggleSend() { + let style = document.getElementById("sendframe").style; + if (style.top !== "100%") { + style.top = "100%"; + } else { + style.top = "200%"; + } +} + +function UtcToLocal(utcTime) { + let date = new Date(); + date.setTime(Date.parse(utcTime)); + + let minutes = date.getMinutes().toString().padStart(2, '0'); + let hours = date.getHours().toString().padStart(2, '0'); + let day = date.getDate().toString().padStart(2, '0'); + let month = (date.getMonth() + 1).toString().padStart(2, '0'); + let year = date.getFullYear(); + + return `${hours}:${minutes} ${day}-${month}-${year}`; +} + +function HtmlEscape(str) { + return str.replaceAll("&", "&amp;") + .replaceAll("<", "&lt;") + .replaceAll(">", "&gt;") + .replaceAll("\"", "&quot;") + .replaceAll("'", "&apos;") + .replaceAll("{", "&#123;") + .replaceAll("}", "&#125;"); +} + +function GetAddrUser(addr) { + let name = addr.match(/"([^"]+)"/); + if (name) { + return name[1]; + } + name = addr.match(/<([^>]+)>/); + if (name) { + return name[1]; + } + return addr; +} + +function OpenMail(id) { + let frame = document.getElementById("contentframe"); + frame.style.visibility = "visible"; + frame.style.width = "100%"; + frame.src = "/open?id=" + id ; +} + +async function SearchInbox() { + document.getElementById("inboxframe").innerHTML = ""; + let q = document.getElementById("q").value; + + try { + let response = await fetch("/search?q=" + q); + if (!response.ok) throw new Error(JSON.stringify({code: response.status})); + let result = await response.json(); + result["HtmlMetas"].forEach(element => { + let date = UtcToLocal(element["Date"]); + let subject = HtmlEscape(element["Subject"]); + let to = HtmlEscape(GetAddrUser(element["To"])); + let from = HtmlEscape(GetAddrUser(element["From"])); + let id = element["Id"]; + let inbox = document.getElementById("inboxframe"); + inbox.innerHTML += `<div class="inbox-entry" onclick="OpenMail('${id}')"> + <div class="sidebyside"><p class="inbox-from">${from}</p><p class="inbox-to">${to}</p><p class="inbox-date">${date}</p></div> + <p class="inbox-subject">${subject}</p> + <hr></div>`; + }); + } catch (errjson) { + let err = JSON.parse(errjson.message); + console.log(err); + if (err.code === 404) document.getElementById("inboxframe").innerHTML = "<p>No results found.</p>"; + else document.getElementById("inboxframe").innerHTML = "<p>An error occurred.</p>"; + } +}
A web/templates/style.css

@@ -0,0 +1,218 @@

+.container { + position: fixed; + left: 50%; + transform: translate(-50%, 0%); + display: flex; + flex-direction: column; + height: calc( 100% - 20px ); + width: calc( 100% - 50px ); + min-width: 550px; +} +iframe, .divframe { + border: 0; + margin: 5px 0 0; + padding: 0; + width: 100%; + height: 100%; + overflow-y: auto; +} +#sendframe { + position: absolute; + top: 200%; + left: 100%; + transform: translateX(calc(-100% - 17px)) translateY(-100%); + z-index: 2; + min-width: 450px; + height: 50%; + width: 40%; + transition: top 0.5s ease; +} +.inbox-to, .inbox-from, .inbox-date { + width: 100%; +} +.inbox-to, .inbox-date { + text-align: right; +} +.inbox-date, .inbox-from, .inbox-to, .inbox-subject { + margin: 0 10px; + padding: 0 10px; +} +.inbox-subject { + color: #999999; +} +hr { + color: #333333; +} +.nomargin { + margin: 0!important; +} +.nopadding { + padding: 0!important; +} +label{ + font-size: 16px; + margin: 5px; +} +.topin { + border-bottom-left-radius: 0 !important; + border-bottom-right-radius: 0 !important; + margin-bottom: -1px !important; + border-bottom: 1px solid #777 !important; +} +.botin { + border-top-left-radius: 0 !important; + border-top-right-radius: 0 !important; + margin-top: -1px !important; + border-top: 1px solid #777 !important; + +} +.leftin { + border-top-right-radius: 0 !important; + border-bottom-right-radius: 0 !important; + margin-right: -1px !important; + border-right: 1px solid #777 !important; + +} +.rightin { + border-top-left-radius: 0 !important; + border-bottom-left-radius: 0 !important; + margin-left: -1px !important; + border-left: 1px solid #777 !important; + +} +input[type="text"], .inputlike { + background-color: #353545; + border: 3px solid transparent; + color: #dddddd; + font-size: 16px; + border-radius: 10px; + line-height: normal; + padding: 5px; + outline: none; + margin: 5px; + width: 100%; +} +textarea { + resize: none; +} +html, body { + height: 100%; + width: 100%; + margin: 0; + padding: 0; + background-color: #262833; +} +.header { + padding: 10px 0; + border-bottom: #444444 solid 1px; +} +.sidebyside { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; +} +.rightdiv { + display: flex; + flex-direction: row; + justify-content: flex-end; + align-items: center; +} +.coldiv { + margin: 0; + padding: 0 15px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} +.main { + margin: 0; + padding: 20px 0 0 0; + height: calc(100% - 50px); +} +li { + margin: 5px; + padding: 5px; +} +h1 { + font-size: 160%; + font-weight: 100; + margin: 0 0 0 5px; + padding: 0; + display: flex; + align-items: center; +} +h6 { + margin: 0 0 0 3px; + padding: 0; + font-size: small; +} +*{ + color: white; + background-color: #262833; + background: #262833; +} +::-webkit-scrollbar { + width: 5px; + height: 5px; + border-radius: 2px; +} + +::-webkit-scrollbar-track { + background: #353545; + border-radius: 2px; +} + +::-webkit-scrollbar-thumb { + background: #555566; + border-radius: 2px; + border: 2px solid #353545; +} + +::-webkit-scrollbar-thumb:hover { + background: #888899; +} + +::-webkit-scrollbar-corner { + background: transparent; +} +a { + color: #5da5ff; +} +a:hover { + color: #40d8ff; +} +button:hover { + color: #fff; + background-color: #131419; + box-shadow: rgba(0, 0, 0, 0.5) 8px 8px 15px; + transform: translateY(-2px); +} + +button:active { + box-shadow: none; + transform: translateY(0); +} + +button, .buttonlike { + appearance: none; + background-color: rgba(0, 0, 0, 0.25); + border: 3px solid transparent; + border-radius: 10px; + box-sizing: border-box; + color: #ffffff; + cursor: pointer; + font-size: 16px; + line-height: normal; + padding: 5px 10px; + outline: none; + margin: 5px; + width: unset; + text-decoration: none; + transition: all 300ms cubic-bezier(.23, 1, 0.32, 1); + user-select: none; + -webkit-user-select: none; + touch-action: manipulation; + will-change: transform; +}
M web/web.goweb/web.go

@@ -1,53 +1,81 @@

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" - "context" "os/signal" - "errors" - "golang.org/x/sys/unix" "path/filepath" - "git.sophuwu.com/mailboxxer/db" "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: Http(), + Handler: HttpHandle{http.FileServer(http.Dir(db.SAVEPATH))}, } - sigchan := make(chan os.Signal) go func() { - signal.Notify(sigchan, unix.SIGINT, unix.SIGQUIT, unix.SIGKILL, unix.SIGSTOP) - sig := <-sigchan - fmt.Printf("\rgot %s\nstopping server...\n", sig.String()) - err := server.Shutdown(context.Background()) - if err != nil { - fmt.Fprintln(os.Stderr, "error: shutdown:", err) + 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") }() - 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) - } - fmt.Println("stopped") + sig := <-sigchan + fmt.Printf("\rgot %s\nstopping server...\n", sig.String()) + _ = server.Shutdown(context.Background()) } func E(s ...string) []any {

@@ -94,114 +122,122 @@ }

t.Execute(w, dat) } -func Http() http.HandlerFunc { - qu, errr := db.NewQuery(30) - if errr != nil { - fmt.Fprintln(os.Stderr, "Error creating query:", errr) - db.Close() - os.Exit(1) +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 } - return func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/" { - r.ParseForm() - var q []string - if r.Form.Get("to") != "" { - q = append(q, fmt.Sprintf(` toaddr LIKE '%%%s%%'`, r.Form.Get("to"))) - } - if r.Form.Get("from") != "" { - q = append(q, fmt.Sprintf(` fromaddr LIKE '%%%s%%'`, r.Form.Get("from"))) - } - if r.Form.Get("subject") != "" { - q = append(q, fmt.Sprintf(` subject LIKE '%%%s%%'`, r.Form.Get("subject"))) - } - if r.Form.Get("date") != "" { - q = append(q, fmt.Sprintf(` date LIKE '%%%s%%'`, r.Form.Get("date"))) - } - where := func() string { - if len(q) == 0 { - return "" - } - return strings.Join(q, " AND ") - }() - var err error - if where != qu.GetWhere() { - err = qu.SetWhere(where) - if err != nil { - TempErr(w, 500) - return - } - } - err = qu.SetPage(ParseInt(r.Form.Get("page")) - 1) + 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, 500) - return - } - if qu.TotalRows() == 0 { 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) - var htmlMetas []HtmlEM - var from *mail.Address - var to []*mail.Address - var addrlist []string - var htmlMeta HtmlEM - for _, em := range qu.Rows() { - htmlMeta = HtmlEM{ - Id: em.Id, - Date: db.TimeStr(em.Date), - Subject: em.Subject, - } - from, err = mail.ParseAddress(em.From) - if err != nil { - htmlMeta.FromAddr = em.From - } else { - htmlMeta.FromAddr = from.Address - htmlMeta.FromName = from.Name - } - if htmlMeta.FromName == "" { - htmlMeta.FromName = htmlMeta.FromAddr - } - 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.ToAddr = to[0].Address - htmlMeta.ToName = to[0].Name - } - if htmlMeta.ToName == "" { - htmlMeta.ToName = htmlMeta.ToAddr - } - htmlMetas = append(htmlMetas, htmlMeta) - } - dat := map[string]any{ - "ToAddr": r.Form.Get("to"), - "FromAddr": r.Form.Get("from"), - "Subject": r.Form.Get("subject"), - "Date": r.Form.Get("date"), - "Page": qu.Page(), - "TotalPages": qu.TotalPages(), - "HtmlMetas": htmlMetas, + 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) } - t.Execute(w, dat) - return } - 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 - } + 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

@@ -273,3 +309,81 @@ }

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) +}