sophuwu.site > mailboxxer
uwu
sophuwu sophie@skisiel.com
Tue, 23 Jul 2024 19:20:14 +0200
commit

9f471806d78bf5b3180d1888ea6dcf1bf1747a1a

parent

12bd57a398624061b1be660c6a7cf450c63ccd0b

5 files changed, 180 insertions(+), 27 deletions(-)

jump to
M cli.gocli.go

@@ -6,14 +6,30 @@ "encoding/base64"

"fmt" "github.com/DusanKasan/parsemail" "github.com/andybalholm/brotli" + "golang.org/x/crypto/ssh/terminal" "golang.org/x/net/html" "io" + "net/mail" + "os" + "os/exec" + "path/filepath" "strings" "time" ) -func TimeStr(t time.Time) string { - t = t.Local() +func GetTSize() (int, int) { + w, h, err := terminal.GetSize(int(os.Stdin.Fd())) + if err != nil { + return 80, 24 + } + return w, h +} + +func TimeStr(s string) string { + t, err := time.Parse(TimeFormat, s) + if err != nil { + return s + } n := time.Now().Local() return strings.ReplaceAll(func() string { if t.Year() != n.Year() {

@@ -27,9 +43,9 @@ if d.Hours() > 24 {

return t.Format("Monday 15:04") } if d.Hours() > 1 { - return fmt.Sprintf("%d hours ago", int(d.Hours())) + return fmt.Sprintf("%d h ago", int(d.Hours())) } - return fmt.Sprintf("%d minutes ago", int(d.Minutes())) + return fmt.Sprintf("%d m ago", int(d.Minutes())) }(), "th", (func() string { if t.Day()%9 < 1 || t.Day()%9 > 3 { return "th"

@@ -38,6 +54,17 @@ return []string{"st", "nd", "rd"}[t.Day()%9-1]

})()) } +func displayAddress(a string) string { + addr, err := mail.ParseAddress(a) + if err != nil { + return a + } + if addr.Name != "" { + return addr.Name + } + return addr.Address +} + func UnBr(buf *bytes.Buffer) error { data := buf.Bytes() buf.Reset()

@@ -46,28 +73,30 @@ _, err := buf.ReadFrom(reader)

return err } -func Show(b []byte) { - e, err := parsemail.Parse(bytes.NewReader(b)) +func Show(b *bytes.Buffer) error { + e, err := parsemail.Parse(b) if err != nil { - fmt.Println(err) - return + return err } + var s string + var ishtml bool if e.HTMLBody != "" { - var b []byte - if b, err = base64.StdEncoding.DecodeString(e.HTMLBody); err == nil { - fmt.Println(RenderHTML(string(b))) - } else { - fmt.Println(RenderHTML(e.HTMLBody)) - } + s = e.HTMLBody + ishtml = true + } else { + s = e.TextBody + ishtml = false } - for _, a := range e.Attachments { - fmt.Println(a.Filename) - fmt.Println(a.ContentType) + var bb []byte + if bb, err = base64.StdEncoding.DecodeString(s); err == nil { + s = string(bb) } - for _, h := range e.EmbeddedFiles { - fmt.Println(h.ContentType) - fmt.Println(h.CID) + if ishtml { + s = RenderHTML(s) } + b.Reset() + b.WriteString(s) + return nil } func node(w io.Writer, n *html.Node) {

@@ -87,8 +116,6 @@ }

// Reset styles after the element switch n.Data { - case "h1", "h2", "p", "br": - fmt.Fprint(w, "\033[0m\n") case "b", "strong", "i", "em": fmt.Fprint(w, "\033[0m") }

@@ -114,3 +141,94 @@ var buff bytes.Buffer

node(&buff, doc) return buff.String() } + +func (em EmailMeta) Display() string { + s := fmt.Sprintf("%13.13s | ", TimeStr(em.Date)) + s += fmt.Sprintf("%40.40s | ", displayAddress(em.From)) + s += fmt.Sprintf("%s", em.Subject) + return s +} + +func DisplayRows(metas []EmailMeta, page int, h int) { + fmt.Println("Page: ", page) + fmt.Printf("id:\t%13.13s | %40.40s | %s\n", "Date", "From", "Subject") + page *= h + for i := page; i < len(metas) && i < page+h; i++ { + fmt.Printf("%d:\t%s\n", i-page, metas[i].Display()) + } +} + +func OpenMail(metas []EmailMeta, page int, h int, s string) error { + n := 0 + for _, r := range s { + if r < '0' || r > '9' { + break + } + n = n*10 + int(r-'0') + } + if !strings.HasPrefix(s, fmt.Sprintf("%d", n)) { + return fmt.Errorf("invalid number") + } + n += page * h + if n >= len(metas) || n < 0 { + return fmt.Errorf("invalid number") + } + id := metas[n].Id + b, err := os.ReadFile(filepath.Join(SAVEPATH, id+".br")) + if err != nil { + return err + } + var email bytes.Buffer + email.Write(b) + if err = UnBr(&email); err != nil { + return err + } + if err = Show(&email); err != nil { + return err + } + cmd := exec.Command("less") + cmd.Stdin = &email + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} + +func CLI(metas *[]EmailMeta) { + // use alternate screen + fmt.Print("\033[?1049h") + defer fmt.Print("\033[?1049l") + page := 0 + var s string + _, h := GetTSize() + h -= 5 + if h > 20 { + h = 20 + } else if h < 5 { + h = 5 + } + for { + // clear screen and move cursor to top + fmt.Print("\033[2J\033[H\r") + if page < 0 { + page = 0 + } + DisplayRows(*metas, page, h) + fmt.Println("n: next, p: previous, q: quit, (id): open") + fmt.Print("(n/p/q/id): ") + fmt.Scanln(&s) + switch s { + case "n": + page++ + case "p": + page-- + case "q": + return + case "": + continue + default: // open mail + if err := OpenMail(*metas, page, h, s); err != nil { + fmt.Println(err) + } + } + } +}
M go.modgo.mod

@@ -14,7 +14,9 @@ github.com/glebarez/go-sqlite v1.22.0 // indirect

github.com/google/uuid v1.5.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + golang.org/x/crypto v0.25.0 // indirect golang.org/x/sys v0.22.0 // indirect + golang.org/x/term v0.22.0 // indirect modernc.org/libc v1.37.6 // indirect modernc.org/mathutil v1.6.0 // indirect modernc.org/memory v1.7.2 // indirect
M go.sumgo.sum

@@ -12,11 +12,15 @@ 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/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= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= +golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= modernc.org/libc v1.37.6 h1:orZH3c5wmhIQFTXF+Nt+eeauyd+ZIt2BX6ARe+kD+aw= modernc.org/libc v1.37.6/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE= modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
M main.gomain.go

@@ -41,6 +41,21 @@ os.MkdirAll(SAVEPATH, 0700)

} } +func ReadRows(rows *sql.Rows) ([]EmailMeta, error) { + var metas []EmailMeta + var meta EmailMeta + var err error + defer rows.Close() + for rows.Next() { + err = rows.Scan(&meta.Id, &meta.Subject, &meta.To, &meta.From, &meta.Date) + if err != nil { + return metas, err + } + metas = append(metas, meta) + } + return metas, nil +} + func main() { db, err := sql.Open("sqlite", DBPATH) if err != nil {

@@ -56,9 +71,9 @@ defer func() {

db.Exec("COMMIT") db.Close() }() - var newEmails []EmailMeta - err = ScanDir(&newEmails) - for _, em := range newEmails { + var metas []EmailMeta + err = ScanDir(&metas) + for _, em := range metas { _, err = db.Exec("INSERT INTO emails (id, subject, toaddr, fromaddr, date) VALUES (?, ?, ?, ?, ?)", em.Id, em.Subject, em.To, em.From, em.Date) if err != nil { fmt.Fprintln(os.Stderr, err)

@@ -69,5 +84,15 @@ if err != nil {

fmt.Fprintln(os.Stderr, err) return } - + rows, err := db.Query("SELECT * FROM emails ORDER BY date DESC") + if err != nil { + fmt.Fprintln(os.Stderr, err) + return + } + metas, err = ReadRows(rows) + if err != nil { + fmt.Fprintln(os.Stderr, err) + return + } + CLI(&metas) }
M parse.goparse.go

@@ -40,6 +40,10 @@ meta, email, err := Parse(filepath.Join(INBOX, f.Name()))

if err != nil { return meta, err } + if meta.Date == "" { + s, _ := f.Info() + meta.Date = s.ModTime().Format(TimeFormat) + } err = SaveEmail(meta, email) if err != nil { return meta, err

@@ -85,7 +89,7 @@ var d time.Time

var err error d, err = e.Date() if err != nil { - d = time.Now().Local() + return "" } return d.Format(TimeFormat) }