diff --git a/mail-bridge b/mail-bridge index 5572835..83c53de 100755 Binary files a/mail-bridge and b/mail-bridge differ diff --git a/main.go b/main.go index f0296f3..3dceb71 100644 --- a/main.go +++ b/main.go @@ -490,6 +490,12 @@ func handleMessages(w http.ResponseWriter, r *http.Request, accountName string, return } + // Check for /messages/{uid}/attachments + if len(pathParts) > 1 && pathParts[1] == "attachments" { + handleGetAttachments(w, r, client, folder, uid) + return + } + switch r.Method { case "GET": 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") 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) +} diff --git a/mime.go b/mime.go index 440fe89..4970a8a 100644 --- a/mime.go +++ b/mime.go @@ -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 func stripHTML(html string) string { var result strings.Builder