Add attachment download endpoint for DocSys integration

This commit is contained in:
Johan Jongsma 2026-02-01 21:30:50 +00:00
parent 3d2e651a5f
commit ed28bb8a64
3 changed files with 133 additions and 0 deletions

Binary file not shown.

44
main.go
View File

@ -490,6 +490,12 @@ func handleMessages(w http.ResponseWriter, r *http.Request, accountName string,
return return
} }
// Check for /messages/{uid}/attachments
if len(pathParts) > 1 && pathParts[1] == "attachments" {
handleGetAttachments(w, r, client, folder, uid)
return
}
switch r.Method { switch r.Method {
case "GET": case "GET":
handleGetMessage(w, r, client, folder, uid) handleGetMessage(w, r, client, folder, uid)
@ -849,3 +855,41 @@ func handleDeleteMessage(w http.ResponseWriter, r *http.Request, client *imapcli
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "deleted"}) json.NewEncoder(w).Encode(map[string]string{"status": "deleted"})
} }
func handleGetAttachments(w http.ResponseWriter, r *http.Request, client *imapclient.Client, folder string, uid uint32) {
uidSet := imap.UIDSetNum(imap.UID(uid))
options := &imap.FetchOptions{
UID: true,
BodySection: []*imap.FetchItemBodySection{{}},
}
fetchCmd := client.Fetch(uidSet, options)
msgData := fetchCmd.Next()
if msgData == nil {
fetchCmd.Close()
http.Error(w, "Message not found", http.StatusNotFound)
return
}
buf, err := msgData.Collect()
if err != nil {
fetchCmd.Close()
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fetchCmd.Close()
if len(buf.BodySection) == 0 {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode([]Attachment{})
return
}
raw := buf.BodySection[0].Bytes
attachments := ExtractAttachments(raw)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(attachments)
}

89
mime.go
View File

@ -120,6 +120,95 @@ func decodeBody(body []byte, encoding string) []byte {
} }
} }
// Attachment represents an email attachment with content
type Attachment struct {
Filename string `json:"filename"`
ContentType string `json:"content_type"`
Size int `json:"size"`
Content string `json:"content"` // base64 encoded
}
// ExtractAttachments extracts all attachments from raw email bytes
func ExtractAttachments(raw []byte) []Attachment {
attachments := make([]Attachment, 0) // Initialize to empty slice (not nil)
entity, err := message.Read(bytes.NewReader(raw))
if err != nil {
return attachments
}
extractAttachmentParts(entity, &attachments)
return attachments
}
func extractAttachmentParts(entity *message.Entity, attachments *[]Attachment) {
mediaType, params, err := mime.ParseMediaType(entity.Header.Get("Content-Type"))
if err != nil {
return
}
if strings.HasPrefix(mediaType, "multipart/") {
// Multipart message - recurse into parts
mr := entity.MultipartReader()
if mr == nil {
return
}
for {
part, err := mr.NextPart()
if err != nil {
break
}
extractAttachmentParts(part, attachments)
}
} else {
// Check if this is an attachment
disp := entity.Header.Get("Content-Disposition")
if disp == "" {
return
}
dispType, dispParams, err := mime.ParseMediaType(disp)
if err != nil {
return
}
if !strings.EqualFold(dispType, "attachment") && !strings.EqualFold(dispType, "inline") {
return
}
// Get filename
filename := dispParams["filename"]
if filename == "" {
filename = params["name"]
}
if filename == "" {
// Skip attachments without filename
return
}
// Read body
body, err := io.ReadAll(entity.Body)
if err != nil {
return
}
// Decode transfer encoding
encoding := strings.ToLower(entity.Header.Get("Content-Transfer-Encoding"))
decoded := decodeBody(body, encoding)
// Encode as base64 for JSON transport
content := base64.StdEncoding.EncodeToString(decoded)
*attachments = append(*attachments, Attachment{
Filename: filename,
ContentType: mediaType,
Size: len(decoded),
Content: content,
})
}
}
// stripHTML removes HTML tags and returns plain text // stripHTML removes HTML tags and returns plain text
func stripHTML(html string) string { func stripHTML(html string) string {
var result strings.Builder var result strings.Builder