linkedin / pyexchange

Python wrapper for Microsoft Exchange
Apache License 2.0
153 stars 93 forks source link

Creating Contacts in Outlook [Started development] #60

Open Yenthe666 opened 9 years ago

Yenthe666 commented 9 years ago

Hi guys,

Since this library had some amazing implementations and did a lot of the things I needed I decided to expand it and put some effort in to it. I'm not used to anything related Exchange / SOAP but tried to manage with what was already here. I've forked this repository and added as much as I could for now. You can see my added code here: https://github.com/linkedin/pyexchange/compare/master...Yenthe666:master

I've now ran stuck on the following error:

pyexchange.exceptions.FailedExchangeException: Unable to connect to Exchange: 500 Server Error: Internal Server Error

Which comes from exceptions.py in the function FailedExchangeException:

class FailedExchangeException(Exception):
  """Raised when the Microsoft Exchange Server returns an error via SOAP for a request."""
  pass

Example code to create a new contact:

from pyexchange import Exchange2010Service, ExchangeNTLMAuthConnection

URL = u'https://mail.yourdomain.com/EWS/Exchange.asmx'
USERNAME = u'DOMAIN\\yenthe'
PASSWORD = u"mypassword"
from datetime import datetime
from pytz import timezone

# Set up the connection to Exchange
connection = ExchangeNTLMAuthConnection(url=URL,
                                        username=USERNAME,
                                        password=PASSWORD)

service = Exchange2010Service(connection)

# You can set event properties when you instantiate the event...
contact = service.contact().new_contact(
  name=u"Yenthe",
  company_name = u"Google",
)

# Connect to Exchange and create the event
contact.create()

Could anybody help me and finish this implementation? I do not know how to fix this issue but I think this is a good start.

Yenthe666 commented 9 years ago

@trustrachel, @got-root, @ematthews could I ask you guys for input please?

catermelon commented 9 years ago

So sorry about the delay - I don't officially maintain this any more. :) I'm at a different company and don't have access to an Exchange server, so I can only give advice.

So the error you're getting probably means the XML you're sending Exchange is either malformed or not correct, so Exchange is confused and doesn't know what to do. PyExchange is just passing on the error.

The first thing to do is test sending the raw SOAP request by using this script. Just change the REQUEST string to whatever XML you're sending:

https://gist.github.com/trustrachel/b70d79ea670f048ed165

If that doesn't work, then just fix as necessary and off you go. If it does work, let me know and I can dig more.

thank you!

Yenthe666 commented 9 years ago

@trustrachel no problem for the delay, I'm happy you took the time to respond. So thanks a lot for that :smile: Alright so I gave your testscript a run with a demo script from SharePoint to create a new contact. I've used the sample from https://msdn.microsoft.com/en-us/library/aa580529(v=exchg.140).aspx in your testscript (https://gist.github.com/trustrachel/b70d79ea670f048ed165)

How the testscript looks:

#!/usr/bin/env python

import os
import logging
import requests
from requests_ntlm import HttpNtlmAuth
import getpass
import xml.dom.minidom

logging.basicConfig(level=logging.DEBUG)

EXCHANGE_SERVER = 'https://mail.mymail.com'
DOMAIN = 'MyDomain'
USERNAME = 'yenthe'#os.environ.get('yenthe.vanginneken') or 'DUMMY_USERNAME'
# PASSWORD is prompted for

# This just asks the server for all the timezones it knows about
# http://msdn.microsoft.com/en-us/library/dd899430(v=exchg.140).aspx
REQUEST = """<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xmlns:xsd="http://www.w3.org/2001/XMLSchema"
 xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
 xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types">
  <soap:Body>
    <CreateItem xmlns="http://schemas.microsoft.com/exchange/services/2006/messages" >
      <SavedItemFolderId>
        <t:DistinguishedFolderId Id="contacts"/>
      </SavedItemFolderId>
      <Items>
        <t:Contact>
          <t:FileAs>SampleContact</t:FileAs>
          <t:GivenName>Tanja</t:GivenName>
          <t:CompanyName>Blue Yonder Airlines</t:CompanyName>
          <t:EmailAddresses>
            <t:Entry Key="EmailAddress1">tplate@example.com</t:Entry>
          </t:EmailAddresses>
          <t:PhysicalAddresses>
            <t:Entry Key="Business">
              <t:Street>1234 56th Ave</t:Street>
              <t:City>La Habra</t:City>
              <t:State>CA</t:State>
              <t:CountryOrRegion>USA</t:CountryOrRegion>
            </t:Entry>
          </t:PhysicalAddresses>
          <t:PhoneNumbers>
            <t:Entry Key="BusinessPhone">4255550199</t:Entry>
          </t:PhoneNumbers>
          <t:JobTitle>Manager</t:JobTitle>
          <t:Surname>Plate</t:Surname>
        </t:Contact>
      </Items>
    </CreateItem>
  </soap:Body>
</soap:Envelope>"""

