Giter VIP home page Giter VIP logo

pyo365's Introduction

This project has been merged into the original one. Check O365 repository.

This fork is no longer maintained. Bugs, and improvements will occurr in O365.




pyo365 - Microsoft Graph and Office 365 API made easy

This project aims is to make it easy to interact with Microsoft Graph and Office 365 Email, Contacts, Calendar, OneDrive, etc.

This project is inspired on the super work done by Toben Archer Python-O365. The oauth part is based on the work done by Royce Melborn which is now integrated with the original project.

I just want to make this project different in almost every sense, and make it also more pythonic. So I ended up rewriting the whole project from scratch.

The result is a package that provides a lot of the Microsoft Graph and Office 365 API capabilities.

This is for example how you send a message:

from pyo365 import Account

credentials = ('client_id', 'client_secret')

account = Account(credentials)
m = account.new_message()
m.to.add('[email protected]')
m.subject = 'Testing!'
m.body = "George Best quote: I've stopped drinking, but only while I'm asleep."
m.send()

Python 3.4 is the minimum required... I was very tempted to just go for 3.6 and use f-strings. Those are fantastic!

This project was also a learning resource for me. This is a list of not so common python characteristics used in this project:

  • New unpacking technics: def method(argument, *, with_name=None, **other_params):
  • Enums: from enum import Enum
  • Factory paradigm
  • Package organization
  • Timezone conversion and timezone aware datetimes
  • Etc. (see the code!)

This project is in early development. Changes that can break your code may be commited. If you want to help please feel free to fork and make pull requests.

What follows is kind of a wiki... but you will get more insights by looking at the code.

Table of contents

Install

pyo365 is available on pypi.org. Simply run pip install pyo365 to install it.

Project dependencies installed by pip:

  • requests
  • requests-oauthlib
  • beatifulsoup4
  • stringcase
  • python-dateutil
  • tzlocal
  • pytz

The first step to be able to work with this library is to register an application and retrieve the auth token. See Authentication.

Protocols

Protocols handles the aspects of comunications between different APIs. This project uses by default either the Office 365 APIs or Microsoft Graph APIs. But, you can use many other Microsoft APIs as long as you implement the protocol needed.

You can use one or the other:

Both protocols are similar but consider the following:

Reasons to use MSGraphProtocol:

  • It is the recommended Protocol by Microsoft.
  • It can access more resources over Office 365 (for example OneDrive)

Reasons to use MSOffice365Protocol:

  • It can send emails with attachments up to 150 MB. MSGraph only allows 4MB on each request.

The default protocol used by the Account Class is MSGraphProtocol.

You can implement your own protocols by inheriting from Protocol to communicate with other Microsoft APIs.

You can instantiate protocols like this:

from pyo365 import MSGraphProtocol

# try the api version beta of the Microsoft Graph endpoint.
protocol = MSGraphProtocol(api_version='beta')  # MSGraphProtocol defaults to v1.0 api version
Resources:

Each API endpoint requires a resource. This usually defines the owner of the data. Every protocol defaults to resource 'ME'. 'ME' is the user which has given consent, but you can change this behaviour by providing a different default resource to the protocol constructor.

For example when accesing a shared mailbox:

# ...
account = Account(credentials=my_credentials, main_resource='[email protected]')
# Any instance created using account will inherit the resource defined for account.

This can be done however at any point. For example at the protocol level:

# ...
my_protocol = MSGraphProtocol(default_resource='[email protected]')

account = Account(credentials=my_credentials, protocol=my_protocol)

# now account is accesing the [email protected] in every api call.
shared_mailbox_messages = account.mailbox().get_messages()

Instead of defining the resource used at the account or protocol level, you can provide it per use case as follows:

# ...
account = Account(credentials=my_credentials)  # account defaults to 'ME' resource

mailbox = account.mailbox('[email protected]')  # mailbox is using '[email protected]' resource instead of 'ME'

# or:

message = Message(parent=account, main_resource='[email protected]')  # message is using '[email protected]' resource

Usually you will work with the default 'ME' resuorce, but you can also use one of the following:

  • 'me': the user which has given consent. the default for every protocol.
  • 'user:[email protected]': a shared mailbox or a user account for which you have permissions. If you don't provide 'user:' will be infered anyways.
  • 'sharepoint:sharepoint-site-id': a sharepoint site id.
  • 'group:group-site-id': a office365 group id.

