Skip to content

Commit ff50a30

Browse files
author
Alejandro Casanovas
committed
You can now save Messages and attached messages as EML files. Fix for googleapis#97.
1 parent c31b258 commit ff50a30

File tree

3 files changed

+95
-4
lines changed

3 files changed

+95
-4
lines changed

O365/account.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ def mailbox(self, resource=None):
139139
:param str resource: Custom resource to be used in this mailbox
140140
(Defaults to parent main_resource)
141141
:return: a representation of account mailbox
142-
:rtype: MailBox
142+
:rtype: O365.mailbox.MailBox
143143
"""
144144
from .mailbox import MailBox
145145
return MailBox(parent=self, main_resource=resource, name='MailBox')

O365/message.py

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
# noinspection PyPep8Naming
66
from bs4 import BeautifulSoup as bs
77
from dateutil.parser import parse
8+
from pathlib import Path
89

910
from .utils import OutlookWellKnowFolderNames, ApiComponent, \
1011
BaseAttachments, BaseAttachment, AttachableMixin, ImportanceLevel, \
@@ -31,17 +32,55 @@ class Flag(CaseEnum):
3132
class MessageAttachment(BaseAttachment):
3233
_endpoints = {
3334
'attach': '/messages/{id}/attachments',
34-
'attachment': '/messages/{id}/attachments/{ida}'
35+
'attachment': '/messages/{id}/attachments/{ida}',
3536
}
3637

3738

3839
class MessageAttachments(BaseAttachments):
3940
_endpoints = {
4041
'attachments': '/messages/{id}/attachments',
41-
'attachment': '/messages/{id}/attachments/{ida}'
42+
'attachment': '/messages/{id}/attachments/{ida}',
43+
'get_mime': '/messages/{id}/attachments/{ida}/$value',
4244
}
4345
_attachment_constructor = MessageAttachment
4446

47+
def save_as_eml(self, attachment, to_path=None):
48+
""" Saves this message as and EML to the file system
49+
:param MessageAttachment attachment: the MessageAttachment to store as eml.
50+
:param Path or str to_path: the path where to store this file
51+
"""
52+
if not attachment or not isinstance(attachment, MessageAttachment) \
53+
or attachment.attachment_id is None or attachment.attachment_type != 'item':
54+
raise ValueError('Must provide a saved "item" attachment of type MessageAttachment')
55+
56+
if to_path is None:
57+
to_path = Path()
58+
else:
59+
if not isinstance(to_path, Path):
60+
to_path = Path(to_path)
61+
62+
if not to_path.suffix:
63+
to_path = to_path.with_suffix('.eml')
64+
65+
msg_id = self._parent.object_id
66+
if msg_id is None:
67+
raise RuntimeError('Attempting to get the mime contents of an unsaved message')
68+
69+
url = self.build_url(self._endpoints.get('get_mime').format(id=msg_id, ida=attachment.attachment_id))
70+
71+
response = self._parent.con.get(url)
72+
73+
if not response:
74+
return False
75+
76+
mime_content = response.content
77+
78+
if mime_content:
79+
with to_path.open('wb') as file_obj:
80+
file_obj.write(mime_content)
81+
return True
82+
return False
83+
4584

4685
class MessageFlag(ApiComponent):
4786
""" A flag on a message """
@@ -172,7 +211,8 @@ class Message(ApiComponent, AttachableMixin, HandleRecipientsMixin):
172211
'copy_message': '/messages/{id}/copy',
173212
'create_reply': '/messages/{id}/createReply',
174213
'create_reply_all': '/messages/{id}/createReplyAll',
175-
'forward_message': '/messages/{id}/createForward'
214+
'forward_message': '/messages/{id}/createForward',
215+
'get_mime': '/messages/{id}/$value',
176216
}
177217

178218
def __init__(self, *, parent=None, con=None, **kwargs):
@@ -961,3 +1001,39 @@ def get_event(self):
9611001
event_data = data.get(self._cc('event'))
9621002

9631003
return Event(parent=self, **{self._cloud_data_key: event_data})
1004+
1005+
def get_mime_content(self):
1006+
""" Returns the MIME contents of this message """
1007+
if self.object_id is None:
1008+
raise RuntimeError('Attempting to get the mime contents of an unsaved message')
1009+
1010+
url = self.build_url(self._endpoints.get('get_mime').format(id=self.object_id))
1011+
1012+
response = self.con.get(url)
1013+
1014+
if not response:
1015+
return None
1016+
1017+
return response.content
1018+
1019+
def save_as_eml(self, to_path=None):
1020+
""" Saves this message as and EML to the file system
1021+
:param Path or str to_path: the path where to store this file
1022+
"""
1023+
1024+
if to_path is None:
1025+
to_path = Path()
1026+
else:
1027+
if not isinstance(to_path, Path):
1028+
to_path = Path(to_path)
1029+
1030+
if not to_path.suffix:
1031+
to_path = to_path.with_suffix('.eml')
1032+
1033+
mime_content = self.get_mime_content()
1034+
1035+
if mime_content:
1036+
with to_path.open('wb') as file_obj:
1037+
file_obj.write(mime_content)
1038+
return True
1039+
return False

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -733,7 +733,22 @@ print(msg.message_headers) # returns a list of dicts.
733733

734734
Note that only message headers and other properties added to the select statement will be present.
735735

736+
##### Saving as EML
737+
Messages and attached messages can be saved as *.eml.
736738

739+
- Save message as "eml":
740+
```python
741+
msg.save_as_eml(to_path=Path('my_saved_email.eml'))
742+
```
743+
- Save attached message as "eml":
744+
745+
Carefull: there's no way to identify that an attachment is in fact a message. You can only check if the attachment.attachment_type == 'item'.
746+
if is of type "item" then it can be a message (or an event, etc...). You will have to determine this yourself.
747+
748+
```python
749+
msg_attachment = msg.attachments[0] # the first attachment is attachment.attachment_type == 'item' and I know it's a message.
750+
mg.attachments.save_as_eml(msg_attachment, to_path=Path('my_saved_email.eml'))
751+
```
737752

738753
## AddressBook
739754
AddressBook groups the funcionality of both the Contact Folders and Contacts. Outlook Distribution Groups are not supported (By the Microsoft API's).

0 commit comments

Comments
 (0)