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.
Files changed (39) hide show
  1. google_api_client_wrapper-1.0.0.dist-info/METADATA +103 -0
  2. google_api_client_wrapper-1.0.0.dist-info/RECORD +39 -0
  3. google_api_client_wrapper-1.0.0.dist-info/WHEEL +5 -0
  4. google_api_client_wrapper-1.0.0.dist-info/licenses/LICENSE +21 -0
  5. google_api_client_wrapper-1.0.0.dist-info/top_level.txt +1 -0
  6. google_client/__init__.py +6 -0
  7. google_client/services/__init__.py +13 -0
  8. google_client/services/calendar/__init__.py +14 -0
  9. google_client/services/calendar/api_service.py +454 -0
  10. google_client/services/calendar/constants.py +48 -0
  11. google_client/services/calendar/exceptions.py +35 -0
  12. google_client/services/calendar/query_builder.py +314 -0
  13. google_client/services/calendar/types.py +403 -0
  14. google_client/services/calendar/utils.py +338 -0
  15. google_client/services/drive/__init__.py +13 -0
  16. google_client/services/drive/api_service.py +1133 -0
  17. google_client/services/drive/constants.py +37 -0
  18. google_client/services/drive/exceptions.py +60 -0
  19. google_client/services/drive/query_builder.py +385 -0
  20. google_client/services/drive/types.py +242 -0
  21. google_client/services/drive/utils.py +392 -0
  22. google_client/services/gmail/__init__.py +16 -0
  23. google_client/services/gmail/api_service.py +715 -0
  24. google_client/services/gmail/constants.py +6 -0
  25. google_client/services/gmail/exceptions.py +45 -0
  26. google_client/services/gmail/query_builder.py +408 -0
  27. google_client/services/gmail/types.py +285 -0
  28. google_client/services/gmail/utils.py +426 -0
  29. google_client/services/tasks/__init__.py +12 -0
  30. google_client/services/tasks/api_service.py +561 -0
  31. google_client/services/tasks/constants.py +32 -0
  32. google_client/services/tasks/exceptions.py +35 -0
  33. google_client/services/tasks/query_builder.py +324 -0
  34. google_client/services/tasks/types.py +156 -0
  35. google_client/services/tasks/utils.py +224 -0
  36. google_client/user_client.py +208 -0
  37. google_client/utils/__init__.py +0 -0
  38. google_client/utils/datetime.py +144 -0
  39. 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
+