// Package mailimportapi stellt die Pflege von Postfaechern (INT-03), // Mail-Regeln/Anhangfiltern (INT-04) und dem Klaerungskorb ueber HTTP // bereit. Keine eigene Fachlogik, nur duenne Wrapper um internal/mailimport. package mailimportapi import ( "context" "encoding/json" "errors" "net/http" "strconv" "strings" "time" "gitea.perlbach24.de/scripte/nexarch/dms/internal/documenttype" "gitea.perlbach24.de/scripte/nexarch/dms/internal/mailimport" "gitea.perlbach24.de/scripte/nexarch/dms/internal/moduleauth" "gitea.perlbach24.de/scripte/nexarch/dms/internal/tag" ) type Server struct { store *mailimport.Store dialer mailimport.Dialer } func NewServer(store *mailimport.Store) *Server { return &Server{store: store} } func (s *Server) RegisterRoutes(mux *http.ServeMux) { mux.HandleFunc("GET /mail-accounts", s.handleListAccounts) mux.HandleFunc("POST /mail-accounts", s.handleCreateAccount) mux.HandleFunc("GET /mail-accounts/{id}", s.handleGetAccount) mux.HandleFunc("PUT /mail-accounts/{id}", s.handleUpdateAccount) mux.HandleFunc("POST /mail-accounts/{id}/test", s.handleTestAccount) mux.HandleFunc("GET /mail-rules", s.handleListRules) mux.HandleFunc("POST /mail-rules", s.handleCreateRule) mux.HandleFunc("DELETE /mail-rules/{id}", s.handleDeleteRule) mux.HandleFunc("GET /mail-filters", s.handleListFilters) mux.HandleFunc("POST /mail-filters", s.handleCreateFilter) mux.HandleFunc("DELETE /mail-filters/{id}", s.handleDeleteFilter) mux.HandleFunc("GET /clarification", s.handleListClarification) mux.HandleFunc("POST /clarification/{id}/apply", s.handleApplyClarification) } func writeJSON(w http.ResponseWriter, status int, v any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(v) } func writeError(w http.ResponseWriter, status int, msg string) { writeJSON(w, status, map[string]string{"error": msg}) } // actor liefert die Kennung fuer Audit-/Berechtigungszwecke. Mit Modul-Token // gilt allein dessen Nutzer; der Interims-Parameter "caller" bleibt Fallback. func actor(r *http.Request) string { if id, ok := moduleauth.IdentityFrom(r.Context()); ok && id.UserID != "" { return id.UserID } return strings.TrimSpace(r.URL.Query().Get("caller")) } func requireActor(w http.ResponseWriter, r *http.Request) (string, bool) { a := actor(r) if a == "" { writeError(w, http.StatusUnauthorized, "nicht angemeldet") return "", false } return a, true } func failMail(w http.ResponseWriter, err error) { switch { case errors.Is(err, mailimport.ErrNotFound), errors.Is(err, mailimport.ErrRuleNotFound): writeError(w, http.StatusNotFound, err.Error()) case err != nil: writeError(w, http.StatusUnprocessableEntity, err.Error()) } } // --- Postfaecher: das Kennwort verlaesst den Server nie in einer Antwort. --- type accountDTO struct { ID string `json:"id,omitempty"` Name string `json:"name"` Host string `json:"host"` Port int `json:"port"` TLS string `json:"tls"` Username string `json:"username"` Password string `json:"password,omitempty"` // nur beim Schreiben gesetzt, nie in Antworten befuellt Mailbox string `json:"mailbox"` FolderID *string `json:"folder_id"` ImportUserID string `json:"import_user_id"` PollSeconds int `json:"poll_interval_seconds"` Enabled bool `json:"enabled"` } func toAccountDTO(a mailimport.Account) accountDTO { return accountDTO{ ID: a.ID, Name: a.Name, Host: a.Host, Port: a.Port, TLS: string(a.TLS), Username: a.Username, Mailbox: a.Mailbox, FolderID: a.FolderID, ImportUserID: a.ImportUserID, PollSeconds: int(a.PollInterval / time.Second), Enabled: a.Enabled, } } func (s *Server) handleListAccounts(w http.ResponseWriter, r *http.Request) { if _, ok := requireActor(w, r); !ok { return } accounts, err := s.store.List(r.Context()) if err != nil { failMail(w, err) return } out := make([]accountDTO, len(accounts)) for i, a := range accounts { out[i] = toAccountDTO(a) } writeJSON(w, http.StatusOK, out) } func (s *Server) handleGetAccount(w http.ResponseWriter, r *http.Request) { if _, ok := requireActor(w, r); !ok { return } a, err := s.store.Get(r.Context(), r.PathValue("id")) if err != nil { failMail(w, err) return } writeJSON(w, http.StatusOK, toAccountDTO(*a)) } func (s *Server) handleCreateAccount(w http.ResponseWriter, r *http.Request) { if _, ok := requireActor(w, r); !ok { return } var d accountDTO if err := json.NewDecoder(r.Body).Decode(&d); err != nil { writeError(w, http.StatusBadRequest, "ungueltiger request-body: "+err.Error()) return } id, err := s.store.Create(r.Context(), mailimport.NewAccount{ Name: d.Name, Host: d.Host, Port: d.Port, TLS: mailimport.TLSMode(d.TLS), Username: d.Username, Password: d.Password, Mailbox: d.Mailbox, FolderID: d.FolderID, ImportUserID: d.ImportUserID, PollInterval: time.Duration(d.PollSeconds) * time.Second, }) if err != nil { failMail(w, err) return } a, err := s.store.Get(r.Context(), id) if err != nil { failMail(w, err) return } writeJSON(w, http.StatusCreated, toAccountDTO(*a)) } func (s *Server) handleUpdateAccount(w http.ResponseWriter, r *http.Request) { if _, ok := requireActor(w, r); !ok { return } var d accountDTO if err := json.NewDecoder(r.Body).Decode(&d); err != nil { writeError(w, http.StatusBadRequest, "ungueltiger request-body: "+err.Error()) return } id := r.PathValue("id") if err := s.store.Update(r.Context(), id, mailimport.AccountUpdate{ Name: d.Name, Host: d.Host, Port: d.Port, TLS: mailimport.TLSMode(d.TLS), Username: d.Username, Password: d.Password, Mailbox: d.Mailbox, FolderID: d.FolderID, ImportUserID: d.ImportUserID, PollInterval: time.Duration(d.PollSeconds) * time.Second, }); err != nil { failMail(w, err) return } if err := s.store.SetEnabled(r.Context(), id, d.Enabled); err != nil { failMail(w, err) return } a, err := s.store.Get(r.Context(), id) if err != nil { failMail(w, err) return } writeJSON(w, http.StatusOK, toAccountDTO(*a)) } func (s *Server) handleTestAccount(w http.ResponseWriter, r *http.Request) { if _, ok := requireActor(w, r); !ok { return } err := s.store.TestConnection(r.Context(), s.dialer, r.PathValue("id")) if err != nil { if errors.Is(err, mailimport.ErrNotFound) { writeError(w, http.StatusNotFound, err.Error()) return } writeJSON(w, http.StatusOK, map[string]any{"ok": false, "error": err.Error()}) return } writeJSON(w, http.StatusOK, map[string]any{"ok": true}) } // --- Regeln und Anhangfilter (MET-05-Muster, siehe autoruleapi) --- type ruleDTO struct { ID string `json:"id,omitempty"` Name string `json:"name"` Priority int `json:"priority"` Enabled bool `json:"enabled"` AccountID *string `json:"account_id"` SenderPattern *string `json:"sender_pattern"` SubjectPattern *string `json:"subject_pattern"` AttachmentMime *string `json:"attachment_mime"` AttachmentFilenamePattern *string `json:"attachment_filename_pattern"` AddTagIDs []string `json:"add_tag_ids"` DocumentTypeID *string `json:"document_type_id"` FolderID *string `json:"folder_id"` StopProcessing bool `json:"stop_processing"` } func toRuleDTO(r mailimport.Rule) ruleDTO { return ruleDTO{r.ID, r.Name, r.Priority, r.Enabled, r.AccountID, r.SenderPattern, r.SubjectPattern, r.AttachmentMime, r.AttachmentFilenamePattern, r.AddTagIDs, r.DocumentTypeID, r.FolderID, r.StopProcessing} } func (d ruleDTO) rule() mailimport.Rule { return mailimport.Rule{Name: d.Name, Priority: d.Priority, Enabled: d.Enabled, AccountID: d.AccountID, SenderPattern: d.SenderPattern, SubjectPattern: d.SubjectPattern, AttachmentMime: d.AttachmentMime, AttachmentFilenamePattern: d.AttachmentFilenamePattern, AddTagIDs: d.AddTagIDs, DocumentTypeID: d.DocumentTypeID, FolderID: d.FolderID, StopProcessing: d.StopProcessing} } func (s *Server) handleListRules(w http.ResponseWriter, r *http.Request) { if _, ok := requireActor(w, r); !ok { return } rules, err := mailimport.ListRules(r.Context(), s.store.Pool) if err != nil { failMail(w, err) return } out := make([]ruleDTO, len(rules)) for i, rule := range rules { out[i] = toRuleDTO(rule) } writeJSON(w, http.StatusOK, out) } func (s *Server) handleCreateRule(w http.ResponseWriter, r *http.Request) { a, ok := requireActor(w, r) if !ok { return } var d ruleDTO if err := json.NewDecoder(r.Body).Decode(&d); err != nil { writeError(w, http.StatusBadRequest, "ungueltiger request-body: "+err.Error()) return } rule, err := mailimport.CreateRule(r.Context(), s.store.Pool, a, d.rule()) if err != nil { failMail(w, err) return } writeJSON(w, http.StatusCreated, toRuleDTO(rule)) } func (s *Server) handleDeleteRule(w http.ResponseWriter, r *http.Request) { a, ok := requireActor(w, r) if !ok { return } if err := mailimport.DeleteRule(r.Context(), s.store.Pool, a, r.PathValue("id")); err != nil { failMail(w, err) return } w.WriteHeader(http.StatusNoContent) } type filterDTO struct { ID string `json:"id,omitempty"` Name string `json:"name"` Enabled bool `json:"enabled"` AccountID *string `json:"account_id"` FilenamePattern *string `json:"filename_pattern"` MimePrefix *string `json:"mime_prefix"` MaxSizeBytes *int64 `json:"max_size_bytes"` } func toFilterDTO(f mailimport.Filter) filterDTO { return filterDTO{f.ID, f.Name, f.Enabled, f.AccountID, f.FilenamePattern, f.MimePrefix, f.MaxSizeBytes} } func (d filterDTO) filter() mailimport.Filter { return mailimport.Filter{Name: d.Name, Enabled: d.Enabled, AccountID: d.AccountID, FilenamePattern: d.FilenamePattern, MimePrefix: d.MimePrefix, MaxSizeBytes: d.MaxSizeBytes} } func (s *Server) handleListFilters(w http.ResponseWriter, r *http.Request) { if _, ok := requireActor(w, r); !ok { return } filters, err := mailimport.ListFilters(r.Context(), s.store.Pool) if err != nil { failMail(w, err) return } out := make([]filterDTO, len(filters)) for i, f := range filters { out[i] = toFilterDTO(f) } writeJSON(w, http.StatusOK, out) } func (s *Server) handleCreateFilter(w http.ResponseWriter, r *http.Request) { a, ok := requireActor(w, r) if !ok { return } var d filterDTO if err := json.NewDecoder(r.Body).Decode(&d); err != nil { writeError(w, http.StatusBadRequest, "ungueltiger request-body: "+err.Error()) return } f, err := mailimport.CreateFilter(r.Context(), s.store.Pool, a, d.filter()) if err != nil { failMail(w, err) return } writeJSON(w, http.StatusCreated, toFilterDTO(f)) } func (s *Server) handleDeleteFilter(w http.ResponseWriter, r *http.Request) { a, ok := requireActor(w, r) if !ok { return } if err := mailimport.DeleteFilter(r.Context(), s.store.Pool, a, r.PathValue("id")); err != nil { failMail(w, err) return } w.WriteHeader(http.StatusNoContent) } // --- Klaerungskorb --- type clarificationDTO struct { MessageID string `json:"message_id"` AccountID string `json:"account_id"` From string `json:"from"` Subject string `json:"subject"` Reason string `json:"reason"` Documents []string `json:"documents"` } func (s *Server) handleListClarification(w http.ResponseWriter, r *http.Request) { if _, ok := requireActor(w, r); !ok { return } limit := 100 if v := r.URL.Query().Get("limit"); v != "" { if n, err := strconv.Atoi(v); err == nil { limit = n } } items, err := mailimport.ListClarification(r.Context(), s.store.Pool, limit) if err != nil { failMail(w, err) return } out := make([]clarificationDTO, len(items)) for i, it := range items { out[i] = clarificationDTO{it.MessageID, it.AccountID, it.From, it.Subject, it.Reason, it.Documents} } writeJSON(w, http.StatusOK, out) } type applyClarificationRequest struct { DocumentTypeID *string `json:"document_type_id"` ConfirmTypeChange bool `json:"confirm_type_change"` AddTagIDs []string `json:"add_tag_ids"` FolderID *string `json:"folder_id"` } func (s *Server) handleApplyClarification(w http.ResponseWriter, r *http.Request) { if _, ok := requireActor(w, r); !ok { return } var d applyClarificationRequest if err := json.NewDecoder(r.Body).Decode(&d); err != nil { writeError(w, http.StatusBadRequest, "ungueltiger request-body: "+err.Error()) return } changeType := func(ctx context.Context, documentID, newTypeID string, confirm bool) error { _, err := documenttype.ChangeDocumentType(ctx, s.store.Pool, documentID, newTypeID, confirm) return err } assign := func(ctx context.Context, documentID, tagID string) error { return tag.Assign(ctx, s.store.Pool, documentID, tagID) } err := mailimport.ApplyClarification(r.Context(), s.store.Pool, r.PathValue("id"), d.DocumentTypeID, d.ConfirmTypeChange, d.AddTagIDs, d.FolderID, changeType, assign) if err != nil { var dataLoss *documenttype.ErrDataLossNotConfirmed var missing *documenttype.ErrMissingRequiredFields switch { case errors.As(err, &dataLoss): writeError(w, http.StatusConflict, err.Error()) return case errors.As(err, &missing): writeError(w, http.StatusUnprocessableEntity, err.Error()) return } failMail(w, err) return } w.WriteHeader(http.StatusNoContent) }