Authentication

You can only authenticate using oauth athentication as Microsoft deprecated basic oauth on November 1st 2018.

  • Oauth authentication: using an authentication token provided after user consent.

The Connection Class handles the authentication.

Oauth Authentication

This section is explained using Microsoft Graph Protocol, almost the same applies to the Office 365 REST API.

Permissions and Scopes:

When using oauth you create an application and allow some resources to be accesed and used by it's users. Then the user can request access to one or more of this resources by providing scopes to the oauth provider.

For example your application can have Calendar.Read, Mail.ReadWrite and Mail.Send permissions, but the application can request access only to the Mail.ReadWrite and Mail.Send permission. This is done by providing scopes to the connection object like so:

from pyo365 import Connection

credentials = ('client_id', 'client_secret')

scopes = ['https://graph.microsoft.com/Mail.ReadWrite', 'https://graph.microsoft.com/Mail.Send']

con = Connection(credentials, scopes=scopes)

Scope implementation depends on the protocol used. So by using protocol data you can automatically set the scopes needed:

You can get the same scopes as before using protocols like this:

protocol_graph = MSGraphProtocol()

scopes_graph = protocol.get_scopes_for('message all')
# scopes here are: ['https://graph.microsoft.com/Mail.ReadWrite', 'https://graph.microsoft.com/Mail.Send']

protocol_office = MSOffice365Protocol()

scopes_office = protocol.get_scopes_for('message all')
# scopes here are: ['https://outlook.office.com/Mail.ReadWrite', 'https://outlook.office.com/Mail.Send']

con = Connection(credentials, scopes=scopes_graph)
Authentication Flow
  1. To work with oauth you first need to register your application at Microsoft Application Registration Portal.

    1. Login at Microsoft Application Registration Portal
    2. Create an app, note your app id (client_id)
    3. Generate a new password (client_secret) under "Application Secrets" section
    4. Under the "Platform" section, add a new Web platform and set "https://outlook.office365.com/owa/" as the redirect URL
    5. Under "Microsoft Graph Permissions" section, add the delegated permissions you want (see scopes), as an example, to read and send emails use:
      1. Mail.ReadWrite
      2. Mail.Send
      3. User.Read
  2. Then you need to login for the first time to get the access token by consenting the application to access the resources it needs.

    1. First get the authorization url.

      url = account.connection.get_authorization_url()
    2. The user must visit this url and give consent to the application. When consent is given, the page will rediret to: "https://outlook.office365.com/owa/".

      Then the user must copy the resulting page url and give it to the connection object:

      result_url = input('Paste the result url here...')
      
      account.connection.request_token(result_url)  # This, if succesful, will store the token in a txt file on the user project folder.

      Take care, the access token must remain protected from unauthorized users.

    3. At this point you will have an access token that will provide valid credentials when using the api. If you change the scope requested, then the current token won't work, and you will need the user to give consent again on the application to gain access to the new scopes requested.

    The access token only lasts 60 minutes, but the app will automatically request new tokens through the refresh tokens, but note that a refresh token only lasts for 90 days. So you must use it before or you will need to request a new access token again (no new consent needed by the user, just a login).

    If your application needs to work for more than 90 days without user interaction and without interacting with the API, then you must implement a periodic call to Connection.refresh_token before the 90 days have passed.

Using pyo365 to authenticate

You can manually authenticate by using a single Connection instance as described before or use the helper methods provided by the library.

  1. account.authenticate:

    This is the preferred way for performing authentication.

    Create an Account instance and authenticate using the authenticate method:

    from pyo365 import Account
    
    account = Account(credentials=('client_id', 'client_secret'))
    result = account.authenticate(scopes=['basic', 'message_all'])  # request a token for this scopes
    
    # this will ask to visit the app consent screen where the user will be asked to give consent on the requested scopes.
    # then the user will have to provide the result url afeter consent. 
    # if all goes as expected, result will be True and a token will be stored in the default location.
  2. oauth_authentication_flow:

    from pyo365 import oauth_authentication_flow
    
    result = oauth_authentication_flow('client_id', 'client_secret', ['scopes_required'])

Account Class and Modularity

Usually you will only need to work with the Account Class. This is a wrapper around all functionality.

But you can also work only with the pieces you want.

For example, instead of:

from pyo365 import Account

