|
89 | 89 | "gog.email": ( |
90 | 90 | "https://www.googleapis.com/auth/gmail.readonly" |
91 | 91 | " https://www.googleapis.com/auth/gmail.send" |
| 92 | + " https://www.googleapis.com/auth/gmail.modify" |
92 | 93 | ), |
93 | 94 | "gog.calendar": "https://www.googleapis.com/auth/calendar", |
94 | 95 | } |
@@ -449,7 +450,10 @@ def _google_api_request( |
449 | 450 | request.add_header("Content-Type", "application/json") |
450 | 451 | try: |
451 | 452 | with urlopen(request, timeout=30) as resp: # noqa: S310 |
452 | | - return json.loads(resp.read().decode("utf-8")) |
| 453 | + raw_body = resp.read().decode("utf-8").strip() |
| 454 | + if not raw_body: |
| 455 | + return {} |
| 456 | + return json.loads(raw_body) |
453 | 457 | except HTTPError as e: |
454 | 458 | if e.code == 401: |
455 | 459 | raise BridgeError( |
@@ -728,6 +732,18 @@ def _handle_definitions() -> dict[str, Any]: |
728 | 732 | "requires_auth": True, |
729 | 733 | "mutating": True, |
730 | 734 | }, |
| 735 | + { |
| 736 | + "name": "archive_messages", |
| 737 | + "description": "Archive or unarchive one or more messages", |
| 738 | + "requires_auth": True, |
| 739 | + "mutating": True, |
| 740 | + }, |
| 741 | + { |
| 742 | + "name": "update_labels", |
| 743 | + "description": "Add/remove labels on one or more messages", |
| 744 | + "requires_auth": True, |
| 745 | + "mutating": True, |
| 746 | + }, |
731 | 747 | ], |
732 | 748 | }, |
733 | 749 | { |
@@ -1427,6 +1443,20 @@ def _invoke_operation( |
1427 | 1443 | account_ref=account_ref, |
1428 | 1444 | ) |
1429 | 1445 |
|
| 1446 | + if capability_id == "gog.email" and operation == "archive_messages": |
| 1447 | + return _invoke_archive_messages( |
| 1448 | + input_data=input_data, |
| 1449 | + access_token=access_token, |
| 1450 | + account_ref=account_ref, |
| 1451 | + ) |
| 1452 | + |
| 1453 | + if capability_id == "gog.email" and operation == "update_labels": |
| 1454 | + return _invoke_update_labels( |
| 1455 | + input_data=input_data, |
| 1456 | + access_token=access_token, |
| 1457 | + account_ref=account_ref, |
| 1458 | + ) |
| 1459 | + |
1430 | 1460 | if capability_id == "gog.calendar" and operation == "list_events": |
1431 | 1461 | return _invoke_list_events( |
1432 | 1462 | input_data=input_data, |
@@ -1888,6 +1918,157 @@ def _invoke_send_message( |
1888 | 1918 | } |
1889 | 1919 |
|
1890 | 1920 |
|
| 1921 | +def _required_string_list( |
| 1922 | + value: Any, |
| 1923 | + *, |
| 1924 | + field_name: str, |
| 1925 | + max_items: int, |
| 1926 | + allow_empty: bool = False, |
| 1927 | +) -> list[str]: |
| 1928 | + if value is None: |
| 1929 | + values: list[Any] = [] |
| 1930 | + elif isinstance(value, list): |
| 1931 | + values = value |
| 1932 | + else: |
| 1933 | + raise BridgeError("capability_invalid_input", f"{field_name} must be a list") |
| 1934 | + |
| 1935 | + result: list[str] = [] |
| 1936 | + seen: set[str] = set() |
| 1937 | + for item in values: |
| 1938 | + text = _optional_text(item) |
| 1939 | + if not text: |
| 1940 | + raise BridgeError( |
| 1941 | + "capability_invalid_input", |
| 1942 | + f"{field_name} entries must be non-empty strings", |
| 1943 | + ) |
| 1944 | + if text in seen: |
| 1945 | + continue |
| 1946 | + seen.add(text) |
| 1947 | + result.append(text) |
| 1948 | + |
| 1949 | + if not allow_empty and not result: |
| 1950 | + raise BridgeError("capability_invalid_input", f"{field_name} is required") |
| 1951 | + if len(result) > max_items: |
| 1952 | + raise BridgeError( |
| 1953 | + "capability_invalid_input", |
| 1954 | + f"{field_name} must contain at most {max_items} items", |
| 1955 | + ) |
| 1956 | + return result |
| 1957 | + |
| 1958 | + |
| 1959 | +def _gmail_modify_messages( |
| 1960 | + *, |
| 1961 | + access_token: str, |
| 1962 | + ids: list[str], |
| 1963 | + add_label_ids: list[str], |
| 1964 | + remove_label_ids: list[str], |
| 1965 | +) -> None: |
| 1966 | + gmail_base = _google_gmail_api_base_url() |
| 1967 | + payload: dict[str, Any] = {} |
| 1968 | + if add_label_ids: |
| 1969 | + payload["addLabelIds"] = add_label_ids |
| 1970 | + if remove_label_ids: |
| 1971 | + payload["removeLabelIds"] = remove_label_ids |
| 1972 | + |
| 1973 | + if len(ids) == 1: |
| 1974 | + _google_api_request( |
| 1975 | + "POST", |
| 1976 | + f"{gmail_base}/gmail/v1/users/me/messages/{ids[0]}/modify", |
| 1977 | + access_token=access_token, |
| 1978 | + json_body=payload, |
| 1979 | + ) |
| 1980 | + return |
| 1981 | + |
| 1982 | + _google_api_request( |
| 1983 | + "POST", |
| 1984 | + f"{gmail_base}/gmail/v1/users/me/messages/batchModify", |
| 1985 | + access_token=access_token, |
| 1986 | + json_body={**payload, "ids": ids}, |
| 1987 | + ) |
| 1988 | + |
| 1989 | + |
| 1990 | +def _invoke_archive_messages( |
| 1991 | + *, |
| 1992 | + input_data: dict[str, Any], |
| 1993 | + access_token: str, |
| 1994 | + account_ref: str, |
| 1995 | +) -> dict[str, Any]: |
| 1996 | + ids = _required_string_list( |
| 1997 | + input_data.get("ids"), |
| 1998 | + field_name="ids", |
| 1999 | + max_items=100, |
| 2000 | + ) |
| 2001 | + raw_archive = input_data.get("archive", True) |
| 2002 | + if not isinstance(raw_archive, bool): |
| 2003 | + raise BridgeError("capability_invalid_input", "archive must be a boolean") |
| 2004 | + archive = raw_archive |
| 2005 | + |
| 2006 | + add_label_ids = [] if archive else ["INBOX"] |
| 2007 | + remove_label_ids = ["INBOX"] if archive else [] |
| 2008 | + _gmail_modify_messages( |
| 2009 | + access_token=access_token, |
| 2010 | + ids=ids, |
| 2011 | + add_label_ids=add_label_ids, |
| 2012 | + remove_label_ids=remove_label_ids, |
| 2013 | + ) |
| 2014 | + return { |
| 2015 | + "output": { |
| 2016 | + "status": "updated", |
| 2017 | + "archive": archive, |
| 2018 | + "updated_count": len(ids), |
| 2019 | + "ids": ids, |
| 2020 | + "account_ref": account_ref, |
| 2021 | + } |
| 2022 | + } |
| 2023 | + |
| 2024 | + |
| 2025 | +def _invoke_update_labels( |
| 2026 | + *, |
| 2027 | + input_data: dict[str, Any], |
| 2028 | + access_token: str, |
| 2029 | + account_ref: str, |
| 2030 | +) -> dict[str, Any]: |
| 2031 | + ids = _required_string_list( |
| 2032 | + input_data.get("ids"), |
| 2033 | + field_name="ids", |
| 2034 | + max_items=100, |
| 2035 | + ) |
| 2036 | + add_label_ids = _required_string_list( |
| 2037 | + input_data.get("add_label_ids"), |
| 2038 | + field_name="add_label_ids", |
| 2039 | + max_items=100, |
| 2040 | + allow_empty=True, |
| 2041 | + ) |
| 2042 | + remove_label_ids = _required_string_list( |
| 2043 | + input_data.get("remove_label_ids"), |
| 2044 | + field_name="remove_label_ids", |
| 2045 | + max_items=100, |
| 2046 | + allow_empty=True, |
| 2047 | + ) |
| 2048 | + if not add_label_ids and not remove_label_ids: |
| 2049 | + raise BridgeError( |
| 2050 | + "capability_invalid_input", |
| 2051 | + "at least one of add_label_ids or remove_label_ids is required", |
| 2052 | + ) |
| 2053 | + |
| 2054 | + _gmail_modify_messages( |
| 2055 | + access_token=access_token, |
| 2056 | + ids=ids, |
| 2057 | + add_label_ids=add_label_ids, |
| 2058 | + remove_label_ids=remove_label_ids, |
| 2059 | + ) |
| 2060 | + return { |
| 2061 | + "output": { |
| 2062 | + "status": "updated", |
| 2063 | + "updated_count": len(ids), |
| 2064 | + "ids": ids, |
| 2065 | + "add_label_ids": add_label_ids, |
| 2066 | + "remove_label_ids": remove_label_ids, |
| 2067 | + "account_ref": account_ref, |
| 2068 | + } |
| 2069 | + } |
| 2070 | + |
| 2071 | + |
1891 | 2072 | def _invoke_list_events( |
1892 | 2073 | *, |
1893 | 2074 | input_data: dict[str, Any], |
|
0 commit comments