-
Notifications
You must be signed in to change notification settings - Fork 36
Consolidate the reply-prefill readers into one shared helper #384
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+199
−87
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| package mail | ||
|
|
||
| import ( | ||
| "context" | ||
|
|
||
| "github.com/basecamp/hey-sdk/go/pkg/generated" | ||
|
|
||
| hey "github.com/basecamp/hey-sdk/go/pkg/hey" | ||
| ) | ||
|
|
||
| // ReplyRecipients is who a reply goes out to, in HEY's three kinds of addressing. | ||
| type ReplyRecipients struct { | ||
| To []string | ||
| CC []string | ||
| BCC []string | ||
| } | ||
|
|
||
| // ReplyPrefill is how a reply starts out, as HEY prefills it: the "Re: …" subject it | ||
| // goes out under, the sender it goes out as, and who it goes out to. The prefill's | ||
| // quoted content is deliberately not carried: a reply's content is the writer's body | ||
| // alone — the server appends the quoted original at delivery (auto_quoting defaults | ||
| // on), so echoing the prefill's quote back would double it. | ||
| type ReplyPrefill struct { | ||
| Subject string | ||
| ActingSenderID int64 | ||
| Addressed ReplyRecipients | ||
| } | ||
|
|
||
| // ReplyPrefillFromServer asks HEY how a reply to the entry starts out | ||
| // (GET /entries/{id}/replies/new): the "Re: …" subject the reply carries; the sender | ||
| // it goes out as — resolved from the entry's own to and from addresses, so a thread on | ||
| // a shared or alternate address answers as that address, not the account default, and | ||
| // named only when it differs from the acting user; and its recipients — the entry's | ||
| // sender moved onto the To line and the acting user's own addresses, aliases and | ||
| // catch-alls excluded — the exclusion no client can compute locally, and the reason | ||
| // a reply used to be able to CC its writer back to themselves. A false answer sends | ||
| // the caller to its local fallback: a failed read needs one, and so does an empty | ||
| // recipient list — on a thread with yourself, everyone HEY excludes is everyone there | ||
| // is, and the local list is what keeps that reply addressable. The subject and sender | ||
| // are answered even when the recipients are not — only they need the fallback, not | ||
| // what HEY already supplied. | ||
| func ReplyPrefillFromServer(ctx context.Context, client *hey.Client, entryID int64) (ReplyPrefill, bool) { | ||
|
jeremy marked this conversation as resolved.
|
||
| prefilled, err := client.Entries().NewReply(ctx, entryID) | ||
| if err != nil || prefilled == nil { | ||
| return ReplyPrefill{}, false | ||
| } | ||
| prefill := ReplyPrefill{ | ||
| Subject: prefilled.Subject, | ||
| ActingSenderID: prefilled.Sender.Id, | ||
| Addressed: ReplyRecipients{ | ||
| To: contactEmails(prefilled.Addressed.Directly), | ||
| CC: contactEmails(prefilled.Addressed.Copied), | ||
| BCC: contactEmails(prefilled.Addressed.Blindcopied), | ||
| }, | ||
| } | ||
| if len(prefill.Addressed.To)+len(prefill.Addressed.CC)+len(prefill.Addressed.BCC) == 0 { | ||
| prefill.Addressed = ReplyRecipients{} | ||
| return prefill, false | ||
| } | ||
| return prefill, true | ||
| } | ||
|
|
||
| // contactEmails answers the contacts' email addresses verbatim, dropping blanks: the | ||
| // prefill's lists are HEY's own computation, not input to clean up. | ||
| func contactEmails(contacts []generated.Contact) []string { | ||
|
jeremy marked this conversation as resolved.
|
||
| var emails []string | ||
|
jeremy marked this conversation as resolved.
|
||
| for _, contact := range contacts { | ||
| if contact.EmailAddress != "" { | ||
| emails = append(emails, contact.EmailAddress) | ||
| } | ||
| } | ||
| return emails | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| package mail | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "net/http" | ||
| "reflect" | ||
| "testing" | ||
|
|
||
| hey "github.com/basecamp/hey-sdk/go/pkg/hey" | ||
| ) | ||
|
|
||
| // replyPrefillClient answers GET /entries/12/replies/new.json with the given body, the | ||
| // way HEY serves a reply prefill. | ||
| func replyPrefillClient(t *testing.T, prefillJSON string) *hey.Client { | ||
| t.Helper() | ||
| return testClient(t, func(w http.ResponseWriter, r *http.Request) { | ||
| if r.URL.Path != "/entries/12/replies/new.json" { | ||
| t.Errorf("read %s %s, want the entry's reply prefill", r.Method, r.URL.Path) | ||
| http.NotFound(w, r) | ||
| return | ||
| } | ||
| w.Header().Set("Content-Type", "application/json") | ||
| fmt.Fprint(w, prefillJSON) | ||
| }) | ||
| } | ||
|
|
||
| func TestReplyPrefillFromServer(t *testing.T) { | ||
|
jeremy marked this conversation as resolved.
|
||
| client := replyPrefillClient(t, `{ | ||
| "subject": "Re: Weekly sync", "content": "<div>quoted</div>", "is_reply": true, | ||
| "sender": {"id": 215, "name": "Support", "email_address": "support@example.com"}, | ||
| "addressed": { | ||
| "directly": [{"id": 31, "name": "Rick", "email_address": "rick@example.com"}, {"id": 32}], | ||
| "copied": [{"id": 33, "email_address": "cc@example.com"}], | ||
| "blindcopied": [{"id": 34, "email_address": "bcc@example.com"}] | ||
| } | ||
| }`) | ||
|
|
||
| prefill, ok := ReplyPrefillFromServer(context.Background(), client, 12) | ||
| if !ok { | ||
| t.Fatal("an addressed prefill answers; no fallback is needed") | ||
| } | ||
| if prefill.Subject != "Re: Weekly sync" { | ||
| t.Errorf("subject = %q, want the prefilled one", prefill.Subject) | ||
| } | ||
| if prefill.ActingSenderID != 215 { | ||
| t.Errorf("acting sender = %d, want the prefill's 215", prefill.ActingSenderID) | ||
| } | ||
| // The addressless contact is dropped; the rest ride verbatim. The quoted content | ||
| // is not carried at all: HEY appends it at delivery, and echoing it back would | ||
| // double the quote. | ||
| want := ReplyRecipients{ | ||
| To: []string{"rick@example.com"}, | ||
| CC: []string{"cc@example.com"}, | ||
| BCC: []string{"bcc@example.com"}, | ||
| } | ||
| if !reflect.DeepEqual(prefill.Addressed, want) { | ||
| t.Errorf("addressed = %+v, want %+v", prefill.Addressed, want) | ||
| } | ||
| } | ||
|
|
||
| // The subject and sender are answered even when the recipients are not: on a thread | ||
| // with yourself, everyone HEY excludes is everyone there is, and only the recipients | ||
| // need the caller's local fallback. | ||
| func TestReplyPrefillFromServerWithoutRecipients(t *testing.T) { | ||
| client := replyPrefillClient(t, `{"subject": "Re: Weekly sync", | ||
| "sender": {"id": 215, "email_address": "support@example.com"}, "addressed": {}}`) | ||
|
|
||
| prefill, ok := ReplyPrefillFromServer(context.Background(), client, 12) | ||
| if ok { | ||
| t.Fatal("a recipientless prefill sends the caller to its local fallback") | ||
| } | ||
| if prefill.Subject != "Re: Weekly sync" || prefill.ActingSenderID != 215 { | ||
| t.Errorf("subject = %q, sender = %d — both survive an empty recipient list", | ||
| prefill.Subject, prefill.ActingSenderID) | ||
| } | ||
| if !reflect.DeepEqual(prefill.Addressed, ReplyRecipients{}) { | ||
| t.Errorf("addressed = %+v, want none", prefill.Addressed) | ||
| } | ||
| } | ||
|
|
||
| // A read that fails answers nothing: subject, sender and recipients all fall back. | ||
| func TestReplyPrefillFromServerUnreachable(t *testing.T) { | ||
| client := testClient(t, func(w http.ResponseWriter, r *http.Request) { | ||
| w.Header().Set("Content-Type", "application/json") | ||
| http.Error(w, `{"message":"not found"}`, http.StatusNotFound) | ||
| }) | ||
|
|
||
| prefill, ok := ReplyPrefillFromServer(context.Background(), client, 12) | ||
| if ok || !reflect.DeepEqual(prefill, ReplyPrefill{}) { | ||
| t.Errorf("prefill = %+v, ok = %v — an unreachable prefill answers nothing", prefill, ok) | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.