feat(mail): 전사 메일함 수집 + 요청자 참여(from/to/cc) 가시성 필터
All checks were successful
build-and-push / build (push) Successful in 32s
All checks were successful
build-and-push / build (push) Successful in 32s
- 수집 대상을 프로젝트 팀 → 전사 모든 구성원 메일함으로 변경(프로젝트 캐시) - Message.Cc 수집 + Involves(email): 요청자가 수신·참조된 메일만 노출 - 공동 메모는 그대로(프로젝트 구성원 공유) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
751aa8ed97
commit
b4b47e5ed1
@ -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 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 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{}
|
||||
}
|
||||
resp["messages"] = msgs
|
||||
s.mailCache.Store(id, &mailCacheEntry{at: time.Now(), msgs: msgs})
|
||||
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
|
||||
}
|
||||
}
|
||||
resp["messages"] = visible
|
||||
writeJSON(w, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
|
||||
@ -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":
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user