mirror of
https://github.com/GRFreire/nthmail.git
synced 2026-01-09 12:59:38 +00:00
add MIME email rendering
This commits adds support for rendering email in text/html, text/markdown and text/plain inside a MIME/multipart mail. Bluemonday was added as a dependency and initialized but it is still not used because the styling of the email is "discarted" too much. But this needs to be fixed before going to production.
This commit is contained in:
parent
f4407d71f8
commit
b2a85dd3f8
@ -1,8 +1,10 @@
|
||||
package main
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"github.com/russross/blackfriday/v2"
|
||||
)
|
||||
|
||||
templ inbox_body(rcpt_addr string, ms []mail) {
|
||||
templ inbox_body(rcpt_addr string, ms []mail_obj) {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
@ -24,7 +26,59 @@ templ inbox_body(rcpt_addr string, ms []mail) {
|
||||
</html>
|
||||
}
|
||||
|
||||
templ mail_comp(m mail) {
|
||||
<h3>From: <{ m.From_addr }> at { time.Unix(m.Arrived_at, 0).Format(time.DateTime) }</h3>
|
||||
<p>{ string(m.Data) }</p>
|
||||
templ mail_comp(m mail_obj) {
|
||||
<h3>From: { m.From } at { m.Date }</h3>
|
||||
if m.To != "" {
|
||||
<h3>To: { m.To }</h3>
|
||||
}
|
||||
if m.Bcc != "" {
|
||||
<h3>Bcc: { m.Bcc }</h3>
|
||||
}
|
||||
<h3>Subject: { m.Subject }</h3>
|
||||
if m.MediaType == NotMultipart {
|
||||
for _, b := range m.Body {
|
||||
@body_plain(b.Data)
|
||||
}
|
||||
} else {
|
||||
if m.MediaType == Mixed {
|
||||
for _, b := range m.Body {
|
||||
switch b.MimeType {
|
||||
case PlainText:
|
||||
@body_plain(b.Data)
|
||||
case Html:
|
||||
@body_html(b.Data)
|
||||
case Markdown:
|
||||
@body_plain(b.Data)
|
||||
default:
|
||||
@body_plain(b.Data)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
switch m.Body[m.PreferedBodyIndex].MimeType {
|
||||
case PlainText:
|
||||
@body_plain(m.Body[m.PreferedBodyIndex].Data)
|
||||
case Html:
|
||||
@body_html(m.Body[m.PreferedBodyIndex].Data)
|
||||
case Markdown:
|
||||
@body_markdown(m.Body[m.PreferedBodyIndex].Data)
|
||||
default:
|
||||
@body_plain(m.Body[m.PreferedBodyIndex].Data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
templ body_plain(s string) {
|
||||
<p>{ s }</p>
|
||||
}
|
||||
|
||||
templ body_html(s string) {
|
||||
<p>
|
||||
@templ.Raw(s)
|
||||
</p>
|
||||
}
|
||||
|
||||
templ body_markdown(s string) {
|
||||
@body_html(string(blackfriday.Run([]byte(s))))
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,25 +1,225 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"mime/quotedprintable"
|
||||
"net/http"
|
||||
"net/mail"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/GRFreire/nthmail/pkg/rig"
|
||||
"github.com/go-chi/chi"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
)
|
||||
|
||||
type mail struct {
|
||||
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 {
|
||||
From string
|
||||
Date string
|
||||
To string
|
||||
Bcc string
|
||||
Subject string
|
||||
|
||||
Body []mail_body
|
||||
MediaType
|
||||
PreferedBodyIndex int
|
||||
}
|
||||
|
||||
func parse_mail(dbm db_mail, policy *bluemonday.Policy) (mail_obj, error) {
|
||||
var m mail_obj
|
||||
|
||||
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"))
|
||||
|
||||
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
|
||||
m.PreferedBodyIndex = -1
|
||||
for i, b := range m.Body {
|
||||
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, 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 main() {
|
||||
db, err := sql.Open("sqlite3", "./db.db")
|
||||
if err != nil {
|
||||
@ -46,6 +246,11 @@ func main() {
|
||||
http.Redirect(res, req, inbox_addr, 307)
|
||||
})
|
||||
|
||||
p := bluemonday.UGCPolicy()
|
||||
p.AllowStyles()
|
||||
p.AllowStyling()
|
||||
p.AllowElements("style")
|
||||
p.AllowUnsafe(true)
|
||||
router.Get("/{rcpt-addr}", func(res http.ResponseWriter, req *http.Request) {
|
||||
rcpt_addr := chi.URLParam(req, "rcpt-addr")
|
||||
if len(rcpt_addr) == 0 {
|
||||
@ -83,9 +288,9 @@ func main() {
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var mails []mail
|
||||
var mails []mail_obj
|
||||
for rows.Next() {
|
||||
var m mail
|
||||
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)
|
||||
@ -95,7 +300,17 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
mails = append(mails, m)
|
||||
mail_obj, err := parse_mail(m, p)
|
||||
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)
|
||||
|
||||
7
go.mod
7
go.mod
@ -3,9 +3,14 @@ module github.com/GRFreire/nthmail
|
||||
go 1.21.6
|
||||
|
||||
require (
|
||||
github.com/a-h/templ v0.2.543 // indirect
|
||||
github.com/a-h/templ v0.2.598 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 // indirect
|
||||
github.com/emersion/go-smtp v0.20.2 // indirect
|
||||
github.com/go-chi/chi v1.5.5 // indirect
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.26 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
golang.org/x/net v0.22.0 // indirect
|
||||
)
|
||||
|
||||
12
go.sum
12
go.sum
@ -1,10 +1,22 @@
|
||||
github.com/a-h/templ v0.2.543 h1:8YyLvyUtf0/IE2nIwZ62Z/m2o2NqwhnMynzOL78Lzbk=
|
||||
github.com/a-h/templ v0.2.543/go.mod h1:jP908DQCwI08IrnTalhzSEH9WJqG/Q94+EODQcJGFUA=
|
||||
github.com/a-h/templ v0.2.598 h1:6jMIHv6wQZvdPxTuv87erW4RqN/FPU0wk7ZHN5wVuuo=
|
||||
github.com/a-h/templ v0.2.598/go.mod h1:SA7mtYwVEajbIXFRh3vKdYm/4FYyLQAtPH1+KxzGPA8=
|
||||
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/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
|
||||
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||
github.com/emersion/go-smtp v0.20.2 h1:peX42Qnh5Q0q3vrAnRy43R/JwTnnv75AebxbkTL7Ia4=
|
||||
github.com/emersion/go-smtp v0.20.2/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
|
||||
github.com/go-chi/chi v1.5.5 h1:vOB/HbEMt9QqBqErz07QehcOKHaWFtuj87tTDVz2qXE=
|
||||
github.com/go-chi/chi v1.5.5/go.mod h1:C9JqLr3tIYjDOZpzn+BCuxY8z8vmca43EeMgyZt7irw=
|
||||
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-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
|
||||
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
|
||||
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
|
||||
Loading…
Reference in New Issue
Block a user