fix(gmail): expose reply threading metadata
Co-authored-by: Solomon Neas <srneas@gmail.com>
This commit is contained in:
parent
3f46ddf14a
commit
63cfefe144
@ -100,6 +100,71 @@ func containsAll(got []string, want []string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func TestExecute_GmailGet_Metadata_DefaultHeadersIncludeThreading(t *testing.T) {
|
||||
origNew := newGmailService
|
||||
t.Cleanup(func() { newGmailService = origNew })
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !strings.Contains(r.URL.Path, "/gmail/v1/users/me/messages/m1") {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
if got := r.URL.Query().Get("format"); got != "metadata" {
|
||||
t.Errorf("format=%q", got)
|
||||
http.Error(w, "bad format", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
want := []string{
|
||||
"From", "To", "Cc", "Bcc", "Subject", "Date",
|
||||
"Message-ID", "In-Reply-To", "References", "List-Unsubscribe",
|
||||
}
|
||||
if gotHeaders := r.URL.Query()["metadataHeaders"]; !containsAll(gotHeaders, want) {
|
||||
t.Errorf("metadataHeaders=%#v missing one of %v", gotHeaders, want)
|
||||
http.Error(w, "bad metadataHeaders", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"id": "m1",
|
||||
"threadId": "t1",
|
||||
"payload": map[string]any{
|
||||
"headers": []map[string]any{
|
||||
{"name": "Message-ID", "value": "<orig@id>"},
|
||||
{"name": "In-Reply-To", "value": "<parent@id>"},
|
||||
{"name": "References", "value": "<parent@id> <orig@id>"},
|
||||
},
|
||||
},
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
svc, err := gmail.NewService(context.Background(),
|
||||
option.WithoutAuthentication(),
|
||||
option.WithHTTPClient(srv.Client()),
|
||||
option.WithEndpoint(srv.URL+"/"),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("NewService: %v", err)
|
||||
}
|
||||
newGmailService = func(context.Context, string) (*gmail.Service, error) { return svc, nil }
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{
|
||||
"--json",
|
||||
"--account", "a@b.com",
|
||||
"gmail", "get", "m1",
|
||||
"--format", "metadata",
|
||||
}); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
})
|
||||
})
|
||||
if !strings.Contains(out, "<orig@id>") || !strings.Contains(out, "<parent@id>") {
|
||||
t.Fatalf("expected threading headers in metadata JSON, got: %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecute_GmailGet_Raw_JSON(t *testing.T) {
|
||||
origNew := newGmailService
|
||||
t.Cleanup(func() { newGmailService = origNew })
|
||||
|
||||
@ -54,7 +54,10 @@ func (c *GmailGetCmd) Run(ctx context.Context, flags *RootFlags) error {
|
||||
if format == gmailFormatMetadata {
|
||||
headerList := splitCSV(c.Headers)
|
||||
if len(headerList) == 0 {
|
||||
headerList = []string{"From", "To", "Cc", "Bcc", "Subject", "Date"}
|
||||
headerList = []string{
|
||||
"From", "To", "Cc", "Bcc", "Subject", "Date",
|
||||
"Message-ID", "In-Reply-To", "References",
|
||||
}
|
||||
}
|
||||
if !hasHeaderName(headerList, "List-Unsubscribe") {
|
||||
headerList = append(headerList, "List-Unsubscribe")
|
||||
@ -72,12 +75,15 @@ func (c *GmailGetCmd) Run(ctx context.Context, flags *RootFlags) error {
|
||||
// Include a flattened headers map for easier querying
|
||||
// (e.g., jq '.headers.to' instead of complex nested queries)
|
||||
headers := map[string]string{
|
||||
"from": headerValue(msg.Payload, "From"),
|
||||
"to": headerValue(msg.Payload, "To"),
|
||||
"cc": headerValue(msg.Payload, "Cc"),
|
||||
"bcc": headerValue(msg.Payload, "Bcc"),
|
||||
"subject": headerValue(msg.Payload, "Subject"),
|
||||
"date": headerValue(msg.Payload, "Date"),
|
||||
"from": headerValue(msg.Payload, "From"),
|
||||
"to": headerValue(msg.Payload, "To"),
|
||||
"cc": headerValue(msg.Payload, "Cc"),
|
||||
"bcc": headerValue(msg.Payload, "Bcc"),
|
||||
"subject": headerValue(msg.Payload, "Subject"),
|
||||
"date": headerValue(msg.Payload, "Date"),
|
||||
"message_id": headerValue(msg.Payload, "Message-ID"),
|
||||
"in_reply_to": headerValue(msg.Payload, "In-Reply-To"),
|
||||
"references": headerValue(msg.Payload, "References"),
|
||||
}
|
||||
payload := map[string]any{
|
||||
"message": msg,
|
||||
|
||||
@ -451,7 +451,11 @@ func fetchReplyInfo(ctx context.Context, svc *gmail.Service, replyToMessageID st
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return replyInfoFromMessage(msg, includeQuoteBodies), nil
|
||||
info := replyInfoFromMessage(msg, includeQuoteBodies)
|
||||
if info.InReplyTo == "" {
|
||||
return nil, fmt.Errorf("reply target message %s has no Message-ID header; cannot set In-Reply-To/References", replyToMessageID)
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// For thread replies, we always need just headers to select the latest message.
|
||||
|
||||
@ -101,6 +101,40 @@ func TestFetchReplyInfoFromThread(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchReplyInfoNoMessageIDFails(t *testing.T) {
|
||||
origNew := newGmailService
|
||||
t.Cleanup(func() { newGmailService = origNew })
|
||||
|
||||
svc, cleanup := newGmailServiceForTest(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
path := strings.TrimPrefix(r.URL.Path, "/gmail/v1")
|
||||
if r.Method != http.MethodGet || path != "/users/me/messages/m0" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"id": "m0",
|
||||
"threadId": "t0",
|
||||
"payload": map[string]any{
|
||||
"headers": []map[string]any{
|
||||
{"name": "From", "value": "a@example.com"},
|
||||
{"name": "Subject", "value": "no message id here"},
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
defer cleanup()
|
||||
newGmailService = func(context.Context, string) (*gmail.Service, error) { return svc, nil }
|
||||
|
||||
_, err := fetchReplyInfo(context.Background(), svc, "m0", "", false)
|
||||
if err == nil {
|
||||
t.Fatal("expected error when reply target lacks Message-ID")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "Message-ID") {
|
||||
t.Fatalf("expected error to mention Message-ID, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteSendResults_JSON(t *testing.T) {
|
||||
u, err := ui.New(ui.Options{Stdout: io.Discard, Stderr: io.Discard, Color: "never"})
|
||||
if err != nil {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user