account = Account(('client_id', 'client_secret'))
message = account.new_message()
# ...
mailbox = account.mailbox()
# ...

You can work only with the required pieces:

from pyo365 import Connection, MSGraphProtocol, Message, MailBox

my_protocol = MSGraphProtocol()
con = Connection(('client_id', 'client_secret'))

message = Message(con=con, protocol=my_protocol)
# ...
mailbox = MailBox(con=con, protocol=my_protocol)
message2 = Message(parent=mailbox)  # message will inherit the connection and protocol from mailbox when using parent.
# ...

It's also easy to implement a custom Class.

Just Inherit from ApiComponent, define the endpoints, and use the connection to make requests. If needed also inherit from Protocol to handle different comunications aspects with the API server.

from pyo365.utils import ApiComponent 

class CustomClass(ApiComponent):
    _endpoints = {'my_url_key': '/customendpoint'}
    
    def __init__(self, *, parent=None, con=None, **kwargs):
        super().__init__(parent=parent, con=con, **kwargs)
        # ...

    def do_some_stuff(self):
        
        # self.build_url just merges the protocol service_url with the enpoint passed as a parameter
        # to change the service_url implement your own protocol inherinting from Protocol Class
        url = self.build_url(self._endpoints.get('my_url_key'))  
        
        my_params = {'param1': 'param1'}

        response = self.con.get(url, params=my_params)  # note the use of the connection here.

        # handle response and return to the user...

MailBox

Mailbox groups the funcionality of both the messages and the email folders.

mailbox = account.mailbox()

inbox = mailbox.inbox_folder()

for message in inbox.get_messages():
    print(message)

sent_folder = mailbox.sent_folder()

for message in sent_folder.get_messages():
    print(message)

m = mailbox.new_message()

m.to.add('[email protected]')
m.body = 'George Best quote: In 1969 I gave up women and alcohol - it was the worst 20 minutes of my life.'
m.save_draft()

Email Folder

Represents a Folder within your email mailbox.

You can get any folder in your mailbox by requesting child folders or filtering by name.

mailbox = account.mailbox()

archive = mailbox.get_folder(folder_name='archive')  # get a folder with 'archive' name

child_folders = archive.get_folders(25) # get at most 25 child folders of 'archive' folder

for folder in child_folders:
    print(folder.name, folder.parent_id)

new_folder = archive.create_child_folder('George Best Quotes')

Message

An email object with all it's data and methods.

Creating a draft message is as easy as this:

message = mailbox.new_message()
message.to.add(['[email protected]', '[email protected]'])
message.sender.address = '[email protected]'  # changing the from address
message.body = 'George Best quote: I might go to Alcoholics Anonymous, but I think it would be difficult for me to remain anonymous'
message.attachments.add('george_best_quotes.txt')
message.save_draft()  # save the message on the cloud as a draft in the drafts folder

Working with saved emails is also easy:

query = mailbox.new_query().on_attribute('subject').contains('george best')  # see Query object in Utils
messages = mailbox.get_messages(limit=25, query=query)

message = messages[0]  # get the first one

message.mark_as_read()
reply_msg = message.reply()

if '[email protected]' in reply_msg.to:  # magic methods implemented
    reply_msg.body = 'George Best quote: I spent a lot of money on booze, birds and fast cars. The rest I just squandered.'
else:
    reply_msg.body = 'George Best quote: I used to go missing a lot... Miss Canada, Miss United Kingdom, Miss World.'

reply_msg.send()

AddressBook

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

Contact Folders

