Add /ingest endpoint - server-side attachment forwarding to DocSys

This commit is contained in:
Johan Jongsma 2026-02-01 21:38:58 +00:00
parent ed28bb8a64
commit 59ddae7540
2 changed files with 130 additions and 0 deletions

Binary file not shown.

130
main.go
View File

@ -496,6 +496,12 @@ func handleMessages(w http.ResponseWriter, r *http.Request, accountName string,
return return
} }
// Check for /messages/{uid}/ingest
if len(pathParts) > 1 && pathParts[1] == "ingest" {
handleIngestAttachments(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)
@ -893,3 +899,127 @@ func handleGetAttachments(w http.ResponseWriter, r *http.Request, client *imapcl
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(attachments) 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)
}