universal-mcp-applications 0.1.32__py3-none-any.whl → 0.1.36rc2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- universal_mcp/applications/ahrefs/app.py +52 -198
- universal_mcp/applications/airtable/app.py +23 -122
- universal_mcp/applications/apollo/app.py +111 -464
- universal_mcp/applications/asana/app.py +417 -1567
- universal_mcp/applications/aws_s3/app.py +36 -103
- universal_mcp/applications/bill/app.py +546 -1957
- universal_mcp/applications/box/app.py +1068 -3981
- universal_mcp/applications/braze/app.py +364 -1430
- universal_mcp/applications/browser_use/app.py +2 -8
- universal_mcp/applications/cal_com_v2/app.py +207 -625
- universal_mcp/applications/calendly/app.py +61 -200
- universal_mcp/applications/canva/app.py +45 -110
- universal_mcp/applications/clickup/app.py +207 -674
- universal_mcp/applications/coda/app.py +146 -426
- universal_mcp/applications/confluence/app.py +310 -1098
- universal_mcp/applications/contentful/app.py +36 -151
- universal_mcp/applications/crustdata/app.py +28 -107
- universal_mcp/applications/dialpad/app.py +283 -756
- universal_mcp/applications/digitalocean/app.py +1766 -5777
- universal_mcp/applications/domain_checker/app.py +3 -54
- universal_mcp/applications/e2b/app.py +14 -64
- universal_mcp/applications/elevenlabs/app.py +9 -47
- universal_mcp/applications/exa/app.py +6 -17
- universal_mcp/applications/falai/app.py +24 -101
- universal_mcp/applications/figma/app.py +53 -137
- universal_mcp/applications/file_system/app.py +2 -13
- universal_mcp/applications/firecrawl/app.py +51 -152
- universal_mcp/applications/fireflies/app.py +59 -281
- universal_mcp/applications/fpl/app.py +91 -528
- universal_mcp/applications/fpl/utils/fixtures.py +15 -49
- universal_mcp/applications/fpl/utils/helper.py +25 -89
- universal_mcp/applications/fpl/utils/league_utils.py +20 -64
- universal_mcp/applications/ghost_content/app.py +52 -161
- universal_mcp/applications/github/app.py +19 -56
- universal_mcp/applications/gong/app.py +88 -248
- universal_mcp/applications/google_calendar/app.py +16 -68
- universal_mcp/applications/google_docs/app.py +85 -189
- universal_mcp/applications/google_drive/app.py +141 -463
- universal_mcp/applications/google_gemini/app.py +12 -64
- universal_mcp/applications/google_mail/app.py +28 -157
- universal_mcp/applications/google_searchconsole/app.py +15 -48
- universal_mcp/applications/google_sheet/app.py +100 -581
- universal_mcp/applications/google_sheet/helper.py +10 -37
- universal_mcp/applications/hashnode/app.py +57 -269
- universal_mcp/applications/heygen/app.py +44 -122
- universal_mcp/applications/http_tools/app.py +10 -32
- universal_mcp/applications/hubspot/api_segments/crm_api.py +460 -1573
- universal_mcp/applications/hubspot/api_segments/marketing_api.py +74 -262
- universal_mcp/applications/hubspot/app.py +23 -87
- universal_mcp/applications/jira/app.py +2071 -7986
- universal_mcp/applications/klaviyo/app.py +494 -1376
- universal_mcp/applications/linkedin/README.md +9 -2
- universal_mcp/applications/linkedin/app.py +240 -181
- universal_mcp/applications/mailchimp/app.py +450 -1605
- universal_mcp/applications/markitdown/app.py +8 -20
- universal_mcp/applications/miro/app.py +217 -699
- universal_mcp/applications/ms_teams/app.py +64 -186
- universal_mcp/applications/neon/app.py +86 -192
- universal_mcp/applications/notion/app.py +21 -36
- universal_mcp/applications/onedrive/app.py +16 -38
- universal_mcp/applications/openai/app.py +42 -165
- universal_mcp/applications/outlook/app.py +24 -84
- universal_mcp/applications/perplexity/app.py +4 -19
- universal_mcp/applications/pipedrive/app.py +832 -3142
- universal_mcp/applications/posthog/app.py +163 -432
- universal_mcp/applications/reddit/app.py +40 -139
- universal_mcp/applications/resend/app.py +41 -107
- universal_mcp/applications/retell/app.py +14 -41
- universal_mcp/applications/rocketlane/app.py +221 -934
- universal_mcp/applications/scraper/README.md +7 -4
- universal_mcp/applications/scraper/app.py +50 -109
- universal_mcp/applications/semanticscholar/app.py +22 -64
- universal_mcp/applications/semrush/app.py +43 -77
- universal_mcp/applications/sendgrid/app.py +512 -1262
- universal_mcp/applications/sentry/app.py +271 -906
- universal_mcp/applications/serpapi/app.py +40 -143
- universal_mcp/applications/sharepoint/app.py +17 -39
- universal_mcp/applications/shopify/app.py +1551 -4287
- universal_mcp/applications/shortcut/app.py +155 -417
- universal_mcp/applications/slack/app.py +33 -115
- universal_mcp/applications/spotify/app.py +126 -325
- universal_mcp/applications/supabase/app.py +104 -213
- universal_mcp/applications/tavily/app.py +1 -1
- universal_mcp/applications/trello/app.py +693 -2656
- universal_mcp/applications/twilio/app.py +14 -50
- universal_mcp/applications/twitter/api_segments/compliance_api.py +4 -14
- universal_mcp/applications/twitter/api_segments/dm_conversations_api.py +6 -18
- universal_mcp/applications/twitter/api_segments/likes_api.py +1 -3
- universal_mcp/applications/twitter/api_segments/lists_api.py +5 -15
- universal_mcp/applications/twitter/api_segments/trends_api.py +1 -3
- universal_mcp/applications/twitter/api_segments/tweets_api.py +9 -31
- universal_mcp/applications/twitter/api_segments/usage_api.py +1 -5
- universal_mcp/applications/twitter/api_segments/users_api.py +14 -42
- universal_mcp/applications/whatsapp/app.py +35 -186
- universal_mcp/applications/whatsapp/audio.py +2 -6
- universal_mcp/applications/whatsapp/whatsapp.py +17 -51
- universal_mcp/applications/whatsapp_business/app.py +70 -283
- universal_mcp/applications/wrike/app.py +45 -118
- universal_mcp/applications/yahoo_finance/app.py +19 -65
- universal_mcp/applications/youtube/app.py +75 -261
- universal_mcp/applications/zenquotes/app.py +2 -2
- {universal_mcp_applications-0.1.32.dist-info → universal_mcp_applications-0.1.36rc2.dist-info}/METADATA +2 -2
- {universal_mcp_applications-0.1.32.dist-info → universal_mcp_applications-0.1.36rc2.dist-info}/RECORD +105 -105
- {universal_mcp_applications-0.1.32.dist-info → universal_mcp_applications-0.1.36rc2.dist-info}/WHEEL +0 -0
- {universal_mcp_applications-0.1.32.dist-info → universal_mcp_applications-0.1.36rc2.dist-info}/licenses/LICENSE +0 -0
|
@@ -4,7 +4,6 @@ import os
|
|
|
4
4
|
import uuid
|
|
5
5
|
import wave
|
|
6
6
|
from typing import Annotated
|
|
7
|
-
|
|
8
7
|
from google import genai
|
|
9
8
|
from google.genai import types
|
|
10
9
|
from PIL import Image
|
|
@@ -22,21 +21,13 @@ class GoogleGeminiApp(APIApplication):
|
|
|
22
21
|
if self._genai_client is not None:
|
|
23
22
|
return self._genai_client
|
|
24
23
|
credentials = self.integration.get_credentials()
|
|
25
|
-
api_key = (
|
|
26
|
-
credentials.get("api_key")
|
|
27
|
-
or credentials.get("API_KEY")
|
|
28
|
-
or credentials.get("apiKey")
|
|
29
|
-
)
|
|
24
|
+
api_key = credentials.get("api_key") or credentials.get("API_KEY") or credentials.get("apiKey")
|
|
30
25
|
if not api_key:
|
|
31
26
|
raise ValueError("API key not found in integration credentials")
|
|
32
27
|
self._genai_client = genai.Client(api_key=api_key)
|
|
33
28
|
return self._genai_client
|
|
34
29
|
|
|
35
|
-
async def generate_text(
|
|
36
|
-
self,
|
|
37
|
-
prompt: Annotated[str, "The prompt to generate text from"],
|
|
38
|
-
model: str = "gemini-2.5-flash",
|
|
39
|
-
) -> str:
|
|
30
|
+
async def generate_text(self, prompt: Annotated[str, "The prompt to generate text from"], model: str = "gemini-2.5-flash") -> str:
|
|
40
31
|
"""Generates text using the Google Gemini model based on a given prompt.
|
|
41
32
|
This tool is suitable for various natural language processing tasks such as content generation, summarization, translation, and question answering.
|
|
42
33
|
|
|
@@ -54,15 +45,13 @@ class GoogleGeminiApp(APIApplication):
|
|
|
54
45
|
Tags:
|
|
55
46
|
text, generate, llm, important
|
|
56
47
|
"""
|
|
57
|
-
response = self.genai_client.models.generate_content(
|
|
58
|
-
contents=prompt, model=model
|
|
59
|
-
)
|
|
48
|
+
response = self.genai_client.models.generate_content(contents=prompt, model=model)
|
|
60
49
|
return response.text
|
|
61
50
|
|
|
62
51
|
async def generate_image(
|
|
63
52
|
self,
|
|
64
53
|
prompt: Annotated[str, "The prompt to generate image from"],
|
|
65
|
-
images: Annotated[list[str], "The reference image URLs"] | None = None,
|
|
54
|
+
images: Annotated[list[str], "The reference image URLs"] | None = None,
|
|
66
55
|
model: str = "gemini-2.5-flash-image-preview",
|
|
67
56
|
) -> dict:
|
|
68
57
|
"""
|
|
@@ -78,7 +67,7 @@ class GoogleGeminiApp(APIApplication):
|
|
|
78
67
|
Returns:
|
|
79
68
|
dict: A dictionary containing:
|
|
80
69
|
- 'type' (str): Always "image".
|
|
81
|
-
- 'data' (str): The base64 encoded image data.
|
|
70
|
+
- 'data' (str): The base64 encoded image data. Either upload or save this and then render the file path/link in markdown to the user. DO NOT use a data url.
|
|
82
71
|
- 'mime_type' (str): The MIME type of the image (e.g., "image/png").
|
|
83
72
|
- 'file_name' (str): A suggested file name for the generated image.
|
|
84
73
|
- 'text' (str): Any accompanying text generated by the model.
|
|
@@ -91,7 +80,6 @@ class GoogleGeminiApp(APIApplication):
|
|
|
91
80
|
Tags:
|
|
92
81
|
image, generate, vision, important
|
|
93
82
|
"""
|
|
94
|
-
# The Gemini API is synchronous, so run in a thread
|
|
95
83
|
contents = [prompt]
|
|
96
84
|
if images:
|
|
97
85
|
for image in images:
|
|
@@ -104,35 +92,20 @@ class GoogleGeminiApp(APIApplication):
|
|
|
104
92
|
else:
|
|
105
93
|
image = Image.open(image)
|
|
106
94
|
contents.append(image)
|
|
107
|
-
response = self.genai_client.models.generate_content(
|
|
108
|
-
model=model,
|
|
109
|
-
contents=contents,
|
|
110
|
-
)
|
|
95
|
+
response = self.genai_client.models.generate_content(model=model, contents=contents)
|
|
111
96
|
candidate = response.candidates[0]
|
|
112
97
|
text = ""
|
|
113
98
|
for part in candidate.content.parts:
|
|
114
99
|
if part.text is not None:
|
|
115
100
|
text += part.text
|
|
116
101
|
elif part.inline_data is not None:
|
|
117
|
-
# Return the raw image bytes
|
|
118
102
|
image_bytes = part.inline_data.data
|
|
119
|
-
|
|
120
103
|
img_base64 = base64.b64encode(image_bytes).decode("utf-8")
|
|
121
|
-
|
|
122
104
|
file_name = f"{uuid.uuid4()}.png"
|
|
123
|
-
|
|
124
|
-
return {
|
|
125
|
-
"type": "image",
|
|
126
|
-
"data": img_base64,
|
|
127
|
-
"mime_type": "image/png",
|
|
128
|
-
"file_name": file_name,
|
|
129
|
-
"text": text,
|
|
130
|
-
}
|
|
105
|
+
return {"type": "image", "data": img_base64, "mime_type": "image/png", "file_name": file_name, "text": text}
|
|
131
106
|
|
|
132
107
|
async def generate_audio(
|
|
133
|
-
self,
|
|
134
|
-
prompt: Annotated[str, "The prompt to generate audio from"],
|
|
135
|
-
model: str = "gemini-2.5-flash-preview-tts",
|
|
108
|
+
self, prompt: Annotated[str, "The prompt to generate audio from"], model: str = "gemini-2.5-flash-preview-tts"
|
|
136
109
|
) -> str:
|
|
137
110
|
"""Generates audio from a given text prompt using the Google Gemini model's Text-to-Speech (TTS) capabilities.
|
|
138
111
|
This tool is useful for converting text into spoken audio, which can be used for voiceovers, accessibility features, or interactive applications.
|
|
@@ -156,7 +129,6 @@ class GoogleGeminiApp(APIApplication):
|
|
|
156
129
|
audio, generate, tts, speech, important
|
|
157
130
|
"""
|
|
158
131
|
|
|
159
|
-
# Set up the wave file to save the output:
|
|
160
132
|
def wave_file(filename, pcm, channels=1, rate=24000, sample_width=2):
|
|
161
133
|
with wave.open(filename, "wb") as wf:
|
|
162
134
|
wf.setnchannels(channels)
|
|
@@ -170,52 +142,28 @@ class GoogleGeminiApp(APIApplication):
|
|
|
170
142
|
config=types.GenerateContentConfig(
|
|
171
143
|
response_modalities=["AUDIO"],
|
|
172
144
|
speech_config=types.SpeechConfig(
|
|
173
|
-
voice_config=types.VoiceConfig(
|
|
174
|
-
prebuilt_voice_config=types.PrebuiltVoiceConfig(
|
|
175
|
-
voice_name="Kore",
|
|
176
|
-
)
|
|
177
|
-
)
|
|
145
|
+
voice_config=types.VoiceConfig(prebuilt_voice_config=types.PrebuiltVoiceConfig(voice_name="Kore"))
|
|
178
146
|
),
|
|
179
147
|
),
|
|
180
148
|
)
|
|
181
|
-
|
|
182
149
|
data = response.candidates[0].content.parts[0].inline_data.data
|
|
183
|
-
|
|
184
150
|
file_name = f"{uuid.uuid4()}.wav"
|
|
185
151
|
wave_file(file_name, data)
|
|
186
|
-
|
|
187
|
-
# read the file
|
|
188
152
|
with open(file_name, "rb") as f:
|
|
189
153
|
data = f.read()
|
|
190
|
-
|
|
191
|
-
# delete the file
|
|
192
154
|
os.remove(file_name)
|
|
193
|
-
|
|
194
|
-
# Convert to base64
|
|
195
155
|
import base64
|
|
196
156
|
|
|
197
157
|
audio_base64 = base64.b64encode(data).decode("utf-8")
|
|
198
|
-
|
|
199
|
-
return {
|
|
200
|
-
"type": "audio",
|
|
201
|
-
"data": audio_base64,
|
|
202
|
-
"mime_type": "audio/wav",
|
|
203
|
-
"file_name": file_name,
|
|
204
|
-
}
|
|
158
|
+
return {"type": "audio", "data": audio_base64, "mime_type": "audio/wav", "file_name": file_name}
|
|
205
159
|
|
|
206
160
|
def list_tools(self):
|
|
207
|
-
return [
|
|
208
|
-
self.generate_text,
|
|
209
|
-
self.generate_image,
|
|
210
|
-
self.generate_audio,
|
|
211
|
-
]
|
|
161
|
+
return [self.generate_text, self.generate_image, self.generate_audio]
|
|
212
162
|
|
|
213
163
|
|
|
214
164
|
async def test_google_gemini():
|
|
215
165
|
app = GoogleGeminiApp()
|
|
216
|
-
await app.generate_image(
|
|
217
|
-
"A beautiful women potrait with red green hair color"
|
|
218
|
-
)
|
|
166
|
+
await app.generate_image("A beautiful women potrait with red green hair color")
|
|
219
167
|
|
|
220
168
|
|
|
221
169
|
if __name__ == "__main__":
|
|
@@ -2,7 +2,6 @@ import base64
|
|
|
2
2
|
import concurrent.futures
|
|
3
3
|
from email.message import EmailMessage
|
|
4
4
|
from typing import Any
|
|
5
|
-
|
|
6
5
|
from loguru import logger
|
|
7
6
|
from universal_mcp.applications.application import APIApplication
|
|
8
7
|
from universal_mcp.integrations import Integration
|
|
@@ -14,14 +13,7 @@ class GoogleMailApp(APIApplication):
|
|
|
14
13
|
self.base_api_url = "https://gmail.googleapis.com/gmail/v1/users/me"
|
|
15
14
|
self.base_url = "https://gmail.googleapis.com"
|
|
16
15
|
|
|
17
|
-
def send_email(
|
|
18
|
-
self,
|
|
19
|
-
to: str,
|
|
20
|
-
subject: str,
|
|
21
|
-
body: str,
|
|
22
|
-
body_type: str = "plain",
|
|
23
|
-
thread_id: str | None = None,
|
|
24
|
-
) -> dict[str, Any]:
|
|
16
|
+
async def send_email(self, to: str, subject: str, body: str, body_type: str = "plain", thread_id: str | None = None) -> dict[str, Any]:
|
|
25
17
|
"""
|
|
26
18
|
Composes and immediately sends an email message via the Gmail API. It can function as a reply within an existing conversation if a `thread_id` is provided. This action is distinct from `send_draft`, which sends a previously saved draft message, or `create_draft`, which only saves an email.
|
|
27
19
|
|
|
@@ -43,17 +35,12 @@ class GoogleMailApp(APIApplication):
|
|
|
43
35
|
Tags:
|
|
44
36
|
send, email, api, communication, important, thread, reply, openWorldHint
|
|
45
37
|
"""
|
|
46
|
-
|
|
47
38
|
url = f"{self.base_api_url}/messages/send"
|
|
48
39
|
raw_message = self._create_message(to, subject, body, body_type)
|
|
49
40
|
email_data = {"raw": raw_message}
|
|
50
|
-
|
|
51
|
-
# Add threadId to make it a proper reply if thread_id is provided
|
|
52
41
|
if thread_id:
|
|
53
42
|
email_data["threadId"] = thread_id
|
|
54
|
-
|
|
55
43
|
response = self._post(url, email_data)
|
|
56
|
-
|
|
57
44
|
return self._handle_response(response)
|
|
58
45
|
|
|
59
46
|
def _create_message(self, to, subject, body, body_type="plain"):
|
|
@@ -69,13 +56,8 @@ class GoogleMailApp(APIApplication):
|
|
|
69
56
|
logger.error(f"Error creating message: {str(e)}")
|
|
70
57
|
raise
|
|
71
58
|
|
|
72
|
-
def create_draft(
|
|
73
|
-
self,
|
|
74
|
-
to: str,
|
|
75
|
-
subject: str,
|
|
76
|
-
body: str,
|
|
77
|
-
body_type: str = "plain",
|
|
78
|
-
thread_id: str | None = None,
|
|
59
|
+
async def create_draft(
|
|
60
|
+
self, to: str, subject: str, body: str, body_type: str = "plain", thread_id: str | None = None
|
|
79
61
|
) -> dict[str, Any]:
|
|
80
62
|
"""
|
|
81
63
|
Saves a new email draft in Gmail with a specified recipient, subject, and body. An optional thread ID can create the draft as a reply within an existing conversation, distinguishing it from `send_email`, which sends immediately.
|
|
@@ -98,24 +80,16 @@ class GoogleMailApp(APIApplication):
|
|
|
98
80
|
Tags:
|
|
99
81
|
create, email, draft, gmail, api, important, thread, reply, html
|
|
100
82
|
"""
|
|
101
|
-
|
|
102
83
|
url = f"{self.base_api_url}/drafts"
|
|
103
|
-
|
|
104
84
|
raw_message = self._create_message(to, subject, body, body_type)
|
|
105
|
-
|
|
106
85
|
draft_data = {"message": {"raw": raw_message}}
|
|
107
|
-
|
|
108
|
-
# Add threadId to make it a proper reply if thread_id is provided
|
|
109
86
|
if thread_id:
|
|
110
87
|
draft_data["message"]["threadId"] = thread_id
|
|
111
|
-
|
|
112
88
|
logger.info(f"Creating draft email to {to}")
|
|
113
|
-
|
|
114
89
|
response = self._post(url, draft_data)
|
|
115
|
-
|
|
116
90
|
return self._handle_response(response)
|
|
117
91
|
|
|
118
|
-
def send_draft(self, draft_id: str) -> dict[str, Any]:
|
|
92
|
+
async def send_draft(self, draft_id: str) -> dict[str, Any]:
|
|
119
93
|
"""
|
|
120
94
|
Sends a pre-existing Gmail draft identified by its unique ID. It posts to the `/drafts/send` endpoint, converting a saved draft into a sent message. This function acts on drafts from `create_draft` and differs from `send_email`, which composes and sends an email in one step.
|
|
121
95
|
|
|
@@ -133,18 +107,13 @@ class GoogleMailApp(APIApplication):
|
|
|
133
107
|
Tags:
|
|
134
108
|
send, email, api, communication, important, draft
|
|
135
109
|
"""
|
|
136
|
-
|
|
137
110
|
url = f"{self.base_api_url}/drafts/send"
|
|
138
|
-
|
|
139
111
|
draft_data = {"id": draft_id}
|
|
140
|
-
|
|
141
112
|
logger.info(f"Sending draft email with ID: {draft_id}")
|
|
142
|
-
|
|
143
113
|
response = self._post(url, draft_data)
|
|
144
|
-
|
|
145
114
|
return self._handle_response(response)
|
|
146
115
|
|
|
147
|
-
def get_draft(self, draft_id: str, format: str = "full") -> dict[str, Any]:
|
|
116
|
+
async def get_draft(self, draft_id: str, format: str = "full") -> dict[str, Any]:
|
|
148
117
|
"""
|
|
149
118
|
Retrieves a specific Gmail draft by its unique ID. This function allows specifying the output format (e.g., full, raw) to control the response detail. Unlike `list_drafts`, it fetches a single, known draft rather than a collection of multiple drafts.
|
|
150
119
|
|
|
@@ -163,24 +132,13 @@ class GoogleMailApp(APIApplication):
|
|
|
163
132
|
Tags:
|
|
164
133
|
retrieve, email, gmail, draft, api, format, important
|
|
165
134
|
"""
|
|
166
|
-
|
|
167
135
|
url = f"{self.base_api_url}/drafts/{draft_id}"
|
|
168
|
-
|
|
169
|
-
# Add format parameter as query param
|
|
170
136
|
params = {"format": format}
|
|
171
|
-
|
|
172
137
|
logger.info(f"Retrieving draft with ID: {draft_id}")
|
|
173
|
-
|
|
174
138
|
response = self._get(url, params=params)
|
|
175
|
-
|
|
176
139
|
return self._handle_response(response)
|
|
177
140
|
|
|
178
|
-
def list_drafts(
|
|
179
|
-
self,
|
|
180
|
-
max_results: int = 20,
|
|
181
|
-
q: str | None = None,
|
|
182
|
-
include_spam_trash: bool = False,
|
|
183
|
-
) -> dict[str, Any]:
|
|
141
|
+
async def list_drafts(self, max_results: int = 20, q: str | None = None, include_spam_trash: bool = False) -> dict[str, Any]:
|
|
184
142
|
"""
|
|
185
143
|
Fetches a list of email drafts, allowing filtering by a search query and limiting results. It can optionally include drafts from spam and trash, returning a collection of draft objects. This is distinct from `get_draft`, which retrieves only a single, specific draft by its ID.
|
|
186
144
|
|
|
@@ -200,25 +158,17 @@ class GoogleMailApp(APIApplication):
|
|
|
200
158
|
Tags:
|
|
201
159
|
list, email, drafts, gmail, api, search, query, pagination, important
|
|
202
160
|
"""
|
|
203
|
-
|
|
204
161
|
url = f"{self.base_api_url}/drafts"
|
|
205
|
-
|
|
206
|
-
# Build query parameters
|
|
207
162
|
params: dict[str, Any] = {"maxResults": max_results}
|
|
208
|
-
|
|
209
163
|
if q:
|
|
210
164
|
params["q"] = q
|
|
211
|
-
|
|
212
165
|
if include_spam_trash:
|
|
213
166
|
params["includeSpamTrash"] = "true"
|
|
214
|
-
|
|
215
167
|
logger.info(f"Retrieving drafts list with params: {params}")
|
|
216
|
-
|
|
217
168
|
response = self._get(url, params=params)
|
|
218
|
-
|
|
219
169
|
return self._handle_response(response)
|
|
220
170
|
|
|
221
|
-
def get_message_details(self, message_id: str) -> dict[str, Any]:
|
|
171
|
+
async def get_message_details(self, message_id: str) -> dict[str, Any]:
|
|
222
172
|
"""
|
|
223
173
|
Retrieves a specific email from Gmail by its ID. It parses the API response to extract and format key details—including sender, subject, body, and attachments—into a structured dictionary. This function provides detailed data for a single message, distinguishing it from `list_messages` which fetches multiple messages.
|
|
224
174
|
|
|
@@ -234,25 +184,18 @@ class GoogleMailApp(APIApplication):
|
|
|
234
184
|
url = f"{self.base_api_url}/messages/{message_id}"
|
|
235
185
|
response = self._get(url)
|
|
236
186
|
raw_data = self._handle_response(response)
|
|
237
|
-
|
|
238
|
-
# Extract headers
|
|
239
187
|
headers = {}
|
|
240
188
|
for header in raw_data.get("payload", {}).get("headers", []):
|
|
241
189
|
name = header.get("name", "")
|
|
242
190
|
value = header.get("value", "")
|
|
243
191
|
headers[name] = value
|
|
244
|
-
|
|
245
|
-
# Extract body content
|
|
246
192
|
body_content = self._extract_email_body(raw_data.get("payload", {}))
|
|
247
193
|
if not body_content:
|
|
248
194
|
if "snippet" in raw_data:
|
|
249
195
|
body_content = f"Preview: {raw_data['snippet']}"
|
|
250
196
|
else:
|
|
251
197
|
body_content = "No content available"
|
|
252
|
-
|
|
253
|
-
# Extract attachments
|
|
254
198
|
attachments = self._extract_attachments(raw_data.get("payload", {}))
|
|
255
|
-
|
|
256
199
|
return {
|
|
257
200
|
"message_id": message_id,
|
|
258
201
|
"from_addr": headers.get("From", "Unknown sender"),
|
|
@@ -275,45 +218,30 @@ class GoogleMailApp(APIApplication):
|
|
|
275
218
|
str: The email body content (plain text preferred, HTML as fallback)
|
|
276
219
|
"""
|
|
277
220
|
try:
|
|
278
|
-
# Handle single part message
|
|
279
221
|
if payload.get("body") and payload.get("body", {}).get("data"):
|
|
280
222
|
return self._decode_base64(payload["body"]["data"])
|
|
281
|
-
|
|
282
|
-
# Handle multipart message
|
|
283
223
|
parts = payload.get("parts", [])
|
|
284
224
|
if not parts:
|
|
285
225
|
return ""
|
|
286
|
-
|
|
287
226
|
plain_text_body = ""
|
|
288
227
|
html_body = ""
|
|
289
|
-
|
|
290
228
|
for part in parts:
|
|
291
229
|
mime_type = part.get("mimeType", "")
|
|
292
|
-
|
|
293
|
-
# Extract plain text
|
|
294
230
|
if mime_type == "text/plain":
|
|
295
231
|
if part.get("body") and part.get("body", {}).get("data"):
|
|
296
232
|
plain_text_body = self._decode_base64(part["body"]["data"])
|
|
297
|
-
|
|
298
|
-
# Extract HTML content
|
|
299
233
|
elif mime_type == "text/html":
|
|
300
234
|
if part.get("body") and part.get("body", {}).get("data"):
|
|
301
235
|
html_body = self._decode_base64(part["body"]["data"])
|
|
302
|
-
|
|
303
|
-
# Handle nested multipart (recursive)
|
|
304
236
|
elif mime_type.startswith("multipart/") and part.get("parts"):
|
|
305
237
|
nested_body = self._extract_email_body(part)
|
|
306
|
-
if nested_body and not plain_text_body:
|
|
238
|
+
if nested_body and (not plain_text_body):
|
|
307
239
|
plain_text_body = nested_body
|
|
308
|
-
|
|
309
|
-
# Prefer plain text, fallback to HTML
|
|
310
240
|
if plain_text_body:
|
|
311
241
|
return plain_text_body
|
|
312
242
|
elif html_body:
|
|
313
243
|
return f"[HTML Content]\n{html_body}"
|
|
314
|
-
|
|
315
244
|
return ""
|
|
316
|
-
|
|
317
245
|
except Exception as e:
|
|
318
246
|
logger.error(f"Error extracting email body: {str(e)}")
|
|
319
247
|
return ""
|
|
@@ -329,7 +257,6 @@ class GoogleMailApp(APIApplication):
|
|
|
329
257
|
list: List of attachment dictionaries with attachment_id, filename, mime_type, and size
|
|
330
258
|
"""
|
|
331
259
|
attachments = []
|
|
332
|
-
|
|
333
260
|
try:
|
|
334
261
|
if payload.get("filename") and payload.get("body", {}).get("attachmentId"):
|
|
335
262
|
attachments.append(
|
|
@@ -340,7 +267,6 @@ class GoogleMailApp(APIApplication):
|
|
|
340
267
|
"size": payload.get("body", {}).get("size", 0),
|
|
341
268
|
}
|
|
342
269
|
)
|
|
343
|
-
|
|
344
270
|
parts = payload.get("parts", [])
|
|
345
271
|
for part in parts:
|
|
346
272
|
if part.get("filename") and part.get("body", {}).get("attachmentId"):
|
|
@@ -352,14 +278,11 @@ class GoogleMailApp(APIApplication):
|
|
|
352
278
|
"size": part.get("body", {}).get("size", 0),
|
|
353
279
|
}
|
|
354
280
|
)
|
|
355
|
-
|
|
356
281
|
elif part.get("parts"):
|
|
357
282
|
nested_attachments = self._extract_attachments(part)
|
|
358
283
|
attachments.extend(nested_attachments)
|
|
359
|
-
|
|
360
284
|
except Exception as e:
|
|
361
285
|
logger.error(f"Error extracting attachments: {str(e)}")
|
|
362
|
-
|
|
363
286
|
return attachments
|
|
364
287
|
|
|
365
288
|
def _decode_base64(self, data):
|
|
@@ -373,19 +296,14 @@ class GoogleMailApp(APIApplication):
|
|
|
373
296
|
str: Decoded string content
|
|
374
297
|
"""
|
|
375
298
|
try:
|
|
376
|
-
# Gmail API uses URL-safe base64 encoding
|
|
377
299
|
decoded_bytes = base64.urlsafe_b64decode(data)
|
|
378
300
|
return decoded_bytes.decode("utf-8")
|
|
379
301
|
except Exception as e:
|
|
380
302
|
logger.error(f"Error decoding base64 data: {str(e)}")
|
|
381
303
|
return f"[Unable to decode content: {str(e)}]"
|
|
382
304
|
|
|
383
|
-
def list_messages(
|
|
384
|
-
self,
|
|
385
|
-
max_results: int = 10,
|
|
386
|
-
q: str | None = None,
|
|
387
|
-
include_spam_trash: bool = False,
|
|
388
|
-
page_token: str | None = None,
|
|
305
|
+
async def list_messages(
|
|
306
|
+
self, max_results: int = 10, q: str | None = None, include_spam_trash: bool = False, page_token: str | None = None
|
|
389
307
|
) -> dict[str, Any]:
|
|
390
308
|
"""
|
|
391
309
|
Fetches a paginated list of detailed email messages using optional search queries. It concurrently retrieves full content (sender, subject, body) for each message, returning the results and a pagination token. This differs from `get_message_details`, which fetches only a single message.
|
|
@@ -423,39 +341,22 @@ class GoogleMailApp(APIApplication):
|
|
|
423
341
|
list, messages, gmail, search, query, pagination, important
|
|
424
342
|
"""
|
|
425
343
|
url = f"{self.base_api_url}/messages?format=metadata"
|
|
426
|
-
|
|
427
|
-
# Build query parameters
|
|
428
344
|
params: dict[str, Any] = {"maxResults": max_results}
|
|
429
|
-
|
|
430
345
|
if q:
|
|
431
346
|
params["q"] = q
|
|
432
|
-
|
|
433
347
|
if include_spam_trash:
|
|
434
348
|
params["includeSpamTrash"] = "true"
|
|
435
|
-
|
|
436
349
|
if page_token:
|
|
437
350
|
params["pageToken"] = page_token
|
|
438
|
-
|
|
439
351
|
logger.info(f"Retrieving messages list with params: {params}")
|
|
440
|
-
|
|
441
352
|
response = self._get(url, params=params)
|
|
442
353
|
data = self._handle_response(response)
|
|
443
|
-
|
|
444
|
-
# Extract message IDs
|
|
445
354
|
messages = data.get("messages", [])
|
|
446
355
|
message_ids = [msg.get("id") for msg in messages if msg.get("id")]
|
|
447
|
-
|
|
448
|
-
# Use ThreadPoolExecutor to get detailed information for each message in parallel
|
|
449
356
|
detailed_messages = []
|
|
450
357
|
if message_ids:
|
|
451
358
|
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
|
|
452
|
-
|
|
453
|
-
future_to_message_id = {
|
|
454
|
-
executor.submit(self.get_message_details, message_id): message_id
|
|
455
|
-
for message_id in message_ids
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
# Collect results as they complete
|
|
359
|
+
future_to_message_id = {executor.submit(self.get_message_details, message_id): message_id for message_id in message_ids}
|
|
459
360
|
for future in concurrent.futures.as_completed(future_to_message_id):
|
|
460
361
|
message_id = future_to_message_id[future]
|
|
461
362
|
try:
|
|
@@ -463,12 +364,7 @@ class GoogleMailApp(APIApplication):
|
|
|
463
364
|
detailed_messages.append(result)
|
|
464
365
|
except Exception as e:
|
|
465
366
|
logger.error(f"Error retrieving message {message_id}: {str(e)}")
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
return {
|
|
469
|
-
"messages": detailed_messages,
|
|
470
|
-
"next_page_token": data.get("nextPageToken"),
|
|
471
|
-
}
|
|
367
|
+
return {"messages": detailed_messages, "next_page_token": data.get("nextPageToken")}
|
|
472
368
|
|
|
473
369
|
def get_email_thread(self, thread_id: str) -> dict[str, Any]:
|
|
474
370
|
"""
|
|
@@ -493,7 +389,7 @@ class GoogleMailApp(APIApplication):
|
|
|
493
389
|
response = self._get(url)
|
|
494
390
|
return self._handle_response(response)
|
|
495
391
|
|
|
496
|
-
def list_labels(self) -> dict[str, Any]:
|
|
392
|
+
async def list_labels(self) -> dict[str, Any]:
|
|
497
393
|
"""
|
|
498
394
|
Fetches a complete list of all available labels from the user's Gmail account via the API. It retrieves both system-defined (e.g., INBOX) and user-created labels, returning their names and IDs, complementing management functions like `create_label` and `update_label`.
|
|
499
395
|
|
|
@@ -510,16 +406,12 @@ class GoogleMailApp(APIApplication):
|
|
|
510
406
|
Tags:
|
|
511
407
|
list, gmail, labels, fetch, organize, important, management
|
|
512
408
|
"""
|
|
513
|
-
|
|
514
409
|
url = f"{self.base_api_url}/labels"
|
|
515
|
-
|
|
516
410
|
logger.info("Retrieving Gmail labels")
|
|
517
|
-
|
|
518
411
|
response = self._get(url)
|
|
519
|
-
|
|
520
412
|
return self._handle_response(response)
|
|
521
413
|
|
|
522
|
-
def create_label(self, name: str) -> dict[str, Any]:
|
|
414
|
+
async def create_label(self, name: str) -> dict[str, Any]:
|
|
523
415
|
"""
|
|
524
416
|
Creates a new Gmail label with a specified name, hardcoding its visibility to ensure it appears in both label and message lists. This function complements `update_label` and `delete_label` by adding new organizational tags to the user's account via the API.
|
|
525
417
|
|
|
@@ -536,23 +428,13 @@ class GoogleMailApp(APIApplication):
|
|
|
536
428
|
Tags:
|
|
537
429
|
create, label, gmail, management, important
|
|
538
430
|
"""
|
|
539
|
-
|
|
540
431
|
url = f"{self.base_api_url}/labels"
|
|
541
|
-
|
|
542
|
-
# Create the label data with just the essential fields
|
|
543
|
-
label_data = {
|
|
544
|
-
"name": name,
|
|
545
|
-
"labelListVisibility": "labelShow", # Show in label list
|
|
546
|
-
"messageListVisibility": "show", # Show in message list
|
|
547
|
-
}
|
|
548
|
-
|
|
432
|
+
label_data = {"name": name, "labelListVisibility": "labelShow", "messageListVisibility": "show"}
|
|
549
433
|
logger.info(f"Creating new Gmail label: {name}")
|
|
550
|
-
|
|
551
434
|
response = self._post(url, label_data)
|
|
552
|
-
|
|
553
435
|
return self._handle_response(response)
|
|
554
436
|
|
|
555
|
-
def get_profile(self) -> dict[str, Any]:
|
|
437
|
+
async def get_profile(self) -> dict[str, Any]:
|
|
556
438
|
"""
|
|
557
439
|
Retrieves the authenticated user's Gmail profile from the API. The profile includes the user's email address, total message and thread counts, and the mailbox's history ID, offering a high-level summary of the account's state.
|
|
558
440
|
|
|
@@ -569,15 +451,12 @@ class GoogleMailApp(APIApplication):
|
|
|
569
451
|
Tags:
|
|
570
452
|
fetch, profile, gmail, user-info, api-request, important
|
|
571
453
|
"""
|
|
572
|
-
|
|
573
454
|
url = f"{self.base_api_url}/profile"
|
|
574
|
-
|
|
575
455
|
logger.info("Retrieving Gmail user profile")
|
|
576
|
-
|
|
577
456
|
response = self._get(url)
|
|
578
457
|
return self._handle_response(response)
|
|
579
458
|
|
|
580
|
-
def update_draft(
|
|
459
|
+
async def update_draft(
|
|
581
460
|
self,
|
|
582
461
|
userId,
|
|
583
462
|
id,
|
|
@@ -670,10 +549,7 @@ class GoogleMailApp(APIApplication):
|
|
|
670
549
|
raise ValueError("Missing required parameter 'userId'")
|
|
671
550
|
if id is None:
|
|
672
551
|
raise ValueError("Missing required parameter 'id'")
|
|
673
|
-
request_body = {
|
|
674
|
-
"id": id,
|
|
675
|
-
"message": message,
|
|
676
|
-
}
|
|
552
|
+
request_body = {"id": id, "message": message}
|
|
677
553
|
request_body = {k: v for k, v in request_body.items() if v is not None}
|
|
678
554
|
url = f"{self.base_url}/gmail/v1/users/{userId}/drafts/{id}"
|
|
679
555
|
query_params = {
|
|
@@ -697,7 +573,7 @@ class GoogleMailApp(APIApplication):
|
|
|
697
573
|
response.raise_for_status()
|
|
698
574
|
return response.json()
|
|
699
575
|
|
|
700
|
-
def trash_message(
|
|
576
|
+
async def trash_message(
|
|
701
577
|
self,
|
|
702
578
|
userId,
|
|
703
579
|
id,
|
|
@@ -763,7 +639,7 @@ class GoogleMailApp(APIApplication):
|
|
|
763
639
|
response.raise_for_status()
|
|
764
640
|
return response.json()
|
|
765
641
|
|
|
766
|
-
def untrash_message(
|
|
642
|
+
async def untrash_message(
|
|
767
643
|
self,
|
|
768
644
|
userId,
|
|
769
645
|
id,
|
|
@@ -829,7 +705,7 @@ class GoogleMailApp(APIApplication):
|
|
|
829
705
|
response.raise_for_status()
|
|
830
706
|
return response.json()
|
|
831
707
|
|
|
832
|
-
def get_attachment(
|
|
708
|
+
async def get_attachment(
|
|
833
709
|
self,
|
|
834
710
|
userId,
|
|
835
711
|
messageId,
|
|
@@ -899,7 +775,7 @@ class GoogleMailApp(APIApplication):
|
|
|
899
775
|
response.raise_for_status()
|
|
900
776
|
return response.json()
|
|
901
777
|
|
|
902
|
-
def update_label(
|
|
778
|
+
async def update_label(
|
|
903
779
|
self,
|
|
904
780
|
userId,
|
|
905
781
|
id,
|
|
@@ -1014,7 +890,7 @@ class GoogleMailApp(APIApplication):
|
|
|
1014
890
|
response.raise_for_status()
|
|
1015
891
|
return response.json()
|
|
1016
892
|
|
|
1017
|
-
def delete_label(
|
|
893
|
+
async def delete_label(
|
|
1018
894
|
self,
|
|
1019
895
|
userId,
|
|
1020
896
|
id,
|
|
@@ -1080,7 +956,7 @@ class GoogleMailApp(APIApplication):
|
|
|
1080
956
|
response.raise_for_status()
|
|
1081
957
|
return response.json()
|
|
1082
958
|
|
|
1083
|
-
def get_filter(
|
|
959
|
+
async def get_filter(
|
|
1084
960
|
self,
|
|
1085
961
|
userId,
|
|
1086
962
|
id,
|
|
@@ -1146,7 +1022,7 @@ class GoogleMailApp(APIApplication):
|
|
|
1146
1022
|
response.raise_for_status()
|
|
1147
1023
|
return response.json()
|
|
1148
1024
|
|
|
1149
|
-
def delete_filter(
|
|
1025
|
+
async def delete_filter(
|
|
1150
1026
|
self,
|
|
1151
1027
|
userId,
|
|
1152
1028
|
id,
|
|
@@ -1212,7 +1088,7 @@ class GoogleMailApp(APIApplication):
|
|
|
1212
1088
|
response.raise_for_status()
|
|
1213
1089
|
return response.json()
|
|
1214
1090
|
|
|
1215
|
-
def get_all_filters(
|
|
1091
|
+
async def get_all_filters(
|
|
1216
1092
|
self,
|
|
1217
1093
|
userId,
|
|
1218
1094
|
access_token=None,
|
|
@@ -1274,7 +1150,7 @@ class GoogleMailApp(APIApplication):
|
|
|
1274
1150
|
response.raise_for_status()
|
|
1275
1151
|
return response.json()
|
|
1276
1152
|
|
|
1277
|
-
def create_filter(
|
|
1153
|
+
async def create_filter(
|
|
1278
1154
|
self,
|
|
1279
1155
|
userId,
|
|
1280
1156
|
access_token=None,
|
|
@@ -1348,11 +1224,7 @@ class GoogleMailApp(APIApplication):
|
|
|
1348
1224
|
"""
|
|
1349
1225
|
if userId is None:
|
|
1350
1226
|
raise ValueError("Missing required parameter 'userId'")
|
|
1351
|
-
request_body = {
|
|
1352
|
-
"action": action,
|
|
1353
|
-
"criteria": criteria,
|
|
1354
|
-
"id": id,
|
|
1355
|
-
}
|
|
1227
|
+
request_body = {"action": action, "criteria": criteria, "id": id}
|
|
1356
1228
|
request_body = {k: v for k, v in request_body.items() if v is not None}
|
|
1357
1229
|
url = f"{self.base_url}/gmail/v1/users/{userId}/settings/filters"
|
|
1358
1230
|
query_params = {
|
|
@@ -1388,7 +1260,6 @@ class GoogleMailApp(APIApplication):
|
|
|
1388
1260
|
self.list_labels,
|
|
1389
1261
|
self.create_label,
|
|
1390
1262
|
self.get_profile,
|
|
1391
|
-
# Auto Generated from openapi spec
|
|
1392
1263
|
self.update_draft,
|
|
1393
1264
|
self.trash_message,
|
|
1394
1265
|
self.untrash_message,
|