Represents a Folder within your Contacts Section in Office 365. AddressBook class represents the parent folder (it's a folder itself).

You can get any folder in your address book by requesting child folders or filtering by name.

address_book = account.address_book()

contacts = address_book.get_contacts(limit=None)  # get all the contacts in the Personal Contacts root folder

work_contacts_folder = address_book.get_folder(folder_name='Work Contacts')  # get a folder with 'Work Contacts' name

message_to_all_contats_in_folder = work_contacts_folder.new_message()  # creates a draft message with all the contacts as recipients

message_to_all_contats_in_folder.subject = 'Hallo!'
message_to_all_contats_in_folder.body = """
George Best quote:

If you'd given me the choice of going out and beating four men and smashing a goal in
from thirty yards against Liverpool or going to bed with Miss World,
it would have been a difficult choice. Luckily, I had both.
"""
message_to_all_contats_in_folder.send()

# querying folders is easy:
child_folders = address_book.get_folders(25) # get at most 25 child folders

for folder in child_folders:
    print(folder.name, folder.parent_id)

# creating a contact folder:
address_book.create_child_folder('new folder')

The Global Address List

Office 365 API (Nor MS Graph API) has no concept such as the Outlook Global Address List. However you can use the Users API to access all the users within your organization.

Without admin consent you can only access a few properties of each user such as name and email and litte more. You can search by name or retrieve a contact specifying the complete email.

  • Basic Permision needed is Users.ReadBasic.All (limit info)
  • Full Permision is Users.Read.All but needs admin consent.

To search the Global Address List (Users API):

global_address_list = account.address_book(address_book='gal')

# start a new query:
q = global_address_list.new_query('display_name')
q.startswith('George Best')

print(global_address_list.get_contacts(query=q))

To retrieve a contact by it's email:

contact = global_address_list.get_contact_by_email('[email protected]')

Contacts

Everything returned from an AddressBook instance is a Contact instance. Contacts have all the information stored as attributes

Creating a contact from an AddressBook:

new_contact = address_book.new_contact()

new_contact.name = 'George Best'
new_contact.job_title = 'football player'
new_contact.emails.add('[email protected]')

new_contact.save()  # saved on the cloud

message = new_contact.new_message()  #  Bonus: send a message to this contact

# ...

new_contact.delete()  # Bonus: deteled from the cloud

Calendar

The calendar and events functionality is group in a Schedule object.

A Schedule instance can list and create calendars. It can also list or create events on the default user calendar. To use other calendars use a Calendar instance.

Working with the Schedule instance:

import datetime as dt

# ...
schedule = account.schedule()

new_event = schedule.new_event()  # creates a new event in the user default calendar
new_event.subject = 'Recruit George Best!'
new_event.location = 'England'

# naive datetimes will automatically be converted to timezone aware datetime
#  objects using the local timezone detected or the protocol provided timezone

new_event.start = dt.datetime(2018, 9, 5, 19, 45) 
# so new_event.start becomes: datetime.datetime(2018, 9, 5, 19, 45, tzinfo=<DstTzInfo 'Europe/Paris' CEST+2:00:00 DST>)

new_event.recurrence.set_daily(1, end=dt.datetime(2018, 9, 10))
new_event.remind_before_minutes = 45

new_event.save()

Working with Calendar instances:

calendar = schedule.get_calendar(calendar_name='Birthdays')

calendar.name = 'Football players birthdays'
calendar.update()

q = calendar.new_query('start').ge(dt.datetime(2018, 5, 20)).chain('and').on_attribute('end').le(dt.datetime(2018, 5, 24))

birthdays = calendar.get_events(query=q)

for event in birthdays:
    if event.subject == 'George Best Birthday':
        # He died in 2005... but we celebrate anyway!
        event.accept("I'll attend!")  # send a response accepting
    else:
        event.decline("No way I'm comming, I'll be in Spain", send_response=False)  # decline the event but don't send a reponse to the organizer

OneDrive

The Storage class handles all functionality around One Drive and Document Library Storage in Sharepoint.

The Storage instance allows to retrieve Drive instances which handles all the Files and Folders from within the selected Storage. Usually you will only need to work with the default drive. But the Storage instances can handle multiple drives.

A Drive will allow you to work with Folders and Files.

account = Account(credentials=my_credentials)

storage = account.storage()  # here we get the storage instance that handles all the storage options.

# list all the drives:
drives = storage.get_drives()

# get the default drive
my_drive = storage.get_default_drive()  # or get_drive('drive-id')

# get some folders:
root_folder = my_drive.get_root_folder()
attachments_folder = my_drive.get_special_folder('attachments')

# iterate over the first 25 items on the root folder
for item in root_folder.get_items(limit=25):
    if item.is_folder:
        print(item.get_items(2))  # print the first to element on this folder.
    elif item.is_file:
        if item.is_photo:
            print(item.camera_model)  # print some metadata of this photo
        elif item.is_image:
            print(item.dimensione)  # print the image dimensions
        else:
            # regular file:
            print(item.mime_type)  # print the mime type

Both Files and Folders are DriveItems. Both Image and Photo are Files, but Photo is also an Image. All have some different methods and properties. Take care when using 'is_xxxx'.

When coping a DriveItem the api can return a direct copy of the item or a pointer to a resource that will inform on the progress of the copy operation.

# copy a file to the documents special folder

documents_folder = drive.get_special_folder('documents')

files = drive.search('george best quotes', limit=1)

if files:
    george_best_quotes = files[0]
    operation = george_best_quotes.copy(target=documents_folder)  # operation here is an instance of CopyOperation
    
    # to check for the result just loop over check_status.
    # check_status is a generator that will yield a new status and progress until the file is finally copied
    for status, progress in operation.check_status():  # if it's an async operations, this will request to the api for the status in every loop
        print('{} - {}'.format(status, progress))  # prints 'in progress - 77.3' until finally completed: 'completed - 100.0'
    copied_item = operation.get_item()  # the copy operation is completed so you can get the item.
    if copied_item:
        copied_item.delete()  # ... oops!

You can also work with share permissions:

current_permisions = file.get_permissions()  # get all the current permissions on this drive_item (some may be inherited)

# share with link
permission = file.share_with_link(share_type='edit')
if permission:
    print(permission.share_link)  # the link you can use to share this drive item
# share with invite
permission = file.share_with_invite(recipients='[email protected]', send_email=True, message='Greetings!!', share_type='edit')
if permission:
    print(permission.granted_to)  # the person you share this item with

You can also:

# download files:
file.download(to_path='/quotes/')

# upload files:

# if the uploaded file is bigger than 4MB the file will be uploaded in chunks of 5 MB until completed.
# this can take several requests and can be time consuming.
uploaded_file = folder.upload_file(item='path_to_my_local_file')

# restore versions:
versiones = file.get_versions()
for version in versions:
    if version.name == '2.0':
        version.restore()  # restore the version 2.0 of this file

# ... and much more ...

Sharepoint

Work in progress

Utils

Pagination

When using certain methods, it is possible that you request more items than the api can return in a single api call. In this case the Api, returns a "next link" url where you can pull more data.

When this is the case, the methods in this library will return a Pagination object which abstracts all this into a single iterator. The pagination object will request "next links" as soon as they are needed.

For example:

maibox = account.mailbox()

messages = mailbox.get_messages(limit=1500)  # the Office 365 and MS Graph API have a 999 items limit returned per api call.

# Here messages is a Pagination instance. It's an Iterator so you can iterate over.

# The first 999 iterations will be normal list iterations, returning one item at a time.
# When the iterator reaches the 1000 item, the Pagination instance will call the api again requesting exactly 500 items
# or the items specified in the batch parameter (see later).

for message in messages:
    print(message.subject)

When using certain methods you will have the option to specify not only a limit option (the number of items to be returned) but a batch option. This option will indicate the method to request data to the api in batches until the limit is reached or the data consumed. This is usefull when you want to optimize memory or network latency.

For example:

messages = mailbox.get_messages(limit=100, batch=25)

# messages here is a Pagination instance
# when iterating over it will call the api 4 times (each requesting 25 items).

for message in messages:  # 100 loops with 4 requests to the api server
    print(message.subject)

The Query helper

When using the Office 365 API you can filter some fields. This filtering is tedious as is using Open Data Protocol (OData).

Every ApiComponent (such as MailBox) implements a new_query method that will return a Query instance. This Query instance can handle the filtering (and sorting and selecting) very easily.

For example:

query = mailbox.new_query()

query = query.on_attribute('subject').contains('george best').chain('or').startswith('quotes')

# 'created_date_time' will automatically be converted to the protocol casing.
# For example when using MS Graph this will become 'createdDateTime'.

query = query.chain('and').on_attribute('created_date_time').greater(datetime(2018, 3, 21))

print(query)

# contains(subject, 'george best') or startswith(subject, 'quotes') and createdDateTime gt '2018-03-21T00:00:00Z'
# note you can pass naive datetimes and those will be converted to you local timezone and then send to the api as UTC in iso8601 format

# To use Query objetcs just pass it to the query parameter:
filtered_messages = mailbox.get_messages(query=query)

You can also specify specific data to be retrieved with "select":

# select only some properties for the retrieved messages:
query = mailbox.new_query().select('subject', 'to_recipients', 'created_date_time')

messages_with_selected_properties = mailbox.get_messages(query=query)

Request Error Handling and Custom Errors

Whenever a Request error raises, the connection object will raise an exception. Then the exception will be captured and logged it to the stdout with it's message, an return Falsy (None, False, [], etc...)

HttpErrors 4xx (Bad Request) and 5xx (Internal Server Error) are considered exceptions and raised also by the connection (you can configure this on the connection).

pyo365's People

Contributors

adityarj avatar ajwillo avatar alexnbferreira avatar blastter avatar eseiver avatar fcollman avatar grif392 avatar jameswinegar avatar mparisot-fa avatar narcolapser avatar phoenixalx avatar sampellino avatar shaqowski avatar thebigbear avatar tuomas56 avatar vincentadriaensen avatar yenthe666 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

pyo365's Issues

setup.py

Error when installing library with Python 3.6, setup.py uses 'distutils.core' and python recommends 'setuptools'

mark_as_unread

Currently, it appears that forwarding emails using the library marks the forwarded message as "read." Is there a way to prevent this behavior, or alternatively, mark the message as unread like we can use mark_as_read right now?

Drive access does not work when accessing sharepoint drives.

How to reproduce

resource = "sites/{hostname}:/{type}/{page}:".format(hostname=company_hostname, type='sites', page="page/subpage")
protocol = MSGraphProtocol()
account = Account(credentials=credentials, protocol=protocol)
storage = account.storage(resource=resource)
drives = storage.get_drives()

for drive in drives:
    drive.get_items() # not working because the _base_url is wrong

Temporary Fix

drive._base_url = drive._base_url.replace(resource, "")

Solution

Drive items cannot be accessed under the resource of a sharepoint.
The drives should be always initialized with the root base_url /drives/{drive_id}.

Feel free to close it ^^

body_type (html and text)

This is more of a feature request than an issue, but it would be nice to be able send emails in both html and text formats (i.e. multiple body_types, for maximum compatibility to people that may not have an HTML compatible client, or prefers to read email as text only). I know how to do this with the python smtplib + email libraries, but to keep things simplified, would be beneficial to be able to do this natively with "only" pyo365.

If this is already possible, I haven't figured out how to make it work.

Number of Attachments Count

Hi Alejandro,

I dumped the message dictionary and found this for the one message with an attachment.

'_Message__attachments': Number of Attachments: unknown

Besides counting each attachment as I loop through the attachments to give me a final count, is there another place the attachment count would be?

TIA,
Joe

Error: Unable to fetch auth token. Error: (invalid_request) AADSTS90014: The request body must contain the following parameter: 'client_id'.

This is a just reporting.
On my O365 enterprise environment, account.connection.request_token fails with following error.

Unable to fetch auth token. Error: (invalid_request) AADSTS90014: The request body must contain the following parameter: 'client_id'.

To resolve issue, I modified request_token in connection.py as follows.

    def request_token(self, authorizated_url, store_token=True, token_path=None):
:
:
        if self.session is None:
            raise RuntimeError("Fist call 'get_authorization_url' to generate a valid oauth object")
        client_id, client_secret = self.auth  #<<<<
:
:
        try:
            self.token = self.session.fetch_token(token_url=self._oauth2_token_url,
                                                  authorization_response=authorizated_url,
                                                  client_secret=client_secret,
                                                  client_id=client_id) #<<<<

Saving new contacts

Hi,

I tried a code-example, but unfortunately that failed: (version 1.01 from PyPi):

    new_contact = address_book.new_contact()
    new_contact.display_name = "George"
    new_contact.emails.add('[email protected]')
    new_contact.save()

gives the error:

requests.exceptions.HTTPError: 405 Client Error: Method Not Allowed for url: https://graph.microsoft.com/v1.0/me/contacts/...

What am I doing wrong?

I also tried to save a contact into a contact-folder like this:

        contactbooks = address_book.get_folders()
        new_contact = contactbooks[0].new_contact()
        new_contact.display_name = "George"
        new_contact.emails.add('[email protected]')
        new_contact.save()

This gives another internal error:

.../lib/python3.6/site-packages/O365/address_book.py", line 211, in save
self._endpoints.get('child_contact').format(self.folder_id))
KeyError: 'id'