if __name__ == '__main__':

  if EXCHANGE_SERVER == '' or USERNAME == "DUMMY_USERNAME" or DOMAIN == "DOMAIN":
    raise SystemExit("Hey, you need to edit the script to add your custom information before you run it.")

  PASSWORD = os.environ.get('EXCHANGE_PASSWORD') or getpass.getpass()

  HEADERS = {
    'Content-type': 'text/xml; charset=utf-8 ',
    'Accept': 'text/xml'
  }

  response = requests.post(EXCHANGE_SERVER,
               auth=HttpNtlmAuth('%s\\%s' % (DOMAIN, USERNAME), PASSWORD), 
               data=REQUEST, 
               headers=HEADERS)

  # lxml is better, but all I want to do is pretty print the XML response
  print(xml.dom.minidom.parseString(response.text).toprettyxml())

When I run this script I will get the following results back (so a succesfull creation):

INFO:urllib3.connectionpool:Starting new HTTPS connection (1): mail.mymail.com
DEBUG:urllib3.connectionpool:Setting read timeout to None
DEBUG:urllib3.connectionpool:"POST /EWS/Exchange.asmx HTTP/1.1" 401 0
DEBUG:urllib3.connectionpool:Setting read timeout to None
DEBUG:urllib3.connectionpool:"POST /EWS/Exchange.asmx HTTP/1.1" 401 0
DEBUG:urllib3.connectionpool:Setting read timeout to None
DEBUG:urllib3.connectionpool:"POST /EWS/Exchange.asmx HTTP/1.1" 200 None
<?xml version="1.0" ?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
    <s:Header>
        <h:ServerVersionInfo MajorBuildNumber="174" MajorVersion="14" MinorBuildNumber="1" MinorVersion="3" xmlns="http://schemas.microsoft.com/exchange/services/2006/types" xmlns:h="http://schemas.microsoft.com/exchange/services/2006/types" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"/>
    </s:Header>
    <s:Body xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
        <m:CreateItemResponse xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages" xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types">
            <m:ResponseMessages>
                <m:CreateItemResponseMessage ResponseClass="Success">
                    <m:ResponseCode>NoError</m:ResponseCode>
                    <m:Items>
                        <t:Contact>
                            <t:ItemId ChangeKey="EQAAABYAAACrppuUEDMRTaciavm/Wr6lAAB7y06S" Id="AAAdAFllbnRoZS5WYW5HaW5uZWtlbkB2YW5yb2V5LmJlAEYAAAAAADrZHpTamLVLl+s1ej9c6goHAKumm5QQMxFNpyJq+b9avqUAAAAAv9wAAKumm5QQMxFNpyJq+b9avqUAAHvLRpIAAA=="/>
                        </t:Contact>
                    </m:Items>
                </m:CreateItemResponseMessage>
            </m:ResponseMessages>
        </m:CreateItemResponse>
    </s:Body>
</s:Envelope>

Now, back to the code that I've added to the library, which you can find here: https://github.com/Yenthe666/pyexchange/blob/master/pyexchange/exchange2010/soap_request.py#L634-L683 The method to create a new contact:

def new_contact(contact):
    """
    Requests a new Contact to be created
    https://msdn.microsoft.com/en-us/library/aa564690(v=exchg.140).aspx
    <m:CreateItem 
    xmlns=m:"http://schemas.microsoft.com/exchange/services/2006/messages"
    xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types">
      <m:SavedItemFolderId>
        <t:DistinguishedFolderId Id="contacts"/>
      </m:SavedItemFolderId>
      <m:Items>
        <t:Contact>
          <t:FileAs>SampleContact</t:FileAs>
          <t:GivenName>{contact.subject}</t:GivenName>
          <t:CompanyName>{contact.body}</t:CompanyName>
          <t:EmailAddresses>
            <t:Entry Key="EmailAddress1">tplate@example.com</t:Entry>
          </t:EmailAddresses>
          <t:PhysicalAddresses>
            <t:Entry Key="Business">
              <t:Street>1234 56th Ave</t:Street>
              <t:City>La Habra</t:City>
              <t:State>CA</t:State>
              <t:CountryOrRegion>USA</t:CountryOrRegion>
            </t:Entry>
          </t:PhysicalAddresses>
          <t:PhoneNumbers>
            <t:Entry Key="BusinessPhone">4255550199</t:Entry>
          </t:PhoneNumbers>
          <t:JobTitle>Manager</t:JobTitle>
          <t:Surname>Plate</t:Surname>
        </t:Contact>
      </m:Items>
    </m:CreateItem>
    """
    id = T.DistinguishedFolderId(Id=contact.calendar_id) if contact.calendar_id in DISTINGUISHED_IDS else T.FolderId(Id=calendar.contact_id)
    root = M.CreateItem(
      M.SavedItemFolderId(id),
      M.Items(
        T.Contact(
          T.GivenName(contact.name),
          T.CompanyName(contact.company_name or u'', BodyType="HTML"),
        )
      ),
      SendMeetingInvitations="SendToAllAndSaveCopy"
    )
    contact_node = root.xpath(u'/m:CreateItem/m:Items/t:Contact', namespaces=NAMESPACES)[0]

    return root

