first commit
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
package webui
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"mailgoserver/internal/db"
|
||||
)
|
||||
|
||||
// emailLogAccessible checks a scoped admin's domain assignment against the sender
|
||||
// domain of an email log's MAIL FROM address — these logs predate per-domain admin
|
||||
// scoping and have no domain_id column, so this is the same text-domain heuristic as
|
||||
// accessibleDomainNames/emailDomain, not a foreign key.
|
||||
func (a *App) emailLogAccessible(r *http.Request, mailFrom string) (bool, error) {
|
||||
names, isGlobal, err := a.accessibleDomainNames(r)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return isGlobal || names[emailDomain(mailFrom)], nil
|
||||
}
|
||||
|
||||
// viewMessageContent mirrors view_message.py's view_message_content().
|
||||
func (a *App) viewMessageContent(w http.ResponseWriter, r *http.Request) {
|
||||
log, err := a.DB.GetEmailLogByID(pathID(r))
|
||||
if err != nil || log == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
if ok, err := a.emailLogAccessible(r, log.MailFrom); err != nil || !ok {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
attachments, _ := a.DB.ListAttachmentsForEmail(log.ID)
|
||||
a.render(w, r, "view_message_content.html", M{"active": "logs", "log": M{
|
||||
"id": log.ID, "mail_from": log.MailFrom, "to_address": log.ToAddress,
|
||||
"cc_addresses": log.CcAddresses, "bcc_addresses": log.BccAddresses,
|
||||
"subject": log.Subject, "created_at": log.CreatedAt, "message_body": log.MessageBody,
|
||||
"email_headers": log.EmailHeaders, "attachments": attachments,
|
||||
}})
|
||||
}
|
||||
|
||||
var extContentType = map[string]string{
|
||||
".txt": "text/plain", ".csv": "text/csv", ".pdf": "application/pdf",
|
||||
".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png", ".gif": "image/gif",
|
||||
".svg": "image/svg+xml", ".html": "text/html", ".htm": "text/html",
|
||||
".json": "application/json", ".xml": "application/xml", ".md": "text/markdown",
|
||||
}
|
||||
|
||||
// downloadAttachment mirrors view_message.py's download_attachment(), including its
|
||||
// CSV-to-HTML-table inline preview special case.
|
||||
func (a *App) downloadAttachment(w http.ResponseWriter, r *http.Request) {
|
||||
att, err := a.DB.GetAttachmentByID(pathID(r))
|
||||
if err != nil || att == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
if !a.attachmentAccessible(w, r, att) {
|
||||
return
|
||||
}
|
||||
if _, err := os.Stat(att.FilePath); err != nil {
|
||||
setFlash(w, "error", "Attachment file not found on disk")
|
||||
http.Redirect(w, r, Prefix+"/logs?type=emails", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
contentType := att.ContentType
|
||||
if contentType == "" || contentType == "application/octet-stream" {
|
||||
if ext := extOfName(att.Filename); ext != "" {
|
||||
if ct, ok := extContentType[ext]; ok {
|
||||
contentType = ct
|
||||
}
|
||||
}
|
||||
}
|
||||
asAttachment := r.URL.Query().Get("download") == "true"
|
||||
|
||||
if contentType == "text/csv" && !asAttachment {
|
||||
data, err := os.ReadFile(att.FilePath)
|
||||
if err != nil {
|
||||
http.Error(w, "read error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.Write([]byte("<table border=1>"))
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
w.Write([]byte("<tr>"))
|
||||
for _, cell := range strings.Split(line, ",") {
|
||||
w.Write([]byte("<td>" + template_htmlEscape(cell) + "</td>"))
|
||||
}
|
||||
w.Write([]byte("</tr>"))
|
||||
}
|
||||
w.Write([]byte("</table>"))
|
||||
return
|
||||
}
|
||||
|
||||
if asAttachment {
|
||||
w.Header().Set("Content-Disposition", `attachment; filename="`+att.Filename+`"`)
|
||||
}
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
http.ServeFile(w, r, att.FilePath)
|
||||
}
|
||||
|
||||
// attachmentAccessible checks the scoped-admin domain restriction against the parent
|
||||
// email log's sender domain; writes 404 and returns false if disallowed.
|
||||
func (a *App) attachmentAccessible(w http.ResponseWriter, r *http.Request, att *db.EmailAttachment) bool {
|
||||
log, err := a.DB.GetEmailLogByID(att.EmailLogID)
|
||||
if err != nil || log == nil {
|
||||
http.NotFound(w, r)
|
||||
return false
|
||||
}
|
||||
if ok, err := a.emailLogAccessible(r, log.MailFrom); err != nil || !ok {
|
||||
http.NotFound(w, r)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func extOfName(filename string) string {
|
||||
if i := strings.LastIndex(filename, "."); i >= 0 {
|
||||
return strings.ToLower(filename[i:])
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func template_htmlEscape(s string) string {
|
||||
r := strings.NewReplacer("&", "&", "<", "<", ">", ">")
|
||||
return r.Replace(s)
|
||||
}
|
||||
|
||||
// deleteAttachment mirrors view_message.py's delete_attachment(): accepts GET or POST.
|
||||
func (a *App) deleteAttachment(w http.ResponseWriter, r *http.Request) {
|
||||
att, err := a.DB.GetAttachmentByID(pathID(r))
|
||||
if err != nil || att == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
if !a.attachmentAccessible(w, r, att) {
|
||||
return
|
||||
}
|
||||
os.Remove(att.FilePath)
|
||||
if err := a.DB.RemoveAttachment(att.ID); err != nil {
|
||||
setFlash(w, "error", "Error deleting attachment")
|
||||
} else {
|
||||
setFlash(w, "success", "Attachment deleted")
|
||||
}
|
||||
http.Redirect(w, r, Prefix+"/logs?type=emails", http.StatusFound)
|
||||
}
|
||||
Reference in New Issue
Block a user