Any ideas?

Best regards and many thanks!

Forwarded message fail to send

I'm sorry for being such a bother, but I came across another issue.

Below is an example of my basic code that is giving me issues-- it's really basic, so I'm not sure what exactly would be causing issues. Basically, when I create a new "forwarded" message, it will allow me to save it (which saves it to my drafts folder) but when I use try to send it, I'm getting the following error:

'requests.exceptions.HTTPError: 400 Client Error: Bad Request for url: https://graph.microsoft.com/v1.0/me/messages/AAMkADliMTc3ZDNiLWZjZmEtNDFjYy04NzcwLWJmZjFiMmJhMDlmYwBGAAAAAACpHnAhRNt-Q4njDfQdqjvXBwAOQfZ77YdrRoTaU4t-LnG7AAAAAAArAABzMA6FP7fBQZ4lrv4MbsYlAAGaPoL5AAA=/send'

I've already (successfully) grabbed the message that I want to forward, as "message".

            forward_message = message.forward()
            
            forward_message.to.add('[email protected]')
            forward_message.body = email_html
            forward_message.send()

As I mentioned, if I go into outlook, I can see my fully formed email sitting there in my drafts folder if I use save_draft() before I use send(), again, not really sure what I'm doing wrong.

filter by receivedDateTime

It appears the pyo365 generates the following URL when using the receivedDateTime filter:
'https://graph.microsoft.com/v1.0/me/messages?%24top=25&%24filter=receivedDateTime+ge+%272018-10-29T05%3A00%3A00%2B00%3A00%27