I have no idea what is wrong with my code though.. Think you could have a look at it too? There must be something wrong.

catermelon commented 9 years ago

Aha, you're not sending the XML you expect. You see where you're setting the root variable? There's a stray SendMeetingInvitations="SendToAllAndSaveCopy", which is only for calendar stuff.

Try getting rid of that. If that still doesn't work, try upping the logging to debug and you should see exactly what's going over the wire to Exchange and what you're getting back. IIRC, Exchange will tell you if you have malformed XML.

catermelon commented 9 years ago

This is the one real flaw in using raw XML instead of a more heavyweight SOAP library, we don't get the benefit of XML schemas. :disappointed:

Yenthe666 commented 9 years ago

@trustrachel thanks for the feedback, good catch there! I've removed the line SendMeetingInvitations="SendToAllAndSaveCopy" so I now have the following code:

def new_contact(contact):
    """
    Requests a new Contact to be created
    https://msdn.microsoft.com/en-us/library/aa564690(v=exchg.140).aspx
    <m:CreateItem 
    xmlns=m:"http://schemas.microsoft.com/exchange/services/2006/messages"
    xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types">
      <m:SavedItemFolderId>
        <t:DistinguishedFolderId Id="contacts"/>
      </m:SavedItemFolderId>
      <m:Items>
        <t:Contact>
          <t:FileAs>SampleContact</t:FileAs>
          <t:GivenName>{contact.subject}</t:GivenName>
          <t:CompanyName>{contact.body}</t:CompanyName>
          <t:EmailAddresses>
            <t:Entry Key="EmailAddress1">tplate@example.com</t:Entry>
          </t:EmailAddresses>
          <t:PhysicalAddresses>
            <t:Entry Key="Business">
              <t:Street>1234 56th Ave</t:Street>
              <t:City>La Habra</t:City>
              <t:State>CA</t:State>
              <t:CountryOrRegion>USA</t:CountryOrRegion>
            </t:Entry>
          </t:PhysicalAddresses>
          <t:PhoneNumbers>
            <t:Entry Key="BusinessPhone">4255550199</t:Entry>
          </t:PhoneNumbers>
          <t:JobTitle>Manager</t:JobTitle>
          <t:Surname>Plate</t:Surname>
        </t:Contact>
      </m:Items>
    </m:CreateItem>
    """
    id = T.DistinguishedFolderId(Id=contact.calendar_id) if contact.calendar_id in DISTINGUISHED_IDS else T.FolderId(Id=calendar.contact_id)
    root = M.CreateItem(
      M.SavedItemFolderId(id),
      M.Items(
        T.Contact(
          T.GivenName(contact.name),
          T.CompanyName(contact.company_name or u'', BodyType="HTML"),
        )
      ),
    )
    contact_node = root.xpath(u'/m:CreateItem/m:Items/t:Contact', namespaces=NAMESPACES)[0]

    return root

So, where exactly is the debugger level defined? I can't find it directly. And yep the debugging is horrid so it seems. Perhaps I should just go with the SOAP without using the library..

catermelon commented 9 years ago

It's just using the standard Python logger - you set it in whatever script you're using to run pyexchange. The easiest thing is to just set the root logger to DEBUG, but I think you can just target pyexchange with logger.getLogger('pyexchange'). If you don't know how to do that, see this: https://docs.python.org/2/howto/logging.html#configuring-logging

SOAP will also work, but I found that they're all either very slow or don't handle unicode.

Yenthe666 commented 9 years ago

@trustrachel I finally had some time to look at it and found the fix. The problem was some HTML field that wasn't allowed there to be used in that way. :+1: Now I have a new error:

AttributeError: 'Exchange2010ContactEvent' object has no attribute '_parse_id_and_change_key_from_response'

Think you could have a look at this? I've made an issue on my own branch here: https://github.com/Yenthe666/pyexchange/issues/1

It looks to me like my self programmed Exchange2010ContactEvent class and Exchange2010FolderService are missing something or are doing something wrong.

Yenthe666 commented 8 years ago

@trustrachel sorry to ping you again but could you have a look please?