diff --git a/mail-bridge b/mail-bridge index 83c53de..779895d 100755 Binary files a/mail-bridge and b/mail-bridge differ diff --git a/main.go b/main.go index 3dceb71..d50d148 100644 --- a/main.go +++ b/main.go @@ -496,6 +496,12 @@ func handleMessages(w http.ResponseWriter, r *http.Request, accountName string, return } + // Check for /messages/{uid}/ingest + if len(pathParts) > 1 && pathParts[1] == "ingest" { + handleIngestAttachments(w, r, client, folder, uid) + return + } + switch r.Method { case "GET": handleGetMessage(w, r, client, folder, uid) @@ -893,3 +899,127 @@ func handleGetAttachments(w http.ResponseWriter, r *http.Request, client *imapcl w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(attachments) } + +// IngestRequest specifies which attachments to forward to DocSys +type IngestRequest struct { + Attachments []string `json:"attachments"` // filenames to ingest (empty = all) +} + +// IngestResult reports the outcome for each attachment +type IngestResult struct { + Filename string `json:"filename"` + Status string `json:"status"` // "success" or "error" + Message string `json:"message,omitempty"` +} + +func handleIngestAttachments(w http.ResponseWriter, r *http.Request, client *imapclient.Client, folder string, uid uint32) { + if r.Method != "POST" { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // Parse request + var req IngestRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil && err != io.EOF { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // Fetch message with body + uidSet := imap.UIDSetNum(imap.UID(uid)) + options := &imap.FetchOptions{ + UID: true, + Envelope: 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 { + http.Error(w, "No message body", http.StatusInternalServerError) + return + } + + // Get email metadata for DocSys + var from, subject string + if env := buf.Envelope; env != nil { + subject = env.Subject + if len(env.From) > 0 { + f := env.From[0] + if f.Name != "" { + from = fmt.Sprintf("%s <%s@%s>", f.Name, f.Mailbox, f.Host) + } else { + from = fmt.Sprintf("%s@%s", f.Mailbox, f.Host) + } + } + } + + // Extract attachments + raw := buf.BodySection[0].Bytes + attachments := ExtractAttachments(raw) + + if len(attachments) == 0 { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode([]IngestResult{}) + return + } + + // Filter to requested attachments (or all if none specified) + wantedSet := make(map[string]bool) + for _, name := range req.Attachments { + wantedSet[name] = true + } + + var results []IngestResult + for _, att := range attachments { + // Skip if not in requested list (when list is specified) + if len(req.Attachments) > 0 && !wantedSet[att.Filename] { + continue + } + + // POST to DocSys + payload := map[string]string{ + "filename": att.Filename, + "content": att.Content, + "source": "email", + "subject": subject, + "from": from, + } + + payloadBytes, _ := json.Marshal(payload) + resp, err := http.Post("http://localhost:9201/api/ingest", "application/json", bytes.NewReader(payloadBytes)) + + result := IngestResult{Filename: att.Filename} + if err != nil { + result.Status = "error" + result.Message = err.Error() + } else { + defer resp.Body.Close() + if resp.StatusCode >= 400 { + body, _ := io.ReadAll(resp.Body) + result.Status = "error" + result.Message = string(body) + } else { + result.Status = "success" + } + } + results = append(results, result) + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(results) +}