google-api-client-wrapper 1.0.0__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.
- google_api_client_wrapper-1.0.0.dist-info/METADATA +103 -0
- google_api_client_wrapper-1.0.0.dist-info/RECORD +39 -0
- google_api_client_wrapper-1.0.0.dist-info/WHEEL +5 -0
- google_api_client_wrapper-1.0.0.dist-info/licenses/LICENSE +21 -0
- google_api_client_wrapper-1.0.0.dist-info/top_level.txt +1 -0
- google_client/__init__.py +6 -0
- google_client/services/__init__.py +13 -0
- google_client/services/calendar/__init__.py +14 -0
- google_client/services/calendar/api_service.py +454 -0
- google_client/services/calendar/constants.py +48 -0
- google_client/services/calendar/exceptions.py +35 -0
- google_client/services/calendar/query_builder.py +314 -0
- google_client/services/calendar/types.py +403 -0
- google_client/services/calendar/utils.py +338 -0
- google_client/services/drive/__init__.py +13 -0
- google_client/services/drive/api_service.py +1133 -0
- google_client/services/drive/constants.py +37 -0
- google_client/services/drive/exceptions.py +60 -0
- google_client/services/drive/query_builder.py +385 -0
- google_client/services/drive/types.py +242 -0
- google_client/services/drive/utils.py +392 -0
- google_client/services/gmail/__init__.py +16 -0
- google_client/services/gmail/api_service.py +715 -0
- google_client/services/gmail/constants.py +6 -0
- google_client/services/gmail/exceptions.py +45 -0
- google_client/services/gmail/query_builder.py +408 -0
- google_client/services/gmail/types.py +285 -0
- google_client/services/gmail/utils.py +426 -0
- google_client/services/tasks/__init__.py +12 -0
- google_client/services/tasks/api_service.py +561 -0
- google_client/services/tasks/constants.py +32 -0
- google_client/services/tasks/exceptions.py +35 -0
- google_client/services/tasks/query_builder.py +324 -0
- google_client/services/tasks/types.py +156 -0
- google_client/services/tasks/utils.py +224 -0
- google_client/user_client.py +208 -0
- google_client/utils/__init__.py +0 -0
- google_client/utils/datetime.py +144 -0
- google_client/utils/validation.py +71 -0
|
@@ -0,0 +1,715 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import os
|
|
3
|
+
from typing import Optional, List, Dict, Any, overload, Union
|
|
4
|
+
|
|
5
|
+
from googleapiclient.errors import HttpError
|
|
6
|
+
|
|
7
|
+
from .types import EmailMessage, EmailAttachment, Label, EmailThread
|
|
8
|
+
from .query_builder import EmailQueryBuilder
|
|
9
|
+
from . import utils
|
|
10
|
+
from .constants import DEFAULT_MAX_RESULTS, MAX_RESULTS_LIMIT
|
|
11
|
+
|
|
12
|
+
from .exceptions import GmailError, GmailPermissionError, AttachmentNotFoundError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class GmailApiService:
|
|
16
|
+
"""
|
|
17
|
+
Service layer for Gmail API operations.
|
|
18
|
+
Contains all Gmail API functionality that was removed from dataclasses.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, service: Any):
|
|
22
|
+
"""
|
|
23
|
+
Initialize Gmail service.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
service: The Gmail API service instance
|
|
27
|
+
"""
|
|
28
|
+
self._service = service
|
|
29
|
+
|
|
30
|
+
def query(self) -> EmailQueryBuilder:
|
|
31
|
+
"""
|
|
32
|
+
Create a new EmailQueryBuilder for building complex email queries with a fluent API.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
EmailQueryBuilder instance for method chaining
|
|
36
|
+
|
|
37
|
+
Example:
|
|
38
|
+
emails = (EmailMessage.query()
|
|
39
|
+
.limit(50)
|
|
40
|
+
.from_sender("sender@example.com")
|
|
41
|
+
.search("meeting")
|
|
42
|
+
.with_attachments()
|
|
43
|
+
.execute())
|
|
44
|
+
"""
|
|
45
|
+
from .query_builder import EmailQueryBuilder
|
|
46
|
+
return EmailQueryBuilder(self)
|
|
47
|
+
|
|
48
|
+
def list_emails(
|
|
49
|
+
self,
|
|
50
|
+
max_results: Optional[int] = DEFAULT_MAX_RESULTS,
|
|
51
|
+
query: Optional[str] = None,
|
|
52
|
+
include_spam_trash: bool = False,
|
|
53
|
+
label_ids: Optional[List[str]] = None
|
|
54
|
+
) -> List[EmailMessage]:
|
|
55
|
+
"""
|
|
56
|
+
Fetches a list of messages from Gmail with optional filtering.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
max_results: Maximum number of messages to retrieve. Defaults to 30.
|
|
60
|
+
query: Gmail search query string (same syntax as Gmail search).
|
|
61
|
+
include_spam_trash: Whether to include messages from spam and trash.
|
|
62
|
+
label_ids: List of label IDs to filter by.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
A list of EmailMessage objects representing the messages found.
|
|
66
|
+
If no messages are found, an empty list is returned.
|
|
67
|
+
"""
|
|
68
|
+
# Input validation
|
|
69
|
+
if max_results and (max_results < 1 or max_results > MAX_RESULTS_LIMIT):
|
|
70
|
+
raise ValueError(f"max_results must be between 1 and {MAX_RESULTS_LIMIT}")
|
|
71
|
+
|
|
72
|
+
# Get list of message IDs
|
|
73
|
+
request_params = {
|
|
74
|
+
'userId': 'me',
|
|
75
|
+
'maxResults': max_results,
|
|
76
|
+
'includeSpamTrash': include_spam_trash
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if query:
|
|
80
|
+
request_params['q'] = query
|
|
81
|
+
if label_ids:
|
|
82
|
+
request_params['labelIds'] = label_ids
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
result = self._service.users().messages().list(**request_params).execute()
|
|
86
|
+
messages = result.get('messages', [])
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# Fetch full message details
|
|
90
|
+
email_messages = []
|
|
91
|
+
for message in messages:
|
|
92
|
+
try:
|
|
93
|
+
email_messages.append(self.get_email(message['id']))
|
|
94
|
+
except Exception as e:
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
return email_messages
|
|
98
|
+
|
|
99
|
+
except Exception as e:
|
|
100
|
+
raise
|
|
101
|
+
|
|
102
|
+
def get_email(self, message_id: str) -> EmailMessage:
|
|
103
|
+
"""
|
|
104
|
+
Retrieves a specific message from Gmail using its unique identifier.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
message_id: The unique identifier of the message to be retrieved.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
An EmailMessage object representing the message with the specified ID.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
gmail_message = self._service.users().messages().get(
|
|
115
|
+
userId='me',
|
|
116
|
+
id=message_id,
|
|
117
|
+
format='full'
|
|
118
|
+
).execute()
|
|
119
|
+
return utils.from_gmail_message(gmail_message)
|
|
120
|
+
except Exception as e:
|
|
121
|
+
raise
|
|
122
|
+
|
|
123
|
+
def send_email(
|
|
124
|
+
self,
|
|
125
|
+
to: List[str],
|
|
126
|
+
subject: Optional[str] = None,
|
|
127
|
+
body_text: Optional[str] = None,
|
|
128
|
+
body_html: Optional[str] = None,
|
|
129
|
+
cc: Optional[List[str]] = None,
|
|
130
|
+
bcc: Optional[List[str]] = None,
|
|
131
|
+
attachment_paths: Optional[List[str]] = None,
|
|
132
|
+
reply_to_message_id: Optional[str] = None,
|
|
133
|
+
references: Optional[str] = None,
|
|
134
|
+
thread_id: Optional[str] = None
|
|
135
|
+
) -> EmailMessage:
|
|
136
|
+
"""
|
|
137
|
+
Sends a new email message.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
to: List of recipient email addresses.
|
|
141
|
+
subject: The subject line of the email.
|
|
142
|
+
body_text: Plain text body of the email (optional).
|
|
143
|
+
body_html: HTML body of the email (optional).
|
|
144
|
+
cc: List of CC recipient email addresses (optional).
|
|
145
|
+
bcc: List of BCC recipient email addresses (optional).
|
|
146
|
+
attachment_paths: List of file paths to attach (optional).
|
|
147
|
+
reply_to_message_id: ID of message this is replying to (optional).
|
|
148
|
+
references: List of references to attach (optional).
|
|
149
|
+
thread_id: ID of the thread to which this message belongs (optional).
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
An EmailMessage object representing the message sent.
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# Create message
|
|
157
|
+
raw_message = utils.create_message(
|
|
158
|
+
to=to,
|
|
159
|
+
subject=subject,
|
|
160
|
+
body_text=body_text,
|
|
161
|
+
body_html=body_html,
|
|
162
|
+
cc=cc,
|
|
163
|
+
bcc=bcc,
|
|
164
|
+
attachment_paths=attachment_paths,
|
|
165
|
+
references=references,
|
|
166
|
+
reply_to_message_id=reply_to_message_id
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
send_result = self._service.users().messages().send(
|
|
172
|
+
userId='me',
|
|
173
|
+
body={'raw': raw_message, 'threadId': thread_id}
|
|
174
|
+
).execute()
|
|
175
|
+
|
|
176
|
+
return self.get_email(send_result['id'])
|
|
177
|
+
|
|
178
|
+
except Exception as e:
|
|
179
|
+
raise
|
|
180
|
+
|
|
181
|
+
def batch_get_emails(self, message_ids: List[str]) -> List["EmailMessage"]:
|
|
182
|
+
"""
|
|
183
|
+
Retrieves multiple emails.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
message_ids: List of message IDs to retrieve
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
List of EmailMessage objects
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
email_messages = []
|
|
193
|
+
for message_id in message_ids:
|
|
194
|
+
try:
|
|
195
|
+
email_messages.append(self.get_email(message_id))
|
|
196
|
+
except Exception as e:
|
|
197
|
+
pass
|
|
198
|
+
|
|
199
|
+
return email_messages
|
|
200
|
+
|
|
201
|
+
def batch_send_emails(self, email_data_list: List[Dict[str, Any]]) -> List["EmailMessage"]:
|
|
202
|
+
"""
|
|
203
|
+
Sends multiple emails.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
email_data_list: List of dictionaries containing email parameters
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
List of sent EmailMessage objects
|
|
210
|
+
"""
|
|
211
|
+
|
|
212
|
+
sent_messages = []
|
|
213
|
+
for email_data in email_data_list:
|
|
214
|
+
try:
|
|
215
|
+
sent_messages.append(self.send_email(**email_data))
|
|
216
|
+
except Exception as e:
|
|
217
|
+
pass
|
|
218
|
+
|
|
219
|
+
return sent_messages
|
|
220
|
+
|
|
221
|
+
def reply(
|
|
222
|
+
self,
|
|
223
|
+
original_email: EmailMessage,
|
|
224
|
+
body_text: Optional[str] = None,
|
|
225
|
+
body_html: Optional[str] = None,
|
|
226
|
+
attachment_paths: Optional[List[str]] = None,
|
|
227
|
+
reply_all: bool = False
|
|
228
|
+
) -> EmailMessage:
|
|
229
|
+
"""
|
|
230
|
+
Sends a reply to the current email message.
|
|
231
|
+
Args:
|
|
232
|
+
original_email: The original email message being replied to
|
|
233
|
+
body_text: Plain text body of the email.
|
|
234
|
+
body_html: HTML body of the email.
|
|
235
|
+
attachment_paths: List of file paths to attach (optional).
|
|
236
|
+
reply_all: A boolean indicating whether to all recipients including cc's.
|
|
237
|
+
Returns:
|
|
238
|
+
An EmailMessage object representing the message sent.
|
|
239
|
+
"""
|
|
240
|
+
if original_email.is_from('me'):
|
|
241
|
+
to = original_email.get_recipient_emails()
|
|
242
|
+
else:
|
|
243
|
+
to = [original_email.sender.email]
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
# Build enhanced references header
|
|
247
|
+
enhanced_references = utils.build_references_header(original_email)
|
|
248
|
+
|
|
249
|
+
return self.send_email(
|
|
250
|
+
to=to,
|
|
251
|
+
subject=original_email.subject,
|
|
252
|
+
body_text=body_text,
|
|
253
|
+
body_html=body_html,
|
|
254
|
+
attachment_paths=attachment_paths,
|
|
255
|
+
reply_to_message_id=original_email.reply_to_id,
|
|
256
|
+
references=enhanced_references,
|
|
257
|
+
thread_id=original_email.thread_id
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
def forward(
|
|
261
|
+
self,
|
|
262
|
+
original_email: EmailMessage,
|
|
263
|
+
to: List[str],
|
|
264
|
+
include_attachments: bool = True
|
|
265
|
+
) -> EmailMessage:
|
|
266
|
+
"""
|
|
267
|
+
Forwards an email message to new recipients.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
original_email: The original email message being forwarded
|
|
271
|
+
to: List of recipient email addresses
|
|
272
|
+
include_attachments: Whether to include original email's attachments
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
An EmailMessage object representing the forwarded message
|
|
276
|
+
"""
|
|
277
|
+
|
|
278
|
+
# Prepare subject with Fwd: prefix
|
|
279
|
+
subject = f"Fwd: {original_email.subject}" if original_email.subject else "Fwd:"
|
|
280
|
+
|
|
281
|
+
# Prepare Text body for forwarding
|
|
282
|
+
forwarded_body_text = None
|
|
283
|
+
if original_email.body_text:
|
|
284
|
+
forwarded_body_text = utils.prepare_forward_body_text(original_email)
|
|
285
|
+
|
|
286
|
+
# Prepare HTML body for forwarding
|
|
287
|
+
forwarded_body_html = None
|
|
288
|
+
if original_email.body_html:
|
|
289
|
+
forwarded_body_html = utils.prepare_forward_body_html(original_email)
|
|
290
|
+
|
|
291
|
+
# Handle original attachments if requested
|
|
292
|
+
attachment_data_list = []
|
|
293
|
+
if include_attachments and original_email.attachments:
|
|
294
|
+
for attachment in original_email.attachments:
|
|
295
|
+
attachment_bytes = self.get_attachment_payload(attachment)
|
|
296
|
+
attachment_data_list.append((attachment.filename, attachment.mime_type, attachment_bytes))
|
|
297
|
+
|
|
298
|
+
raw_message = utils.create_message(
|
|
299
|
+
to=to,
|
|
300
|
+
subject=subject,
|
|
301
|
+
body_text=forwarded_body_text,
|
|
302
|
+
body_html=forwarded_body_html,
|
|
303
|
+
attachment_data_list=attachment_data_list if attachment_data_list else None
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
try:
|
|
307
|
+
send_result = self._service.users().messages().send(
|
|
308
|
+
userId='me',
|
|
309
|
+
body={'raw': raw_message}
|
|
310
|
+
).execute()
|
|
311
|
+
|
|
312
|
+
return self.get_email(send_result['id'])
|
|
313
|
+
|
|
314
|
+
except Exception as e:
|
|
315
|
+
raise
|
|
316
|
+
|
|
317
|
+
def mark_as_read(self, email: EmailMessage) -> bool:
|
|
318
|
+
"""
|
|
319
|
+
Marks a message as read by removing the UNREAD label.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
email: The email message being marked as read.
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
True if the operation was successful, False otherwise.
|
|
326
|
+
"""
|
|
327
|
+
|
|
328
|
+
try:
|
|
329
|
+
self._service.users().messages().modify(
|
|
330
|
+
userId='me',
|
|
331
|
+
id=email.message_id,
|
|
332
|
+
body={'removeLabelIds': ['UNREAD']}
|
|
333
|
+
).execute()
|
|
334
|
+
email.is_read = True
|
|
335
|
+
return True
|
|
336
|
+
except Exception as e:
|
|
337
|
+
return False
|
|
338
|
+
|
|
339
|
+
def mark_as_unread(self, email: EmailMessage) -> bool:
|
|
340
|
+
"""
|
|
341
|
+
Marks a message as unread by adding the UNREAD label.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
email: The email message being marked as unread
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
True if the operation was successful, False otherwise.
|
|
348
|
+
"""
|
|
349
|
+
|
|
350
|
+
try:
|
|
351
|
+
self._service.users().messages().modify(
|
|
352
|
+
userId='me',
|
|
353
|
+
id=email.message_id,
|
|
354
|
+
body={'addLabelIds': ['UNREAD']}
|
|
355
|
+
).execute()
|
|
356
|
+
email.is_read = False
|
|
357
|
+
return True
|
|
358
|
+
except Exception as e:
|
|
359
|
+
return False
|
|
360
|
+
|
|
361
|
+
def add_label(self, email: EmailMessage, labels: List[str]) -> bool:
|
|
362
|
+
"""
|
|
363
|
+
Adds labels to a message.
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
email: The email message to add labels to
|
|
367
|
+
labels: List of label IDs to add.
|
|
368
|
+
|
|
369
|
+
Returns:
|
|
370
|
+
True if the operation was successful, False otherwise.
|
|
371
|
+
"""
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
try:
|
|
375
|
+
self._service.users().messages().modify(
|
|
376
|
+
userId='me',
|
|
377
|
+
id=email.message_id,
|
|
378
|
+
body={'addLabelIds': labels}
|
|
379
|
+
).execute()
|
|
380
|
+
# Update local state
|
|
381
|
+
for label in labels:
|
|
382
|
+
if label not in email.labels:
|
|
383
|
+
email.labels.append(label)
|
|
384
|
+
break
|
|
385
|
+
return True
|
|
386
|
+
except Exception as e:
|
|
387
|
+
return False
|
|
388
|
+
|
|
389
|
+
def remove_label(self, email: EmailMessage, labels: List[str]) -> bool:
|
|
390
|
+
"""
|
|
391
|
+
Removes labels from a message.
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
email: The email message to remove labels from
|
|
395
|
+
labels: List of label IDs to remove.
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
True if the operation was successful, False otherwise.
|
|
399
|
+
"""
|
|
400
|
+
|
|
401
|
+
try:
|
|
402
|
+
self._service.users().messages().modify(
|
|
403
|
+
userId='me',
|
|
404
|
+
id=email.message_id,
|
|
405
|
+
body={'removeLabelIds': labels}
|
|
406
|
+
).execute()
|
|
407
|
+
# Update local state
|
|
408
|
+
for label in labels:
|
|
409
|
+
try:
|
|
410
|
+
email.labels.remove(label)
|
|
411
|
+
except ValueError:
|
|
412
|
+
continue
|
|
413
|
+
return True
|
|
414
|
+
except Exception as e:
|
|
415
|
+
return False
|
|
416
|
+
|
|
417
|
+
def delete_email(self, email: EmailMessage, permanent: bool = False) -> bool:
|
|
418
|
+
"""
|
|
419
|
+
Deletes a message (moves to trash or permanently deletes).
|
|
420
|
+
|
|
421
|
+
Args:
|
|
422
|
+
email: The email message being deleted
|
|
423
|
+
permanent: If True, permanently deletes the message. If False, moves to trash.
|
|
424
|
+
|
|
425
|
+
Returns:
|
|
426
|
+
True if the operation was successful, False otherwise.
|
|
427
|
+
"""
|
|
428
|
+
|
|
429
|
+
try:
|
|
430
|
+
if permanent:
|
|
431
|
+
self._service.users().messages().delete(userId='me', id=email.message_id).execute()
|
|
432
|
+
else:
|
|
433
|
+
self._service.users().messages().trash(userId='me', id=email.message_id).execute()
|
|
434
|
+
return True
|
|
435
|
+
except Exception as e:
|
|
436
|
+
return False
|
|
437
|
+
|
|
438
|
+
def get_attachment_payload(self, attachment: EmailAttachment) -> bytes:
|
|
439
|
+
attachment_ = self._service.users().messages().attachments().get(
|
|
440
|
+
userId='me',
|
|
441
|
+
messageId=attachment.message_id,
|
|
442
|
+
id=attachment.attachment_id
|
|
443
|
+
).execute()
|
|
444
|
+
data = attachment_['data']
|
|
445
|
+
data = base64.urlsafe_b64decode(data + '===')
|
|
446
|
+
|
|
447
|
+
return data
|
|
448
|
+
|
|
449
|
+
def download_attachment(self, attachment: EmailAttachment, download_folder: str = 'attachments'):
|
|
450
|
+
if not os.path.exists(download_folder):
|
|
451
|
+
os.makedirs(download_folder)
|
|
452
|
+
|
|
453
|
+
try:
|
|
454
|
+
|
|
455
|
+
with open(os.path.join(download_folder, attachment.filename), 'wb') as f:
|
|
456
|
+
f.write(self.get_attachment_payload(attachment))
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
except HttpError as e:
|
|
460
|
+
if e.resp.status == 403:
|
|
461
|
+
raise GmailPermissionError(f"Permission denied accessing attachment: {e}")
|
|
462
|
+
elif e.resp.status == 404:
|
|
463
|
+
raise AttachmentNotFoundError(f"Attachment not found: {e}")
|
|
464
|
+
else:
|
|
465
|
+
raise GmailError(f"Gmail API error downloading attachment: {e}")
|
|
466
|
+
except (ValueError, KeyError) as e:
|
|
467
|
+
raise GmailError(f"Invalid attachment data: {e}")
|
|
468
|
+
except Exception as e:
|
|
469
|
+
raise
|
|
470
|
+
|
|
471
|
+
def create_label(self, name: str) -> "Label":
|
|
472
|
+
"""
|
|
473
|
+
Creates a new label in Gmail.
|
|
474
|
+
Args:
|
|
475
|
+
name: The name of the label to create.
|
|
476
|
+
|
|
477
|
+
Returns:
|
|
478
|
+
A Label object representing the created label including its ID, name, and type.
|
|
479
|
+
"""
|
|
480
|
+
sanitized_name = name if len(name) <= 20 else f"{name[:20]}...({len(name)} chars)"
|
|
481
|
+
|
|
482
|
+
try:
|
|
483
|
+
label = self._service.users().labels().create(
|
|
484
|
+
userId='me',
|
|
485
|
+
body={'name': name, 'type': 'user'}
|
|
486
|
+
).execute()
|
|
487
|
+
return Label(
|
|
488
|
+
id=label.get('id'),
|
|
489
|
+
name=label.get('name'),
|
|
490
|
+
type=label.get('type', 'user')
|
|
491
|
+
)
|
|
492
|
+
except Exception as e:
|
|
493
|
+
raise
|
|
494
|
+
|
|
495
|
+
def list_labels(self) -> List["Label"]:
|
|
496
|
+
"""
|
|
497
|
+
Fetches a list of labels from Gmail.
|
|
498
|
+
Returns:
|
|
499
|
+
A list of Label objects representing the labels.
|
|
500
|
+
"""
|
|
501
|
+
|
|
502
|
+
try:
|
|
503
|
+
labels_response = self._service.users().labels().list(userId='me').execute()
|
|
504
|
+
labels = labels_response.get('labels', [])
|
|
505
|
+
|
|
506
|
+
labels_list = []
|
|
507
|
+
for label in labels:
|
|
508
|
+
labels_list.append(
|
|
509
|
+
Label(
|
|
510
|
+
id=label.get('id'),
|
|
511
|
+
name=label.get('name'),
|
|
512
|
+
type=label.get('type')
|
|
513
|
+
)
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
return labels_list
|
|
517
|
+
|
|
518
|
+
except Exception as e:
|
|
519
|
+
raise
|
|
520
|
+
|
|
521
|
+
@overload
|
|
522
|
+
def delete_label(self, label_id: str) -> bool: ...
|
|
523
|
+
@overload
|
|
524
|
+
def delete_label(self, label: Label) -> bool: ...
|
|
525
|
+
|
|
526
|
+
def delete_label(self, label: Union[Label, str]) -> bool:
|
|
527
|
+
"""
|
|
528
|
+
Deletes this label.
|
|
529
|
+
|
|
530
|
+
Args:
|
|
531
|
+
label: The label or label id to delete
|
|
532
|
+
|
|
533
|
+
Returns:
|
|
534
|
+
True if the label was successfully deleted, False otherwise.
|
|
535
|
+
"""
|
|
536
|
+
|
|
537
|
+
if isinstance(label, Label):
|
|
538
|
+
label = label.id
|
|
539
|
+
|
|
540
|
+
try:
|
|
541
|
+
self._service.users().labels().delete(userId='me', id=label).execute()
|
|
542
|
+
return True
|
|
543
|
+
except Exception as e:
|
|
544
|
+
return False
|
|
545
|
+
|
|
546
|
+
def update_label(self, label: Label, new_name: str) -> "Label":
|
|
547
|
+
"""
|
|
548
|
+
Updates the name of this label.
|
|
549
|
+
Args:
|
|
550
|
+
label: The label to update
|
|
551
|
+
new_name: The new name for the label.
|
|
552
|
+
|
|
553
|
+
Returns:
|
|
554
|
+
The updated Label object.
|
|
555
|
+
"""
|
|
556
|
+
|
|
557
|
+
try:
|
|
558
|
+
updated_label = self._service.users().labels().patch(
|
|
559
|
+
userId='me',
|
|
560
|
+
id=label.id,
|
|
561
|
+
body={'name': new_name}
|
|
562
|
+
).execute()
|
|
563
|
+
label.name = updated_label.get('name')
|
|
564
|
+
return label
|
|
565
|
+
except Exception as e:
|
|
566
|
+
raise
|
|
567
|
+
|
|
568
|
+
def list_threads(
|
|
569
|
+
self,
|
|
570
|
+
max_results: Optional[int] = DEFAULT_MAX_RESULTS,
|
|
571
|
+
query: Optional[str] = None,
|
|
572
|
+
include_spam_trash: bool = False,
|
|
573
|
+
label_ids: Optional[List[str]] = None
|
|
574
|
+
) -> List[EmailThread]:
|
|
575
|
+
"""
|
|
576
|
+
Fetches a list of threads from Gmail with optional filtering.
|
|
577
|
+
|
|
578
|
+
Args:
|
|
579
|
+
max_results: Maximum number of threads to retrieve. Defaults to 30.
|
|
580
|
+
query: Gmail search query string (same syntax as Gmail search).
|
|
581
|
+
include_spam_trash: Whether to include threads from spam and trash.
|
|
582
|
+
label_ids: List of label IDs to filter by.
|
|
583
|
+
|
|
584
|
+
Returns:
|
|
585
|
+
A list of EmailThread objects representing the threads found.
|
|
586
|
+
"""
|
|
587
|
+
# Input validation
|
|
588
|
+
if max_results and (max_results < 1 or max_results > MAX_RESULTS_LIMIT):
|
|
589
|
+
raise ValueError(f"max_results must be between 1 and {MAX_RESULTS_LIMIT}")
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
# Get list of thread IDs
|
|
594
|
+
request_params = {
|
|
595
|
+
'userId': 'me',
|
|
596
|
+
'maxResults': max_results,
|
|
597
|
+
'includeSpamTrash': include_spam_trash
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
if query:
|
|
601
|
+
request_params['q'] = query
|
|
602
|
+
if label_ids:
|
|
603
|
+
request_params['labelIds'] = label_ids
|
|
604
|
+
|
|
605
|
+
try:
|
|
606
|
+
result = self._service.users().threads().list(**request_params).execute()
|
|
607
|
+
threads = result.get('threads', [])
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
# Fetch full thread details
|
|
611
|
+
email_threads = []
|
|
612
|
+
for thread in threads:
|
|
613
|
+
try:
|
|
614
|
+
email_threads.append(self.get_thread(thread['id']))
|
|
615
|
+
except Exception as e:
|
|
616
|
+
pass
|
|
617
|
+
|
|
618
|
+
return email_threads
|
|
619
|
+
|
|
620
|
+
except Exception as e:
|
|
621
|
+
raise
|
|
622
|
+
|
|
623
|
+
def get_thread(self, thread_id: str) -> EmailThread:
|
|
624
|
+
"""
|
|
625
|
+
Retrieves a specific thread from Gmail using its unique identifier.
|
|
626
|
+
|
|
627
|
+
Args:
|
|
628
|
+
thread_id: The unique identifier of the thread to be retrieved.
|
|
629
|
+
|
|
630
|
+
Returns:
|
|
631
|
+
An EmailThread object representing the thread with all its messages.
|
|
632
|
+
"""
|
|
633
|
+
|
|
634
|
+
try:
|
|
635
|
+
gmail_thread = self._service.users().threads().get(
|
|
636
|
+
userId='me',
|
|
637
|
+
id=thread_id,
|
|
638
|
+
format='full'
|
|
639
|
+
).execute()
|
|
640
|
+
return utils.from_gmail_thread(gmail_thread)
|
|
641
|
+
except Exception as e:
|
|
642
|
+
raise
|
|
643
|
+
|
|
644
|
+
def delete_thread(self, thread: EmailThread, permanent: bool = False) -> bool:
|
|
645
|
+
"""
|
|
646
|
+
Deletes a thread (moves to trash or permanently deletes).
|
|
647
|
+
|
|
648
|
+
Args:
|
|
649
|
+
thread: The EmailThread object being deleted
|
|
650
|
+
permanent: If True, permanently deletes the thread. If False, moves to trash.
|
|
651
|
+
|
|
652
|
+
Returns:
|
|
653
|
+
True if the operation was successful, False otherwise.
|
|
654
|
+
"""
|
|
655
|
+
|
|
656
|
+
try:
|
|
657
|
+
if permanent:
|
|
658
|
+
self._service.users().threads().delete(userId='me', id=thread.thread_id).execute()
|
|
659
|
+
else:
|
|
660
|
+
self._service.users().threads().trash(userId='me', id=thread.thread_id).execute()
|
|
661
|
+
return True
|
|
662
|
+
except Exception as e:
|
|
663
|
+
return False
|
|
664
|
+
|
|
665
|
+
def modify_thread_labels(self, thread: EmailThread, add_labels: Optional[List[str]] = None,
|
|
666
|
+
remove_labels: Optional[List[str]] = None) -> bool:
|
|
667
|
+
"""
|
|
668
|
+
Modifies labels applied to a thread.
|
|
669
|
+
|
|
670
|
+
Args:
|
|
671
|
+
thread: The EmailThread object to modify labels for
|
|
672
|
+
add_labels: List of label IDs to add to the thread
|
|
673
|
+
remove_labels: List of label IDs to remove from the thread
|
|
674
|
+
|
|
675
|
+
Returns:
|
|
676
|
+
True if the operation was successful, False otherwise.
|
|
677
|
+
"""
|
|
678
|
+
|
|
679
|
+
if not add_labels and not remove_labels:
|
|
680
|
+
return True
|
|
681
|
+
|
|
682
|
+
try:
|
|
683
|
+
body = {}
|
|
684
|
+
if add_labels:
|
|
685
|
+
body['addLabelIds'] = add_labels
|
|
686
|
+
if remove_labels:
|
|
687
|
+
body['removeLabelIds'] = remove_labels
|
|
688
|
+
|
|
689
|
+
self._service.users().threads().modify(
|
|
690
|
+
userId='me',
|
|
691
|
+
id=thread.thread_id,
|
|
692
|
+
body=body
|
|
693
|
+
).execute()
|
|
694
|
+
|
|
695
|
+
return True
|
|
696
|
+
except Exception as e:
|
|
697
|
+
return False
|
|
698
|
+
|
|
699
|
+
def untrash_thread(self, thread: EmailThread) -> bool:
|
|
700
|
+
"""
|
|
701
|
+
Removes a thread from trash.
|
|
702
|
+
|
|
703
|
+
Args:
|
|
704
|
+
thread: The EmailThread object to untrash
|
|
705
|
+
|
|
706
|
+
Returns:
|
|
707
|
+
True if the operation was successful, False otherwise.
|
|
708
|
+
"""
|
|
709
|
+
|
|
710
|
+
try:
|
|
711
|
+
self._service.users().threads().untrash(userId='me', id=thread.thread_id).execute()
|
|
712
|
+
return True
|
|
713
|
+
except Exception as e:
|
|
714
|
+
return False
|
|
715
|
+
|