mirror of
https://github.com/GRFreire/nthmail.git
synced 2026-01-09 21:09:39 +00:00
refactor web_server code
- Move mail functions to its own packages; - Move bigger router functions to its own functions, and separate router creation from main function; - Edit the algorithm that choses the format will be chosen with regards to format preference to a simples one.
This commit is contained in:
parent
f547eb161e
commit
bf21d05b6d
@ -2,9 +2,10 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/GRFreire/nthmail/pkg/mail_utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
templ inbox_body(rcpt_addr string, ms []mail_obj) {
|
templ inbox_body(rcpt_addr string, ms []mail_utils.Mail_obj) {
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
@ -26,7 +27,7 @@ templ inbox_body(rcpt_addr string, ms []mail_obj) {
|
|||||||
</html>
|
</html>
|
||||||
}
|
}
|
||||||
|
|
||||||
templ mail_comp(m mail_obj, rcpt_addr string) {
|
templ mail_comp(m mail_utils.Mail_obj, rcpt_addr string) {
|
||||||
<a href={ templ.SafeURL(fmt.Sprintf("/%s/%d", rcpt_addr, m.Id)) }>
|
<a href={ templ.SafeURL(fmt.Sprintf("/%s/%d", rcpt_addr, m.Id)) }>
|
||||||
<div>
|
<div>
|
||||||
<p><b>{ m.Subject }</b></p>
|
<p><b>{ m.Subject }</b></p>
|
||||||
|
|||||||
@ -2,9 +2,10 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/russross/blackfriday/v2"
|
"github.com/russross/blackfriday/v2"
|
||||||
|
"github.com/GRFreire/nthmail/pkg/mail_utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
templ mail_body_comp(rcpt_addr string, m mail_obj) {
|
templ mail_body_comp(rcpt_addr string, m mail_utils.Mail_obj) {
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
@ -25,13 +26,13 @@ templ mail_body_comp(rcpt_addr string, m mail_obj) {
|
|||||||
</html>
|
</html>
|
||||||
}
|
}
|
||||||
|
|
||||||
templ mime_type(b mail_body) {
|
templ mime_type(b mail_utils.Mail_body) {
|
||||||
switch b.MimeType {
|
switch b.MimeType {
|
||||||
case Html:
|
case mail_utils.Html:
|
||||||
@body_html(b.Data)
|
@body_html(b.Data)
|
||||||
case Markdown:
|
case mail_utils.Markdown:
|
||||||
@body_markdown(b.Data)
|
@body_markdown(b.Data)
|
||||||
case PlainText:
|
case mail_utils.PlainText:
|
||||||
@body_plain(b.Data)
|
@body_plain(b.Data)
|
||||||
default:
|
default:
|
||||||
@body_plain(b.Data)
|
@body_plain(b.Data)
|
||||||
|
|||||||
@ -1,422 +1,34 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/base64"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"log"
|
"log"
|
||||||
"mime"
|
|
||||||
"mime/multipart"
|
|
||||||
"mime/quotedprintable"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/mail"
|
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
|
|
||||||
|
"github.com/GRFreire/nthmail/pkg/mail_utils"
|
||||||
"github.com/GRFreire/nthmail/pkg/rig"
|
"github.com/GRFreire/nthmail/pkg/rig"
|
||||||
"github.com/go-chi/chi"
|
"github.com/go-chi/chi"
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "github.com/mattn/go-sqlite3"
|
||||||
)
|
)
|
||||||
|
|
||||||
type db_mail struct {
|
|
||||||
Id int
|
|
||||||
Arrived_at int64
|
|
||||||
Rcpt_addr, From_addr string
|
|
||||||
Data []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
type MIMEType uint8
|
|
||||||
type MediaType uint8
|
|
||||||
|
|
||||||
const (
|
|
||||||
PlainText MIMEType = iota
|
|
||||||
Html
|
|
||||||
Markdown
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
NotMultipart MediaType = iota
|
|
||||||
Alternative
|
|
||||||
Mixed
|
|
||||||
)
|
|
||||||
|
|
||||||
type mail_body struct {
|
|
||||||
MimeType MIMEType
|
|
||||||
Data string
|
|
||||||
}
|
|
||||||
|
|
||||||
type mail_obj struct {
|
|
||||||
Id int
|
|
||||||
From string
|
|
||||||
Date string
|
|
||||||
To string
|
|
||||||
Bcc string
|
|
||||||
Subject string
|
|
||||||
|
|
||||||
Body []mail_body
|
|
||||||
MediaType
|
|
||||||
PreferedBodyIndex int
|
|
||||||
}
|
|
||||||
|
|
||||||
func parse_mime_format(s string) (MIMEType, bool) {
|
|
||||||
var t MIMEType
|
|
||||||
|
|
||||||
if s == "" {
|
|
||||||
return t, false
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case strings.Compare(s, "html") == 0:
|
|
||||||
t = Html
|
|
||||||
case strings.Compare(s, "md") == 0:
|
|
||||||
t = Markdown
|
|
||||||
case strings.Compare(s, "text") == 0:
|
|
||||||
t = PlainText
|
|
||||||
default:
|
|
||||||
return t, false
|
|
||||||
}
|
|
||||||
|
|
||||||
return t, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func parse_mail(dbm db_mail, header_only bool) (mail_obj, error) {
|
|
||||||
var m mail_obj
|
|
||||||
m.Id = dbm.Id
|
|
||||||
|
|
||||||
mail_msg, err := mail.ReadMessage(bytes.NewReader(dbm.Data))
|
|
||||||
if err != nil {
|
|
||||||
return m, errors.New("Could not read message")
|
|
||||||
}
|
|
||||||
|
|
||||||
// HEADERS
|
|
||||||
dec := new(mime.WordDecoder)
|
|
||||||
m.From, _ = dec.DecodeHeader(mail_msg.Header.Get("From"))
|
|
||||||
m.Date, _ = dec.DecodeHeader(mail_msg.Header.Get("Date"))
|
|
||||||
m.To, _ = dec.DecodeHeader(mail_msg.Header.Get("To"))
|
|
||||||
m.Bcc, _ = dec.DecodeHeader(mail_msg.Header.Get("Bcc"))
|
|
||||||
m.Subject, _ = dec.DecodeHeader(mail_msg.Header.Get("Subject"))
|
|
||||||
|
|
||||||
if header_only {
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
content_type := mail_msg.Header.Get("Content-Type")
|
|
||||||
mediaType, params, err := mime.ParseMediaType(content_type)
|
|
||||||
if err != nil {
|
|
||||||
return m, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if content_type == "" || !strings.HasPrefix(mediaType, "multipart/") {
|
|
||||||
var txt []byte
|
|
||||||
_, err := mail_msg.Body.Read(txt)
|
|
||||||
if err != nil {
|
|
||||||
return m, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var body mail_body
|
|
||||||
body.MimeType = PlainText
|
|
||||||
body.Data = string(txt)
|
|
||||||
|
|
||||||
m.MediaType = NotMultipart
|
|
||||||
m.Body = append(m.Body, body)
|
|
||||||
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if mediaType == "multipart/mixed" {
|
|
||||||
m.MediaType = Mixed
|
|
||||||
} else if mediaType == "multipart/alternative" {
|
|
||||||
m.MediaType = Alternative
|
|
||||||
} else {
|
|
||||||
return m, errors.New("Not supported multipart type")
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := parse_mail_part(mail_msg.Body, params["boundary"])
|
|
||||||
if err != nil {
|
|
||||||
return m, err
|
|
||||||
}
|
|
||||||
|
|
||||||
m.Body = body
|
|
||||||
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parse_mail_part(mime_data io.Reader, boundary string) ([]mail_body, error) {
|
|
||||||
var body []mail_body
|
|
||||||
|
|
||||||
reader := multipart.NewReader(mime_data, boundary)
|
|
||||||
if reader == nil {
|
|
||||||
return body, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
new_part, err := reader.NextPart()
|
|
||||||
if err == io.EOF {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return body, err
|
|
||||||
}
|
|
||||||
|
|
||||||
mediaType, params, err := mime.ParseMediaType(new_part.Header.Get("Content-Type"))
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return body, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(mediaType, "multipart/") {
|
|
||||||
body_part, err := parse_mail_part(new_part, params["boundary"])
|
|
||||||
if err != nil {
|
|
||||||
return body, err
|
|
||||||
}
|
|
||||||
|
|
||||||
body = append(body, body_part...)
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
part_data, err := io.ReadAll(new_part)
|
|
||||||
if err != nil {
|
|
||||||
return body, err
|
|
||||||
}
|
|
||||||
content_transfer_encoding := new_part.Header.Get("Content-Transfer-Encoding")
|
|
||||||
content_type := new_part.Header.Get("Content-Type")
|
|
||||||
|
|
||||||
var part_body mail_body
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case strings.HasPrefix(content_type, "text/plain"):
|
|
||||||
part_body.MimeType = PlainText
|
|
||||||
case strings.HasPrefix(content_type, "text/markdown"):
|
|
||||||
part_body.MimeType = Markdown
|
|
||||||
case strings.HasPrefix(content_type, "text/html"):
|
|
||||||
part_body.MimeType = Html
|
|
||||||
default:
|
|
||||||
return body, errors.New("Content type not supported: " + content_type)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case strings.Compare(content_transfer_encoding, "BASE64") == 0:
|
|
||||||
decoded_content, err := base64.StdEncoding.DecodeString(string(part_data))
|
|
||||||
if err != nil {
|
|
||||||
return body, err
|
|
||||||
}
|
|
||||||
|
|
||||||
part_body.Data = string(decoded_content)
|
|
||||||
|
|
||||||
case strings.Compare(content_transfer_encoding, "QUOTED-PRINTABLE") == 0:
|
|
||||||
decoded_content, err := io.ReadAll(quotedprintable.NewReader(bytes.NewReader(part_data)))
|
|
||||||
if err != nil {
|
|
||||||
return body, err
|
|
||||||
}
|
|
||||||
|
|
||||||
part_body.Data = string(decoded_content)
|
|
||||||
|
|
||||||
default:
|
|
||||||
part_body.Data = string(part_data)
|
|
||||||
}
|
|
||||||
|
|
||||||
body = append(body, part_body)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return body, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func set_format_index(m mail_obj, format MIMEType, pref bool) mail_obj {
|
|
||||||
m.PreferedBodyIndex = -1
|
|
||||||
for i, b := range m.Body {
|
|
||||||
if pref && b.MimeType == format {
|
|
||||||
m.PreferedBodyIndex = i
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.PreferedBodyIndex == -1 {
|
|
||||||
m.PreferedBodyIndex = i
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.MimeType == Html {
|
|
||||||
m.PreferedBodyIndex = i
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.MimeType == Markdown {
|
|
||||||
if m.Body[i].MimeType != Html {
|
|
||||||
m.PreferedBodyIndex = i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
server := &ServerResouces{}
|
||||||
|
|
||||||
db, err := sql.Open("sqlite3", "./db.db")
|
db, err := sql.Open("sqlite3", "./db.db")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
server.db = db
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
|
|
||||||
domain, exists := os.LookupEnv("MAIL_SERVER_DOMAIN")
|
domain, exists := os.LookupEnv("MAIL_SERVER_DOMAIN")
|
||||||
if !exists {
|
if !exists {
|
||||||
domain = "localhost"
|
domain = "localhost"
|
||||||
}
|
}
|
||||||
|
server.domain = domain
|
||||||
router := chi.NewRouter()
|
|
||||||
|
|
||||||
router.Get("/", func(res http.ResponseWriter, req *http.Request) {
|
|
||||||
page := index_page()
|
|
||||||
page.Render(req.Context(), res)
|
|
||||||
})
|
|
||||||
|
|
||||||
router.Get("/random", func(res http.ResponseWriter, req *http.Request) {
|
|
||||||
inbox_name := rig.GenerateRandomInboxName()
|
|
||||||
inbox_addr := fmt.Sprintf("/%s@%s", inbox_name, domain)
|
|
||||||
|
|
||||||
http.Redirect(res, req, inbox_addr, 307)
|
|
||||||
})
|
|
||||||
|
|
||||||
router.Get("/{rcpt-addr}", func(res http.ResponseWriter, req *http.Request) {
|
|
||||||
rcpt_addr := chi.URLParam(req, "rcpt-addr")
|
|
||||||
if len(rcpt_addr) == 0 {
|
|
||||||
res.WriteHeader(404)
|
|
||||||
res.Write([]byte("inbox not found"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tx, err := db.Begin()
|
|
||||||
if err != nil {
|
|
||||||
res.WriteHeader(500)
|
|
||||||
res.Write([]byte("internal server error"))
|
|
||||||
|
|
||||||
log.Println("could not begin db transaction")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
stmt, err := tx.Prepare("SELECT mails.id, mails.arrived_at, mails.rcpt_addr, mails.from_addr, mails.data FROM mails WHERE mails.rcpt_addr = ?")
|
|
||||||
if err != nil {
|
|
||||||
res.WriteHeader(500)
|
|
||||||
res.Write([]byte("internal server error"))
|
|
||||||
|
|
||||||
log.Println("could not prepare db stmt")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer stmt.Close()
|
|
||||||
|
|
||||||
rows, err := stmt.Query(rcpt_addr)
|
|
||||||
if err != nil {
|
|
||||||
res.WriteHeader(500)
|
|
||||||
res.Write([]byte("internal server error"))
|
|
||||||
|
|
||||||
log.Println("could not query db stmt")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
var mails []mail_obj
|
|
||||||
for rows.Next() {
|
|
||||||
var m db_mail
|
|
||||||
err = rows.Scan(&m.Id, &m.Arrived_at, &m.Rcpt_addr, &m.From_addr, &m.Data)
|
|
||||||
if err != nil {
|
|
||||||
res.WriteHeader(500)
|
|
||||||
res.Write([]byte("internal server error"))
|
|
||||||
|
|
||||||
log.Println("could not scan db row")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
mail_obj, err := parse_mail(m, true)
|
|
||||||
if err != nil {
|
|
||||||
res.WriteHeader(500)
|
|
||||||
res.Write([]byte("internal server error"))
|
|
||||||
|
|
||||||
log.Println("could not parse mail")
|
|
||||||
log.Println(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
mails = append(mails, mail_obj)
|
|
||||||
}
|
|
||||||
|
|
||||||
body := inbox_body(rcpt_addr, mails)
|
|
||||||
body.Render(req.Context(), res)
|
|
||||||
})
|
|
||||||
|
|
||||||
router.Get("/{rcpt-addr}/{mail-id}", func(res http.ResponseWriter, req *http.Request) {
|
|
||||||
rcpt_addr := chi.URLParam(req, "rcpt-addr")
|
|
||||||
if len(rcpt_addr) == 0 {
|
|
||||||
res.WriteHeader(404)
|
|
||||||
res.Write([]byte("inbox not found"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
mail_id := chi.URLParam(req, "mail-id")
|
|
||||||
if len(rcpt_addr) == 0 {
|
|
||||||
res.WriteHeader(404)
|
|
||||||
res.Write([]byte("mail not found"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tx, err := db.Begin()
|
|
||||||
if err != nil {
|
|
||||||
res.WriteHeader(500)
|
|
||||||
res.Write([]byte("internal server error"))
|
|
||||||
|
|
||||||
log.Println("could not begin db transaction")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
stmt, err := tx.Prepare("SELECT mails.id, mails.arrived_at, mails.rcpt_addr, mails.from_addr, mails.data FROM mails WHERE mails.rcpt_addr = ? AND mails.id = ?")
|
|
||||||
if err != nil {
|
|
||||||
res.WriteHeader(500)
|
|
||||||
res.Write([]byte("internal server error"))
|
|
||||||
|
|
||||||
log.Println("could not prepare db stmt")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer stmt.Close()
|
|
||||||
|
|
||||||
row := stmt.QueryRow(rcpt_addr, mail_id)
|
|
||||||
if err != nil {
|
|
||||||
res.WriteHeader(500)
|
|
||||||
res.Write([]byte("internal server error"))
|
|
||||||
|
|
||||||
log.Println("could not query db stmt")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
format, f_pref := parse_mime_format(req.URL.Query().Get("format"))
|
|
||||||
var m db_mail
|
|
||||||
err = row.Scan(&m.Id, &m.Arrived_at, &m.Rcpt_addr, &m.From_addr, &m.Data)
|
|
||||||
if err != nil {
|
|
||||||
res.WriteHeader(500)
|
|
||||||
res.Write([]byte("internal server error"))
|
|
||||||
|
|
||||||
log.Println("could not scan db row")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
mail_obj, err := parse_mail(m, false)
|
|
||||||
if err != nil {
|
|
||||||
res.WriteHeader(500)
|
|
||||||
res.Write([]byte("internal server error"))
|
|
||||||
|
|
||||||
log.Println("could not parse mail")
|
|
||||||
log.Println(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
mail_obj = set_format_index(mail_obj, format, f_pref)
|
|
||||||
|
|
||||||
body := mail_body_comp(rcpt_addr, mail_obj)
|
|
||||||
body.Render(req.Context(), res)
|
|
||||||
})
|
|
||||||
|
|
||||||
var port int
|
var port int
|
||||||
port_str, exists := os.LookupEnv("WEB_SERVER_PORT")
|
port_str, exists := os.LookupEnv("WEB_SERVER_PORT")
|
||||||
@ -429,9 +41,175 @@ func main() {
|
|||||||
port = 3000
|
port = 3000
|
||||||
}
|
}
|
||||||
|
|
||||||
|
router := server.Routes()
|
||||||
|
|
||||||
log.Println("Listening on port", port)
|
log.Println("Listening on port", port)
|
||||||
err = http.ListenAndServe(fmt.Sprintf(":%d", port), router)
|
err = http.ListenAndServe(fmt.Sprintf(":%d", port), router)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ServerResouces struct {
|
||||||
|
db *sql.DB
|
||||||
|
domain string
|
||||||
|
}
|
||||||
|
|
||||||
|
type db_mail struct {
|
||||||
|
Id int
|
||||||
|
Arrived_at int64
|
||||||
|
Rcpt_addr, From_addr string
|
||||||
|
Data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sr ServerResouces) Routes() chi.Router {
|
||||||
|
router := chi.NewRouter()
|
||||||
|
|
||||||
|
router.Get("/", func(res http.ResponseWriter, req *http.Request) {
|
||||||
|
page := index_page()
|
||||||
|
page.Render(req.Context(), res)
|
||||||
|
})
|
||||||
|
|
||||||
|
router.Get("/random", func(res http.ResponseWriter, req *http.Request) {
|
||||||
|
inbox_name := rig.GenerateRandomInboxName()
|
||||||
|
inbox_addr := fmt.Sprintf("/%s@%s", inbox_name, sr.domain)
|
||||||
|
|
||||||
|
http.Redirect(res, req, inbox_addr, 307)
|
||||||
|
})
|
||||||
|
|
||||||
|
router.Get("/{rcpt-addr}", sr.handleInbox)
|
||||||
|
router.Get("/{rcpt-addr}/{mail-id}", sr.handleMail)
|
||||||
|
|
||||||
|
return router
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sr ServerResouces) handleInbox(res http.ResponseWriter, req *http.Request) {
|
||||||
|
rcpt_addr := chi.URLParam(req, "rcpt-addr")
|
||||||
|
if len(rcpt_addr) == 0 {
|
||||||
|
res.WriteHeader(404)
|
||||||
|
res.Write([]byte("inbox not found"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := sr.db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
res.WriteHeader(500)
|
||||||
|
res.Write([]byte("internal server error"))
|
||||||
|
|
||||||
|
log.Println("could not begin db transaction")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
stmt, err := tx.Prepare("SELECT mails.id, mails.arrived_at, mails.rcpt_addr, mails.from_addr, mails.data FROM mails WHERE mails.rcpt_addr = ?")
|
||||||
|
if err != nil {
|
||||||
|
res.WriteHeader(500)
|
||||||
|
res.Write([]byte("internal server error"))
|
||||||
|
|
||||||
|
log.Println("could not prepare db stmt")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
|
||||||
|
rows, err := stmt.Query(rcpt_addr)
|
||||||
|
if err != nil {
|
||||||
|
res.WriteHeader(500)
|
||||||
|
res.Write([]byte("internal server error"))
|
||||||
|
|
||||||
|
log.Println("could not query db stmt")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var mails []mail_utils.Mail_obj
|
||||||
|
for rows.Next() {
|
||||||
|
var m db_mail
|
||||||
|
err = rows.Scan(&m.Id, &m.Arrived_at, &m.Rcpt_addr, &m.From_addr, &m.Data)
|
||||||
|
if err != nil {
|
||||||
|
res.WriteHeader(500)
|
||||||
|
res.Write([]byte("internal server error"))
|
||||||
|
|
||||||
|
log.Println("could not scan db row")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mail_obj, err := mail_utils.Parse_mail(m.Data, true)
|
||||||
|
mail_obj.Id = m.Id
|
||||||
|
if err != nil {
|
||||||
|
res.WriteHeader(500)
|
||||||
|
res.Write([]byte("internal server error"))
|
||||||
|
|
||||||
|
log.Println("could not parse mail")
|
||||||
|
log.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mails = append(mails, mail_obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
body := inbox_body(rcpt_addr, mails)
|
||||||
|
body.Render(req.Context(), res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sr ServerResouces) handleMail(res http.ResponseWriter, req *http.Request) {
|
||||||
|
rcpt_addr := chi.URLParam(req, "rcpt-addr")
|
||||||
|
if len(rcpt_addr) == 0 {
|
||||||
|
res.WriteHeader(404)
|
||||||
|
res.Write([]byte("inbox not found"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mail_id := chi.URLParam(req, "mail-id")
|
||||||
|
if len(rcpt_addr) == 0 {
|
||||||
|
res.WriteHeader(404)
|
||||||
|
res.Write([]byte("mail not found"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := sr.db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
res.WriteHeader(500)
|
||||||
|
res.Write([]byte("internal server error"))
|
||||||
|
|
||||||
|
log.Println("could not begin db transaction")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
stmt, err := tx.Prepare("SELECT mails.id, mails.arrived_at, mails.rcpt_addr, mails.from_addr, mails.data FROM mails WHERE mails.rcpt_addr = ? AND mails.id = ?")
|
||||||
|
if err != nil {
|
||||||
|
res.WriteHeader(500)
|
||||||
|
res.Write([]byte("internal server error"))
|
||||||
|
|
||||||
|
log.Println("could not prepare db stmt")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
|
||||||
|
row := stmt.QueryRow(rcpt_addr, mail_id)
|
||||||
|
|
||||||
|
format, f_pref := mail_utils.Parse_mime_format(req.URL.Query().Get("format"))
|
||||||
|
var m db_mail
|
||||||
|
err = row.Scan(&m.Id, &m.Arrived_at, &m.Rcpt_addr, &m.From_addr, &m.Data)
|
||||||
|
if err != nil {
|
||||||
|
res.WriteHeader(500)
|
||||||
|
res.Write([]byte("internal server error"))
|
||||||
|
|
||||||
|
log.Println("could not scan db row")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mail_obj, err := mail_utils.Parse_mail(m.Data, false)
|
||||||
|
mail_obj.Id = m.Id
|
||||||
|
if err != nil {
|
||||||
|
res.WriteHeader(500)
|
||||||
|
res.Write([]byte("internal server error"))
|
||||||
|
|
||||||
|
log.Println("could not parse mail")
|
||||||
|
log.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mail_obj = mail_utils.Set_format_index(mail_obj, format, f_pref)
|
||||||
|
|
||||||
|
body := mail_body_comp(rcpt_addr, mail_obj)
|
||||||
|
body.Render(req.Context(), res)
|
||||||
|
}
|
||||||
|
|||||||
233
pkg/mail_utils/mail_utils.go
Normal file
233
pkg/mail_utils/mail_utils.go
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
package mail_utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"mime"
|
||||||
|
"mime/multipart"
|
||||||
|
"mime/quotedprintable"
|
||||||
|
"net/mail"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MIMEType uint8
|
||||||
|
type MediaType uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
PlainText MIMEType = iota
|
||||||
|
Html
|
||||||
|
Markdown
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
NotMultipart MediaType = iota
|
||||||
|
Alternative
|
||||||
|
Mixed
|
||||||
|
)
|
||||||
|
|
||||||
|
type Mail_body struct {
|
||||||
|
MimeType MIMEType
|
||||||
|
Data string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Mail_obj struct {
|
||||||
|
Id int
|
||||||
|
From string
|
||||||
|
Date string
|
||||||
|
To string
|
||||||
|
Bcc string
|
||||||
|
Subject string
|
||||||
|
|
||||||
|
Body []Mail_body
|
||||||
|
MediaType
|
||||||
|
PreferedBodyIndex int
|
||||||
|
}
|
||||||
|
|
||||||
|
func Parse_mime_format(s string) (MIMEType, bool) {
|
||||||
|
var t MIMEType
|
||||||
|
|
||||||
|
if s == "" {
|
||||||
|
return t, false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case strings.Compare(s, "html") == 0:
|
||||||
|
t = Html
|
||||||
|
case strings.Compare(s, "md") == 0:
|
||||||
|
t = Markdown
|
||||||
|
case strings.Compare(s, "text") == 0:
|
||||||
|
t = PlainText
|
||||||
|
default:
|
||||||
|
return t, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return t, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func Parse_mail(m_data []byte, header_only bool) (Mail_obj, error) {
|
||||||
|
var m Mail_obj
|
||||||
|
|
||||||
|
mail_msg, err := mail.ReadMessage(bytes.NewReader(m_data))
|
||||||
|
if err != nil {
|
||||||
|
return m, errors.New("Could not read message")
|
||||||
|
}
|
||||||
|
|
||||||
|
// HEADERS
|
||||||
|
dec := new(mime.WordDecoder)
|
||||||
|
m.From, _ = dec.DecodeHeader(mail_msg.Header.Get("From"))
|
||||||
|
m.Date, _ = dec.DecodeHeader(mail_msg.Header.Get("Date"))
|
||||||
|
m.To, _ = dec.DecodeHeader(mail_msg.Header.Get("To"))
|
||||||
|
m.Bcc, _ = dec.DecodeHeader(mail_msg.Header.Get("Bcc"))
|
||||||
|
m.Subject, _ = dec.DecodeHeader(mail_msg.Header.Get("Subject"))
|
||||||
|
|
||||||
|
if header_only {
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
content_type := mail_msg.Header.Get("Content-Type")
|
||||||
|
mediaType, params, err := mime.ParseMediaType(content_type)
|
||||||
|
if err != nil {
|
||||||
|
return m, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if content_type == "" || !strings.HasPrefix(mediaType, "multipart/") {
|
||||||
|
var txt []byte
|
||||||
|
_, err := mail_msg.Body.Read(txt)
|
||||||
|
if err != nil {
|
||||||
|
return m, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var body Mail_body
|
||||||
|
body.MimeType = PlainText
|
||||||
|
body.Data = string(txt)
|
||||||
|
|
||||||
|
m.MediaType = NotMultipart
|
||||||
|
m.Body = append(m.Body, body)
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if mediaType == "multipart/mixed" {
|
||||||
|
m.MediaType = Mixed
|
||||||
|
} else if mediaType == "multipart/alternative" {
|
||||||
|
m.MediaType = Alternative
|
||||||
|
} else {
|
||||||
|
return m, errors.New("Not supported multipart type")
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := Parse_mail_part(mail_msg.Body, params["boundary"])
|
||||||
|
if err != nil {
|
||||||
|
return m, err
|
||||||
|
}
|
||||||
|
|
||||||
|
m.Body = body
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Parse_mail_part(mime_data io.Reader, boundary string) ([]Mail_body, error) {
|
||||||
|
var body []Mail_body
|
||||||
|
|
||||||
|
reader := multipart.NewReader(mime_data, boundary)
|
||||||
|
if reader == nil {
|
||||||
|
return body, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
new_part, err := reader.NextPart()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return body, err
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaType, params, err := mime.ParseMediaType(new_part.Header.Get("Content-Type"))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return body, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(mediaType, "multipart/") {
|
||||||
|
body_part, err := Parse_mail_part(new_part, params["boundary"])
|
||||||
|
if err != nil {
|
||||||
|
return body, err
|
||||||
|
}
|
||||||
|
|
||||||
|
body = append(body, body_part...)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
part_data, err := io.ReadAll(new_part)
|
||||||
|
if err != nil {
|
||||||
|
return body, err
|
||||||
|
}
|
||||||
|
content_transfer_encoding := new_part.Header.Get("Content-Transfer-Encoding")
|
||||||
|
content_type := new_part.Header.Get("Content-Type")
|
||||||
|
|
||||||
|
var part_body Mail_body
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(content_type, "text/plain"):
|
||||||
|
part_body.MimeType = PlainText
|
||||||
|
case strings.HasPrefix(content_type, "text/markdown"):
|
||||||
|
part_body.MimeType = Markdown
|
||||||
|
case strings.HasPrefix(content_type, "text/html"):
|
||||||
|
part_body.MimeType = Html
|
||||||
|
default:
|
||||||
|
return body, errors.New("Content type not supported: " + content_type)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case strings.Compare(content_transfer_encoding, "BASE64") == 0:
|
||||||
|
decoded_content, err := base64.StdEncoding.DecodeString(string(part_data))
|
||||||
|
if err != nil {
|
||||||
|
return body, err
|
||||||
|
}
|
||||||
|
|
||||||
|
part_body.Data = string(decoded_content)
|
||||||
|
|
||||||
|
case strings.Compare(content_transfer_encoding, "QUOTED-PRINTABLE") == 0:
|
||||||
|
decoded_content, err := io.ReadAll(quotedprintable.NewReader(bytes.NewReader(part_data)))
|
||||||
|
if err != nil {
|
||||||
|
return body, err
|
||||||
|
}
|
||||||
|
|
||||||
|
part_body.Data = string(decoded_content)
|
||||||
|
|
||||||
|
default:
|
||||||
|
part_body.Data = string(part_data)
|
||||||
|
}
|
||||||
|
|
||||||
|
body = append(body, part_body)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return body, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Set_format_index(m Mail_obj, format MIMEType, pref bool) Mail_obj {
|
||||||
|
priority := []MIMEType{Html, Markdown, PlainText}
|
||||||
|
|
||||||
|
m.PreferedBodyIndex = -1
|
||||||
|
curr_p := len(priority)
|
||||||
|
for i, b := range m.Body {
|
||||||
|
if pref && format == b.MimeType {
|
||||||
|
m.PreferedBodyIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
p := slices.Index(priority, b.MimeType)
|
||||||
|
if p < curr_p {
|
||||||
|
curr_p = p
|
||||||
|
m.PreferedBodyIndex = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return m
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user