diff --git a/internal/httpapi/handlers_projects.go b/internal/httpapi/handlers_projects.go index 9e33017..08bc1cf 100644 --- a/internal/httpapi/handlers_projects.go +++ b/internal/httpapi/handlers_projects.go @@ -507,38 +507,41 @@ func (s *Server) handleListProjectMails(w http.ResponseWriter, r *http.Request) writeJSON(w, http.StatusOK, resp) return } + // The cache holds the COMPANY-WIDE aggregate for the domain; we then filter + // per-viewer below (a member only sees mail they're a party to). + var agg []mailsync.Message if v, ok := s.mailCache.Load(id); ok { if e := v.(*mailCacheEntry); time.Since(e.at) < mailCacheTTL { - resp["messages"] = e.msgs - writeJSON(w, http.StatusOK, resp) - return + agg = e.msgs } } - // impersonation set = project members + PM (the team that corresponds with the client) - var pms []models.ProjectMember - s.db.Where("project_id = ?", id).Find(&pms) - set := map[string]bool{} - for _, m := range pms { - if m.MemberEmail != "" { - set[m.MemberEmail] = true + if agg == nil { + // 전사 모든 구성원 메일함을 impersonate해 도메인 메일을 모은다(구성원은 여러 + // 프로젝트에 걸쳐 있으므로 전사 수집 후 프로젝트 도메인으로 필터하는 편이 단순). + var mailboxes []string + s.db.Model(&models.Member{}).Where("status <> ?", "inactive").Pluck("email", &mailboxes) + msgs, err := s.mail.ListForDomain(r.Context(), mailboxes, p.ClientDomain, 200) + if err != nil { + resp["error"] = err.Error() + } + if msgs == nil { + msgs = []mailsync.Message{} + } + agg = msgs + s.mailCache.Store(id, &mailCacheEntry{at: time.Now(), msgs: agg}) + } + // per-viewer visibility: only mail the requester sent/received/was CC'd on. + me := s.email(r) + visible := make([]mailsync.Message, 0, len(agg)) + for _, m := range agg { + if m.Involves(me) { + visible = append(visible, m) + } + if len(visible) >= 80 { + break } } - if p.PMEmail != "" { - set[p.PMEmail] = true - } - mailboxes := make([]string, 0, len(set)) - for e := range set { - mailboxes = append(mailboxes, e) - } - msgs, err := s.mail.ListForDomain(r.Context(), mailboxes, p.ClientDomain, 60) - if err != nil { - resp["error"] = err.Error() - } - if msgs == nil { - msgs = []mailsync.Message{} - } - resp["messages"] = msgs - s.mailCache.Store(id, &mailCacheEntry{at: time.Now(), msgs: msgs}) + resp["messages"] = visible writeJSON(w, http.StatusOK, resp) } diff --git a/internal/mailsync/mailsync.go b/internal/mailsync/mailsync.go index aefa00e..53e661b 100644 --- a/internal/mailsync/mailsync.go +++ b/internal/mailsync/mailsync.go @@ -47,6 +47,7 @@ type Message struct { ThreadID string `json:"threadId"` From string `json:"from"` To string `json:"to"` + Cc string `json:"cc"` Subject string `json:"subject"` Date string `json:"date"` // raw Date header Snippet string `json:"snippet"` @@ -54,6 +55,16 @@ type Message struct { TS int64 `json:"ts"` // internalDate (ms) for sorting/formatting } +// Involves reports whether the given email appears in From/To/Cc (case-insensitive). +// Used to gate per-viewer visibility: a member only sees mail they're a party to. +func (m Message) Involves(email string) bool { + if email == "" { + return false + } + hay := strings.ToLower(m.From + " " + m.To + " " + m.Cc) + return strings.Contains(hay, strings.ToLower(email)) +} + type tokenEntry struct { token string expires time.Time @@ -187,7 +198,7 @@ func (s *Service) getMeta(ctx context.Context, subject, id string) (Message, err if err != nil { return Message{}, err } - u := fmt.Sprintf("https://gmail.googleapis.com/gmail/v1/users/me/messages/%s?format=metadata&metadataHeaders=From&metadataHeaders=To&metadataHeaders=Subject&metadataHeaders=Date&metadataHeaders=Message-ID", id) + u := fmt.Sprintf("https://gmail.googleapis.com/gmail/v1/users/me/messages/%s?format=metadata&metadataHeaders=From&metadataHeaders=To&metadataHeaders=Cc&metadataHeaders=Subject&metadataHeaders=Date&metadataHeaders=Message-ID", id) req, _ := http.NewRequestWithContext(ctx, http.MethodGet, u, nil) req.Header.Set("Authorization", "Bearer "+tok) resp, err := s.client.Do(req) @@ -220,6 +231,8 @@ func (s *Service) getMeta(ctx context.Context, subject, id string) (Message, err m.From = h.Value case "to": m.To = h.Value + case "cc": + m.Cc = h.Value case "subject": m.Subject = h.Value case "date":