#21: support binary attachment downloads via /api/v1/mail/message/:uid/attachment/:filename in mail API
This commit is contained in:
1 parent
86aad5087b
commit
972189a589
2 files changed
+75
-1
No files matched your search
@@ -110,3 +110,36 @@ func collectParts(entity *message.Entity, out *RenderedMessage) {
|
|||||||
out.HTML = string(body)
|
out.HTML = string(body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExtractAttachment walks the MIME tree and returns bytes and content-type for a named attachment.
|
||||||
|
func ExtractAttachment(entity *message.Entity, targetName string) ([]byte, string) {
|
||||||
|
if multipart := entity.MultipartReader(); multipart != nil {
|
||||||
|
for {
|
||||||
|
part, err := multipart.NextPart()
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if data, ctype := ExtractAttachment(part, targetName); data != nil {
|
||||||
|
return data, ctype
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaType, _, _ := entity.Header.ContentType()
|
||||||
|
disposition, params, err := entity.Header.ContentDisposition()
|
||||||
|
name := params["filename"]
|
||||||
|
if name == "" {
|
||||||
|
_, paramsCT, _ := entity.Header.ContentType()
|
||||||
|
name = paramsCT["name"]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err == nil && disposition == "attachment" && (targetName == "" || strings.EqualFold(name, targetName))) ||
|
||||||
|
(targetName != "" && strings.EqualFold(name, targetName)) {
|
||||||
|
body, err := io.ReadAll(entity.Body)
|
||||||
|
if err == nil {
|
||||||
|
return body, mediaType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, ""
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,12 +1,15 @@
|
|||||||
package httppresentation
|
package httppresentation
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/emersion/go-message"
|
||||||
|
|
||||||
"lambdamail/protocols/internal/application/usecase"
|
"lambdamail/protocols/internal/application/usecase"
|
||||||
"lambdamail/protocols/internal/infrastructure/postgres"
|
"lambdamail/protocols/internal/infrastructure/postgres"
|
||||||
)
|
)
|
||||||
@@ -99,7 +102,14 @@ func (m *mailAPI) handleMessage(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
uid, err := strconv.ParseUint(strings.TrimPrefix(r.URL.Path, "/api/v1/mail/message/"), 10, 32)
|
path := strings.TrimPrefix(r.URL.Path, "/api/v1/mail/message/")
|
||||||
|
parts := strings.Split(path, "/")
|
||||||
|
if len(parts) == 0 || parts[0] == "" {
|
||||||
|
writeError(w, http.StatusBadRequest, "INVALID_UID", "Message UID is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uid, err := strconv.ParseUint(parts[0], 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(w, http.StatusBadRequest, "INVALID_UID", "Message UID must be a number")
|
writeError(w, http.StatusBadRequest, "INVALID_UID", "Message UID must be a number")
|
||||||
return
|
return
|
||||||
@@ -116,12 +126,43 @@ func (m *mailAPI) handleMessage(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle attachment subpath: /api/v1/mail/message/{uid}/attachment/{filename}
|
||||||
|
if len(parts) >= 3 && parts[1] == "attachment" {
|
||||||
|
m.handleAttachmentDownload(w, r, raw, parts[2])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Parsed here rather than in the browser: this service already depends on
|
// Parsed here rather than in the browser: this service already depends on
|
||||||
// a MIME library, and a second parser in TypeScript would be a second set
|
// a MIME library, and a second parser in TypeScript would be a second set
|
||||||
// of edge cases to get wrong.
|
// of edge cases to get wrong.
|
||||||
writeJSON(w, http.StatusOK, usecase.RenderMessage(raw, uint32(uid)))
|
writeJSON(w, http.StatusOK, usecase.RenderMessage(raw, uint32(uid)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *mailAPI) handleAttachmentDownload(w http.ResponseWriter, _ *http.Request, raw []byte, filename string) {
|
||||||
|
entity, err := message.Read(bytes.NewReader(raw))
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusNotFound, "NOT_FOUND", "Attachment not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, contentType := usecase.ExtractAttachment(entity, filename)
|
||||||
|
if data == nil {
|
||||||
|
writeError(w, http.StatusNotFound, "NOT_FOUND", "Attachment not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if contentType == "" {
|
||||||
|
contentType = "application/octet-stream"
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", contentType)
|
||||||
|
w.Header().Set("Content-Disposition", "attachment; filename=\""+filename+"\"")
|
||||||
|
w.Header().Set("Content-Length", strconv.Itoa(len(data)))
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = w.Write(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
func (m *mailAPI) handleSeen(w http.ResponseWriter, r *http.Request) {
|
func (m *mailAPI) handleSeen(w http.ResponseWriter, r *http.Request) {
|
||||||
session, ok := m.authenticate(w, r)
|
session, ok := m.authenticate(w, r)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
Reference in new issue
Block a user