Skip to content

Commit dc3ddbc

Browse files
committed
Handle duplicate Gmail label creation
1 parent d3fa283 commit dc3ddbc

3 files changed

Lines changed: 67 additions & 17 deletions

File tree

inboxanchor/api/v1/routers/frontend.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,11 @@ def _provider_runtime_error_message(provider_name: str, exc: Exception) -> str:
271271
"backend, so the unread scan never completed. Check the backend machine's "
272272
"internet/DNS access, then retry Refresh unread scan."
273273
)
274+
if "gmail.googleapis.com" in lowered and "/labels" in lowered and "409" in lowered:
275+
return (
276+
"Gmail rejected a duplicate label create request. InboxAnchor should refresh its "
277+
"provider label cache and retry instead of failing the workflow."
278+
)
274279
if provider_name == "gmail":
275280
return f"Gmail connected, but InboxAnchor could not fetch unread mail: {message}"
276281
if provider_name in {"imap", "yahoo", "outlook"}:

inboxanchor/connectors/gmail_transport.py

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -595,28 +595,36 @@ def _label_name_to_id(self, label_name: str) -> str:
595595
if label_name in self._label_cache:
596596
return self._label_cache[label_name]
597597

598-
if self._service is not None:
599-
service = self._build_service()
600-
created = self._execute_legacy(
601-
service.users().labels().create(
602-
userId=self.user_id,
603-
body={
598+
try:
599+
if self._service is not None:
600+
service = self._build_service()
601+
created = self._execute_legacy(
602+
service.users().labels().create(
603+
userId=self.user_id,
604+
body={
605+
"name": label_name,
606+
"messageListVisibility": "show",
607+
"labelListVisibility": "labelShow",
608+
},
609+
)
610+
)
611+
else:
612+
created = self._request_json(
613+
"POST",
614+
"labels",
615+
json_body={
604616
"name": label_name,
605617
"messageListVisibility": "show",
606618
"labelListVisibility": "labelShow",
607619
},
608620
)
609-
)
610-
else:
611-
created = self._request_json(
612-
"POST",
613-
"labels",
614-
json_body={
615-
"name": label_name,
616-
"messageListVisibility": "show",
617-
"labelListVisibility": "labelShow",
618-
},
619-
)
621+
except Exception as exc:
622+
if self._is_label_conflict_error(exc):
623+
self._refresh_label_cache()
624+
existing_id = self._label_cache.get(label_name)
625+
if existing_id:
626+
return existing_id
627+
raise
620628
self._label_cache[label_name] = created["id"]
621629
return created["id"]
622630

@@ -655,6 +663,17 @@ def _is_missing_label_error(exc: Exception) -> bool:
655663
return True
656664
return "404" in str(exc)
657665

666+
@staticmethod
667+
def _is_label_conflict_error(exc: Exception) -> bool:
668+
status_code = getattr(getattr(exc, "response", None), "status_code", None)
669+
if status_code == 409:
670+
return True
671+
legacy_status = getattr(getattr(exc, "resp", None), "status", None)
672+
if legacy_status == 409:
673+
return True
674+
lowered = str(exc).lower()
675+
return "409" in lowered
676+
658677
def apply_labels(self, email_ids: list[str], labels: list[str]) -> None:
659678
if not labels:
660679
return

tests/test_gmail_transport.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -631,6 +631,32 @@ def test_google_api_transport_apply_labels_uses_batch_modify_endpoint():
631631
assert session.calls[1][3]["addLabelIds"] == ["Label_1"]
632632

633633

634+
def test_google_api_transport_reuses_existing_label_after_conflict():
635+
session = StubSession(
636+
[
637+
StubResponse({"labels": []}),
638+
StubResponse({}, status_code=409),
639+
StubResponse({"labels": [{"id": "Label_1", "name": "work"}]}),
640+
StubResponse({}),
641+
]
642+
)
643+
transport = GoogleAPITransport(
644+
credentials_path="~/credentials.json",
645+
token_path="~/token.json",
646+
session=session,
647+
)
648+
649+
transport.apply_labels(["msg-1"], ["work"])
650+
651+
assert session.calls[0][0] == "GET"
652+
assert session.calls[1][0] == "POST"
653+
assert session.calls[1][1].endswith("/users/me/labels")
654+
assert session.calls[2][0] == "GET"
655+
assert session.calls[3][0] == "POST"
656+
assert session.calls[3][1].endswith("/users/me/messages/batchModify")
657+
assert session.calls[3][3]["addLabelIds"] == ["Label_1"]
658+
659+
634660
def test_google_api_transport_can_delete_labels_from_gmail():
635661
session = StubSession(
636662
[

0 commit comments

Comments
 (0)