This generates a 400 error with the Microsoft Graph API: Failure: Status Code 400

I can manually correct the URL to https://graph.microsoft.com/v1.0/me/messages?%24top=25&%24filter=receivedDateTime+ge+2018-10-29T05%3A00%3A00%2B00%3A00
and it works fine (note that all I did was take out the single quotations/%27 from the datetime stamp.)

I'm not EXACTLY sure where this URL is being generated, or I would attempt to submit a patch myself. (If I figure it out, I may still do this.)

Refresh Token

Hi Alejandro,

I'm now stuck trying to refresh the token. I'm trying to write the code so the user authenticates once and then the token is refreshed every hour. I've read this section

If your application needs to work for more than 90 days without user interaction and without interacting with the API, then you must implement a periodic call to Connection.refresh_token before the 90 days have passed.

I haven't been able to figure out what is needed to refresh the token. My code looks like this

def main():
    token_file = Path(TOKEN_PATH)

    if not token_file.is_file():
        authentication()

    while True:
        try:
            print_email()
        except TokenExpiredError:
            create_time = os.stat(TOKEN_PATH).st_ctime
            print("Create Time", create_time)

            Connection.refresh_token()

The code checks to see if the token file exists. If it doesn't exist, then create it. A first login. Then loop forever looking for email. When the token expires check the create time and then try to do a refresh of the token.

