├── peas ├── pyActiveSync │ ├── __init__.py │ ├── objects │ │ ├── __init__.py │ │ ├── MSASRM.py │ │ ├── MSASDTYPE.py │ │ ├── MSASNOTE.py │ │ ├── MSASDOC.py │ │ ├── MSASHTTP.py │ │ ├── MSASTASK.py │ │ ├── MSASCNTC.py │ │ └── MSASAIRS.py │ ├── client │ │ ├── __init__.py │ │ ├── GetAttachment.py │ │ ├── SendMail.py │ │ ├── FolderDelete.py │ │ ├── FolderUpdate.py │ │ ├── MoveItems.py │ │ ├── FolderCreate.py │ │ ├── SmartReply.py │ │ ├── SmartForward.py │ │ ├── Ping.py │ │ ├── ValidateCert.py │ │ ├── MeetingResponse.py │ │ ├── Search.py │ │ ├── FolderSync.py │ │ ├── GetItemEstimate.py │ │ ├── ResolveRecipients.py │ │ ├── Sync.py │ │ ├── ItemOperations.py │ │ └── Provision.py │ ├── utils │ │ ├── __init__.py │ │ ├── code_page.py │ │ ├── wapxml.py │ │ └── wbxml.py │ └── misc_tests.py ├── __init__.py ├── eas_client │ ├── __init__.py │ ├── autodiscovery.py │ └── activesync_producers.py ├── py_eas_helper.py ├── peas.py ├── __main__.py └── py_activesync_helper.py ├── scripts └── peas ├── screenshots └── main.png ├── setup.py ├── extractemails.py ├── LICENSE.md └── README.md /peas/pyActiveSync/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /peas/pyActiveSync/objects/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /peas/__init__.py: -------------------------------------------------------------------------------- 1 | from peas import * 2 | -------------------------------------------------------------------------------- /scripts/peas: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | python2 -m peas.__main__ "$@" 4 | -------------------------------------------------------------------------------- /peas/eas_client/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ["autodiscovery","activesync","activesync_producers"] -------------------------------------------------------------------------------- /screenshots/main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReversecLabs/peas/HEAD/screenshots/main.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from distutils.core import setup 4 | 5 | setup(name='PEAS', 6 | version='1.0', 7 | description='ActiveSync Library', 8 | author='Adam Rutherford', 9 | author_email='adam.rutherford@mwrinfosecurity.com', 10 | packages=['peas', 'peas.eas_client', 11 | 'peas.pyActiveSync', 'peas.pyActiveSync.client', 'peas.pyActiveSync.objects', 'peas.pyActiveSync.utils'], 12 | scripts=['scripts/peas'], 13 | ) 14 | -------------------------------------------------------------------------------- /extractemails.py: -------------------------------------------------------------------------------- 1 | """Example script using PEAS to extract emails.""" 2 | 3 | __author__ = 'Adam Rutherford' 4 | 5 | import sys 6 | import os 7 | import time 8 | import random 9 | import subprocess 10 | from pprint import pprint 11 | 12 | import peas 13 | import _creds 14 | 15 | 16 | def main(): 17 | 18 | peas.show_banner() 19 | 20 | client = peas.Peas() 21 | 22 | client.set_creds(_creds.CREDS) 23 | 24 | print("Extracting all emails with pyActiveSync") 25 | client.set_backend(peas.PY_ACTIVE_SYNC) 26 | 27 | emails = client.extract_emails() 28 | 29 | pprint(emails) 30 | print 31 | 32 | print("Extracting all emails with py-eas-client") 33 | client.set_backend(peas.PY_EAS_CLIENT) 34 | 35 | emails = client.extract_emails() 36 | 37 | pprint(emails) 38 | print 39 | 40 | 41 | if __name__ == '__main__': 42 | main() 43 | -------------------------------------------------------------------------------- /peas/pyActiveSync/client/__init__.py: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Copyright (C) 2013 Sol Birnbaum 3 | # 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either version 2 7 | # of the License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 17 | # MA 02110-1301, USA. 18 | ######################################################################## -------------------------------------------------------------------------------- /peas/pyActiveSync/utils/__init__.py: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Copyright (C) 2013 Sol Birnbaum 3 | # 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either version 2 7 | # of the License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 17 | # MA 02110-1301, USA. 18 | ######################################################################## -------------------------------------------------------------------------------- /peas/pyActiveSync/objects/MSASRM.py: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Copyright (C) 2013 Sol Birnbaum 3 | # 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either version 2 7 | # of the License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 17 | # MA 02110-1301, USA. 18 | ######################################################################## 19 | 20 | """[MS-ASRM] Rights Management objects""" -------------------------------------------------------------------------------- /peas/pyActiveSync/objects/MSASDTYPE.py: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Copyright (C) 2013 Sol Birnbaum 3 | # 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either version 2 7 | # of the License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 17 | # MA 02110-1301, USA. 18 | ######################################################################## 19 | 20 | """[MS-ASDTYPE] MS-AS data types objects""" 21 | 22 | class datatype_TimeZone: 23 | def get_local_timezone_bytes(): 24 | #TODO 25 | return 26 | def get_timezone_bytes(timezone): 27 | #TODO 28 | return 29 | class Timezones: 30 | GMT = 0 31 | #TODO 32 | -------------------------------------------------------------------------------- /peas/pyActiveSync/client/GetAttachment.py: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Copyright (C) 2013 Sol Birnbaum 3 | # 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either version 2 7 | # of the License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 17 | # MA 02110-1301, USA. 18 | ######################################################################## 19 | 20 | from ..utils.wapxml import wapxmltree, wapxmlnode 21 | 22 | class GetAttachment: 23 | """http://msdn.microsoft.com/en-us/library/ee218451(v=exchg.80).aspx""" 24 | @staticmethod 25 | def build(*arg): 26 | raise NotImplementedError("GetAttachment is just an HTTP Post with 'AttachmentName=' as a command parameter. No wapxml building necessary.") 27 | 28 | @staticmethod 29 | def parse(*arg): 30 | raise NotImplementedError("GetAttachment is just an HTTP Post with 'AttachmentName=' as a command parameter. No wapxml parsing necessary.") 31 | -------------------------------------------------------------------------------- /peas/py_eas_helper.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Adam Rutherford' 2 | 3 | from twisted.internet import reactor 4 | 5 | import eas_client.activesync 6 | 7 | 8 | def body_result(result, emails, num_emails): 9 | 10 | emails.append(result['Properties']['Body']) 11 | 12 | # Stop after receiving final email. 13 | if len(emails) == num_emails: 14 | reactor.stop() 15 | 16 | 17 | def sync_result(result, fid, async, emails): 18 | 19 | assert hasattr(result, 'keys') 20 | 21 | num_emails = len(result.keys()) 22 | 23 | for fetch_id in result.keys(): 24 | 25 | async.add_operation(async.fetch, collectionId=fid, serverId=fetch_id, 26 | fetchType=4, mimeSupport=2).addBoth(body_result, emails, num_emails) 27 | 28 | 29 | def fsync_result(result, async, emails): 30 | 31 | for (fid, finfo) in result.iteritems(): 32 | if finfo['DisplayName'] == 'Inbox': 33 | async.add_operation(async.sync, fid).addBoth(sync_result, fid, async, emails) 34 | break 35 | 36 | 37 | def prov_result(success, async, emails): 38 | 39 | if success: 40 | async.add_operation(async.folder_sync).addBoth(fsync_result, async, emails) 41 | else: 42 | reactor.stop() 43 | 44 | 45 | def extract_emails(creds): 46 | 47 | emails = [] 48 | 49 | async = eas_client.activesync.ActiveSync(creds['domain'], creds['user'], creds['password'], 50 | creds['server'], True, device_id=creds['device_id'], verbose=False) 51 | 52 | async.add_operation(async.provision).addBoth(prov_result, async, emails) 53 | 54 | reactor.run() 55 | 56 | return emails 57 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 MWR InfoSecurity 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | * Neither the name of MWR InfoSecurity nor the names of its contributors 13 | may be used to endorse or promote products derived from this software 14 | without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL MWR INFOSECURITY BE LIABLE FOR ANY 20 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | 28 | This licence does not apply to the following components: 29 | 30 | - pyActiveSync, released under the GNU GENERAL PUBLIC LICENSE and available to download from: https://github.com/solbirn/pyActiveSync 31 | - py-eas-client, released under the GNU GENERAL PUBLIC LICENSE and available to download from: https://github.com/ghiewa/py-eas-client/ -------------------------------------------------------------------------------- /peas/pyActiveSync/utils/code_page.py: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Copyright (C) 2013 Sol Birnbaum 3 | # 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either version 2 7 | # of the License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 17 | # MA 02110-1301, USA. 18 | ######################################################################## 19 | 20 | 21 | class code_page(object): 22 | """A code page is a map of tokens to tags""" 23 | def __init__(self, namespace=None, xmlns=None, index=None): 24 | self.namespace = namespace 25 | self.xmlns = xmlns 26 | self.index = index 27 | self.tokens = {} 28 | self.tags = {} 29 | 30 | def add(self, token, tag): 31 | self.tags.update({ token : tag }) 32 | self.tokens.update({ tag : token }) 33 | 34 | def get(self, t, token_or_tag): 35 | if t == 0: 36 | return get_token(token_or_tag) 37 | elif t == 1: 38 | return get_tag(token_or_tag) 39 | 40 | def get_token(self, tag): 41 | return self.tokens[tag] 42 | 43 | def get_tag(self, token): 44 | #print token, self.xmlns 45 | return self.tags[token] 46 | 47 | def __repr__(self): 48 | import pprint 49 | return "\r\n Namespace:%s - Xmlns:%s\r\n%s\r\n" % (self.namespace, self.xmlns, pprint.pformat(self.tokens)) 50 | 51 | def __iter__(self): 52 | lnamespace = self.namespace 53 | lxmlns = self.xmlns 54 | for tag, token in self.tags.items(): 55 | yield (lnamespace, lxmlns, tag, token) 56 | -------------------------------------------------------------------------------- /peas/pyActiveSync/objects/MSASNOTE.py: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Copyright (C) 2013 Sol Birnbaum 3 | # 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either version 2 7 | # of the License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 17 | # MA 02110-1301, USA. 18 | ######################################################################## 19 | 20 | """[MS-ASNOTE] Note objects""" 21 | 22 | from MSASEMAIL import airsyncbase_Body 23 | 24 | def parse_note(data): 25 | note_dict = {} 26 | note_base = data.get_children() 27 | note_dict.update({"server_id" : note_base[0].text}) 28 | note_elements = note_base[1].get_children() 29 | for element in note_elements: 30 | if element.tag == "airsyncbase:Body": 31 | body = airsyncbase_Body() 32 | body.parse(element) 33 | note_dict.update({ "airsyncbase_Body" : body }) 34 | elif element.tag == "notes:Subject": 35 | note_dict.update({ "notes_Subject" : element.text }) 36 | elif element.tag == "notes:MessageClass": 37 | note_dict.update({ "notes_MessageClass" : element.text }) 38 | elif element.tag == "notes:LastModifiedDate": 39 | note_dict.update({ "notes_LastModifiedDate" : element.text }) 40 | elif element.tag == "notes:Categories": 41 | categories_list = [] 42 | categories = element.get_children() 43 | for category_element in categories: 44 | categories_list.append(category_element.text) 45 | note_dict.update({ "notes_Categories" : categories_list }) 46 | return note_dict -------------------------------------------------------------------------------- /peas/pyActiveSync/objects/MSASDOC.py: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Copyright (C) 2013 Sol Birnbaum 3 | # 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either version 2 7 | # of the License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 17 | # MA 02110-1301, USA. 18 | ######################################################################## 19 | 20 | """[MS-ASDOC] Document objects""" 21 | 22 | @staticmethod 23 | def parse_document(data): 24 | document_dict = {} 25 | document_base = data.get_children() 26 | document_dict.update({"server_id" : document_base[0].text}) 27 | document_elements = document_base[1].get_children() 28 | for element in document_elements: 29 | if element.tag == "documentlibrary:ContentLength": 30 | document_dict.update({ "documentlibrary_ContentLength" : element.text }) 31 | elif element.tag == "documentlibrary:ContentType": 32 | document_dict.update({ "documentlibrary_ContentType" : element.text }) 33 | elif element.tag == "documentlibrary:CreationDate": 34 | document_dict.update({ "documentlibrary_CreationDate" : element.text }) 35 | elif element.tag == "documentlibrary:DisplayName": 36 | document_dict.update({ "documentlibrary_DisplayName" : element.text }) 37 | elif element.tag == "documentlibrary:IsFolder": 38 | document_dict.update({ "documentlibrary_IsFolder" : element.text }) 39 | elif element.tag == "documentlibrary:IsHidden": 40 | document_dict.update({ "documentlibrary_IsHidden" : element.text }) 41 | elif element.tag == "documentlibrary:LastModifiedDate": 42 | document_dict.update({ "documentlibrary_LastModifiedDate" : element.text }) 43 | elif element.tag == "documentlibrary:LinkId": 44 | document_dict.update({ "documentlibrary_LinkId" : element.text }) 45 | return document_dict -------------------------------------------------------------------------------- /peas/pyActiveSync/client/SendMail.py: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Copyright (C) 2013 Sol Birnbaum 3 | # 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either version 2 7 | # of the License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 17 | # MA 02110-1301, USA. 18 | ######################################################################## 19 | 20 | from ..utils.wapxml import wapxmltree, wapxmlnode 21 | 22 | class SendMail: 23 | """http://msdn.microsoft.com/en-us/library/ee178477(v=exchg.80).aspx""" 24 | 25 | @staticmethod 26 | def build(client_id, mime, account_id=None, save_in_sent_items=True, template_id=None): 27 | sendmail_xmldoc_req = wapxmltree() 28 | xmlrootnode = wapxmlnode("SendMail") 29 | sendmail_xmldoc_req.set_root(xmlrootnode, "composemail") 30 | xml_clientid_node = wapxmlnode("ClientId", xmlrootnode, client_id) 31 | if account_id: 32 | xml_accountid_node = wapxmlnode("AccountId", xmlrootnode, account_id) 33 | xml_saveinsentiems_node = wapxmlnode("SaveInSentItems", xmlrootnode, str(int(save_in_sent_items))) 34 | xml_mime_node = wapxmlnode("Mime", xmlrootnode, None, mime) 35 | #xml_templateid_node = wapxmlnode("rm:TemplateID", xmlrootnode, template_id) 36 | return sendmail_xmldoc_req 37 | 38 | @staticmethod 39 | def parse(wapxml): 40 | 41 | namespace = "composemail" 42 | root_tag = "SendMail" 43 | 44 | root_element = wapxml.get_root() 45 | if root_element.get_xmlns() != namespace: 46 | raise AttributeError("Xmlns '%s' submitted to '%s' parser. Should be '%s'." % (root_element.get_xmlns(), root_tag, namespace)) 47 | if root_element.tag != root_tag: 48 | raise AttributeError("Root tag '%s' submitted to '%s' parser. Should be '%s'." % (root_element.tag, root_tag, root_tag)) 49 | 50 | sendmail_children = root_element.get_children() 51 | 52 | status = None 53 | 54 | for element in sendmail_children: 55 | if element.tag is "Status": 56 | status = element.text 57 | return status 58 | 59 | -------------------------------------------------------------------------------- /peas/pyActiveSync/client/FolderDelete.py: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Copyright (C) 2013 Sol Birnbaum 3 | # 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either version 2 7 | # of the License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 17 | # MA 02110-1301, USA. 18 | ######################################################################## 19 | 20 | from ..utils.wapxml import wapxmltree, wapxmlnode 21 | 22 | class FolderDelete: 23 | """http://msdn.microsoft.com/en-us/library/ee201525(v=exchg.80).aspx""" 24 | 25 | @staticmethod 26 | def build(synckey, server_id): 27 | folderdelete_xmldoc_req = wapxmltree() 28 | xmlrootnode = wapxmlnode("FolderDelete") 29 | folderdelete_xmldoc_req.set_root(xmlrootnode, "folderhierarchy") 30 | xmlsynckeynode = wapxmlnode("SyncKey", xmlrootnode, synckey) 31 | xmlserveridnode = wapxmlnode("ServerId", xmlrootnode, server_id) 32 | return folderdelete_xmldoc_req 33 | 34 | @staticmethod 35 | def parse(wapxml): 36 | 37 | namespace = "folderhierarchy" 38 | root_tag = "FolderDelete" 39 | 40 | root_element = wapxml.get_root() 41 | if root_element.get_xmlns() != namespace: 42 | raise AttributeError("Xmlns '%s' submitted to '%s' parser. Should be '%s'." % (root_element.get_xmlns(), root_tag, namespace)) 43 | if root_element.tag != root_tag: 44 | raise AttributeError("Root tag '%s' submitted to '%s' parser. Should be '%s'." % (root_element.tag, root_tag, root_tag)) 45 | 46 | folderhierarchy_folderdelete_children = root_element.get_children() 47 | 48 | folderhierarchy_folderdelete_status = None 49 | folderhierarchy_folderdelete_synckey = None 50 | folderhierarchy_folderdelete_serverid = None 51 | 52 | for element in folderhierarchy_folderdelete_children: 53 | if element.tag is "Status": 54 | folderhierarchy_folderdelete_status = element.text 55 | if folderhierarchy_folderdelete_status != "1": 56 | print "FolderDelete Exception: %s" % folderhierarchy_folderdelete_status 57 | elif element.tag == "SyncKey": 58 | folderhierarchy_folderdelete_synckey = element.text 59 | return (folderhierarchy_folderdelete_status, folderhierarchy_folderdelete_synckey) -------------------------------------------------------------------------------- /peas/pyActiveSync/client/FolderUpdate.py: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Copyright (C) 2013 Sol Birnbaum 3 | # 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either version 2 7 | # of the License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 17 | # MA 02110-1301, USA. 18 | ######################################################################## 19 | 20 | from ..utils.wapxml import wapxmltree, wapxmlnode 21 | 22 | class FolderUpdate: 23 | """http://msdn.microsoft.com/en-us/library/ee160573(v=exchg.80).aspx""" 24 | 25 | @staticmethod 26 | def build(synckey, server_id, parent_id, display_name): 27 | folderupdate_xmldoc_req = wapxmltree() 28 | xmlrootnode = wapxmlnode("FolderUpdate") 29 | folderupdate_xmldoc_req.set_root(xmlrootnode, "folderhierarchy") 30 | xmlsynckeynode = wapxmlnode("SyncKey", xmlrootnode, synckey) 31 | xmlserveridnode = wapxmlnode("ServerId", xmlrootnode, server_id) 32 | xmlparentidnode = wapxmlnode("ParentId", xmlrootnode, parent_id) 33 | xmldisplaynamenode = wapxmlnode("DisplayName", xmlrootnode, display_name) 34 | return folderupdate_xmldoc_req 35 | 36 | @staticmethod 37 | def parse(wapxml): 38 | 39 | namespace = "folderhierarchy" 40 | root_tag = "FolderUpdate" 41 | 42 | root_element = wapxml.get_root() 43 | if root_element.get_xmlns() != namespace: 44 | raise AttributeError("Xmlns '%s' submitted to '%s' parser. Should be '%s'." % (root_element.get_xmlns(), root_tag, namespace)) 45 | if root_element.tag != root_tag: 46 | raise AttributeError("Root tag '%s' submitted to '%s' parser. Should be '%s'." % (root_element.tag, root_tag, root_tag)) 47 | 48 | folderhierarchy_folderupdate_children = root_element.get_children() 49 | 50 | folderhierarchy_folderupdate_status = None 51 | folderhierarchy_folderupdate_synckey = None 52 | folderhierarchy_folderupdate_serverid = None 53 | 54 | for element in folderhierarchy_folderupdate_children: 55 | if element.tag is "Status": 56 | folderhierarchy_folderupdate_status = element.text 57 | if folderhierarchy_folderupdate_status != "1": 58 | print "FolderUpdate Exception: %s" % folderhierarchy_folderupdate_status 59 | elif element.tag == "SyncKey": 60 | folderhierarchy_folderupdate_synckey = element.text 61 | return (folderhierarchy_folderupdate_status, folderhierarchy_folderupdate_synckey) -------------------------------------------------------------------------------- /peas/pyActiveSync/client/MoveItems.py: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Copyright (C) 2013 Sol Birnbaum 3 | # 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either version 2 7 | # of the License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 17 | # MA 02110-1301, USA. 18 | ######################################################################## 19 | 20 | from ..utils.wapxml import wapxmltree, wapxmlnode 21 | 22 | class MoveItems(object): 23 | """http://msdn.microsoft.com/en-us/library/gg675499(v=exchg.80).aspx""" 24 | 25 | @staticmethod 26 | def build(moves): 27 | if len(moves) < 1: 28 | raise AttributeError("MoveItems builder: No moves supplied to MoveItems request builder.") 29 | moveitems_xmldoc_req = wapxmltree() 30 | xmlrootnode = wapxmlnode("MoveItems") 31 | moveitems_xmldoc_req.set_root(xmlrootnode, "move") 32 | for move in moves: 33 | xmlmovenode = wapxmlnode("Move", xmlrootnode) 34 | src_msg_id, src_fld_id, dst_fld_id = move 35 | xmlsrcmsgidnode = wapxmlnode("SrcMsgId", xmlmovenode, src_msg_id) 36 | xmlsrcfldidnode = wapxmlnode("SrcFldId", xmlmovenode, src_fld_id) 37 | xmldstfldidnode = wapxmlnode("DstFldId", xmlmovenode, dst_fld_id) 38 | return moveitems_xmldoc_req 39 | 40 | @staticmethod 41 | def parse(wapxml): 42 | 43 | namespace = "move" 44 | root_tag = "MoveItems" 45 | 46 | root_element = wapxml.get_root() 47 | if root_element.get_xmlns() != namespace: 48 | raise AttributeError("Xmlns '%s' submitted to '%s' parser. Should be '%s'." % (root_element.get_xmlns(), root_tag, namespace)) 49 | if root_element.tag != root_tag: 50 | raise AttributeError("Root tag '%s' submitted to '%s' parser. Should be '%s'." % (root_element.tag, root_tag, root_tag)) 51 | 52 | move_moveitems_children = root_element.get_children() 53 | 54 | responses = [] 55 | 56 | for response_element in move_moveitems_children: 57 | src_id = "" 58 | status = "" 59 | dst_id = "" 60 | for element in response_element: 61 | if element.tag is "Status": 62 | status = element.text 63 | if status != "3": 64 | print "MoveItems Exception: %s" % status 65 | elif element.tag == "SrcMsgId": 66 | src_id = element.text 67 | elif element.tag == "DstMsgId": 68 | dst_id = element.text 69 | responses.append((src_id, status, dst_id)) 70 | 71 | return responses 72 | 73 | 74 | -------------------------------------------------------------------------------- /peas/pyActiveSync/client/FolderCreate.py: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Copyright (C) 2013 Sol Birnbaum 3 | # 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either version 2 7 | # of the License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 17 | # MA 02110-1301, USA. 18 | ######################################################################## 19 | 20 | from ..utils.wapxml import wapxmltree, wapxmlnode 21 | 22 | class FolderCreate: 23 | """http://msdn.microsoft.com/en-us/library/gg650949(v=exchg.80).aspx""" 24 | 25 | @staticmethod 26 | def build(synckey, parent_id, display_name, _type): 27 | foldercreate_xmldoc_req = wapxmltree() 28 | xmlrootnode = wapxmlnode("FolderCreate") 29 | foldercreate_xmldoc_req.set_root(xmlrootnode, "folderhierarchy") 30 | xmlsynckeynode = wapxmlnode("SyncKey", xmlrootnode, synckey) 31 | xmlparentidnode = wapxmlnode("ParentId", xmlrootnode, parent_id) 32 | xmldisplaynamenode = wapxmlnode("DisplayName", xmlrootnode, display_name) 33 | xmltypenode = wapxmlnode("Type", xmlrootnode, _type) #See objects.MSASCMD.FolderHierarchy.FolderCreate.Type 34 | return foldercreate_xmldoc_req 35 | 36 | @staticmethod 37 | def parse(wapxml): 38 | 39 | namespace = "folderhierarchy" 40 | root_tag = "FolderCreate" 41 | 42 | root_element = wapxml.get_root() 43 | if root_element.get_xmlns() != namespace: 44 | raise AttributeError("Xmlns '%s' submitted to '%s' parser. Should be '%s'." % (root_element.get_xmlns(), root_tag, namespace)) 45 | if root_element.tag != root_tag: 46 | raise AttributeError("Root tag '%s' submitted to '%s' parser. Should be '%s'." % (root_element.tag, root_tag, root_tag)) 47 | 48 | folderhierarchy_foldercreate_children = root_element.get_children() 49 | 50 | folderhierarchy_foldercreate_status = None 51 | folderhierarchy_foldercreate_synckey = None 52 | folderhierarchy_foldercreate_serverid = None 53 | 54 | for element in folderhierarchy_foldercreate_children: 55 | if element.tag is "Status": 56 | folderhierarchy_foldercreate_status = element.text 57 | if folderhierarchy_foldercreate_status != "1": 58 | print "FolderCreate Exception: %s" % folderhierarchy_foldercreate_status 59 | elif element.tag == "SyncKey": 60 | folderhierarchy_foldercreate_synckey = element.text 61 | elif element.tag == "ServerId": 62 | folderhierarchy_foldercreate_serverid = element.text 63 | return (folderhierarchy_foldercreate_status, folderhierarchy_foldercreate_synckey, folderhierarchy_foldercreate_serverid) -------------------------------------------------------------------------------- /peas/pyActiveSync/client/SmartReply.py: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Copyright (C) 2013 Sol Birnbaum 3 | # 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either version 2 7 | # of the License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 17 | # MA 02110-1301, USA. 18 | ######################################################################## 19 | 20 | from ..utils.wapxml import wapxmltree, wapxmlnode 21 | 22 | class SmartReply: 23 | """http://msdn.microsoft.com/en-us/library/ee217283(v=exchg.80).aspx""" 24 | 25 | @staticmethod 26 | def build(client_id, source, mime, replace_mime=False, save_in_sent_items=True, template_id=None): 27 | smartreply_xmldoc_req = wapxmltree() 28 | xmlrootnode = wapxmlnode("SmartReply") 29 | smartreply_xmldoc_req.set_root(xmlrootnode, "composemail") 30 | xml_clientid_node = wapxmlnode("ClientId", xmlrootnode, client_id) 31 | xml_source_node = wapxmlnode("Source", xmlrootnode) 32 | if source.has_key("FolderId"): 33 | wapxmlnode("FolderId", xml_source_node, source["FolderId"]) 34 | if source.has_key("ItemId"): 35 | wapxmlnode("ItemId", xml_source_node, source["ItemId"]) 36 | if source.has_key("LongId"): 37 | wapxmlnode("LongId", xml_source_node, source["LongId"]) 38 | if source.has_key("InstanceId"): 39 | wapxmlnode("InstanceId", xml_source_node, source["InstanceId"]) 40 | xml_accountid_node = wapxmlnode("AccountId", xmlrootnode, display_name) 41 | xml_saveinsentiems_node = wapxmlnode("SaveInSentItems", xmlrootnode, str(int(save_in_sent_items))) 42 | if replace_mime: 43 | xml_replacemime_node = wapxmlnode("ReplaceMime", xmlrootnode) 44 | xml_mime_node = wapxmlnode("Mime", xmlrootnode, mime) 45 | xml_templateid_node = wapxmlnode("rm:TemplateID", xmlrootnode, template_id) 46 | return smartreply_xmldoc_req 47 | 48 | @staticmethod 49 | def parse(wapxml): 50 | 51 | namespace = "composemail" 52 | root_tag = "SmartReply" 53 | 54 | root_element = wapxml.get_root() 55 | if root_element.get_xmlns() != namespace: 56 | raise AttributeError("Xmlns '%s' submitted to '%s' parser. Should be '%s'." % (root_element.get_xmlns(), root_tag, namespace)) 57 | if root_element.tag != root_tag: 58 | raise AttributeError("Root tag '%s' submitted to '%s' parser. Should be '%s'." % (root_element.tag, root_tag, root_tag)) 59 | 60 | smartreply_children = root_element.get_children() 61 | 62 | status = None 63 | 64 | for element in smartreply_children: 65 | if element.tag is "Status": 66 | status = element.text 67 | return status 68 | 69 | -------------------------------------------------------------------------------- /peas/pyActiveSync/client/SmartForward.py: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Copyright (C) 2013 Sol Birnbaum 3 | # 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either version 2 7 | # of the License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 17 | # MA 02110-1301, USA. 18 | ######################################################################## 19 | 20 | from ..utils.wapxml import wapxmltree, wapxmlnode 21 | 22 | class SmartForward: 23 | """http://msdn.microsoft.com/en-us/library/ee201840(v=exchg.80).aspx""" 24 | 25 | @staticmethod 26 | def build(client_id, source, mime, replace_mime=False, save_in_sent_items=True, template_id=None): 27 | smartforward_xmldoc_req = wapxmltree() 28 | xmlrootnode = wapxmlnode("SmartForward") 29 | smartforward_xmldoc_req.set_root(xmlrootnode, "composemail") 30 | xml_clientid_node = wapxmlnode("ClientId", xmlrootnode, client_id) 31 | xml_source_node = wapxmlnode("Source", xmlrootnode) 32 | if source.has_key("FolderId"): 33 | wapxmlnode("FolderId", xml_source_node, source["FolderId"]) 34 | if source.has_key("ItemId"): 35 | wapxmlnode("ItemId", xml_source_node, source["ItemId"]) 36 | if source.has_key("LongId"): 37 | wapxmlnode("LongId", xml_source_node, source["LongId"]) 38 | if source.has_key("InstanceId"): 39 | wapxmlnode("InstanceId", xml_source_node, source["InstanceId"]) 40 | xml_accountid_node = wapxmlnode("AccountId", xmlrootnode, display_name) 41 | xml_saveinsentiems_node = wapxmlnode("SaveInSentItems", xmlrootnode, str(int(save_in_sent_items))) 42 | if replace_mime: 43 | xml_replacemime_node = wapxmlnode("ReplaceMime", xmlrootnode) 44 | xml_mime_node = wapxmlnode("Mime", xmlrootnode, mime) 45 | xml_templateid_node = wapxmlnode("rm:TemplateID", xmlrootnode, template_id) 46 | return smartforward_xmldoc_req 47 | 48 | @staticmethod 49 | def parse(wapxml): 50 | 51 | namespace = "composemail" 52 | root_tag = "SmartForward" 53 | 54 | root_element = wapxml.get_root() 55 | if root_element.get_xmlns() != namespace: 56 | raise AttributeError("Xmlns '%s' submitted to '%s' parser. Should be '%s'." % (root_element.get_xmlns(), root_tag, namespace)) 57 | if root_element.tag != root_tag: 58 | raise AttributeError("Root tag '%s' submitted to '%s' parser. Should be '%s'." % (root_element.tag, root_tag, root_tag)) 59 | 60 | smartforward_children = root_element.get_children() 61 | 62 | status = None 63 | 64 | for element in smartforward_children: 65 | if element.tag is "Status": 66 | status = element.text 67 | return status 68 | 69 | -------------------------------------------------------------------------------- /peas/pyActiveSync/client/Ping.py: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Copyright (C) 2013 Sol Birnbaum 3 | # 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either version 2 7 | # of the License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 17 | # MA 02110-1301, USA. 18 | ######################################################################## 19 | 20 | from ..utils.wapxml import wapxmltree, wapxmlnode 21 | 22 | class Ping(object): 23 | """http://msdn.microsoft.com/en-us/library/gg675609(v=exchg.80).aspx""" 24 | 25 | @staticmethod 26 | def build(heatbeat_interval="30", folders=None): 27 | if not folders: 28 | raise AttributeError("Ping builder: No folders included in ping request builder. Must ping at least one folder.") 29 | ping_xmldoc_req = wapxmltree() 30 | xmlrootnode = wapxmlnode("Ping") 31 | ping_xmldoc_req.set_root(xmlrootnode, "ping") 32 | xmlheartbeatintervalnode = wapxmlnode("HeartbeatInterval", xmlrootnode, heatbeat_interval) 33 | xmlfoldersnode = wapxmlnode("Folders", xmlrootnode) 34 | for folder in folders: 35 | xmlfoldernode = wapxmlnode("Folder", xmlfoldersnode) 36 | xmlidnode = wapxmlnode("Id", xmlfoldernode, folder[0]) 37 | xmlclassnode = wapxmlnode("Class", xmlfoldernode, folder[1]) 38 | return ping_xmldoc_req 39 | 40 | @staticmethod 41 | def parse(wapxml): 42 | 43 | namespace = "ping" 44 | root_tag = "Ping" 45 | 46 | root_element = wapxml.get_root() 47 | if root_element.get_xmlns() != namespace: 48 | raise AttributeError("Xmlns '%s' submitted to '%s' parser. Should be '%s'." % (root_element.get_xmlns(), root_tag, namespace)) 49 | if root_element.tag != root_tag: 50 | raise AttributeError("Root tag '%s' submitted to '%s' parser. Should be '%s'." % (root_element.tag, root_tag, root_tag)) 51 | 52 | ping_ping_children = root_element.get_children() 53 | 54 | heartbeat_interval = "" 55 | status = "" 56 | folders = [] 57 | max_folders = "" 58 | 59 | for element in ping_ping_children: 60 | if element.tag is "Status": 61 | status = element.text 62 | if (status != "1") and (status != "2"): 63 | print "Ping Exception: %s" % status 64 | elif element.tag == "HeartbeatInterval": 65 | heartbeat_interval = element.text 66 | elif element.tag == "MaxFolders": 67 | max_folders = element.text 68 | elif element.tag == "Folders": 69 | folders_list = element.get_children() 70 | if len(folders_list) > 0: 71 | for folder in folders_list: 72 | folders.append(folder.text) 73 | return (status, heartbeat_interval, max_folders, folders) 74 | 75 | -------------------------------------------------------------------------------- /peas/pyActiveSync/client/ValidateCert.py: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Copyright (C) 2013 Sol Birnbaum 3 | # 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either version 2 7 | # of the License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 17 | # MA 02110-1301, USA. 18 | ######################################################################## 19 | 20 | from ..utils.wapxml import wapxmltree, wapxmlnode 21 | import base64 22 | 23 | class ValidateCert: 24 | """http://msdn.microsoft.com/en-us/library/gg675590(v=exchg.80).aspx""" 25 | 26 | @staticmethod 27 | def build(certificate, certificate_chain_list=[], pre_encoded = False, check_crl = True): 28 | validatecert_xmldoc_req = wapxmltree() 29 | xmlrootnode = wapxmlnode("ValidateCert") 30 | validatecert_xmldoc_req.set_root(xmlrootnode, "validatecert") 31 | if len(certificate_chain_list) > 0: 32 | xmlcertchainnode = wapxmlnode("CertificateChain", xmlrootnode) 33 | for cert in certificate_chain_list: 34 | if pre_encoded: 35 | wapxmlnode("Certificate", xmlcertchainnode, cert) 36 | else: 37 | wapxmlnode("Certificate", xmlcertchainnode, base64.b64encode(cert)) 38 | xmlcertsnode = wapxmlnode("Certificates", xmlrootnode) 39 | if pre_encoded: 40 | xmlcertnode = wapxmlnode("Certificate", xmlcertsnode, certificate) 41 | else: 42 | xmlcertnode = wapxmlnode("Certificate", xmlcertsnode, base64.b64encode(certificate)) 43 | if check_crl: 44 | xmlcertsnode = wapxmlnode("CheckCRL", xmlrootnode, "1") 45 | return validatecert_xmldoc_req 46 | 47 | @staticmethod 48 | def parse(wapxml): 49 | 50 | namespace = "validatecert" 51 | root_tag = "ValidateCert" 52 | 53 | root_element = wapxml.get_root() 54 | if root_element.get_xmlns() != namespace: 55 | raise AttributeError("Xmlns '%s' submitted to '%s' parser. Should be '%s'." % (root_element.get_xmlns(), root_tag, namespace)) 56 | if root_element.tag != root_tag: 57 | raise AttributeError("Root tag '%s' submitted to '%s' parser. Should be '%s'." % (root_element.tag, root_tag, root_tag)) 58 | 59 | validatecert_validatecert_status = None 60 | validatecert_validatecert_cert_status = None 61 | 62 | for element in validatecert_validatecert_children: 63 | if element.tag is "Status": 64 | validatecert_validatecert_status = element.text 65 | if validatecert_validatecert_status != "1": 66 | print "ValidateCert Exception: %s" % validatecert_validatecert_status 67 | elif element.tag == "Certificate": 68 | validatecert_validatecert_cert_status = element.get_children()[0].text 69 | return (validatecert_validatecert_status, validatecert_validatecert_cert_status) 70 | 71 | 72 | -------------------------------------------------------------------------------- /peas/pyActiveSync/client/MeetingResponse.py: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Copyright (C) 2013 Sol Birnbaum 3 | # 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either version 2 7 | # of the License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 17 | # MA 02110-1301, USA. 18 | ######################################################################## 19 | 20 | from ..utils.wapxml import wapxmltree, wapxmlnode 21 | 22 | class MeetingResponse: 23 | """description of class""" 24 | 25 | @staticmethod 26 | def build(responses): 27 | meetingresponse_xmldoc_req = wapxmltree() 28 | xmlrootnode = wapxmlnode("MeetingResponse") 29 | meetingresponse_xmldoc_req.set_root(xmlrootnode, "meetingresponse") 30 | for response in responses: 31 | xmlrequestnode = wapxmlnode("Request", xmlrootnode) 32 | xmluserresponsenode = wapxmlnode("UserResponse", xmlrequestnode, response["UserResponse"]) 33 | if response.has_Key("CollectionId"): 34 | xmlcollectionidnode = wapxmlnode("CollectionId", xmlrequestnode, response["CollectionId"]) 35 | xmlrequestidnode = wapxmlnode("RequestId", xmlrequestnode, response["RequestId"]) 36 | elif response.has_Key("LongId"): 37 | xmllongidnode = wapxmlnode("search:LongId", xmlrequestnode, response["LongId"]) 38 | else: 39 | raise AttributeError("MeetingResponse missing meeting id") 40 | xmlinstanceidnode = wapxmlnode("InstanceId", xmlrequestnode, response["InstanceId"]) 41 | return meetingresponse_xmldoc_req 42 | 43 | @staticmethod 44 | def parse(wapxml): 45 | 46 | namespace = "meetingresponse" 47 | root_tag = "MeetingResponse" 48 | 49 | root_element = wapxml.get_root() 50 | if root_element.get_xmlns() != namespace: 51 | raise AttributeError("Xmlns '%s' submitted to '%s' parser. Should be '%s'." % (root_element.get_xmlns(), root_tag, namespace)) 52 | if root_element.tag != root_tag: 53 | raise AttributeError("Root tag '%s' submitted to '%s' parser. Should be '%s'." % (root_element.tag, root_tag, root_tag)) 54 | 55 | meetingresponse_meetingresponse_children = root_element.get_children() 56 | 57 | responses = [] 58 | 59 | for element in meetingresponse_meetingresponse_children: 60 | if element.tag is "Result": 61 | result_elements = element.get_children() 62 | for result_element in result_elements: 63 | request_id = None 64 | calendar_id = None 65 | if result_element.tag == "RequestId": 66 | request_id = result_element.text 67 | elif result_element.tag == "Status": 68 | status = result_element.text 69 | elif result_element.tag == "CalendarId": 70 | calendar_id = result_element.text 71 | responses.append(status, request_id, calendar_id) 72 | else: 73 | raise AttributeError("MeetingResponse error. Server returned unknown element instead of 'Result'.") 74 | return responses 75 | 76 | -------------------------------------------------------------------------------- /peas/eas_client/autodiscovery.py: -------------------------------------------------------------------------------- 1 | from twisted.internet import reactor 2 | from twisted.web.client import Agent 3 | from twisted.web.http_headers import Headers 4 | from xml.dom.minidom import getDOMImplementation 5 | from zope.interface import implements 6 | from twisted.internet.defer import succeed 7 | from twisted.web.iweb import IBodyProducer 8 | 9 | version = "1.0" 10 | 11 | class AutoDiscoveryProducer(object): 12 | implements(IBodyProducer) 13 | def __init__(self, email_address): 14 | impl = getDOMImplementation() 15 | newdoc = impl.createDocument(None, "Autodiscover", None) 16 | top_element = newdoc.documentElement 17 | top_element.setAttribute("xmlns", "http://schemas.microsoft.com/exchange/autodiscover/mobilesync/requestschema/2006") 18 | req_elem = newdoc.createElement('Request') 19 | top_element.appendChild(req_elem) 20 | email_elem = newdoc.createElement('EMailAddress') 21 | req_elem.appendChild(email_elem) 22 | email_elem.appendChild(newdoc.createTextNode(email_address)) 23 | resp_schema = newdoc.createElement('AcceptableResponseSchema') 24 | req_elem.appendChild(resp_schema) 25 | resp_schema.appendChild(newdoc.createTextNode("http://schemas.microsoft.com/exchange/autodiscover/mobilesync/responseschema/2006")) 26 | self.body = newdoc.toxml("utf-8") 27 | self.length = len(self.body) 28 | 29 | def startProducing(self, consumer): 30 | consumer.write(self.body) 31 | return succeed(None) 32 | 33 | def pauseProducing(self): 34 | pass 35 | 36 | def stopProducing(self): 37 | pass 38 | 39 | class AutoDiscover: 40 | """The AutoDiscover class is used to find EAS servers using only an email address""" 41 | STATE_INIT = 0 42 | STATE_XML_REQUEST = 1 43 | STATE_XML_AUTODISCOVER_REQUEST = 2 44 | STATE_INSECURE = 3 45 | STATE_SRV = 4 46 | STATE_REDIRECT = 5 47 | LAST_STATE = 6 48 | AD_REQUESTS = {STATE_XML_REQUEST:"https://%s/autodiscover/autodiscover.xml", 49 | STATE_XML_AUTODISCOVER_REQUEST:"https://autodiscover.%s/autodiscover/autodiscover.xml", 50 | STATE_INSECURE:"http://autodiscover.%s/autodiscover/autodiscover.xml"} 51 | 52 | def __init__(self, email): 53 | self.email = email 54 | self.email_domain = email.split("@")[1] 55 | self.agent = Agent(reactor) 56 | self.state = AutoDiscover.STATE_INIT 57 | self.redirect_urls = [] 58 | def handle_redirect(self, new_url): 59 | if new_url in self.redirect_urls: 60 | raise Exception("AutoDiscover", "Circular redirection") 61 | self.redirect_urls.append(new_url) 62 | self.state = AutoDiscover.STATE_REDIRECT 63 | print "Making request to",new_url 64 | d = self.agent.request( 65 | 'GET', 66 | new_url, 67 | Headers({'User-Agent': ['python-EAS-Client %s'%version]}), 68 | AutoDiscoveryProducer(self.email)) 69 | d.addCallback(self.autodiscover_response) 70 | d.addErrback(self.autodiscover_error) 71 | return d 72 | def autodiscover_response(self, result): 73 | print "RESPONSE",result,result.code 74 | if result.code == 302: 75 | # TODO: "Redirect responses" validation 76 | return self.handle_redirect(result.headers.getRawHeaders("location")[0]) 77 | return result 78 | def autodiscover_error(self, error): 79 | print "ERROR",error,error.value.reasons[0] 80 | if self.state < AutoDiscover.LAST_STATE: 81 | return self.autodiscover() 82 | raise error 83 | def autodiscover(self): 84 | self.state += 1 85 | if self.state in AutoDiscover.AD_REQUESTS: 86 | print "Making request to",AutoDiscover.AD_REQUESTS[self.state]%self.email_domain 87 | body = AutoDiscoveryProducer(self.email) 88 | d = self.agent.request( 89 | 'GET', 90 | AutoDiscover.AD_REQUESTS[self.state]%self.email_domain, 91 | Headers({'User-Agent': ['python-EAS-Client %s'%version]}), 92 | body) 93 | d.addCallback(self.autodiscover_response) 94 | d.addErrback(self.autodiscover_error) 95 | return d 96 | else: 97 | raise Exception("Unsupported state",str(self.state)) -------------------------------------------------------------------------------- /peas/pyActiveSync/client/Search.py: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Created 2016 based on code Copyright (C) 2013 Sol Birnbaum 3 | # 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either version 2 7 | # of the License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 17 | # MA 02110-1301, USA. 18 | ######################################################################## 19 | 20 | from ..utils.wapxml import wapxmltree, wapxmlnode 21 | 22 | 23 | class Search: 24 | """https://msdn.microsoft.com/en-us/library/gg675482(v=exchg.80).aspx 25 | 26 | Currently for DocumentLibrary searches only. 27 | """ 28 | 29 | @staticmethod 30 | def build(unc_path, return_range='0-999', username=None, password=None): 31 | 32 | xmldoc_req = wapxmltree() 33 | xmlrootnode = wapxmlnode("Search") 34 | xmldoc_req.set_root(xmlrootnode, "search") 35 | 36 | store_node = wapxmlnode("Store", xmlrootnode) 37 | 38 | # "GAL" to search the Global Address List. 39 | # "Mailbox" to search the mailbox. 40 | # "DocumentLibrary" to search a Windows SharePoint Services library or a UNC library. 41 | name_node = wapxmlnode("Name", store_node, "DocumentLibrary") 42 | 43 | query_node = wapxmlnode("Query", store_node) 44 | equal_to_node = wapxmlnode("EqualTo", query_node) 45 | link_id = wapxmlnode("documentlibrary:LinkId", equal_to_node) 46 | value_node = wapxmlnode("Value", equal_to_node, unc_path) 47 | 48 | options_node = wapxmlnode("Options", store_node) 49 | range_node = wapxmlnode("Range", options_node, return_range) 50 | 51 | if username is not None: 52 | username_node = wapxmlnode("UserName", options_node, username) 53 | if password is not None: 54 | password_node = wapxmlnode("Password", options_node, password) 55 | 56 | return xmldoc_req 57 | 58 | @staticmethod 59 | def parse(wapxml): 60 | 61 | namespace = "search" 62 | root_tag = "Search" 63 | 64 | root_element = wapxml.get_root() 65 | if root_element.get_xmlns() != namespace: 66 | raise AttributeError("Xmlns '%s' submitted to '%s' parser. Should be '%s'." % (root_element.get_xmlns(), root_tag, namespace)) 67 | if root_element.tag != root_tag: 68 | raise AttributeError("Root tag '%s' submitted to '%s' parser. Should be '%s'." % (root_element.tag, root_tag, root_tag)) 69 | 70 | children = root_element.get_children() 71 | 72 | status = None 73 | results = [] 74 | 75 | for element in children: 76 | if element.tag is "Status": 77 | status = element.text 78 | if status != "1": 79 | print "%s Exception: %s" % (root_tag, status) 80 | elif element.tag == "Response": 81 | 82 | properties = element.basic_xpath('Store/Result/Properties') 83 | for properties_el in properties: 84 | result = {} 85 | properties_children = properties_el.get_children() 86 | for prop_el in properties_children: 87 | prop = prop_el.tag.partition(':')[2] 88 | result[prop] = prop_el.text 89 | results.append(result) 90 | 91 | return status, results 92 | 93 | -------------------------------------------------------------------------------- /peas/pyActiveSync/client/FolderSync.py: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Copyright (C) 2013 Sol Birnbaum 3 | # 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either version 2 7 | # of the License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 17 | # MA 02110-1301, USA. 18 | ######################################################################## 19 | 20 | from ..utils.wapxml import wapxmltree, wapxmlnode 21 | 22 | from ..objects.MSASCMD import FolderHierarchy 23 | 24 | class FolderSync: 25 | """http://msdn.microsoft.com/en-us/library/ee237648(v=exchg.80).aspx""" 26 | 27 | @staticmethod 28 | def build(synckey): 29 | foldersync_xmldoc_req = wapxmltree() 30 | xmlrootnode = wapxmlnode("FolderSync") 31 | foldersync_xmldoc_req.set_root(xmlrootnode, "folderhierarchy") 32 | xmlsynckeynode = wapxmlnode("SyncKey", xmlrootnode, synckey) 33 | return foldersync_xmldoc_req 34 | 35 | @staticmethod 36 | def parse(wapxml): 37 | 38 | namespace = "folderhierarchy" 39 | root_tag = "FolderSync" 40 | 41 | root_element = wapxml.get_root() 42 | if root_element.get_xmlns() != namespace: 43 | raise AttributeError("Xmlns '%s' submitted to '%s' parser. Should be '%s'." % (root_element.get_xmlns(), root_tag, namespace)) 44 | if root_element.tag != root_tag: 45 | raise AttributeError("Root tag '%s' submitted to '%s' parser. Should be '%s'." % (root_element.tag, root_tag, root_tag)) 46 | 47 | folderhierarchy_foldersync_children = root_element.get_children() 48 | if len(folderhierarchy_foldersync_children) > 3: 49 | raise AttributeError("%s response does not conform to any known %s responses." % (root_tag, root_tag)) 50 | #if folderhierarchy_foldersync_children[0].tag != "Collections": 51 | # raise AttributeError("%s response does not conform to any known %s responses." % (root_tag, root_tag)) 52 | 53 | folderhierarchy_foldersync_status = None 54 | folderhierarchy_foldersync_synckey = None 55 | folderhierarchy_foldersync_changes = None 56 | 57 | changes = [] 58 | 59 | for element in folderhierarchy_foldersync_children: 60 | if element.tag is "Status": 61 | folderhierarchy_foldersync_status = element.text 62 | if folderhierarchy_foldersync_status != "1": 63 | print "FolderSync Exception: %s" % folderhierarchy_foldersync_status 64 | elif element.tag == "SyncKey": 65 | folderhierarchy_foldersync_synckey = element.text 66 | elif element.tag == "Changes": 67 | folderhierarchy_foldersync_changes = element.get_children() 68 | folderhierarchy_foldersync_changes_count = int(folderhierarchy_foldersync_changes[0].text) 69 | if folderhierarchy_foldersync_changes_count > 0: 70 | for change_index in range(1, folderhierarchy_foldersync_changes_count+1): 71 | folderhierarchy_foldersync_change_element = folderhierarchy_foldersync_changes[change_index] 72 | folderhierarchy_foldersync_change_childern = folderhierarchy_foldersync_change_element.get_children() 73 | new_change = FolderHierarchy.Folder() 74 | for element in folderhierarchy_foldersync_change_childern: 75 | if element.tag == "ServerId": 76 | new_change.ServerId = element.text 77 | elif element.tag == "ParentId": 78 | new_change.ParentId = element.text 79 | elif element.tag == "DisplayName": 80 | new_change.DisplayName = element.text 81 | elif element.tag == "Type": 82 | new_change.Type = element.text 83 | changes.append((folderhierarchy_foldersync_change_element.tag, new_change)) 84 | return (changes, folderhierarchy_foldersync_synckey, folderhierarchy_foldersync_status) -------------------------------------------------------------------------------- /peas/eas_client/activesync_producers.py: -------------------------------------------------------------------------------- 1 | from twisted.internet.defer import succeed 2 | from twisted.web.iweb import IBodyProducer 3 | from zope.interface import implements 4 | from dewbxml import wbxmlparser, wbxmlreader, wbxmldocument, wbxmlelement, wbxmlstring 5 | import struct 6 | 7 | class WBXMLProducer(object): 8 | implements(IBodyProducer) 9 | def __init__(self, wbdoc, verbose=False): 10 | self.verbose=verbose 11 | self.wb = wbdoc 12 | self.body = str(self.wb.tobytes()) 13 | self.length = len(self.body) 14 | def startProducing(self, consumer): 15 | #if self.verbose: print "Producing",self.body.encode("hex"), self.wb 16 | consumer.write(self.body) 17 | return succeed(None) 18 | def pauseProducing(self): pass 19 | def stopProducing(self): pass 20 | 21 | def convert_array_to_children(in_elem, in_val): 22 | if isinstance(in_val, list): 23 | for v in in_val: 24 | if len(v) > 2: 25 | add_elem = wbxmlelement(v[0], page_num=v[2]) 26 | else: 27 | add_elem = wbxmlelement(v[0], page_num=in_elem.page_num) 28 | in_elem.addchild(add_elem) 29 | convert_array_to_children(add_elem, v[1]) 30 | elif isinstance(in_val, dict): 31 | print "FOUND OPAQUE THING",in_val 32 | in_elem.addchild(wbxmlstring(struct.pack(in_val["fmt"],in_val["val"]), opaque=True)) 33 | print "OPAQUE PRODUCED",in_elem 34 | elif in_val != None: 35 | in_elem.addchild(wbxmlstring(in_val)) 36 | 37 | def convert_dict_to_wbxml(indict, default_page_num=None): 38 | wb = wbxmldocument() 39 | wb.encoding = "utf-8" 40 | wb.version = "1.3" 41 | wb.schema = "activesync" 42 | assert len(indict) == 1 # must be only one root element 43 | #print "Root",indict.keys()[0] 44 | if default_page_num != None: 45 | root = wbxmlelement(indict.keys()[0], page_num=default_page_num) 46 | else: 47 | root = wbxmlelement(indict.keys()[0]) 48 | wb.addchild(root) 49 | convert_array_to_children(root, indict.values()[0]) 50 | return wb 51 | 52 | class FolderSyncProducer(WBXMLProducer): 53 | def __init__(self, sync_key, verbose=False): 54 | wb = convert_dict_to_wbxml({ 55 | "FolderSync": [ 56 | ("SyncKey", str(sync_key)) 57 | ] 58 | }, default_page_num=7); 59 | return WBXMLProducer.__init__(self, wb, verbose=verbose) 60 | 61 | 62 | 63 | class ItemOperationsProducer(WBXMLProducer): 64 | def __init__(self, opname, collection_id, server_id, fetch_type, mimeSupport, store="Mailbox", verbose=False): 65 | server_ids = [] 66 | if isinstance(server_id, list): 67 | server_ids.extend(server_id) 68 | else: 69 | server_ids.append(server_id) 70 | wbdict = { 71 | "ItemOperations": [] 72 | } 73 | for sid in server_ids: 74 | if store == "Mailbox": 75 | wbdict["ItemOperations"].append((opname, [ 76 | ("Store", str(store)), 77 | ("CollectionId", str(collection_id), 0), 78 | ("ServerId", str(sid), 0), 79 | ("Options",[ 80 | ("MIMESupport", str(mimeSupport), 0), 81 | ("BodyPreference", [ 82 | ("Type", str(fetch_type)), 83 | ("TruncationSize", str(512)) 84 | ], 17) 85 | ]), 86 | ])) 87 | else: 88 | wbdict["ItemOperations"].append((opname, [ 89 | ("Store", str(store)), 90 | ("LinkId", str(sid), 19), 91 | ("Options",[]), 92 | ])) 93 | wb = convert_dict_to_wbxml(wbdict, default_page_num=20) 94 | return WBXMLProducer.__init__(self, wb, verbose=verbose) 95 | 96 | class SyncProducer(WBXMLProducer): 97 | def __init__(self, collection_id, sync_key, get_body, verbose=False): 98 | wbdict = { 99 | "Sync": [ 100 | ("Collections", [ 101 | ("Collection", [ 102 | ("SyncKey", str(sync_key)), 103 | ("CollectionId", str(collection_id)), 104 | ("DeletesAsMoves", "1"), 105 | ]) 106 | ]) 107 | ] 108 | } 109 | if sync_key != 0: 110 | wbdict["Sync"][0][1][0][1].append(("GetChanges","1")) 111 | wbdict["Sync"][0][1][0][1].append(("WindowSize","512")) 112 | if get_body: 113 | wbdict["Sync"][0][1][0][1].append(("Options",[ 114 | ("MIMESupport", "0"), 115 | ("BodyPreference", [ 116 | ("Type", "2"), 117 | ("TruncationSize", "5120"), 118 | ], 17) 119 | ])) 120 | wb = convert_dict_to_wbxml(wbdict, default_page_num=0) 121 | return WBXMLProducer.__init__(self, wb, verbose=verbose) 122 | 123 | class ProvisionProducer(WBXMLProducer): 124 | def __init__(self, policyKey=None, verbose=False): 125 | wbdict = { 126 | "Provision": [ 127 | ("Policies", [ 128 | ("Policy", [ 129 | ("PolicyType", "MS-EAS-Provisioning-WBXML"), 130 | ]) 131 | ]) 132 | ] 133 | } 134 | 135 | if policyKey != None: 136 | wbdict["Provision"][0][1][0][1].append(("PolicyKey",str(policyKey))) 137 | wbdict["Provision"][0][1][0][1].append(("Status","1")) 138 | 139 | wb = convert_dict_to_wbxml(wbdict, default_page_num=14) 140 | 141 | return WBXMLProducer.__init__(self, wb, verbose=verbose) -------------------------------------------------------------------------------- /peas/pyActiveSync/utils/wapxml.py: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Modified 2016 from code copyright (C) 2013 Sol Birnbaum 3 | # 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either version 2 7 | # of the License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 17 | # MA 02110-1301, USA. 18 | ######################################################################## 19 | 20 | 21 | class wapxmltree(object): 22 | def __init__(self, inwapxmlstr=None): 23 | self.header = "" 24 | self._root_node = None 25 | if inwapxmlstr: 26 | self.parse_string(inwapxmlstr) 27 | return 28 | def parse_string(self, xmlstr): 29 | return 30 | def set_root(self, root_node, xmlns): 31 | self._root_node = root_node 32 | self._root_node.set_root(True, xmlns, self) 33 | def get_root(self): 34 | return self._root_node 35 | def __repr__(self): 36 | if self._root_node: 37 | return self.header + repr(self._root_node) 38 | 39 | 40 | class wapxmlnode(object): 41 | def __init__(self, tag, parent=None, text=None, cdata=None): 42 | self.tag = tag 43 | self.text = text 44 | self.cdata = cdata 45 | self._children = [] 46 | self._is_root = None 47 | self._xmlns = None 48 | self._parent = None 49 | if parent: 50 | try: 51 | self.set_parent(parent) 52 | except Exception, e: 53 | print e 54 | def set_parent(self, parent): 55 | parent.add_child(self) 56 | self._parent = parent 57 | def get_parent(self): 58 | return self._parent 59 | def add_child(self, child): 60 | self._children.append(child) 61 | def remove_child(self, child): 62 | self._children.remove(child) 63 | def set_root(self, true_or_false, xmlns=None, parent=None): 64 | self._is_root = true_or_false 65 | self._xmlns = xmlns 66 | self._parent = parent 67 | def is_root(self): 68 | return self._is_root 69 | def set_xmlns(self, xmlns): 70 | self._xmlns = xmlns 71 | def get_xmlns(self): 72 | return self._xmlns 73 | def has_children(self): 74 | if len(self._children) > 0: 75 | return True 76 | else: 77 | return False 78 | def get_children(self): 79 | return self._children 80 | 81 | def basic_xpath(self, tag_path): 82 | """Get all the children with the final tag name after following the tag path list e.g. search/results.""" 83 | 84 | tag_path = tag_path.split('/') 85 | 86 | results = [] 87 | for child in self._children: 88 | if child.tag == tag_path[0]: 89 | if len(tag_path) == 1: 90 | results.append(child) 91 | else: 92 | results.extend(child.basic_xpath('/'.join(tag_path[1:]))) 93 | return results 94 | 95 | def __repr__(self, tabs=" "): 96 | if (self.text != None) or (self.cdata != None) or (len(self._children)>0): 97 | inner_text = "" 98 | if self.text != None: 99 | inner_text+=str(self.text) 100 | if self.cdata != None: 101 | inner_text+= "" % str(self.cdata) 102 | if self.has_children(): 103 | for child in self._children: 104 | inner_text+=child.__repr__(tabs+" ") 105 | if not self._is_root: 106 | end_tabs = "" 107 | if self.has_children(): end_tabs = "\r\n"+tabs 108 | return "\r\n%s<%s>%s%s" % (tabs, self.tag, inner_text, end_tabs, self.tag) 109 | else: return "\r\n<%s xmlns=\"%s:\">%s\r\n" % (self.tag, self._xmlns, inner_text, self.tag) 110 | elif self._is_root: 111 | return "\r\n<%s xmlns=\"%s:\">" % (self.tag, self._xmlns, self.tag) 112 | else: 113 | return "%s<%s />" % (tabs, self.tag) 114 | def __iter__(self): 115 | if len(self._children) > 0: 116 | for child in self._children: 117 | yield child 118 | def __str__(self): 119 | return self.__repr__() 120 | 121 | 122 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /peas/peas.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Adam Rutherford' 2 | 3 | import requests 4 | from requests.packages.urllib3.exceptions import InsecureRequestWarning 5 | 6 | import py_eas_helper 7 | import py_activesync_helper 8 | 9 | 10 | PY_ACTIVE_SYNC = 1 11 | PY_EAS_CLIENT = 2 12 | 13 | 14 | class Peas: 15 | 16 | def __init__(self): 17 | self._backend = PY_ACTIVE_SYNC 18 | 19 | self._creds = { 20 | 'server': None, 21 | 'user': None, 22 | 'password': None, 23 | 'domain': None, # This could be optional. 24 | 'device_id': None, # This could be optional. 25 | } 26 | 27 | requests.packages.urllib3.disable_warnings(InsecureRequestWarning) 28 | 29 | def set_backend(self, backend_id): 30 | """Set which backend library to use.""" 31 | 32 | assert(backend_id in [PY_ACTIVE_SYNC, PY_EAS_CLIENT]) 33 | 34 | self._backend = backend_id 35 | 36 | def set_creds(self, creds): 37 | """Configure which exchange server, credentials and other settings to use.""" 38 | self._creds.update(creds) 39 | 40 | def extract_emails_py_active_sync(self): 41 | emails = py_activesync_helper.extract_emails(self._creds) 42 | return emails 43 | 44 | def extract_emails_py_eas_client(self): 45 | 46 | emails = py_eas_helper.extract_emails(self._creds) 47 | return emails 48 | 49 | def extract_emails(self): 50 | """Retrieve and return emails.""" 51 | 52 | if self._backend == PY_ACTIVE_SYNC: 53 | return self.extract_emails_py_active_sync() 54 | 55 | if self._backend == PY_EAS_CLIENT: 56 | return self.extract_emails_py_eas_client() 57 | 58 | # TODO: This returns a response object. Make it a public method when it returns something more generic. 59 | def _get_options(self): 60 | 61 | assert self._backend == PY_ACTIVE_SYNC 62 | 63 | as_conn = py_activesync_helper.ASHTTPConnector(self._creds['server']) #e.g. "as.myserver.com" 64 | as_conn.set_credential(self._creds['user'], self._creds['password']) 65 | return as_conn.get_options() 66 | 67 | def check_auth(self): 68 | """Perform an OPTIONS request which will fail if the credentials are incorrect. 69 | 70 | 401 Unauthorized is returned if the credentials are incorrect but other status codes may be possible, 71 | leading to false negatives. 72 | """ 73 | 74 | resp = self._get_options() 75 | return resp.status == 200 76 | 77 | def disable_certificate_verification(self): 78 | 79 | assert self._backend == PY_ACTIVE_SYNC 80 | 81 | py_activesync_helper.disable_certificate_verification() 82 | 83 | def get_server_headers(self): 84 | """Get the ActiveSync web server headers.""" 85 | 86 | sess = requests.Session() 87 | 88 | url = 'https://' + self._creds['server'] + '/Microsoft-Server-ActiveSync' 89 | 90 | # TODO: Allow user to specify if SSL is verified. 91 | resp = sess.get(url, verify=False) 92 | 93 | return resp.headers 94 | 95 | def get_unc_listing(self, unc_path): 96 | """Retrieve and return a file listing of the given UNC path.""" 97 | 98 | assert self._backend == PY_ACTIVE_SYNC 99 | 100 | # Use alternative credentials for SMB if supplied. 101 | user = self._creds.get('smb_user', self._creds['user']) 102 | password = self._creds.get('smb_password', self._creds['password']) 103 | 104 | # Enable the option to send no credentials at all. 105 | if user == '': 106 | user = None 107 | if password == '': 108 | password = None 109 | 110 | results = py_activesync_helper.get_unc_listing(self._creds, unc_path, 111 | username=user, password=password) 112 | 113 | return results 114 | 115 | def get_unc_file(self, unc_path): 116 | """Return the file data of the file at the given UNC path.""" 117 | 118 | assert self._backend == PY_ACTIVE_SYNC 119 | 120 | # Use alternative credentials for SMB if supplied. 121 | user = self._creds.get('smb_user', self._creds['user']) 122 | password = self._creds.get('smb_password', self._creds['password']) 123 | 124 | # Enable the option to send no credentials at all. 125 | if user == '': 126 | user = None 127 | if password == '': 128 | password = None 129 | 130 | data = py_activesync_helper.get_unc_file(self._creds, unc_path, 131 | username=user, password=password) 132 | 133 | return data 134 | 135 | 136 | def show_banner(): 137 | print(r''' _ __ ___ __ _ ___ 138 | | '_ \ / _ \/ _' / __| 139 | | |_) | __/ (_| \__ \ 140 | | .__/ \___|\__._|___/ 141 | |_| - Probe ActiveSync 142 | ''') 143 | 144 | 145 | def main(): 146 | show_banner() 147 | 148 | 149 | if __name__ == '__main__': 150 | main() 151 | -------------------------------------------------------------------------------- /peas/pyActiveSync/client/GetItemEstimate.py: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Copyright (C) 2013 Sol Birnbaum 3 | # 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either version 2 7 | # of the License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 17 | # MA 02110-1301, USA. 18 | ######################################################################## 19 | 20 | from ..utils.wapxml import wapxmltree, wapxmlnode 21 | 22 | class GetItemEstimate: 23 | class getitemestimate_response: 24 | def __init__(self): 25 | self.Status = None 26 | self.CollectionId = None 27 | self.Estimate = None 28 | 29 | @staticmethod 30 | def build(synckeys, collection_ids, options): 31 | getitemestimate_xmldoc_req = wapxmltree() 32 | xmlrootgetitemestimatenode = wapxmlnode("GetItemEstimate") 33 | getitemestimate_xmldoc_req.set_root(xmlrootgetitemestimatenode, "getitemestimate") 34 | 35 | xmlcollectionsnode = wapxmlnode("Collections", xmlrootgetitemestimatenode) 36 | 37 | for collection_id in collection_ids: 38 | xml_Collection_node = wapxmlnode("Collection", xmlcollectionsnode) 39 | try: 40 | xml_gie_airsyncSyncKey_node = wapxmlnode("airsync:SyncKey", xml_Collection_node, synckeys[collection_id]) 41 | except KeyError: 42 | xml_gie_airsyncSyncKey_node = wapxmlnode("airsync:SyncKey", xml_Collection_node, "0") 43 | xml_gie_CollectionId_node = wapxmlnode("CollectionId", xml_Collection_node, collection_id)#? 44 | if options[collection_id].has_key("ConversationMode"): 45 | xml_gie_ConverationMode_node = wapxmlnode("airsync:ConversationMode", xml_Collection_node, options[collection_id]["ConversationMode"])#? 46 | xml_gie_airsyncOptions_node = wapxmlnode("airsync:Options", xml_Collection_node) 47 | xml_gie_airsyncClass_node = wapxmlnode("airsync:Class", xml_gie_airsyncOptions_node, options[collection_id]["Class"]) #STR #http://msdn.microsoft.com/en-us/library/gg675489(v=exchg.80).aspx 48 | if options[collection_id].has_key("FilterType"): 49 | xml_gie_airsyncFilterType_node = wapxmlnode("airsync:FilterType", xml_gie_airsyncOptions_node, options[collection_id]["FilterType"]) #INT #http://msdn.microsoft.com/en-us/library/gg663562(v=exchg.80).aspx 50 | if options[collection_id].has_key("MaxItems"): 51 | xml_gie_airsyncMaxItems_node = wapxmlnode("airsync:MaxItems", xml_gie_airsyncMaxItems_node, options[collection_id]["MaxItems"]) #OPTIONAL #INT #http://msdn.microsoft.com/en-us/library/gg675531(v=exchg.80).aspx 52 | return getitemestimate_xmldoc_req 53 | 54 | @staticmethod 55 | def parse(wapxml): 56 | 57 | namespace = "getitemestimate" 58 | root_tag = "GetItemEstimate" 59 | 60 | root_element = wapxml.get_root() 61 | if root_element.get_xmlns() != namespace: 62 | raise AttributeError("Xmlns '%s' submitted to '%s' parser. Should be '%s'." % (root_element.get_xmlns(), root_tag, namespace)) 63 | if root_element.tag != root_tag: 64 | raise AttributeError("Root tag '%s' submitted to '%s' parser. Should be '%s'." % (root_element.tag, root_tag, root_tag)) 65 | 66 | getitemestimate_getitemestimate_children = root_element.get_children() 67 | 68 | #getitemestimate_responses = getitemestimate_getitemestimate_children.get_children() 69 | 70 | responses = [] 71 | 72 | for getitemestimate_response_child in getitemestimate_getitemestimate_children: 73 | response = GetItemEstimate.getitemestimate_response() 74 | if getitemestimate_response_child.tag is "Status": 75 | response.Status = getitemestimate_response_child.text 76 | for element in getitemestimate_response_child: 77 | if element.tag is "Status": 78 | response.Status = element.text 79 | elif element.tag == "Collection": 80 | getitemestimate_collection_children = element.get_children() 81 | collection_id = 0 82 | estimate = 0 83 | for collection_child in getitemestimate_collection_children: 84 | if collection_child.tag == "CollectionId": 85 | response.CollectionId = collection_child.text 86 | elif collection_child.tag == "Estimate": 87 | response.Estimate = collection_child.text 88 | responses.append(response) 89 | return responses 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /peas/pyActiveSync/client/ResolveRecipients.py: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Copyright (C) 2013 Sol Birnbaum 3 | # 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either version 2 7 | # of the License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 17 | # MA 02110-1301, USA. 18 | ######################################################################## 19 | 20 | from ..utils.wapxml import wapxmltree, wapxmlnode 21 | 22 | class ResolveRecipients: 23 | """http://msdn.microsoft.com/en-us/library/gg650949(v=exchg.80).aspx""" 24 | 25 | @staticmethod 26 | def build(to, cert_retrieval=0, max_certs=9999, max_recipients=9999, start_time=None, end_time=None, max_picture_size=100, max_pictures=9999): 27 | resolverecipients_xmldoc_req = wapxmltree() 28 | xmlrootnode = wapxmlnode("ResolveRecipients") 29 | resolverecipients_xmldoc_req.set_root(xmlrootnode, "resolverecipients") 30 | xmltonode = wapxmlnode("To", xmlrootnode, to) 31 | #xmloptionsnode = wapxmlnode("Options", xmlrootnode) 32 | #xmlcertificateretrievalnode = wapxmlnode("CertificateRetrieval", xmloptionsnode, cert_retrieveal) 33 | #xmlmaxcertificatesnode = wapxmlnode("MaxCertificates", xmloptionsnode, max_certs) #0-9999 34 | #xmlmaxambiguousrecipientsnode = wapxmlnode("MaxAmbiguousRecipients", xmloptionsnode, max_recipients) #0-9999 35 | #xmlavailabilitynode = wapxmlnode("Availability", xmloptionsnode) 36 | #xmlstarttimenode = wapxmlnode("StartTime", xmlavailabilitynode, start_time) #datetime 37 | #xmlendtimenode = wapxmlnode("EndTime", xmlavailabilitynode, end_time) #datetime 38 | #xmlpicturenode = wapxmlnode("Picture", xmloptionsnode) 39 | #xmlmaxsizenode = wapxmlnode("MaxSize", xmlpicturenode, max_size) #must be > 100KB 40 | #xmlmaxpicturesnode = wapxmlnode("MaxPictures", xmlpicturenode, max_pictures) 41 | return resolverecipients_xmldoc_req 42 | 43 | @staticmethod 44 | def parse(wapxml): 45 | 46 | namespace = "resolverecipients" 47 | root_tag = "ResolveRecipients" 48 | 49 | root_element = wapxml.get_root() 50 | if root_element.get_xmlns() != namespace: 51 | raise AttributeError("Xmlns '%s' submitted to '%s' parser. Should be '%s'." % (root_element.get_xmlns(), root_tag, namespace)) 52 | if root_element.tag != root_tag: 53 | raise AttributeError("Root tag '%s' submitted to '%s' parser. Should be '%s'." % (root_element.tag, root_tag, root_tag)) 54 | 55 | folderhierarchy_resolverecipients_children = root_element.get_children() 56 | 57 | recipients = [] 58 | 59 | for element in folderhierarchy_resolverecipients_children: 60 | if element.tag is "Status": 61 | folderhierarchy_resolverecipients_status = element.text 62 | if folderhierarchy_resolverecipients_status != "1": 63 | print "ResolveRecipients Status: %s" % folderhierarchy_resolverecipients_status 64 | elif element.tag == "Response": 65 | for response_element in element.get_children(): 66 | if response_element.tag == "To": 67 | response_to = response_element.text 68 | elif response_element.tag == "Status": 69 | response_status = response_element.text 70 | elif response_element.tag == "RecipientCount": 71 | response_count = response_element.text 72 | elif response_element.tag == "Recipient": 73 | response_status = response_element.text 74 | for recipient_element in response_element.get_children(): 75 | if recipient_element.tag == "Type": 76 | recipient_type = recipient_element.text 77 | elif recipient_element.tag == "DisplayName": 78 | recipient_displayname = recipient_element.text 79 | elif recipient_element.tag == "EmailAddress": 80 | recipient_emailaddress = recipient_element.text 81 | elif recipient_element.tag == "Availability": 82 | recipient_availability = recipient_element 83 | elif recipient_element.tag == "Certificates": 84 | recipient_certificates = recipient_element 85 | elif recipient_element.tag == "Picture": 86 | recipient_picture = recipient_element.text 87 | recipients.append((recipient_type, recipient_displayname, recipient_emailaddress, recipient_availability, recipient_certificates, recipient_picture)) 88 | 89 | return (response_status, recipients, response_count) -------------------------------------------------------------------------------- /peas/pyActiveSync/objects/MSASHTTP.py: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Copyright (C) 2013 Sol Birnbaum 3 | # 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either version 2 7 | # of the License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 17 | # MA 02110-1301, USA. 18 | ######################################################################## 19 | 20 | import httplib, urllib 21 | 22 | class ASHTTPConnector(object): 23 | """ActiveSync HTTP object""" 24 | USER_AGENT = "Python" 25 | POST_URL_TEMPLATE = "/Microsoft-Server-ActiveSync?Cmd=%s&User=%s&DeviceId=123456&DeviceType=Python" 26 | POST_GETATTACHMENT_URL_TEMPLATE = "/Microsoft-Server-ActiveSync?Cmd=%s&AttachmentName=%s&User=%s&DeviceId=123456&DeviceType=Python" 27 | 28 | def __init__(self, server, port=443, ssl=True): 29 | 30 | self.server = server 31 | self.port = port 32 | self.ssl = ssl 33 | self.policykey = 0 34 | self.headers = { 35 | "Content-Type": "application/vnd.ms-sync.wbxml", 36 | "User-Agent" : self.USER_AGENT, 37 | "MS-ASProtocolVersion" : "14.1", 38 | "Accept-Language" : "en_us" 39 | } 40 | return 41 | 42 | def set_credential(self, username, password): 43 | import base64 44 | self.username = username 45 | self.credential = base64.b64encode(username+":"+password) 46 | self.headers.update({"Authorization" : "Basic " + self.credential}) 47 | 48 | def do_post(self, url, body, headers, redirected=False): 49 | if self.ssl: 50 | conn = httplib.HTTPSConnection(self.server, self.port) 51 | conn.request("POST", url, body, headers) 52 | else: 53 | conn = httplib.HTTPConnection(self.server, self.port) 54 | conn.request("POST", url, body, headers) 55 | res = conn.getresponse() 56 | if res.status == 451: 57 | self.server = res.getheader("X-MS-Location").split()[2] 58 | if not redirected: 59 | return self.do_post(url, body, headers, False) 60 | else: 61 | raise Exception("Redirect loop encountered. Stopping request.") 62 | else: 63 | return res 64 | 65 | 66 | def post(self, cmd, body): 67 | url = self.POST_URL_TEMPLATE % (cmd, self.username) 68 | res = self.do_post(url, body, self.headers) 69 | #print res.status, res.reason, res.getheaders() 70 | return res.read() 71 | 72 | def fetch_multipart(self, body, filename="fetched_file.tmp"): 73 | """http://msdn.microsoft.com/en-us/library/ee159875(v=exchg.80).aspx""" 74 | headers = self.headers 75 | headers.update({"MS-ASAcceptMultiPart":"T"}) 76 | url = self.POST_URL_TEMPLATE % ("ItemOperations", self.username) 77 | res = self.do_post(url, body, headers) 78 | if res.getheaders()["Content-Type"] == "application/vnd.ms-sync.multipart": 79 | PartCount = int(res.read(4)) 80 | PartMetaData = [] 81 | for partindex in range(0, PartCount): 82 | PartMetaData.append((int(res.read(4))), (int(res.read(4)))) 83 | wbxml_part = res.read(PartMetaData[0][1]) 84 | fetched_file = open(filename, "wb") 85 | for partindex in range(1, PartCount): 86 | fetched_file.write(res.read(PartMetaData[0][partindex])) 87 | fetched_file.close() 88 | return wbxml, filename 89 | else: 90 | raise TypeError("Client requested MultiPart response, but server responsed with inline.") 91 | 92 | def get_attachment(self, attachment_name): #attachment_name = DisplayName of attachment from an MSASAIRS.Attachment object 93 | url = self.POST_GETATTACHMENT_URL_TEMPLATE % ("GetAttachment", attachment_name, self.username) 94 | res = self.do_post(url, "", self.headers) 95 | try: 96 | content_type = res.getheader("Content-Type") 97 | except: 98 | content_type = "text/plain" 99 | res.status 100 | return res.read(), res.status, content_type 101 | 102 | def get_options(self): 103 | conn = httplib.HTTPSConnection(self.server, self.port) 104 | conn.request("OPTIONS", "/Microsoft-Server-ActiveSync", None, self.headers) 105 | res = conn.getresponse() 106 | return res 107 | 108 | def options(self): 109 | res = self.get_options() 110 | if res.status is 200: 111 | self._server_protocol_versions = res.getheader("ms-asprotocolversions") 112 | self._server_protocol_commands = res.getheader("ms-asprotocolcommands") 113 | self._server_version = res.getheader("ms-server-activesync") 114 | return True 115 | else: 116 | print "Connection Error!:" 117 | print res.status, res.reason 118 | for header in res.getheaders(): 119 | print header[0]+":",header[1] 120 | return False 121 | 122 | def get_policykey(self): 123 | return self.policykey 124 | 125 | def set_policykey(self, policykey): 126 | self.policykey = policykey 127 | self.headers.update({ "X-MS-PolicyKey" : self.policykey }) -------------------------------------------------------------------------------- /peas/pyActiveSync/objects/MSASTASK.py: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Copyright (C) 2013 Sol Birnbaum 3 | # 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either version 2 7 | # of the License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 17 | # MA 02110-1301, USA. 18 | ######################################################################## 19 | 20 | """[MS-ASTASK] Task objects""" 21 | 22 | from MSASEMAIL import airsyncbase_Body 23 | 24 | def parse_task(data): 25 | task_dict = {} 26 | task_base = data.get_children() 27 | task_dict.update({"server_id" : task_base[0].text}) 28 | task_elements = task_base[1].get_children() 29 | for element in task_elements: 30 | if element.tag == "airsyncbase:Body": 31 | body = airsyncbase_Body() 32 | body.parse(element) 33 | task_dict.update({ "airsyncbase_Body" : body }) 34 | elif element.tag == "tasks:CalendarType": 35 | task_dict.update({ "tasks_CalendarType" : element.text }) 36 | elif element.tag == "tasks:Categories": 37 | categories_list = [] 38 | categories = element.get_children() 39 | for category_element in categories: 40 | categories_list.append(category_element.text) 41 | task_dict.update({ "tasks_Categories" : categories_list }) 42 | elif element.tag == "tasks:Complete": 43 | task_dict.update({ "tasks_Complete" : element.text }) 44 | elif element.tag == "tasks:DateCompleted": 45 | task_dict.update({ "tasks_DateCompleted" : element.text }) 46 | elif element.tag == "tasks:DueDate": 47 | task_dict.update({ "tasks_DueDate" : element.text }) 48 | elif element.tag == "tasks:Importance": 49 | task_dict.update({ "tasks_Importance" : element.text }) 50 | elif element.tag == "tasks:OrdinalDate": 51 | task_dict.update({ "tasks_OrdinalDate" : element.text }) 52 | elif element.tag == "tasks:Recurrence": 53 | recurrence_dict = {} 54 | for recurrence_element in element.get_children(): 55 | if recurrence_element.tag == "tasks:Type": 56 | recurrence_dict.update({ "tasks_Type" : recurrence_element.text }) 57 | elif recurrence_element.tag == "tasks:Occurrences": 58 | recurrence_dict.update({ "tasks_Occurrences" : recurrence_element.text }) 59 | elif recurrence_element.tag == "tasks:Regenerate": 60 | recurrence_dict.update({ "tasks_Regenerate" : recurrence_element.text }) 61 | elif recurrence_element.tag == "tasks:DeadOccur": 62 | recurrence_dict.update({ "tasks_DeadOccur" : recurrence_element.text }) 63 | elif recurrence_element.tag == "tasks:FirstDayOfWeek": 64 | recurrence_dict.update({ "tasks_FirstDayOfWeek" : recurrence_element.text }) 65 | elif recurrence_element.tag == "tasks:Interval": 66 | recurrence_dict.update({ "tasks_Interval" : recurrence_element.text }) 67 | elif recurrence_element.tag == "tasks:IsLeapMonth": 68 | recurrence_dict.update({ "tasks_IsLeapMonth" : recurrence_element.text }) 69 | elif recurrence_element.tag == "tasks:WeekOfMonth": 70 | recurrence_dict.update({ "tasks_WeekOfMonth" : recurrence_element.text }) 71 | elif recurrence_element.tag == "tasks:DayOfMonth": 72 | recurrence_dict.update({ "tasks_DayOfMonth" : recurrence_element.text }) 73 | elif recurrence_element.tag == "tasks:DayOfWeek": 74 | recurrence_dict.update({ "tasks_DayOfWeek" : recurrence_element.text }) 75 | elif recurrence_element.tag == "tasks:MonthOfYear": 76 | recurrence_dict.update({ "tasks_MonthOfYear" : recurrence_element.text }) 77 | elif recurrence_element.tag == "tasks:Until": 78 | recurrence_dict.update({ "tasks_Until" : recurrence_element.text }) 79 | elif recurrence_element.tag == "tasks:Start": 80 | recurrence_dict.update({ "tasks_Start" : recurrence_element.text }) 81 | elif recurrence_element.tag == "tasks:CalendarType": 82 | recurrence_dict.update({ "tasks_CalendarType" : recurrence_element.text }) 83 | task_dict.update({ "tasks_Recurrence" : recurrence_dict }) 84 | elif element.tag == "tasks:ReminderSet": 85 | task_dict.update({ "tasks_ReminderSet" : element.text }) 86 | elif element.tag == "tasks:ReminderTime": 87 | task_dict.update({ "tasks_ReminderTime" : element.text }) 88 | elif element.tag == "tasks:Sensitivity": 89 | task_dict.update({ "tasks_Sensitivity" : element.text }) 90 | elif element.tag == "tasks:StartDate": 91 | task_dict.update({ "tasks_StartDate" : element.text }) 92 | elif element.tag == "tasks:Subject": 93 | task_dict.update({ "tasks_Subject" : element.text }) 94 | elif element.tag == "tasks:SubOrdinalDate": 95 | task_dict.update({ "tasks_SubOrdinalDate" : element.text }) 96 | elif element.tag == "tasks:UtcDueDate": 97 | task_dict.update({ "tasks_UtcDueDate" : element.text }) 98 | elif element.tag == "tasks:UtcStartDate": 99 | task_dict.update({ "tasks_UtcStartDate" : element.text }) 100 | return task_dict -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PEAS 2 | PEAS is a Python 2 library and command line application for running commands on an ActiveSync server e.g. Microsoft Exchange. It is based on [research](https://labs.mwrinfosecurity.com/blog/accessing-internal-fileshares-through-exchange-activesync) into Exchange ActiveSync protocol by Adam Rutherford and David Chismon of MWR. 3 | 4 | ## Prerequisites 5 | 6 | * `python` is Python 2, otherwise use `python2` 7 | * Python [Requests](http://docs.python-requests.org/) library 8 | 9 | ## Significant source files 10 | Path | Functionality 11 | --- | --- 12 | `peas/__main__.py` | The command line application. 13 | `peas/peas.py` | The PEAS client class that exclusively defines the interface to PEAS. 14 | `peas/py_activesync_helper.py` | The helper functions that control the interface to pyActiveSync. 15 | `peas/pyActiveSync/client` | The pyActiveSync EAS command builders and parsers. 16 | 17 | ## Optional installation 18 | `python setup.py install` 19 | 20 | # PEAS application 21 | PEAS can be run without installation from the parent `peas` directory (containing this readme). PEAS can also be run with the command `peas` after installation. 22 | 23 | ## Running PEAS 24 | 25 | `python -m peas [options] ` 26 | 27 | 28 | ## Example usage 29 | ### Check server 30 | `python -m peas 10.207.7.100` 31 | 32 | ### Check credentials 33 | `python -m peas --check -u luke2 -p ChangeMe123 10.207.7.100` 34 | 35 | ### Get emails 36 | `python -m peas --emails -u luke2 -p ChangeMe123 10.207.7.100` 37 | 38 | ### Save emails to directory 39 | `python -m peas --emails -O emails -u luke2 -p ChangeMe123 10.207.7.100` 40 | 41 | ### List file shares 42 | `python -m peas --list-unc='\\fictitious-dc' -u luke2 -p ChangeMe123 10.207.7.100` 43 | 44 | `python -m peas --list-unc='\\fictitious-dc\guestshare' -u luke2 -p ChangeMe123 10.207.7.100` 45 | 46 | **Note:** Using an IP address or FQDN instead of a hostname in the UNC path may fail. 47 | 48 | ### View file on file share 49 | `python -m peas --dl-unc='\\fictitious-dc\guestshare\fileonguestshare.txt' -u luke2 -p ChangeMe123 10.207.7.100` 50 | 51 | ### Save file from file share 52 | `python -m peas --dl-unc='\\fictitious-dc\guestshare\fileonguestshare.txt' -o file.txt -u luke2 -p ChangeMe123 10.207.7.100` 53 | 54 | ### Command line arguments 55 | 56 | Run `python -m peas --help` for the latest options. 57 | 58 | Options: 59 | -h, --help show this help message and exit 60 | -u USER username 61 | -p PASSWORD password 62 | --smb-user=USER username to use for SMB operations 63 | --smb-pass=PASSWORD password to use for SMB operations 64 | --verify-ssl verify SSL certificates (important) 65 | -o FILENAME output to file 66 | -O PATH output directory (for specific commands only, not 67 | combined with -o) 68 | -F repr,hex,b64,stdout,stderr,file 69 | output formatting and encoding options 70 | --check check if account can be accessed with given password 71 | --emails retrieve emails 72 | --list-unc=UNC_PATH list the files at a given UNC path 73 | --dl-unc=UNC_PATH download the file at a given UNC path 74 | 75 | 76 | ## PEAS library 77 | 78 | PEAS can be imported as a library. 79 | 80 | ### Example code 81 | 82 | import peas 83 | 84 | # Create an instance of the PEAS client. 85 | client = peas.Peas() 86 | 87 | # Display the documentation for the PEAS client. 88 | help(client) 89 | 90 | # Disable certificate verification so self-signed certificates don't cause errors. 91 | client.disable_certificate_verification() 92 | 93 | # Set the credentials and server to connect to. 94 | client.set_creds({ 95 | 'server': '10.207.7.100', 96 | 'user': 'luke2', 97 | 'password': 'ChangeMe123', 98 | }) 99 | 100 | # Check the credentials are accepted. 101 | print("Auth result:", client.check_auth()) 102 | 103 | # Retrieve a file share directory listing. 104 | listing = client.get_unc_listing(r'\\fictitious-dc\guestshare') 105 | print(listing) 106 | 107 | # Retrieve emails. 108 | emails = client.extract_emails() 109 | print(emails) 110 | 111 | ## Extending 112 | 113 | To extend the functionality of PEAS, there is a four step process: 114 | 115 | 1. Create a builder and parser for the EAS command if it has not been implemented in `pyActiveSync/client`. Copying an existing source file for another command and then editing it has proved effective. The [Microsoft EAS documentation](https://msdn.microsoft.com/en-us/library/ee202197%28v=exchg.80%29.aspx) describes the structure of the XML that must be created and parsed from the response. 116 | 117 | 2. Create a helper function in `py_activesync_helper.py` that connects to the EAS server over HTTPS, builds and runs the command to achieve the desired functionality. Again, copying an existing function such as `get_unc_listing` can be effective. 118 | 119 | 3. Create a method in the `Peas` class that calls the helper function to achieve the desired functionality. This is where PEAS would decide which backend helper function to call if py-eas-client was also an option. 120 | 121 | 4. Add command line support for the feature to the PEAS application by editing `peas/__main__.py`. A new option should be added that when set, calls the method created in the previous step. 122 | 123 | 124 | ## Limitations 125 | 126 | PEAS has been tested on Kali 2.0 against Microsoft Exchange Server 2013 and 2016. The domain controller was Windows 2012 and the Exchange server was running on the same machine. Results with other configurations may vary. 127 | 128 | py-eas-client support is limited to retrieving emails and causes a dependency on Twisted. It was included when the library was being evaluated but it makes sense to remove it from PEAS now, as all functionality can be provided by pyActiveSync. 129 | 130 | The licence may be restrictive due to the inclusion of pyActiveSync, which uses the GPLv2. 131 | 132 | The requirement to know the hostname of the target machine for file share access may impede enumeration. 133 | -------------------------------------------------------------------------------- /peas/__main__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Adam Rutherford' 2 | 3 | import sys 4 | import os 5 | import hashlib 6 | from optparse import OptionParser 7 | 8 | import peas 9 | 10 | 11 | def error(msg): 12 | sys.stderr.write("[-] " + msg + "\n") 13 | 14 | 15 | def positive(msg): 16 | sys.stdout.write("[+] " + msg + "\n") 17 | 18 | 19 | def negative(msg): 20 | sys.stdout.write("[-] " + msg + "\n") 21 | 22 | 23 | def create_arg_parser(): 24 | 25 | usage = "python -m peas [options] " 26 | parser = OptionParser(usage=usage) 27 | 28 | # Settings: 29 | parser.add_option("-u", None, dest="user", 30 | help="username", metavar="USER") 31 | 32 | parser.add_option("-p", None, dest="password", 33 | help="password", metavar="PASSWORD") 34 | 35 | parser.add_option("-q", None, dest="quiet", 36 | action="store_true", default=False, 37 | help="suppress all unnecessary output") 38 | 39 | parser.add_option("--smb-user", None, 40 | dest="smb_user", 41 | help="username to use for SMB operations", 42 | metavar="USER") 43 | 44 | parser.add_option("--smb-pass", None, 45 | dest="smb_password", 46 | help="password to use for SMB operations", 47 | metavar="PASSWORD") 48 | 49 | parser.add_option("--verify-ssl", None, dest="verify_ssl", 50 | action="store_true", default=False, 51 | help="verify SSL certificates (important)") 52 | 53 | parser.add_option("-o", None, dest="file", 54 | help="output to file", metavar="FILENAME") 55 | 56 | parser.add_option("-O", None, dest="output_dir", 57 | help="output directory (for specific commands only, not combined with -o)", metavar="PATH") 58 | 59 | parser.add_option("-F", None, dest="format", 60 | help="output formatting and encoding options", 61 | metavar="repr,hex,b64,stdout,stderr,file") 62 | 63 | # Functionality: 64 | parser.add_option("--check", None, 65 | action="store_true", dest="check", 66 | help="check if account can be accessed with given password") 67 | 68 | parser.add_option("--emails", None, 69 | action="store_true", dest="extract_emails", 70 | help="retrieve emails") 71 | 72 | parser.add_option("--list-unc", None, 73 | dest="list_unc", 74 | help="list the files at a given UNC path", 75 | metavar="UNC_PATH") 76 | 77 | parser.add_option("--dl-unc", None, 78 | dest="dl_unc", 79 | help="download the file at a given UNC path", 80 | metavar="UNC_PATH") 81 | 82 | return parser 83 | 84 | 85 | def init_authed_client(options, verify=True): 86 | 87 | if options.user is None: 88 | error("A username must be specified for this command.") 89 | return False 90 | if options.password is None: 91 | error("A password must be specified for this command.") 92 | return False 93 | 94 | client = peas.Peas() 95 | 96 | creds = { 97 | 'server': options.server, 98 | 'user': options.user, 99 | 'password': options.password, 100 | } 101 | if options.smb_user is not None: 102 | creds['smb_user'] = options.smb_user 103 | if options.smb_password is not None: 104 | creds['smb_password'] = options.smb_password 105 | 106 | client.set_creds(creds) 107 | 108 | if not verify: 109 | client.disable_certificate_verification() 110 | 111 | return client 112 | 113 | 114 | def check_server(options): 115 | 116 | client = peas.Peas() 117 | 118 | client.set_creds({'server': options.server}) 119 | 120 | if not options.verify_ssl: 121 | client.disable_certificate_verification() 122 | 123 | result = client.get_server_headers() 124 | output_result(str(result), options, default='stdout') 125 | 126 | 127 | def check(options): 128 | 129 | client = init_authed_client(options, verify=options.verify_ssl) 130 | if not client: 131 | return 132 | 133 | creds_valid = client.check_auth() 134 | if creds_valid: 135 | positive("Auth success.") 136 | else: 137 | negative("Auth failure.") 138 | 139 | 140 | def extract_emails(options): 141 | 142 | client = init_authed_client(options, verify=options.verify_ssl) 143 | if not client: 144 | return 145 | 146 | emails = client.extract_emails() 147 | # TODO: Output the emails in a more useful format. 148 | for i, email in enumerate(emails): 149 | 150 | if options.output_dir: 151 | fname = 'email_%d_%s.xml' % (i, hashlib.md5(email).hexdigest()) 152 | path = os.path.join(options.output_dir, fname) 153 | open(path, 'wb').write(email.strip() + '\n') 154 | else: 155 | output_result(email + '\n', options, default='repr') 156 | 157 | if options.output_dir: 158 | print("Wrote %d emails to %r" % (len(emails), options.output_dir)) 159 | 160 | 161 | def list_unc(options): 162 | 163 | client = init_authed_client(options, verify=options.verify_ssl) 164 | if not client: 165 | return 166 | 167 | path = options.list_unc 168 | records = client.get_unc_listing(path) 169 | 170 | output = [] 171 | 172 | if not options.quiet: 173 | print("Listing: %s\n" % (path,)) 174 | 175 | for record in records: 176 | 177 | name = record.get('DisplayName') 178 | path = record.get('LinkId') 179 | is_folder = record.get('IsFolder') == '1' 180 | is_hidden = record.get('IsHidden') == '1' 181 | size = record.get('ContentLength', '0') + 'B' 182 | ctype = record.get('ContentType', '-') 183 | last_mod = record.get('LastModifiedDate', '-') 184 | created = record.get('CreationDate', '-') 185 | 186 | attrs = ('f' if is_folder else '-') + ('h' if is_hidden else '-') 187 | 188 | output.append("%s %-24s %-24s %-24s %-12s %s" % (attrs, created, last_mod, ctype, size, path)) 189 | 190 | output_result('\n'.join(output) + '\n', options, default='stdout') 191 | 192 | 193 | def dl_unc(options): 194 | 195 | client = init_authed_client(options, verify=options.verify_ssl) 196 | if not client: 197 | return 198 | 199 | path = options.dl_unc 200 | data = client.get_unc_file(path) 201 | 202 | if not options.quiet: 203 | print("Downloading: %s\n" % (path,)) 204 | 205 | output_result(data, options, default='repr') 206 | 207 | 208 | def output_result(data, options, default='repr'): 209 | 210 | fmt = options.format 211 | if not fmt: 212 | fmt = 'file' if options.file else default 213 | actions = fmt.split(',') 214 | 215 | # Write to file at the end if a filename is specified. 216 | if options.file and 'file' not in actions: 217 | actions.append('file') 218 | 219 | # Process the output based on the format/encoding options chosen. 220 | encoding_used = True 221 | for action in actions: 222 | if action == 'repr': 223 | data = repr(data) 224 | encoding_used = False 225 | elif action == 'hex': 226 | data = data.encode('hex') 227 | encoding_used = False 228 | elif action in ['base64', 'b64']: 229 | data = data.encode('base64') 230 | encoding_used = False 231 | elif action == 'stdout': 232 | print(data) 233 | encoding_used = True 234 | elif action == 'stderr': 235 | sys.stderr.write(data) 236 | encoding_used = True 237 | # Allow the user to write the file after other encodings have been applied. 238 | elif action == 'file': 239 | if options.file: 240 | open(options.file, 'wb').write(data) 241 | if not options.quiet: 242 | print("Wrote %d bytes to %r." % (len(data), options.file)) 243 | else: 244 | error("No filename specified.") 245 | encoding_used = True 246 | 247 | # Print now if an encoding has been used but never output. 248 | if not encoding_used: 249 | print(data) 250 | 251 | 252 | def process_options(options): 253 | 254 | # Create the output directory if necessary. 255 | if options.output_dir: 256 | try: 257 | os.makedirs(options.output_dir) 258 | except OSError: 259 | pass 260 | 261 | return options 262 | 263 | 264 | def main(): 265 | 266 | # Parse the arguments to the program into an options object. 267 | arg_parser = create_arg_parser() 268 | (options, args) = arg_parser.parse_args() 269 | 270 | if not options.quiet: 271 | peas.show_banner() 272 | 273 | options = process_options(options) 274 | 275 | # The server is required as an argument. 276 | if not args: 277 | arg_parser.print_help() 278 | return 279 | options.server = args[0] 280 | 281 | # Perform the requested functionality. 282 | ran = False 283 | if options.check: 284 | check(options) 285 | ran = True 286 | if options.extract_emails: 287 | extract_emails(options) 288 | ran = True 289 | if options.list_unc: 290 | list_unc(options) 291 | ran = True 292 | if options.dl_unc: 293 | dl_unc(options) 294 | ran = True 295 | if not ran: 296 | check_server(options) 297 | 298 | 299 | if __name__ == '__main__': 300 | main() 301 | -------------------------------------------------------------------------------- /peas/pyActiveSync/misc_tests.py: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Copyright (C) 2013 Sol Birnbaum 3 | # 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either version 2 7 | # of the License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 17 | # MA 02110-1301, USA. 18 | ######################################################################## 19 | 20 | # Tests 21 | 22 | import sys, time 23 | import ssl 24 | 25 | from utils.as_code_pages import as_code_pages 26 | from utils.wbxml import wbxml_parser 27 | from utils.wapxml import wapxmltree, wapxmlnode 28 | from client.storage import storage 29 | 30 | from client.FolderSync import FolderSync 31 | from client.Sync import Sync 32 | from client.GetItemEstimate import GetItemEstimate 33 | from client.ResolveRecipients import ResolveRecipients 34 | from client.FolderCreate import FolderCreate 35 | from client.FolderUpdate import FolderUpdate 36 | from client.FolderDelete import FolderDelete 37 | from client.Ping import Ping 38 | from client.MoveItems import MoveItems 39 | from client.Provision import Provision 40 | from client.ItemOperations import ItemOperations 41 | from client.ValidateCert import ValidateCert 42 | from client.SendMail import SendMail 43 | from client.SmartForward import SmartForward 44 | from client.SmartReply import SmartReply 45 | 46 | from objects.MSASHTTP import ASHTTPConnector 47 | from objects.MSASCMD import FolderHierarchy, as_status 48 | from objects.MSASAIRS import airsync_FilterType, airsync_Conflict, airsync_MIMETruncation, airsync_MIMESupport, airsync_Class, airsyncbase_Type 49 | 50 | from proto_creds import * #create a file proto_creds.py with vars: as_server, as_user, as_pass 51 | 52 | 53 | # Disable SSL certificate verification. 54 | ssl._create_default_https_context = ssl._create_unverified_context 55 | 56 | 57 | pyver = sys.version_info 58 | 59 | storage.create_db_if_none() 60 | conn, curs = storage.get_conn_curs() 61 | device_info = {"Model":"%d.%d.%d" % (pyver[0], pyver[1], pyver[2]), "IMEI":"123456", "FriendlyName":"My pyAS Client", "OS":"Python", "OSLanguage":"en-us", "PhoneNumber": "NA", "MobileOperator":"NA", "UserAgent": "pyAS"} 62 | 63 | #create wbxml_parser test 64 | cp, cp_sh = as_code_pages.build_as_code_pages() 65 | parser = wbxml_parser(cp, cp_sh) 66 | 67 | #create activesync connector 68 | as_conn = ASHTTPConnector(as_server) #e.g. "as.myserver.com" 69 | as_conn.set_credential(as_user, as_pass) 70 | as_conn.options() 71 | policykey = storage.get_keyvalue("X-MS-PolicyKey") 72 | if policykey: 73 | as_conn.set_policykey(policykey) 74 | 75 | def as_request(cmd, wapxml_req): 76 | print "\r\n%s Request:" % cmd 77 | print wapxml_req 78 | res = as_conn.post(cmd, parser.encode(wapxml_req)) 79 | wapxml_res = parser.decode(res) 80 | print "\r\n%s Response:" % cmd 81 | print wapxml_res 82 | return wapxml_res 83 | 84 | #Provision functions 85 | def do_apply_eas_policies(policies): 86 | for policy in policies.keys(): 87 | print "Virtually applying %s = %s" % (policy, policies[policy]) 88 | return True 89 | 90 | def do_provision(): 91 | provision_xmldoc_req = Provision.build("0", device_info) 92 | as_conn.set_policykey("0") 93 | provision_xmldoc_res = as_request("Provision", provision_xmldoc_req) 94 | status, policystatus, policykey, policytype, policydict, settings_status = Provision.parse(provision_xmldoc_res) 95 | as_conn.set_policykey(policykey) 96 | storage.update_keyvalue("X-MS-PolicyKey", policykey) 97 | storage.update_keyvalue("EASPolicies", repr(policydict)) 98 | if do_apply_eas_policies(policydict): 99 | provision_xmldoc_req = Provision.build(policykey) 100 | provision_xmldoc_res = as_request("Provision", provision_xmldoc_req) 101 | status, policystatus, policykey, policytype, policydict, settings_status = Provision.parse(provision_xmldoc_res) 102 | if status == "1": 103 | as_conn.set_policykey(policykey) 104 | storage.update_keyvalue("X-MS-PolicyKey", policykey) 105 | 106 | #FolderSync + Provision 107 | foldersync_xmldoc_req = FolderSync.build(storage.get_synckey("0")) 108 | foldersync_xmldoc_res = as_request("FolderSync", foldersync_xmldoc_req) 109 | changes, synckey, status = FolderSync.parse(foldersync_xmldoc_res) 110 | if int(status) > 138 and int(status) < 145: 111 | print as_status("FolderSync", status) 112 | do_provision() 113 | foldersync_xmldoc_res = as_request("FolderSync", foldersync_xmldoc_req) 114 | changes, synckey, status = FolderSync.parse(foldersync_xmldoc_res) 115 | if int(status) > 138 and int(status) < 145: 116 | print as_status("FolderSync", status) 117 | raise Exception("Unresolvable provisoning error: %s. Cannot continue..." % status) 118 | if len(changes) > 0: 119 | storage.update_folderhierarchy(changes) 120 | storage.update_synckey(synckey, "0", curs) 121 | conn.commit() 122 | 123 | #ItemOperations 124 | itemoperations_params = [{"Name":"Fetch","Store":"Mailbox", "FileReference":"%34%67%32"}] 125 | itemoperations_xmldoc_req = ItemOperations.build(itemoperations_params) 126 | print "\r\nItemOperations Request:\r\n", itemoperations_xmldoc_req 127 | #itemoperations_xmldoc_res, attachment_file = as_conn.fetch_multipart(itemoperations_xmldoc_req, "myattachment1.txt") 128 | #itemoperations_xmldoc_res_parsed = ItemOperations.parse(itemoperations_xmldoc_res) 129 | #print itemoperations_xmldoc_res 130 | 131 | #FolderCreate 132 | parent_folder = storage.get_folderhierarchy_folder_by_name("Inbox", curs) 133 | new_folder = FolderHierarchy.Folder(parent_folder[0], "TestFolder1", str(FolderHierarchy.FolderCreate.Type.Mail)) 134 | foldercreate_xmldoc_req = FolderCreate.build(storage.get_synckey("0"), new_folder.ParentId, new_folder.DisplayName, new_folder.Type) 135 | foldercreate_xmldoc_res = as_request("FolderCreate", foldercreate_xmldoc_req) 136 | foldercreate_res_parsed = FolderCreate.parse(foldercreate_xmldoc_res) 137 | if foldercreate_res_parsed[0] == "1": 138 | new_folder.ServerId = foldercreate_res_parsed[2] 139 | storage.insert_folderhierarchy_change(new_folder, curs) 140 | storage.update_synckey(foldercreate_res_parsed[1], "0", curs) 141 | conn.commit() 142 | else: 143 | print as_status("FolderCreate", foldercreate_res_parsed[0]) 144 | 145 | time.sleep(5) 146 | 147 | #FolderUpdate 148 | old_folder_name = "TestFolder1" 149 | new_folder_name = "TestFolder2" 150 | #new_parent_id = parent_folder = storage.get_folderhierarchy_folder_by_name("Inbox", curs) 151 | folder_row = storage.get_folderhierarchy_folder_by_name(old_folder_name, curs) 152 | update_folder = FolderHierarchy.Folder(folder_row[1], new_folder_name, folder_row[3], folder_row[0]) 153 | folderupdate_xmldoc_req = FolderUpdate.build(storage.get_synckey("0"), update_folder.ServerId, update_folder.ParentId, update_folder.DisplayName) 154 | folderupdate_xmldoc_res = as_request("FolderUpdate", folderupdate_xmldoc_req) 155 | folderupdate_res_parsed = FolderUpdate.parse(folderupdate_xmldoc_res) 156 | if folderupdate_res_parsed[0] == "1": 157 | new_folder.DisplayName = new_folder_name 158 | storage.update_folderhierarchy_change(new_folder, curs) 159 | storage.update_synckey(folderupdate_res_parsed[1], "0", curs) 160 | conn.commit() 161 | 162 | time.sleep(5) 163 | 164 | #FolderDelete 165 | try: 166 | folder_name = "TestFolder2" 167 | folder_row = storage.get_folderhierarchy_folder_by_name(folder_name, curs) 168 | delete_folder = FolderHierarchy.Folder() 169 | delete_folder.ServerId = folder_row[0] 170 | folderdelete_xmldoc_req = FolderDelete.build(storage.get_synckey("0"), delete_folder.ServerId) 171 | folderdelete_xmldoc_res = as_request("FolderDelete", folderdelete_xmldoc_req) 172 | folderdelete_res_parsed = FolderDelete.parse(folderdelete_xmldoc_res) 173 | if folderdelete_res_parsed[0] == "1": 174 | storage.delete_folderhierarchy_change(delete_folder, curs) 175 | storage.update_synckey(folderdelete_res_parsed[1], "0", curs) 176 | conn.commit() 177 | except TypeError, e: 178 | print "\r\n%s\r\n" % e 179 | pass 180 | 181 | #ResolveRecipients 182 | resolverecipients_xmldoc_req = ResolveRecipients.build("thunderbird") 183 | resolverecipients_xmldoc_res = as_request("ResolveRecipients", resolverecipients_xmldoc_req) 184 | 185 | 186 | #SendMail 187 | import email.mime.text 188 | email_mid = storage.get_new_mid() 189 | my_email = email.mime.text.MIMEText("Test email #%s from pyAS." % email_mid) 190 | my_email["Subject"] = "Test #%s from pyAS!" % email_mid 191 | 192 | my_email["From"] = as_user 193 | my_email["To"] = as_user 194 | 195 | sendmail_xmldoc_req = SendMail.build(email_mid, my_email) 196 | print "\r\nRequest:" 197 | print sendmail_xmldoc_req 198 | res = as_conn.post("SendMail", parser.encode(sendmail_xmldoc_req)) 199 | print "\r\nResponse:" 200 | if res == '': 201 | print "\r\nTest message sent successfully!" 202 | else: 203 | sendmail_xmldoc_res = parser.decode(res) 204 | print sendmail_xmldoc_res 205 | sendmail_res = SendMail.parse(sendmail_xmldoc_res) 206 | 207 | ##MoveItems 208 | #moveitems_xmldoc_req = MoveItems.build([("5:24","5","10")]) 209 | #moveitems_xmldoc_res = as_request("MoveItems", moveitems_xmldoc_req) 210 | #moveitems_res = MoveItems.parse(moveitems_xmldoc_res) 211 | #for moveitem_res in moveitems_res: 212 | # if moveitem_res[1] == "3": 213 | # storage.update_email({"server_id": moveitem_res[0] ,"ServerId": moveitem_res[2]}, curs) 214 | # conn.commit() -------------------------------------------------------------------------------- /peas/pyActiveSync/objects/MSASCNTC.py: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Copyright (C) 2013 Sol Birnbaum 3 | # 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either version 2 7 | # of the License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 17 | # MA 02110-1301, USA. 18 | ######################################################################## 19 | 20 | """[MS-ASCNTC] Contact objects""" 21 | 22 | from MSASEMAIL import airsyncbase_Body 23 | 24 | def parse_contact(data): 25 | contact_dict = {} 26 | contact_base = data.get_children() 27 | contact_dict.update({"server_id" : contact_base[0].text}) 28 | contact_elements = contact_base[1].get_children() 29 | for element in contact_elements: 30 | if element.tag == "contacts2:AccountName": 31 | contact_dict.update({ "contacts2_AccountName" : element.text }) 32 | elif element.tag == "contacts:Alias": 33 | contact_dict.update({ "contacts_Alias" : element.text }) 34 | elif element.tag == "contacts:Anniversary": 35 | contact_dict.update({ "contacts_Anniversary" : element.text }) 36 | elif element.tag == "contacts:AssistantName": 37 | contact_dict.update({ "contacts_AssistantName" : element.text }) 38 | elif element.tag == "contacts:AssistantPhoneNumber": 39 | contact_dict.update({ "contacts_AssistantPhoneNumber" : element.text }) 40 | elif element.tag == "contacts:Birthday": 41 | contact_dict.update({ "contacts_Birthday" : element.text }) 42 | elif element.tag == "airsyncbase:Body": 43 | body = airsyncbase_Body() 44 | body.parse(element) 45 | contact_dict.update({ "airsyncbase_Body" : body }) 46 | elif element.tag == "contacts:BusinessAddressCity": 47 | contact_dict.update({ "contacts_BusinessAddressCity" : element.text }) 48 | elif element.tag == "contacts:BusinessAddressCountry": 49 | contact_dict.update({ "contacts_BusinessAddressCountry" : element.text }) 50 | elif element.tag == "contacts:BusinessAddressPostalCode": 51 | contact_dict.update({ "contacts_BusinessAddressPostalCode" : element.text }) 52 | elif element.tag == "contacts:BusinessAddressState": 53 | contact_dict.update({ "contacts_BusinessAddressState" : element.text }) 54 | elif element.tag == "contacts:BusinessAddressStreet": 55 | contact_dict.update({ "contacts_BusinessAddressStreet" : element.text }) 56 | elif element.tag == "contacts:BusinessFaxNumber": 57 | contact_dict.update({ "contacts_BusinessFaxNumber" : element.text }) 58 | elif element.tag == "contacts:BusinessPhoneNumber": 59 | contact_dict.update({ "contacts_BusinessPhoneNumber" : element.text }) 60 | elif element.tag == "contacts:Business2PhoneNumber": 61 | contact_dict.update({ "contacts_Business2PhoneNumber" : element.text }) 62 | elif element.tag == "contacts:CarPhoneNumber": 63 | contact_dict.update({ "contacts_CarPhoneNumber" : element.text }) 64 | elif element.tag == "contacts:Categories": 65 | categories_list = [] 66 | categories = element.get_children() 67 | for category_element in categories: 68 | categories_list.append(category_element.text) 69 | contact_dict.update({ "contacts_Categories" : categories_list }) 70 | elif element.tag == "contacts:Children": 71 | children_list = [] 72 | children = element.get_children() 73 | for child_element in children: 74 | children_list.append(child_element.text) 75 | contact_dict.update({ "contacts_Children" : children_list }) 76 | elif element.tag == "contacts2:CompanyMainPhone": 77 | contact_dict.update({ "contacts2_CompanyMainPhone" : element.text }) 78 | elif element.tag == "contacts:CompanyName": 79 | contact_dict.update({ "contacts_CompanyName" : element.text }) 80 | elif element.tag == "contacts2:CustomerId": 81 | contact_dict.update({ "contacts2_CustomerId" : element.text }) 82 | elif element.tag == "contacts:Department": 83 | contact_dict.update({ "contacts_Department" : element.text }) 84 | elif element.tag == "contacts:Email1Address": 85 | contact_dict.update({ "contacts_Email1Address" : element.text }) 86 | elif element.tag == "contacts:Email2Address": 87 | contact_dict.update({ "contacts_Email2Address" : element.text }) 88 | elif element.tag == "contacts:Email3Address": 89 | contact_dict.update({ "contacts_Email3Address" : element.text }) 90 | elif element.tag == "contacts:FileAs": 91 | contact_dict.update({ "contacts_FileAs" : element.text }) 92 | elif element.tag == "contacts:FirstName": 93 | contact_dict.update({ "contacts_FirstName" : element.text }) 94 | elif element.tag == "contacts2:GovernmentId": 95 | contact_dict.update({ "contacts2_GovernmentId" : element.text }) 96 | elif element.tag == "contacts:HomeAddressCity": 97 | contact_dict.update({ "contacts_HomeAddressCity" : element.text }) 98 | elif element.tag == "contacts:HomeAddressCountry": 99 | contact_dict.update({ "contacts_HomeAddressCountry" : element.text }) 100 | elif element.tag == "contacts:HomeAddressPostalCode": 101 | contact_dict.update({ "contacts_HomeAddressPostalCode" : element.text }) 102 | elif element.tag == "contacts:HomeAddressState": 103 | contact_dict.update({ "contacts_HomeAddressState" : element.text }) 104 | elif element.tag == "contacts:HomeAddressStreet": 105 | contact_dict.update({ "contacts_HomeAddressStreet" : element.text }) 106 | elif element.tag == "contacts:HomeFaxNumber": 107 | contact_dict.update({ "contacts_HomeFaxNumber" : element.text }) 108 | elif element.tag == "contacts:HomePhoneNumber": 109 | contact_dict.update({ "contacts_HomePhoneNumber" : element.text }) 110 | elif element.tag == "contacts:Home2PhoneNumber": 111 | contact_dict.update({ "contacts_Home2PhoneNumber" : element.text }) 112 | elif element.tag == "contacts2:IMAddress": 113 | contact_dict.update({ "contacts2_IMAddress" : element.text }) 114 | elif element.tag == "contacts2:IMAddress2": 115 | contact_dict.update({ "contacts2_IMAddress2" : element.text }) 116 | elif element.tag == "contacts2:IMAddress3": 117 | contact_dict.update({ "contacts_IMAddress3" : element.text }) 118 | elif element.tag == "contacts:JobTitle": 119 | contact_dict.update({ "contacts_JobTitle" : element.text }) 120 | elif element.tag == "contacts:LastName": 121 | contact_dict.update({ "contacts_LastName" : element.text }) 122 | elif element.tag == "contacts2:ManagerName": 123 | contact_dict.update({ "contacts2_ManagerName" : element.text }) 124 | elif element.tag == "contacts:MiddleName": 125 | contact_dict.update({ "contacts_MiddleName" : element.text }) 126 | elif element.tag == "contacts2:MMS": 127 | contact_dict.update({ "contacts2_MMS" : element.text }) 128 | elif element.tag == "contacts:MobilePhoneNumber": 129 | contact_dict.update({ "contacts_MobilePhoneNumber" : element.text }) 130 | elif element.tag == "contacts2:NickName": 131 | contact_dict.update({ "contacts2_NickName" : element.text }) 132 | elif element.tag == "contacts:OfficeLocation": 133 | contact_dict.update({ "contacts_OfficeLocation" : element.text }) 134 | elif element.tag == "contacts:OtherAddressCity": 135 | contact_dict.update({ "contacts_OtherAddressCity" : element.text }) 136 | elif element.tag == "contacts:OtherAddressCountry": 137 | contact_dict.update({ "contacts_OtherAddressCountry" : element.text }) 138 | elif element.tag == "contacts:OtherAddressPostalCode": 139 | contact_dict.update({ "contacts_OtherAddressPostalCode" : element.text }) 140 | elif element.tag == "contacts:OtherAddressState": 141 | contact_dict.update({ "contacts_OtherAddressState" : element.text }) 142 | elif element.tag == "contacts:OtherAddressStreet": 143 | contact_dict.update({ "contacts_OtherAddressStreet" : element.text }) 144 | elif element.tag == "contacts:PagerNumber": 145 | contact_dict.update({ "contacts_PagerNumber" : element.text }) 146 | elif element.tag == "contacts:Picture": 147 | contact_dict.update({ "contacts_Picture" : element.text }) 148 | elif element.tag == "contacts:RadioPhoneNumber": 149 | contact_dict.update({ "contacts_RadioPhoneNumber" : element.text }) 150 | elif element.tag == "contacts:Spouse": 151 | contact_dict.update({ "contacts_Spouse" : element.text }) 152 | elif element.tag == "contacts:Suffix": 153 | contact_dict.update({ "contacts_Suffix" : element.text }) 154 | elif element.tag == "contacts:Title": 155 | contact_dict.update({ "contacts_Title" : element.text }) 156 | elif element.tag == "contacts:WebPage": 157 | contact_dict.update({ "contacts_WebPage" : element.text }) 158 | elif element.tag == "contacts:WeightedRank": 159 | contact_dict.update({ "contacts_WeightedRank" : element.text }) 160 | elif element.tag == "contacts:YomiCompanyName": 161 | contact_dict.update({ "contacts_YomiCompanyName" : element.text }) 162 | elif element.tag == "contacts:YomiFirstName": 163 | contact_dict.update({ "contacts_YomiFirstName" : element.text }) 164 | elif element.tag == "contacts:YomiLastName": 165 | contact_dict.update({ "contacts_YomiLastName" : element.text }) 166 | return contact_dict -------------------------------------------------------------------------------- /peas/pyActiveSync/client/Sync.py: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Copyright (C) 2013 Sol Birnbaum 3 | # 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either version 2 7 | # of the License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 17 | # MA 02110-1301, USA. 18 | ######################################################################## 19 | 20 | from ..utils.wapxml import wapxmltree, wapxmlnode 21 | 22 | from ..objects.MSASCMD import FolderHierarchy 23 | from ..objects.MSASEMAIL import parse_email 24 | from ..objects.MSASCNTC import parse_contact 25 | from ..objects.MSASCAL import parse_calendar 26 | from ..objects.MSASTASK import parse_task 27 | from ..objects.MSASNOTE import parse_note 28 | 29 | class Sync: 30 | """'Sync' command builders and parsers""" 31 | class sync_response_collection: 32 | def __init__(self): 33 | self.SyncKey = 0 34 | self.CollectionId = None 35 | self.Status = 0 36 | self.MoreAvailable = None 37 | self.Commands = [] 38 | self.Responses = None 39 | 40 | @staticmethod 41 | def build(synckeys, collections): 42 | as_sync_xmldoc_req = wapxmltree() 43 | xml_as_sync_rootnode = wapxmlnode("Sync") 44 | as_sync_xmldoc_req.set_root(xml_as_sync_rootnode, "airsync") 45 | 46 | xml_as_collections_node = wapxmlnode("Collections", xml_as_sync_rootnode) 47 | 48 | for collection_id in collections.keys(): 49 | xml_as_Collection_node = wapxmlnode("Collection", xml_as_collections_node) #http://msdn.microsoft.com/en-us/library/gg650891(v=exchg.80).aspx 50 | try: 51 | xml_as_SyncKey_node = wapxmlnode("SyncKey", xml_as_Collection_node, synckeys[collection_id]) #http://msdn.microsoft.com/en-us/library/gg663426(v=exchg.80).aspx 52 | except KeyError: 53 | xml_as_SyncKey_node = wapxmlnode("SyncKey", xml_as_Collection_node, "0") 54 | 55 | xml_as_CollectionId_node = wapxmlnode("CollectionId", xml_as_Collection_node, collection_id) #http://msdn.microsoft.com/en-us/library/gg650886(v=exchg.80).aspx 56 | 57 | for parameter in collections[collection_id].keys(): 58 | if parameter == "Options": 59 | xml_as_Options_node = wapxmlnode(parameter, xml_as_Collection_node) 60 | for option_parameter in collections[collection_id][parameter].keys(): 61 | if option_parameter.startswith("airsync"): 62 | for airsyncpref_node in collections[collection_id][parameter][option_parameter]: 63 | xml_as_Options_airsyncpref_node = wapxmlnode(option_parameter.replace("_",":"), xml_as_Options_node) 64 | wapxmlnode("airsyncbase:Type", xml_as_Options_airsyncpref_node, airsyncpref_node["Type"]) 65 | tmp = airsyncpref_node["Type"] 66 | del airsyncpref_node["Type"] 67 | for airsyncpref_parameter in airsyncpref_node.keys(): 68 | wapxmlnode("airsyncbase:%s" % airsyncpref_parameter, xml_as_Options_airsyncpref_node, airsyncpref_node[airsyncpref_parameter]) 69 | airsyncpref_node["Type"] = tmp 70 | elif option_parameter.startswith("rm"): 71 | wapxmlnode(option_parameter.replace("_",":"), xml_as_Options_node, collections[collection_id][parameter][option_parameter]) 72 | else: 73 | wapxmlnode(option_parameter, xml_as_Options_node, collections[collection_id][parameter][option_parameter]) 74 | else: 75 | wapxmlnode(parameter, xml_as_Collection_node, collections[collection_id][parameter]) 76 | return as_sync_xmldoc_req 77 | 78 | @staticmethod 79 | def deepsearch_content_class(item): 80 | elements = item.get_children() 81 | for element in elements: 82 | if element.has_children(): 83 | content_class = Sync.deepsearch_content_class(element) 84 | if content_class: 85 | return content_class 86 | if (element.tag == "email:To") or (element.tag == "email:From"): 87 | return "Email" 88 | elif (element.tag == "contacts:FileAs") or (element.tag == "contacts:Email1Address"): 89 | return "Contacts" 90 | elif (element.tag == "calendar:Subject") or (element.tag == "calendar:UID"): 91 | return "Calendar" 92 | elif (element.tag == "tasks:Type"): 93 | return "Tasks" 94 | elif (element.tag == "notes:MessageClass"): 95 | return "Notes" 96 | 97 | @staticmethod 98 | def parse_item(item, collection_id, collectionid_to_type_dict = None): 99 | if collectionid_to_type_dict: 100 | try: 101 | content_class = FolderHierarchy.FolderTypeToClass[collectionid_to_type_dict[collection_id]] 102 | except: 103 | content_class = Sync.deepsearch_content_class(item) 104 | else: 105 | content_class = Sync.deepsearch_content_class(item) 106 | try: 107 | if content_class == "Email": 108 | return parse_email(item), content_class 109 | elif content_class == "Contacts": 110 | return parse_contact(item), content_class 111 | elif content_class == "Calendar": 112 | return parse_calendar(item), content_class 113 | elif content_class == "Tasks": 114 | return parse_task(item), content_class 115 | elif content_class == "Notes": 116 | return parse_note(item), content_class 117 | except Exception, e: 118 | if collectionid_to_type_dict: 119 | return Sync.parse_item(item, collection_id, None) 120 | else: 121 | print e 122 | pass 123 | raise LookupError("Could not determine content class of item for parsing. \r\n------\r\nItem:\r\n%s" % repr(item)) 124 | 125 | @staticmethod 126 | def parse(wapxml, collectionid_to_type_dict = None): 127 | 128 | namespace = "airsync" 129 | root_tag = "Sync" 130 | 131 | root_element = wapxml.get_root() 132 | if root_element.get_xmlns() != namespace: 133 | raise AttributeError("Xmlns '%s' submitted to '%s' parser. Should be '%s'." % (root_element.get_xmlns(), root_tag, namespace)) 134 | if root_element.tag != root_tag: 135 | raise AttributeError("Root tag '%s' submitted to '%s' parser. Should be '%s'." % (root_element.tag, root_tag, root_tag)) 136 | 137 | airsyncbase_sync_children = root_element.get_children() 138 | if len(airsyncbase_sync_children) > 1: 139 | raise AttributeError("%s response does not conform to any known %s responses." % (root_tag, root_tag)) 140 | if airsyncbase_sync_children[0].tag == "Status": 141 | if airsyncbase_sync_children[0].text == "4": 142 | print "Sync Status: 4, Protocol Error." 143 | if airsyncbase_sync_children[0].tag != "Collections": 144 | raise AttributeError("%s response does not conform to any known %s responses." % (root_tag, root_tag)) 145 | 146 | response = [] 147 | 148 | airsyncbase_sync_collections_children = airsyncbase_sync_children[0].get_children() 149 | airsyncbase_sync_collections_children_count = len(airsyncbase_sync_collections_children) 150 | collections_counter = 0 151 | while collections_counter < airsyncbase_sync_collections_children_count: 152 | 153 | if airsyncbase_sync_collections_children[collections_counter].tag != "Collection": 154 | raise AttributeError("Sync response does not conform to any known Sync responses.") 155 | 156 | airsyncbase_sync_collection_children = airsyncbase_sync_collections_children[collections_counter].get_children() 157 | airsyncbase_sync_collection_children_count = len(airsyncbase_sync_collection_children) 158 | collection_counter = 0 159 | new_collection = Sync.sync_response_collection() 160 | while collection_counter < airsyncbase_sync_collection_children_count: 161 | if airsyncbase_sync_collection_children[collection_counter].tag == "SyncKey": 162 | new_collection.SyncKey = airsyncbase_sync_collection_children[collection_counter].text 163 | elif airsyncbase_sync_collection_children[collection_counter].tag == "CollectionId": 164 | new_collection.CollectionId = airsyncbase_sync_collection_children[collection_counter].text 165 | elif airsyncbase_sync_collection_children[collection_counter].tag == "Status": 166 | new_collection.Status = airsyncbase_sync_collection_children[collection_counter].text 167 | if new_collection.Status != "1": 168 | response.append(new_collection) 169 | elif airsyncbase_sync_collection_children[collection_counter].tag == "MoreAvailable": 170 | new_collection.MoreAvailable = True 171 | elif airsyncbase_sync_collection_children[collection_counter].tag == "Commands": 172 | airsyncbase_sync_commands_children = airsyncbase_sync_collection_children[collection_counter].get_children() 173 | airsyncbase_sync_commands_children_count = len(airsyncbase_sync_commands_children) 174 | commands_counter = 0 175 | while commands_counter < airsyncbase_sync_commands_children_count: 176 | if airsyncbase_sync_commands_children[commands_counter].tag == "Add": 177 | add_item = Sync.parse_item(airsyncbase_sync_commands_children[commands_counter], new_collection.CollectionId, collectionid_to_type_dict) 178 | new_collection.Commands.append(("Add", add_item)) 179 | elif airsyncbase_sync_commands_children[commands_counter].tag == "Delete": 180 | new_collection.Commands.append(("Delete", airsyncbase_sync_commands_children[commands_counter].get_children()[0].text)) 181 | elif airsyncbase_sync_commands_children[commands_counter].tag == "Change": 182 | update_item = Sync.parse_item(airsyncbase_sync_commands_children[commands_counter], new_collection.CollectionId, collectionid_to_type_dict) 183 | new_collection.Commands.append(("Change", update_item)) 184 | elif airsyncbase_sync_commands_children[commands_counter].tag == "SoftDelete": 185 | new_collection.Commands.append(("SoftDelete", airsyncbase_sync_commands_children[commands_counter].get_children()[0].text)) 186 | commands_counter+=1 187 | elif airsyncbase_sync_collection_children[collection_counter].tag == "Responses": 188 | print airsyncbase_sync_collection_children[collection_counter] 189 | collection_counter+=1 190 | response.append(new_collection) 191 | collections_counter+=1 192 | return response 193 | -------------------------------------------------------------------------------- /peas/pyActiveSync/client/ItemOperations.py: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Copyright (C) 2013 Sol Birnbaum 3 | # 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either version 2 7 | # of the License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 17 | # MA 02110-1301, USA. 18 | ######################################################################## 19 | 20 | from ..utils.wapxml import wapxmltree, wapxmlnode 21 | 22 | from ..objects.MSASAIRS import airsyncbase_Body, airsyncbase_BodyPart 23 | 24 | class ItemOperations: 25 | """http://msdn.microsoft.com/en-us/library/ee202415(v=exchg.80).aspx""" 26 | 27 | @staticmethod 28 | def build(operations): 29 | itemoperations_xmldoc_req = wapxmltree() 30 | xmlrootnode = wapxmlnode("ItemOperations") 31 | itemoperations_xmldoc_req.set_root(xmlrootnode, "itemoperations") 32 | 33 | for operation in range(0,len(operations)): 34 | if operations[operation]["Name"] == "EmptyFolderContents": 35 | xmlemptyfoldercontentsnode = wapxmlnode("EmptyFolderContents", xmlrootnode) 36 | xmlcollectionidnode = wapxmlnode("airsync:CollectionId", xmlemptyfoldercontentsnode, operations[operation]["CollectionId"]) 37 | if operations[operation].has_key("DeleteSubFolders"): 38 | xmloptionsnode = wapxmlnode("Options", xmlemptyfoldercontentsnode) 39 | xmldeletesubfoldersnode = wapxmlnode("DeleteSubFolders", xmloptionsnode, operations[operation]["DeleteSubFolders"]) 40 | 41 | elif operations[operation]["Name"] == "Fetch": 42 | xmlfetchnode = wapxmlnode("Fetch", xmlrootnode) 43 | xmloptionsnode = wapxmlnode("Store", xmlfetchnode, operations[operation]["Store"]) 44 | if operations[operation].has_key("LinkId"): 45 | xmllinkidnode = wapxmlnode("documentlibrary:LinkId", xmlfetchnode, operations[operation]["LinkId"]) #URI of document 46 | if operations[operation].has_key("LongId"): 47 | xmllongidnode = wapxmlnode("search:LongId", xmlfetchnode, operations[operation]["LongId"]) 48 | if operations[operation].has_key("CollectionId"): 49 | xmlcollectionidnode = wapxmlnode("airsync:CollectionId", xmlfetchnode, operations[operation]["CollectionId"]) 50 | if operations[operation].has_key("ServerId"): 51 | xmlserveridnode = wapxmlnode("airsync:ServerId", xmlfetchnode, operations[operation]["ServerId"]) 52 | if operations[operation].has_key("FileReference"): 53 | xmlfilereferencenode = wapxmlnode("airsyncbase:FileReference", xmlfetchnode, operations[operation]["FileReference"]) #only range option can be specified 54 | if operations[operation].has_key("RemoveRightsManagementProtection"): 55 | xmlremovermnode = wapxmlnode("rm:RemoveRightsManagementProtection", xmlfetchnode) #Empty element 56 | if len(xmlfetchnode.get_children()) < 2: #let's make sure one of the above item locations was supplied 57 | raise AttributeError("ItemOperations Fetch: No item to be fetched supplied.") 58 | 59 | xmloptionsnode = wapxmlnode("Options") 60 | if operations[operation].has_key("Schema"): 61 | xmlschemanode = wapxmlnode("Schema", xmloptionsnode, operations[operation]["Schema"]) #fetch only specific properties of an item. mailbox store only. cannot use for attachments. 62 | if operations[operation].has_key("Range"): 63 | xmlrangenode = wapxmlnode("Range", xmloptionsnode, operations[operation]["Range"]) #select bytes is only for documents and attachments 64 | if operations[operation].has_key("UserName"): #select username and password to use for fetch. i imagine this is only for documents. 65 | if not operations[operation].has_key("Password"): 66 | raise AttributeError("ItemOperations Fetch: Username supplied for fetch operation, but no password supplied. Aborting.") 67 | return 68 | xmlusernamenode = wapxmlnode("UserName", xmloptionsnode, operations[operation]["UserName"]) #username to use for fetch 69 | xmlpasswordnode = wapxmlnode("Password", xmloptionsnode, operations[operation]["Password"]) #password to use for fetch 70 | if operations[operation].has_key("MIMESupport"): 71 | xmlmimesupportnode = wapxmlnode("airsync:MIMESupport", xmloptionsnode, operations[operation]["MIMESupport"]) #objects.MSASAIRS.airsync_MIMESupport 72 | if operations[operation].has_key("BodyPreference"): 73 | xmlbodypreferencenode = wapxmlnode("airsyncbase:BodyPreference", xmloptionsnode, operations[operation]["BodyPreference"]) 74 | if operations[operation].has_key("BodyPartPreference"): 75 | xmlbodypartpreferencenode = wapxmlnode("airsyncbase:BodyPartPreference", xmloptionsnode, operations[operation]["BodyPartPreference"]) 76 | if operations[operation].has_key("RightsManagementSupport"): 77 | xmlrmsupportnode = wapxmlnode("rm:RightsManagementSupport", xmloptionsnode, operations[operation]["RightsManagementSupport"])#1=Supports RM. Decrypt message before send. 2=Do not decrypt message before send 78 | if len(xmloptionsnode.get_children()) > 0: 79 | xmloptionsnode.set_parent(xmlfetchnode) 80 | 81 | elif operations[operation]["Name"] == "Move": 82 | xmlmovenode = wapxmlnode("Move", xmlrootnode) 83 | xmlconversationidnode = wapxmlnode("ConversationId", xmlmovenode, operations[operation]["ConversationId"]) 84 | xmldstfldidnode = wapxmlnode("DstFldId", xmlmovenode, operations[operation]["DstFldId"]) 85 | if operations[operation].has_key("MoveAlways"): 86 | xmloptionsnode = wapxmlnode("Options", xmlmovenode) 87 | xmlmovealwaysnode = wapxmlnode("MoveAlways", xmloptionsnode, operations[operation]["MoveAlways"]) #also move future emails in this conversation to selected folder. 88 | else: 89 | raise AttributeError("Unknown operation %s submitted to ItemOperations wapxml builder." % operation) 90 | 91 | return itemoperations_xmldoc_req 92 | 93 | @staticmethod 94 | def parse(wapxml): 95 | 96 | namespace = "itemoperations" 97 | root_tag = "ItemOperations" 98 | 99 | root_element = wapxml.get_root() 100 | if root_element.get_xmlns() != namespace: 101 | raise AttributeError("Xmlns '%s' submitted to '%s' parser. Should be '%s'." % (root_element.get_xmlns(), root_tag, namespace)) 102 | if root_element.tag != root_tag: 103 | raise AttributeError("Root tag '%s' submitted to '%s' parser. Should be '%s'." % (root_element.tag, root_tag, root_tag)) 104 | 105 | itemoperations_itemoperations_children = root_element.get_children() 106 | 107 | itemoperations_itemoperations_status = None 108 | 109 | responses = [] 110 | 111 | for element in itemoperations_itemoperations_children: 112 | if element.tag is "Status": 113 | itemoperations_itemoperations_status = element.text 114 | if itemoperations_itemoperations_status != "1": 115 | print "FolderSync Exception: %s" % itemoperations_itemoperations_status 116 | elif element.tag == "Response": 117 | response_elements = element.get_children() 118 | for response_element in response_elements: 119 | if response_element.tag == "EmptyFolderContents": 120 | efc_elements = response_element.get_children() 121 | for efc_element in efc_elements: 122 | if efc_element.tag == "Status": 123 | efc_status = efc_element.text 124 | elif efc_element.tag == "airsync:CollectionId": 125 | efc_collectionid = efc_element.text 126 | responses.append(("EmptyFolderContents", efc_status, efc_collectionid)) 127 | elif response_element.tag == "Fetch": 128 | fetch_elements = response_element.get_children() 129 | fetch_id = None 130 | fetch_properties = None 131 | fetch_class = None 132 | for fetch_element in fetch_elements: 133 | if fetch_element.tag == "Status": 134 | fetch_status = fetch_element.text 135 | elif fetch_element.tag == "search:LongId": 136 | fetch_id = fetch_element.text 137 | elif fetch_element.tag == "airsync:CollectionId": 138 | fetch_id = fetch_element.text 139 | elif fetch_element.tag == "airsync:ServerId": 140 | fetch_id = fetch_element.text 141 | elif fetch_element.tag == "documentlibrary:LinkId": 142 | fetch_id = fetch_element.text 143 | elif fetch_element.tag == "airsync:Class": 144 | fetch_class = fetch_element.text 145 | elif fetch_element.tag == "Properties": 146 | property_elements = fetch_element.get_children() 147 | fetch_properties = {} 148 | for property_element in property_elements: 149 | if property_element.tag == "Range": 150 | fetch_properties.update({ "Range" : property_element.text }) 151 | elif property_element.tag == "Data": 152 | fetch_properties.update({ "Data" : property_element.text }) 153 | elif property_element.tag == "Part": 154 | fetch_properties.update({ "Part" : property_element.text }) 155 | elif property_element.tag == "Version": #datetime 156 | fetch_properties.update({ "Version" : property_element.text }) 157 | elif property_element.tag == "Total": 158 | fetch_properties.update({ "Total" : property_element.text }) 159 | elif property_element.tag == "airsyncbase:Body": 160 | fetch_properties.update({ "Body" : airsyncbase_Body(property_element) }) 161 | elif property_element.tag == "airsyncbase:BodyPart": 162 | fetch_properties.update({ "BodyPart" : airsyncbase_BodyPart(property_element) }) 163 | elif property_element.tag == "rm:RightsManagementLicense": 164 | fetch_properties.update({ "RightsManagementLicense" : property_element }) #need to create rm license parser 165 | responses.append(("Fetch", fetch_status, fetch_id, fetch_properties, fetch_class)) 166 | elif response_element.tag == "Move": 167 | move_elements = response_element.get_children() 168 | for move_element in move_elements: 169 | if move_element.tag == "Status": 170 | move_status = move_element.text 171 | elif move_element.tag == "ConversationId": 172 | move_conversationid = move_element.text 173 | responses.append(("Move", move_status, move_conversationid)) 174 | return responses -------------------------------------------------------------------------------- /peas/pyActiveSync/objects/MSASAIRS.py: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Copyright (C) 2013 Sol Birnbaum 3 | # 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either version 2 7 | # of the License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 17 | # MA 02110-1301, USA. 18 | ######################################################################## 19 | 20 | """[MS-ASAIRS] AirSyncBase namespace objects""" 21 | 22 | class airsyncbase_Type: #http://msdn.microsoft.com/en-us/library/hh475675(v=exchg.80).aspx 23 | Plaintext = 1 24 | HTML = 2 25 | RTF = 3 26 | MIME = 4 27 | 28 | class airsyncbase_NativeBodyType: #http://msdn.microsoft.com/en-us/library/ee218276(v=exchg.80).aspx 29 | Plaintext = 1 30 | HTML = 2 31 | RTF = 3 32 | 33 | class airsyncbase_Method: #http://msdn.microsoft.com/en-us/library/ee160322(v=exchg.80).aspx 34 | Normal_attachment = 1 #Regular attachment 35 | Reserved1 = 2 36 | Reserved2 = 3 37 | Reserved3 = 4 38 | Embedded_message = 5 #Email with .eml extension 39 | Attach_OLE = 6 #OLE such as inline image 40 | 41 | class airsyncbase_BodyPart_status: 42 | Success = 1 43 | Too_long = 176 44 | 45 | class airsync_MIMESupport: 46 | Never = 0 47 | SMIMEOnly = 1 48 | Always = 2 49 | 50 | class airsync_Class: 51 | Email = "Email" 52 | Contacts = "Contacts" 53 | Calendar = "Calendar" 54 | Tasks = "Tasks" 55 | Notes = "Notes" 56 | SMS = "SMS" 57 | 58 | class airsync_FilterType: # Email | Calendar | Tasks 59 | NoFilter = "0" # Y | Y | Y 60 | OneDay = "1" # Y | N | N 61 | ThreeDays = "2" # Y | N | N 62 | OneWeek = "3" # Y | N | N 63 | TwoWeeks = "4" # Y | Y | N 64 | OneMonth = "5" # Y | Y | N 65 | ThreeMonths = "6" # N | Y | N 66 | SixMonths = "7" # N | Y | N 67 | IncompleteTasks = "8" # N | N | Y 68 | 69 | class airsync_Conflict: 70 | ClientReplacesServer = 0 71 | ServerReplacesClient = 1 72 | 73 | class airsync_MIMETruncation: 74 | TruncateAll = 0 75 | Over4096chars = 1 76 | Over5120chars = 2 77 | Over7168chars = 3 78 | Over10240chars = 4 79 | Over20480chars = 5 80 | Over51200chars = 6 81 | Over102400chars = 7 82 | TruncateNone = 8 83 | 84 | class airsyncbase_Body(object): 85 | def __init__(self):#, type, estimated_data_size=None, truncated=None, data=None, part=None, preview=None): 86 | self.airsyncbase_Type = None #type #Required. Integer. Max 1. See "MSASAIRS.Type" enum. 87 | self.airsyncbase_EstimatedDataSize = None #estimated_data_size #Optional. Integer. Max 1. Estimated data size before content filtering rules. http://msdn.microsoft.com/en-us/library/hh475714(v=exchg.80).aspx 88 | self.airsyncbase_Truncated = None #truncated #Optional. Boolean. Max 1. Specifies whether body is truncated as per airsync:BodyPreference element. http://msdn.microsoft.com/en-us/library/ee219390(v=exchg.80).aspx 89 | self.airsyncbase_Data = None #data #Optional. String (formated as per Type; RTF is base64 string). http://msdn.microsoft.com/en-us/library/ee202985(v=exchg.80).aspx 90 | self.airsyncbase_Part = None #part #Optional. Integer. See "MSASCMD.Part". Only present in multipart "MSASCMD.ItemsOperations" response. http://msdn.microsoft.com/en-us/library/hh369854(v=exchg.80).aspx 91 | self.airsyncbase_Preview = None #preview #Optional. String (unicode). Plaintext preview message. http://msdn.microsoft.com/en-us/library/ff849891(v=exchg.80).aspx 92 | def parse(self, imwapxml_airsyncbase_Body): 93 | body_elements = imwapxml_airsyncbase_Body.get_children() 94 | for element in body_elements: 95 | if element.tag == "airsyncbase:Type": 96 | self.airsyncbase_Type = element.text 97 | elif element.tag == "airsyncbase:EstimatedDataSize": 98 | self.airsyncbase_EstimatedDataSize = element.text 99 | elif element.tag == "airsyncbase:Truncated": 100 | self.airsyncbase_Truncated = element.text 101 | elif element.tag == "airsyncbase:Data": 102 | self.airsyncbase_Data = element.text 103 | elif element.tag == "airsyncbase:Part": 104 | self.airsyncbase_Part = element.text 105 | elif element.tag == "airsyncbase:Preview": 106 | self.airsyncbase_Preview = element.text 107 | def marshal(self): 108 | import base64 109 | return "%s//%s//%s//%s//%s//%s" % (repr(self.airsyncbase_Type), repr(self.airsyncbase_EstimatedDataSize), repr(self.airsyncbase_Truncated), base64.b64encode(self.airsyncbase_Data), repr(self.airsyncbase_Part), repr(self.airsyncbase_Preview)) 110 | def __repr__(self): 111 | return self.marshal() 112 | 113 | class airsyncbase_BodyPart(object): 114 | def __init__(self): 115 | self.airsyncbase_BodyPart_status = airsyncbase_BodyPart_status.Too_long #Required. Byte. See airsyncbase_BodyPart_status enum. 116 | self.airsyncbase_Type = airsyncbase_Type.HTML #Required. Integer. Max 1. See "MSASAIRS.Type" enum. 117 | self.airsyncbase_EstimatedDataSize = estimated_data_size #Optional. Integer. Max 1. Estimated data size before content filtering rules. http://msdn.microsoft.com/en-us/library/hh475714(v=exchg.80).aspx 118 | self.airsyncbase_Truncated = truncated #Optional. Boolean. Max 1. Specifies whether body is truncated as per airsync:BodyPreference element. http://msdn.microsoft.com/en-us/library/ee219390(v=exchg.80).aspx 119 | self.airsyncbase_Data = data #Optional. String (formated as per Type; RTF is base64 string). http://msdn.microsoft.com/en-us/library/ee202985(v=exchg.80).aspx 120 | self.airsyncbase_Part = part #Optional. Integer. See "MSASCMD.Part". Only present in multipart "MSASCMD.ItemsOperations" response. http://msdn.microsoft.com/en-us/library/hh369854(v=exchg.80).aspx 121 | self.airsyncbase_Preview = preview #Optional. String (unicode). Plaintext preview message. http://msdn.microsoft.com/en-us/library/ff849891(v=exchg.80).aspx 122 | def parse(self, imwapxml_airsyncbase_BodyPart): 123 | bodypart_elements = imwapxml_airsyncbase_BodyPart.get_children() 124 | for element in bodypart_elements: 125 | if element.tag == "airsyncbase:Type": 126 | self.airsyncbase_Type = element.text 127 | elif element.tag == "airsyncbase:EstimatedDataSize": 128 | self.airsyncbase_EstimatedDataSize = element.text 129 | elif element.tag == "airsyncbase:Truncated": 130 | self.airsyncbase_Truncated = element.text 131 | elif element.tag == "airsyncbase:Data": 132 | self.airsyncbase_Data = element.text 133 | elif element.tag == "airsyncbase:Part": 134 | self.airsyncbase_Part = element.text 135 | elif element.tag == "airsyncbase:Preview": 136 | self.airsyncbase_Preview = element.text 137 | elif element.tag == "airsyncbase:Status": 138 | self.airsyncbase_BodyPart_status = element.text 139 | 140 | class airsyncbase_Attachment(object): #Repsonse-only object. 141 | def __init__(self):#, file_reference, method, estimated_data_size, display_name=None, content_id=None, content_location = None, is_inline = None, email2_UmAttDuration=None, email2_UmAttOrder=None): 142 | self.airsyncbase_DisplayName = None #display_name #Optional. String. http://msdn.microsoft.com/en-us/library/ee160854(v=exchg.80).aspx 143 | self.airsyncbase_FileReference = None #file_reference #Required. String. Location of attachment on server. http://msdn.microsoft.com/en-us/library/ff850023(v=exchg.80).aspx 144 | self.airsyncbase_Method = None #method #Required. Byte. See "MSASAIRS.Method". Type of attachment. http://msdn.microsoft.com/en-us/library/ee160322(v=exchg.80).aspx 145 | self.airsyncbase_EstimatedDataSize = None #estimated_data_size #Required. Integer. Max 1. Estimated data size before content filtering rules. http://msdn.microsoft.com/en-us/library/hh475714(v=exchg.80).aspx 146 | self.airsyncbase_ContentId = None #content_id #Optional. String. Max 1. Unique object id of attachment - informational only. 147 | self.airsyncbase_ContentLocation = None #content_location #Optional. String. Max 1. Contains the relative URI for an attachment, and is used to match a reference to an inline attachment in an HTML message to the attachment in the attachments table. http://msdn.microsoft.com/en-us/library/ee204563(v=exchg.80).aspx 148 | self.airsyncbase_IsInline = None #is_inline #Optional. Boolean. Max 1. Specifies whether the attachment is embedded in the message. http://msdn.microsoft.com/en-us/library/ee237093(v=exchg.80).aspx 149 | self.email2_UmAttDuration = None #email2_UmAttDuration #Optional. Integer. Duration of the most recent electronic voice mail attachment in seconds. Only used in "IPM.Note.Microsoft.Voicemail", "IPM.Note.RPMSG.Microsoft.Voicemail", or "IPM.Note.Microsoft.Missed.Voice". 150 | self.email2_UmAttOrder = None #email2_UmAttOrder #Optional. Integer. Order of electronic voice mail attachments. Only used in "IPM.Note.Microsoft.Voicemail", "IPM.Note.RPMSG.Microsoft.Voicemail", or "IPM.Note.Microsoft.Missed.Voice". 151 | def parse(self, imwapxml_airsyncbase_Attachment): 152 | attachment_elements = imwapxml_airsyncbase_Attachment.get_children() 153 | for element in attachment_elements: 154 | if element.tag == "airsyncbase:DisplayName": 155 | self.airsyncbase_DisplayName = element.text 156 | elif element.tag == "airsyncbase:FileReference": 157 | self.airsyncbase_FileReference = element.text 158 | elif element.tag == "airsyncbase:Method": 159 | self.airsyncbase_Method = element.text 160 | elif element.tag == "airsyncbase:EstimatedDataSize": 161 | self.airsyncbase_EstimatedDataSize = element.text 162 | elif element.tag == "airsyncbase:ContentId": 163 | self.airsyncbase_ContentId = element.text 164 | elif element.tag == "airsyncbase:ContentLocation": 165 | self.airsyncbase_ContentLocation = element.text 166 | elif element.tag == "airsyncbase:IsInline": 167 | self.airsyncbase_IsInline = element.text 168 | elif element.tag == "email2:UmAttDuration": 169 | self.email2_UmAttDuration = element.text 170 | elif element.tag == "email2:UmAttOrder": 171 | self.email2_UmAttOrder = element.text 172 | def marshal(self): 173 | import base64 174 | return base64.b64encode("%s//%s//%s//%s//%s//%s//%s//%s//%s" % (repr(self.airsyncbase_DisplayName), repr(self.airsyncbase_FileReference), repr(self.airsyncbase_Method), repr(self.airsyncbase_EstimatedDataSize), repr(self.airsyncbase_ContentId),repr(self.airsyncbase_ContentLocation), repr(self.airsyncbase_IsInline), repr(self.email2_UmAttDuration),repr(self.email2_UmAttOrder))) 175 | def __repr__(self): 176 | return self.marshal() 177 | 178 | class airsyncbase_Attachments: 179 | @staticmethod 180 | def parse(inwapxml_airsyncbase_Attachments): 181 | attachment_elements = inwapxml_airsyncbase_Attachments.get_children() 182 | attachments = [] 183 | for attachment in attachment_elements: 184 | new_attachment = airsyncbase_Attachment() 185 | new_attachment.parse(attachment) 186 | attachments.append(new_attachment) 187 | return attachments 188 | 189 | 190 | 191 | 192 | 193 | -------------------------------------------------------------------------------- /peas/pyActiveSync/utils/wbxml.py: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Copyright (C) 2013 Sol Birnbaum 3 | # 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either version 2 7 | # of the License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 17 | # MA 02110-1301, USA. 18 | ######################################################################## 19 | 20 | 21 | from wapxml import wapxmltree, wapxmlnode 22 | 23 | class wbxml_parser(object): 24 | """WBXML Parser""" 25 | 26 | VERSION_BYTE = 0x03 27 | PUBLIC_IDENTIFIER_BYTE = 0x01 28 | CHARSET_BYTE = 0x6A #Currently, only UTF-8 is used by MS-ASWBXML 29 | STRING_TABLE_LENGTH_BYTE = 0x00 #String tables are not used by MS-ASWBXML 30 | 31 | class GlobalTokens: 32 | SWITCH_PAGE = 0x00 33 | END = 0x01 34 | ENTITY = 0x02 #Not used by MS-ASWBXML 35 | STR_I = 0x03 36 | LITERAL = 0x04 37 | EXT_I_0 = 0x40 #Not used by MS-ASWBXML 38 | EXT_I_1 = 0x41 #Not used by MS-ASWBXML 39 | EXT_I_2 = 0x42 #Not used by MS-ASWBXML 40 | PI = 0x43 #Not used by MS-ASWBXML 41 | LITERAL_C = 0x44 #Not used by MS-ASWBXML 42 | EXT_T_0 = 0x80 #Not used by MS-ASWBXML 43 | EXT_T_1 = 0x81 #Not used by MS-ASWBXML 44 | EXT_T_2 = 0x82 #Not used by MS-ASWBXML 45 | STR_T = 0x83 #Not used by MS-ASWBXML 46 | LITERAL_A = 0x84 #Not used by MS-ASWBXML 47 | EXT_0 = 0xC0 #Not used by MS-ASWBXML 48 | EXT_1 = 0xC1 #Not used by MS-ASWBXML 49 | EXT_2 = 0xC2 #Not used by MS-ASWBXML 50 | OPAQUE = 0xC3 51 | LITERAL_AC = 0xC4 #Not used by MS-ASWBXML 52 | 53 | def __init__(self, code_pages, cp_shorthand={}): 54 | self.wapxml = None 55 | self.wbxml = None 56 | self.pointer = 0 57 | self.code_pages = code_pages 58 | self.cp_shorthand = cp_shorthand 59 | return 60 | 61 | def encode(self, inwapxml=None): 62 | wbxml_bytes = bytearray() 63 | 64 | if not inwapxml: return wbxml_bytes 65 | 66 | #add headers 67 | wbxml_bytes.append(self.VERSION_BYTE) 68 | wbxml_bytes.extend(self.encode_multibyte_integer(self.PUBLIC_IDENTIFIER_BYTE)) 69 | wbxml_bytes.extend(self.encode_multibyte_integer(self.CHARSET_BYTE)) 70 | wbxml_bytes.extend(self.encode_multibyte_integer(self.STRING_TABLE_LENGTH_BYTE)) 71 | 72 | #add code_page/xmlns 73 | wbxml_bytes.append(self.GlobalTokens.SWITCH_PAGE) 74 | current_code_page_index = self.encode_xmlns_as_codepage(inwapxml.get_root().get_xmlns()) 75 | wbxml_bytes.append(current_code_page_index) 76 | self.current_code_page = self.code_pages[current_code_page_index] 77 | self.default_code_page = self.code_pages[current_code_page_index] 78 | 79 | #add root token/tag 80 | token_tag = self.current_code_page.get_token(inwapxml.get_root().tag) 81 | if inwapxml.get_root().has_children(): 82 | token_tag |= 0x40 83 | wbxml_bytes.append(token_tag) 84 | 85 | current_node = inwapxml.get_root() 86 | 87 | if current_node.has_children(): 88 | for child in current_node.get_children(): 89 | self.encode_node_recursive(child, wbxml_bytes) 90 | wbxml_bytes.append(self.GlobalTokens.END) 91 | return wbxml_bytes 92 | 93 | def encode_node_recursive(self, current_node, wbxml_bytes): 94 | if ":" in current_node.tag: 95 | split_xmlns_tag = current_node.tag.split(":") 96 | possibly_new_code_page = self.code_pages[self.encode_xmlns_as_codepage(split_xmlns_tag[0])] 97 | if possibly_new_code_page.index != self.current_code_page.index: 98 | wbxml_bytes.append(self.GlobalTokens.SWITCH_PAGE) 99 | wbxml_bytes.append(possibly_new_code_page.index) 100 | self.current_code_page = possibly_new_code_page 101 | token_tag = self.current_code_page.get_token(split_xmlns_tag[1]) 102 | else: 103 | if self.current_code_page.index != self.default_code_page.index: 104 | wbxml_bytes.append(self.GlobalTokens.SWITCH_PAGE) 105 | wbxml_bytes.append(self.default_code_page.index) 106 | self.current_code_page = self.code_pages[self.default_code_page.index] 107 | token_tag = self.current_code_page.get_token(current_node.tag) 108 | token_tag |= 0x40 109 | wbxml_bytes.append(token_tag) 110 | #text, cdata = None, None 111 | if current_node.text: 112 | wbxml_bytes.append(self.GlobalTokens.STR_I) 113 | wbxml_bytes.extend(self.encode_string(current_node.text)) 114 | elif current_node.cdata: 115 | wbxml_bytes.append(self.GlobalTokens.OPAQUE) 116 | if current_node.tag == "Mime": 117 | wbxml_bytes.extend(self.encode_string_as_opaquedata(current_node.cdata.as_string())) 118 | else: #ConversationMode or ConversationId 119 | wbxml_bytes.extend(self.encode_hexstring_as_opaquedata(current_node.cdata)) 120 | if current_node.has_children(): 121 | for child in current_node.get_children(): 122 | self.encode_node_recursive(child, wbxml_bytes) #will have to use class var for current_code_page since stack is being replaced on recursive iter 123 | wbxml_bytes.append(self.GlobalTokens.END) 124 | 125 | def decode(self, inwbxml=None): 126 | if inwbxml: 127 | self.wbxml = bytearray() 128 | self.wbxml.extend(inwbxml) 129 | elif not self.wbxml: 130 | raise AttributeError("Cannot decode if no wbxml. wbxml must be passed to decode as wbxml_parser.decode(inwbxml), or bytearray() must be set directly at wbxml_parser.wbxml.") 131 | self.pointer = 0 132 | ver = self.decode_byte() 133 | public_id = self.decode_multibyte_integer() 134 | charset = self.decode_multibyte_integer() 135 | string_table_len = self.decode_multibyte_integer() 136 | 137 | if charset is not 0x6A: 138 | raise AttributeError("Currently, only UTF-8 is used by MS-ASWBXML") 139 | return 140 | if string_table_len > 0: 141 | raise AttributeError("String tables are not used by MS-ASWBXML") 142 | return 143 | 144 | wapxmldoc = wapxmltree() 145 | current_element = None 146 | first_iter = True 147 | 148 | byte = self.decode_byte() 149 | if byte is not self.GlobalTokens.SWITCH_PAGE: 150 | if self.default_code_page: 151 | default_code_page = self.default_code_page 152 | self.pointer-=1 153 | else: 154 | raise AttributeError("No first or default code page defined.") 155 | else: 156 | default_code_page = self.code_pages[self.decode_byte()] 157 | root_element = wapxmlnode("?") 158 | current_code_page = default_code_page 159 | root_element.set_xmlns(current_code_page.xmlns) 160 | wapxmldoc.set_root(root_element, root_element.get_xmlns()) 161 | current_element = root_element 162 | 163 | temp_xmlns = "" 164 | 165 | 166 | while self.pointer < len(inwbxml): 167 | byte = self.decode_byte() 168 | if byte is self.GlobalTokens.SWITCH_PAGE: 169 | current_code_page = self.code_pages[self.decode_byte()] 170 | if current_code_page != default_code_page: 171 | temp_xmlns = current_code_page.xmlns + ":" 172 | else: 173 | temp_xmlns = "" 174 | elif byte is self.GlobalTokens.END: 175 | if not current_element.is_root(): 176 | current_element = current_element.get_parent() 177 | else: 178 | if self.pointer < len(self.wbxml): 179 | raise EOFError("END token incorrectly placed after root node.") 180 | else: 181 | return wapxmldoc 182 | elif byte is self.GlobalTokens.STR_I: 183 | current_element.text = self.decode_string() 184 | elif byte is self.GlobalTokens.OPAQUE: 185 | opq_len = self.decode_byte() 186 | opq_str = "" 187 | if current_element.tag == "Mime": 188 | opq_str = self.decode_string(opq_len) 189 | else: 190 | import binascii 191 | opq_str = binascii.hexlify(self.decode_binary(opq_len)) 192 | current_element.text = opq_str 193 | else: 194 | if byte & 0x80 > 0: 195 | raise AttributeError("Token has attributes. MS-ASWBXML does not use attributes.") 196 | token = byte & 0x3f 197 | tag_token = temp_xmlns + current_code_page.get_tag(token) 198 | if not first_iter: 199 | new_element = wapxmlnode(tag_token, current_element) 200 | if (byte & 0x40): #check to see if new element has children 201 | current_element = new_element 202 | elif current_element.is_root(): 203 | current_element.tag = tag_token 204 | first_iter = False 205 | else: 206 | raise IndexError("Missing root element.") 207 | return wapxmldoc 208 | 209 | 210 | # encode helper functions 211 | def encode_xmlns_as_codepage(self, inxmlns_or_namespace): 212 | lc_inxmlns = inxmlns_or_namespace.lower() 213 | for cp_index, code_page in self.code_pages.items(): 214 | if code_page.xmlns == lc_inxmlns: 215 | return cp_index 216 | if inxmlns_or_namespace in self.cp_shorthand.keys(): 217 | lc_inxmlns = self.cp_shorthand[inxmlns_or_namespace].lower() 218 | for cp_index, code_page in self.code_pages.items(): 219 | if code_page.xmlns == lc_inxmlns: 220 | return cp_index 221 | raise IndexError("No such code page exists in current object") 222 | 223 | def encode_string(self, string): 224 | string = str(string) 225 | retarray = bytearray(string, "utf-8") 226 | retarray.append("\x00") 227 | return retarray 228 | 229 | def encode_string_as_opaquedata(self, string): 230 | retarray = bytearray() 231 | retarray.extend(self.encode_multibyte_integer(len(string))) 232 | retarray.extend(bytearray(string, "utf-8")) 233 | return retarray 234 | 235 | def encode_hexstring_as_opaquedata(self, hexstring): 236 | retarray = bytearray() 237 | retarray.extend(self.encode_multibyte_integer(len(hexstring))) 238 | retarray.extend(hexstring) 239 | return retarray 240 | 241 | def encode_multibyte_integer(self, integer): 242 | retarray = bytearray() 243 | if integer == 0: 244 | retarray.append(integer) 245 | return retarray 246 | last = True 247 | while integer > 0: 248 | if last: 249 | retarray.append( integer & 0x7f ) 250 | last = False 251 | else: 252 | retarray.append( ( integer & 0x7f ) | 0x80 ) 253 | integer = integer >> 7 254 | retarray.reverse() 255 | return retarray 256 | 257 | # decode helper functions 258 | def decode_codepages_as_xmlns(self): 259 | return 260 | 261 | def decode_string(self, length=None): 262 | retarray = bytearray() 263 | if length is None: 264 | #terminator = b"\x00" 265 | while self.wbxml[self.pointer] != 0:#terminator: 266 | retarray.append(self.wbxml[self.pointer]) 267 | self.pointer += 1 268 | self.pointer+=1 269 | else: 270 | for i in range(0, length): 271 | retarray.append(self.wbxml[self.pointer]) 272 | self.pointer+=1 273 | return str(retarray) 274 | 275 | def decode_byte(self): 276 | self.pointer+=1 277 | return self.wbxml[self.pointer-1] 278 | 279 | def decode_multibyte_integer(self): 280 | #print "indices: ", self.pointer, "of", len(self.wbxml) 281 | if self.pointer >= len(self.wbxml): 282 | raise IndexError("wbxml is truncated. nothing left to decode") 283 | integer = 0 284 | while ( self.wbxml[self.pointer] & 0x80 ) != 0: 285 | integer = integer << 7 286 | integer = integer + ( self.wbxml[self.pointer] & 0x7f ) 287 | self.pointer += 1 288 | integer = integer << 7 289 | integer = integer + ( self.wbxml[self.pointer] & 0x7f ) 290 | self.pointer += 1 291 | return integer 292 | 293 | def decode_binary(self, length=0): 294 | retarray = bytearray() 295 | for i in range(0, length): 296 | retarray.append(self.wbxml[self.pointer]) 297 | self.pointer+=1 298 | return retarray 299 | -------------------------------------------------------------------------------- /peas/pyActiveSync/client/Provision.py: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Copyright (C) 2013 Sol Birnbaum 3 | # 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either version 2 7 | # of the License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 17 | # MA 02110-1301, USA. 18 | ######################################################################## 19 | 20 | from ..utils.wapxml import wapxmltree, wapxmlnode 21 | 22 | class Provision: 23 | """http://msdn.microsoft.com/en-us/library/ff850179(v=exchg.80).aspx""" 24 | 25 | @staticmethod 26 | def build(policykey, settings=None): 27 | provision_xmldoc_req = wapxmltree() 28 | xmlrootnode = wapxmlnode("Provision") 29 | provision_xmldoc_req.set_root(xmlrootnode, "provision") 30 | 31 | if policykey == "0": 32 | xml_settings_deviceinformation_node = wapxmlnode("settings:DeviceInformation", xmlrootnode) 33 | xml_settings_set_node = wapxmlnode("settings:Set", xml_settings_deviceinformation_node) 34 | xml_settings_model_node = wapxmlnode("settings:Model", xml_settings_set_node, settings["Model"]) 35 | xml_settings_model_node = wapxmlnode("settings:IMEI", xml_settings_set_node, settings["IMEI"]) 36 | xml_settings_model_node = wapxmlnode("settings:FriendlyName", xml_settings_set_node, settings["FriendlyName"]) 37 | xml_settings_model_node = wapxmlnode("settings:OS", xml_settings_set_node, settings["OS"]) 38 | xml_settings_model_node = wapxmlnode("settings:OSLanguage", xml_settings_set_node, settings["OSLanguage"]) 39 | xml_settings_model_node = wapxmlnode("settings:PhoneNumber", xml_settings_set_node, settings["PhoneNumber"]) 40 | xml_settings_model_node = wapxmlnode("settings:MobileOperator", xml_settings_set_node, settings["MobileOperator"]) 41 | xml_settings_model_node = wapxmlnode("settings:UserAgent", xml_settings_set_node, settings["UserAgent"]) 42 | 43 | xml_policies_node = wapxmlnode("Policies", xmlrootnode) 44 | xml_policy_node = wapxmlnode("Policy", xml_policies_node) 45 | xml_policytype_node = wapxmlnode("PolicyType", xml_policy_node, "MS-EAS-Provisioning-WBXML") 46 | else: 47 | xml_policies_node = wapxmlnode("Policies", xmlrootnode) 48 | xml_policy_node = wapxmlnode("Policy", xml_policies_node) 49 | xml_policytype_node = wapxmlnode("PolicyType", xml_policy_node, "MS-EAS-Provisioning-WBXML") 50 | xml_policytype_node = wapxmlnode("PolicyKey", xml_policy_node, policykey) 51 | xml_policytype_node = wapxmlnode("Status", xml_policy_node, "1") 52 | 53 | return provision_xmldoc_req 54 | 55 | @staticmethod 56 | def parse(wapxml): 57 | 58 | namespace = "provision" 59 | root_tag = "Provision" 60 | 61 | root_element = wapxml.get_root() 62 | if root_element.get_xmlns() != namespace: 63 | raise AttributeError("Xmlns '%s' submitted to '%s' parser. Should be '%s'." % (root_element.get_xmlns(), root_tag, namespace)) 64 | if root_element.tag != root_tag: 65 | raise AttributeError("Root tag '%s' submitted to '%s' parser. Should be '%s'." % (root_element.tag, root_tag, root_tag)) 66 | 67 | provison_provison_children = root_element.get_children() 68 | 69 | policy_dict = {} 70 | settings_status = "" 71 | policy_status = "" 72 | policy_key = "0" 73 | policy_type = "" 74 | status = "" 75 | 76 | for element in provison_provison_children: 77 | if element.tag is "Status": 78 | status = element.text 79 | if (status != "1") and (status != "2"): 80 | print "Provision Exception: %s" % status 81 | elif element.tag == "Policies": 82 | policy_elements = element.get_children()[0].get_children() 83 | for policy_element in policy_elements: 84 | if policy_element.tag == "PolicyType": 85 | policy_type = policy_element.text 86 | elif policy_element.tag == "Status": 87 | policy_status = policy_element.text 88 | elif policy_element.tag == "PolicyKey": 89 | policy_key = policy_element.text 90 | elif policy_element.tag == "Data": 91 | eas_provision_elements = policy_element.get_children()[0].get_children() 92 | for eas_provision_element in eas_provision_elements: 93 | if eas_provision_element.tag == "AllowBluetooth": 94 | policy_dict.update({"AllowBluetooth":eas_provision_element.text}) 95 | elif eas_provision_element.tag == "AllowBluetooth": 96 | policy_dict.update({"AllowBluetooth":eas_provision_element.text}) 97 | elif eas_provision_element.tag == "AllowBrowser": 98 | policy_dict.update({"AllowBrowser":eas_provision_element.text}) 99 | elif eas_provision_element.tag == "AllowCamera": 100 | policy_dict.update({"AllowCamera":eas_provision_element.text}) 101 | elif eas_provision_element.tag == "AllowConsumerEmail": 102 | policy_dict.update({"AllowConsumerEmail":eas_provision_element.text}) 103 | elif eas_provision_element.tag == "AllowDesktopSync": 104 | policy_dict.update({"AllowDesktopSync":eas_provision_element.text}) 105 | elif eas_provision_element.tag == "AllowHTMLEmail": 106 | policy_dict.update({"AllowHTMLEmail":eas_provision_element.text}) 107 | elif eas_provision_element.tag == "AllowInternetSharing": 108 | policy_dict.update({"AllowInternetSharing":eas_provision_element.text}) 109 | elif eas_provision_element.tag == "AllowIrDA": 110 | policy_dict.update({"AllowIrDA":eas_provision_element.text}) 111 | elif eas_provision_element.tag == "AllowPOPIMAPEmail": 112 | policy_dict.update({"AllowPOPIMAPEmail":eas_provision_element.text}) 113 | elif eas_provision_element.tag == "AllowRemoteDesktop": 114 | policy_dict.update({"AllowRemoteDesktop":eas_provision_element.text}) 115 | elif eas_provision_element.tag == "AllowSimpleDevicePassword": 116 | policy_dict.update({"AllowSimpleDevicePassword":eas_provision_element.text}) 117 | elif eas_provision_element.tag == "AllowSMIMEEncryptionAlgorithmNegotiation": 118 | policy_dict.update({"AllowSMIMEEncryptionAlgorithmNegotiation":eas_provision_element.text}) 119 | elif eas_provision_element.tag == "AllowSMIMESoftCerts": 120 | policy_dict.update({"AllowSMIMESoftCerts":eas_provision_element.text}) 121 | elif eas_provision_element.tag == "AllowStorageCard": 122 | policy_dict.update({"AllowStorageCard":eas_provision_element.text}) 123 | elif eas_provision_element.tag == "AllowTextMessaging": 124 | policy_dict.update({"AllowTextMessaging":eas_provision_element.text}) 125 | elif eas_provision_element.tag == "AllowUnsignedApplications": 126 | policy_dict.update({"AllowUnsignedApplications":eas_provision_element.text}) 127 | elif eas_provision_element.tag == "AllowUnsignedInstallationPackages": 128 | policy_dict.update({"AllowUnsignedInstallationPackages":eas_provision_element.text}) 129 | elif eas_provision_element.tag == "AllowWifi": 130 | policy_dict.update({"AllowWifi":eas_provision_element.text}) 131 | elif eas_provision_element.tag == "AlphanumericDevicePasswordRequired": 132 | policy_dict.update({"AlphanumericDevicePasswordRequired":eas_provision_element.text}) 133 | elif eas_provision_element.tag == "ApprovedApplicationList": 134 | policy_dict.update({"ApprovedApplicationList":eas_provision_element.text}) 135 | elif eas_provision_element.tag == "AttachmentsEnabled": 136 | policy_dict.update({"AttachmentsEnabled":eas_provision_element.text}) 137 | elif eas_provision_element.tag == "DevicePasswordEnabled": 138 | policy_dict.update({"DevicePasswordEnabled":eas_provision_element.text}) 139 | elif eas_provision_element.tag == "DevicePasswordExpiration": 140 | policy_dict.update({"DevicePasswordExpiration":eas_provision_element.text}) 141 | elif eas_provision_element.tag == "DevicePasswordHistory": 142 | policy_dict.update({"DevicePasswordHistory":eas_provision_element.text}) 143 | elif eas_provision_element.tag == "MaxAttachmentSize": 144 | policy_dict.update({"MaxAttachmentSize":eas_provision_element.text}) 145 | elif eas_provision_element.tag == "MaxCalendarAgeFilter": 146 | policy_dict.update({"MaxCalendarAgeFilter":eas_provision_element.text}) 147 | elif eas_provision_element.tag == "MaxDevicePasswordFailedAttempts": 148 | policy_dict.update({"MaxDevicePasswordFailedAttempts":eas_provision_element.text}) 149 | elif eas_provision_element.tag == "MaxEmailAgeFilter": 150 | policy_dict.update({"MaxEmailAgeFilter":eas_provision_element.text}) 151 | elif eas_provision_element.tag == "MaxEmailBodyTruncationSize": 152 | policy_dict.update({"MaxEmailBodyTruncationSize":eas_provision_element.text}) 153 | elif eas_provision_element.tag == "MaxEmailHTMLBodyTruncationSize": 154 | policy_dict.update({"MaxEmailHTMLBodyTruncationSize":eas_provision_element.text}) 155 | elif eas_provision_element.tag == "MaxInactivityTimeDeviceLock": 156 | policy_dict.update({"MaxInactivityTimeDeviceLock":eas_provision_element.text}) 157 | elif eas_provision_element.tag == "MinDevicePasswordComplexCharacters": 158 | policy_dict.update({"MinDevicePasswordComplexCharacters":eas_provision_element.text}) 159 | elif eas_provision_element.tag == "MinDevicePasswordLength": 160 | policy_dict.update({"MinDevicePasswordLength":eas_provision_element.text}) 161 | elif eas_provision_element.tag == "PasswordRecoveryEnabled": 162 | policy_dict.update({"PasswordRecoveryEnabled":eas_provision_element.text}) 163 | elif eas_provision_element.tag == "RequireDeviceEncryption": 164 | policy_dict.update({"RequireDeviceEncryption":eas_provision_element.text}) 165 | elif eas_provision_element.tag == "RequireEncryptedSMIMEMessages": 166 | policy_dict.update({"RequireEncryptedSMIMEMessages":eas_provision_element.text}) 167 | elif eas_provision_element.tag == "RequireEncryptionSMIMEAlgorithm": 168 | policy_dict.update({"RequireEncryptionSMIMEAlgorithm":eas_provision_element.text}) 169 | elif eas_provision_element.tag == "RequireManualSyncWhenRoaming": 170 | policy_dict.update({"RequireManualSyncWhenRoaming":eas_provision_element.text}) 171 | elif eas_provision_element.tag == "RequireSignedSMIMEAlgorithm": 172 | policy_dict.update({"RequireSignedSMIMEAlgorithm":eas_provision_element.text}) 173 | elif eas_provision_element.tag == "RequireSignedSMIMEMessages": 174 | policy_dict.update({"RequireSignedSMIMEMessages":eas_provision_element.text}) 175 | elif eas_provision_element.tag == "RequireStorageCardEncryption": 176 | policy_dict.update({"RequireStorageCardEncryption":eas_provision_element.text}) 177 | elif eas_provision_element.tag == "UnapprovedInROMApplicationList": 178 | policy_dict.update({"UnapprovedInROMApplicationList":eas_provision_element.text}) 179 | elif element.tag == "settings:DeviceInformation": 180 | device_information_children = element.get_children() 181 | for device_information_element in device_information_children: 182 | if device_information_element == "settings:Status": 183 | settings_status = device_information_element.text 184 | return (status, policy_status, policy_key, policy_type, policy_dict, settings_status) -------------------------------------------------------------------------------- /peas/py_activesync_helper.py: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Modified 2016 from code Copyright (C) 2013 Sol Birnbaum 3 | # 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either version 2 7 | # of the License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 17 | # MA 02110-1301, USA. 18 | ########################################################################\ 19 | 20 | import ssl 21 | 22 | # https://docs.python.org/2/library/xml.html#xml-vulnerabilities 23 | from lxml import etree as ElementTree 24 | 25 | from pyActiveSync.utils.as_code_pages import as_code_pages 26 | from pyActiveSync.utils.wbxml import wbxml_parser 27 | from pyActiveSync.client.storage import storage 28 | 29 | from pyActiveSync.client.FolderSync import FolderSync 30 | from pyActiveSync.client.Sync import Sync 31 | from pyActiveSync.client.GetItemEstimate import GetItemEstimate 32 | from pyActiveSync.client.Provision import Provision 33 | from pyActiveSync.client.Search import Search 34 | from pyActiveSync.client.ItemOperations import ItemOperations 35 | 36 | from pyActiveSync.objects.MSASHTTP import ASHTTPConnector 37 | from pyActiveSync.objects.MSASCMD import as_status 38 | from pyActiveSync.objects.MSASAIRS import airsync_FilterType, airsync_Conflict, airsync_MIMETruncation, \ 39 | airsync_MIMESupport, \ 40 | airsync_Class, airsyncbase_Type 41 | 42 | 43 | # Create WBXML parser instance. 44 | parser = wbxml_parser(*as_code_pages.build_as_code_pages()) 45 | 46 | 47 | def _parse_for_emails(res, emails): 48 | 49 | data = str(res) 50 | 51 | etparser = ElementTree.XMLParser(recover=True) 52 | tree = ElementTree.fromstring(data, etparser) 53 | 54 | for item in tree.iter('{airsync:}ApplicationData'): 55 | s = ElementTree.tostring(item) 56 | emails.append(s) 57 | 58 | 59 | def as_request(as_conn, cmd, wapxml_req): 60 | #print "\r\n%s Request:" % cmd 61 | #print wapxml_req 62 | res = as_conn.post(cmd, parser.encode(wapxml_req)) 63 | wapxml_res = parser.decode(res) 64 | #print "\r\n%s Response:" % cmd 65 | #print wapxml_res 66 | return wapxml_res 67 | 68 | 69 | #Provision functions 70 | def do_apply_eas_policies(policies): 71 | for policy in policies.keys(): 72 | #print "Virtually applying %s = %s" % (policy, policies[policy]) 73 | pass 74 | return True 75 | 76 | 77 | def do_provision(as_conn, device_info): 78 | provision_xmldoc_req = Provision.build("0", device_info) 79 | as_conn.set_policykey("0") 80 | provision_xmldoc_res = as_request(as_conn, "Provision", provision_xmldoc_req) 81 | status, policystatus, policykey, policytype, policydict, settings_status = Provision.parse(provision_xmldoc_res) 82 | as_conn.set_policykey(policykey) 83 | storage.update_keyvalue("X-MS-PolicyKey", policykey) 84 | storage.update_keyvalue("EASPolicies", repr(policydict)) 85 | if do_apply_eas_policies(policydict): 86 | provision_xmldoc_req = Provision.build(policykey) 87 | provision_xmldoc_res = as_request(as_conn, "Provision", provision_xmldoc_req) 88 | status, policystatus, policykey, policytype, policydict, settings_status = Provision.parse(provision_xmldoc_res) 89 | if status == "1": 90 | as_conn.set_policykey(policykey) 91 | storage.update_keyvalue("X-MS-PolicyKey", policykey) 92 | 93 | 94 | #Sync function 95 | def do_sync(as_conn, curs, collections, emails_out): 96 | 97 | as_sync_xmldoc_req = Sync.build(storage.get_synckeys_dict(curs), collections) 98 | #print "\r\nSync Request:" 99 | #print as_sync_xmldoc_req 100 | res = as_conn.post("Sync", parser.encode(as_sync_xmldoc_req)) 101 | #print "\r\nSync Response:" 102 | if res == '': 103 | #print "Nothing to Sync!" 104 | pass 105 | else: 106 | collectionid_to_type_dict = storage.get_serverid_to_type_dict() 107 | as_sync_xmldoc_res = parser.decode(res) 108 | #print type(as_sync_xmldoc_res), dir(as_sync_xmldoc_res), as_sync_xmldoc_res 109 | 110 | _parse_for_emails(as_sync_xmldoc_res, emails_out) 111 | 112 | sync_res = Sync.parse(as_sync_xmldoc_res, collectionid_to_type_dict) 113 | storage.update_items(sync_res) 114 | return sync_res 115 | 116 | 117 | #GetItemsEstimate 118 | def do_getitemestimates(as_conn, curs, collection_ids, gie_options): 119 | getitemestimate_xmldoc_req = GetItemEstimate.build(storage.get_synckeys_dict(curs), collection_ids, gie_options) 120 | getitemestimate_xmldoc_res = as_request(as_conn, "GetItemEstimate", getitemestimate_xmldoc_req) 121 | 122 | getitemestimate_res = GetItemEstimate.parse(getitemestimate_xmldoc_res) 123 | return getitemestimate_res 124 | 125 | 126 | def getitemestimate_check_prime_collections(as_conn, curs, getitemestimate_responses, emails_out): 127 | has_synckey = [] 128 | needs_synckey = {} 129 | for response in getitemestimate_responses: 130 | if response.Status == "1": 131 | has_synckey.append(response.CollectionId) 132 | elif response.Status == "2": 133 | #print "GetItemEstimate Status: Unknown CollectionId (%s) specified. Removing." % response.CollectionId 134 | pass 135 | elif response.Status == "3": 136 | #print "GetItemEstimate Status: Sync needs to be primed." 137 | pass 138 | needs_synckey.update({response.CollectionId: {}}) 139 | has_synckey.append( 140 | response.CollectionId) #technically *will* have synckey after do_sync() need end of function 141 | else: 142 | #print as_status("GetItemEstimate", response.Status) 143 | pass 144 | if len(needs_synckey) > 0: 145 | do_sync(as_conn, curs, needs_synckey, emails_out) 146 | return has_synckey, needs_synckey 147 | 148 | 149 | def sync(as_conn, curs, collections, collection_sync_params, gie_options, emails_out): 150 | getitemestimate_responses = do_getitemestimates(as_conn, curs, collections, gie_options) 151 | 152 | has_synckey, just_got_synckey = getitemestimate_check_prime_collections(as_conn, curs, getitemestimate_responses, 153 | emails_out) 154 | 155 | if (len(has_synckey) < collections) or (len(just_got_synckey) > 0): #grab new estimates, since they changed 156 | getitemestimate_responses = do_getitemestimates(as_conn, curs, has_synckey, gie_options) 157 | 158 | collections_to_sync = {} 159 | 160 | for response in getitemestimate_responses: 161 | if response.Status == "1": 162 | if int(response.Estimate) > 0: 163 | collections_to_sync.update({response.CollectionId: collection_sync_params[response.CollectionId]}) 164 | else: 165 | #print "GetItemEstimate Status (error): %s, CollectionId: %s." % (response.Status, response.CollectionId) 166 | pass 167 | 168 | if len(collections_to_sync) > 0: 169 | sync_res = do_sync(as_conn, curs, collections_to_sync, emails_out) 170 | 171 | if sync_res: 172 | while True: 173 | for coll_res in sync_res: 174 | if coll_res.MoreAvailable is None: 175 | del collections_to_sync[coll_res.CollectionId] 176 | if len(collections_to_sync.keys()) > 0: 177 | #print "Collections to sync:", collections_to_sync 178 | sync_res = do_sync(as_conn, curs, collections_to_sync, emails_out) 179 | else: 180 | break 181 | 182 | 183 | def disable_certificate_verification(): 184 | 185 | ssl._create_default_https_context = ssl._create_unverified_context 186 | 187 | 188 | def extract_emails(creds): 189 | 190 | storage.erase_db() 191 | storage.create_db_if_none() 192 | 193 | conn, curs = storage.get_conn_curs() 194 | device_info = {"Model": "1234", "IMEI": "123457", 195 | "FriendlyName": "My pyAS Client 2", "OS": "Python", "OSLanguage": "en-us", "PhoneNumber": "NA", 196 | "MobileOperator": "NA", "UserAgent": "pyAS"} 197 | 198 | #create ActiveSync connector 199 | as_conn = ASHTTPConnector(creds['server']) #e.g. "as.myserver.com" 200 | as_conn.set_credential(creds['user'], creds['password']) 201 | 202 | #FolderSync + Provision 203 | foldersync_xmldoc_req = FolderSync.build(storage.get_synckey("0")) 204 | foldersync_xmldoc_res = as_request(as_conn, "FolderSync", foldersync_xmldoc_req) 205 | changes, synckey, status = FolderSync.parse(foldersync_xmldoc_res) 206 | if 138 < int(status) < 145: 207 | ret = as_status("FolderSync", status) 208 | #print ret 209 | do_provision(as_conn, device_info) 210 | foldersync_xmldoc_res = as_request(as_conn, "FolderSync", foldersync_xmldoc_req) 211 | changes, synckey, status = FolderSync.parse(foldersync_xmldoc_res) 212 | if 138 < int(status) < 145: 213 | ret = as_status("FolderSync", status) 214 | #print ret 215 | raise Exception("Unresolvable provisioning error: %s. Cannot continue..." % status) 216 | if len(changes) > 0: 217 | storage.update_folderhierarchy(changes) 218 | storage.update_synckey(synckey, "0", curs) 219 | conn.commit() 220 | 221 | collection_id_of = storage.get_folder_name_to_id_dict() 222 | 223 | inbox = collection_id_of["Inbox"] 224 | 225 | collection_sync_params = { 226 | inbox: 227 | { #"Supported":"", 228 | #"DeletesAsMoves":"1", 229 | #"GetChanges":"1", 230 | "WindowSize": "512", 231 | "Options": { 232 | "FilterType": airsync_FilterType.OneMonth, 233 | "Conflict": airsync_Conflict.ServerReplacesClient, 234 | "MIMETruncation": airsync_MIMETruncation.TruncateNone, 235 | "MIMESupport": airsync_MIMESupport.SMIMEOnly, 236 | "Class": airsync_Class.Email, 237 | #"MaxItems":"300", #Recipient information cache sync requests only. Max number of frequently used contacts. 238 | "airsyncbase_BodyPreference": [{ 239 | "Type": airsyncbase_Type.HTML, 240 | "TruncationSize": "1000000000", # Max 4,294,967,295 241 | "AllOrNone": "1", 242 | # I.e. Do not return any body, if body size > tuncation size 243 | #"Preview": "255", # Size of message preview to return 0-255 244 | }, 245 | { 246 | "Type": airsyncbase_Type.MIME, 247 | "TruncationSize": "3000000000", # Max 4,294,967,295 248 | "AllOrNone": "1", 249 | # I.e. Do not return any body, if body size > tuncation size 250 | #"Preview": "255", # Size of message preview to return 0-255 251 | } 252 | ], 253 | #"airsyncbase_BodyPartPreference":"", 254 | #"rm_RightsManagementSupport":"1" 255 | }, 256 | #"ConversationMode":"1", 257 | #"Commands": {"Add":None, "Delete":None, "Change":None, "Fetch":None} 258 | }, 259 | } 260 | 261 | gie_options = { 262 | inbox: 263 | { #"ConversationMode": "0", 264 | "Class": airsync_Class.Email, 265 | "FilterType": airsync_FilterType.OneMonth 266 | #"MaxItems": "" #Recipient information cache sync requests only. Max number of frequently used contacts. 267 | }, 268 | } 269 | 270 | collections = [inbox] 271 | emails = [] 272 | 273 | sync(as_conn, curs, collections, collection_sync_params, gie_options, emails) 274 | 275 | if storage.close_conn_curs(conn): 276 | del conn, curs 277 | 278 | return emails 279 | 280 | 281 | def get_unc_listing(creds, unc_path, username=None, password=None): 282 | 283 | # Create ActiveSync connector. 284 | as_conn = ASHTTPConnector(creds['server']) 285 | as_conn.set_credential(creds['user'], creds['password']) 286 | 287 | # Perform request. 288 | search_xmldoc_req = Search.build(unc_path, username=username, password=password) 289 | search_xmldoc_res = as_request(as_conn, "Search", search_xmldoc_req) 290 | 291 | # Parse response. 292 | status, records = Search.parse(search_xmldoc_res) 293 | return records 294 | 295 | 296 | def get_unc_file(creds, unc_path, username=None, password=None): 297 | 298 | # Create ActiveSync connector. 299 | as_conn = ASHTTPConnector(creds['server']) 300 | as_conn.set_credential(creds['user'], creds['password']) 301 | 302 | # Perform request. 303 | operation = {'Name': 'Fetch', 'Store': 'DocumentLibrary', 'LinkId': unc_path} 304 | if username is not None: 305 | operation['UserName'] = username 306 | if password is not None: 307 | operation['Password'] = password 308 | operations = [operation] 309 | 310 | xmldoc_req = ItemOperations.build(operations) 311 | xmldoc_res = as_request(as_conn, "ItemOperations", xmldoc_req) 312 | responses = ItemOperations.parse(xmldoc_res) 313 | 314 | # Parse response. 315 | op, _, path, info, _ = responses[0] 316 | data = info['Data'].decode('base64') 317 | return data 318 | --------------------------------------------------------------------------------