package handler import ( "net/http" "github.com/agentmail/gateway/internal/middleware" "github.com/agentmail/gateway/internal/models" "github.com/agentmail/gateway/internal/repo" "github.com/agentmail/gateway/internal/sse" "github.com/google/uuid" ) // ---------- Contacts(左侧联系人界面,按登录用户隔离) ---------- // GET /api/v1/contacts?archived=false func ListContacts(w http.ResponseWriter, r *http.Request) { user := middleware.GetUser(r) if user == nil { Error(w, http.StatusUnauthorized, "not authenticated") return } archived := r.URL.Query().Get("archived") == "true" // 管理员可用 ?all=true 查看全部 scope := user.Username if user.IsAdmin() && r.URL.Query().Get("all") == "true" { scope = "" } contacts, err := repo.ListContactsFor(r.Context(), scope, archived) if err != nil { Error(w, http.StatusInternalServerError, "Failed to list contacts") return } JSON(w, http.StatusOK, map[string]interface{}{ "contacts": emptySlice(contacts), }) } type archiveRequest struct { Address string `json:"address"` SessionID string `json:"session_id"` } // POST /api/v1/contacts/archive // 归档指定 name@path.session:Agent 侧会话归档 + 邮箱界面移除 func ArchiveContact(w http.ResponseWriter, r *http.Request) { user := middleware.GetUser(r) if user == nil { Error(w, http.StatusUnauthorized, "not authenticated") return } var req archiveRequest if err := Decode(r, &req); err != nil { Error(w, http.StatusBadRequest, "Invalid JSON") return } var sessionID uuid.UUID switch { case req.SessionID != "": id, err := uuid.Parse(req.SessionID) if err != nil { Error(w, http.StatusBadRequest, "Invalid session_id") return } sessionID = id case req.Address != "": addr, err := models.ParseAddress(req.Address) if err != nil { Error(w, http.StatusBadRequest, "Invalid address: "+err.Error()) return } id, err := repo.FindSessionByAddress(r.Context(), addr.Name, addr.Path, addr.Session) if err != nil { Error(w, http.StatusNotFound, "No session matches "+req.Address) return } sessionID = id default: Error(w, http.StatusBadRequest, "Provide address or session_id") return } // 鉴权:只能归档自己参与的会话(管理员不限) allowed, err := repo.UserCanAccessSession(r.Context(), user, sessionID) if err != nil { Error(w, http.StatusInternalServerError, "Failed to check permission") return } if !allowed { Error(w, http.StatusForbidden, "无权归档他人的会话") return } session, err := repo.GetSessionByID(r.Context(), sessionID) if err != nil { Error(w, http.StatusNotFound, "Session not found") return } mails, _ := repo.GetSessionMails(r.Context(), sessionID) if err := repo.ArchiveSession(r.Context(), sessionID); err != nil { Error(w, http.StatusInternalServerError, "Failed to archive session") return } alias := "" if session.Alias != nil { alias = *session.Alias } payload := map[string]interface{}{ "session_id": sessionID.String(), "session_alias": alias, "archived_by": user.Username, } // 通知会话内所有参与方(Agent 与人类),各自归档/移除 notified := map[string]bool{} for _, m := range mails { names := append([]string{m.FromName, m.ToName}, ccNames(m.CCList)...) for _, name := range names { if name == "" || notified[name] { continue } notified[name] = true sse.Default.SendToRecipient(name, "session_archived", payload) } } if !notified[user.Username] { sse.Default.SendToUser(user.Username, "session_archived", payload) } JSON(w, http.StatusOK, map[string]interface{}{ "status": "archived", "session_id": sessionID.String(), "session_alias": alias, }) } // GET /api/v1/contacts/suggest?name=xxx&path=yyy // 三段式补全:无 name 给 Agent+人类用户名;有 name 给工作区;两者都有给会话别名(含 new) func SuggestAddress(w http.ResponseWriter, r *http.Request) { user := middleware.GetUser(r) if user == nil { Error(w, http.StatusUnauthorized, "not authenticated") return } name := r.URL.Query().Get("name") path := r.URL.Query().Get("path") if name == "" { agents, err := repo.ListAgents(r.Context(), "") if err != nil { Error(w, http.StatusInternalServerError, "Failed to list agents") return } users, _ := repo.ListActiveUsernames(r.Context()) names := make([]string, 0, len(agents)+len(users)) for _, a := range agents { names = append(names, a.Name) } for _, u := range users { if u == user.Username { continue // 不建议给自己发信 } names = append(names, u) } JSON(w, http.StatusOK, map[string]interface{}{ "kind": "name", "suggestions": emptySlice(names), }) return } if path == "" { // 人类用户没有工作区,直接给空列表(前端会继续走 session 段) paths, _ := repo.SuggestPaths(r.Context(), name) JSON(w, http.StatusOK, map[string]interface{}{ "kind": "path", "suggestions": emptySlice(paths), }) return } sessions, _ := repo.SuggestSessionsFor(r.Context(), user.Username, name, path) sessions = append(sessions, "new") JSON(w, http.StatusOK, map[string]interface{}{ "kind": "session", "suggestions": sessions, }) } func ccNames(list []models.Address) []string { out := make([]string, 0, len(list)) for _, a := range list { if a.Name != "" { out = append(out, a.Name) } } return out }