I must be missing something when it comes refreshing the token.

TIA,
Joe

Question about get_authorization_url

Hi Alejandro,

I'm walking through the README.md working out the order of the code needed to pull from a mailbox and I ran into this error

Traceback (most recent call last):
File "", line 1, in
File "/anaconda3/lib/python3.7/site-packages/pyo365/connection.py", line 301, in get_authorization_url
raise ValueError('Must provide at least one scope')
ValueError: Must provide at least one scope

This is the order of the steps I did.

Python 3.7.0 (default, Jun 28 2018, 07:39:16)
[Clang 4.0.1 (tags/RELEASE_401/final)] :: Anaconda, Inc. on darwin
Type "help", "copyright", "credits" or "license" for more information.

url = account.connection.get_authorization_url()
Traceback (most recent call last):
File "", line 1, in
NameError: name 'account' is not defined
from pyo365 import Account
credentials = ('client_id', 'client_secret')
account = Account(credentials)
url = account.connection.get_authorization_url()
Traceback (most recent call last):
File "", line 1, in
File "/anaconda3/lib/python3.7/site-packages/pyo365/connection.py", line 301, in get_authorization_url
raise ValueError('Must provide at least one scope')
ValueError: Must provide at least one scope
scopes = ['https://graph.microsoft.com/Mail.ReadWrite', 'https://graph.microsoft.com/Mail.Send']
from pyo365 import Connection
con = Connection(credentials, scopes=scopes)
url = account.connection.get_authorization_url()
Traceback (most recent call last):
File "", line 1, in
File "/anaconda3/lib/python3.7/site-packages/pyo365/connection.py", line 301, in get_authorization_url
raise ValueError('Must provide at least one scope')
ValueError: Must provide at least one scope

