package webui
import (
"fmt"
"html/template"
"io"
"net/http"
"strconv"
"time"
"github.com/jhillyerd/inbucket/pkg/server/web"
"github.com/jhillyerd/inbucket/pkg/storage"
"github.com/jhillyerd/inbucket/pkg/stringutil"
"github.com/jhillyerd/inbucket/pkg/webui/sanitize"
"github.com/rs/zerolog/log"
)
// JSONMessage formats message data for the UI.
type JSONMessage struct {
Mailbox string `json:"mailbox"`
ID string `json:"id"`
From string `json:"from"`
To []string `json:"to"`
Subject string `json:"subject"`
Date time.Time `json:"date"`
PosixMillis int64 `json:"posix-millis"`
Size int64 `json:"size"`
Seen bool `json:"seen"`
Header map[string][]string `json:"header"`
Text string `json:"text"`
HTML string `json:"html"`
Attachments []*JSONAttachment `json:"attachments"`
}
// JSONAttachment formats attachment data for the UI.
type JSONAttachment struct {
ID string `json:"id"`
FileName string `json:"filename"`
ContentType string `json:"content-type"`
}
// MailboxMessage outputs a particular message as JSON for the UI.
func MailboxMessage(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
id := ctx.Vars["id"]
name, err := ctx.Manager.MailboxForAddress(ctx.Vars["name"])
if err != nil {
return err
}
msg, err := ctx.Manager.GetMessage(name, id)
if err != nil && err != storage.ErrNotExist {
return fmt.Errorf("GetMessage(%q) failed: %v", id, err)
}
if msg == nil {
http.NotFound(w, req)
return nil
}
attachParts := msg.Attachments()
attachments := make([]*JSONAttachment, len(attachParts))
for i, part := range attachParts {
attachments[i] = &JSONAttachment{
ID: strconv.Itoa(i),
FileName: part.FileName,
ContentType: part.ContentType,
}
}
// Sanitize HTML body.
htmlBody := ""
if msg.HTML() != "" {
if str, err := sanitize.HTML(msg.HTML()); err == nil {
htmlBody = str
} else {
htmlBody = "Inbucket HTML sanitizer failed."
log.Warn().Str("module", "webui").Str("mailbox", name).Str("id", id).Err(err).
Msg("HTML sanitizer failed")
}
}
return web.RenderJSON(w,
&JSONMessage{
Mailbox: name,
ID: msg.ID,
From: msg.From.String(),
To: stringutil.StringAddressList(msg.To),
Subject: msg.Subject,
Date: msg.Date,
PosixMillis: msg.Date.UnixNano() / 1000000,
Size: msg.Size,
Seen: msg.Seen,
Header: msg.Header(),
Text: web.TextToHTML(msg.Text()),
HTML: htmlBody,
Attachments: attachments,
})
}
// MailboxHTML displays the HTML content of a message. Renders a partial
func MailboxHTML(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
// Don't have to validate these aren't empty, Gorilla returns 404
id := ctx.Vars["id"]
name, err := ctx.Manager.MailboxForAddress(ctx.Vars["name"])
if err != nil {
return err
}
msg, err := ctx.Manager.GetMessage(name, id)
if err == storage.ErrNotExist {
http.NotFound(w, req)
return nil
}
if err != nil {
// This doesn't indicate empty, likely an IO error
return fmt.Errorf("GetMessage(%q) failed: %v", id, err)
}
// Render partial template
w.Header().Set("Content-Type", "text/html; charset=UTF-8")
return web.RenderPartial("mailbox/_html.html", w, map[string]interface{}{
"ctx": ctx,
"name": name,
"message": msg,
"body": template.HTML(msg.HTML()),
})
}
// MailboxSource displays the raw source of a message, including headers. Renders text/plain
func MailboxSource(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
// Don't have to validate these aren't empty, Gorilla returns 404
id := ctx.Vars["id"]
name, err := ctx.Manager.MailboxForAddress(ctx.Vars["name"])
if err != nil {
return err
}
r, err := ctx.Manager.SourceReader(name, id)
if err == storage.ErrNotExist {
http.NotFound(w, req)
return nil
}
if err != nil {
// This doesn't indicate missing, likely an IO error
return fmt.Errorf("SourceReader(%q) failed: %v", id, err)
}
// Output message source
w.Header().Set("Content-Type", "text/plain")
_, err = io.Copy(w, r)
return err
}
// MailboxViewAttach sends the attachment to the client for online viewing
func MailboxViewAttach(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
// Don't have to validate these aren't empty, Gorilla returns 404
name, err := ctx.Manager.MailboxForAddress(ctx.Vars["name"])
if err != nil {
ctx.Session.AddFlash(err.Error(), "errors")
_ = ctx.Session.Save(req, w)
http.Redirect(w, req, web.Reverse("RootIndex"), http.StatusSeeOther)
return nil
}
id := ctx.Vars["id"]
numStr := ctx.Vars["num"]
num, err := strconv.ParseUint(numStr, 10, 32)
if err != nil {
ctx.Session.AddFlash("Attachment number must be unsigned numeric", "errors")
_ = ctx.Session.Save(req, w)
http.Redirect(w, req, web.Reverse("RootIndex"), http.StatusSeeOther)
return nil
}
msg, err := ctx.Manager.GetMessage(name, id)
if err == storage.ErrNotExist {
http.NotFound(w, req)
return nil
}
if err != nil {
// This doesn't indicate empty, likely an IO error
return fmt.Errorf("GetMessage(%q) failed: %v", id, err)
}
if int(num) >= len(msg.Attachments()) {
ctx.Session.AddFlash("Attachment number too high", "errors")
_ = ctx.Session.Save(req, w)
http.Redirect(w, req, web.Reverse("RootIndex"), http.StatusSeeOther)
return nil
}
// Output attachment
part := msg.Attachments()[num]
w.Header().Set("Content-Type", part.ContentType)
_, err = w.Write(part.Content)
return err
}