Even though I set the scope I must have missed a step I must have set it the wrong way. What is needed to get the url line to work?

TIA,
Joe

List names of message attachments

Hi Alejandro,

I've been poking at the pyo365 code and I still don't see how I can list the message attachment names. I haven't figured out how you count the number of attachments and then list the attachment names. Is there a method or property that I'm missing?

TIA,
Joe

Unable to complete authentication

Hi,

I tried to use the first example to send a message but it said No auth token found. Authentication Flow needed. So I follow the authentication flow section. I completed the registration and input my clientID and password. At 2i get the authorisation url, I tried url = account.connection.get_authorization_url(). I got the message that "Must provide at least one scope".

I am not sure what this means so I tried the Using pyo365 to authenticate section. I ran result = oauth_authentication_flow('client_id', 'client_secret', ['scopes_required']) and I got a url. I visited the url to provide consent then the web direct me to the outlook mailbox page. I paste the url to Paste the authenticated url here but then my python freezed.

Could anyone help me and advise how I could fix this issue?

Many thanks,
Wen

erroneous call to _to_api_data

When working with addressbooks, I hit a problem with pyo365/address_book.py:124:

cf. pyo365/address_book.py : 112ff

       data = {
            'displayName': self.display_name,
            'givenName': self.name,
            'surname': self.surname,
            'title': self.title,
            'jobTitle': self.job_title,
            'companyName': self.company_name,
            'department': self.department,
            'officeLocation': self.office_location,
            'businessPhones': self.business_phones,
            'mobilePhone': self.mobile_phone,
            'homePhones': self.home_phones,
            'emailAddresses': self.emails.to_api_data(),
            'businessAddress': self.business_addresses,
            'homesAddress': self.home_addresses,
            'otherAddress': self.other_addresses,
            'categories': self.categories}
        return data

This gives the error:

AttributeError: 'Recipients' object has no attribute 'to_api_data'

I changed pyo365/address_book.py to:

       data = {
            'displayName': self.display_name,
            'givenName': self.name,
            'surname': self.surname,
            'title': self.title,
            'jobTitle': self.job_title,
            'companyName': self.company_name,
            'department': self.department,
            'officeLocation': self.office_location,
            'businessPhones': self.business_phones,
            'mobilePhone': self.mobile_phone,
            'homePhones': self.home_phones,
            'emailAddresses': self.emails,
            'businessAddress': self.business_addresses,
            'homesAddress': self.home_addresses,
            'otherAddress': self.other_addresses,
            'categories': self.categories}
        return data

Now this seems to work!?

Change output of message.body from HTML to text

Hi Alejandro,

I would like to change the output of the message.body from HTML to text. I see in the body_type request that body_type can be set to use text for sending mail. Can something similar be done to print the email message as text?

Tia,
Joe

Publish PIP package?

Hello,
I am looking at using your package in one of opensource automation software, do you have any plans to publish this as pip package/PyPi ?

Thanks

Global address book filter with multiple paramaters

I seem to be having problems using a query with multiple parameters. The code below works if the "q.endswith" line is removed.

global_address_list = account.address_book(address_book='gal')
q = global_address_list.new_query('display_name')
q.startswith("John")
q.endswith("Smith")
print(global_address_list.get_contacts(query=q, limit=5))

Client Error: 400 Client Error: Bad Request for url: https://graph.microsoft.com/v1.0/users?%24top=5&%24filter=startswith%28displayName%2C+%27John%27%29+and+endswith%28displayName%2C+%27Smith%27%29

get_contacts() -> KeyError: 'id'

Hi,

perhaps I am missing something, but using addressbook's get_contacts()
expects a mapping in line 286, at least I think so:

cf. pyo365/address_book.py:283ff:

[..]
if self.root:
    url = self.build_url(self._endpoints.get('root_contacts'))
else:
    url = self.build_url(self._endpoints.get('folder_contacts').format(self.folder_id))
[..]

Using the code in original form gives:

KeyError: 'id'

But changing line 286 into

[..]
if self.root:
    url = self.build_url(self._endpoints.get('root_contacts'))
else:
    url = self.build_url(self._endpoints.get('folder_contacts').format(id=self.folder_id))
[..]

appears to work fine. If I didn't miss anything, perhaps You'll add this change
to the code!?

Kind regards

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.