diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..e2d3f6e --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,31 @@ +Copyright (c) 2016 MWR InfoSecurity +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of MWR InfoSecurity nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL MWR INFOSECURITY BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +This licence does not apply to the following components: + +- pyActiveSync, released under the GNU GENERAL PUBLIC LICENSE and available to download from: https://github.com/solbirn/pyActiveSync +- py-eas-client, released under the GNU GENERAL PUBLIC LICENSE and available to download from: https://github.com/ghiewa/py-eas-client/ \ No newline at end of file diff --git a/README.md b/README.md index 9a59c65..b7c661a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,132 @@ -# peas -Pending +# PEAS +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. + +## Prerequisites + +* `python` is Python 2, otherwise use `python2` +* Python [Requests](http://docs.python-requests.org/) library + +## Significant source files +Path | Functionality +--- | --- +`peas/__main__.py` | The command line application. +`peas/peas.py` | The PEAS client class that exclusively defines the interface to PEAS. +`peas/py_activesync_helper.py` | The helper functions that control the interface to pyActiveSync. +`peas/pyActiveSync/client` | The pyActiveSync EAS command builders and parsers. + +## Optional installation +`python setup.py install` + +# PEAS application +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. + +## Running PEAS + +`python -m peas [options] ` + + +## Example usage +### Check server +`python -m peas 10.207.7.100` + +### Check credentials +`python -m peas --check -u luke2 -p ChangeMe123 10.207.7.100` + +### Get emails +`python -m peas --emails -u luke2 -p ChangeMe123 10.207.7.100` + +### Save emails to directory +`python -m peas --emails -O emails -u luke2 -p ChangeMe123 10.207.7.100` + +### List file shares +`python -m peas --list-unc='\\fictitious-dc' -u luke2 -p ChangeMe123 10.207.7.100` + +`python -m peas --list-unc='\\fictitious-dc\guestshare' -u luke2 -p ChangeMe123 10.207.7.100` + +**Note:** Using an IP address or FQDN instead of a hostname in the UNC path may fail. + +### View file on file share +`python -m peas --dl-unc='\\fictitious-dc\guestshare\fileonguestshare.txt' -u luke2 -p ChangeMe123 10.207.7.100` + +### Save file from file share +`python -m peas --dl-unc='\\fictitious-dc\guestshare\fileonguestshare.txt' -o file.txt -u luke2 -p ChangeMe123 10.207.7.100` + +### Command line arguments + +Run `python -m peas --help` for the latest options. + + Options: + -h, --help show this help message and exit + -u USER username + -p PASSWORD password + --smb-user=USER username to use for SMB operations + --smb-pass=PASSWORD password to use for SMB operations + --verify-ssl verify SSL certificates (important) + -o FILENAME output to file + -O PATH output directory (for specific commands only, not + combined with -o) + -F repr,hex,b64,stdout,stderr,file + output formatting and encoding options + --check check if account can be accessed with given password + --emails retrieve emails + --list-unc=UNC_PATH list the files at a given UNC path + --dl-unc=UNC_PATH download the file at a given UNC path + + +## PEAS library + +PEAS can be imported as a library. + +### Example code + + import peas + + # Create an instance of the PEAS client. + client = peas.Peas() + + # Display the documentation for the PEAS client. + help(client) + + # Disable certificate verification so self-signed certificates don't cause errors. + client.disable_certificate_verification() + + # Set the credentials and server to connect to. + client.set_creds({ + 'server': '10.207.7.100', + 'user': 'luke2', + 'password': 'ChangeMe123', + }) + + # Check the credentials are accepted. + print("Auth result:", client.check_auth()) + + # Retrieve a file share directory listing. + listing = client.get_unc_listing(r'\\fictitious-dc\guestshare') + print(listing) + + # Retrieve emails. + emails = client.extract_emails() + print(emails) + +## Extending + +To extend the functionality of PEAS, there is a four step process: + +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. + +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. + +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. + +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. + + +## Limitations + +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. + +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. + +The licence may be restrictive due to the inclusion of pyActiveSync, which uses the GPLv2. + +The requirement to know the hostname of the target machine for file share access may impede enumeration. diff --git a/extractemails.py b/extractemails.py new file mode 100644 index 0000000..d7f7576 --- /dev/null +++ b/extractemails.py @@ -0,0 +1,42 @@ +"""Example script using PEAS to extract emails.""" + +__author__ = 'Adam Rutherford' + +import sys +import os +import time +import random +import subprocess +from pprint import pprint + +import peas +import _creds + + +def main(): + + peas.show_banner() + + client = peas.Peas() + + client.set_creds(_creds.CREDS) + + print("Extracting all emails with pyActiveSync") + client.set_backend(peas.PY_ACTIVE_SYNC) + + emails = client.extract_emails() + + pprint(emails) + print + + print("Extracting all emails with py-eas-client") + client.set_backend(peas.PY_EAS_CLIENT) + + emails = client.extract_emails() + + pprint(emails) + print + + +if __name__ == '__main__': + main() diff --git a/peas/__init__.py b/peas/__init__.py new file mode 100644 index 0000000..6ddf3f4 --- /dev/null +++ b/peas/__init__.py @@ -0,0 +1 @@ +from peas import * diff --git a/peas/__main__.py b/peas/__main__.py new file mode 100644 index 0000000..e56d100 --- /dev/null +++ b/peas/__main__.py @@ -0,0 +1,300 @@ +__author__ = 'Adam Rutherford' + +import sys +import os +import hashlib +from optparse import OptionParser + +import peas + + +def error(msg): + sys.stderr.write("[-] " + msg + "\n") + + +def positive(msg): + sys.stdout.write("[+] " + msg + "\n") + + +def negative(msg): + sys.stdout.write("[-] " + msg + "\n") + + +def create_arg_parser(): + + usage = "python -m peas [options] " + parser = OptionParser(usage=usage) + + # Settings: + parser.add_option("-u", None, dest="user", + help="username", metavar="USER") + + parser.add_option("-p", None, dest="password", + help="password", metavar="PASSWORD") + + parser.add_option("-q", None, dest="quiet", + action="store_true", default=False, + help="suppress all unnecessary output") + + parser.add_option("--smb-user", None, + dest="smb_user", + help="username to use for SMB operations", + metavar="USER") + + parser.add_option("--smb-pass", None, + dest="smb_password", + help="password to use for SMB operations", + metavar="PASSWORD") + + parser.add_option("--verify-ssl", None, dest="verify_ssl", + action="store_true", default=False, + help="verify SSL certificates (important)") + + parser.add_option("-o", None, dest="file", + help="output to file", metavar="FILENAME") + + parser.add_option("-O", None, dest="output_dir", + help="output directory (for specific commands only, not combined with -o)", metavar="PATH") + + parser.add_option("-F", None, dest="format", + help="output formatting and encoding options", + metavar="repr,hex,b64,stdout,stderr,file") + + # Functionality: + parser.add_option("--check", None, + action="store_true", dest="check", + help="check if account can be accessed with given password") + + parser.add_option("--emails", None, + action="store_true", dest="extract_emails", + help="retrieve emails") + + parser.add_option("--list-unc", None, + dest="list_unc", + help="list the files at a given UNC path", + metavar="UNC_PATH") + + parser.add_option("--dl-unc", None, + dest="dl_unc", + help="download the file at a given UNC path", + metavar="UNC_PATH") + + return parser + + +def init_authed_client(options, verify=True): + + if options.user is None: + error("A username must be specified for this command.") + return False + if options.password is None: + error("A password must be specified for this command.") + return False + + client = peas.Peas() + + creds = { + 'server': options.server, + 'user': options.user, + 'password': options.password, + } + if options.smb_user is not None: + creds['smb_user'] = options.smb_user + if options.smb_password is not None: + creds['smb_password'] = options.smb_password + + client.set_creds(creds) + + if not verify: + client.disable_certificate_verification() + + return client + + +def check_server(options): + + client = peas.Peas() + + client.set_creds({'server': options.server}) + + if not options.verify_ssl: + client.disable_certificate_verification() + + result = client.get_server_headers() + output_result(str(result), options, default='stdout') + + +def check(options): + + client = init_authed_client(options, verify=options.verify_ssl) + if not client: + return + + creds_valid = client.check_auth() + if creds_valid: + positive("Auth success.") + else: + negative("Auth failure.") + + +def extract_emails(options): + + client = init_authed_client(options, verify=options.verify_ssl) + if not client: + return + + emails = client.extract_emails() + # TODO: Output the emails in a more useful format. + for i, email in enumerate(emails): + + if options.output_dir: + fname = 'email_%d_%s.xml' % (i, hashlib.md5(email).hexdigest()) + path = os.path.join(options.output_dir, fname) + open(path, 'wb').write(email.strip() + '\n') + else: + output_result(email + '\n', options, default='repr') + + if options.output_dir: + print("Wrote %d emails to %r" % (len(emails), options.output_dir)) + + +def list_unc(options): + + client = init_authed_client(options, verify=options.verify_ssl) + if not client: + return + + path = options.list_unc + records = client.get_unc_listing(path) + + output = [] + + if not options.quiet: + print("Listing: %s\n" % (path,)) + + for record in records: + + name = record.get('DisplayName') + path = record.get('LinkId') + is_folder = record.get('IsFolder') == '1' + is_hidden = record.get('IsHidden') == '1' + size = record.get('ContentLength', '0') + 'B' + ctype = record.get('ContentType', '-') + last_mod = record.get('LastModifiedDate', '-') + created = record.get('CreationDate', '-') + + attrs = ('f' if is_folder else '-') + ('h' if is_hidden else '-') + + output.append("%s %-24s %-24s %-24s %-12s %s" % (attrs, created, last_mod, ctype, size, path)) + + output_result('\n'.join(output) + '\n', options, default='stdout') + + +def dl_unc(options): + + client = init_authed_client(options, verify=options.verify_ssl) + if not client: + return + + path = options.dl_unc + data = client.get_unc_file(path) + + if not options.quiet: + print("Downloading: %s\n" % (path,)) + + output_result(data, options, default='repr') + + +def output_result(data, options, default='repr'): + + fmt = options.format + if not fmt: + fmt = 'file' if options.file else default + actions = fmt.split(',') + + # Write to file at the end if a filename is specified. + if options.file and 'file' not in actions: + actions.append('file') + + # Process the output based on the format/encoding options chosen. + encoding_used = True + for action in actions: + if action == 'repr': + data = repr(data) + encoding_used = False + elif action == 'hex': + data = data.encode('hex') + encoding_used = False + elif action in ['base64', 'b64']: + data = data.encode('base64') + encoding_used = False + elif action == 'stdout': + print(data) + encoding_used = True + elif action == 'stderr': + sys.stderr.write(data) + encoding_used = True + # Allow the user to write the file after other encodings have been applied. + elif action == 'file': + if options.file: + open(options.file, 'wb').write(data) + if not options.quiet: + print("Wrote %d bytes to %r." % (len(data), options.file)) + else: + error("No filename specified.") + encoding_used = True + + # Print now if an encoding has been used but never output. + if not encoding_used: + print(data) + + +def process_options(options): + + # Create the output directory if necessary. + if options.output_dir: + try: + os.makedirs(options.output_dir) + except OSError: + pass + + return options + + +def main(): + + # Parse the arguments to the program into an options object. + arg_parser = create_arg_parser() + (options, args) = arg_parser.parse_args() + + if not options.quiet: + peas.show_banner() + + options = process_options(options) + + # The server is required as an argument. + if not args: + arg_parser.print_help() + return + options.server = args[0] + + # Perform the requested functionality. + ran = False + if options.check: + check(options) + ran = True + if options.extract_emails: + extract_emails(options) + ran = True + if options.list_unc: + list_unc(options) + ran = True + if options.dl_unc: + dl_unc(options) + ran = True + if not ran: + check_server(options) + + +if __name__ == '__main__': + main() diff --git a/peas/eas_client/__init__.py b/peas/eas_client/__init__.py new file mode 100644 index 0000000..b448978 --- /dev/null +++ b/peas/eas_client/__init__.py @@ -0,0 +1 @@ +__all__ = ["autodiscovery","activesync","activesync_producers"] \ No newline at end of file diff --git a/peas/eas_client/activesync.py b/peas/eas_client/activesync.py new file mode 100644 index 0000000..ac2f860 --- /dev/null +++ b/peas/eas_client/activesync.py @@ -0,0 +1,351 @@ +from twisted.internet import reactor, protocol, defer +from twisted.internet.ssl import ClientContextFactory +from twisted.python.failure import Failure +from twisted.web.client import Agent +from twisted.web.http_headers import Headers +from xml.dom.minidom import getDOMImplementation +import base64, urlparse, StringIO, uuid, sys +from urllib import urlencode +from dewbxml import wbxmlparser, wbxmlreader, wbxmldocument, wbxmlelement, wbxmlstring +from activesync_producers import WBXMLProducer, FolderSyncProducer, SyncProducer, ProvisionProducer, ItemOperationsProducer + +version = "1.0" + +class DataReader(wbxmlreader): + def __init__(self, data): + self._wbxmlreader__bytes = StringIO.StringIO(data) + +def convert_wbelem_to_dict(wbe): + if isinstance(wbe, wbxmlelement): + out_dict = {} + k = wbe.name + if len(wbe.children) == 1: + v = convert_wbelem_to_dict(wbe.children[0]) + else: + name_dupe = False + child_names = [] + for child in wbe.children: + if isinstance(child, wbxmlelement): + if child.name in child_names: + name_dupe = True + break + child_names.append(child.name) + if not name_dupe: + v = {} + for child in wbe.children: + v.update(convert_wbelem_to_dict(child)) + else: + v = [] + for child in wbe.children: + v.append(convert_wbelem_to_dict(child)) + out_dict[k] = v + else: + return str(wbe).strip() + return out_dict + + +class WBXMLHandler(protocol.Protocol): + def __init__(self, deferred, verbose=False): + self.deferred = deferred + self.d = '' + self.verbose = verbose + def dataReceived(self, data): + self.d += data + def connectionLost(self, reason): + if self.verbose: print "FINISHED LOADING"#, self.d.encode("hex") + if not len(self.d): + # this is valid from sync command + self.deferred.callback(None) + return + wb = wbxmlparser() + doc = wb.parse(DataReader(self.d)) + res_dict = convert_wbelem_to_dict(doc.root) + if self.verbose: print "Result:",res_dict + if "Status" in res_dict.values()[0]: + err_status = int(res_dict.values()[0]["Status"]) + if err_status != 1: + # application-layer error + self.deferred.errback("ActiveSync error %d"%err_status) + return + self.deferred.callback(res_dict) + + +class WebClientContextFactory(ClientContextFactory): + def getContext(self, hostname, port): + return ClientContextFactory.getContext(self) + +class ActiveSync(object): + def __init__(self, domain, username, pw, server, use_ssl, policy_key=0, server_version="14.0", device_type="iPhone", device_id=None, verbose=False): + self.use_ssl = use_ssl + self.domain = domain + self.username = username + self.password = pw + self.server = server + self.device_id = device_id + if not self.device_id: + self.device_id = str(uuid.uuid4()).replace("-","")[:32] + self.server_version = server_version + self.device_type = device_type + self.policy_key = policy_key + self.folder_data = {} + self.verbose = verbose + self.collection_data = {} + clientContext = WebClientContextFactory() + self.agent = Agent(reactor, clientContext) + self.operation_queue = defer.DeferredQueue() + self.queue_deferred = self.operation_queue.get() + self.queue_deferred.addCallback(self.queue_full) + + # Response processing + + def activesync_error(self, err): + if self.verbose: print "ERROR",err + return Failure(exc_value=err, exc_type="ActiveSync") + def options_response(self, resp): + if resp.code != 200: + return self.activesync_error("Response code %d"%resp.code) + supported_commands = resp.headers.getRawHeaders("ms-asprotocolcommands") + return supported_commands + + def wbxml_response(self, response): + if response.code != 200: + return self.activesync_error("Response code %d"%response.code) + d = defer.Deferred() + response.deliverBody(WBXMLHandler(d, self.verbose)) + return d + + def process_fetch(self, resp): + if isinstance(resp["ItemOperations"]["Response"], list): # multifetch + return resp["ItemOperations"]["Response"] + else: + return resp["ItemOperations"]["Response"]["Fetch"] + + def process_sync(self, resp, collection_id): + if not resp: + return self.collection_data[collection_id]["data"] + + sync_key = resp["Sync"]["Collections"]["Collection"]["SyncKey"] + collection_id = resp["Sync"]["Collections"]["Collection"]["CollectionId"] + + assert collection_id != None + if collection_id not in self.collection_data: # initial sync + self.collection_data[collection_id] = {"key":sync_key} + return self.sync(collection_id, sync_key) + else: + self.collection_data[collection_id]["key"] = sync_key + if "data" not in self.collection_data[collection_id]: + self.collection_data[collection_id]["data"] = {} + if "Commands" in resp["Sync"]["Collections"]["Collection"]: + + commands = resp["Sync"]["Collections"]["Collection"]["Commands"] + if isinstance(commands, dict): + + for command, cmdinfo in commands.iteritems(): + if self.verbose: + print "PROCESS COMMAND:", command, cmdinfo + if command == 'Add': + server_id = cmdinfo['ServerId'] + self.collection_data[collection_id]['data'][server_id] = cmdinfo + + else: + # This seems to assume "commands" is a list but it was a dict when tested. + for command in resp["Sync"]["Collections"]["Collection"]["Commands"]: + if self.verbose: + print "PROCESS COMMAND",command + print "all commands:", resp["Sync"]["Collections"]["Collection"]["Commands"] + if "Add" in command: + try: + server_id = command["Add"]["ServerId"] + except: + print "ERROR: Unexpected add format:",command["Add"] + continue + self.collection_data[collection_id]["data"][server_id] = command["Add"] + + if "MoreAvailable" in resp["Sync"]["Collections"]["Collection"]: + if self.verbose: print "MORE AVAILABLE, syncing again" + return self.sync(collection_id, sync_key) + + return self.collection_data[collection_id]["data"] + + def process_folder_sync(self, resp): + if "folders" not in self.folder_data: + self.folder_data["folders"] = {} + self.folder_data["key"] = resp["FolderSync"]["SyncKey"] + for change in resp["FolderSync"]["Changes"]: + if "Add" in change: + server_id = change["Add"]["ServerId"] + self.folder_data["folders"][server_id] = change["Add"] + return self.folder_data["folders"] + + def acknowledge_result(self, policyKey): + if self.verbose: print "FINAL POLICY KEY",policyKey + self.policy_key = policyKey + return True + def process_policy_key(self, resp): + try: + policyKey = resp["Provision"]["Policies"]["Policy"]["PolicyKey"] + except: + raise Exception("ActiveSync","Retrieving policy key failed",sys.exc_info()[0]) + return policyKey + + + # Request helpers + + def get_url(self): + scheme = "http" + if self.use_ssl: + scheme = "https" + return "%s://%s/Microsoft-Server-ActiveSync"%(scheme, self.server) + def add_parameters(self, url, params): + ps = list(urlparse.urlparse(url)) + ps[4] = urlencode(params) + return urlparse.urlunparse(ps) + def authorization_header(self): + return "Basic "+base64.b64encode("%s\%s:%s"%(self.domain.lower(),self.username.lower(),self.password)) + + # Request queueing + + def queue_full(self, next_request): + if self.verbose: print "Queue full",next_request + method = next_request[0] + retd = next_request[-1] + args = next_request[1:-2] + kwargs = next_request[-2] + d = method(*args, **kwargs) + d.addCallback(self.request_finished, retd) + d.addErrback(self.request_failed, retd) + + def request_finished(self, obj, return_deferred): + if self.verbose: print "Request finished, resetting queue",obj,return_deferred + self.queue_deferred = self.operation_queue.get() + self.queue_deferred.addCallback(self.queue_full) + return_deferred.callback(obj) + + def request_failed(self, failure, return_deferred): + if self.verbose: print "Request failed, resetting queue",failure,return_deferred + self.queue_deferred = self.operation_queue.get() + self.queue_deferred.addCallback(self.queue_full) + return_deferred.errback(failure) + + def add_operation(self, *operation_method_and_args, **kwargs): + if self.verbose: print "Add operation",operation_method_and_args + ret_d = defer.Deferred() + self.operation_queue.put(operation_method_and_args+(kwargs,ret_d,)) + return ret_d + + # Supported Requests + + def get_options(self): + if self.verbose: print "Options, get URL:",self.get_url(),"Authorization",self.authorization_header() + d = self.agent.request( + 'OPTIONS', + self.get_url(), + Headers({'User-Agent': ['python-EAS-Client %s'%version], 'Authorization': [self.authorization_header()]}), + None) + d.addCallback(self.options_response) + d.addErrback(self.activesync_error) + return d + + def acknowledge(self, policyKey): + self.policy_key = policyKey + prov_url = self.add_parameters(self.get_url(), {"Cmd":"Provision", "User":self.username, "DeviceId":self.device_id, "DeviceType":self.device_type}) + d = self.agent.request( + 'POST', + prov_url, + Headers({'User-Agent': ['python-EAS-Client %s'%version], + 'Authorization': [self.authorization_header()], + 'MS-ASProtocolVersion': [self.server_version], + 'X-MS-PolicyKey': [str(self.policy_key)], + 'Content-Type': ["application/vnd.ms-sync.wbxml"]}), + ProvisionProducer(policyKey, verbose=self.verbose)) + d.addCallback(self.wbxml_response) + d.addCallback(self.process_policy_key) + d.addCallback(self.acknowledge_result) + d.addErrback(self.activesync_error) + return d + + def provision(self): + prov_url = self.add_parameters(self.get_url(), {"Cmd":"Provision", "User":self.username, "DeviceId":self.device_id, "DeviceType":self.device_type}) + d = self.agent.request( + 'POST', + prov_url, + Headers({'User-Agent': ['python-EAS-Client %s'%version], + 'Authorization': [self.authorization_header()], + 'MS-ASProtocolVersion': [self.server_version], + 'X-MS-PolicyKey': [str(self.policy_key)], + 'Content-Type': ["application/vnd.ms-sync.wbxml"]}), + ProvisionProducer(verbose=self.verbose)) + d.addCallback(self.wbxml_response) + d.addCallback(self.process_policy_key) + d.addCallback(self.acknowledge) + d.addErrback(self.activesync_error) + return d + + def folder_sync(self, sync_key=0): + if sync_key == 0 and "key" in self.folder_data: + sync_key = self.folder_data["key"] + sync_url = self.add_parameters(self.get_url(), {"Cmd":"FolderSync", "User":self.username, "DeviceId":self.device_id, "DeviceType":self.device_type}) + d = self.agent.request( + 'POST', + sync_url, + Headers({'User-Agent': ['python-EAS-Client %s'%version], + 'Authorization': [self.authorization_header()], + 'MS-ASProtocolVersion': [self.server_version], + 'X-MS-PolicyKey': [str(self.policy_key)], + 'Content-Type': ["application/vnd.ms-sync.wbxml"]}), + FolderSyncProducer(sync_key, verbose=self.verbose)) + d.addCallback(self.wbxml_response) + d.addCallback(self.process_folder_sync) + d.addErrback(self.activesync_error) + return d + + def sync(self, collectionId, sync_key=0, get_body=False): + if sync_key == 0 and collectionId in self.collection_data: + sync_key = self.collection_data[collectionId]["key"] + + sync_url = self.add_parameters(self.get_url(), {"Cmd":"Sync", "User":self.username, "DeviceId":self.device_id, "DeviceType":self.device_type}) + d = self.agent.request( + 'POST', + sync_url, + Headers({'User-Agent': ['python-EAS-Client %s'%version], + 'Authorization': [self.authorization_header()], + 'MS-ASProtocolVersion': [self.server_version], + 'X-MS-PolicyKey': [str(self.policy_key)], + 'Content-Type': ["application/vnd.ms-sync.wbxml"]}), + SyncProducer(collectionId, sync_key, get_body, verbose=self.verbose)) + d.addCallback(self.wbxml_response) + d.addCallback(self.process_sync, collectionId) + d.addErrback(self.activesync_error) + return d + + def fetch(self, collectionId, serverId, fetchType, mimeSupport=0): + fetch_url = self.add_parameters(self.get_url(), {"Cmd":"ItemOperations", "User":self.username, "DeviceId":self.device_id, "DeviceType":self.device_type}) + d = self.agent.request( + 'POST', + fetch_url, + Headers({'User-Agent': ['python-EAS-Client %s'%version], + 'Authorization': [self.authorization_header()], + 'MS-ASProtocolVersion': [self.server_version], + 'X-MS-PolicyKey': [str(self.policy_key)], + 'Content-Type': ["application/vnd.ms-sync.wbxml"]}), + ItemOperationsProducer("Fetch", collectionId, serverId, fetchType, mimeSupport, store="Mailbox", verbose=self.verbose)) + d.addCallback(self.wbxml_response) + d.addCallback(self.process_fetch) + d.addErrback(self.activesync_error) + return d + + def fetch_link(self, linkId): + fetch_url = self.add_parameters(self.get_url(), {"Cmd":"ItemOperations", "User":self.username, "DeviceId":self.device_id, "DeviceType":self.device_type}) + d = self.agent.request( + 'POST', + fetch_url, + Headers({'User-Agent': ['python-EAS-Client %s'%version], + 'Authorization': [self.authorization_header()], + 'MS-ASProtocolVersion': [self.server_version], + 'X-MS-PolicyKey': [str(self.policy_key)], + 'Content-Type': ["application/vnd.ms-sync.wbxml"]}), + ItemOperationsProducer("Fetch", None, linkId, None, None, store="DocumentLibrary", verbose=self.verbose)) + d.addCallback(self.wbxml_response) + d.addCallback(self.process_fetch) + d.addErrback(self.activesync_error) + return d diff --git a/peas/eas_client/activesync_producers.py b/peas/eas_client/activesync_producers.py new file mode 100644 index 0000000..95f2bb8 --- /dev/null +++ b/peas/eas_client/activesync_producers.py @@ -0,0 +1,141 @@ +from twisted.internet.defer import succeed +from twisted.web.iweb import IBodyProducer +from zope.interface import implements +from dewbxml import wbxmlparser, wbxmlreader, wbxmldocument, wbxmlelement, wbxmlstring +import struct + +class WBXMLProducer(object): + implements(IBodyProducer) + def __init__(self, wbdoc, verbose=False): + self.verbose=verbose + self.wb = wbdoc + self.body = str(self.wb.tobytes()) + self.length = len(self.body) + def startProducing(self, consumer): + #if self.verbose: print "Producing",self.body.encode("hex"), self.wb + consumer.write(self.body) + return succeed(None) + def pauseProducing(self): pass + def stopProducing(self): pass + +def convert_array_to_children(in_elem, in_val): + if isinstance(in_val, list): + for v in in_val: + if len(v) > 2: + add_elem = wbxmlelement(v[0], page_num=v[2]) + else: + add_elem = wbxmlelement(v[0], page_num=in_elem.page_num) + in_elem.addchild(add_elem) + convert_array_to_children(add_elem, v[1]) + elif isinstance(in_val, dict): + print "FOUND OPAQUE THING",in_val + in_elem.addchild(wbxmlstring(struct.pack(in_val["fmt"],in_val["val"]), opaque=True)) + print "OPAQUE PRODUCED",in_elem + elif in_val != None: + in_elem.addchild(wbxmlstring(in_val)) + +def convert_dict_to_wbxml(indict, default_page_num=None): + wb = wbxmldocument() + wb.encoding = "utf-8" + wb.version = "1.3" + wb.schema = "activesync" + assert len(indict) == 1 # must be only one root element + #print "Root",indict.keys()[0] + if default_page_num != None: + root = wbxmlelement(indict.keys()[0], page_num=default_page_num) + else: + root = wbxmlelement(indict.keys()[0]) + wb.addchild(root) + convert_array_to_children(root, indict.values()[0]) + return wb + +class FolderSyncProducer(WBXMLProducer): + def __init__(self, sync_key, verbose=False): + wb = convert_dict_to_wbxml({ + "FolderSync": [ + ("SyncKey", str(sync_key)) + ] + }, default_page_num=7); + return WBXMLProducer.__init__(self, wb, verbose=verbose) + + + +class ItemOperationsProducer(WBXMLProducer): + def __init__(self, opname, collection_id, server_id, fetch_type, mimeSupport, store="Mailbox", verbose=False): + server_ids = [] + if isinstance(server_id, list): + server_ids.extend(server_id) + else: + server_ids.append(server_id) + wbdict = { + "ItemOperations": [] + } + for sid in server_ids: + if store == "Mailbox": + wbdict["ItemOperations"].append((opname, [ + ("Store", str(store)), + ("CollectionId", str(collection_id), 0), + ("ServerId", str(sid), 0), + ("Options",[ + ("MIMESupport", str(mimeSupport), 0), + ("BodyPreference", [ + ("Type", str(fetch_type)), + ("TruncationSize", str(512)) + ], 17) + ]), + ])) + else: + wbdict["ItemOperations"].append((opname, [ + ("Store", str(store)), + ("LinkId", str(sid), 19), + ("Options",[]), + ])) + wb = convert_dict_to_wbxml(wbdict, default_page_num=20) + return WBXMLProducer.__init__(self, wb, verbose=verbose) + +class SyncProducer(WBXMLProducer): + def __init__(self, collection_id, sync_key, get_body, verbose=False): + wbdict = { + "Sync": [ + ("Collections", [ + ("Collection", [ + ("SyncKey", str(sync_key)), + ("CollectionId", str(collection_id)), + ("DeletesAsMoves", "1"), + ]) + ]) + ] + } + if sync_key != 0: + wbdict["Sync"][0][1][0][1].append(("GetChanges","1")) + wbdict["Sync"][0][1][0][1].append(("WindowSize","512")) + if get_body: + wbdict["Sync"][0][1][0][1].append(("Options",[ + ("MIMESupport", "0"), + ("BodyPreference", [ + ("Type", "2"), + ("TruncationSize", "5120"), + ], 17) + ])) + wb = convert_dict_to_wbxml(wbdict, default_page_num=0) + return WBXMLProducer.__init__(self, wb, verbose=verbose) + +class ProvisionProducer(WBXMLProducer): + def __init__(self, policyKey=None, verbose=False): + wbdict = { + "Provision": [ + ("Policies", [ + ("Policy", [ + ("PolicyType", "MS-EAS-Provisioning-WBXML"), + ]) + ]) + ] + } + + if policyKey != None: + wbdict["Provision"][0][1][0][1].append(("PolicyKey",str(policyKey))) + wbdict["Provision"][0][1][0][1].append(("Status","1")) + + wb = convert_dict_to_wbxml(wbdict, default_page_num=14) + + return WBXMLProducer.__init__(self, wb, verbose=verbose) \ No newline at end of file diff --git a/peas/eas_client/autodiscovery.py b/peas/eas_client/autodiscovery.py new file mode 100644 index 0000000..00ab198 --- /dev/null +++ b/peas/eas_client/autodiscovery.py @@ -0,0 +1,97 @@ +from twisted.internet import reactor +from twisted.web.client import Agent +from twisted.web.http_headers import Headers +from xml.dom.minidom import getDOMImplementation +from zope.interface import implements +from twisted.internet.defer import succeed +from twisted.web.iweb import IBodyProducer + +version = "1.0" + +class AutoDiscoveryProducer(object): + implements(IBodyProducer) + def __init__(self, email_address): + impl = getDOMImplementation() + newdoc = impl.createDocument(None, "Autodiscover", None) + top_element = newdoc.documentElement + top_element.setAttribute("xmlns", "http://schemas.microsoft.com/exchange/autodiscover/mobilesync/requestschema/2006") + req_elem = newdoc.createElement('Request') + top_element.appendChild(req_elem) + email_elem = newdoc.createElement('EMailAddress') + req_elem.appendChild(email_elem) + email_elem.appendChild(newdoc.createTextNode(email_address)) + resp_schema = newdoc.createElement('AcceptableResponseSchema') + req_elem.appendChild(resp_schema) + resp_schema.appendChild(newdoc.createTextNode("http://schemas.microsoft.com/exchange/autodiscover/mobilesync/responseschema/2006")) + self.body = newdoc.toxml("utf-8") + self.length = len(self.body) + + def startProducing(self, consumer): + consumer.write(self.body) + return succeed(None) + + def pauseProducing(self): + pass + + def stopProducing(self): + pass + +class AutoDiscover: + """The AutoDiscover class is used to find EAS servers using only an email address""" + STATE_INIT = 0 + STATE_XML_REQUEST = 1 + STATE_XML_AUTODISCOVER_REQUEST = 2 + STATE_INSECURE = 3 + STATE_SRV = 4 + STATE_REDIRECT = 5 + LAST_STATE = 6 + AD_REQUESTS = {STATE_XML_REQUEST:"https://%s/autodiscover/autodiscover.xml", + STATE_XML_AUTODISCOVER_REQUEST:"https://autodiscover.%s/autodiscover/autodiscover.xml", + STATE_INSECURE:"http://autodiscover.%s/autodiscover/autodiscover.xml"} + + def __init__(self, email): + self.email = email + self.email_domain = email.split("@")[1] + self.agent = Agent(reactor) + self.state = AutoDiscover.STATE_INIT + self.redirect_urls = [] + def handle_redirect(self, new_url): + if new_url in self.redirect_urls: + raise Exception("AutoDiscover", "Circular redirection") + self.redirect_urls.append(new_url) + self.state = AutoDiscover.STATE_REDIRECT + print "Making request to",new_url + d = self.agent.request( + 'GET', + new_url, + Headers({'User-Agent': ['python-EAS-Client %s'%version]}), + AutoDiscoveryProducer(self.email)) + d.addCallback(self.autodiscover_response) + d.addErrback(self.autodiscover_error) + return d + def autodiscover_response(self, result): + print "RESPONSE",result,result.code + if result.code == 302: + # TODO: "Redirect responses" validation + return self.handle_redirect(result.headers.getRawHeaders("location")[0]) + return result + def autodiscover_error(self, error): + print "ERROR",error,error.value.reasons[0] + if self.state < AutoDiscover.LAST_STATE: + return self.autodiscover() + raise error + def autodiscover(self): + self.state += 1 + if self.state in AutoDiscover.AD_REQUESTS: + print "Making request to",AutoDiscover.AD_REQUESTS[self.state]%self.email_domain + body = AutoDiscoveryProducer(self.email) + d = self.agent.request( + 'GET', + AutoDiscover.AD_REQUESTS[self.state]%self.email_domain, + Headers({'User-Agent': ['python-EAS-Client %s'%version]}), + body) + d.addCallback(self.autodiscover_response) + d.addErrback(self.autodiscover_error) + return d + else: + raise Exception("Unsupported state",str(self.state)) \ No newline at end of file diff --git a/peas/eas_client/dewbxml.py b/peas/eas_client/dewbxml.py new file mode 100644 index 0000000..717c080 --- /dev/null +++ b/peas/eas_client/dewbxml.py @@ -0,0 +1,902 @@ +#! /usr/bin/env python +#coding=utf-8 + +r'''Converter of Wireless Binary XML (WBXML) documents to plain-text XML. + + When invoked without arguments, DeWBXML opens one file dialog asking for an + input WBXML file, and another for defining the path to the output plain-text + XML file. Execution is terminated if any of those dialogs is canceled. + + Alternatively, the program can be invoked from command line with either two + arguments (the input and output paths) or just one (in which case the output + is written to standard output). +''' + +__license__ = r''' +Copyright (c) 2010 Helio Perroni Filho + +This file is part of DeWBXML. + +DeWBXML is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +DeWBXML is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with DeWBXML. If not, see . +''' + +__version__ = '2010-07-14r' + +#import provisioning +#import rightsobjects +#import wml13 + +from base64 import b64encode +from sys import stdout +from traceback import print_exc +from struct import pack + +# List of known charsets, indexed by their IANA numbers. +_charsets = { + 3: 'iso-ir-6', + 4: 'iso-ir-100', + 5: 'iso-ir-101', + 6: 'iso-ir-109', + 7: 'iso-ir-110', + 8: 'iso-ir-144', + 9: 'iso-ir-127', + 10: 'iso-ir-126', + 106: 'utf-8' +} + +# List of known WBXML encondings for plain-text XML applications. +_applications = { + 0x01: { + 'dtd': r'activesync', + 'elements': [ + { # Page 0 - AirSync + "name":"AirSync", + 0x05: ('Sync', None), + 0x06: ('Responses', None), + 0x07: ('Add', None), + 0x08: ('Change', None), + 0x09: ('Delete', None), + 0x0a: ('Fetch', None), + 0x0b: ('SyncKey', None), + 0x0c: ('ClientId', None), + 0x0d: ('ServerId', None), + 0x0e: ('Status', None), + 0x0f: ('Collection', None), + 0x10: ('Class', None), + 0x12: ('CollectionId', None), + 0x13: ('GetChanges', None), + 0x14: ('MoreAvailable', None), + 0x15: ('WindowSize', None), + 0x16: ('Commands', None), + 0x17: ('Options', None), + 0x18: ('FilterType', None), + 0x1b: ('Conflict', None), + 0x1c: ('Collections', None), + 0x1d: ('ApplicationData', None), + 0x1e: ('DeletesAsMoves', None), + 0x20: ('Supported', None), + 0x21: ('SoftDelete', None), + 0x22: ('MIMESupport', None), + 0x23: ('MIMETruncation', None), + 0x24: ('Wait', None), + 0x25: ('Limit', None), + 0x26: ('Partial', None), + 0x27: ('ConversationMode', None), + 0x28: ('MaxItems', None), + 0x29: ('HeartbeatInterval', None), + }, + { # Page 1 - Contacts + "name":"Contacts", + 0x05: ('Anniversary', None), + 0x06: ('AssistantName', None), + 0x07: ('AssistantPhoneNumber', None), + 0x08: ('Birthday', None), + 0x0c: ('Business2PhoneNumber', None), + 0x0d: ('BusinessAddressCity', None), + 0x0e: ('BusinessAddressCountry', None), + 0x0f: ('BusinessAddressPostalCode', None), + 0x10: ('BusinessAddressState', None), + 0x11: ('BusinessAddressStreet', None), + 0x12: ('BusinessFaxNumber', None), + 0x13: ('BusinessPhoneNumber', None), + 0x14: ('CarPhoneNumber', None), + 0x15: ('Categories', None), + 0x16: ('Category', None), + 0x17: ('Children', None), + 0x18: ('Child', None), + 0x19: ('CompanyName', None), + 0x1a: ('Department', None), + 0x1b: ('Email1Address', None), + 0x1c: ('Email2Address', None), + 0x1d: ('Email3Address', None), + 0x1e: ('FileAs', None), + 0x1f: ('FirstName', None), + 0x20: ('Home2PhoneNumber', None), + 0x21: ('HomeAddressCity', None), + 0x22: ('HomeAddressCountry', None), + 0x23: ('HomeAddressPostalCode', None), + 0x24: ('HomeAddressState', None), + 0x25: ('HomeAddressStreet', None), + 0x26: ('HomeFaxNumber', None), + 0x27: ('HomePhoneNumber', None), + 0x28: ('JobTitle', None), + 0x29: ('LastName', None), + 0x2a: ('MiddleName', None), + 0x2b: ('MobilePhoneNumber', None), + 0x2c: ('OfficeLocation', None), + 0x2d: ('OtherAddressCity', None), + 0x2e: ('OtherAddressCountry', None), + 0x2f: ('OtherAddressPostalCode', None), + 0x30: ('OtherAddressState', None), + 0x31: ('OtherAddressStreet', None), + 0x32: ('PagerNumber', None), + 0x33: ('RadioPhoneNumber', None), + 0x34: ('Spouse', None), + 0x35: ('Suffix', None), + 0x36: ('Title', None), + 0x37: ('WebPage', None), + 0x38: ('YomiCompanyName', None), + 0x39: ('YomiFirstName', None), + 0x3a: ('YomiLastName', None), + 0x3c: ('Picture', None), + 0x3d: ('Alias', None), + 0x3e: ('WeightedRank', None), + }, + { # Page 2 - Email + "name":"Email", + 0x0f: ('DateReceived', None), + 0x11: ('DisplayTo', None), + 0x12: ('Importance', None), + 0x13: ('MessageClass', None), + 0x14: ('Subject', None), + 0x15: ('Read', None), + 0x16: ('To', None), + 0x17: ('Cc', None), + 0x18: ('From', None), + 0x19: ('ReplyTo', None), + 0x1a: ('AllDayEvent', None), + 0x1b: ('Categories', None), + 0x1c: ('Category', None), + 0x1d: ('DtStamp', None), + 0x1e: ('EndTime', None), + 0x1f: ('InstanceType', None), + 0x20: ('BusyStatus', None), + 0x21: ('Location', None), + 0x22: ('MeetingRequest', None), + 0x23: ('Organizer', None), + 0x24: ('RecurrenceId', None), + 0x25: ('Reminder', None), + 0x26: ('ResponseRequested', None), + 0x27: ('Recurrences', None), + 0x28: ('Recurrence', None), + 0x29: ('Type', None), + 0x2a: ('Until', None), + 0x2b: ('Occurrences', None), + 0x2c: ('Interval', None), + 0x2d: ('DayOfWeek', None), + 0x2e: ('DayOfMonth', None), + 0x2f: ('WeekOfMonth', None), + 0x30: ('MonthOfYear', None), + 0x31: ('StartTime', None), + 0x32: ('Sensitivity', None), + 0x33: ('TimeZone', None), + 0x34: ('GlobalObjId', None), + 0x35: ('ThreadTopic', None), + 0x39: ('InternetCPID', None), + 0x3a: ('Flag', None), + 0x3b: ('Status', None), + 0x3c: ('ContentClass', None), + 0x3d: ('FlagType', None), + 0x3e: ('CompleteTime', None), + 0x3f: ('DisallowNewTimeProposal', None), + }, + { # Page 3 - AirNotify + }, + { # Page 4 - Calendar + }, + { # Page 5 - Move + "name":"Move", + 0x05: ('MoveItems', None), + 0x06: ('Move', None), + 0x07: ('SrcMsgId', None), + 0x08: ('SrcFldId', None), + 0x09: ('DstFldId', None), + 0x0a: ('Response', None), + 0x0b: ('Status', None), + 0x0c: ('DstMsgId', None), + }, + { # Page 6 - GetItemEstimate + }, + { # Page 7 - FolderHierarchy + "name":"FolderHierarchy", + 0x07: ('DisplayName', None), + 0x08: ('ServerId', None), + 0x09: ('ParentId', None), + 0x0a: ('Type', None), + 0x0c: ('Status', None), + 0x0e: ('Changes', None), + 0x0f: ('Add', None), + 0x10: ('Delete', None), + 0x11: ('Update', None), + 0x12: ('SyncKey', None), + 0x13: ('FolderCreate', None), + 0x14: ('FolderDelete', None), + 0x15: ('FolderUpdate', None), + 0x16: ('FolderSync', None), + 0x17: ('Count', None), + }, + { # Page 8 - MeetingResponse + }, + { # Page 9 - Tasks + "name":"Tasks", + 0x08: ('Categories', None), + 0x09: ('Category', None), + 0x0a: ('Complete', None), + 0x0b: ('DateCompleted', None), + 0x0c: ('DueDate', None), + 0x0d: ('UtcDueDate', None), + 0x0e: ('Importance', None), + 0x0f: ('Recurrence', None), + 0x10: ('Type', None), + 0x11: ('Start', None), + 0x12: ('Until', None), + 0x13: ('Occurrences', None), + 0x14: ('Interval', None), + 0x15: ('DayOfMonth', None), + 0x16: ('DayOfWeek', None), + 0x17: ('WeekOfMonth', None), + 0x18: ('MonthOfYear', None), + 0x19: ('Regenerate', None), + 0x1a: ('DeadOccur', None), + 0x1b: ('ReminderSet', None), + 0x1c: ('ReminderTime', None), + 0x1d: ('Sensitivity', None), + 0x1e: ('StartDate', None), + 0x1f: ('UtcStartDate', None), + 0x20: ('Subject', None), + 0x22: ('OrdinalDate', None), + 0x23: ('SubOrdinalDate', None), + 0x24: ('CalendarType', None), + 0x25: ('IsLeapMonth', None), + 0x26: ('FirstDayOfWeek', None), + }, + { # Page 10 - ResolveRecipients + }, + { # Page 11 - ValidateCert + }, + { # Page 12 - Contacts2 + }, + { # Page 13 - Ping + }, + { # Page 14 - Provision + "name":"Provision", + 0x05: ('Provision', None), + 0x06: ('Policies', None), + 0x07: ('Policy', None), + 0x08: ('PolicyType', None), + 0x09: ('PolicyKey', None), + 0x0a: ('Data', None), + 0x0b: ('Status', None), + 0x0c: ('RemoteWipe', None), + 0x0d: ('EASProvisionDoc', None), + 0x0e: ('DevicePasswordEnabled', None), + 0x0f: ('AlphanumericDevicePasswordRequired', None), + 0x10: ('DeviceEncryptionEnabled', None), + 0x11: ('PasswordRecoveryEnabled', None), + 0x13: ('AttachmentsEnabled', None), + 0x14: ('MinDevicePasswordLength', None), + 0x15: ('MaxInactivityTimeDeviceLock', None), + 0x16: ('MaxDevicePasswordFailedAttempts', None), + 0x17: ('MaxAttachmentSize', None), + 0x18: ('AllowSimpleDevicePassword', None), + 0x19: ('DevicePasswordExpiration', None), + 0x1a: ('DevicePasswordHistory', None), + 0x1b: ('AllowStorageCard', None), + 0x1c: ('AllowCamera', None), + 0x1d: ('RequireDeviceEncryption', None), + 0x1e: ('AllowUnsignedApplications', None), + 0x1f: ('AllowUnsignedInstallationPackages', None), + 0x20: ('MinDevicePasswordComplexCharacters', None), + 0x21: ('AllowWiFi', None), + 0x22: ('AllowTextMessaging', None), + 0x23: ('AllowPOPIMAPEmail', None), + 0x24: ('AllowBluetooth', None), + 0x25: ('AllowIrDA', None), + 0x26: ('RequireManualSyncWhenRoaming', None), + 0x27: ('AllowDesktopSync', None), + 0x28: ('MaxCalendarAgeFilter', None), + 0x29: ('AllowHTMLEmail', None), + 0x2a: ('MaxEmailAgeFilter', None), + 0x2b: ('MaxEmailBodyTruncationSize', None), + 0x2c: ('MaxEmailHTMLBodyTruncationSize', None), + 0x2d: ('RequireSignedSMIMEMessages', None), + 0x2e: ('RequireEncryptedSMIMEMessages', None), + 0x2f: ('RequireSignedSMIMEAlgorithm', None), + 0x30: ('RequireEncryptionSMIMEAlgorithm', None), + 0x31: ('AllowSMIMEEncryptionAlgorithmNegotiation', None), + 0x32: ('AllowSMIMESoftCerts', None), + 0x33: ('AllowBrowser', None), + 0x34: ('AllowConsumerEmail', None), + 0x35: ('AllowRemoteDesktop', None), + 0x36: ('AllowInternetSharing', None), + 0x37: ('UnapprovedInROMApplicationList', None), + 0x38: ('ApplicationName', None), + 0x39: ('ApprovedApplicationList', None), + 0x3a: ('Hash', None), + }, + { # Page 15 - Search + }, + { # Page 16 - GAL + }, + { # Page 17 - AirSyncBase + "name":"AirSyncBase", + 0x05: ('BodyPreference', None), + 0x06: ('Type', None), + 0x07: ('TruncationSize', None), + 0x08: ('AllOrNone', None), + 0x0a: ('Body', None), + 0x0b: ('Data', None), + 0x0c: ('EstimatedDataSize', None), + 0x0d: ('Truncated', None), + 0x0e: ('Attachments', None), + 0x0f: ('Attachment', None), + 0x10: ('DisplayName', None), + 0x11: ('FileReference', None), + 0x12: ('Method', None), + 0x13: ('ContentId', None), + 0x14: ('ContentLocation', None), + 0x15: ('IsInline', None), + 0x16: ('NativeBodyType', None), + 0x17: ('ContentType', None), + 0x18: ('Preview', None), + 0x19: ('BodyPartReference', None), + 0x1a: ('BodyPart', None), + 0x1b: ('Status', None), + }, + { # Page 18 + }, + { # Page 19 + "name":"DocumentLibrary", + 0x05: ('LinkId', None), + 0x06: ('DisplayName', None), + 0x07: ('IsFolder', None), + 0x08: ('CreationDate', None), + 0x09: ('LastModifiedDate', None), + 0x0a: ('IsHidden', None), + 0x0b: ('ContentLength', None), + 0x0c: ('ContentType', None), + }, + { # Page 20 + "name":"ItemOperations", + 0x05: ('ItemOperations', None), + 0x06: ('Fetch', None), + 0x07: ('Store', None), + 0x08: ('Options', None), + 0x09: ('Range', None), + 0x0a: ('Total', None), + 0x0b: ('Properties', None), + 0x0c: ('Data', None), + 0x0d: ('Status', None), + 0x0e: ('Response', None), + 0x0f: ('Version', None), + 0x10: ('Schema', None), + 0x11: ('Part', None), + 0x12: ('EmptyFolderContents', None), + 0x13: ('DeleteSubFolders', None), + 0x14: ('UserName', None), + 0x15: ('Password', None), + 0x16: ('Move', None), + 0x17: ('DstFldId', None), + 0x18: ('ConversationId', None), + 0x19: ('MoveAlways', None), + }, + { # Page 21 + }, + { # Page 22 - Email2 + "name":"Email2", + 0x05: ('UmCallerID', None), + 0x06: ('UmUserNotes', None), + 0x07: ('UmAttDuration', None), + 0x08: ('UmAttOrder', None), + 0x09: ('ConversationId', None), + 0x0a: ('ConversationIndex', None), + 0x0b: ('LastVerbExecuted', None), + 0x0c: ('LastVerbExecutionTime', None), + 0x0d: ('ReceivedAsBcc', None), + 0x0e: ('Sender', None), + 0x0f: ('CalendarType', None), + 0x10: ('IsLeapMonth', None), + 0x11: ('AccountId', None), + 0x12: ('FirstDayOfWeek', None), + 0x13: ('MeetingMessageType', None), + }, + ] + } +} + +# Special WBXML tokens. +SWITCH_PAGE = 0x00 +END = 0x01 +STR_I = 0x03 +STR_T = 0x83 +OPAQUE = 0xC3 + + +class wbxmldocument(object): + r'''Class for WBXML DOM document objects. + ''' + def __init__(self): + r'''Creates a new WBXML DOM document object. + ''' + self.encoding = '' + self.schema = '' + self.version = '' + self.__stringtable = [] + self.root = None + + def __str__(self): + r'''Converts this document object (and contained element objects, + recursively) to string. + ''' + return \ + r'' + \ + '\n\n' + \ + r'' + \ + '\n\n' + \ + r'' + \ + '\n\n' + \ + r'' + \ + '\n\n' + \ + str(self.root) + + def addchild(self, root): + r'''Sets this document's root object. It's a convenience method meant + for easing the implementation of the DOM parser. + ''' + self.root = root + root.parent = self + + def tobytes(self): + version_major = int(self.version.split(".")[0]) + version_minor = int(self.version.split(".")[1]) + + enc_token = 1 + for (token,enc) in _applications.iteritems(): + if enc == self.schema: + enc_token = token + break + + chars_token = 0 + for (token,enc) in _charsets.iteritems(): + if enc == self.encoding: + chars_token = token + break + + assert len(self.__stringtable) == 0 + + return pack("BBBB", ((version_major-1)<<4)|version_minor, enc_token, chars_token, len(self.__stringtable))+self.root.tobytes(enc_token) + + @property + def stringtable(self): + string = '' + table = [] + for code in self.__stringtable: + if code != 0x00: + string += chr(code) + else: + table.append(string) + string = '' + + return table + + @stringtable.setter + def stringtable(self, table): + self.__stringtable = table + + +class wbxmlelement(object): + r'''Class for WBXML DOM elements. + ''' + def __init__(self, name = None, attributes = {}, page_num=None): + r'''Creates a new WBXML DOM element object. + ''' + self.page_num = page_num + self.parent = None + self.name = name + self.attributes = dict(attributes) + self.children = [] + + def __str__(self): + r'''Converts this element object (and contained element objects, + recursively) to string. + ''' + return self.tostring(0) + + def tostring(self, level): + r'''Converts this element object (and contained element objects, + recursively) to string, idented to the given ident level. + ''' + ident = level * ' ' + attributes = '' + for (name, value) in self.attributes.items(): + attributes += ' ' + name + '="' + value + '"' + + closebracket = '' + children = '' + closetag = '' + + start_name = self.name + if self.page_num != None and "name" in _applications[0x1]["elements"][self.page_num]: + start_name = _applications[0x1]["elements"][self.page_num]["name"]+":"+start_name + + if len(self.children) > 0: + closebracket = '>\n' + closetag = ident + '' + for child in self.children: + children += child.tostring(level + 1) + else: + closebracket = ' />' + return ident + '<' + start_name + attributes + closebracket + children + closetag + '\n' + + def tobytes(self, enc_token): + tag_val = 0 + tag_pagenum = 0 + for page_num in range(len(_applications[enc_token]["elements"])): + if self.page_num != None and page_num != self.page_num: + continue + for token,tokeninfo in _applications[enc_token]["elements"][page_num].iteritems(): + if tokeninfo[0] == self.name: + tag_val = token + tag_pagenum = page_num + break + if len(self.attributes): + tag_val |= 0b10000000 + if len(self.children): + tag_val |= 0b01000000 + byte_data = pack("BBB", 0, tag_pagenum, tag_val) + assert len(self.attributes) == 0 + for child in self.children: + byte_data += child.tobytes(enc_token) + byte_data += pack("B", 1) + return byte_data + + def addchild(self, child): + r'''Adds a child element to this element object. + ''' + self.children.append(child) + child.parent = self + + +class wbxmlstring(object): + r'''Class for text elements. + ''' + def __init__(self, value, opaque=False): + r'''Creates a new text element object from a string. + ''' + self.__value = value + self.is_opaque = opaque + + def __str__(self): + r'''Converts this text element to string. + ''' + return self.tostring(0) + + def tobytes(self, enc_token): + if self.is_opaque: + return pack("BB", OPAQUE, len(self.__value))+self.__value + return pack("B", STR_I)+self.__value+pack("B",0) + + def tostring(self, level): + r'''Converts this text element to string, idented to the given ident + level. + ''' + if self.is_opaque: + return level * ' ' + "OPAQUE: "+self.__value.encode("hex") + '\n' + return level * ' ' + self.__value + '\n' + + +class wbxmlreader(object): + r'''File reader for WBXML documents. Implements several conveniences for + parsing WBXML files. + ''' + def __init__(self, path): + r'''Creates a new WBXML reader for the file at the given path. + ''' + self.__bytes = open(path, 'rb') + + def __iter__(self): + r'''Returns an iterator over this reader (actually, the object itself). + ''' + return self + + def next(self): + r'''Reads one binary token from the WBXML file and advances the file + pointer one position. If the end-of-file has already been reached, + raises the StopIteration exception. + ''' + return self.read() + + def read(self, length = None): + r'''Reads a sequence of one or more tokens from the underlying WBXML + file, incrementing the file pointer accordingly. + + If the length is ommited, one token is read and returned as an + integer; otherwise, at most (length) tokens are read and returned as + a character string. This holds true even for length = 1, so + reader.read(1) returns a single-character string. + + If a previous operation reached the end-of-file, this method raises + the StopIteration exception. + ''' + data = self.__bytes.read(1 if length == None else length) + + if len(data) == 0: + raise StopIteration() + + return ord(data) if length == None else data + + def readopaque(self): + r'''Reads an opaque data buffer from the WBXML file, and returns it + as a base64 string. The file pointer is incremented until past the + end of the buffer. + ''' + length = self.read() + data = self.read(length) + return b64encode(data) + + def readstring(self): + r'''Reads tokens from the WBXML file until the end-of-string character + (0x00) is reached, returning the result as a string. The file + pointer is incremented until past the end-of-string character. + ''' + data = '' + while True: + char = self.read(1) + if char == '\0': + return data + data += char + + +class wbxmlparser(object): + r'''A DOM parser for Wireless Binary XML documents. + ''' + def __init__(self, applications={}, charsets={}): + r'''Creates a new parser object. + ''' + self.__applications = dict(_applications) + self.__applications.update(applications) + + self.__charsets = dict(_charsets) + self.__charsets.update(charsets) + + self.__encoding = None + self.__page = 0 + self.__strings = [] + + def parse(self, data): + r'''Parses a WBXML file and returns a WBXML DOM document object. + + If data is a string, it is interpreted as a path to a WBXML file; + otherwise, it's expected to be a wbxmlreader object. + ''' + if isinstance(data, basestring): + data = wbxmlreader(data) + + doc = wbxmldocument() + try: + self.__version(data, doc) + self.__publicid(data, doc) + self.__charset(data, doc) + self.__stringtable(data, doc) + self.__body(data, doc) + except Exception as e: + print_exc(file=stdout) + + return doc + + def __get(self, *keys): + r'''Walks the current WBXML token specification, returning the object + (either leaf or subtree) at the end of the path. + + If the path is not found, raises a KeyError exception. + ''' + data = self.__encoding['elements'] + try: + for key in keys: + data = data[key] + return data + except: + raise KeyError('(' + ', '.join([hex(k) for k in keys]) + ')') + + def __version(self, data, doc): + r'''Sets the version attribute of a WBXML DOM document object. + ''' + token = data.read() + minor = 0b1111 & token + major = (token >> 4) + 1 + doc.version = `major` + '.' + `minor` + + def __publicid(self, data, doc): + r'''Sets the schema attribute of a WBXML DOM document object. Also sets + the active WBXML token specification. + ''' + token = data.read() + self.__encoding = self.__applications[token] + doc.schema = self.__encoding['dtd'] + + def __charset(self, data, doc): + r'''Sets the encoding attribute of a WBXML DOM document object. + ''' + token = data.read() + doc.encoding = self.__charsets[token] + + def __stringtable(self, data, doc): + r'''Sets the string table of a WBXML DOM document object. + ''' + length = data.read() + self.__strings = [data.read() for i in range(0, length)] + doc.stringtable = self.__strings + + def __readstringtable(self, offset): + table = self.__strings + string = '' + for i in range(offset, len(table)): + code = table[i] + if code != 0x00: + string += chr(code) + else: + break + + return string + + def __body(self, data, doc): + r'''Parses the body of a WBXML document, constructing the element DOM + tree. + ''' + self.__elements(data, doc) + + def __elements(self, data, parent): + r'''Parses the children of a parent WBXML element, as well as their + children recursively. + ''' + for token in data: + node = None + if token == END: + return + elif token == STR_I: + node = wbxmlstring(data.readstring()) + elif token == OPAQUE: + node = wbxmlstring(data.readopaque()) + elif token == SWITCH_PAGE: + self.__page = data.read() + continue + else: + (tag, hasattributes, hascontents) = ( + (0b00111111 & token), # Base tag code + ((0b10000000 & token) >> 7) == 1, # "Has attributes" bit + ((0b01000000 & token) >> 6) == 1 # "Has contents" bit + ) + + name = self.__get(self.__page, tag, 0) + node = wbxmlelement(name) + if hasattributes: + self.__attributes(data, tag, node) + if hascontents: + self.__elements(data, node) + parent.addchild(node) + + def __attributes(self, data, element, node): + r'''Parses the attributes of a WBXML element. + ''' + for token in data: + if token == END: + return + elif token == SWITCH_PAGE: + self.__page = data.read() + else: + self.__value(data, element, token, node) + + def __value(self, data, element, attribute, node): + (name, value) = self.__get(self.__page, element, 1, attribute) + if value != None and not (isinstance(value, dict) or callable(value)): + node.attributes[name] = value + return + + token = data.read() + if token == STR_I: + node.attributes[name] = data.readstring() + elif token == STR_T: + offset = data.read() + node.attributes[name] = self.__readstringtable(offset) + elif value == None: + node.attributes[name] = str(token) + elif isinstance(value, dict): + node.attributes[name] = value[token] + else: + node.attributes[name] = value(node, token) + + +def dialog(): + r'''Opens the input and output file dialogs, then calls the parse() function. + ''' + from Tkinter import Tk + import tkFileDialog + root = Tk() + root.withdraw() + + from sys import stdin, stdout + + stdout.write('Path to the input WBXML file: ') + + binary = tkFileDialog.askopenfilename( + master = root, + title = 'Open WBXML File', + filetypes = [('Wireless Binary XML', '.wbxml'), ('All Files', '*')] + ) + + if binary == '': + root.quit() + return + + stdout.write(binary + '\n\n') + + stdout.write('Path to the output plain-text XML file: ') + + plain = tkFileDialog.asksaveasfilename( + master = root, + title = "Save Plain-Text XML File", + defaultextension = ".xml", + filetypes = [('Plain-Text XML', '.xml'), ('All Files', '*')] + ) + + if plain == '': + root.quit() + return + + stdout.write(plain + '\n\n') + + root.quit() + + stdout.write('Decoding WBXML file... ') + + parse(binary, plain) + + stdout.write('Done.') + stdin.read() + + +def parse(binary, plain = None): + r'''Parses an input WBXML file. Results are written to a plain-text output + file if it is given; otherwise, the standard output is used. + ''' + wbxml = wbxmlparser().parse(binary) + out = open(plain, 'w') if plain != None else stdout + out.write(str(wbxml)) + if hasattr(out, 'close'): + out.close() + + +def main(): + r'''Function invoked when this module is ran as a script. + ''' + import sys + if len(sys.argv) > 1: + parse(*sys.argv[1:]) + else: + dialog() + + +# Command-line entry point +if __name__ == '__main__': + main() diff --git a/peas/peas.py b/peas/peas.py new file mode 100644 index 0000000..7e35149 --- /dev/null +++ b/peas/peas.py @@ -0,0 +1,150 @@ +__author__ = 'Adam Rutherford' + +import requests +from requests.packages.urllib3.exceptions import InsecureRequestWarning + +import py_eas_helper +import py_activesync_helper + + +PY_ACTIVE_SYNC = 1 +PY_EAS_CLIENT = 2 + + +class Peas: + + def __init__(self): + self._backend = PY_ACTIVE_SYNC + + self._creds = { + 'server': None, + 'user': None, + 'password': None, + 'domain': None, # This could be optional. + 'device_id': None, # This could be optional. + } + + requests.packages.urllib3.disable_warnings(InsecureRequestWarning) + + def set_backend(self, backend_id): + """Set which backend library to use.""" + + assert(backend_id in [PY_ACTIVE_SYNC, PY_EAS_CLIENT]) + + self._backend = backend_id + + def set_creds(self, creds): + """Configure which exchange server, credentials and other settings to use.""" + self._creds.update(creds) + + def extract_emails_py_active_sync(self): + emails = py_activesync_helper.extract_emails(self._creds) + return emails + + def extract_emails_py_eas_client(self): + + emails = py_eas_helper.extract_emails(self._creds) + return emails + + def extract_emails(self): + """Retrieve and return emails.""" + + if self._backend == PY_ACTIVE_SYNC: + return self.extract_emails_py_active_sync() + + if self._backend == PY_EAS_CLIENT: + return self.extract_emails_py_eas_client() + + # TODO: This returns a response object. Make it a public method when it returns something more generic. + def _get_options(self): + + assert self._backend == PY_ACTIVE_SYNC + + as_conn = py_activesync_helper.ASHTTPConnector(self._creds['server']) #e.g. "as.myserver.com" + as_conn.set_credential(self._creds['user'], self._creds['password']) + return as_conn.get_options() + + def check_auth(self): + """Perform an OPTIONS request which will fail if the credentials are incorrect. + + 401 Unauthorized is returned if the credentials are incorrect but other status codes may be possible, + leading to false negatives. + """ + + resp = self._get_options() + return resp.status == 200 + + def disable_certificate_verification(self): + + assert self._backend == PY_ACTIVE_SYNC + + py_activesync_helper.disable_certificate_verification() + + def get_server_headers(self): + """Get the ActiveSync web server headers.""" + + sess = requests.Session() + + url = 'https://' + self._creds['server'] + '/Microsoft-Server-ActiveSync' + + # TODO: Allow user to specify if SSL is verified. + resp = sess.get(url, verify=False) + + return resp.headers + + def get_unc_listing(self, unc_path): + """Retrieve and return a file listing of the given UNC path.""" + + assert self._backend == PY_ACTIVE_SYNC + + # Use alternative credentials for SMB if supplied. + user = self._creds.get('smb_user', self._creds['user']) + password = self._creds.get('smb_password', self._creds['password']) + + # Enable the option to send no credentials at all. + if user == '': + user = None + if password == '': + password = None + + results = py_activesync_helper.get_unc_listing(self._creds, unc_path, + username=user, password=password) + + return results + + def get_unc_file(self, unc_path): + """Return the file data of the file at the given UNC path.""" + + assert self._backend == PY_ACTIVE_SYNC + + # Use alternative credentials for SMB if supplied. + user = self._creds.get('smb_user', self._creds['user']) + password = self._creds.get('smb_password', self._creds['password']) + + # Enable the option to send no credentials at all. + if user == '': + user = None + if password == '': + password = None + + data = py_activesync_helper.get_unc_file(self._creds, unc_path, + username=user, password=password) + + return data + + +def show_banner(): + print(r''' _ __ ___ __ _ ___ +| '_ \ / _ \/ _' / __| +| |_) | __/ (_| \__ \ +| .__/ \___|\__._|___/ +|_| - Probe ActiveSync +''') + + +def main(): + show_banner() + + +if __name__ == '__main__': + main() diff --git a/peas/pyActiveSync/__init__.py b/peas/pyActiveSync/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/peas/pyActiveSync/client/FolderCreate.py b/peas/pyActiveSync/client/FolderCreate.py new file mode 100644 index 0000000..338f984 --- /dev/null +++ b/peas/pyActiveSync/client/FolderCreate.py @@ -0,0 +1,63 @@ +######################################################################## +# Copyright (C) 2013 Sol Birnbaum +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +######################################################################## + +from ..utils.wapxml import wapxmltree, wapxmlnode + +class FolderCreate: + """http://msdn.microsoft.com/en-us/library/gg650949(v=exchg.80).aspx""" + + @staticmethod + def build(synckey, parent_id, display_name, _type): + foldercreate_xmldoc_req = wapxmltree() + xmlrootnode = wapxmlnode("FolderCreate") + foldercreate_xmldoc_req.set_root(xmlrootnode, "folderhierarchy") + xmlsynckeynode = wapxmlnode("SyncKey", xmlrootnode, synckey) + xmlparentidnode = wapxmlnode("ParentId", xmlrootnode, parent_id) + xmldisplaynamenode = wapxmlnode("DisplayName", xmlrootnode, display_name) + xmltypenode = wapxmlnode("Type", xmlrootnode, _type) #See objects.MSASCMD.FolderHierarchy.FolderCreate.Type + return foldercreate_xmldoc_req + + @staticmethod + def parse(wapxml): + + namespace = "folderhierarchy" + root_tag = "FolderCreate" + + root_element = wapxml.get_root() + if root_element.get_xmlns() != namespace: + raise AttributeError("Xmlns '%s' submitted to '%s' parser. Should be '%s'." % (root_element.get_xmlns(), root_tag, namespace)) + if root_element.tag != root_tag: + raise AttributeError("Root tag '%s' submitted to '%s' parser. Should be '%s'." % (root_element.tag, root_tag, root_tag)) + + folderhierarchy_foldercreate_children = root_element.get_children() + + folderhierarchy_foldercreate_status = None + folderhierarchy_foldercreate_synckey = None + folderhierarchy_foldercreate_serverid = None + + for element in folderhierarchy_foldercreate_children: + if element.tag is "Status": + folderhierarchy_foldercreate_status = element.text + if folderhierarchy_foldercreate_status != "1": + print "FolderCreate Exception: %s" % folderhierarchy_foldercreate_status + elif element.tag == "SyncKey": + folderhierarchy_foldercreate_synckey = element.text + elif element.tag == "ServerId": + folderhierarchy_foldercreate_serverid = element.text + return (folderhierarchy_foldercreate_status, folderhierarchy_foldercreate_synckey, folderhierarchy_foldercreate_serverid) \ No newline at end of file diff --git a/peas/pyActiveSync/client/FolderDelete.py b/peas/pyActiveSync/client/FolderDelete.py new file mode 100644 index 0000000..c58bd3f --- /dev/null +++ b/peas/pyActiveSync/client/FolderDelete.py @@ -0,0 +1,59 @@ +######################################################################## +# Copyright (C) 2013 Sol Birnbaum +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +######################################################################## + +from ..utils.wapxml import wapxmltree, wapxmlnode + +class FolderDelete: + """http://msdn.microsoft.com/en-us/library/ee201525(v=exchg.80).aspx""" + + @staticmethod + def build(synckey, server_id): + folderdelete_xmldoc_req = wapxmltree() + xmlrootnode = wapxmlnode("FolderDelete") + folderdelete_xmldoc_req.set_root(xmlrootnode, "folderhierarchy") + xmlsynckeynode = wapxmlnode("SyncKey", xmlrootnode, synckey) + xmlserveridnode = wapxmlnode("ServerId", xmlrootnode, server_id) + return folderdelete_xmldoc_req + + @staticmethod + def parse(wapxml): + + namespace = "folderhierarchy" + root_tag = "FolderDelete" + + root_element = wapxml.get_root() + if root_element.get_xmlns() != namespace: + raise AttributeError("Xmlns '%s' submitted to '%s' parser. Should be '%s'." % (root_element.get_xmlns(), root_tag, namespace)) + if root_element.tag != root_tag: + raise AttributeError("Root tag '%s' submitted to '%s' parser. Should be '%s'." % (root_element.tag, root_tag, root_tag)) + + folderhierarchy_folderdelete_children = root_element.get_children() + + folderhierarchy_folderdelete_status = None + folderhierarchy_folderdelete_synckey = None + folderhierarchy_folderdelete_serverid = None + + for element in folderhierarchy_folderdelete_children: + if element.tag is "Status": + folderhierarchy_folderdelete_status = element.text + if folderhierarchy_folderdelete_status != "1": + print "FolderDelete Exception: %s" % folderhierarchy_folderdelete_status + elif element.tag == "SyncKey": + folderhierarchy_folderdelete_synckey = element.text + return (folderhierarchy_folderdelete_status, folderhierarchy_folderdelete_synckey) \ No newline at end of file diff --git a/peas/pyActiveSync/client/FolderSync.py b/peas/pyActiveSync/client/FolderSync.py new file mode 100644 index 0000000..756d3d5 --- /dev/null +++ b/peas/pyActiveSync/client/FolderSync.py @@ -0,0 +1,84 @@ +######################################################################## +# Copyright (C) 2013 Sol Birnbaum +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +######################################################################## + +from ..utils.wapxml import wapxmltree, wapxmlnode + +from ..objects.MSASCMD import FolderHierarchy + +class FolderSync: + """http://msdn.microsoft.com/en-us/library/ee237648(v=exchg.80).aspx""" + + @staticmethod + def build(synckey): + foldersync_xmldoc_req = wapxmltree() + xmlrootnode = wapxmlnode("FolderSync") + foldersync_xmldoc_req.set_root(xmlrootnode, "folderhierarchy") + xmlsynckeynode = wapxmlnode("SyncKey", xmlrootnode, synckey) + return foldersync_xmldoc_req + + @staticmethod + def parse(wapxml): + + namespace = "folderhierarchy" + root_tag = "FolderSync" + + root_element = wapxml.get_root() + if root_element.get_xmlns() != namespace: + raise AttributeError("Xmlns '%s' submitted to '%s' parser. Should be '%s'." % (root_element.get_xmlns(), root_tag, namespace)) + if root_element.tag != root_tag: + raise AttributeError("Root tag '%s' submitted to '%s' parser. Should be '%s'." % (root_element.tag, root_tag, root_tag)) + + folderhierarchy_foldersync_children = root_element.get_children() + if len(folderhierarchy_foldersync_children) > 3: + raise AttributeError("%s response does not conform to any known %s responses." % (root_tag, root_tag)) + #if folderhierarchy_foldersync_children[0].tag != "Collections": + # raise AttributeError("%s response does not conform to any known %s responses." % (root_tag, root_tag)) + + folderhierarchy_foldersync_status = None + folderhierarchy_foldersync_synckey = None + folderhierarchy_foldersync_changes = None + + changes = [] + + for element in folderhierarchy_foldersync_children: + if element.tag is "Status": + folderhierarchy_foldersync_status = element.text + if folderhierarchy_foldersync_status != "1": + print "FolderSync Exception: %s" % folderhierarchy_foldersync_status + elif element.tag == "SyncKey": + folderhierarchy_foldersync_synckey = element.text + elif element.tag == "Changes": + folderhierarchy_foldersync_changes = element.get_children() + folderhierarchy_foldersync_changes_count = int(folderhierarchy_foldersync_changes[0].text) + if folderhierarchy_foldersync_changes_count > 0: + for change_index in range(1, folderhierarchy_foldersync_changes_count+1): + folderhierarchy_foldersync_change_element = folderhierarchy_foldersync_changes[change_index] + folderhierarchy_foldersync_change_childern = folderhierarchy_foldersync_change_element.get_children() + new_change = FolderHierarchy.Folder() + for element in folderhierarchy_foldersync_change_childern: + if element.tag == "ServerId": + new_change.ServerId = element.text + elif element.tag == "ParentId": + new_change.ParentId = element.text + elif element.tag == "DisplayName": + new_change.DisplayName = element.text + elif element.tag == "Type": + new_change.Type = element.text + changes.append((folderhierarchy_foldersync_change_element.tag, new_change)) + return (changes, folderhierarchy_foldersync_synckey, folderhierarchy_foldersync_status) \ No newline at end of file diff --git a/peas/pyActiveSync/client/FolderUpdate.py b/peas/pyActiveSync/client/FolderUpdate.py new file mode 100644 index 0000000..515844a --- /dev/null +++ b/peas/pyActiveSync/client/FolderUpdate.py @@ -0,0 +1,61 @@ +######################################################################## +# Copyright (C) 2013 Sol Birnbaum +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +######################################################################## + +from ..utils.wapxml import wapxmltree, wapxmlnode + +class FolderUpdate: + """http://msdn.microsoft.com/en-us/library/ee160573(v=exchg.80).aspx""" + + @staticmethod + def build(synckey, server_id, parent_id, display_name): + folderupdate_xmldoc_req = wapxmltree() + xmlrootnode = wapxmlnode("FolderUpdate") + folderupdate_xmldoc_req.set_root(xmlrootnode, "folderhierarchy") + xmlsynckeynode = wapxmlnode("SyncKey", xmlrootnode, synckey) + xmlserveridnode = wapxmlnode("ServerId", xmlrootnode, server_id) + xmlparentidnode = wapxmlnode("ParentId", xmlrootnode, parent_id) + xmldisplaynamenode = wapxmlnode("DisplayName", xmlrootnode, display_name) + return folderupdate_xmldoc_req + + @staticmethod + def parse(wapxml): + + namespace = "folderhierarchy" + root_tag = "FolderUpdate" + + root_element = wapxml.get_root() + if root_element.get_xmlns() != namespace: + raise AttributeError("Xmlns '%s' submitted to '%s' parser. Should be '%s'." % (root_element.get_xmlns(), root_tag, namespace)) + if root_element.tag != root_tag: + raise AttributeError("Root tag '%s' submitted to '%s' parser. Should be '%s'." % (root_element.tag, root_tag, root_tag)) + + folderhierarchy_folderupdate_children = root_element.get_children() + + folderhierarchy_folderupdate_status = None + folderhierarchy_folderupdate_synckey = None + folderhierarchy_folderupdate_serverid = None + + for element in folderhierarchy_folderupdate_children: + if element.tag is "Status": + folderhierarchy_folderupdate_status = element.text + if folderhierarchy_folderupdate_status != "1": + print "FolderUpdate Exception: %s" % folderhierarchy_folderupdate_status + elif element.tag == "SyncKey": + folderhierarchy_folderupdate_synckey = element.text + return (folderhierarchy_folderupdate_status, folderhierarchy_folderupdate_synckey) \ No newline at end of file diff --git a/peas/pyActiveSync/client/GetAttachment.py b/peas/pyActiveSync/client/GetAttachment.py new file mode 100644 index 0000000..f68659a --- /dev/null +++ b/peas/pyActiveSync/client/GetAttachment.py @@ -0,0 +1,30 @@ +######################################################################## +# Copyright (C) 2013 Sol Birnbaum +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +######################################################################## + +from ..utils.wapxml import wapxmltree, wapxmlnode + +class GetAttachment: + """http://msdn.microsoft.com/en-us/library/ee218451(v=exchg.80).aspx""" + @staticmethod + def build(*arg): + raise NotImplementedError("GetAttachment is just an HTTP Post with 'AttachmentName=' as a command parameter. No wapxml building necessary.") + + @staticmethod + def parse(*arg): + raise NotImplementedError("GetAttachment is just an HTTP Post with 'AttachmentName=' as a command parameter. No wapxml parsing necessary.") diff --git a/peas/pyActiveSync/client/GetItemEstimate.py b/peas/pyActiveSync/client/GetItemEstimate.py new file mode 100644 index 0000000..33eb734 --- /dev/null +++ b/peas/pyActiveSync/client/GetItemEstimate.py @@ -0,0 +1,93 @@ +######################################################################## +# Copyright (C) 2013 Sol Birnbaum +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +######################################################################## + +from ..utils.wapxml import wapxmltree, wapxmlnode + +class GetItemEstimate: + class getitemestimate_response: + def __init__(self): + self.Status = None + self.CollectionId = None + self.Estimate = None + + @staticmethod + def build(synckeys, collection_ids, options): + getitemestimate_xmldoc_req = wapxmltree() + xmlrootgetitemestimatenode = wapxmlnode("GetItemEstimate") + getitemestimate_xmldoc_req.set_root(xmlrootgetitemestimatenode, "getitemestimate") + + xmlcollectionsnode = wapxmlnode("Collections", xmlrootgetitemestimatenode) + + for collection_id in collection_ids: + xml_Collection_node = wapxmlnode("Collection", xmlcollectionsnode) + try: + xml_gie_airsyncSyncKey_node = wapxmlnode("airsync:SyncKey", xml_Collection_node, synckeys[collection_id]) + except KeyError: + xml_gie_airsyncSyncKey_node = wapxmlnode("airsync:SyncKey", xml_Collection_node, "0") + xml_gie_CollectionId_node = wapxmlnode("CollectionId", xml_Collection_node, collection_id)#? + if options[collection_id].has_key("ConversationMode"): + xml_gie_ConverationMode_node = wapxmlnode("airsync:ConversationMode", xml_Collection_node, options[collection_id]["ConversationMode"])#? + xml_gie_airsyncOptions_node = wapxmlnode("airsync:Options", xml_Collection_node) + 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 + if options[collection_id].has_key("FilterType"): + 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 + if options[collection_id].has_key("MaxItems"): + 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 + return getitemestimate_xmldoc_req + + @staticmethod + def parse(wapxml): + + namespace = "getitemestimate" + root_tag = "GetItemEstimate" + + root_element = wapxml.get_root() + if root_element.get_xmlns() != namespace: + raise AttributeError("Xmlns '%s' submitted to '%s' parser. Should be '%s'." % (root_element.get_xmlns(), root_tag, namespace)) + if root_element.tag != root_tag: + raise AttributeError("Root tag '%s' submitted to '%s' parser. Should be '%s'." % (root_element.tag, root_tag, root_tag)) + + getitemestimate_getitemestimate_children = root_element.get_children() + + #getitemestimate_responses = getitemestimate_getitemestimate_children.get_children() + + responses = [] + + for getitemestimate_response_child in getitemestimate_getitemestimate_children: + response = GetItemEstimate.getitemestimate_response() + if getitemestimate_response_child.tag is "Status": + response.Status = getitemestimate_response_child.text + for element in getitemestimate_response_child: + if element.tag is "Status": + response.Status = element.text + elif element.tag == "Collection": + getitemestimate_collection_children = element.get_children() + collection_id = 0 + estimate = 0 + for collection_child in getitemestimate_collection_children: + if collection_child.tag == "CollectionId": + response.CollectionId = collection_child.text + elif collection_child.tag == "Estimate": + response.Estimate = collection_child.text + responses.append(response) + return responses + + + + diff --git a/peas/pyActiveSync/client/ItemOperations.py b/peas/pyActiveSync/client/ItemOperations.py new file mode 100644 index 0000000..f8aa02d --- /dev/null +++ b/peas/pyActiveSync/client/ItemOperations.py @@ -0,0 +1,174 @@ +######################################################################## +# Copyright (C) 2013 Sol Birnbaum +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +######################################################################## + +from ..utils.wapxml import wapxmltree, wapxmlnode + +from ..objects.MSASAIRS import airsyncbase_Body, airsyncbase_BodyPart + +class ItemOperations: + """http://msdn.microsoft.com/en-us/library/ee202415(v=exchg.80).aspx""" + + @staticmethod + def build(operations): + itemoperations_xmldoc_req = wapxmltree() + xmlrootnode = wapxmlnode("ItemOperations") + itemoperations_xmldoc_req.set_root(xmlrootnode, "itemoperations") + + for operation in range(0,len(operations)): + if operations[operation]["Name"] == "EmptyFolderContents": + xmlemptyfoldercontentsnode = wapxmlnode("EmptyFolderContents", xmlrootnode) + xmlcollectionidnode = wapxmlnode("airsync:CollectionId", xmlemptyfoldercontentsnode, operations[operation]["CollectionId"]) + if operations[operation].has_key("DeleteSubFolders"): + xmloptionsnode = wapxmlnode("Options", xmlemptyfoldercontentsnode) + xmldeletesubfoldersnode = wapxmlnode("DeleteSubFolders", xmloptionsnode, operations[operation]["DeleteSubFolders"]) + + elif operations[operation]["Name"] == "Fetch": + xmlfetchnode = wapxmlnode("Fetch", xmlrootnode) + xmloptionsnode = wapxmlnode("Store", xmlfetchnode, operations[operation]["Store"]) + if operations[operation].has_key("LinkId"): + xmllinkidnode = wapxmlnode("documentlibrary:LinkId", xmlfetchnode, operations[operation]["LinkId"]) #URI of document + if operations[operation].has_key("LongId"): + xmllongidnode = wapxmlnode("search:LongId", xmlfetchnode, operations[operation]["LongId"]) + if operations[operation].has_key("CollectionId"): + xmlcollectionidnode = wapxmlnode("airsync:CollectionId", xmlfetchnode, operations[operation]["CollectionId"]) + if operations[operation].has_key("ServerId"): + xmlserveridnode = wapxmlnode("airsync:ServerId", xmlfetchnode, operations[operation]["ServerId"]) + if operations[operation].has_key("FileReference"): + xmlfilereferencenode = wapxmlnode("airsyncbase:FileReference", xmlfetchnode, operations[operation]["FileReference"]) #only range option can be specified + if operations[operation].has_key("RemoveRightsManagementProtection"): + xmlremovermnode = wapxmlnode("rm:RemoveRightsManagementProtection", xmlfetchnode) #Empty element + if len(xmlfetchnode.get_children()) < 2: #let's make sure one of the above item locations was supplied + raise AttributeError("ItemOperations Fetch: No item to be fetched supplied.") + + xmloptionsnode = wapxmlnode("Options") + if operations[operation].has_key("Schema"): + xmlschemanode = wapxmlnode("Schema", xmloptionsnode, operations[operation]["Schema"]) #fetch only specific properties of an item. mailbox store only. cannot use for attachments. + if operations[operation].has_key("Range"): + xmlrangenode = wapxmlnode("Range", xmloptionsnode, operations[operation]["Range"]) #select bytes is only for documents and attachments + if operations[operation].has_key("UserName"): #select username and password to use for fetch. i imagine this is only for documents. + if not operations[operation].has_key("Password"): + raise AttributeError("ItemOperations Fetch: Username supplied for fetch operation, but no password supplied. Aborting.") + return + xmlusernamenode = wapxmlnode("UserName", xmloptionsnode, operations[operation]["UserName"]) #username to use for fetch + xmlpasswordnode = wapxmlnode("Password", xmloptionsnode, operations[operation]["Password"]) #password to use for fetch + if operations[operation].has_key("MIMESupport"): + xmlmimesupportnode = wapxmlnode("airsync:MIMESupport", xmloptionsnode, operations[operation]["MIMESupport"]) #objects.MSASAIRS.airsync_MIMESupport + if operations[operation].has_key("BodyPreference"): + xmlbodypreferencenode = wapxmlnode("airsyncbase:BodyPreference", xmloptionsnode, operations[operation]["BodyPreference"]) + if operations[operation].has_key("BodyPartPreference"): + xmlbodypartpreferencenode = wapxmlnode("airsyncbase:BodyPartPreference", xmloptionsnode, operations[operation]["BodyPartPreference"]) + if operations[operation].has_key("RightsManagementSupport"): + xmlrmsupportnode = wapxmlnode("rm:RightsManagementSupport", xmloptionsnode, operations[operation]["RightsManagementSupport"])#1=Supports RM. Decrypt message before send. 2=Do not decrypt message before send + if len(xmloptionsnode.get_children()) > 0: + xmloptionsnode.set_parent(xmlfetchnode) + + elif operations[operation]["Name"] == "Move": + xmlmovenode = wapxmlnode("Move", xmlrootnode) + xmlconversationidnode = wapxmlnode("ConversationId", xmlmovenode, operations[operation]["ConversationId"]) + xmldstfldidnode = wapxmlnode("DstFldId", xmlmovenode, operations[operation]["DstFldId"]) + if operations[operation].has_key("MoveAlways"): + xmloptionsnode = wapxmlnode("Options", xmlmovenode) + xmlmovealwaysnode = wapxmlnode("MoveAlways", xmloptionsnode, operations[operation]["MoveAlways"]) #also move future emails in this conversation to selected folder. + else: + raise AttributeError("Unknown operation %s submitted to ItemOperations wapxml builder." % operation) + + return itemoperations_xmldoc_req + + @staticmethod + def parse(wapxml): + + namespace = "itemoperations" + root_tag = "ItemOperations" + + root_element = wapxml.get_root() + if root_element.get_xmlns() != namespace: + raise AttributeError("Xmlns '%s' submitted to '%s' parser. Should be '%s'." % (root_element.get_xmlns(), root_tag, namespace)) + if root_element.tag != root_tag: + raise AttributeError("Root tag '%s' submitted to '%s' parser. Should be '%s'." % (root_element.tag, root_tag, root_tag)) + + itemoperations_itemoperations_children = root_element.get_children() + + itemoperations_itemoperations_status = None + + responses = [] + + for element in itemoperations_itemoperations_children: + if element.tag is "Status": + itemoperations_itemoperations_status = element.text + if itemoperations_itemoperations_status != "1": + print "FolderSync Exception: %s" % itemoperations_itemoperations_status + elif element.tag == "Response": + response_elements = element.get_children() + for response_element in response_elements: + if response_element.tag == "EmptyFolderContents": + efc_elements = response_element.get_children() + for efc_element in efc_elements: + if efc_element.tag == "Status": + efc_status = efc_element.text + elif efc_element.tag == "airsync:CollectionId": + efc_collectionid = efc_element.text + responses.append(("EmptyFolderContents", efc_status, efc_collectionid)) + elif response_element.tag == "Fetch": + fetch_elements = response_element.get_children() + fetch_id = None + fetch_properties = None + fetch_class = None + for fetch_element in fetch_elements: + if fetch_element.tag == "Status": + fetch_status = fetch_element.text + elif fetch_element.tag == "search:LongId": + fetch_id = fetch_element.text + elif fetch_element.tag == "airsync:CollectionId": + fetch_id = fetch_element.text + elif fetch_element.tag == "airsync:ServerId": + fetch_id = fetch_element.text + elif fetch_element.tag == "documentlibrary:LinkId": + fetch_id = fetch_element.text + elif fetch_element.tag == "airsync:Class": + fetch_class = fetch_element.text + elif fetch_element.tag == "Properties": + property_elements = fetch_element.get_children() + fetch_properties = {} + for property_element in property_elements: + if property_element.tag == "Range": + fetch_properties.update({ "Range" : property_element.text }) + elif property_element.tag == "Data": + fetch_properties.update({ "Data" : property_element.text }) + elif property_element.tag == "Part": + fetch_properties.update({ "Part" : property_element.text }) + elif property_element.tag == "Version": #datetime + fetch_properties.update({ "Version" : property_element.text }) + elif property_element.tag == "Total": + fetch_properties.update({ "Total" : property_element.text }) + elif property_element.tag == "airsyncbase:Body": + fetch_properties.update({ "Body" : airsyncbase_Body(property_element) }) + elif property_element.tag == "airsyncbase:BodyPart": + fetch_properties.update({ "BodyPart" : airsyncbase_BodyPart(property_element) }) + elif property_element.tag == "rm:RightsManagementLicense": + fetch_properties.update({ "RightsManagementLicense" : property_element }) #need to create rm license parser + responses.append(("Fetch", fetch_status, fetch_id, fetch_properties, fetch_class)) + elif response_element.tag == "Move": + move_elements = response_element.get_children() + for move_element in move_elements: + if move_element.tag == "Status": + move_status = move_element.text + elif move_element.tag == "ConversationId": + move_conversationid = move_element.text + responses.append(("Move", move_status, move_conversationid)) + return responses \ No newline at end of file diff --git a/peas/pyActiveSync/client/MeetingResponse.py b/peas/pyActiveSync/client/MeetingResponse.py new file mode 100644 index 0000000..ffabea0 --- /dev/null +++ b/peas/pyActiveSync/client/MeetingResponse.py @@ -0,0 +1,75 @@ +######################################################################## +# Copyright (C) 2013 Sol Birnbaum +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +######################################################################## + +from ..utils.wapxml import wapxmltree, wapxmlnode + +class MeetingResponse: + """description of class""" + + @staticmethod + def build(responses): + meetingresponse_xmldoc_req = wapxmltree() + xmlrootnode = wapxmlnode("MeetingResponse") + meetingresponse_xmldoc_req.set_root(xmlrootnode, "meetingresponse") + for response in responses: + xmlrequestnode = wapxmlnode("Request", xmlrootnode) + xmluserresponsenode = wapxmlnode("UserResponse", xmlrequestnode, response["UserResponse"]) + if response.has_Key("CollectionId"): + xmlcollectionidnode = wapxmlnode("CollectionId", xmlrequestnode, response["CollectionId"]) + xmlrequestidnode = wapxmlnode("RequestId", xmlrequestnode, response["RequestId"]) + elif response.has_Key("LongId"): + xmllongidnode = wapxmlnode("search:LongId", xmlrequestnode, response["LongId"]) + else: + raise AttributeError("MeetingResponse missing meeting id") + xmlinstanceidnode = wapxmlnode("InstanceId", xmlrequestnode, response["InstanceId"]) + return meetingresponse_xmldoc_req + + @staticmethod + def parse(wapxml): + + namespace = "meetingresponse" + root_tag = "MeetingResponse" + + root_element = wapxml.get_root() + if root_element.get_xmlns() != namespace: + raise AttributeError("Xmlns '%s' submitted to '%s' parser. Should be '%s'." % (root_element.get_xmlns(), root_tag, namespace)) + if root_element.tag != root_tag: + raise AttributeError("Root tag '%s' submitted to '%s' parser. Should be '%s'." % (root_element.tag, root_tag, root_tag)) + + meetingresponse_meetingresponse_children = root_element.get_children() + + responses = [] + + for element in meetingresponse_meetingresponse_children: + if element.tag is "Result": + result_elements = element.get_children() + for result_element in result_elements: + request_id = None + calendar_id = None + if result_element.tag == "RequestId": + request_id = result_element.text + elif result_element.tag == "Status": + status = result_element.text + elif result_element.tag == "CalendarId": + calendar_id = result_element.text + responses.append(status, request_id, calendar_id) + else: + raise AttributeError("MeetingResponse error. Server returned unknown element instead of 'Result'.") + return responses + diff --git a/peas/pyActiveSync/client/MoveItems.py b/peas/pyActiveSync/client/MoveItems.py new file mode 100644 index 0000000..e96cb84 --- /dev/null +++ b/peas/pyActiveSync/client/MoveItems.py @@ -0,0 +1,73 @@ +######################################################################## +# Copyright (C) 2013 Sol Birnbaum +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +######################################################################## + +from ..utils.wapxml import wapxmltree, wapxmlnode + +class MoveItems(object): + """http://msdn.microsoft.com/en-us/library/gg675499(v=exchg.80).aspx""" + + @staticmethod + def build(moves): + if len(moves) < 1: + raise AttributeError("MoveItems builder: No moves supplied to MoveItems request builder.") + moveitems_xmldoc_req = wapxmltree() + xmlrootnode = wapxmlnode("MoveItems") + moveitems_xmldoc_req.set_root(xmlrootnode, "move") + for move in moves: + xmlmovenode = wapxmlnode("Move", xmlrootnode) + src_msg_id, src_fld_id, dst_fld_id = move + xmlsrcmsgidnode = wapxmlnode("SrcMsgId", xmlmovenode, src_msg_id) + xmlsrcfldidnode = wapxmlnode("SrcFldId", xmlmovenode, src_fld_id) + xmldstfldidnode = wapxmlnode("DstFldId", xmlmovenode, dst_fld_id) + return moveitems_xmldoc_req + + @staticmethod + def parse(wapxml): + + namespace = "move" + root_tag = "MoveItems" + + root_element = wapxml.get_root() + if root_element.get_xmlns() != namespace: + raise AttributeError("Xmlns '%s' submitted to '%s' parser. Should be '%s'." % (root_element.get_xmlns(), root_tag, namespace)) + if root_element.tag != root_tag: + raise AttributeError("Root tag '%s' submitted to '%s' parser. Should be '%s'." % (root_element.tag, root_tag, root_tag)) + + move_moveitems_children = root_element.get_children() + + responses = [] + + for response_element in move_moveitems_children: + src_id = "" + status = "" + dst_id = "" + for element in response_element: + if element.tag is "Status": + status = element.text + if status != "3": + print "MoveItems Exception: %s" % status + elif element.tag == "SrcMsgId": + src_id = element.text + elif element.tag == "DstMsgId": + dst_id = element.text + responses.append((src_id, status, dst_id)) + + return responses + + diff --git a/peas/pyActiveSync/client/Ping.py b/peas/pyActiveSync/client/Ping.py new file mode 100644 index 0000000..96817ee --- /dev/null +++ b/peas/pyActiveSync/client/Ping.py @@ -0,0 +1,74 @@ +######################################################################## +# Copyright (C) 2013 Sol Birnbaum +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +######################################################################## + +from ..utils.wapxml import wapxmltree, wapxmlnode + +class Ping(object): + """http://msdn.microsoft.com/en-us/library/gg675609(v=exchg.80).aspx""" + + @staticmethod + def build(heatbeat_interval="30", folders=None): + if not folders: + raise AttributeError("Ping builder: No folders included in ping request builder. Must ping at least one folder.") + ping_xmldoc_req = wapxmltree() + xmlrootnode = wapxmlnode("Ping") + ping_xmldoc_req.set_root(xmlrootnode, "ping") + xmlheartbeatintervalnode = wapxmlnode("HeartbeatInterval", xmlrootnode, heatbeat_interval) + xmlfoldersnode = wapxmlnode("Folders", xmlrootnode) + for folder in folders: + xmlfoldernode = wapxmlnode("Folder", xmlfoldersnode) + xmlidnode = wapxmlnode("Id", xmlfoldernode, folder[0]) + xmlclassnode = wapxmlnode("Class", xmlfoldernode, folder[1]) + return ping_xmldoc_req + + @staticmethod + def parse(wapxml): + + namespace = "ping" + root_tag = "Ping" + + root_element = wapxml.get_root() + if root_element.get_xmlns() != namespace: + raise AttributeError("Xmlns '%s' submitted to '%s' parser. Should be '%s'." % (root_element.get_xmlns(), root_tag, namespace)) + if root_element.tag != root_tag: + raise AttributeError("Root tag '%s' submitted to '%s' parser. Should be '%s'." % (root_element.tag, root_tag, root_tag)) + + ping_ping_children = root_element.get_children() + + heartbeat_interval = "" + status = "" + folders = [] + max_folders = "" + + for element in ping_ping_children: + if element.tag is "Status": + status = element.text + if (status != "1") and (status != "2"): + print "Ping Exception: %s" % status + elif element.tag == "HeartbeatInterval": + heartbeat_interval = element.text + elif element.tag == "MaxFolders": + max_folders = element.text + elif element.tag == "Folders": + folders_list = element.get_children() + if len(folders_list) > 0: + for folder in folders_list: + folders.append(folder.text) + return (status, heartbeat_interval, max_folders, folders) + diff --git a/peas/pyActiveSync/client/Provision.py b/peas/pyActiveSync/client/Provision.py new file mode 100644 index 0000000..b80fbc0 --- /dev/null +++ b/peas/pyActiveSync/client/Provision.py @@ -0,0 +1,184 @@ +######################################################################## +# Copyright (C) 2013 Sol Birnbaum +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +######################################################################## + +from ..utils.wapxml import wapxmltree, wapxmlnode + +class Provision: + """http://msdn.microsoft.com/en-us/library/ff850179(v=exchg.80).aspx""" + + @staticmethod + def build(policykey, settings=None): + provision_xmldoc_req = wapxmltree() + xmlrootnode = wapxmlnode("Provision") + provision_xmldoc_req.set_root(xmlrootnode, "provision") + + if policykey == "0": + xml_settings_deviceinformation_node = wapxmlnode("settings:DeviceInformation", xmlrootnode) + xml_settings_set_node = wapxmlnode("settings:Set", xml_settings_deviceinformation_node) + xml_settings_model_node = wapxmlnode("settings:Model", xml_settings_set_node, settings["Model"]) + xml_settings_model_node = wapxmlnode("settings:IMEI", xml_settings_set_node, settings["IMEI"]) + xml_settings_model_node = wapxmlnode("settings:FriendlyName", xml_settings_set_node, settings["FriendlyName"]) + xml_settings_model_node = wapxmlnode("settings:OS", xml_settings_set_node, settings["OS"]) + xml_settings_model_node = wapxmlnode("settings:OSLanguage", xml_settings_set_node, settings["OSLanguage"]) + xml_settings_model_node = wapxmlnode("settings:PhoneNumber", xml_settings_set_node, settings["PhoneNumber"]) + xml_settings_model_node = wapxmlnode("settings:MobileOperator", xml_settings_set_node, settings["MobileOperator"]) + xml_settings_model_node = wapxmlnode("settings:UserAgent", xml_settings_set_node, settings["UserAgent"]) + + xml_policies_node = wapxmlnode("Policies", xmlrootnode) + xml_policy_node = wapxmlnode("Policy", xml_policies_node) + xml_policytype_node = wapxmlnode("PolicyType", xml_policy_node, "MS-EAS-Provisioning-WBXML") + else: + xml_policies_node = wapxmlnode("Policies", xmlrootnode) + xml_policy_node = wapxmlnode("Policy", xml_policies_node) + xml_policytype_node = wapxmlnode("PolicyType", xml_policy_node, "MS-EAS-Provisioning-WBXML") + xml_policytype_node = wapxmlnode("PolicyKey", xml_policy_node, policykey) + xml_policytype_node = wapxmlnode("Status", xml_policy_node, "1") + + return provision_xmldoc_req + + @staticmethod + def parse(wapxml): + + namespace = "provision" + root_tag = "Provision" + + root_element = wapxml.get_root() + if root_element.get_xmlns() != namespace: + raise AttributeError("Xmlns '%s' submitted to '%s' parser. Should be '%s'." % (root_element.get_xmlns(), root_tag, namespace)) + if root_element.tag != root_tag: + raise AttributeError("Root tag '%s' submitted to '%s' parser. Should be '%s'." % (root_element.tag, root_tag, root_tag)) + + provison_provison_children = root_element.get_children() + + policy_dict = {} + settings_status = "" + policy_status = "" + policy_key = "0" + policy_type = "" + status = "" + + for element in provison_provison_children: + if element.tag is "Status": + status = element.text + if (status != "1") and (status != "2"): + print "Provision Exception: %s" % status + elif element.tag == "Policies": + policy_elements = element.get_children()[0].get_children() + for policy_element in policy_elements: + if policy_element.tag == "PolicyType": + policy_type = policy_element.text + elif policy_element.tag == "Status": + policy_status = policy_element.text + elif policy_element.tag == "PolicyKey": + policy_key = policy_element.text + elif policy_element.tag == "Data": + eas_provision_elements = policy_element.get_children()[0].get_children() + for eas_provision_element in eas_provision_elements: + if eas_provision_element.tag == "AllowBluetooth": + policy_dict.update({"AllowBluetooth":eas_provision_element.text}) + elif eas_provision_element.tag == "AllowBluetooth": + policy_dict.update({"AllowBluetooth":eas_provision_element.text}) + elif eas_provision_element.tag == "AllowBrowser": + policy_dict.update({"AllowBrowser":eas_provision_element.text}) + elif eas_provision_element.tag == "AllowCamera": + policy_dict.update({"AllowCamera":eas_provision_element.text}) + elif eas_provision_element.tag == "AllowConsumerEmail": + policy_dict.update({"AllowConsumerEmail":eas_provision_element.text}) + elif eas_provision_element.tag == "AllowDesktopSync": + policy_dict.update({"AllowDesktopSync":eas_provision_element.text}) + elif eas_provision_element.tag == "AllowHTMLEmail": + policy_dict.update({"AllowHTMLEmail":eas_provision_element.text}) + elif eas_provision_element.tag == "AllowInternetSharing": + policy_dict.update({"AllowInternetSharing":eas_provision_element.text}) + elif eas_provision_element.tag == "AllowIrDA": + policy_dict.update({"AllowIrDA":eas_provision_element.text}) + elif eas_provision_element.tag == "AllowPOPIMAPEmail": + policy_dict.update({"AllowPOPIMAPEmail":eas_provision_element.text}) + elif eas_provision_element.tag == "AllowRemoteDesktop": + policy_dict.update({"AllowRemoteDesktop":eas_provision_element.text}) + elif eas_provision_element.tag == "AllowSimpleDevicePassword": + policy_dict.update({"AllowSimpleDevicePassword":eas_provision_element.text}) + elif eas_provision_element.tag == "AllowSMIMEEncryptionAlgorithmNegotiation": + policy_dict.update({"AllowSMIMEEncryptionAlgorithmNegotiation":eas_provision_element.text}) + elif eas_provision_element.tag == "AllowSMIMESoftCerts": + policy_dict.update({"AllowSMIMESoftCerts":eas_provision_element.text}) + elif eas_provision_element.tag == "AllowStorageCard": + policy_dict.update({"AllowStorageCard":eas_provision_element.text}) + elif eas_provision_element.tag == "AllowTextMessaging": + policy_dict.update({"AllowTextMessaging":eas_provision_element.text}) + elif eas_provision_element.tag == "AllowUnsignedApplications": + policy_dict.update({"AllowUnsignedApplications":eas_provision_element.text}) + elif eas_provision_element.tag == "AllowUnsignedInstallationPackages": + policy_dict.update({"AllowUnsignedInstallationPackages":eas_provision_element.text}) + elif eas_provision_element.tag == "AllowWifi": + policy_dict.update({"AllowWifi":eas_provision_element.text}) + elif eas_provision_element.tag == "AlphanumericDevicePasswordRequired": + policy_dict.update({"AlphanumericDevicePasswordRequired":eas_provision_element.text}) + elif eas_provision_element.tag == "ApprovedApplicationList": + policy_dict.update({"ApprovedApplicationList":eas_provision_element.text}) + elif eas_provision_element.tag == "AttachmentsEnabled": + policy_dict.update({"AttachmentsEnabled":eas_provision_element.text}) + elif eas_provision_element.tag == "DevicePasswordEnabled": + policy_dict.update({"DevicePasswordEnabled":eas_provision_element.text}) + elif eas_provision_element.tag == "DevicePasswordExpiration": + policy_dict.update({"DevicePasswordExpiration":eas_provision_element.text}) + elif eas_provision_element.tag == "DevicePasswordHistory": + policy_dict.update({"DevicePasswordHistory":eas_provision_element.text}) + elif eas_provision_element.tag == "MaxAttachmentSize": + policy_dict.update({"MaxAttachmentSize":eas_provision_element.text}) + elif eas_provision_element.tag == "MaxCalendarAgeFilter": + policy_dict.update({"MaxCalendarAgeFilter":eas_provision_element.text}) + elif eas_provision_element.tag == "MaxDevicePasswordFailedAttempts": + policy_dict.update({"MaxDevicePasswordFailedAttempts":eas_provision_element.text}) + elif eas_provision_element.tag == "MaxEmailAgeFilter": + policy_dict.update({"MaxEmailAgeFilter":eas_provision_element.text}) + elif eas_provision_element.tag == "MaxEmailBodyTruncationSize": + policy_dict.update({"MaxEmailBodyTruncationSize":eas_provision_element.text}) + elif eas_provision_element.tag == "MaxEmailHTMLBodyTruncationSize": + policy_dict.update({"MaxEmailHTMLBodyTruncationSize":eas_provision_element.text}) + elif eas_provision_element.tag == "MaxInactivityTimeDeviceLock": + policy_dict.update({"MaxInactivityTimeDeviceLock":eas_provision_element.text}) + elif eas_provision_element.tag == "MinDevicePasswordComplexCharacters": + policy_dict.update({"MinDevicePasswordComplexCharacters":eas_provision_element.text}) + elif eas_provision_element.tag == "MinDevicePasswordLength": + policy_dict.update({"MinDevicePasswordLength":eas_provision_element.text}) + elif eas_provision_element.tag == "PasswordRecoveryEnabled": + policy_dict.update({"PasswordRecoveryEnabled":eas_provision_element.text}) + elif eas_provision_element.tag == "RequireDeviceEncryption": + policy_dict.update({"RequireDeviceEncryption":eas_provision_element.text}) + elif eas_provision_element.tag == "RequireEncryptedSMIMEMessages": + policy_dict.update({"RequireEncryptedSMIMEMessages":eas_provision_element.text}) + elif eas_provision_element.tag == "RequireEncryptionSMIMEAlgorithm": + policy_dict.update({"RequireEncryptionSMIMEAlgorithm":eas_provision_element.text}) + elif eas_provision_element.tag == "RequireManualSyncWhenRoaming": + policy_dict.update({"RequireManualSyncWhenRoaming":eas_provision_element.text}) + elif eas_provision_element.tag == "RequireSignedSMIMEAlgorithm": + policy_dict.update({"RequireSignedSMIMEAlgorithm":eas_provision_element.text}) + elif eas_provision_element.tag == "RequireSignedSMIMEMessages": + policy_dict.update({"RequireSignedSMIMEMessages":eas_provision_element.text}) + elif eas_provision_element.tag == "RequireStorageCardEncryption": + policy_dict.update({"RequireStorageCardEncryption":eas_provision_element.text}) + elif eas_provision_element.tag == "UnapprovedInROMApplicationList": + policy_dict.update({"UnapprovedInROMApplicationList":eas_provision_element.text}) + elif element.tag == "settings:DeviceInformation": + device_information_children = element.get_children() + for device_information_element in device_information_children: + if device_information_element == "settings:Status": + settings_status = device_information_element.text + return (status, policy_status, policy_key, policy_type, policy_dict, settings_status) \ No newline at end of file diff --git a/peas/pyActiveSync/client/ResolveRecipients.py b/peas/pyActiveSync/client/ResolveRecipients.py new file mode 100644 index 0000000..608f115 --- /dev/null +++ b/peas/pyActiveSync/client/ResolveRecipients.py @@ -0,0 +1,89 @@ +######################################################################## +# Copyright (C) 2013 Sol Birnbaum +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +######################################################################## + +from ..utils.wapxml import wapxmltree, wapxmlnode + +class ResolveRecipients: + """http://msdn.microsoft.com/en-us/library/gg650949(v=exchg.80).aspx""" + + @staticmethod + 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): + resolverecipients_xmldoc_req = wapxmltree() + xmlrootnode = wapxmlnode("ResolveRecipients") + resolverecipients_xmldoc_req.set_root(xmlrootnode, "resolverecipients") + xmltonode = wapxmlnode("To", xmlrootnode, to) + #xmloptionsnode = wapxmlnode("Options", xmlrootnode) + #xmlcertificateretrievalnode = wapxmlnode("CertificateRetrieval", xmloptionsnode, cert_retrieveal) + #xmlmaxcertificatesnode = wapxmlnode("MaxCertificates", xmloptionsnode, max_certs) #0-9999 + #xmlmaxambiguousrecipientsnode = wapxmlnode("MaxAmbiguousRecipients", xmloptionsnode, max_recipients) #0-9999 + #xmlavailabilitynode = wapxmlnode("Availability", xmloptionsnode) + #xmlstarttimenode = wapxmlnode("StartTime", xmlavailabilitynode, start_time) #datetime + #xmlendtimenode = wapxmlnode("EndTime", xmlavailabilitynode, end_time) #datetime + #xmlpicturenode = wapxmlnode("Picture", xmloptionsnode) + #xmlmaxsizenode = wapxmlnode("MaxSize", xmlpicturenode, max_size) #must be > 100KB + #xmlmaxpicturesnode = wapxmlnode("MaxPictures", xmlpicturenode, max_pictures) + return resolverecipients_xmldoc_req + + @staticmethod + def parse(wapxml): + + namespace = "resolverecipients" + root_tag = "ResolveRecipients" + + root_element = wapxml.get_root() + if root_element.get_xmlns() != namespace: + raise AttributeError("Xmlns '%s' submitted to '%s' parser. Should be '%s'." % (root_element.get_xmlns(), root_tag, namespace)) + if root_element.tag != root_tag: + raise AttributeError("Root tag '%s' submitted to '%s' parser. Should be '%s'." % (root_element.tag, root_tag, root_tag)) + + folderhierarchy_resolverecipients_children = root_element.get_children() + + recipients = [] + + for element in folderhierarchy_resolverecipients_children: + if element.tag is "Status": + folderhierarchy_resolverecipients_status = element.text + if folderhierarchy_resolverecipients_status != "1": + print "ResolveRecipients Status: %s" % folderhierarchy_resolverecipients_status + elif element.tag == "Response": + for response_element in element.get_children(): + if response_element.tag == "To": + response_to = response_element.text + elif response_element.tag == "Status": + response_status = response_element.text + elif response_element.tag == "RecipientCount": + response_count = response_element.text + elif response_element.tag == "Recipient": + response_status = response_element.text + for recipient_element in response_element.get_children(): + if recipient_element.tag == "Type": + recipient_type = recipient_element.text + elif recipient_element.tag == "DisplayName": + recipient_displayname = recipient_element.text + elif recipient_element.tag == "EmailAddress": + recipient_emailaddress = recipient_element.text + elif recipient_element.tag == "Availability": + recipient_availability = recipient_element + elif recipient_element.tag == "Certificates": + recipient_certificates = recipient_element + elif recipient_element.tag == "Picture": + recipient_picture = recipient_element.text + recipients.append((recipient_type, recipient_displayname, recipient_emailaddress, recipient_availability, recipient_certificates, recipient_picture)) + + return (response_status, recipients, response_count) \ No newline at end of file diff --git a/peas/pyActiveSync/client/Search.py b/peas/pyActiveSync/client/Search.py new file mode 100644 index 0000000..b3ffbb0 --- /dev/null +++ b/peas/pyActiveSync/client/Search.py @@ -0,0 +1,92 @@ +######################################################################## +# Created 2016 based on code Copyright (C) 2013 Sol Birnbaum +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +######################################################################## + +from ..utils.wapxml import wapxmltree, wapxmlnode + + +class Search: + """https://msdn.microsoft.com/en-us/library/gg675482(v=exchg.80).aspx + + Currently for DocumentLibrary searches only. + """ + + @staticmethod + def build(unc_path, return_range='0-999', username=None, password=None): + + xmldoc_req = wapxmltree() + xmlrootnode = wapxmlnode("Search") + xmldoc_req.set_root(xmlrootnode, "search") + + store_node = wapxmlnode("Store", xmlrootnode) + + # "GAL" to search the Global Address List. + # "Mailbox" to search the mailbox. + # "DocumentLibrary" to search a Windows SharePoint Services library or a UNC library. + name_node = wapxmlnode("Name", store_node, "DocumentLibrary") + + query_node = wapxmlnode("Query", store_node) + equal_to_node = wapxmlnode("EqualTo", query_node) + link_id = wapxmlnode("documentlibrary:LinkId", equal_to_node) + value_node = wapxmlnode("Value", equal_to_node, unc_path) + + options_node = wapxmlnode("Options", store_node) + range_node = wapxmlnode("Range", options_node, return_range) + + if username is not None: + username_node = wapxmlnode("UserName", options_node, username) + if password is not None: + password_node = wapxmlnode("Password", options_node, password) + + return xmldoc_req + + @staticmethod + def parse(wapxml): + + namespace = "search" + root_tag = "Search" + + root_element = wapxml.get_root() + if root_element.get_xmlns() != namespace: + raise AttributeError("Xmlns '%s' submitted to '%s' parser. Should be '%s'." % (root_element.get_xmlns(), root_tag, namespace)) + if root_element.tag != root_tag: + raise AttributeError("Root tag '%s' submitted to '%s' parser. Should be '%s'." % (root_element.tag, root_tag, root_tag)) + + children = root_element.get_children() + + status = None + results = [] + + for element in children: + if element.tag is "Status": + status = element.text + if status != "1": + print "%s Exception: %s" % (root_tag, status) + elif element.tag == "Response": + + properties = element.basic_xpath('Store/Result/Properties') + for properties_el in properties: + result = {} + properties_children = properties_el.get_children() + for prop_el in properties_children: + prop = prop_el.tag.partition(':')[2] + result[prop] = prop_el.text + results.append(result) + + return status, results + diff --git a/peas/pyActiveSync/client/SendMail.py b/peas/pyActiveSync/client/SendMail.py new file mode 100644 index 0000000..999f466 --- /dev/null +++ b/peas/pyActiveSync/client/SendMail.py @@ -0,0 +1,58 @@ +######################################################################## +# Copyright (C) 2013 Sol Birnbaum +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +######################################################################## + +from ..utils.wapxml import wapxmltree, wapxmlnode + +class SendMail: + """http://msdn.microsoft.com/en-us/library/ee178477(v=exchg.80).aspx""" + + @staticmethod + def build(client_id, mime, account_id=None, save_in_sent_items=True, template_id=None): + sendmail_xmldoc_req = wapxmltree() + xmlrootnode = wapxmlnode("SendMail") + sendmail_xmldoc_req.set_root(xmlrootnode, "composemail") + xml_clientid_node = wapxmlnode("ClientId", xmlrootnode, client_id) + if account_id: + xml_accountid_node = wapxmlnode("AccountId", xmlrootnode, account_id) + xml_saveinsentiems_node = wapxmlnode("SaveInSentItems", xmlrootnode, str(int(save_in_sent_items))) + xml_mime_node = wapxmlnode("Mime", xmlrootnode, None, mime) + #xml_templateid_node = wapxmlnode("rm:TemplateID", xmlrootnode, template_id) + return sendmail_xmldoc_req + + @staticmethod + def parse(wapxml): + + namespace = "composemail" + root_tag = "SendMail" + + root_element = wapxml.get_root() + if root_element.get_xmlns() != namespace: + raise AttributeError("Xmlns '%s' submitted to '%s' parser. Should be '%s'." % (root_element.get_xmlns(), root_tag, namespace)) + if root_element.tag != root_tag: + raise AttributeError("Root tag '%s' submitted to '%s' parser. Should be '%s'." % (root_element.tag, root_tag, root_tag)) + + sendmail_children = root_element.get_children() + + status = None + + for element in sendmail_children: + if element.tag is "Status": + status = element.text + return status + diff --git a/peas/pyActiveSync/client/SmartForward.py b/peas/pyActiveSync/client/SmartForward.py new file mode 100644 index 0000000..e1b3d0a --- /dev/null +++ b/peas/pyActiveSync/client/SmartForward.py @@ -0,0 +1,68 @@ +######################################################################## +# Copyright (C) 2013 Sol Birnbaum +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +######################################################################## + +from ..utils.wapxml import wapxmltree, wapxmlnode + +class SmartForward: + """http://msdn.microsoft.com/en-us/library/ee201840(v=exchg.80).aspx""" + + @staticmethod + def build(client_id, source, mime, replace_mime=False, save_in_sent_items=True, template_id=None): + smartforward_xmldoc_req = wapxmltree() + xmlrootnode = wapxmlnode("SmartForward") + smartforward_xmldoc_req.set_root(xmlrootnode, "composemail") + xml_clientid_node = wapxmlnode("ClientId", xmlrootnode, client_id) + xml_source_node = wapxmlnode("Source", xmlrootnode) + if source.has_key("FolderId"): + wapxmlnode("FolderId", xml_source_node, source["FolderId"]) + if source.has_key("ItemId"): + wapxmlnode("ItemId", xml_source_node, source["ItemId"]) + if source.has_key("LongId"): + wapxmlnode("LongId", xml_source_node, source["LongId"]) + if source.has_key("InstanceId"): + wapxmlnode("InstanceId", xml_source_node, source["InstanceId"]) + xml_accountid_node = wapxmlnode("AccountId", xmlrootnode, display_name) + xml_saveinsentiems_node = wapxmlnode("SaveInSentItems", xmlrootnode, str(int(save_in_sent_items))) + if replace_mime: + xml_replacemime_node = wapxmlnode("ReplaceMime", xmlrootnode) + xml_mime_node = wapxmlnode("Mime", xmlrootnode, mime) + xml_templateid_node = wapxmlnode("rm:TemplateID", xmlrootnode, template_id) + return smartforward_xmldoc_req + + @staticmethod + def parse(wapxml): + + namespace = "composemail" + root_tag = "SmartForward" + + root_element = wapxml.get_root() + if root_element.get_xmlns() != namespace: + raise AttributeError("Xmlns '%s' submitted to '%s' parser. Should be '%s'." % (root_element.get_xmlns(), root_tag, namespace)) + if root_element.tag != root_tag: + raise AttributeError("Root tag '%s' submitted to '%s' parser. Should be '%s'." % (root_element.tag, root_tag, root_tag)) + + smartforward_children = root_element.get_children() + + status = None + + for element in smartforward_children: + if element.tag is "Status": + status = element.text + return status + diff --git a/peas/pyActiveSync/client/SmartReply.py b/peas/pyActiveSync/client/SmartReply.py new file mode 100644 index 0000000..bfcacd1 --- /dev/null +++ b/peas/pyActiveSync/client/SmartReply.py @@ -0,0 +1,68 @@ +######################################################################## +# Copyright (C) 2013 Sol Birnbaum +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +######################################################################## + +from ..utils.wapxml import wapxmltree, wapxmlnode + +class SmartReply: + """http://msdn.microsoft.com/en-us/library/ee217283(v=exchg.80).aspx""" + + @staticmethod + def build(client_id, source, mime, replace_mime=False, save_in_sent_items=True, template_id=None): + smartreply_xmldoc_req = wapxmltree() + xmlrootnode = wapxmlnode("SmartReply") + smartreply_xmldoc_req.set_root(xmlrootnode, "composemail") + xml_clientid_node = wapxmlnode("ClientId", xmlrootnode, client_id) + xml_source_node = wapxmlnode("Source", xmlrootnode) + if source.has_key("FolderId"): + wapxmlnode("FolderId", xml_source_node, source["FolderId"]) + if source.has_key("ItemId"): + wapxmlnode("ItemId", xml_source_node, source["ItemId"]) + if source.has_key("LongId"): + wapxmlnode("LongId", xml_source_node, source["LongId"]) + if source.has_key("InstanceId"): + wapxmlnode("InstanceId", xml_source_node, source["InstanceId"]) + xml_accountid_node = wapxmlnode("AccountId", xmlrootnode, display_name) + xml_saveinsentiems_node = wapxmlnode("SaveInSentItems", xmlrootnode, str(int(save_in_sent_items))) + if replace_mime: + xml_replacemime_node = wapxmlnode("ReplaceMime", xmlrootnode) + xml_mime_node = wapxmlnode("Mime", xmlrootnode, mime) + xml_templateid_node = wapxmlnode("rm:TemplateID", xmlrootnode, template_id) + return smartreply_xmldoc_req + + @staticmethod + def parse(wapxml): + + namespace = "composemail" + root_tag = "SmartReply" + + root_element = wapxml.get_root() + if root_element.get_xmlns() != namespace: + raise AttributeError("Xmlns '%s' submitted to '%s' parser. Should be '%s'." % (root_element.get_xmlns(), root_tag, namespace)) + if root_element.tag != root_tag: + raise AttributeError("Root tag '%s' submitted to '%s' parser. Should be '%s'." % (root_element.tag, root_tag, root_tag)) + + smartreply_children = root_element.get_children() + + status = None + + for element in smartreply_children: + if element.tag is "Status": + status = element.text + return status + diff --git a/peas/pyActiveSync/client/Sync.py b/peas/pyActiveSync/client/Sync.py new file mode 100644 index 0000000..70be96a --- /dev/null +++ b/peas/pyActiveSync/client/Sync.py @@ -0,0 +1,192 @@ +######################################################################## +# Copyright (C) 2013 Sol Birnbaum +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +######################################################################## + +from ..utils.wapxml import wapxmltree, wapxmlnode + +from ..objects.MSASCMD import FolderHierarchy +from ..objects.MSASEMAIL import parse_email +from ..objects.MSASCNTC import parse_contact +from ..objects.MSASCAL import parse_calendar +from ..objects.MSASTASK import parse_task +from ..objects.MSASNOTE import parse_note + +class Sync: + """'Sync' command builders and parsers""" + class sync_response_collection: + def __init__(self): + self.SyncKey = 0 + self.CollectionId = None + self.Status = 0 + self.MoreAvailable = None + self.Commands = [] + self.Responses = None + + @staticmethod + def build(synckeys, collections): + as_sync_xmldoc_req = wapxmltree() + xml_as_sync_rootnode = wapxmlnode("Sync") + as_sync_xmldoc_req.set_root(xml_as_sync_rootnode, "airsync") + + xml_as_collections_node = wapxmlnode("Collections", xml_as_sync_rootnode) + + for collection_id in collections.keys(): + xml_as_Collection_node = wapxmlnode("Collection", xml_as_collections_node) #http://msdn.microsoft.com/en-us/library/gg650891(v=exchg.80).aspx + try: + 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 + except KeyError: + xml_as_SyncKey_node = wapxmlnode("SyncKey", xml_as_Collection_node, "0") + + xml_as_CollectionId_node = wapxmlnode("CollectionId", xml_as_Collection_node, collection_id) #http://msdn.microsoft.com/en-us/library/gg650886(v=exchg.80).aspx + + for parameter in collections[collection_id].keys(): + if parameter == "Options": + xml_as_Options_node = wapxmlnode(parameter, xml_as_Collection_node) + for option_parameter in collections[collection_id][parameter].keys(): + if option_parameter.startswith("airsync"): + for airsyncpref_node in collections[collection_id][parameter][option_parameter]: + xml_as_Options_airsyncpref_node = wapxmlnode(option_parameter.replace("_",":"), xml_as_Options_node) + wapxmlnode("airsyncbase:Type", xml_as_Options_airsyncpref_node, airsyncpref_node["Type"]) + tmp = airsyncpref_node["Type"] + del airsyncpref_node["Type"] + for airsyncpref_parameter in airsyncpref_node.keys(): + wapxmlnode("airsyncbase:%s" % airsyncpref_parameter, xml_as_Options_airsyncpref_node, airsyncpref_node[airsyncpref_parameter]) + airsyncpref_node["Type"] = tmp + elif option_parameter.startswith("rm"): + wapxmlnode(option_parameter.replace("_",":"), xml_as_Options_node, collections[collection_id][parameter][option_parameter]) + else: + wapxmlnode(option_parameter, xml_as_Options_node, collections[collection_id][parameter][option_parameter]) + else: + wapxmlnode(parameter, xml_as_Collection_node, collections[collection_id][parameter]) + return as_sync_xmldoc_req + + @staticmethod + def deepsearch_content_class(item): + elements = item.get_children() + for element in elements: + if element.has_children(): + content_class = Sync.deepsearch_content_class(element) + if content_class: + return content_class + if (element.tag == "email:To") or (element.tag == "email:From"): + return "Email" + elif (element.tag == "contacts:FileAs") or (element.tag == "contacts:Email1Address"): + return "Contacts" + elif (element.tag == "calendar:Subject") or (element.tag == "calendar:UID"): + return "Calendar" + elif (element.tag == "tasks:Type"): + return "Tasks" + elif (element.tag == "notes:MessageClass"): + return "Notes" + + @staticmethod + def parse_item(item, collection_id, collectionid_to_type_dict = None): + if collectionid_to_type_dict: + try: + content_class = FolderHierarchy.FolderTypeToClass[collectionid_to_type_dict[collection_id]] + except: + content_class = Sync.deepsearch_content_class(item) + else: + content_class = Sync.deepsearch_content_class(item) + try: + if content_class == "Email": + return parse_email(item), content_class + elif content_class == "Contacts": + return parse_contact(item), content_class + elif content_class == "Calendar": + return parse_calendar(item), content_class + elif content_class == "Tasks": + return parse_task(item), content_class + elif content_class == "Notes": + return parse_note(item), content_class + except Exception, e: + if collectionid_to_type_dict: + return Sync.parse_item(item, collection_id, None) + else: + print e + pass + raise LookupError("Could not determine content class of item for parsing. \r\n------\r\nItem:\r\n%s" % repr(item)) + + @staticmethod + def parse(wapxml, collectionid_to_type_dict = None): + + namespace = "airsync" + root_tag = "Sync" + + root_element = wapxml.get_root() + if root_element.get_xmlns() != namespace: + raise AttributeError("Xmlns '%s' submitted to '%s' parser. Should be '%s'." % (root_element.get_xmlns(), root_tag, namespace)) + if root_element.tag != root_tag: + raise AttributeError("Root tag '%s' submitted to '%s' parser. Should be '%s'." % (root_element.tag, root_tag, root_tag)) + + airsyncbase_sync_children = root_element.get_children() + if len(airsyncbase_sync_children) > 1: + raise AttributeError("%s response does not conform to any known %s responses." % (root_tag, root_tag)) + if airsyncbase_sync_children[0].tag == "Status": + if airsyncbase_sync_children[0].text == "4": + print "Sync Status: 4, Protocol Error." + if airsyncbase_sync_children[0].tag != "Collections": + raise AttributeError("%s response does not conform to any known %s responses." % (root_tag, root_tag)) + + response = [] + + airsyncbase_sync_collections_children = airsyncbase_sync_children[0].get_children() + airsyncbase_sync_collections_children_count = len(airsyncbase_sync_collections_children) + collections_counter = 0 + while collections_counter < airsyncbase_sync_collections_children_count: + + if airsyncbase_sync_collections_children[collections_counter].tag != "Collection": + raise AttributeError("Sync response does not conform to any known Sync responses.") + + airsyncbase_sync_collection_children = airsyncbase_sync_collections_children[collections_counter].get_children() + airsyncbase_sync_collection_children_count = len(airsyncbase_sync_collection_children) + collection_counter = 0 + new_collection = Sync.sync_response_collection() + while collection_counter < airsyncbase_sync_collection_children_count: + if airsyncbase_sync_collection_children[collection_counter].tag == "SyncKey": + new_collection.SyncKey = airsyncbase_sync_collection_children[collection_counter].text + elif airsyncbase_sync_collection_children[collection_counter].tag == "CollectionId": + new_collection.CollectionId = airsyncbase_sync_collection_children[collection_counter].text + elif airsyncbase_sync_collection_children[collection_counter].tag == "Status": + new_collection.Status = airsyncbase_sync_collection_children[collection_counter].text + if new_collection.Status != "1": + response.append(new_collection) + elif airsyncbase_sync_collection_children[collection_counter].tag == "MoreAvailable": + new_collection.MoreAvailable = True + elif airsyncbase_sync_collection_children[collection_counter].tag == "Commands": + airsyncbase_sync_commands_children = airsyncbase_sync_collection_children[collection_counter].get_children() + airsyncbase_sync_commands_children_count = len(airsyncbase_sync_commands_children) + commands_counter = 0 + while commands_counter < airsyncbase_sync_commands_children_count: + if airsyncbase_sync_commands_children[commands_counter].tag == "Add": + add_item = Sync.parse_item(airsyncbase_sync_commands_children[commands_counter], new_collection.CollectionId, collectionid_to_type_dict) + new_collection.Commands.append(("Add", add_item)) + elif airsyncbase_sync_commands_children[commands_counter].tag == "Delete": + new_collection.Commands.append(("Delete", airsyncbase_sync_commands_children[commands_counter].get_children()[0].text)) + elif airsyncbase_sync_commands_children[commands_counter].tag == "Change": + update_item = Sync.parse_item(airsyncbase_sync_commands_children[commands_counter], new_collection.CollectionId, collectionid_to_type_dict) + new_collection.Commands.append(("Change", update_item)) + elif airsyncbase_sync_commands_children[commands_counter].tag == "SoftDelete": + new_collection.Commands.append(("SoftDelete", airsyncbase_sync_commands_children[commands_counter].get_children()[0].text)) + commands_counter+=1 + elif airsyncbase_sync_collection_children[collection_counter].tag == "Responses": + print airsyncbase_sync_collection_children[collection_counter] + collection_counter+=1 + response.append(new_collection) + collections_counter+=1 + return response diff --git a/peas/pyActiveSync/client/ValidateCert.py b/peas/pyActiveSync/client/ValidateCert.py new file mode 100644 index 0000000..40bb0bc --- /dev/null +++ b/peas/pyActiveSync/client/ValidateCert.py @@ -0,0 +1,71 @@ +######################################################################## +# Copyright (C) 2013 Sol Birnbaum +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +######################################################################## + +from ..utils.wapxml import wapxmltree, wapxmlnode +import base64 + +class ValidateCert: + """http://msdn.microsoft.com/en-us/library/gg675590(v=exchg.80).aspx""" + + @staticmethod + def build(certificate, certificate_chain_list=[], pre_encoded = False, check_crl = True): + validatecert_xmldoc_req = wapxmltree() + xmlrootnode = wapxmlnode("ValidateCert") + validatecert_xmldoc_req.set_root(xmlrootnode, "validatecert") + if len(certificate_chain_list) > 0: + xmlcertchainnode = wapxmlnode("CertificateChain", xmlrootnode) + for cert in certificate_chain_list: + if pre_encoded: + wapxmlnode("Certificate", xmlcertchainnode, cert) + else: + wapxmlnode("Certificate", xmlcertchainnode, base64.b64encode(cert)) + xmlcertsnode = wapxmlnode("Certificates", xmlrootnode) + if pre_encoded: + xmlcertnode = wapxmlnode("Certificate", xmlcertsnode, certificate) + else: + xmlcertnode = wapxmlnode("Certificate", xmlcertsnode, base64.b64encode(certificate)) + if check_crl: + xmlcertsnode = wapxmlnode("CheckCRL", xmlrootnode, "1") + return validatecert_xmldoc_req + + @staticmethod + def parse(wapxml): + + namespace = "validatecert" + root_tag = "ValidateCert" + + root_element = wapxml.get_root() + if root_element.get_xmlns() != namespace: + raise AttributeError("Xmlns '%s' submitted to '%s' parser. Should be '%s'." % (root_element.get_xmlns(), root_tag, namespace)) + if root_element.tag != root_tag: + raise AttributeError("Root tag '%s' submitted to '%s' parser. Should be '%s'." % (root_element.tag, root_tag, root_tag)) + + validatecert_validatecert_status = None + validatecert_validatecert_cert_status = None + + for element in validatecert_validatecert_children: + if element.tag is "Status": + validatecert_validatecert_status = element.text + if validatecert_validatecert_status != "1": + print "ValidateCert Exception: %s" % validatecert_validatecert_status + elif element.tag == "Certificate": + validatecert_validatecert_cert_status = element.get_children()[0].text + return (validatecert_validatecert_status, validatecert_validatecert_cert_status) + + diff --git a/peas/pyActiveSync/client/__init__.py b/peas/pyActiveSync/client/__init__.py new file mode 100644 index 0000000..7774985 --- /dev/null +++ b/peas/pyActiveSync/client/__init__.py @@ -0,0 +1,18 @@ +######################################################################## +# Copyright (C) 2013 Sol Birnbaum +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +######################################################################## \ No newline at end of file diff --git a/peas/pyActiveSync/client/storage.py b/peas/pyActiveSync/client/storage.py new file mode 100644 index 0000000..47209f1 --- /dev/null +++ b/peas/pyActiveSync/client/storage.py @@ -0,0 +1,479 @@ +######################################################################## +# Copyright (C) 2013 Sol Birnbaum +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +######################################################################## + +import sqlite3 + +class storage: + @staticmethod + def set_keyvalue(key, value, path="pyas.asdb"): + conn = sqlite3.connect(path) + curs = conn.cursor() + curs.execute("INSERT INTO KeyValue VALUES ('%s', '%s')" % (key, value)) + conn.commit() + conn.close() + + @staticmethod + def update_keyvalue(key, value, path="pyas.asdb"): + conn = sqlite3.connect(path) + curs = conn.cursor() + sql = "UPDATE KeyValue SET Value='%s' WHERE Key='%s'" % (value.replace("'","''"), key) + curs.execute(sql) + conn.commit() + conn.close() + + @staticmethod + def get_keyvalue(key, path="pyas.asdb"): + conn = sqlite3.connect(path) + curs = conn.cursor() + curs.execute("SELECT Value FROM KeyValue WHERE Key='%s'" % key) + try: + value = curs.fetchone()[0] + conn.close() + return value + except: + conn.close() + return None + + @staticmethod + def create_db(path=None): + if path: + if path != "pyas.asdb": + if not path[-1] == "\\": + path = path + "\\pyas.asdb" + else: + path="pyas.asdb" + conn = sqlite3.connect(path) + curs = conn.cursor() + curs.execute("""CREATE TABLE FolderHierarchy (ServerId text, ParentId text, DisplayName text, Type text)""") + curs.execute("""CREATE TABLE SyncKeys (SyncKey text, CollectionId text)""") + curs.execute("""CREATE TABLE KeyValue (Key text, Value blob)""") + + curs.execute("""CREATE TABLE MSASEMAIL (ServerId text, + email_To text, + email_Cc text, + email_From text, + email_Subject text, + email_ReplyTo text, + email_DateReceived text, + email_DisplayTo text, + email_ThreadTopic text, + email_Importance text, + email_Read text, + airsyncbase_Attachments text, + airsyncbase_Body text, + email_MessageClass text, + email_InternetCPID text, + email_Flag text, + airsyncbase_NativeBodyType text, + email_ContentClass text, + email2_UmCallerId text, + email2_UmUserNotes text, + email2_ConversationId text, + email2_ConversationIndex text, + email2_LastVerbExecuted text, + email2_LastVerbExecutedTime text, + email2_ReceivedAsBcc text, + email2_Sender text, + email_Categories text, + airsyncbase_BodyPart text, + email2_AccountId text, + rm_RightsManagementLicense text)""") + + curs.execute("""CREATE TABLE MSASCAL (ServerId text, + calendar_TimeZone text, + calendar_DtStamp text, + calendar_StartTime text, + calendar_Subject text, + calendar_UID text, + calendar_OrganizerName text, + calendar_OrganizerEmail text, + calendar_Location text, + calendar_EndTime text, + airsyncbase_Body text, + calendar_Sensitivity text, + calendar_BusyStatus text, + calendar_AllDayEvent text, + calendar_Reminder text, + calendar_MeetingStatus text, + airsyncbase_NativeBodyType text, + calendar_ResponseRequested text, + calendar_ResponseType text, + calendar_AppointmentReplyTime text, + calendar_Attendees text, + calendar_Categories text, + calendar_Recurrence text, + calendar_OnlineMeetingConfLink text, + calendar_OnlineMeetingExternalLink text, + calendar_DisallowNewTimeProposal text, + calendar_Exceptions text)""") + + curs.execute("""CREATE TABLE MSASCNTC (ServerId text, + contacts2_AccountName text, + contacts_Alias text, + contacts_Anniversary text, + contacts_AssistantName text, + contacts_AssistantPhoneNumber text, + contacts_Birthday text, + airsyncbase_Body text, + contacts_BusinessAddressCity text, + contacts_BusinessAddressCountry text, + contacts_BusinessAddressPostalCode text, + contacts_BusinessAddressState text, + contacts_BusinessAddressStreet text, + contacts_BusinessFaxNumber text, + contacts_BusinessPhoneNumber text, + contacts_Business2PhoneNumber text, + contacts_CarPhoneNumber text, + contacts_Categories text, + contacts_Children text, + contacts_2CompanyMainPhone text, + contacts_CompanyName text, + contacts_2CustomerId text, + contacts_Department text, + contacts_FileAs text, + contacts_FirstName text, + contacts_LastName text, + contacts_Email1Address text, + contacts_Email2Address text, + contacts_Email3Address text, + contacts2_GovernmentId text, + contacts_HomeAddressCity text, + contacts_HomeAddressCountry text, + contacts_HomeAddressPostalCode text, + contacts_HomeAddressState text, + contacts_HomeAddressStreet text, + contacts_HomeFaxNumber text, + contacts_HomePhoneNumber text, + contacts_Home2PhoneNumber text, + contacts2_IMAddress text, + contacts2_IMAddress2 text, + contacts2_IMAddress3 text, + contacts_JobTitle text, + contacts2_ManagerName text, + contacts_MiddleName text, + contacts2_MMS text, + contacts_MobilePhoneNumber text, + contacts2_NickName text, + contacts_OfficeLocation text, + contacts_OtherAddressCity text, + contacts_OtherAddressCountry text, + contacts_OtherAddressPostalCode text, + contacts_OtherAddressState text, + contacts_OtherAddressStreet text, + contacts_PagerNumber text, + contacts_Picture text, + contacts_RadioPhoneNumber text, + contacts_Spouse text, + contacts_Suffix text, + contacts_Title text, + contacts_WebPage text, + contacts_WeightedRank text, + contacts_YomiCompanyName text, + contacts_YomiFirstName text, + contacts_YomiLastName text)""") + + curs.execute("""CREATE TABLE MSASNOTE (ServerId text, + notes_Subject text, + airsyncbase_Body text, + notes_MessageClass text, + notes_LastModifiedDate text)""") + + curs.execute("""CREATE TABLE MSASTASK (ServerId text, + airsyncbase_Body text, + tasks_Categories text, + tasks_Complete text, + tasks_DateCompleted text, + tasks_DueDate text, + tasks_Importance text, + tasks_OrdinalDate text, + tasks_Recurrence text, + tasks_ReminderSet text, + tasks_ReminderTime text, + tasks_Sensitivity text, + tasks_StartDate text, + tasks_Subject text, + tasks_SubOrdinalDate text, + tasks_UtcDueDate text, + tasks_UtcStartDate text)""") + + conn.commit() + + indicies = ['CREATE UNIQUE INDEX "main"."MSASEMAIL_ServerId_Idx" ON "MSASEMAIL" ("ServerId" ASC)', + 'CREATE UNIQUE INDEX "main"."MSASCAL_ServerId_Idx" ON "MSASCAL" ("ServerId" ASC)', + 'CREATE UNIQUE INDEX "main"."MSASCNTC_ServerId_Idx" ON "MSASCNTC" ("ServerId" ASC)', + 'CREATE UNIQUE INDEX "main"."MSASNOTE_ServerId_Idx" ON "MSASNOTE" ("ServerId" ASC)', + 'CREATE UNIQUE INDEX "main"."MSASTASK_ServerId_Idx" ON "MSASTASK" ("ServerId" ASC)', + 'CREATE UNIQUE INDEX "main"."SyncKey_CollectionId_Idx" ON "SyncKeys" ("CollectionId" ASC)', + 'CREATE UNIQUE INDEX "main"."KeyValue_Key_Idx" ON "KeyValue" ("Key" ASC)', + 'CREATE UNIQUE INDEX "main"."FolderHierarchy_ServerId_Idx" ON "FolderHierarchy" ("ServerId" ASC)', + 'CREATE INDEX "main"."FolderHierarchy_ParentType_Idx" ON "FolderHierarchy" ("ParentId" ASC, "Type" ASC)', + ] + for index in indicies: + curs.execute(index) + storage.set_keyvalue("X-MS-PolicyKey", "0") + storage.set_keyvalue("EASPolicies", "") + storage.set_keyvalue("MID", "0") + conn.commit() + + conn.close() + + @staticmethod + def get_conn_curs(path="pyas.asdb"): + conn = sqlite3.connect(path) + curs = conn.cursor() + return conn, curs + + @staticmethod + def close_conn_curs(conn): + try: + conn.commit() + conn.close() + except: + return False + return True + + + @staticmethod + def insert_folderhierarchy_change(folder, curs): + sql = "INSERT INTO FolderHierarchy VALUES ('%s', '%s', '%s', '%s')""" % (folder.ServerId, folder.ParentId, folder.DisplayName, folder.Type) + curs.execute(sql) + + @staticmethod + def update_folderhierarchy_change(folder, curs): + sql = "UPDATE FolderHierarchy SET ParentId='%s', DisplayName='%s', Type='%s' WHERE ServerId == '%s'""" % (folder.ParentId, folder.DisplayName, folder.Type, folder.ServerId) + curs.execute(sql) + + @staticmethod + def delete_folderhierarchy_change(folder, curs): + #Command only sent we permement delete is requested. Otherwise it would be 'Update' to ParentId='3' (Deleted Items). + #sql = "UPDATE FolderHierarchy SET ParentId='4' WHERE ServerId == '%s'""" % (folder.ServerId) + sql = "DELETE FROM MSASEMAIL WHERE ServerId like '%s:%%'" % (folder.ServerId) + curs.execute(sql) + sql = "DELETE FROM FolderHierarchy WHERE ServerId == '%s'" % (folder.ServerId) + curs.execute(sql) + + @staticmethod + def update_folderhierarchy(changes, path="pyas.asdb"): + conn = sqlite3.connect(path) + curs = conn.cursor() + for change in changes: + if change[0] == "Update": + storage.update_folderhierarchy_change(change[1], curs) + elif change[0] == "Delete": + storage.delete_folderhierarchy_change(change[1], curs) + elif change[0] == "Add": + storage.insert_folderhierarchy_change(change[1], curs) + conn.commit() + conn.close() + + @staticmethod + def get_folderhierarchy_folder_by_name(foldername, curs): + sql = "SELECT * FROM FolderHierarchy WHERE DisplayName = '%s'" % foldername + curs.execute(sql) + folder_row = curs.fetchone() + if folder_row: + return folder_row + else: + return False + + @staticmethod + def get_folderhierarchy_folder_by_id(server_id, curs): + sql = "SELECT * FROM FolderHierarchy WHERE ServerId = '%s'" % server_id + curs.execute(sql) + folder_row = curs.fetchone() + if folder_row: + return folder_row + else: + return False + + @staticmethod + def insert_item(table, calendar_dict, curs): + server_id = calendar_dict["server_id"] + del calendar_dict["server_id"] + calendar_cols = "" + calendar_vals = "" + for calendar_field in calendar_dict.keys(): + calendar_cols += (", '%s'" % calendar_field) + calendar_vals += (", '%s'" % repr(calendar_dict[calendar_field]).replace("'","''")) + sql = "INSERT INTO %s ( 'ServerId' %s ) VALUES ( '%s' %s )" % (table, calendar_cols, server_id, calendar_vals) + curs.execute(sql) + + @staticmethod + def update_item(table, calendar_dict, curs): + server_id = calendar_dict["server_id"] + del calendar_dict["server_id"] + calendar_sql = "" + for calendar_field in calendar_dict.keys(): + calendar_sql += (", %s='%s' " % (calendar_field, repr(calendar_dict[calendar_field]).replace("'","''"))) + calendar_sql = calendar_sql.lstrip(", ") + sql = "UPDATE %s SET %s WHERE ServerId='%s'" % (table, calendar_sql, server_id) + curs.execute(sql) + + @staticmethod + def delete_item(table, sever_id, curs): + sql = "DELETE FROM %s WHERE ServerId='%s'" % (table, sever_id) + curs.execute(sql) + + class ItemOps: + Insert = 0 + Delete = 1 + Update = 2 + SoftDelete = 3 + + class_to_table_dict = { + "Email" : "MSASEMAIL", + "Calendar" : "MSASCAL", + "Contacts" : "MSASCNTC", + "Tasks" : "MSASTASK", + "Notes" : "MSASNOTE", + "SMS" : "MSASMS", + "Document" : "MSASDOC" + } + + @staticmethod + def item_operation(method, item_class, data, curs): + if method == storage.ItemOps.Insert: + storage.insert_item(storage.class_to_table_dict[item_class], data, curs) + elif method == storage.ItemOps.Delete: + storage.delete_item(storage.class_to_table_dict[item_class], data, curs) + elif method == storage.ItemOps.Update: + storage.update_item(storage.class_to_table_dict[item_class], data, curs) + elif method == storage.ItemOps.SoftDelete: + storage.delete_item(storage.class_to_table_dict[item_class], data, curs) + + @staticmethod + def update_items(collections, path="pyas.asdb"): + conn = sqlite3.connect(path) + curs = conn.cursor() + for collection in collections: + for command in collection.Commands: + if command[0] == "Add": + storage.item_operation(storage.ItemOps.Insert, command[1][1], command[1][0], curs) + if command[0] == "Delete": + storage.item_operation(storage.ItemOps.Delete, command[1][1], command[1][0], curs) + elif command[0] == "Change": + storage.item_operation(storage.ItemOps.Update, command[1][1], command[1][0], curs) + elif command[0] == "SoftDelete": + storage.item_operation(storage.ItemOps.SoftDelete, command[1][1], command[1][0], curs) + if collection.SyncKey > 1: + storage.update_synckey(collection.SyncKey, collection.CollectionId, curs) + conn.commit() + else: + conn.close() + raise AttributeError("SyncKey incorrect") + + conn.commit() + conn.close() + + @staticmethod + def get_emails_by_collectionid(collectionid, curs): + sql = "SELECT * from MSASEMAIL WHERE ServerId like '%s:%%'" % collectionid + curs.execute(sql) + return curs.fetchall() + + @staticmethod + def update_synckey(synckey, collectionid, curs=None): + cleanup = False + if not curs: + cleanup = True + conn = sqlite3.connect("pyas.asdb") + curs = conn.cursor() + curs.execute("SELECT SyncKey FROM SyncKeys WHERE CollectionId = %s" % collectionid) + prev_synckey = curs.fetchone() + if not prev_synckey: + curs.execute("INSERT INTO SyncKeys VALUES ('%s', '%s')" % (synckey, collectionid)) + else: + curs.execute("UPDATE SyncKeys SET SyncKey='%s' WHERE CollectionId='%s'" % (synckey, collectionid)) + if cleanup: + conn.commit() + conn.close() + + @staticmethod + def get_synckey(collectionid, path="pyas.asdb"): + conn = sqlite3.connect(path) + curs = conn.cursor() + curs.execute("SELECT SyncKey FROM SyncKeys WHERE CollectionId = %s" % collectionid) + try: + synckey = curs.fetchone()[0] + except TypeError: + synckey = "0" + conn.close() + return synckey + + @staticmethod + def create_db_if_none(path="pyas.asdb"): + import os + if not os.path.isfile(path): + storage.create_db(path) + + + @staticmethod + def erase_db(path="pyas.asdb"): + import os + if os.path.isfile(path): + os.remove(path) + + + @staticmethod + def get_folder_name_to_id_dict(path="pyas.asdb"): + conn = sqlite3.connect(path) + curs = conn.cursor() + curs.execute("SELECT DisplayName, ServerId FROM FolderHierarchy") + id_name_list_of_tuples = curs.fetchall() + name_id_dict = {} + for id_name in id_name_list_of_tuples: + name_id_dict.update({ id_name[0] : id_name[1] }) + conn.close() + return name_id_dict + + @staticmethod + def get_synckeys_dict(curs, path="pyas.asdb"): + conn = sqlite3.connect(path) + curs = conn.cursor() + curs.execute("SELECT * FROM SyncKeys") + synckeys_rows = curs.fetchall() + synckeys_dict = {} + if synckeys_rows: + if len(synckeys_rows) > 0: + for synckey_row in synckeys_rows: + synckeys_dict.update({synckey_row[1]:synckey_row[0]}) + return synckeys_dict + + @staticmethod + def get_new_mid(path="pyas.asdb"): + pmid = int(storage.get_keyvalue("MID")) + mid = str(pmid+1) + storage.update_keyvalue("MID", mid) + return mid + + @staticmethod + def get_serverid_to_type_dict(path="pyas.asdb"): + conn = sqlite3.connect(path) + curs = conn.cursor() + curs.execute("SELECT * FROM FolderHierarchy") + folders_rows = curs.fetchall() + conn.close() + folders_dict = {} + if folders_rows: + if len(folders_rows) > 0: + for folders_row in folders_rows: + folders_dict.update({folders_row[0]:folders_row[3]}) + else: + raise LookupError("No folders found in FolderHierarchy table. Did you run a FolderSync yet?") + return folders_dict \ No newline at end of file diff --git a/peas/pyActiveSync/dev_playground.py b/peas/pyActiveSync/dev_playground.py new file mode 100644 index 0000000..71df813 --- /dev/null +++ b/peas/pyActiveSync/dev_playground.py @@ -0,0 +1,459 @@ +######################################################################## +# Copyright (C) 2013 Sol Birnbaum +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +######################################################################## + +# Code Playground + +import sys, time +import ssl + +from utils.as_code_pages import as_code_pages +from utils.wbxml import wbxml_parser +from utils.wapxml import wapxmltree, wapxmlnode +from client.storage import storage + +from client.FolderSync import FolderSync +from client.Sync import Sync +from client.GetItemEstimate import GetItemEstimate +from client.Ping import Ping +from client.Provision import Provision +from client.ValidateCert import ValidateCert + +from objects.MSASHTTP import ASHTTPConnector +from objects.MSASCMD import FolderHierarchy, as_status +from objects.MSASAIRS import airsync_FilterType, airsync_Conflict, airsync_MIMETruncation, airsync_MIMESupport, \ + airsync_Class, airsyncbase_Type + +from proto_creds import * #create a file proto_creds.py with vars: as_server, as_user, as_pass + +# Disable SSL certificate verification. +ssl._create_default_https_context = ssl._create_unverified_context + +pyver = sys.version_info + +storage.create_db_if_none() +conn, curs = storage.get_conn_curs() +device_info = {"Model": "%d.%d.%d" % (pyver[0], pyver[1], pyver[2]), "IMEI": "123457", + "FriendlyName": "My pyAS Client 2", "OS": "Python", "OSLanguage": "en-us", "PhoneNumber": "NA", + "MobileOperator": "NA", "UserAgent": "pyAS"} + +#create wbxml_parser test +cp, cp_sh = as_code_pages.build_as_code_pages() +parser = wbxml_parser(cp, cp_sh) + +#create ActiveSync connector +as_conn = ASHTTPConnector(as_server) #e.g. "as.myserver.com" +as_conn.set_credential(as_user, as_pass) +as_conn.options() +policykey = storage.get_keyvalue("X-MS-PolicyKey") +if policykey: + as_conn.set_policykey(policykey) + + +def as_request(cmd, wapxml_req): + print "\r\n%s Request:" % cmd + print wapxml_req + res = as_conn.post(cmd, parser.encode(wapxml_req)) + wapxml_res = parser.decode(res) + print "\r\n%s Response:" % cmd + print wapxml_res + return wapxml_res + + +#Provision functions +def do_apply_eas_policies(policies): + for policy in policies.keys(): + print "Virtually applying %s = %s" % (policy, policies[policy]) + return True + + +def do_provision(): + provision_xmldoc_req = Provision.build("0", device_info) + as_conn.set_policykey("0") + provision_xmldoc_res = as_request("Provision", provision_xmldoc_req) + status, policystatus, policykey, policytype, policydict, settings_status = Provision.parse(provision_xmldoc_res) + as_conn.set_policykey(policykey) + storage.update_keyvalue("X-MS-PolicyKey", policykey) + storage.update_keyvalue("EASPolicies", repr(policydict)) + if do_apply_eas_policies(policydict): + provision_xmldoc_req = Provision.build(policykey) + provision_xmldoc_res = as_request("Provision", provision_xmldoc_req) + status, policystatus, policykey, policytype, policydict, settings_status = Provision.parse(provision_xmldoc_res) + if status == "1": + as_conn.set_policykey(policykey) + storage.update_keyvalue("X-MS-PolicyKey", policykey) + +#FolderSync + Provision +foldersync_xmldoc_req = FolderSync.build(storage.get_synckey("0")) +foldersync_xmldoc_res = as_request("FolderSync", foldersync_xmldoc_req) +changes, synckey, status = FolderSync.parse(foldersync_xmldoc_res) +if int(status) > 138 and int(status) < 145: + print as_status("FolderSync", status) + do_provision() + foldersync_xmldoc_res = as_request("FolderSync", foldersync_xmldoc_req) + changes, synckey, status = FolderSync.parse(foldersync_xmldoc_res) + if int(status) > 138 and int(status) < 145: + print as_status("FolderSync", status) + raise Exception("Unresolvable provisoning error: %s. Cannot continue..." % status) +if len(changes) > 0: + storage.update_folderhierarchy(changes) + storage.update_synckey(synckey, "0", curs) + conn.commit() + +collection_id_of = storage.get_folder_name_to_id_dict() + +INBOX = collection_id_of["Inbox"] +SENT_ITEMS = collection_id_of["Sent Items"] +CALENDAR = collection_id_of["Calendar"] +CONTACTS = collection_id_of["Contacts"] +if 'Suggested Contacts' in collection_id_of: + SUGGESTED_CONTACTS = collection_id_of["Suggested Contacts"] +else: + SUGGESTED_CONTACTS = None +NOTES = collection_id_of["Notes"] +TASKS = collection_id_of["Tasks"] + +collection_sync_params = { + INBOX: + { #"Supported":"", + #"DeletesAsMoves":"1", + #"GetChanges":"1", + "WindowSize": "512", + "Options": { + "FilterType": airsync_FilterType.OneMonth, + "Conflict": airsync_Conflict.ServerReplacesClient, + "MIMETruncation": airsync_MIMETruncation.TruncateNone, + "MIMESupport": airsync_MIMESupport.SMIMEOnly, + "Class": airsync_Class.Email, + #"MaxItems":"300", #Recipient information cache sync requests only. Max number of frequently used contacts. + "airsyncbase_BodyPreference": [{ + "Type": airsyncbase_Type.HTML, + "TruncationSize": "1000000000", # Max 4,294,967,295 + "AllOrNone": "1", + # I.e. Do not return any body, if body size > tuncation size + #"Preview": "255", # Size of message preview to return 0-255 + }, + { + "Type": airsyncbase_Type.MIME, + "TruncationSize": "3000000000", # Max 4,294,967,295 + "AllOrNone": "1", + # I.e. Do not return any body, if body size > tuncation size + #"Preview": "255", # Size of message preview to return 0-255 + } + ], + #"airsyncbase_BodyPartPreference":"", + #"rm_RightsManagementSupport":"1" + }, + #"ConversationMode":"1", + #"Commands": {"Add":None, "Delete":None, "Change":None, "Fetch":None} + }, + SENT_ITEMS: + { #"Supported":"", + #"DeletesAsMoves":"1", + #"GetChanges":"1", + "WindowSize": "512", + "Options": { + "FilterType": airsync_FilterType.OneMonth, + "Conflict": airsync_Conflict.ServerReplacesClient, + "MIMETruncation": airsync_MIMETruncation.TruncateNone, + "MIMESupport": airsync_MIMESupport.SMIMEOnly, + "Class": airsync_Class.Email, + #"MaxItems":"300", #Recipient information cache sync requests only. Max number of frequently used contacts. + "airsyncbase_BodyPreference": [{ + "Type": airsyncbase_Type.HTML, + "TruncationSize": "1000000000", # Max 4,294,967,295 + "AllOrNone": "1", + # I.e. Do not return any body, if body size > tuncation size + #"Preview": "255", # Size of message preview to return 0-255 + }, + { + "Type": airsyncbase_Type.MIME, + "TruncationSize": "3000000000", # Max 4,294,967,295 + "AllOrNone": "1", + # I.e. Do not return any body, if body size > tuncation size + #"Preview": "255", # Size of message preview to return 0-255 + } + ], + #"airsyncbase_BodyPartPreference":"", + #"rm_RightsManagementSupport":"1" + }, + #"ConversationMode":"1", + #"Commands": {"Add":None, "Delete":None, "Change":None, "Fetch":None} + }, + CALENDAR: + { + "WindowSize": "512", + "Options": { + "FilterType": airsync_FilterType.OneMonth, + "Conflict": airsync_Conflict.ServerReplacesClient, + "MIMETruncation": airsync_MIMETruncation.TruncateNone, + "MIMESupport": airsync_MIMESupport.SMIMEOnly, + "Class": airsync_Class.Calendar, + "airsyncbase_BodyPreference": [{ + "Type": airsyncbase_Type.HTML, + "TruncationSize": "1000000000", # Max 4,294,967,295 + "AllOrNone": "1", + # I.e. Do not return any body, if body size > tuncation size + #"Preview": "255", # Size of message preview to return 0-255 + }, + { + "Type": airsyncbase_Type.MIME, + "TruncationSize": "3000000000", # Max 4,294,967,295 + "AllOrNone": "1", + # I.e. Do not return any body, if body size > tuncation size + } + ], + }, + }, + CONTACTS: + { + "WindowSize": "512", + "Options": { + #"FilterType": airsync_FilterType.OneWeek, + "Conflict": airsync_Conflict.ServerReplacesClient, + "MIMETruncation": airsync_MIMETruncation.TruncateNone, + "MIMESupport": airsync_MIMESupport.SMIMEOnly, + "Class": airsync_Class.Contacts, + "airsyncbase_BodyPreference": [{ + "Type": airsyncbase_Type.HTML, + "TruncationSize": "1000000000", # Max 4,294,967,295 + "AllOrNone": "1", + # I.e. Do not return any body, if body size > tuncation size + #"Preview": "255", # Size of message preview to return 0-255 + }, + { + "Type": airsyncbase_Type.MIME, + "TruncationSize": "3000000000", # Max 4,294,967,295 + "AllOrNone": "1", + # I.e. Do not return any body, if body size > tuncation size + } + ], + }, + }, + + NOTES: + { + "WindowSize": "512", + "Options": { + #"FilterType": airsync_FilterType.OneWeek, + "Conflict": airsync_Conflict.ServerReplacesClient, + "MIMETruncation": airsync_MIMETruncation.TruncateNone, + "MIMESupport": airsync_MIMESupport.SMIMEOnly, + "Class": airsync_Class.Notes, + "airsyncbase_BodyPreference": [{ + "Type": airsyncbase_Type.HTML, + "TruncationSize": "1000000000", # Max 4,294,967,295 + "AllOrNone": "1", + # I.e. Do not return any body, if body size > tuncation size + #"Preview": "255", # Size of message preview to return 0-255 + }, + { + "Type": airsyncbase_Type.MIME, + "TruncationSize": "3000000000", # Max 4,294,967,295 + "AllOrNone": "1", + # I.e. Do not return any body, if body size > tuncation size + } + ], + }, + }, + TASKS: + { + "WindowSize": "512", + "Options": { + #"FilterType": airsync_FilterType.OneWeek, + "Conflict": airsync_Conflict.ServerReplacesClient, + "MIMETruncation": airsync_MIMETruncation.TruncateNone, + "MIMESupport": airsync_MIMESupport.SMIMEOnly, + "Class": airsync_Class.Tasks, + "airsyncbase_BodyPreference": [{ + "Type": airsyncbase_Type.HTML, + "TruncationSize": "1000000000", # Max 4,294,967,295 + "AllOrNone": "1", + # I.e. Do not return any body, if body size > tuncation size + #"Preview": "255", # Size of message preview to return 0-255 + }, + { + "Type": airsyncbase_Type.MIME, + "TruncationSize": "3000000000", # Max 4,294,967,295 + "AllOrNone": "1", + # I.e. Do not return any body, if body size > tuncation size + } + ], + }, + } +} + +if SUGGESTED_CONTACTS: + collection_sync_params[SUGGESTED_CONTACTS] = { + "WindowSize": "512", + "Options": { + #"FilterType": airsync_FilterType.OneWeek, + "Conflict": airsync_Conflict.ServerReplacesClient, + "MIMETruncation": airsync_MIMETruncation.TruncateNone, + "MIMESupport": airsync_MIMESupport.SMIMEOnly, + "Class": airsync_Class.Contacts, + "airsyncbase_BodyPreference": [{ + "Type": airsyncbase_Type.HTML, + "TruncationSize": "1000000000", # Max 4,294,967,295 + "AllOrNone": "1", + # I.e. Do not return any body, if body size > tuncation size + #"Preview": "255", # Size of message preview to return 0-255 + }, + { + "Type": airsyncbase_Type.MIME, + "TruncationSize": "3000000000", # Max 4,294,967,295 + "AllOrNone": "1", + # I.e. Do not return any body, if body size > tuncation size + } + ], + }, + } + +gie_options = { + INBOX: + { #"ConversationMode": "0", + "Class": airsync_Class.Email, + "FilterType": airsync_FilterType.OneMonth + #"MaxItems": "" #Recipient information cache sync requests only. Max number of frequently used contacts. + }, + SENT_ITEMS: + { + "Class": airsync_Class.Email, + "FilterType": airsync_FilterType.OneMonth + }, + CALENDAR: + {"Class": airsync_Class.Calendar, + "FilterType": airsync_FilterType.OneMonth}, + CONTACTS: + { + "Class": airsync_Class.Contacts, + }, + SUGGESTED_CONTACTS: + { + "Class": airsync_Class.Contacts, + }, + NOTES: + { + "Class": airsync_Class.Notes, + }, + TASKS: + { + "Class": airsync_Class.Tasks, + } +} + + +#Sync function +def do_sync(collections): + as_sync_xmldoc_req = Sync.build(storage.get_synckeys_dict(curs), collections) + print "\r\nRequest:" + print as_sync_xmldoc_req + res = as_conn.post("Sync", parser.encode(as_sync_xmldoc_req)) + print "\r\nResponse:" + if res == '': + print "Nothing to Sync!" + else: + collectionid_to_type_dict = storage.get_serverid_to_type_dict() + as_sync_xmldoc_res = parser.decode(res) + print as_sync_xmldoc_res + sync_res = Sync.parse(as_sync_xmldoc_res, collectionid_to_type_dict) + storage.update_items(sync_res) + return sync_res + + +#GetItemsEstimate +def do_getitemestimates(collection_ids): + getitemestimate_xmldoc_req = GetItemEstimate.build(storage.get_synckeys_dict(curs), collection_ids, gie_options) + getitemestimate_xmldoc_res = as_request("GetItemEstimate", getitemestimate_xmldoc_req) + + getitemestimate_res = GetItemEstimate.parse(getitemestimate_xmldoc_res) + return getitemestimate_res + + +def getitemestimate_check_prime_collections(getitemestimate_responses): + has_synckey = [] + needs_synckey = {} + for response in getitemestimate_responses: + if response.Status == "1": + has_synckey.append(response.CollectionId) + elif response.Status == "2": + print "GetItemEstimate Status: Unknown CollectionId (%s) specified. Removing." % response.CollectionId + elif response.Status == "3": + print "GetItemEstimate Status: Sync needs to be primed." + needs_synckey.update({response.CollectionId: {}}) + has_synckey.append( + response.CollectionId) #technically *will* have synckey after do_sync() need end of function + else: + print as_status("GetItemEstimate", response.Status) + if len(needs_synckey) > 0: + do_sync(needs_synckey) + return has_synckey, needs_synckey + + +def sync(collections): + getitemestimate_responses = do_getitemestimates(collections) + + has_synckey, just_got_synckey = getitemestimate_check_prime_collections(getitemestimate_responses) + + if (len(has_synckey) < collections) or (len(just_got_synckey) > 0): #grab new estimates, since they changed + getitemestimate_responses = do_getitemestimates(has_synckey) + + collections_to_sync = {} + + for response in getitemestimate_responses: + if response.Status == "1": + if int(response.Estimate) > 0: + collections_to_sync.update({response.CollectionId: collection_sync_params[response.CollectionId]}) + else: + print "GetItemEstimate Status (error): %s, CollectionId: %s." % (response.Status, response.CollectionId) + + if len(collections_to_sync) > 0: + sync_res = do_sync(collections_to_sync) + if sync_res: + while True: + for coll_res in sync_res: + if coll_res.MoreAvailable is None: + del collections_to_sync[coll_res.CollectionId] + if len(collections_to_sync.keys()) > 0: + print collections_to_sync + sync_res = do_sync(collections_to_sync) + else: + break + + +collections = [INBOX, SENT_ITEMS, CALENDAR, CONTACTS, NOTES, TASKS] +if SUGGESTED_CONTACTS: + collections.append(SUGGESTED_CONTACTS) +sync(collections) + +#Ping (push), GetItemsEstimate and Sync process test +#Ping + + +ping_args = [(INBOX, "Email"), (SENT_ITEMS, "Email"), (CALENDAR, "Calendar"), (CONTACTS, "Contacts"), + (NOTES, "Notes"), (TASKS, "Tasks")] +if SUGGESTED_CONTACTS: + ping_args.append((SUGGESTED_CONTACTS, "Contacts")) +ping_xmldoc_req = Ping.build("120", ping_args) +ping_xmldoc_res = as_request("Ping", ping_xmldoc_req) +ping_res = Ping.parse(ping_xmldoc_res) +if ping_res[0] == "2": #2=New changes available + sync(ping_res[3]) + +if storage.close_conn_curs(conn): + del conn, curs \ No newline at end of file diff --git a/peas/pyActiveSync/misc_tests.py b/peas/pyActiveSync/misc_tests.py new file mode 100644 index 0000000..59da36d --- /dev/null +++ b/peas/pyActiveSync/misc_tests.py @@ -0,0 +1,214 @@ +######################################################################## +# Copyright (C) 2013 Sol Birnbaum +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +######################################################################## + +# Tests + +import sys, time +import ssl + +from utils.as_code_pages import as_code_pages +from utils.wbxml import wbxml_parser +from utils.wapxml import wapxmltree, wapxmlnode +from client.storage import storage + +from client.FolderSync import FolderSync +from client.Sync import Sync +from client.GetItemEstimate import GetItemEstimate +from client.ResolveRecipients import ResolveRecipients +from client.FolderCreate import FolderCreate +from client.FolderUpdate import FolderUpdate +from client.FolderDelete import FolderDelete +from client.Ping import Ping +from client.MoveItems import MoveItems +from client.Provision import Provision +from client.ItemOperations import ItemOperations +from client.ValidateCert import ValidateCert +from client.SendMail import SendMail +from client.SmartForward import SmartForward +from client.SmartReply import SmartReply + +from objects.MSASHTTP import ASHTTPConnector +from objects.MSASCMD import FolderHierarchy, as_status +from objects.MSASAIRS import airsync_FilterType, airsync_Conflict, airsync_MIMETruncation, airsync_MIMESupport, airsync_Class, airsyncbase_Type + +from proto_creds import * #create a file proto_creds.py with vars: as_server, as_user, as_pass + + +# Disable SSL certificate verification. +ssl._create_default_https_context = ssl._create_unverified_context + + +pyver = sys.version_info + +storage.create_db_if_none() +conn, curs = storage.get_conn_curs() +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"} + +#create wbxml_parser test +cp, cp_sh = as_code_pages.build_as_code_pages() +parser = wbxml_parser(cp, cp_sh) + +#create activesync connector +as_conn = ASHTTPConnector(as_server) #e.g. "as.myserver.com" +as_conn.set_credential(as_user, as_pass) +as_conn.options() +policykey = storage.get_keyvalue("X-MS-PolicyKey") +if policykey: + as_conn.set_policykey(policykey) + +def as_request(cmd, wapxml_req): + print "\r\n%s Request:" % cmd + print wapxml_req + res = as_conn.post(cmd, parser.encode(wapxml_req)) + wapxml_res = parser.decode(res) + print "\r\n%s Response:" % cmd + print wapxml_res + return wapxml_res + +#Provision functions +def do_apply_eas_policies(policies): + for policy in policies.keys(): + print "Virtually applying %s = %s" % (policy, policies[policy]) + return True + +def do_provision(): + provision_xmldoc_req = Provision.build("0", device_info) + as_conn.set_policykey("0") + provision_xmldoc_res = as_request("Provision", provision_xmldoc_req) + status, policystatus, policykey, policytype, policydict, settings_status = Provision.parse(provision_xmldoc_res) + as_conn.set_policykey(policykey) + storage.update_keyvalue("X-MS-PolicyKey", policykey) + storage.update_keyvalue("EASPolicies", repr(policydict)) + if do_apply_eas_policies(policydict): + provision_xmldoc_req = Provision.build(policykey) + provision_xmldoc_res = as_request("Provision", provision_xmldoc_req) + status, policystatus, policykey, policytype, policydict, settings_status = Provision.parse(provision_xmldoc_res) + if status == "1": + as_conn.set_policykey(policykey) + storage.update_keyvalue("X-MS-PolicyKey", policykey) + +#FolderSync + Provision +foldersync_xmldoc_req = FolderSync.build(storage.get_synckey("0")) +foldersync_xmldoc_res = as_request("FolderSync", foldersync_xmldoc_req) +changes, synckey, status = FolderSync.parse(foldersync_xmldoc_res) +if int(status) > 138 and int(status) < 145: + print as_status("FolderSync", status) + do_provision() + foldersync_xmldoc_res = as_request("FolderSync", foldersync_xmldoc_req) + changes, synckey, status = FolderSync.parse(foldersync_xmldoc_res) + if int(status) > 138 and int(status) < 145: + print as_status("FolderSync", status) + raise Exception("Unresolvable provisoning error: %s. Cannot continue..." % status) +if len(changes) > 0: + storage.update_folderhierarchy(changes) + storage.update_synckey(synckey, "0", curs) + conn.commit() + +#ItemOperations +itemoperations_params = [{"Name":"Fetch","Store":"Mailbox", "FileReference":"%34%67%32"}] +itemoperations_xmldoc_req = ItemOperations.build(itemoperations_params) +print "\r\nItemOperations Request:\r\n", itemoperations_xmldoc_req +#itemoperations_xmldoc_res, attachment_file = as_conn.fetch_multipart(itemoperations_xmldoc_req, "myattachment1.txt") +#itemoperations_xmldoc_res_parsed = ItemOperations.parse(itemoperations_xmldoc_res) +#print itemoperations_xmldoc_res + +#FolderCreate +parent_folder = storage.get_folderhierarchy_folder_by_name("Inbox", curs) +new_folder = FolderHierarchy.Folder(parent_folder[0], "TestFolder1", str(FolderHierarchy.FolderCreate.Type.Mail)) +foldercreate_xmldoc_req = FolderCreate.build(storage.get_synckey("0"), new_folder.ParentId, new_folder.DisplayName, new_folder.Type) +foldercreate_xmldoc_res = as_request("FolderCreate", foldercreate_xmldoc_req) +foldercreate_res_parsed = FolderCreate.parse(foldercreate_xmldoc_res) +if foldercreate_res_parsed[0] == "1": + new_folder.ServerId = foldercreate_res_parsed[2] + storage.insert_folderhierarchy_change(new_folder, curs) + storage.update_synckey(foldercreate_res_parsed[1], "0", curs) + conn.commit() +else: + print as_status("FolderCreate", foldercreate_res_parsed[0]) + +time.sleep(5) + +#FolderUpdate +old_folder_name = "TestFolder1" +new_folder_name = "TestFolder2" +#new_parent_id = parent_folder = storage.get_folderhierarchy_folder_by_name("Inbox", curs) +folder_row = storage.get_folderhierarchy_folder_by_name(old_folder_name, curs) +update_folder = FolderHierarchy.Folder(folder_row[1], new_folder_name, folder_row[3], folder_row[0]) +folderupdate_xmldoc_req = FolderUpdate.build(storage.get_synckey("0"), update_folder.ServerId, update_folder.ParentId, update_folder.DisplayName) +folderupdate_xmldoc_res = as_request("FolderUpdate", folderupdate_xmldoc_req) +folderupdate_res_parsed = FolderUpdate.parse(folderupdate_xmldoc_res) +if folderupdate_res_parsed[0] == "1": + new_folder.DisplayName = new_folder_name + storage.update_folderhierarchy_change(new_folder, curs) + storage.update_synckey(folderupdate_res_parsed[1], "0", curs) + conn.commit() + +time.sleep(5) + +#FolderDelete +try: + folder_name = "TestFolder2" + folder_row = storage.get_folderhierarchy_folder_by_name(folder_name, curs) + delete_folder = FolderHierarchy.Folder() + delete_folder.ServerId = folder_row[0] + folderdelete_xmldoc_req = FolderDelete.build(storage.get_synckey("0"), delete_folder.ServerId) + folderdelete_xmldoc_res = as_request("FolderDelete", folderdelete_xmldoc_req) + folderdelete_res_parsed = FolderDelete.parse(folderdelete_xmldoc_res) + if folderdelete_res_parsed[0] == "1": + storage.delete_folderhierarchy_change(delete_folder, curs) + storage.update_synckey(folderdelete_res_parsed[1], "0", curs) + conn.commit() +except TypeError, e: + print "\r\n%s\r\n" % e + pass + +#ResolveRecipients +resolverecipients_xmldoc_req = ResolveRecipients.build("thunderbird") +resolverecipients_xmldoc_res = as_request("ResolveRecipients", resolverecipients_xmldoc_req) + + +#SendMail +import email.mime.text +email_mid = storage.get_new_mid() +my_email = email.mime.text.MIMEText("Test email #%s from pyAS." % email_mid) +my_email["Subject"] = "Test #%s from pyAS!" % email_mid + +my_email["From"] = as_user +my_email["To"] = as_user + +sendmail_xmldoc_req = SendMail.build(email_mid, my_email) +print "\r\nRequest:" +print sendmail_xmldoc_req +res = as_conn.post("SendMail", parser.encode(sendmail_xmldoc_req)) +print "\r\nResponse:" +if res == '': + print "\r\nTest message sent successfully!" +else: + sendmail_xmldoc_res = parser.decode(res) + print sendmail_xmldoc_res + sendmail_res = SendMail.parse(sendmail_xmldoc_res) + +##MoveItems +#moveitems_xmldoc_req = MoveItems.build([("5:24","5","10")]) +#moveitems_xmldoc_res = as_request("MoveItems", moveitems_xmldoc_req) +#moveitems_res = MoveItems.parse(moveitems_xmldoc_res) +#for moveitem_res in moveitems_res: +# if moveitem_res[1] == "3": +# storage.update_email({"server_id": moveitem_res[0] ,"ServerId": moveitem_res[2]}, curs) +# conn.commit() \ No newline at end of file diff --git a/peas/pyActiveSync/objects/MSASAIRS.py b/peas/pyActiveSync/objects/MSASAIRS.py new file mode 100644 index 0000000..374e1dc --- /dev/null +++ b/peas/pyActiveSync/objects/MSASAIRS.py @@ -0,0 +1,192 @@ +######################################################################## +# Copyright (C) 2013 Sol Birnbaum +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +######################################################################## + +"""[MS-ASAIRS] AirSyncBase namespace objects""" + +class airsyncbase_Type: #http://msdn.microsoft.com/en-us/library/hh475675(v=exchg.80).aspx + Plaintext = 1 + HTML = 2 + RTF = 3 + MIME = 4 + +class airsyncbase_NativeBodyType: #http://msdn.microsoft.com/en-us/library/ee218276(v=exchg.80).aspx + Plaintext = 1 + HTML = 2 + RTF = 3 + +class airsyncbase_Method: #http://msdn.microsoft.com/en-us/library/ee160322(v=exchg.80).aspx + Normal_attachment = 1 #Regular attachment + Reserved1 = 2 + Reserved2 = 3 + Reserved3 = 4 + Embedded_message = 5 #Email with .eml extension + Attach_OLE = 6 #OLE such as inline image + +class airsyncbase_BodyPart_status: + Success = 1 + Too_long = 176 + +class airsync_MIMESupport: + Never = 0 + SMIMEOnly = 1 + Always = 2 + +class airsync_Class: + Email = "Email" + Contacts = "Contacts" + Calendar = "Calendar" + Tasks = "Tasks" + Notes = "Notes" + SMS = "SMS" + +class airsync_FilterType: # Email | Calendar | Tasks + NoFilter = "0" # Y | Y | Y + OneDay = "1" # Y | N | N + ThreeDays = "2" # Y | N | N + OneWeek = "3" # Y | N | N + TwoWeeks = "4" # Y | Y | N + OneMonth = "5" # Y | Y | N + ThreeMonths = "6" # N | Y | N + SixMonths = "7" # N | Y | N + IncompleteTasks = "8" # N | N | Y + +class airsync_Conflict: + ClientReplacesServer = 0 + ServerReplacesClient = 1 + +class airsync_MIMETruncation: + TruncateAll = 0 + Over4096chars = 1 + Over5120chars = 2 + Over7168chars = 3 + Over10240chars = 4 + Over20480chars = 5 + Over51200chars = 6 + Over102400chars = 7 + TruncateNone = 8 + +class airsyncbase_Body(object): + def __init__(self):#, type, estimated_data_size=None, truncated=None, data=None, part=None, preview=None): + self.airsyncbase_Type = None #type #Required. Integer. Max 1. See "MSASAIRS.Type" enum. + 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 + 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 + 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 + 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 + self.airsyncbase_Preview = None #preview #Optional. String (unicode). Plaintext preview message. http://msdn.microsoft.com/en-us/library/ff849891(v=exchg.80).aspx + def parse(self, imwapxml_airsyncbase_Body): + body_elements = imwapxml_airsyncbase_Body.get_children() + for element in body_elements: + if element.tag == "airsyncbase:Type": + self.airsyncbase_Type = element.text + elif element.tag == "airsyncbase:EstimatedDataSize": + self.airsyncbase_EstimatedDataSize = element.text + elif element.tag == "airsyncbase:Truncated": + self.airsyncbase_Truncated = element.text + elif element.tag == "airsyncbase:Data": + self.airsyncbase_Data = element.text + elif element.tag == "airsyncbase:Part": + self.airsyncbase_Part = element.text + elif element.tag == "airsyncbase:Preview": + self.airsyncbase_Preview = element.text + def marshal(self): + import base64 + 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)) + def __repr__(self): + return self.marshal() + +class airsyncbase_BodyPart(object): + def __init__(self): + self.airsyncbase_BodyPart_status = airsyncbase_BodyPart_status.Too_long #Required. Byte. See airsyncbase_BodyPart_status enum. + self.airsyncbase_Type = airsyncbase_Type.HTML #Required. Integer. Max 1. See "MSASAIRS.Type" enum. + 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 + 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 + 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 + 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 + self.airsyncbase_Preview = preview #Optional. String (unicode). Plaintext preview message. http://msdn.microsoft.com/en-us/library/ff849891(v=exchg.80).aspx + def parse(self, imwapxml_airsyncbase_BodyPart): + bodypart_elements = imwapxml_airsyncbase_BodyPart.get_children() + for element in bodypart_elements: + if element.tag == "airsyncbase:Type": + self.airsyncbase_Type = element.text + elif element.tag == "airsyncbase:EstimatedDataSize": + self.airsyncbase_EstimatedDataSize = element.text + elif element.tag == "airsyncbase:Truncated": + self.airsyncbase_Truncated = element.text + elif element.tag == "airsyncbase:Data": + self.airsyncbase_Data = element.text + elif element.tag == "airsyncbase:Part": + self.airsyncbase_Part = element.text + elif element.tag == "airsyncbase:Preview": + self.airsyncbase_Preview = element.text + elif element.tag == "airsyncbase:Status": + self.airsyncbase_BodyPart_status = element.text + +class airsyncbase_Attachment(object): #Repsonse-only object. + 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): + self.airsyncbase_DisplayName = None #display_name #Optional. String. http://msdn.microsoft.com/en-us/library/ee160854(v=exchg.80).aspx + 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 + 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 + 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 + self.airsyncbase_ContentId = None #content_id #Optional. String. Max 1. Unique object id of attachment - informational only. + 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 + 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 + 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". + 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". + def parse(self, imwapxml_airsyncbase_Attachment): + attachment_elements = imwapxml_airsyncbase_Attachment.get_children() + for element in attachment_elements: + if element.tag == "airsyncbase:DisplayName": + self.airsyncbase_DisplayName = element.text + elif element.tag == "airsyncbase:FileReference": + self.airsyncbase_FileReference = element.text + elif element.tag == "airsyncbase:Method": + self.airsyncbase_Method = element.text + elif element.tag == "airsyncbase:EstimatedDataSize": + self.airsyncbase_EstimatedDataSize = element.text + elif element.tag == "airsyncbase:ContentId": + self.airsyncbase_ContentId = element.text + elif element.tag == "airsyncbase:ContentLocation": + self.airsyncbase_ContentLocation = element.text + elif element.tag == "airsyncbase:IsInline": + self.airsyncbase_IsInline = element.text + elif element.tag == "email2:UmAttDuration": + self.email2_UmAttDuration = element.text + elif element.tag == "email2:UmAttOrder": + self.email2_UmAttOrder = element.text + def marshal(self): + import base64 + 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))) + def __repr__(self): + return self.marshal() + +class airsyncbase_Attachments: + @staticmethod + def parse(inwapxml_airsyncbase_Attachments): + attachment_elements = inwapxml_airsyncbase_Attachments.get_children() + attachments = [] + for attachment in attachment_elements: + new_attachment = airsyncbase_Attachment() + new_attachment.parse(attachment) + attachments.append(new_attachment) + return attachments + + + + + diff --git a/peas/pyActiveSync/objects/MSASCAL.py b/peas/pyActiveSync/objects/MSASCAL.py new file mode 100644 index 0000000..d0682a2 --- /dev/null +++ b/peas/pyActiveSync/objects/MSASCAL.py @@ -0,0 +1,241 @@ +######################################################################## +# Copyright (C) 2013 Sol Birnbaum +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +######################################################################## + +"""[MS-ASCAL] Calendar objects""" + +from MSASEMAIL import airsyncbase_Body + +def parse_calendar(data): + calendar_dict = {} + calendar_base = data.get_children() + calendar_dict.update({"server_id" : calendar_base[0].text}) + calendar_elements = calendar_base[1].get_children() + for element in calendar_elements: + if element.tag == "calendar:AllDayEvent": + calendar_dict.update({ "calendar_AllDayEvent" : element.text }) + elif element.tag == "calendar:AppointmentReplyTime": + calendar_dict.update({ "calendar_AppointmentReplyTime" : element.text }) + elif element.tag == "calendar:Attendees": + attendees_list = [] + for attendee in element.get_children(): + attendee_dict = {} + for attendee_element in attendee.get_children(): + if attendee_element.tag == "calendar:AttendeeStatus": + attendee_dict.update({ "calendar_AttendeeStatus" : attendee_element.text }) + elif attendee_element.tag == "calendar:AttendeeType": + attendee_dict.update({ "calendar_AttendeeType" : attendee_element.text }) + elif attendee_element.tag == "calendar:Name": + attendee_dict.update({ "calendar_Name" : attendee_element.text }) + elif attendee_element.tag == "calendar:Email": + attendee_dict.update({ "calendar_Email" : attendee_element.text }) + attendees_list.append(attendee_dict) + calendar_dict.update({ "calendar_Attendees" : attendees_list }) + elif element.tag == "airsyncbase:Body": + body = airsyncbase_Body() + body.parse(element) + calendar_dict.update({ "airsyncbase_Body" : body }) + elif element.tag == "calendar:BusyStatus": + calendar_dict.update({ "calendar_BusyStatus" : element.text }) + elif element.tag == "calendar:Categories": + categories_list = [] + categories = element.get_children() + for category_element in categories: + categories_list.append(category_element.text) + calendar_dict.update({ "calendar_Categories" : categories_list }) + elif element.tag == "calendar:DisallowNewTimeProposal": + calendar_dict.update({ "calendar_DisallowNewTimeProposal" : element.text }) + elif element.tag == "calendar:DtStamp": + calendar_dict.update({ "calendar_DtStamp" : element.text }) + elif element.tag == "calendar:EndTime": + calendar_dict.update({ "calendar_EndTime" : element.text }) + elif element.tag == "calendar:Exceptions": + exceptions_list = [] + for recurrence_exception in element.get_children(): + exception_dict = {} + for exception_element in recurrence_exception.get_children(): + if exception_element.tag == "calendar:Deleted": + exception_dict.update({ "calendar_Deleted" : exception_element.text }) + elif exception_element.tag == "calendar:ExceptionStartTime": + exception_dict.update({ "calendar_ExceptionStartTime" : exception_element.text }) + elif exception_element.tag == "calendar:AllDayEvent": + exception_dict.update({ "calendar_AllDayEvent" : exception_element.text }) + elif exception_element.tag == "calendar:AppointmentReplyTime": + exception_dict.update({ "calendar_AppointmentReplyTime" : exception_element.text }) + elif exception_element.tag == "calendar:Attendees": + attendees_list = [] + for attendee in element.get_children(): + attendee_dict = {} + for attendee_element in attendee.get_children(): + if attendee_element.tag == "calendar:AttendeeStatus": + attendee_dict.update({ "calendar_AttendeeStatus" : attendee_element.text }) + elif attendee_element.tag == "calendar:AttendeeType": + attendee_dict.update({ "calendar_AttendeeType" : attendee_element.text }) + elif attendee_element.tag == "calendar:Name": + attendee_dict.update({ "calendar_Name" : attendee_element.text }) + elif attendee_element.tag == "calendar:Email": + attendee_dict.update({ "calendar_Email" : attendee_element.text }) + attendees_list.append(attendee_dict) + exception_dict.update({ "calendar_Attendees" : attendees_list }) + elif exception_element.tag == "airsyncbase:Body": + body = airsyncbase_Body() + body.parse(element) + exception_dict.update({ "airsyncbase_Body" : body }) + elif exception_element.tag == "calendar:BusyStatus": + exception_dict.update({ "calendar_BusyStatus" : exception_element.text }) + elif exception_element.tag == "calendar:Categories": + categories_list = [] + categories = element.get_children() + for category_element in categories: + categories_list.append(category_element.text) + exception_dict.update({ "calendar_Categories" : categories_list }) + elif exception_element.tag == "calendar:StartTime": + exception_dict.update({ "calendar_StartTime" : exception_element.text }) + elif exception_element.tag == "calendar:OnlineMeetingConfLink": + exception_dict.update({ "calendar_OnlineMeetingConfLink" : exception_element.text }) + elif exception_element.tag == "calendar:OnlineMeetingExternalLink": + exception_dict.update({ "calendar_OnlineMeetingExternalLink" : exception_element.text }) + elif exception_element.tag == "calendar:ResponseType": + exception_dict.update({ "calendar_ResponseType" : exception_element.text }) + elif exception_element.tag == "calendar:Location": + exception_dict.update({ "calendar_Location" : exception_element.text }) + elif exception_element.tag == "calendar:MeetingStatus": + exception_dict.update({ "calendar_MeetingStatus" : exception_element.text }) + elif exception_element.tag == "calendar:EndTime": + exception_dict.update({ "calendar_EndTime" : exception_element.text }) + elif exception_element.tag == "calendar:DtStamp": + exception_dict.update({ "calendar_DtStamp" : exception_element.text }) + elif exception_element.tag == "calendar:Sensitivity": + exception_dict.update({ "calendar_Sensitivity" : exception_element.text }) + elif exception_element.tag == "calendar:Reminder": + exception_dict.update({ "calendar_Reminder" : exception_element.text }) + elif exception_element.tag == "calendar:Subject": + exception_dict.update({ "calendar_Subject" : exception_element.text }) + exceptions_list.append(exception_dict) + calendar_dict.update({ "calendar_Exceptions" : exceptions_list }) + elif element.tag == "calendar:Location": + calendar_dict.update({ "calendar_Location" : element.text }) + elif element.tag == "calendar:MeetingStatus": + calendar_dict.update({ "calendar_MeetingStatus" : element.text }) + elif element.tag == "airsyncbase:NativeBodyType": + calendar_dict.update({ "airsyncbase_NativeBodyType" : element.text }) + elif element.tag == "calendar:OnlineMeetingConfLink": + calendar_dict.update({ "calendar_OnlineMeetingConfLink" : element.text }) + elif element.tag == "calendar:OnlineMeetingExternalLink": + calendar_dict.update({ "calendar_OnlineMeetingExternalLink" : element.text }) + elif element.tag == "calendar:OrganizerEmail": + calendar_dict.update({ "calendar_OrganizerEmail" : element.text }) + elif element.tag == "calendar:OrganizerName": + calendar_dict.update({ "calendar_OrganizerName" : element.text }) + elif element.tag == "calendar:Recurrence": + recurrence_dict = {} + for recurrence_element in element.get_children(): + if recurrence_element.tag == "calendar:Type": + recurrence_dict.update({ "calendar_Type" : recurrence_element.text }) + elif recurrence_element.tag == "calendar:Occurrences": + recurrence_dict.update({ "calendar_Occurrences" : recurrence_element.text }) + elif recurrence_element.tag == "calendar:FirstDayOfWeek": + recurrence_dict.update({ "calendar_FirstDayOfWeek" : recurrence_element.text }) + elif recurrence_element.tag == "calendar:Interval": + recurrence_dict.update({ "calendar_Interval" : recurrence_element.text }) + elif recurrence_element.tag == "calendar:IsLeapMonth": + recurrence_dict.update({ "calendar_IsLeapMonth" : recurrence_element.text }) + elif recurrence_element.tag == "calendar:WeekOfMonth": + recurrence_dict.update({ "calendar_WeekOfMonth" : recurrence_element.text }) + elif recurrence_element.tag == "calendar:DayOfMonth": + recurrence_dict.update({ "calendar_DayOfMonth" : recurrence_element.text }) + elif recurrence_element.tag == "calendar:DayOfWeek": + recurrence_dict.update({ "calendar_DayOfWeek" : recurrence_element.text }) + elif recurrence_element.tag == "calendar:MonthOfYear": + recurrence_dict.update({ "calendar_MonthOfYear" : recurrence_element.text }) + elif recurrence_element.tag == "calendar:Until": + recurrence_dict.update({ "calendar_Until" : recurrence_element.text }) + elif recurrence_element.tag == "calendar:CalendarType": + recurrence_dict.update({ "calendar_CalendarType" : recurrence_element.text }) + calendar_dict.update({ "calendar_Recurrence" : recurrence_dict }) + elif element.tag == "calendar:Reminder": + calendar_dict.update({ "calendar_Reminder" : element.text }) + elif element.tag == "calendar:ResponseRequested": + calendar_dict.update({ "calendar_ResponseRequested" : element.text }) + elif element.tag == "calendar:ResponseType": + calendar_dict.update({ "calendar_ResponseType" : element.text }) + elif element.tag == "calendar:Sensitivity": + calendar_dict.update({ "calendar_Sensitivity" : element.text }) + elif element.tag == "calendar:StartTime": + calendar_dict.update({ "calendar_StartTime" : element.text }) + elif element.tag == "calendar:Subject": + calendar_dict.update({ "calendar_Subject" : element.text }) + elif element.tag == "calendar:Timezone": + calendar_dict.update({ "calendar_Timezone" : element.text }) + elif element.tag == "calendar:UID": + calendar_dict.update({ "calendar_UID" : element.text }) + return calendar_dict + +class calendar_Attendee(object): + def __init__(self, email=None, name=None, attendee_status=None, attendee_type=None): + self.email = email + self.name = name + self.attendee_status = attendee_status + self.attendee_type = attendee_type + self.delim = "/&*/" + def init_from_storage(self, blob): + import base64 + parts = base64.b64decode(blob).split(self.delim) + self.email = parts[0] + self.name = parts[1] + self.attendee_status = parts[2] + self.attendee_type = parts[3] + def marshal_for_storage(self): + import base64 + return base64.b64encode("%s%s%s%s%s%s%s%s" % (self.email, self.delim, self.name, self.delim, self.attendee_status, self.delim, self.attendee_type)) + +class calendar_Recurrence(object): + def __init__(self): + self.calendar_Type = calendar_Type.Daily # Required. Byte. See "MSASCAL.Type" for enum. + self.calendar_Interval = 1 # Required. Integer. An Interval element value of 1 indicates that the meeting occurs every week, month, or year, depending upon the value of "self".Type. An Interval value of 2 indicates that the meeting occurs every other week, month, or year. + self.calendar_Until = None # Optional. dateTime. End of recurrence. + self.calendar_Occurances = None # Optional. Integer. Number of occurrences before the series of recurring meetings ends. + self.calendar_WeekOfMonth = None # Optional. Integer. The week of the month in which the meeting recurs. Required when the Type is set to a value of 6. + self.calendar_DayOfMonth = None # Optional. Integer. The day of the month on which the meeting recurs. Required when the Type is set to a value of 2 or 5. + self.calendar_DayOfWeek = None # Optional. Integer. See "MSASCAL.DayOfWeek" emun for values. The day of the week on which this meeting recurs. Can be anded together for multiple days of week. Required when the Type is set to a value of 1 or 6 + self.calendar_MonthOfYear = None # Optional. Integer. The month of the year in which the meeting recurs. Required when the Type is set to a value of 6. + self.calendar_CalendarType = calendar_CalendarType.Default # Required. Byte. See "MSASCAL.calendar_CalendarType" for enum. + self.calendar_IsLeapMonth = 0 # Optional. Byte. Does the recurrence takes place in the leap month of the given year? + self.calendar_FirstDayOfWeek = calendar_FirstDayOfWeek.Sunday # Optional. Byte. See "MSASCAL.calendar_FirstDayOfWeek" for enum. What is considered the first day of the week for this recurrence? + +class calendar_Exception(object): + def __init__(self): + self.calendar_Deleted = None # This element is optional. + self.calendar_ExceptionStartTime = None # One instance of this element is required. + self.calendar_Subject = None # This element is optional. + self.calendar_StartTime = None # This element is optional. + self.calendar_EndTime = None # This element is optional. + self.airsyncbase_Body = None # This element is optional. + self.calendar_Location = None # This element is optional. + self.calendar_Categories = None # This element is optional. + self.calendar_Sensitivity = None # This element is optional. + self.calendar_BusyStatus = None # This element is optional. + self.calendar_AllDayEvent = None # This element is optional. + self.calendar_Reminder = None # This element is optional. + self.calendar_DtStamp = None # This element is optional. + self.calendar_MeetingStatus = None # This element is optional. + self.calendar_Attendees = None # This element is optional. + self.calendar_AppointmentReplyTime = None # This element is optional in command responses. It is not included in command requests. + self.calendar_ResponseType = None # This element is optional in command responses. It is not included in command requests. + self.calendar_OnlineMeetingConfLink = None # This element is optional in command responses. It is not included in command requests. + self.calendar_OnlineMeetingExternalLink = None # This element is optional in command responses. It is not included in command requests. \ No newline at end of file diff --git a/peas/pyActiveSync/objects/MSASCMD.py b/peas/pyActiveSync/objects/MSASCMD.py new file mode 100644 index 0000000..97c6db8 --- /dev/null +++ b/peas/pyActiveSync/objects/MSASCMD.py @@ -0,0 +1,385 @@ +######################################################################## +# Copyright (C) 2013 Sol Birnbaum +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +######################################################################## + +"""[MS-ASCMD] Generic/various class namespace objects""" + +class FolderHierarchy: + Status = { + "1": ( "Success.", "Server successfully completed command.", "None.", "Global" ), + "6": ( "An error occurred on the server.", "Server misconfiguration, temporary system issue, or bad item. This is frequently a transient condition.", "Retry the FolderSync command. If continued attempts to synchronization fail, consider returning to synchronization key zero (0).", "Global" ), + "9": ( "Synchronization key mismatch or invalid synchronization key.", "The client sent a malformed or mismatched synchronization key, or the synchronization state is corrupted on the server.", "Delete folders added since last synchronization and return to synchronization key to zero (0).", "Global" ), + "10": ( "Malformed request.", "The client sent a command request that contains a semantic error, or the client attempted to create a default folder, such as the Inbox folder, Outbox folder, or Contacts folder.", "Double-check the request for accuracy.", "Global" ), + "11": ( "An unknown error occurred.", "Unknown.", "None.", "Global" ), + "12": ( "Code unknown.", "Unusual back-end issue.", "None.", "Global" ) + } + class DefaultFoldersIds: + Calendar = 1 + Contacts = 2 + DeletedItems = 3 + Drafts = 4 + Inbox = 5 + Journal = 6 + JunkEmail = 7 + Notes = 8 + Outbox = 9 + SentItems = 10 + Tasks = 11 + RecipientInfo = "RI" + class Type: + Generic = 1 + Inbox = 2 + Drafts = 3 + DeletedItems = 4 + SentItems = 5 + Outbox = 6 + Tasks = 7 + Calendar = 8 + Contacts = 9 + Notes = 10 + Journal = 11 + JunkEmail = 12 + MailG = 12 + CalendarG = 13 + ContactsG = 14 + TasksG = 15 + JournalG = 16 + NotesG = 17 + RecipientInfo = 19 + FolderTypeToClass = { + "1" : "Email", + "2" : "Email", + "3" : "Email", + "4" : "Email", + "5" : "Email", + "6" : "Email", + "7" : "Tasks", + "8" : "Calendar", + "9" : "Contacts", + "10" : "Notes", + "11" : "Email", + "12" : "Email", + "13" : "Calendar", + "14" : "Contacts", + "15" : "Tasks", + "16" : "Email", + "17" : "Notes", + "19" : "Contacts" #? + } + + class FolderCreate: + Status = { + "2": ( "A folder that has this name already exists.", "The parent folder already contains a folder that has this name.", "Prompt user to supply a unique name.", "Item" ), + "3": ( "The specified parent folder is a special system folder.", "The specified parent folder is the Recipient information folder.", "Create the folder under a different parent.", "Item" ), + "5": ( "The specified parent folder was not found.", "The parent folder does not exist on the server, possibly because it has been deleted or renamed.", "Issue a FolderSync command for the new hierarchy and prompt the user for a new parent folder.", "Item" ) + } + + class Type: + Generic = 1 + Mail = 12 + Calendar = 13 + Contacts = 14 + Tasks = 15 + Journal = 16 + Notes = 17 + + class FolderDelete: + Status = { + "3": ( "The specified folder is a special system folder, such as the Inbox folder, Outbox folder, Contacts folder, Recipient information, or Drafts folder, and cannot be deleted by the client.", "The client specified a special folder in a FolderDelete command request. special folders cannot be deleted.", "None.", "Item" ), + "4": ( "The specified folder does not exist.", "The client specified a nonexistent folder in a FolderDelete command request.", "Issue a FolderSync command for the new hierarchy.", "Item" ) + } + class FolderUpdate: + Status = { + "2": ( "A folder with that name already exists or the specified folder is a special folder.", "A folder with that name already exists or the specified folder is a special folder, such as the Inbox, Outbox, Contacts, or Drafts folders. Special folders cannot be updated.", "None.", "Item" ), + "3": ( "The specified folder is the Recipient information folder, which cannot be updated by the client.", "The client specified the Recipient information folder, which is a special folder. Special folders cannot be updated.", "None.", "Item" ), + "4": ( "The specified folder does not exist.", "Client specified a nonexistent folder in a FolderUpdate command request.", "Issue a FolderSync command for the new hierarchy.", "Item" ), + "5": ( "The specified parent folder was not found.", "Client specified a nonexistent folder in a FolderUpdate command request.", "Issue a FolderSync command for the new hierarchy.", "Item" ), + } + class FolderSync: + Status = {} + + class Folder: + def __init__(self, ParentId=None, DisplayName=None, Type=None, ServerId=None): + self.ServerId = ServerId + self.ParentId = ParentId + self.DisplayName = DisplayName + self.Type = Type + +class ResolveRecipients: + class ResolveRecipients: + Status = { + "1": "Success.", + "5": "Protocol error. Either an invalid parameter was specified or the range exceeded limits.", + "6": "An error occurred on the server. The client SHOULD retry the request.", + } + class Response: + Status = { + "1": "The recipient was resolved successfully.", + "2": "The recipient was found to be ambiguous. The returned list of recipients are suggestions. No certificate nodes were returned. Prompt the user to select the intended recipient from the list returned.", + "3": "The recipient was found to be ambiguous. The returned list is a partial list of suggestions. The total count of recipients can be obtained from the RecipientCount element. No certificate nodes were returned. Prompt the user to select the intended recipient from the list returned or to get more recipients.", + "4": "The recipient did not resolve to any contact or GAL entry. No certificates were returned. Inform the user of the error and direct the user to check the spelling.", + } + class Availability: + Status = { + "1": "Free/busy data was successfully retrieved for a given recipient. This value does not indicate that the response is complete.", + "160": "There were more than 100 recipients identified by the To elements in the ResolveRecipient request.", + "161": "The distribution group identified by the To element of the ResolveRecipient request included more than 20 recipients.", + "162": "The free/busy data could not be retrieved by the server due to a temporary failure. The client SHOULD reissue the request. This error is caused by a timeout value being reached while requesting free/busy data for some users, but not others.", + "163": "Free/busy data could not be retrieved from the server for a given recipient. Clients SHOULD NOT reissue the request as it is caused by a lack of permission to retrieve the data.", + } + class Certificates: + Status = { + "1": "One or more certificates were successfully returned.", + "7": "The recipient does not have a valid S/MIME certificate. No certificates were returned.", + "8": "The global certificate limit was reached and the recipient's certificate could not be returned. The count certificates not returned can be obtained from the CertificateCount element. Retry with fewer recipients if possible, otherwise prompt the user.", + } + class Picture: + Status = { + "1": "The contact photo was retrieved successfully.", + "173": "The user does not have a contact photo.", + "174": "The contact photo exceeded the size limit set by the MaxSize element.", + "175": "The number of contact photos returned exceeded the size limit set by the MaxPictures element.", + } + + class CertificateRetrieval: + DoNotRetrieve = 0 + RetrieveFull = 1 + RetrieveMini = 2 + class Type: + Contacts = 1 + GAL = 2 + +class Provision: + Status = { + "1": ( "Success.", "Server successfully completed command.", "None.", "Global" ), + "2": ("Protocol error.", "Syntax error in the Provision command request.", "Fix syntax in the request and resubmit."), + "3": ("An error occurred on the server.", "Server misconfiguration, temporary system issue, or bad item. This is frequently a transient condition.", "Retry."), + } + class Policy: + Status = { + "1": ("Success.", "The requested policy data is included in the response.", "Apply the policy."), + "2": ("Policy not defined.", "No policy of the requested type is defined on the server.", "Stop sending policy information. No policy is implemented."), + "3": ("The policy type is unknown.", "The client sent a policy that the server does not recognize.", "Issue a request with a value of \"MS-EAS-Provisioning-WBXML\" in the PolicyType element."), + "4": ("Policy data is corrupt.", "The policy data on the server is corrupt.", "Server administrator intervention is required."), + "5": ("Policy key mismatch.", "The client is trying to acknowledge an out-of-date or invalid policy.", "Issue a new Provision request to obtain a valid policy key."), + } + +class Ping: + Status = { + "1": ( "The heartbeat interval expired before any changes occurred in the folders being monitored. ", "", "Reissue the Ping command request.", "Global" ), + "2": ( "Changes occurred in at least one of the monitored folders. The response specifies the changed folders.", "", "Issue a Sync command request for each folder that was specified in the Ping command response to retrieve the server changes. Reissue the Ping command when the Sync command completes to stay up to date.", "Global" ), + "3": ( "The Ping command request omitted required parameters.", "The Ping command request did not specify all the necessary parameters. The client MUST issue a Ping request that includes both the heartbeat interval and the folder list at least once. The server saves the heartbeat interval value, so only the folder list is required on subsequent requests.", "Reissue the Ping command request with the entire XML body.", "Global" ), + "4": ( "Syntax error in Ping command request.", "Frequently caused by poorly formatted WBXML.", "Double-check the request for accuracy.", "Global" ), + "5": ( "The specified heartbeat interval is outside the allowed range. For intervals that were too short, the response contains the shortest allowed interval. For intervals that were too long, the response contains the longest allowed interval.", "The client sent a Ping command request with a heartbeat interval that was either too long or too short.", "Reissue the Ping command by using a heartbeat interval inside the allowed range. Setting the interval to the value returned in the Ping response will most closely accommodate the original value specified.", "Global" ), + "6": ( "The Ping command request specified more than the allowed number of folders to monitor. The response indicates the allowed number in the MaxFolders element.", "The client sent a Ping command request that specified more folders than the server is configured to monitor.", "Direct the user to select fewer folders to monitor. Resend the Ping command request with the new, shorter list.", "Global" ), + "7": ( "Direct the user to select fewer folders to monitor. Resend the Ping command request with the new, shorter list.", "The folder hierarchy is out of date; a folder hierarchy sync is required.", "Issue a FolderSync command to get the new hierarchy and prompt the user, if it is necessary, for new folders to monitor. Reissue the Ping command.", "Global" ), + "8": ( "An error occurred on the server.", "Server misconfiguration, temporary system issue, or bad item. This is frequently a transient condition.", "Retry the Ping command.", "Global" ), + } + class Class: + Email = "Email" + Calendar = "Calendar" + Contacts = "Contacts" + Tasks = "Tasks" + Notes = "Notes" + +class GetItemEstimate: + Status = { + "1": ( "Success.", "Server successfully completed command.", "None.", "Global" ), + "2": ( "A collection was invalid or one of the specified collection IDs was invalid. ", "One or more of the specified folders does not exist or an incorrect folder was requested.", "Issue a FolderSync command to get the new hierarchy. Then retry with a valid collection or collection ID.", "Item" ), + "3": ( "The synchronization state has not been primed.", "The client has issued a GetItemEstimate command without first issuing a Sync command request with an SyncKey element value of zero (0).", "Issue a Sync command with synchronization key of zero (0) before issuing the GetItemEstimate command again.", "Item" ), + "4": ( "The specified synchronization key was invalid.", "Malformed or mismatched synchronization key, or the synchronization state is corrupted on the server.", "Issue a successful Sync command prior to issuing the GetItemEstimate command again. If the error is repeated, issue a Sync command with an SyncKey element value of zero (0).", "Global" ) + } + +class ItemOperations: + class StoreTypes: + DocumentLibrary = "Document Library" + Mailbox = "Mailbox" + +class MeetingResponse: + class UserResponse: + Accepted = 1 + Tentitive = 2 + Declined = 3 + +class MoveItems: + Status = { + "1": ( "Invalid source collection ID or invalid source Item ID.", "The source folder collection ID (CollectionId element value) is not recognized by the server, possibly because the source folder has been deleted. Or, the item with the Item ID (SrcMsgId element) has been previously moved out of the folder with the Folder ID (SrcFldId element).", "Issue a FolderSync command to get the new hierarchy. Then, issue a Sync command for the SrcFldId and reissue the MoveItems command request if the items are still present in this source collection.", "Item" ), + "2": ( "Invalid destination collection ID.", "The destination folder collection ID (CollectionId element value) is not recognized by the server, possibly because the source folder has been deleted.", "Issue a FolderSync command to get the new hierarchy. Then, use a valid collection ID.", "Item" ), + "3": ( "Success.", "Server successfully completed command.", "None.", "Global" ), + "4": ( "Source and destination collection IDs are the same.", "The client supplied a destination folder that is the same as the source.", "Send only requests where the CollectionId element values for the source and destination differ.", "Item" ), + "5": ( "One of the following failures occurred: the item cannot be moved to more than one item at a time, or the source or destination item was locked.", "More than one DstFldId element was included in the request or an item with that name already exists.", "Retry the MoveItems command request with only one DstFldId element or move the item to another location.", "Global" ), + "7": ( "Source or destination item was locked.", "Transient server condition.", "Retry the MoveItems command request.", "Item" ), + } + +class Search: + class Name: + Mailbox = "Mailbox" + DocumentLibrary = "DocumentLibrary" + GAL = "GAL" + +class Sync: + Status = { + "1": ( "Success.", "Server successfully completed command.", "None.", "Global" ), + "3": ( "Invalid synchronization key.", "Invalid or mismatched synchronization key, or the synchronization state corrupted on server.", "MUST return to SyncKey element value of 0 for the collection. The client SHOULD either delete any items that were added since the last successful Sync or the client MUST add those items back to the server after completing the full resynchronization. ", "Global" ), + "4": ( "Protocol error.", "There was a semantic error in the synchronization request. The client is issuing a request that does not comply with the specification requirements.", "Double-check the request for accuracy and retry the Sync command.", "Global or Item" ), + "5": ( "Server error.", "Server misconfiguration, temporary system issue, or bad item. This is frequently a transient condition.", "Retry the synchronization. If continued attempts to synchronization fail, consider returning to synchronization key 0.", "Global" ), + "6": ( "Error in client/server conversion.", "The client has sent a malformed or invalid item.", "Stop sending the item. This is not a transient condition. ", "Item" ), + "7": ( "Conflict matching the client and server object.", "The client has changed an item for which the conflict policy indicates that the server's changes take precedence.", "If it is necessary, inform the user that the change they made to the item has been overwritten by a server change.", "Item" ), + "8": ( "Object not found.", "The client issued a fetch or change operation that has an ItemID value that is no longer valid on the server (for example, the item was deleted).", "Issue a synchronization request and prompt the user if necessary.", "Item" ), + "9": ( "The Sync command cannot be completed.", "User account could be out of disk space.", "Free space in the user's mailbox and retry the Sync command.", "Item" ), + "12": ( "The folder hierarchy has changed.", "Mailbox folders are not synchronized.", "Perform a FolderSync command and then retry the Sync command.", "Global" ), + "13": ( "The Sync command request is not complete.", "An empty or partial Sync command request is received and the cached set of notify-able collections is missing.", "Resend a full Sync command request.", "Item" ), + "14": ( "Invalid Wait or HeartbeatInterval value.", "The Sync request was processed successfully but the wait interval (Wait element value) or heartbeat interval (HeartbeatInterval element value) that is specified by the client is outside the range set by the server administrator.\r\n\r\nIf the HeartbeatInterval element value or Wait element value included in the Sync request is larger than the maximum allowable value, the response contains a Limit element that specifies the maximum allowed value.\r\n\r\nIf the HeartbeatInterval element value or Wait value included in the Sync request is smaller than the minimum allowable value, the response contains a Limit element that specifies the minimum allowed value.", "Update the Wait element value according to the Limit element and then resend the Sync command request.", "Item" ), + "15": ( "Invalid Sync command request.", "Too many collections are included in the Sync request.", "Notify the user and synchronize fewer folders within one request.", "Item" ), + "16": ( "Retry", "Something on the server caused a retriable error.", "Resend the request.", "Global" ), + } + +CommonStatuses = { + "101": ("InvalidContent", "The body of the HTTP request sent by the client is invalid.Ensure the HTTP request is using the specified Content-Type and length, and that the request is not missing.Examples:Ping command with a text/plain body, or SendMail command with version 12.1 and a WBXML body. "), + "102": ("InvalidWBXML", "The request contains WBXML but it could not be decoded into XML."), + "103": ("InvalidXML", "The XML provided in the request does not follow the protocol requirements."), + "104": ("InvalidDateTime", "The request contains a timestamp that could not be parsed into a valid date and time."), + "105": ("InvalidCombinationOfIDs", "The request contains a combination of parameters that is invalid."), + "106": ("InvalidIDs", "The request contains one or more IDs that could not be parsed into valid values.That is different from specifying an ID in the proper format that does not resolve to an existing item."), + "107": ("InvalidMIME", "The request contains MIME that could not be parsed."), + "108": ("DeviceIdMissingOrInvalid", "The device ID is either missing or has an invalid format."), + "109": ("DeviceTypeMissingOrInvalid", "The device type is either missing or has an invalid format."), + "110": ("ServerError", "The server encountered an unknown error, the device SHOULD NOT retry later."), + "111": ("ServerErrorRetryLater", "The server encountered an unknown error, the device SHOULD retry later."), + "112": ("ActiveDirectoryAccessDenied", "The server does not have access to read/write to an object in the directory service."), + "113": ("MailboxQuotaExceeded", "The mailbox has reached its size quota."), + "114": ("MailboxServerOffline", "The mailbox server is offline."), + "115": ("SendQuotaExceeded", "The request would exceed the send quota."), + "116": ("MessageRecipientUnresolved", "One of the recipients could not be resolved to an email address."), + "117": ("MessageReplyNotAllowed", "The mailbox server will not allow a reply of this message."), + "118": ("Message PreviouslySent", "The message was already sent in a previous request. The server determined this by remembering the ClientId values of the last few sent messages. This request contains a ClientId that was already used in a recent message."), + "119": ("MessageHasNoRecipient", "The message being sent contains no recipient."), + "120": ("MailSubmissionFailed", "The server failed to submit the message for delivery."), + "121": ("MessageReplyFailed", "The server failed to create a reply message."), + "122": ("AttachmentIsTooLarge", "The attachment is too large to be processed by this request."), + "123": ("UserHasNoMailbox", "A mailbox could not be found for the user."), + "124": ("UserCannotBeAnonymous", "The request was sent without credentials. Anonymous requests are not allowed."), + "125": ("UserPrincipalCouldNotBeFound", "The user was not found in the directory service."), + "126": ("UserDisabledForSync", "The user object in the directory service indicates that this user is not allowed to use ActiveSync."), + "127": ("UserOnNewMailboxCannotSync", "The server is configured to prevent users from syncing."), + "128": ("UserOnLegacyMailboxCannotSync", "The server is configured to prevent users on legacy servers from syncing."), + "129": ("DeviceIsBlockedForThisUser", "The user is configured to allow only some devices to sync. This device is not the allowed device."), + "130": ("AccessDenied", "The user is not allowed to perform that request."), + "131": ("AccountDisabled", "The user's account is disabled."), + "132": ("SyncStateNotFound", "The server's data file that contains the state of the client was unexpectedly missing. It might have disappeared while the request was in progress. The next request will likely answer a sync key error and the device will be forced to do full sync."), + "133": ("SyncStateLocked", "The server's data file that contains the state of the client is locked, possibly because the mailbox is being moved or was recently moved."), + "134": ("SyncStateCorrupt", "The server's data file that contains the state of the client appears to be corrupt."), + "135": ("SyncStateAlreadyExists", "The server's data file that contains the state of the client already exists. This can happen with two initial syncs are executed concurrently."), + "136": ("SyncStateVersionInvalid", "The version of the server's data file that contains the state of the client is invalid."), + "137": ("CommandNotSupported", "The command is not supported by this server."), + "138": ("VersionNotSupported", "The command is not supported in the protocol version specified."), + "139": ("DeviceNotFullyProvisionable", "The device uses a protocol version that cannot send all the policy settings the admin enabled."), + "140": ("RemoteWipeRequested", "A remote wipe was requested. The device SHOULD provision to get the request and then do another provision to acknowledge it."), + "141": ("LegacyDeviceOnStrictPolicy", "A policy is in place but the device is not provisionable."), + "142": ("DeviceNotProvisioned", "There is a policy in place; the device needs to provision."), + "143": ("PolicyRefresh", "The policy is configured to be refreshed every few hours. The device needs to re-provision."), + "144": ("InvalidPolicyKey", "The device's policy key is invalid. The policy has probably changed on the server. The device needs to re-provision."), + "145": ("ExternallyManagedDevicesNotAllowed", "The device claimed to be externally managed, but the server doesn't allow externally managed devices to sync."), + "146": ("NoRecurrenceInCalendar", "The request tried to forward an occurrence of a meeting that has no recurrence."), + "147": ("UnexpectedItemClass", "The request tried to operate on a type of items unknown to the server."), + "148": ("RemoteServerHasNoSSL", "The request needs to be proxied to another server but that server doesn't have Secure Sockets Layer enabled. This server is configured to only proxy requests to servers with SSL enabled."), + "149": ("InvalidStoredRequest", "The server had stored the previous request from that device. When the device sent an empty request, the server tried to re-execute that previous request but it was found to be impossible. The device needs to send the full request again."), + "150": ("ItemNotFound", "The ItemId value specified in the SmartReply command or SmartForward command request could not be found in the mailbox."), + "151": ("TooManyFolders", "The mailbox contains too many folders. By default, the mailbox cannot contain more than 1000 folders."), + "152": ("NoFoldersFound", "The mailbox contains no folders."), + "153": ("ItemsLostAfterMove", "After moving items to the destination folder, some of those items could not be found."), + "154": ("FailureInMoveOperation", "The mailbox server returned an unknown error while moving items."), + "155": ("MoveCommandDisallowedForNonPersistentMoveAction", "An ItemOperations command request to move a conversation is missing the MoveAlways element."), + "156": ("MoveCommandInvalidDestinationFolder", "The destination folder for the move is invalid."), + "160": ("AvailabilityTooManyRecipients", "The command has reached the maximum number of recipients that it can request availability for."), + "161": ("AvailabilityDLLimitReached", "The size of the distribution list is larger than the availability service is configured to process."), + "162": ("AvailabilityTransientFailure", "Availability service request failed with a transient error."), + "163": ("AvailabilityFailure", "Availability service request failed with an error."), + "164": ("BodyPartPreferenceTypeNotSupported", "The BodyPartPreference node has an unsupported Type element value."), + "165": ("DeviceInformationRequired", "The required DeviceInformation element is missing in the Provision request."), + "166": ("InvalidAccountId", "The AccountId value is not valid."), + "167": ("AccountSendDisabled", "The AccountId value specified in the request does not support sending email."), + "168": ("IRM_FeatureDisabled", "The Information Rights Management feature is disabled."), + "169": ("IRM_TransientError", "Information Rights Management encountered an error."), + "170": ("IRM_PermanentError", "Information Rights Management encountered an error."), + "171": ("IRM_InvalidTemplateID", "The Template ID value is not valid."), + "172": ("IRM_OperationNotPermitted", "Information Rights Management does not support the specified operation."), + "173": ("NoPicture", "The user does not have a contact photo."), + "174": ("PictureTooLarge", "The contact photo exceeds the size limit set by the MaxSize element."), + "175": ("PictureLimitReached", "The number of contact photos returned exceeds the size limit set by the MaxPictures element."), + "176": ("BodyPart_ConversationTooLarge", "The conversation is too large to compute the body parts. Try requesting the body of the item again, without body parts."), + } + +def as_status(cmd, status): + info= None + if cmd == "Provision": + try: + info = Provision.Status[status] + except KeyError: + try: + info = Provision.Policy.Status[status] + except KeyError: + try: + info = CommonStatuses[status] + except KeyError: + info = "Status %s for command %s not found" % (cmd, status) + elif cmd == "FolderSync": + try: + info = FolderHierarchy.Status[status] + except KeyError: + try: + info = FolderHierarchy.FolderSync.Status[status] + except KeyError: + try: + info = CommonStatuses[status] + except KeyError: + info = "Status %s for command %s not found" % (cmd, status) + elif cmd == "FolderCreate": + try: + info = FolderHierarchy.Status[status] + except KeyError: + try: + info = FolderHierarchy.FolderCreate.Status[status] + except KeyError: + try: + info = CommonStatuses[status] + except KeyError: + info = "Status %s for command %s not found" % (cmd, status) + elif cmd == "GetItemEstimate": + try: + info = GetItemEstimate.Status[status] + except KeyError: + try: + info = CommonStatuses[status] + except KeyError: + info = "Status %s for command %s not found" % (cmd, status) + if info: + if isinstance(info, tuple): + return_str = "\r\n%s status number: %s\r\n-----------------" % (cmd, status) + for i in range(0,len(info)): + if i == 0: + return_str += "\r\nStatus message: %s" % info[i] + elif i == 1: + return_str += "\r\nStatus details: %s" % info[i] + elif i == 2: + return_str += "\r\nStatus suggested resolution: %s" % info[i] + elif i == 3: + return_str += "\r\nStatus scope: %s" % info[i] + return return_str + else: + return info diff --git a/peas/pyActiveSync/objects/MSASCNTC.py b/peas/pyActiveSync/objects/MSASCNTC.py new file mode 100644 index 0000000..c510d3e --- /dev/null +++ b/peas/pyActiveSync/objects/MSASCNTC.py @@ -0,0 +1,166 @@ +######################################################################## +# Copyright (C) 2013 Sol Birnbaum +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +######################################################################## + +"""[MS-ASCNTC] Contact objects""" + +from MSASEMAIL import airsyncbase_Body + +def parse_contact(data): + contact_dict = {} + contact_base = data.get_children() + contact_dict.update({"server_id" : contact_base[0].text}) + contact_elements = contact_base[1].get_children() + for element in contact_elements: + if element.tag == "contacts2:AccountName": + contact_dict.update({ "contacts2_AccountName" : element.text }) + elif element.tag == "contacts:Alias": + contact_dict.update({ "contacts_Alias" : element.text }) + elif element.tag == "contacts:Anniversary": + contact_dict.update({ "contacts_Anniversary" : element.text }) + elif element.tag == "contacts:AssistantName": + contact_dict.update({ "contacts_AssistantName" : element.text }) + elif element.tag == "contacts:AssistantPhoneNumber": + contact_dict.update({ "contacts_AssistantPhoneNumber" : element.text }) + elif element.tag == "contacts:Birthday": + contact_dict.update({ "contacts_Birthday" : element.text }) + elif element.tag == "airsyncbase:Body": + body = airsyncbase_Body() + body.parse(element) + contact_dict.update({ "airsyncbase_Body" : body }) + elif element.tag == "contacts:BusinessAddressCity": + contact_dict.update({ "contacts_BusinessAddressCity" : element.text }) + elif element.tag == "contacts:BusinessAddressCountry": + contact_dict.update({ "contacts_BusinessAddressCountry" : element.text }) + elif element.tag == "contacts:BusinessAddressPostalCode": + contact_dict.update({ "contacts_BusinessAddressPostalCode" : element.text }) + elif element.tag == "contacts:BusinessAddressState": + contact_dict.update({ "contacts_BusinessAddressState" : element.text }) + elif element.tag == "contacts:BusinessAddressStreet": + contact_dict.update({ "contacts_BusinessAddressStreet" : element.text }) + elif element.tag == "contacts:BusinessFaxNumber": + contact_dict.update({ "contacts_BusinessFaxNumber" : element.text }) + elif element.tag == "contacts:BusinessPhoneNumber": + contact_dict.update({ "contacts_BusinessPhoneNumber" : element.text }) + elif element.tag == "contacts:Business2PhoneNumber": + contact_dict.update({ "contacts_Business2PhoneNumber" : element.text }) + elif element.tag == "contacts:CarPhoneNumber": + contact_dict.update({ "contacts_CarPhoneNumber" : element.text }) + elif element.tag == "contacts:Categories": + categories_list = [] + categories = element.get_children() + for category_element in categories: + categories_list.append(category_element.text) + contact_dict.update({ "contacts_Categories" : categories_list }) + elif element.tag == "contacts:Children": + children_list = [] + children = element.get_children() + for child_element in children: + children_list.append(child_element.text) + contact_dict.update({ "contacts_Children" : children_list }) + elif element.tag == "contacts2:CompanyMainPhone": + contact_dict.update({ "contacts2_CompanyMainPhone" : element.text }) + elif element.tag == "contacts:CompanyName": + contact_dict.update({ "contacts_CompanyName" : element.text }) + elif element.tag == "contacts2:CustomerId": + contact_dict.update({ "contacts2_CustomerId" : element.text }) + elif element.tag == "contacts:Department": + contact_dict.update({ "contacts_Department" : element.text }) + elif element.tag == "contacts:Email1Address": + contact_dict.update({ "contacts_Email1Address" : element.text }) + elif element.tag == "contacts:Email2Address": + contact_dict.update({ "contacts_Email2Address" : element.text }) + elif element.tag == "contacts:Email3Address": + contact_dict.update({ "contacts_Email3Address" : element.text }) + elif element.tag == "contacts:FileAs": + contact_dict.update({ "contacts_FileAs" : element.text }) + elif element.tag == "contacts:FirstName": + contact_dict.update({ "contacts_FirstName" : element.text }) + elif element.tag == "contacts2:GovernmentId": + contact_dict.update({ "contacts2_GovernmentId" : element.text }) + elif element.tag == "contacts:HomeAddressCity": + contact_dict.update({ "contacts_HomeAddressCity" : element.text }) + elif element.tag == "contacts:HomeAddressCountry": + contact_dict.update({ "contacts_HomeAddressCountry" : element.text }) + elif element.tag == "contacts:HomeAddressPostalCode": + contact_dict.update({ "contacts_HomeAddressPostalCode" : element.text }) + elif element.tag == "contacts:HomeAddressState": + contact_dict.update({ "contacts_HomeAddressState" : element.text }) + elif element.tag == "contacts:HomeAddressStreet": + contact_dict.update({ "contacts_HomeAddressStreet" : element.text }) + elif element.tag == "contacts:HomeFaxNumber": + contact_dict.update({ "contacts_HomeFaxNumber" : element.text }) + elif element.tag == "contacts:HomePhoneNumber": + contact_dict.update({ "contacts_HomePhoneNumber" : element.text }) + elif element.tag == "contacts:Home2PhoneNumber": + contact_dict.update({ "contacts_Home2PhoneNumber" : element.text }) + elif element.tag == "contacts2:IMAddress": + contact_dict.update({ "contacts2_IMAddress" : element.text }) + elif element.tag == "contacts2:IMAddress2": + contact_dict.update({ "contacts2_IMAddress2" : element.text }) + elif element.tag == "contacts2:IMAddress3": + contact_dict.update({ "contacts_IMAddress3" : element.text }) + elif element.tag == "contacts:JobTitle": + contact_dict.update({ "contacts_JobTitle" : element.text }) + elif element.tag == "contacts:LastName": + contact_dict.update({ "contacts_LastName" : element.text }) + elif element.tag == "contacts2:ManagerName": + contact_dict.update({ "contacts2_ManagerName" : element.text }) + elif element.tag == "contacts:MiddleName": + contact_dict.update({ "contacts_MiddleName" : element.text }) + elif element.tag == "contacts2:MMS": + contact_dict.update({ "contacts2_MMS" : element.text }) + elif element.tag == "contacts:MobilePhoneNumber": + contact_dict.update({ "contacts_MobilePhoneNumber" : element.text }) + elif element.tag == "contacts2:NickName": + contact_dict.update({ "contacts2_NickName" : element.text }) + elif element.tag == "contacts:OfficeLocation": + contact_dict.update({ "contacts_OfficeLocation" : element.text }) + elif element.tag == "contacts:OtherAddressCity": + contact_dict.update({ "contacts_OtherAddressCity" : element.text }) + elif element.tag == "contacts:OtherAddressCountry": + contact_dict.update({ "contacts_OtherAddressCountry" : element.text }) + elif element.tag == "contacts:OtherAddressPostalCode": + contact_dict.update({ "contacts_OtherAddressPostalCode" : element.text }) + elif element.tag == "contacts:OtherAddressState": + contact_dict.update({ "contacts_OtherAddressState" : element.text }) + elif element.tag == "contacts:OtherAddressStreet": + contact_dict.update({ "contacts_OtherAddressStreet" : element.text }) + elif element.tag == "contacts:PagerNumber": + contact_dict.update({ "contacts_PagerNumber" : element.text }) + elif element.tag == "contacts:Picture": + contact_dict.update({ "contacts_Picture" : element.text }) + elif element.tag == "contacts:RadioPhoneNumber": + contact_dict.update({ "contacts_RadioPhoneNumber" : element.text }) + elif element.tag == "contacts:Spouse": + contact_dict.update({ "contacts_Spouse" : element.text }) + elif element.tag == "contacts:Suffix": + contact_dict.update({ "contacts_Suffix" : element.text }) + elif element.tag == "contacts:Title": + contact_dict.update({ "contacts_Title" : element.text }) + elif element.tag == "contacts:WebPage": + contact_dict.update({ "contacts_WebPage" : element.text }) + elif element.tag == "contacts:WeightedRank": + contact_dict.update({ "contacts_WeightedRank" : element.text }) + elif element.tag == "contacts:YomiCompanyName": + contact_dict.update({ "contacts_YomiCompanyName" : element.text }) + elif element.tag == "contacts:YomiFirstName": + contact_dict.update({ "contacts_YomiFirstName" : element.text }) + elif element.tag == "contacts:YomiLastName": + contact_dict.update({ "contacts_YomiLastName" : element.text }) + return contact_dict \ No newline at end of file diff --git a/peas/pyActiveSync/objects/MSASDOC.py b/peas/pyActiveSync/objects/MSASDOC.py new file mode 100644 index 0000000..bf31c0d --- /dev/null +++ b/peas/pyActiveSync/objects/MSASDOC.py @@ -0,0 +1,45 @@ +######################################################################## +# Copyright (C) 2013 Sol Birnbaum +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +######################################################################## + +"""[MS-ASDOC] Document objects""" + +@staticmethod +def parse_document(data): + document_dict = {} + document_base = data.get_children() + document_dict.update({"server_id" : document_base[0].text}) + document_elements = document_base[1].get_children() + for element in document_elements: + if element.tag == "documentlibrary:ContentLength": + document_dict.update({ "documentlibrary_ContentLength" : element.text }) + elif element.tag == "documentlibrary:ContentType": + document_dict.update({ "documentlibrary_ContentType" : element.text }) + elif element.tag == "documentlibrary:CreationDate": + document_dict.update({ "documentlibrary_CreationDate" : element.text }) + elif element.tag == "documentlibrary:DisplayName": + document_dict.update({ "documentlibrary_DisplayName" : element.text }) + elif element.tag == "documentlibrary:IsFolder": + document_dict.update({ "documentlibrary_IsFolder" : element.text }) + elif element.tag == "documentlibrary:IsHidden": + document_dict.update({ "documentlibrary_IsHidden" : element.text }) + elif element.tag == "documentlibrary:LastModifiedDate": + document_dict.update({ "documentlibrary_LastModifiedDate" : element.text }) + elif element.tag == "documentlibrary:LinkId": + document_dict.update({ "documentlibrary_LinkId" : element.text }) + return document_dict \ No newline at end of file diff --git a/peas/pyActiveSync/objects/MSASDTYPE.py b/peas/pyActiveSync/objects/MSASDTYPE.py new file mode 100644 index 0000000..41739c0 --- /dev/null +++ b/peas/pyActiveSync/objects/MSASDTYPE.py @@ -0,0 +1,31 @@ +######################################################################## +# Copyright (C) 2013 Sol Birnbaum +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +######################################################################## + +"""[MS-ASDTYPE] MS-AS data types objects""" + +class datatype_TimeZone: + def get_local_timezone_bytes(): + #TODO + return + def get_timezone_bytes(timezone): + #TODO + return + class Timezones: + GMT = 0 + #TODO diff --git a/peas/pyActiveSync/objects/MSASEMAIL.py b/peas/pyActiveSync/objects/MSASEMAIL.py new file mode 100644 index 0000000..a39bc60 --- /dev/null +++ b/peas/pyActiveSync/objects/MSASEMAIL.py @@ -0,0 +1,415 @@ +######################################################################## +# Copyright (C) 2013 Sol Birnbaum +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +######################################################################## + +"""[MS-ASEMAIL] Email class namespace objects""" + +from MSASAIRS import airsyncbase_Type, airsyncbase_Body, airsyncbase_Attachment, airsyncbase_Attachments, airsyncbase_Method, airsyncbase_NativeBodyType, airsyncbase_BodyPart + +class email_Importance: + Low = 0 + Normal = 1 + High = 2 + +class email_MessageClass: #http://msdn.microsoft.com/en-us/library/ee200767(v=exchg.80).aspx + IPM_Note = "IPM.Note" #Normal e-mail message. + IPM_Note_SMIME = "IPM.Note.SMIME" #The message is encrypted and can also be signed. + IPM_Note_SMIME_MultipartSigned = "IPM.Note.SMIME.MultipartSigned" #The message is clear signed. + IPM_Note_Receipt_SMIME = "IPM.Note.Receipt.SMIME" #The message is a secure read receipt. + IPM_InfoPathForm = "IPM.InfoPathForm" #An InfoPath form. + IPM_Schedule_Meeting = "IPM.Schedule.Meeting" #Meeting request. + IPM_Notification_Meeting = "IPM.Notification.Meeting" #Meeting notification. + IPM_Post = "IPM.Post" #Post. + IPM_Octel_Voice = "IPM.Octel.Voice" #Octel voice message. + IPM_Voicenotes = "IPM.Voicenotes" #Electronic voice notes. + IPM_Sharing = "IPM.Sharing" #Shared message. + + REPORT_IPM_NOTE_NDR = "REPORT.IPM.NOTE.NDR" #Non-delivery report for a standard message. + REPORT_IPM_NOTE_DR = "REPORT.IPM.NOTE.DR" #Delivery receipt for a standard message. + REPORT_IPM_NOTE_DELAYED = "REPORT.IPM.NOTE.DELAYED" #Delivery receipt for a delayed message. + REPORT_IPM_NOTE_IPNRN = "REPORT.IPM.NOTE.IPNRN" #Read receipt for a standard message. + REPORT_IPM_NOTE_IPNNRN = "REPORT.IPM.NOTE.IPNNRN" #Non-read receipt for a standard message. + REPORT_IPM_SCHEDULE_MEETING_REQUEST_NDR = "REPORT.IPM.SCHEDULE.MEETING.REQUEST.NDR" #Non-delivery report for a meeting request. + REPORT_IPM_SCHEDULE_MEETING_RESP_POS_NDR = "REPORT.IPM.SCHEDULE.MEETING.RESP.NDR" #Non-delivery report for a positive meeting response (accept). + REPORT_IPM_SCHEDULE_MEETING_RESP_TENT_NDR = "REPORT.IPM.SCHEDULE.MEETING.TENT.NDR" #Non-delivery report for a Tentative meeting response. + REPORT_IPM_SCHEDULE_MEETING_CANCELED_NDR = "REPORT.IPM.SCHEDULE.MEETING.CANCELED.NDR" #Non-delivery report for a cancelled meeting notification. + REPORT_IPM_NOTE_SMIME_NDR = "REPORT.IPM.NOTE.SMIME.NDR" #Non-delivery report for a Secure MIME (S/MIME) encrypted and opaque-signed message. + REPORT_IPM_NOTE_SMIME_DR = "REPORT.IPM.NOTE.SMIME.DR" #Delivery receipt for an S/MIME encrypted and opaque-signed message. + REPORT_IPM_NOTE_SMIME_MULTIPARTSIGNED_NDR = "REPORT.IPM.NOTE.SMIME.MULTIPARTSIGNED.NDR" #Non-delivery report for an S/MIME clear-signed message. + REPORT_IPM_NOTE_SMIME_MULTIPARTSIGNED_DR = "REPORT.IPM.NOTE.SMIME.MULTIPARTSIGNED.DR" #Delivery receipt for an S/MIME clear-signed message. + +class email_InstanceType: + Single = 0 + Recurring_Master = 1 + Recurring_Instance = 2 + Recurring_Exception = 3 + + +class email_Type: #Recurrence type #http://msdn.microsoft.com/en-us/library/ee203639(v=exchg.80).aspx + Daily = 0 + Weekly = 1 + Monthly_Nth_day = 2 + Monthly = 3 + Yearly_Nth_day_Nth_month = 4 + Yearly_Nth_day_of_week_Nth_month = 5 + +class email2_CalendarType: #http://msdn.microsoft.com/en-us/library/ee625428(v=exchg.80).aspx + Default = 0 + Gregorian = 1 + Gregorian_US = 2 + Japan = 3 + Tiawan = 4 + Korea = 5 + Hijri = 6 + Thai = 7 + Hebrew = 8 + GregorianMeFrench = 9 + Gregorian_Arabic = 10 + Gregorian_translated_English = 11 + Gregorian_translated_French = 12 + Japanese_Lunar = 13 + Chinese_Lunar = 14 + Korean_Lunar = 15 + +class email_DayOfWeek: + Sunday = 1 + Monday = 2 + Tuesday = 4 + Wednesday = 8 + Thursday = 16 + Friday = 32 + Saturday = 64 + +class email2_FirstDayOfWeek: + Sunday = 0 + Monday = 1 + Tuesday = 2 + Wednesday = 2 + Thursday = 4 + Friday = 4 + Saturday = 6 + +class email_Sensitivity: + Normal = 0 + Personal = 1 + Private = 2 + Confidential = 3 + +class email_BusyStatus: + Free = 0 + Tentative = 1 + Busy = 2 + OutOfOffice = 3 + +class email_Flag_status: + Cleared = 0 + Complete = 1 + Active = 2 + +class email2_MeetingMessageType: #http://msdn.microsoft.com/en-us/library/ff631404(v=exchg.80).aspx + Silent = 0 #A silent update was performed, or the message type is unspecified. + Initial = 1 #Initial meeting request. + Full = 2 #Full update. + Informational = 3 #Informational update. + Outdated = 4 #Outdated. A newer meeting request or meeting update was received after this message. + Delegators_Copy = 5 #Identifies the delegator's copy of the meeting request. + Delegated = 6 #Identifies that the meeting request has been delegated and the meeting request MUST NOT be responded to. + +class email2_LastVerbExecuted: #http://msdn.microsoft.com/en-us/library/ee201536(v=exchg.80).aspx + Unknown = 0 + ReplyToSender = 1 + ReplyToAll = 2 + Forward = 3 + +class email_Recurrence(object): #http://msdn.microsoft.com/en-us/library/ee160268(v=exchg.80).aspx + def __init__(self): + self.email_Type = email_Type.Daily #Required. Byte. See "MSASEMAIL.Type" for enum. + self.email_Interval = 1 #Required. Integer. An Interval element value of 1 indicates that the meeting occurs every week, month, or year, depending upon the value of "self".Type. An Interval value of 2 indicates that the meeting occurs every other week, month, or year. + self.email_Until = None #Optional. dateTime. End of recurrence. + self.email_Occurances = None #Optional. Integer. Number of occurrences before the series of recurring meetings ends. + self.email_WeekOfMonth = None #Optional. Integer. The week of the month in which the meeting recurs. Required when the Type is set to a value of 6. + self.email_DayOfMonth = None #Optional. Integer. The day of the month on which the meeting recurs. Required when the Type is set to a value of 2 or 5. + self.email_DayOfWeek = None #Optional. Integer. See "MSASEMAIL.DayOfWeek" emun for values. The day of the week on which this meeting recurs. Can be anded together for multiple days of week. Required when the Type is set to a value of 1 or 6 + self.email_MonthOfYear = None #Optional. Integer. The month of the year in which the meeting recurs. Required when the Type is set to a value of 6. + self.email2_CalendarType = email2_CalendarType.Default #Required. Byte. See "MSASEMAIL.email2_CalendarType" for enum. + self.email2_IsLeapMonth = 0 #Optional. Byte. Does the recurrence takes place in the leap month of the given year? + self.email2_FirstDayOfWeek = email2_FirstDayOfWeek.Sunday #Optional. Byte. See "MSASEMAIL.email2_FirstDayOfWeek" for enum. What is considered the first day of the week for this recurrence? + +class email_MeetingRequest(object): #http://msdn.microsoft.com/en-us/library/ee157541(v=exchg.80).aspx + def __init__(self): + self.email_AllDayEvent = False #Optional. Byte. Is meeting all day? 0 or 1. + self.email_StartTime = None #Optional. dateTime. + self.email_DtStamp = None #Required. dateTime. Time that the MeetingRequest item was created. + self.email_EndTime = None #Optional. dateTime. + self.email_InstanceType = email_InstanceType.Single #Optional. Byte. See "MSASEMAIL.InstanceTypes" enum. + self.email_Location = "" #Optional. String. Location of meeting. + self.email_Organizer = "" #Optional. Email address as String. Email address of meeting organizer. + self.email_RecurrenceId = None #Optional. dateTime of specific instance of recurring meeting. + self.email_Reminder = 0 #Optional. Interger?. Time in seconds before meeting that reminder will be triggered. + self.email_ResponseRequested = 1 #Optional. Byte. Has the organizer requested a response of this MeetingRequest? 0 or 1. + self.email_Recurrences = [] #Optional. List of "MSASEMAIL.Recurrence". If specified, at least one recurrence in list is required. + self.email_Sensitivity = email_Sensitivity.Normal #Optional. Integer. See "MSASEMAIL.Sensitivity" for enum. How sensitive is the meeting? Default is Normal. + self.email_BusyStatus = email_BusyStatus.Tentative #Optional. Integer. See "MSASEMAIL.BusyStatus" for enum. Default is Tentantive. + self.email_TimeZone = "" #Required. String formated as per http://msdn.microsoft.com/en-us/library/ee204550(v=exchg.80).aspx. + self.email_GlobalObjId = self.set_GlobalObjId() #Required. Generated by self.generate_GlobalObjId() + self.email_DisallowNewTimeProposal = 0 #Optional. Byte. 0 = new time proposals allowed, >0 = new time proposals not allowed. Default is 0. + self.email_MeetingMessageType = email2_MeetingMessageType.Silent #Optional. Byte. See "MSASEMAIL.email2_MeetingMessageType" for enum. Default is Silent. + def set_GlobalObjId(self): + #TODO + return + def set_TimeZone(self, intimezone=None): + from MSASDTYPE import datatype_TimeZone + if intimezone: + self.TimeZone = datatype_TimeZone.get_timezone_bytes(intimezone) + else: + self.TimeZone = datatype_TimeZone.get_local_timezone_bytes() + +class email_Flag(object): #http://msdn.microsoft.com/en-us/library/ee160518(v=exchg.80).aspx + def __init__(self): + self.tasks_Subject = "" #Optional. String. + self.email_flag_Status = email_Flag_status.Active + self.email_FlagType = "" #Optional. String. A string the 'explains' the flagging, such as "flag for follow up". + self.tasks_DateCompleted = None #Optional. dateTime. If set, email_CompleteTime is also required. + self.email_CompleteTime = None #Require if complete. dateTime. + self.tasks_StartDate = None #Optional. dateTime. If set, the other tasks_*Date elements must also be set. + self.tasks_DueDate = None #Optional. dateTime. If set, the other tasks_*Date elements must also be set. + self.tasks_UtcStartDate = None #Optional. dateTime. If set, the other tasks_*Date elements must also be set. + self.tasks_UtcDueDate = None #Optional. dateTime. If set, the other tasks_*Date elements must also be set. + self.tasks_ReminderSet = 0 #Optional. Byte. Is reminder set? 0 or 1. + self.tasks_ReminderTime = None #Optional. dateTime + self.tasks_OrdinalDate = None #Optional. dateTime. Time at which flag was set. + self.tasks_SubOrdinalDate = None #Optional. String. Should be used for sorting. + def parse(self, inwapxml_airsync_Flag): + email_elements = inwapxml_airsync_Flag.get_children() + for element in email_elements: + if element.tag == "tasks:Subject": + self.tasks_Subject = element.text + elif element.tag == "email:Status": + self.email_flag_Status = element.text + elif element.tag == "email:FlagType": + self.email_FlagType = element.text + elif element.tag == "tasks:DateCompleted": + self.tasks_DateCompleted = element.text + elif element.tag == "email:CompleteTime": + self.email_CompleteTime = element.text + elif element.tag == "tasks:StartDate": + self.tasks_StartDate = element.text + elif element.tag == "tasks:DueDate": + self.tasks_DueDate = element.text + elif element.tag == "tasks:UtcStartDate": + self.tasks_UtcStartDate = element.text + elif element.tag == "tasks:UtcDueDate": + self.tasks_UtcDueDate = element.text + elif element.tag == "tasks:ReminderSet": + self.tasks_ReminderSet = element.text + elif element.tag == "tasks:ReminderTime": + self.tasks_ReminderTime = element.text + elif element.tag == "tasks:OrdinalDate": + self.tasks_OrdinalDate = element.text + elif element.tag == "tasks:SubOrdinalDate": + self.tasks_SubOrdinalDate = element.text + def marshal(self): + import base64 + return base64.b64encode("%s//%s//%s//%s//%s//%s//%s//%s//%s//%s//%s//%s//%s" % (self.tasks_Subject, str(self.email_flag_Status), self.email_FlagType, self.tasks_DateCompleted, self.email_CompleteTime, self.tasks_StartDate, + self.tasks_DueDate, self.tasks_UtcStartDate, self.tasks_UtcDueDate, self.tasks_ReminderSet, self.tasks_ReminderTime, self.tasks_OrdinalDate, self.tasks_SubOrdinalDate)) + def __repr__(self): + return self.marshal() + +#class email_Category(object): +# def __init__(self, category): +# self.name = category #Required. String. Name of category. + +class Email(object): + """Aggregation of email elements that can be included in an 'Email' item to or from an AS server.""" + def __init__(self): + self.server_id = "" + self.email_To = [] #String. List of string seperated by commas. + self.email_Cc = [] #String. List of string seperated by commas. + self.email_From = "" #String + self.email_Subject = "" #String + self.email_ReplyTo = "" #String. Specifies the e-mail address to which replies will be addressed by default. + self.email_DateReceived = None #dataTime. + self.email_DisplayTo = [] #String. List of display names of recipient seperated by semi-colons. + self.email_ThreadTopic = "" #String + self.email_Importance = 1 #Byte. See "MSASEMAIL.Importance" + self.email_Read = 0 #Boolean. Whether or not email has been read. + self.airsyncbase_Attachments = [] #"MSASAIRS.Attachments". List of "MSASAIRS.Attachment"s. + self.airsyncbase_Body = None #"MSASAIRS.Body". Email message body. + self.email_MessageClass = email_MessageClass.IPM_Note #String. See "MSASEMAIL.MessageClass" enum. + self.email_InternetCPID = "" #Required. String. Original MIME language code page ID. + self.email_Flag = None #Optional. email_Flag object. + self.airsyncbase_NativeBodyType = airsyncbase_NativeBodyType.HTML #Optional. Byte enum. BodyType stored on server before any modification during transport. + self.email_ContentClass = "" #Optional. String. The content class of the data. + self.email2_UmCalledId = "" #Optional. Server to client. See http://msdn.microsoft.com/en-us/library/ee200631(v=exchg.80).aspx for when required. + self.email2_UmUserNotes = "" #Optional. Server to client. See http://msdn.microsoft.com/en-us/library/ee158056(v=exchg.80).aspx for when required. + self.email2_ConversationId = "" #Required. Byte array transfered as wbxml opaque data. + self.email2_ConversationIndex = "" #Required. Byte array transfered as wbxml opaque data. Contains a set of timestamps used by clients to generate a conversation tree view. The first timestamp identifies the date and time when the message was originally sent by the server. Additional timestamps are added when the message is forwarded or replied to. + self.email2_LastVerbExecuted = "" #Optional. Integer. Contains the most recent email action. Can be used to choose email message icon. + self.email2_LastVerbExecutedTime = "" #Optional. dateTime. Contains the time of the email2_LastVerbExecuted. + self.email2_ReceivedAsBcc = "0" #Optional. Boolean. Was email received as BCC. + self.email2_Sender = "" #Optional. String. If email was from delegate, the delegate's name appears here. + self.email_Categories = [] #Optional. List of "email_Category"s that apply to this message. Max 300. http://msdn.microsoft.com/en-us/library/ee625079(v=exchg.80).aspx + self.airsyncbase_BodyPart = "" #Optional. See "airsyncbase_BodyPart". + self.email2_AccountId = "" #Optional. Specific account email was sent to (i.e. if not to PrimarySmtpAddress). + self.rm_RightsManagementLicense = [] #Optional. Contains rights management information. + def __repr__(self): + return "\r\n%s\r\n------Start Email------\r\nFrom: %s\r\nTo: %s\r\nCc: %s\r\nSubject: %s\r\nDateReceived: %s\r\nMessageClass: %s\r\nContentClass: %s\r\n\r\n%s\r\n-------End Email-------\r\n" % (super(Email, self).__repr__(), self.email_From, self.email_To, self.email_Cc, self.email_Subject, self.email_DateReceived, self.email_MessageClass,self.email_ContentClass,self.airsyncbase_Body.airsyncbase_Data) + def parse(self, inwapxml_airsync_command): + email_base = inwapxml_airsync_command.get_children() + self.server_id = email_base[0].text + email_elements = email_base[1].get_children() + for element in email_elements: + if element.tag == "email:To": + self.email_To = element.text + elif element.tag == "email:Cc": + self.email_Cc = element.text + elif element.tag == "email:From": + self.email_From = element.text + elif element.tag == "email:Subject": + self.email_Subject = element.text + elif element.tag == "email:ReplyTo": + self.email_ReplyTo = element.text + elif element.tag == "email:DateReceived": + self.email_DateReceived = element.text + elif element.tag == "email:DisplayTo": + self.email_DisplayTo = element.text + elif element.tag == "email:ThreadTopic": + self.email_TreadTopic = element.text + elif element.tag == "email:Importance": + self.email_Importance = element.text + elif element.tag == "email:Read": + self.email_Read = element.text + elif element.tag == "airsyncbase:Attachments": + self.airsyncbase_Attachments = airsyncbase_Attachments.parse(element) + elif element.tag == "airsyncbase:Body": + body = airsyncbase_Body() + body.parse(element) + self.airsyncbase_Body = body + elif element.tag == "email:MessageClass": + self.email_MessageClass = element.text + elif element.tag == "email:InternetCPID": + self.email_InternetCPID = element.text + elif element.tag == "email:Flag": + flag = email_Flag() + flag.parse(element) + self.email_Flag = flag + elif element.tag == "airsyncbase:NativeBodyType": + self.airsyncbase_NativeBodyType = element.text + elif element.tag == "email:ContentClass": + self.email_ContentClass = element.text + elif element.tag == "email2:UmCallerId": + self.email2_UmCalledId = element.text + elif element.tag == "email2:UmUserNotes": + self.email2_UmUserNotes = element.text + elif element.tag == "email2:ConversationId": + self.email2_ConversationId = element.text + elif element.tag == "email2:ConversationIndex": + self.email2_ConversationIndex = element.text + elif element.tag == "email2:LastVerbExecuted": + self.email2_LastVerbExecuted = element.text + elif element.tag == "email2:LastVerbExecutedTime": + self.email2_LastVerbExecutedTime = element.text + elif element.tag == "email2:ReceivedAsBcc": + self.email2_ReceivedAsBcc = element.text + elif element.tag == "email2:Sender": + self.email2_Sender = element.text + elif element.tag == "email:Categories": + categories_elements = element.get_children() + for category in categories_elements: + self.email_Categories.append(category.text) + elif element.tag == "airsyncbase:BodyPart": + self.airsyncbase_Body = airsyncbase_BodyPart.parse(element) + elif element.tag == "email2:AccountId": + self.email2_AccountId = element.text + elif element.tag == "rm:RightsManagementLicense": + continue + +def parse_email(data, type=1): + email_dict = {} + if type == 1: + email_base = data.get_children() + email_dict.update({"server_id" : email_base[0].text}) + email_elements = email_base[1].get_children() + for element in email_elements: + if element.tag == "email:To": + email_dict.update({ "email_To" : element.text }) + elif element.tag == "email:Cc": + email_dict.update({ "email_Cc" : element.text }) + elif element.tag == "email:From": + email_dict.update({ "email_From" : element.text }) + elif element.tag == "email:Subject": + email_dict.update({ "email_Subject" : element.text }) + elif element.tag == "email:ReplyTo": + email_dict.update({ "email_ReplyTo" : element.text }) + elif element.tag == "email:DateReceived": + email_dict.update({ "email_DateReceived" : element.text }) + elif element.tag == "email:DisplayTo": + email_dict.update({ "email_DisplayTo" : element.text }) + elif element.tag == "email:ThreadTopic": + email_dict.update({ "email_ThreadTopic" : element.text }) + elif element.tag == "email:Importance": + email_dict.update({ "email_Importance" : element.text }) + elif element.tag == "email:Read": + email_dict.update({ "email_Read" : element.text }) + elif element.tag == "airsyncbase:Attachments": + email_dict.update({ "airsyncbase_Attachments" : airsyncbase_Attachments.parse(element)}) + elif element.tag == "airsyncbase:Body": + body = airsyncbase_Body() + body.parse(element) + email_dict.update({ "airsyncbase_Body" : body }) + elif element.tag == "email:MessageClass": + email_dict.update({ "email_MessageClass" : element.text }) + elif element.tag == "email:InternetCPID": + email_dict.update({ "email_InternetCPID" : element.text }) + elif element.tag == "email:Flag": + flag = email_Flag() + flag.parse(element) + email_dict.update({ "email_Flag" : flag}) + elif element.tag == "airsyncbase:NativeBodyType": + email_dict.update({ "airsyncbase_NativeBodyType" : element.text }) + elif element.tag == "email:ContentClass": + email_dict.update({ "email_ContentClass" : element.text }) + elif element.tag == "email2:UmCallerId": + email_dict.update({ "email2_UmCalledId" : element.text }) + elif element.tag == "email2:UmUserNotes": + email_dict.update({ "email2_UmUserNotes" : element.text }) + elif element.tag == "email2:ConversationId": + email_dict.update({ "email2_ConversationId" : element.text }) + elif element.tag == "email2:ConversationIndex": + email_dict.update({ "email2_ConversationIndex" : element.text }) + elif element.tag == "email2:LastVerbExecuted": + email_dict.update({ "email2_LastVerbExecuted" : element.text }) + elif element.tag == "email2:LastVerbExecutedTime": + email_dict.update({ "email2_LastVerbExecutedTime" : element.text }) + elif element.tag == "email2:ReceivedAsBcc": + email_dict.update({ "email2_ReceivedAsBcc" : element.text }) + elif element.tag == "email2:Sender": + email_dict.update({ "email2_Sender" : element.text }) + elif element.tag == "email:Categories": + categories_list = [] + categories = element.get_children() + for category_element in categories: + categories_list.append(category_element.text) + email_dict.update({ "email_Categories" : categories_list }) + elif element.tag == "airsyncbase:BodyPart": + email_dict.update({ "airsyncbase_Body" : airsyncbase_BodyPart.parse(element)}) + elif element.tag == "email2:AccountId": + email_dict.update({ "email2_AccountId" : element.text }) + elif element.tag == "rm:RightsManagementLicense": + continue + return email_dict \ No newline at end of file diff --git a/peas/pyActiveSync/objects/MSASHTTP.py b/peas/pyActiveSync/objects/MSASHTTP.py new file mode 100644 index 0000000..dbd5444 --- /dev/null +++ b/peas/pyActiveSync/objects/MSASHTTP.py @@ -0,0 +1,127 @@ +######################################################################## +# Copyright (C) 2013 Sol Birnbaum +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +######################################################################## + +import httplib, urllib + +class ASHTTPConnector(object): + """ActiveSync HTTP object""" + USER_AGENT = "Python" + POST_URL_TEMPLATE = "/Microsoft-Server-ActiveSync?Cmd=%s&User=%s&DeviceId=123456&DeviceType=Python" + POST_GETATTACHMENT_URL_TEMPLATE = "/Microsoft-Server-ActiveSync?Cmd=%s&AttachmentName=%s&User=%s&DeviceId=123456&DeviceType=Python" + + def __init__(self, server, port=443, ssl=True): + + self.server = server + self.port = port + self.ssl = ssl + self.policykey = 0 + self.headers = { + "Content-Type": "application/vnd.ms-sync.wbxml", + "User-Agent" : self.USER_AGENT, + "MS-ASProtocolVersion" : "14.1", + "Accept-Language" : "en_us" + } + return + + def set_credential(self, username, password): + import base64 + self.username = username + self.credential = base64.b64encode(username+":"+password) + self.headers.update({"Authorization" : "Basic " + self.credential}) + + def do_post(self, url, body, headers, redirected=False): + if self.ssl: + conn = httplib.HTTPSConnection(self.server, self.port) + conn.request("POST", url, body, headers) + else: + conn = httplib.HTTPConnection(self.server, self.port) + conn.request("POST", url, body, headers) + res = conn.getresponse() + if res.status == 451: + self.server = res.getheader("X-MS-Location").split()[2] + if not redirected: + return self.do_post(url, body, headers, False) + else: + raise Exception("Redirect loop encountered. Stopping request.") + else: + return res + + + def post(self, cmd, body): + url = self.POST_URL_TEMPLATE % (cmd, self.username) + res = self.do_post(url, body, self.headers) + #print res.status, res.reason, res.getheaders() + return res.read() + + def fetch_multipart(self, body, filename="fetched_file.tmp"): + """http://msdn.microsoft.com/en-us/library/ee159875(v=exchg.80).aspx""" + headers = self.headers + headers.update({"MS-ASAcceptMultiPart":"T"}) + url = self.POST_URL_TEMPLATE % ("ItemOperations", self.username) + res = self.do_post(url, body, headers) + if res.getheaders()["Content-Type"] == "application/vnd.ms-sync.multipart": + PartCount = int(res.read(4)) + PartMetaData = [] + for partindex in range(0, PartCount): + PartMetaData.append((int(res.read(4))), (int(res.read(4)))) + wbxml_part = res.read(PartMetaData[0][1]) + fetched_file = open(filename, "wb") + for partindex in range(1, PartCount): + fetched_file.write(res.read(PartMetaData[0][partindex])) + fetched_file.close() + return wbxml, filename + else: + raise TypeError("Client requested MultiPart response, but server responsed with inline.") + + def get_attachment(self, attachment_name): #attachment_name = DisplayName of attachment from an MSASAIRS.Attachment object + url = self.POST_GETATTACHMENT_URL_TEMPLATE % ("GetAttachment", attachment_name, self.username) + res = self.do_post(url, "", self.headers) + try: + content_type = res.getheader("Content-Type") + except: + content_type = "text/plain" + res.status + return res.read(), res.status, content_type + + def get_options(self): + conn = httplib.HTTPSConnection(self.server, self.port) + conn.request("OPTIONS", "/Microsoft-Server-ActiveSync", None, self.headers) + res = conn.getresponse() + return res + + def options(self): + res = self.get_options() + if res.status is 200: + self._server_protocol_versions = res.getheader("ms-asprotocolversions") + self._server_protocol_commands = res.getheader("ms-asprotocolcommands") + self._server_version = res.getheader("ms-server-activesync") + return True + else: + print "Connection Error!:" + print res.status, res.reason + for header in res.getheaders(): + print header[0]+":",header[1] + return False + + def get_policykey(self): + return self.policykey + + def set_policykey(self, policykey): + self.policykey = policykey + self.headers.update({ "X-MS-PolicyKey" : self.policykey }) \ No newline at end of file diff --git a/peas/pyActiveSync/objects/MSASNOTE.py b/peas/pyActiveSync/objects/MSASNOTE.py new file mode 100644 index 0000000..3e9e82c --- /dev/null +++ b/peas/pyActiveSync/objects/MSASNOTE.py @@ -0,0 +1,46 @@ +######################################################################## +# Copyright (C) 2013 Sol Birnbaum +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +######################################################################## + +"""[MS-ASNOTE] Note objects""" + +from MSASEMAIL import airsyncbase_Body + +def parse_note(data): + note_dict = {} + note_base = data.get_children() + note_dict.update({"server_id" : note_base[0].text}) + note_elements = note_base[1].get_children() + for element in note_elements: + if element.tag == "airsyncbase:Body": + body = airsyncbase_Body() + body.parse(element) + note_dict.update({ "airsyncbase_Body" : body }) + elif element.tag == "notes:Subject": + note_dict.update({ "notes_Subject" : element.text }) + elif element.tag == "notes:MessageClass": + note_dict.update({ "notes_MessageClass" : element.text }) + elif element.tag == "notes:LastModifiedDate": + note_dict.update({ "notes_LastModifiedDate" : element.text }) + elif element.tag == "notes:Categories": + categories_list = [] + categories = element.get_children() + for category_element in categories: + categories_list.append(category_element.text) + note_dict.update({ "notes_Categories" : categories_list }) + return note_dict \ No newline at end of file diff --git a/peas/pyActiveSync/objects/MSASRM.py b/peas/pyActiveSync/objects/MSASRM.py new file mode 100644 index 0000000..9c0ea3d --- /dev/null +++ b/peas/pyActiveSync/objects/MSASRM.py @@ -0,0 +1,20 @@ +######################################################################## +# Copyright (C) 2013 Sol Birnbaum +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +######################################################################## + +"""[MS-ASRM] Rights Management objects""" \ No newline at end of file diff --git a/peas/pyActiveSync/objects/MSASTASK.py b/peas/pyActiveSync/objects/MSASTASK.py new file mode 100644 index 0000000..a7091f6 --- /dev/null +++ b/peas/pyActiveSync/objects/MSASTASK.py @@ -0,0 +1,100 @@ +######################################################################## +# Copyright (C) 2013 Sol Birnbaum +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +######################################################################## + +"""[MS-ASTASK] Task objects""" + +from MSASEMAIL import airsyncbase_Body + +def parse_task(data): + task_dict = {} + task_base = data.get_children() + task_dict.update({"server_id" : task_base[0].text}) + task_elements = task_base[1].get_children() + for element in task_elements: + if element.tag == "airsyncbase:Body": + body = airsyncbase_Body() + body.parse(element) + task_dict.update({ "airsyncbase_Body" : body }) + elif element.tag == "tasks:CalendarType": + task_dict.update({ "tasks_CalendarType" : element.text }) + elif element.tag == "tasks:Categories": + categories_list = [] + categories = element.get_children() + for category_element in categories: + categories_list.append(category_element.text) + task_dict.update({ "tasks_Categories" : categories_list }) + elif element.tag == "tasks:Complete": + task_dict.update({ "tasks_Complete" : element.text }) + elif element.tag == "tasks:DateCompleted": + task_dict.update({ "tasks_DateCompleted" : element.text }) + elif element.tag == "tasks:DueDate": + task_dict.update({ "tasks_DueDate" : element.text }) + elif element.tag == "tasks:Importance": + task_dict.update({ "tasks_Importance" : element.text }) + elif element.tag == "tasks:OrdinalDate": + task_dict.update({ "tasks_OrdinalDate" : element.text }) + elif element.tag == "tasks:Recurrence": + recurrence_dict = {} + for recurrence_element in element.get_children(): + if recurrence_element.tag == "tasks:Type": + recurrence_dict.update({ "tasks_Type" : recurrence_element.text }) + elif recurrence_element.tag == "tasks:Occurrences": + recurrence_dict.update({ "tasks_Occurrences" : recurrence_element.text }) + elif recurrence_element.tag == "tasks:Regenerate": + recurrence_dict.update({ "tasks_Regenerate" : recurrence_element.text }) + elif recurrence_element.tag == "tasks:DeadOccur": + recurrence_dict.update({ "tasks_DeadOccur" : recurrence_element.text }) + elif recurrence_element.tag == "tasks:FirstDayOfWeek": + recurrence_dict.update({ "tasks_FirstDayOfWeek" : recurrence_element.text }) + elif recurrence_element.tag == "tasks:Interval": + recurrence_dict.update({ "tasks_Interval" : recurrence_element.text }) + elif recurrence_element.tag == "tasks:IsLeapMonth": + recurrence_dict.update({ "tasks_IsLeapMonth" : recurrence_element.text }) + elif recurrence_element.tag == "tasks:WeekOfMonth": + recurrence_dict.update({ "tasks_WeekOfMonth" : recurrence_element.text }) + elif recurrence_element.tag == "tasks:DayOfMonth": + recurrence_dict.update({ "tasks_DayOfMonth" : recurrence_element.text }) + elif recurrence_element.tag == "tasks:DayOfWeek": + recurrence_dict.update({ "tasks_DayOfWeek" : recurrence_element.text }) + elif recurrence_element.tag == "tasks:MonthOfYear": + recurrence_dict.update({ "tasks_MonthOfYear" : recurrence_element.text }) + elif recurrence_element.tag == "tasks:Until": + recurrence_dict.update({ "tasks_Until" : recurrence_element.text }) + elif recurrence_element.tag == "tasks:Start": + recurrence_dict.update({ "tasks_Start" : recurrence_element.text }) + elif recurrence_element.tag == "tasks:CalendarType": + recurrence_dict.update({ "tasks_CalendarType" : recurrence_element.text }) + task_dict.update({ "tasks_Recurrence" : recurrence_dict }) + elif element.tag == "tasks:ReminderSet": + task_dict.update({ "tasks_ReminderSet" : element.text }) + elif element.tag == "tasks:ReminderTime": + task_dict.update({ "tasks_ReminderTime" : element.text }) + elif element.tag == "tasks:Sensitivity": + task_dict.update({ "tasks_Sensitivity" : element.text }) + elif element.tag == "tasks:StartDate": + task_dict.update({ "tasks_StartDate" : element.text }) + elif element.tag == "tasks:Subject": + task_dict.update({ "tasks_Subject" : element.text }) + elif element.tag == "tasks:SubOrdinalDate": + task_dict.update({ "tasks_SubOrdinalDate" : element.text }) + elif element.tag == "tasks:UtcDueDate": + task_dict.update({ "tasks_UtcDueDate" : element.text }) + elif element.tag == "tasks:UtcStartDate": + task_dict.update({ "tasks_UtcStartDate" : element.text }) + return task_dict \ No newline at end of file diff --git a/peas/pyActiveSync/objects/__init__.py b/peas/pyActiveSync/objects/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/peas/pyActiveSync/utils/__init__.py b/peas/pyActiveSync/utils/__init__.py new file mode 100644 index 0000000..7774985 --- /dev/null +++ b/peas/pyActiveSync/utils/__init__.py @@ -0,0 +1,18 @@ +######################################################################## +# Copyright (C) 2013 Sol Birnbaum +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +######################################################################## \ No newline at end of file diff --git a/peas/pyActiveSync/utils/as_code_pages.py b/peas/pyActiveSync/utils/as_code_pages.py new file mode 100644 index 0000000..c779ef1 --- /dev/null +++ b/peas/pyActiveSync/utils/as_code_pages.py @@ -0,0 +1,717 @@ +######################################################################## +# Copyright (C) 2013 Sol Birnbaum +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +######################################################################## + + +from code_page import code_page + +class as_code_pages: + """MS-ASWBXML code pages builder""" + @staticmethod + def build_as_code_pages(): + code_pages = {} + + code_pages.update({ + 0: code_page("AirSync", "airsync", 0) + }) + code_pages[0].add(0x05, "Sync") + code_pages[0].add(0x06, "Responses") + code_pages[0].add(0x07, "Add") + code_pages[0].add(0x08, "Change") + code_pages[0].add(0x09, "Delete") + code_pages[0].add(0x0A, "Fetch") + code_pages[0].add(0x0B, "SyncKey") + code_pages[0].add(0x0C, "ClientId") + code_pages[0].add(0x0D, "ServerId") + code_pages[0].add(0x0E, "Status") + code_pages[0].add(0x0F, "Collection") + code_pages[0].add(0x10, "Class") + code_pages[0].add(0x12, "CollectionId") + code_pages[0].add(0x13, "GetChanges") + code_pages[0].add(0x14, "MoreAvailable") + code_pages[0].add(0x15, "WindowSize") + code_pages[0].add(0x16, "Commands") + code_pages[0].add(0x17, "Options") + code_pages[0].add(0x18, "FilterType") + code_pages[0].add(0x1B, "Conflict") + code_pages[0].add(0x1C, "Collections") + code_pages[0].add(0x1D, "ApplicationData") + code_pages[0].add(0x1E, "DeletesAsMoves") + code_pages[0].add(0x20, "Supported") + code_pages[0].add(0x21, "SoftDelete") + code_pages[0].add(0x22, "MIMESupport") + code_pages[0].add(0x23, "MIMETruncation") + code_pages[0].add(0x24, "Wait") + code_pages[0].add(0x25, "Limit") + code_pages[0].add(0x26, "Partial") + code_pages[0].add(0x27, "ConversationMode") + code_pages[0].add(0x28, "MaxItems") + code_pages[0].add(0x29, "HeartbeatInterval") + + code_pages.update({ + 1 : code_page( "Contacts" , "contacts", 1 ) + }) + code_pages[1].add(0x05, "Anniversary") + code_pages[1].add(0x06, "AssistantName") + code_pages[1].add(0x07, "AssistantPhoneNumber") + code_pages[1].add(0x08, "Birthday") + code_pages[1].add(0x0C, "Business2PhoneNumber") + code_pages[1].add(0x0D, "BusinessAddressCity") + code_pages[1].add(0x0E, "BusinessAddressCountry") + code_pages[1].add(0x0F, "BusinessAddressPostalCode") + code_pages[1].add(0x10, "BusinessAddressState") + code_pages[1].add(0x11, "BusinessAddressStreet") + code_pages[1].add(0x12, "BusinessFaxNumber") + code_pages[1].add(0x13, "BusinessPhoneNumber") + code_pages[1].add(0x14, "CarPhoneNumber") + code_pages[1].add(0x15, "Categories") + code_pages[1].add(0x16, "Category") + code_pages[1].add(0x17, "Children") + code_pages[1].add(0x18, "Child") + code_pages[1].add(0x19, "CompanyName") + code_pages[1].add(0x1A, "Department") + code_pages[1].add(0x1B, "Email1Address") + code_pages[1].add(0x1C, "Email2Address") + code_pages[1].add(0x1D, "Email3Address") + code_pages[1].add(0x1E, "FileAs") + code_pages[1].add(0x1F, "FirstName") + code_pages[1].add(0x20, "Home2PhoneNumber") + code_pages[1].add(0x21, "HomeAddressCity") + code_pages[1].add(0x22, "HomeAddressCountry") + code_pages[1].add(0x23, "HomeAddressPostalCode") + code_pages[1].add(0x24, "HomeAddressState") + code_pages[1].add(0x25, "HomeAddressStreet") + code_pages[1].add(0x26, "HomeFaxNumber") + code_pages[1].add(0x27, "HomePhoneNumber") + code_pages[1].add(0x28, "JobTitle") + code_pages[1].add(0x29, "LastName") + code_pages[1].add(0x2A, "MiddleName") + code_pages[1].add(0x2B, "MobilePhoneNumber") + code_pages[1].add(0x2C, "OfficeLocation") + code_pages[1].add(0x2D, "OtherAddressCity") + code_pages[1].add(0x2E, "OtherAddressCountry") + code_pages[1].add(0x2F, "OtherAddressPostalCode") + code_pages[1].add(0x30, "OtherAddressState") + code_pages[1].add(0x31, "OtherAddressStreet") + code_pages[1].add(0x32, "PagerNumber") + code_pages[1].add(0x33, "RadioPhoneNumber") + code_pages[1].add(0x34, "Spouse") + code_pages[1].add(0x35, "Suffix") + code_pages[1].add(0x36, "Title") + code_pages[1].add(0x37, "WebPage") + code_pages[1].add(0x38, "YomiCompanyName") + code_pages[1].add(0x39, "YomiFirstName") + code_pages[1].add(0x3A, "YomiLastName") + code_pages[1].add(0x3C, "Picture") + code_pages[1].add(0x3D, "Alias") + code_pages[1].add(0x3E, "WeightedRank") + + + # Code Page 2: Email + + code_pages.update({ + 2 : code_page( "Email" , "email", 2 ) + }) + code_pages[2].add(0x0F, "DateReceived") + code_pages[2].add(0x11, "DisplayTo") + code_pages[2].add(0x12, "Importance") + code_pages[2].add(0x13, "MessageClass") + code_pages[2].add(0x14, "Subject") + code_pages[2].add(0x15, "Read") + code_pages[2].add(0x16, "To") + code_pages[2].add(0x17, "Cc") + code_pages[2].add(0x18, "From") + code_pages[2].add(0x19, "ReplyTo") + code_pages[2].add(0x1A, "AllDayEvent") + code_pages[2].add(0x1B, "Categories") + code_pages[2].add(0x1C, "Category") + code_pages[2].add(0x1D, "DtStamp") + code_pages[2].add(0x1E, "EndTime") + code_pages[2].add(0x1F, "InstanceType") + code_pages[2].add(0x20, "BusyStatus") + code_pages[2].add(0x21, "Location") + code_pages[2].add(0x22, "MeetingRequest") + code_pages[2].add(0x23, "Organizer") + code_pages[2].add(0x24, "RecurrenceId") + code_pages[2].add(0x25, "Reminder") + code_pages[2].add(0x26, "ResponseRequested") + code_pages[2].add(0x27, "Recurrences") + code_pages[2].add(0x28, "Recurrence") + code_pages[2].add(0x29, "Type") + code_pages[2].add(0x2A, "Until") + code_pages[2].add(0x2B, "Occurrences") + code_pages[2].add(0x2C, "Interval") + code_pages[2].add(0x2D, "DayOfWeek") + code_pages[2].add(0x2E, "DayOfMonth") + code_pages[2].add(0x2F, "WeekOfMonth") + code_pages[2].add(0x30, "MonthOfYear") + code_pages[2].add(0x31, "StartTime") + code_pages[2].add(0x32, "Sensitivity") + code_pages[2].add(0x33, "TimeZone") + code_pages[2].add(0x34, "GlobalObjId") + code_pages[2].add(0x35, "ThreadTopic") + code_pages[2].add(0x39, "InternetCPID") + code_pages[2].add(0x3A, "Flag") + code_pages[2].add(0x3B, "Status") + code_pages[2].add(0x3C, "ContentClass") + code_pages[2].add(0x3D, "FlagType") + code_pages[2].add(0x3E, "CompleteTime") + code_pages[2].add(0x3F, "DisallowNewTimeProposal") + + + # Code Page 3: AirNotify + code_pages.update({ + 3 : code_page( "AirNotify" , "airnotify", 3 ) + }) + + + # Code Page 4: Calendar + code_pages.update({ + 4 : code_page( "Calendar" , "calendar", 4 ) + }) + code_pages[4].add(0x05, "TimeZone") + code_pages[4].add(0x06, "AllDayEvent") + code_pages[4].add(0x07, "Attendees") + code_pages[4].add(0x08, "Attendee") + code_pages[4].add(0x09, "Email") + code_pages[4].add(0x0A, "Name") + code_pages[4].add(0x0D, "BusyStatus") + code_pages[4].add(0x0E, "Categories") + code_pages[4].add(0x0F, "Category") + code_pages[4].add(0x11, "DtStamp") + code_pages[4].add(0x12, "EndTime") + code_pages[4].add(0x13, "Exception") + code_pages[4].add(0x14, "Exceptions") + code_pages[4].add(0x15, "Deleted") + code_pages[4].add(0x16, "ExceptionStartTime") + code_pages[4].add(0x17, "Location") + code_pages[4].add(0x18, "MeetingStatus") + code_pages[4].add(0x19, "OrganizerEmail") + code_pages[4].add(0x1A, "OrganizerName") + code_pages[4].add(0x1B, "Recurrence") + code_pages[4].add(0x1C, "Type") + code_pages[4].add(0x1D, "Until") + code_pages[4].add(0x1E, "Occurrences") + code_pages[4].add(0x1F, "Interval") + code_pages[4].add(0x20, "DayOfWeek") + code_pages[4].add(0x21, "DayOfMonth") + code_pages[4].add(0x22, "WeekOfMonth") + code_pages[4].add(0x23, "MonthOfYear") + code_pages[4].add(0x24, "Reminder") + code_pages[4].add(0x25, "Sensitivity") + code_pages[4].add(0x26, "Subject") + code_pages[4].add(0x27, "StartTime") + code_pages[4].add(0x28, "UID") + code_pages[4].add(0x29, "AttendeeStatus") + code_pages[4].add(0x2A, "AttendeeType") + code_pages[4].add(0x33, "DisallowNewTimeProposal") + code_pages[4].add(0x34, "ResponseRequested") + code_pages[4].add(0x35, "AppointmentReplyTime") + code_pages[4].add(0x36, "ResponseType") + code_pages[4].add(0x37, "CalendarType") + code_pages[4].add(0x38, "IsLeapMonth") + code_pages[4].add(0x39, "FirstDayOfWeek") + code_pages[4].add(0x3A, "OnlineMeetingConfLink") + code_pages[4].add(0x3B, "OnlineMeetingExternalLink") + + + # Code Page 5: Move + code_pages.update({ + 5 : code_page( "Move" , "move", 5 ) + }) + code_pages[5].add(0x05, "MoveItems") + code_pages[5].add(0x06, "Move") + code_pages[5].add(0x07, "SrcMsgId") + code_pages[5].add(0x08, "SrcFldId") + code_pages[5].add(0x09, "DstFldId") + code_pages[5].add(0x0A, "Response") + code_pages[5].add(0x0B, "Status") + code_pages[5].add(0x0C, "DstMsgId") + + + # Code Page 6: GetItemEstimate + code_pages.update({ + 6 : code_page( "GetItemEstimate" , "getitemestimate", 6 ) + }) + code_pages[6].add(0x05, "GetItemEstimate") + code_pages[6].add(0x06, "Version") + code_pages[6].add(0x07, "Collections") + code_pages[6].add(0x08, "Collection") + code_pages[6].add(0x09, "Class") + code_pages[6].add(0x0A, "CollectionId") + code_pages[6].add(0x0B, "DateTime") + code_pages[6].add(0x0C, "Estimate") + code_pages[6].add(0x0D, "Response") + code_pages[6].add(0x0E, "Status") + + + # Code Page 7: FolderHierarchy + code_pages.update({ + 7 : code_page( "FolderHierarchy" , "folderhierarchy", 7 ) + }) + code_pages[7].add(0x07, "DisplayName") + code_pages[7].add(0x08, "ServerId") + code_pages[7].add(0x09, "ParentId") + code_pages[7].add(0x0A, "Type") + code_pages[7].add(0x0C, "Status") + code_pages[7].add(0x0E, "Changes") + code_pages[7].add(0x0F, "Add") + code_pages[7].add(0x10, "Delete") + code_pages[7].add(0x11, "Update") + code_pages[7].add(0x12, "SyncKey") + code_pages[7].add(0x13, "FolderCreate") + code_pages[7].add(0x14, "FolderDelete") + code_pages[7].add(0x15, "FolderUpdate") + code_pages[7].add(0x16, "FolderSync") + code_pages[7].add(0x17, "Count") + + + # Code Page 8: MeetingResponse + code_pages.update({ + 8 : code_page( "MeetingResponse" , "meetingresponse", 8 ) + }) + code_pages[8].add(0x05, "CalendarId") + code_pages[8].add(0x06, "CollectionId") + code_pages[8].add(0x07, "MeetingResponse") + code_pages[8].add(0x08, "RequestId") + code_pages[8].add(0x09, "Request") + code_pages[8].add(0x0A, "Result") + code_pages[8].add(0x0B, "Status") + code_pages[8].add(0x0C, "UserResponse") + code_pages[8].add(0x0E, "InstanceId") + + + # Code Page 9: Tasks + code_pages.update({ + 9 : code_page( "Tasks" , "tasks", 9 ) + }) + code_pages[9].add(0x08, "Categories") + code_pages[9].add(0x09, "Category") + code_pages[9].add(0x0A, "Complete") + code_pages[9].add(0x0B, "DateCompleted") + code_pages[9].add(0x0C, "DueDate") + code_pages[9].add(0x0D, "UtcDueDate") + code_pages[9].add(0x0E, "Importance") + code_pages[9].add(0x0F, "Recurrence") + code_pages[9].add(0x10, "Type") + code_pages[9].add(0x11, "Start") + code_pages[9].add(0x12, "Until") + code_pages[9].add(0x13, "Occurrences") + code_pages[9].add(0x14, "Interval") + code_pages[9].add(0x15, "DayOfMonth") + code_pages[9].add(0x16, "DayOfWeek") + code_pages[9].add(0x17, "WeekOfMonth") + code_pages[9].add(0x18, "MonthOfYear") + code_pages[9].add(0x19, "Regenerate") + code_pages[9].add(0x1A, "DeadOccur") + code_pages[9].add(0x1B, "ReminderSet") + code_pages[9].add(0x1C, "ReminderTime") + code_pages[9].add(0x1D, "Sensitivity") + code_pages[9].add(0x1E, "StartDate") + code_pages[9].add(0x1F, "UtcStartDate") + code_pages[9].add(0x20, "Subject") + code_pages[9].add(0x22, "OrdinalDate") + code_pages[9].add(0x23, "SubOrdinalDate") + code_pages[9].add(0x24, "CalendarType") + code_pages[9].add(0x25, "IsLeapMonth") + code_pages[9].add(0x26, "FirstDayOfWeek") + + + # Code Page 10: ResolveRecipients + code_pages.update({ + 10 : code_page( "ResolveRecipients" , "resolverecipients", 10 ) + }) + code_pages[10].add(0x05, "ResolveRecipients") + code_pages[10].add(0x06, "Response") + code_pages[10].add(0x07, "Status") + code_pages[10].add(0x08, "Type") + code_pages[10].add(0x09, "Recipient") + code_pages[10].add(0x0A, "DisplayName") + code_pages[10].add(0x0B, "EmailAddress") + code_pages[10].add(0x0C, "Certificates") + code_pages[10].add(0x0D, "Certificate") + code_pages[10].add(0x0E, "MiniCertificate") + code_pages[10].add(0x0F, "Options") + code_pages[10].add(0x10, "To") + code_pages[10].add(0x11, "CertificateRetrieval") + code_pages[10].add(0x12, "RecipientCount") + code_pages[10].add(0x13, "MaxCertificates") + code_pages[10].add(0x14, "MaxAmbiguousRecipients") + code_pages[10].add(0x15, "CertificateCount") + code_pages[10].add(0x16, "Availability") + code_pages[10].add(0x17, "StartTime") + code_pages[10].add(0x18, "EndTime") + code_pages[10].add(0x19, "MergedFreeBusy") + code_pages[10].add(0x1A, "Picture") + code_pages[10].add(0x1B, "MaxSize") + code_pages[10].add(0x1C, "Data") + code_pages[10].add(0x1D, "MaxPictures") + + + # Code Page 11: ValidateCert + code_pages.update({ + 11 : code_page( "ValidateCert" , "validatecert", 11 ) + }) + code_pages[11].add(0x05, "ValidateCert") + code_pages[11].add(0x06, "Certificates") + code_pages[11].add(0x07, "Certificate") + code_pages[11].add(0x08, "CertificateChain") + code_pages[11].add(0x09, "CheckCRL") + code_pages[11].add(0x0A, "Status") + + + # Code Page 12: Contacts2 + code_pages.update({ + 12 : code_page( "Contacts2" , "contacts2", 12 ) + }) + code_pages[12].add(0x05, "CustomerId") + code_pages[12].add(0x06, "GovernmentId") + code_pages[12].add(0x07, "IMAddress") + code_pages[12].add(0x08, "IMAddress2") + code_pages[12].add(0x09, "IMAddress3") + code_pages[12].add(0x0A, "ManagerName") + code_pages[12].add(0x0B, "CompanyMainPhone") + code_pages[12].add(0x0C, "AccountName") + code_pages[12].add(0x0D, "NickName") + code_pages[12].add(0x0E, "MMS") + + + # Code Page 13: Ping + code_pages.update({ + 13 : code_page( "Ping" , "ping", 13 ) + }) + code_pages[13].add(0x05, "Ping") + code_pages[13].add(0x06, "AutdState") # Per MS-ASWBXML, this tag is not used by protocol + code_pages[13].add(0x07, "Status") + code_pages[13].add(0x08, "HeartbeatInterval") + code_pages[13].add(0x09, "Folders") + code_pages[13].add(0x0A, "Folder") + code_pages[13].add(0x0B, "Id") + code_pages[13].add(0x0C, "Class") + code_pages[13].add(0x0D, "MaxFolders") + + + # Code Page 14: Provision + code_pages.update({ + 14 : code_page( "Provision" , "provision", 14 ) + }) + code_pages[14].add(0x05, "Provision") + code_pages[14].add(0x06, "Policies") + code_pages[14].add(0x07, "Policy") + code_pages[14].add(0x08, "PolicyType") + code_pages[14].add(0x09, "PolicyKey") + code_pages[14].add(0x0A, "Data") + code_pages[14].add(0x0B, "Status") + code_pages[14].add(0x0C, "RemoteWipe") + code_pages[14].add(0x0D, "EASProvisionDoc") + code_pages[14].add(0x0E, "DevicePasswordEnabled") + code_pages[14].add(0x0F, "AlphanumericDevicePasswordRequired") + #code_pages[14].add(0x10, "DeviceEncryptionEnabled") + code_pages[14].add(0x10, "RequireStorageCardEncryption") + code_pages[14].add(0x11, "PasswordRecoveryEnabled") + code_pages[14].add(0x13, "AttachmentsEnabled") + code_pages[14].add(0x14, "MinDevicePasswordLength") + code_pages[14].add(0x15, "MaxInactivityTimeDeviceLock") + code_pages[14].add(0x16, "MaxDevicePasswordFailedAttempts") + code_pages[14].add(0x17, "MaxAttachmentSize") + code_pages[14].add(0x18, "AllowSimpleDevicePassword") + code_pages[14].add(0x19, "DevicePasswordExpiration") + code_pages[14].add(0x1A, "DevicePasswordHistory") + code_pages[14].add(0x1B, "AllowStorageCard") + code_pages[14].add(0x1C, "AllowCamera") + code_pages[14].add(0x1D, "RequireDeviceEncryption") + code_pages[14].add(0x1E, "AllowUnsignedApplications") + code_pages[14].add(0x1F, "AllowUnsignedInstallationPackages") + code_pages[14].add(0x20, "MinDevicePasswordComplexCharacters") + code_pages[14].add(0x21, "AllowWiFi") + code_pages[14].add(0x22, "AllowTextMessaging") + code_pages[14].add(0x23, "AllowPOPIMAPEmail") + code_pages[14].add(0x24, "AllowBluetooth") + code_pages[14].add(0x25, "AllowIrDA") + code_pages[14].add(0x26, "RequireManualSyncWhenRoaming") + code_pages[14].add(0x27, "AllowDesktopSync") + code_pages[14].add(0x28, "MaxCalendarAgeFilter") + code_pages[14].add(0x29, "AllowHTMLEmail") + code_pages[14].add(0x2A, "MaxEmailAgeFilter") + code_pages[14].add(0x2B, "MaxEmailBodyTruncationSize") + code_pages[14].add(0x2C, "MaxEmailHTMLBodyTruncationSize") + code_pages[14].add(0x2D, "RequireSignedSMIMEMessages") + code_pages[14].add(0x2E, "RequireEncryptedSMIMEMessages") + code_pages[14].add(0x2F, "RequireSignedSMIMEAlgorithm") + code_pages[14].add(0x30, "RequireEncryptionSMIMEAlgorithm") + code_pages[14].add(0x31, "AllowSMIMEEncryptionAlgorithmNegotiation") + code_pages[14].add(0x32, "AllowSMIMESoftCerts") + code_pages[14].add(0x33, "AllowBrowser") + code_pages[14].add(0x34, "AllowConsumerEmail") + code_pages[14].add(0x35, "AllowRemoteDesktop") + code_pages[14].add(0x36, "AllowInternetSharing") + code_pages[14].add(0x37, "UnapprovedInROMApplicationList") + code_pages[14].add(0x38, "ApplicationName") + code_pages[14].add(0x39, "ApprovedApplicationList") + code_pages[14].add(0x3A, "Hash") + + + # Code Page 15: Search + code_pages.update({ + 15 : code_page( "Search" , "search", 15 ) + }) + code_pages[15].add(0x05, "Search") + code_pages[15].add(0x07, "Store") + code_pages[15].add(0x08, "Name") + code_pages[15].add(0x09, "Query") + code_pages[15].add(0x0A, "Options") + code_pages[15].add(0x0B, "Range") + code_pages[15].add(0x0C, "Status") + code_pages[15].add(0x0D, "Response") + code_pages[15].add(0x0E, "Result") + code_pages[15].add(0x0F, "Properties") + code_pages[15].add(0x10, "Total") + code_pages[15].add(0x11, "EqualTo") + code_pages[15].add(0x12, "Value") + code_pages[15].add(0x13, "And") + code_pages[15].add(0x14, "Or") + code_pages[15].add(0x15, "FreeText") + code_pages[15].add(0x17, "DeepTraversal") + code_pages[15].add(0x18, "LongId") + code_pages[15].add(0x19, "RebuildResults") + code_pages[15].add(0x1A, "LessThan") + code_pages[15].add(0x1B, "GreaterThan") + code_pages[15].add(0x1E, "UserName") + code_pages[15].add(0x1F, "Password") + code_pages[15].add(0x20, "ConversationId") + code_pages[15].add(0x21, "Picture") + code_pages[15].add(0x22, "MaxSize") + code_pages[15].add(0x23, "MaxPictures") + + + # Code Page 16: GAL + code_pages.update({ + 16 : code_page( "GAL" , "gal", 16 ) + }) + code_pages[16].add(0x05, "DisplayName") + code_pages[16].add(0x06, "Phone") + code_pages[16].add(0x07, "Office") + code_pages[16].add(0x08, "Title") + code_pages[16].add(0x09, "Company") + code_pages[16].add(0x0A, "Alias") + code_pages[16].add(0x0B, "FirstName") + code_pages[16].add(0x0C, "LastName") + code_pages[16].add(0x0D, "HomePhone") + code_pages[16].add(0x0E, "MobilePhone") + code_pages[16].add(0x0F, "EmailAddress") + code_pages[16].add(0x10, "Picture") + code_pages[16].add(0x11, "Status") + code_pages[16].add(0x12, "Data") + + + # Code Page 17: AirSyncBase + code_pages.update({ + 17 : code_page( "AirSyncBase" , "airsyncbase", 17 ) + }) + code_pages[17].add(0x05, "BodyPreference") + code_pages[17].add(0x06, "Type") + code_pages[17].add(0x07, "TruncationSize") + code_pages[17].add(0x08, "AllOrNone") + code_pages[17].add(0x0A, "Body") + code_pages[17].add(0x0B, "Data") + code_pages[17].add(0x0C, "EstimatedDataSize") + code_pages[17].add(0x0D, "Truncated") + code_pages[17].add(0x0E, "Attachments") + code_pages[17].add(0x0F, "Attachment") + code_pages[17].add(0x10, "DisplayName") + code_pages[17].add(0x11, "FileReference") + code_pages[17].add(0x12, "Method") + code_pages[17].add(0x13, "ContentId") + code_pages[17].add(0x14, "ContentLocation") + code_pages[17].add(0x15, "IsInline") + code_pages[17].add(0x16, "NativeBodyType") + code_pages[17].add(0x17, "ContentType") + code_pages[17].add(0x18, "Preview") + code_pages[17].add(0x19, "BodyPartPreference") + code_pages[17].add(0x1A, "BodyPart") + code_pages[17].add(0x1B, "Status") + + + # Code Page 18: Settings + code_pages.update({ + 18 : code_page( "Settings" , "settings", 18 ) + }) + code_pages[18].add(0x05, "Settings") + code_pages[18].add(0x06, "Status") + code_pages[18].add(0x07, "Get") + code_pages[18].add(0x08, "Set") + code_pages[18].add(0x09, "Oof") + code_pages[18].add(0x0A, "OofState") + code_pages[18].add(0x0B, "StartTime") + code_pages[18].add(0x0C, "EndTime") + code_pages[18].add(0x0D, "OofMessage") + code_pages[18].add(0x0E, "AppliesToInternal") + code_pages[18].add(0x0F, "AppliesToExternalKnown") + code_pages[18].add(0x10, "AppliesToExternalUnknown") + code_pages[18].add(0x11, "Enabled") + code_pages[18].add(0x12, "ReplyMessage") + code_pages[18].add(0x13, "BodyType") + code_pages[18].add(0x14, "DevicePassword") + code_pages[18].add(0x15, "Password") + code_pages[18].add(0x16, "DeviceInformation") + code_pages[18].add(0x17, "Model") + code_pages[18].add(0x18, "IMEI") + code_pages[18].add(0x19, "FriendlyName") + code_pages[18].add(0x1A, "OS") + code_pages[18].add(0x1B, "OSLanguage") + code_pages[18].add(0x1C, "PhoneNumber") + code_pages[18].add(0x1D, "UserInformation") + code_pages[18].add(0x1E, "EmailAddresses") + code_pages[18].add(0x1F, "SMTPAddress") + code_pages[18].add(0x20, "UserAgent") + code_pages[18].add(0x21, "EnableOutboundSMS") + code_pages[18].add(0x22, "MobileOperator") + code_pages[18].add(0x23, "PrimarySmtpAddress") + code_pages[18].add(0x24, "Accounts") + code_pages[18].add(0x25, "Account") + code_pages[18].add(0x26, "AccountId") + code_pages[18].add(0x27, "AccountName") + code_pages[18].add(0x28, "UserDisplayName") + code_pages[18].add(0x29, "SendDisabled") + code_pages[18].add(0x2B, "RightsManagementInformation") + + + # Code Page 19: DocumentLibrary + code_pages.update({ + 19 : code_page( "DocumentLibrary" , "documentlibrary", 19 ) + }) + code_pages[19].add(0x05, "LinkId") + code_pages[19].add(0x06, "DisplayName") + code_pages[19].add(0x07, "IsFolder") + code_pages[19].add(0x08, "CreationDate") + code_pages[19].add(0x09, "LastModifiedDate") + code_pages[19].add(0x0A, "IsHidden") + code_pages[19].add(0x0B, "ContentLength") + code_pages[19].add(0x0C, "ContentType") + + + # Code Page 20: ItemOperations + code_pages.update({ + 20 : code_page( "ItemOperations" , "itemoperations", 20 ) + }) + code_pages[20].add(0x05, "ItemOperations") + code_pages[20].add(0x06, "Fetch") + code_pages[20].add(0x07, "Store") + code_pages[20].add(0x08, "Options") + code_pages[20].add(0x09, "Range") + code_pages[20].add(0x0A, "Total") + code_pages[20].add(0x0B, "Properties") + code_pages[20].add(0x0C, "Data") + code_pages[20].add(0x0D, "Status") + code_pages[20].add(0x0E, "Response") + code_pages[20].add(0x0F, "Version") + code_pages[20].add(0x10, "Schema") + code_pages[20].add(0x11, "Part") + code_pages[20].add(0x12, "EmptyFolderContents") + code_pages[20].add(0x13, "DeleteSubFolders") + code_pages[20].add(0x14, "UserName") + code_pages[20].add(0x15, "Password") + code_pages[20].add(0x16, "Move") + code_pages[20].add(0x17, "DstFldId") + code_pages[20].add(0x18, "ConversationId") + code_pages[20].add(0x19, "MoveAlways") + + + # Code Page 21: ComposeMail + code_pages.update({ + 21 : code_page( "ComposeMail" , "composemail", 21 ) + }) + code_pages[21].add(0x05, "SendMail") + code_pages[21].add(0x06, "SmartForward") + code_pages[21].add(0x07, "SmartReply") + code_pages[21].add(0x08, "SaveInSentItems") + code_pages[21].add(0x09, "ReplaceMime") + code_pages[21].add(0x0B, "Source") + code_pages[21].add(0x0C, "FolderId") + code_pages[21].add(0x0D, "ItemId") + code_pages[21].add(0x0E, "LongId") + code_pages[21].add(0x0F, "InstanceId") + code_pages[21].add(0x10, "Mime") + code_pages[21].add(0x11, "ClientId") + code_pages[21].add(0x12, "Status") + code_pages[21].add(0x13, "AccountId") + + + # Code Page 22: Email2 + code_pages.update({ + 22 : code_page( "Email2" , "email2", 22 ) + }) + code_pages[22].add(0x05, "UmCallerID") + code_pages[22].add(0x06, "UmUserNotes") + code_pages[22].add(0x07, "UmAttDuration") + code_pages[22].add(0x08, "UmAttOrder") + code_pages[22].add(0x09, "ConversationId") + code_pages[22].add(0x0A, "ConversationIndex") + code_pages[22].add(0x0B, "LastVerbExecuted") + code_pages[22].add(0x0C, "LastVerbExecutionTime") + code_pages[22].add(0x0D, "ReceivedAsBcc") + code_pages[22].add(0x0E, "Sender") + code_pages[22].add(0x0F, "CalendarType") + code_pages[22].add(0x10, "IsLeapMonth") + code_pages[22].add(0x11, "AccountId") + code_pages[22].add(0x12, "FirstDayOfWeek") + code_pages[22].add(0x13, "MeetingMessageType") + + + # Code Page 23: Notes + code_pages.update({ + 23 : code_page( "Notes" , "notes", 23 ) + }) + code_pages[23].add(0x05, "Subject") + code_pages[23].add(0x06, "MessageClass") + code_pages[23].add(0x07, "LastModifiedDate") + code_pages[23].add(0x08, "Categories") + code_pages[23].add(0x09, "Category") + + + # Code Page 24: RightsManagement + code_pages.update({ + 24 : code_page( "RightsManagement" , "rightsmanagement", 24 ) + }) + code_pages[24].add(0x05, "RightsManagementSupport") + code_pages[24].add(0x06, "RightsManagementTemplates") + code_pages[24].add(0x07, "RightsManagementTemplate") + code_pages[24].add(0x08, "RightsManagementLicense") + code_pages[24].add(0x09, "EditAllowed") + code_pages[24].add(0x0A, "ReplyAllowed") + code_pages[24].add(0x0B, "ReplyAllAllowed") + code_pages[24].add(0x0C, "ForwardAllowed") + code_pages[24].add(0x0D, "ModifyRecipientsAllowed") + code_pages[24].add(0x0E, "ExtractAllowed") + code_pages[24].add(0x0F, "PrintAllowed") + code_pages[24].add(0x10, "ExportAllowed") + code_pages[24].add(0x11, "ProgrammaticAccessAllowed") + code_pages[24].add(0x12, "Owner") + code_pages[24].add(0x13, "ContentExpiryDate") + code_pages[24].add(0x14, "TemplateID") + code_pages[24].add(0x15, "TemplateName") + code_pages[24].add(0x16, "TemplateDescription") + code_pages[24].add(0x17, "ContentOwner") + code_pages[24].add(0x18, "RemoveRightsManagementDistribution") + + cp_shorthand = {"rm":"RightsManagement"} + + return code_pages, cp_shorthand + + + diff --git a/peas/pyActiveSync/utils/code_page.py b/peas/pyActiveSync/utils/code_page.py new file mode 100644 index 0000000..d559341 --- /dev/null +++ b/peas/pyActiveSync/utils/code_page.py @@ -0,0 +1,55 @@ +######################################################################## +# Copyright (C) 2013 Sol Birnbaum +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +######################################################################## + + +class code_page(object): + """A code page is a map of tokens to tags""" + def __init__(self, namespace=None, xmlns=None, index=None): + self.namespace = namespace + self.xmlns = xmlns + self.index = index + self.tokens = {} + self.tags = {} + + def add(self, token, tag): + self.tags.update({ token : tag }) + self.tokens.update({ tag : token }) + + def get(self, t, token_or_tag): + if t == 0: + return get_token(token_or_tag) + elif t == 1: + return get_tag(token_or_tag) + + def get_token(self, tag): + return self.tokens[tag] + + def get_tag(self, token): + #print token, self.xmlns + return self.tags[token] + + def __repr__(self): + import pprint + return "\r\n Namespace:%s - Xmlns:%s\r\n%s\r\n" % (self.namespace, self.xmlns, pprint.pformat(self.tokens)) + + def __iter__(self): + lnamespace = self.namespace + lxmlns = self.xmlns + for tag, token in self.tags.items(): + yield (lnamespace, lxmlns, tag, token) diff --git a/peas/pyActiveSync/utils/wapxml.py b/peas/pyActiveSync/utils/wapxml.py new file mode 100644 index 0000000..18cfb6c --- /dev/null +++ b/peas/pyActiveSync/utils/wapxml.py @@ -0,0 +1,124 @@ +######################################################################## +# Modified 2016 from code copyright (C) 2013 Sol Birnbaum +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +######################################################################## + + +class wapxmltree(object): + def __init__(self, inwapxmlstr=None): + self.header = "" + self._root_node = None + if inwapxmlstr: + self.parse_string(inwapxmlstr) + return + def parse_string(self, xmlstr): + return + def set_root(self, root_node, xmlns): + self._root_node = root_node + self._root_node.set_root(True, xmlns, self) + def get_root(self): + return self._root_node + def __repr__(self): + if self._root_node: + return self.header + repr(self._root_node) + + +class wapxmlnode(object): + def __init__(self, tag, parent=None, text=None, cdata=None): + self.tag = tag + self.text = text + self.cdata = cdata + self._children = [] + self._is_root = None + self._xmlns = None + self._parent = None + if parent: + try: + self.set_parent(parent) + except Exception, e: + print e + def set_parent(self, parent): + parent.add_child(self) + self._parent = parent + def get_parent(self): + return self._parent + def add_child(self, child): + self._children.append(child) + def remove_child(self, child): + self._children.remove(child) + def set_root(self, true_or_false, xmlns=None, parent=None): + self._is_root = true_or_false + self._xmlns = xmlns + self._parent = parent + def is_root(self): + return self._is_root + def set_xmlns(self, xmlns): + self._xmlns = xmlns + def get_xmlns(self): + return self._xmlns + def has_children(self): + if len(self._children) > 0: + return True + else: + return False + def get_children(self): + return self._children + + def basic_xpath(self, tag_path): + """Get all the children with the final tag name after following the tag path list e.g. search/results.""" + + tag_path = tag_path.split('/') + + results = [] + for child in self._children: + if child.tag == tag_path[0]: + if len(tag_path) == 1: + results.append(child) + else: + results.extend(child.basic_xpath('/'.join(tag_path[1:]))) + return results + + def __repr__(self, tabs=" "): + if (self.text != None) or (self.cdata != None) or (len(self._children)>0): + inner_text = "" + if self.text != None: + inner_text+=str(self.text) + if self.cdata != None: + inner_text+= "" % str(self.cdata) + if self.has_children(): + for child in self._children: + inner_text+=child.__repr__(tabs+" ") + if not self._is_root: + end_tabs = "" + if self.has_children(): end_tabs = "\r\n"+tabs + return "\r\n%s<%s>%s%s" % (tabs, self.tag, inner_text, end_tabs, self.tag) + else: return "\r\n<%s xmlns=\"%s:\">%s\r\n" % (self.tag, self._xmlns, inner_text, self.tag) + elif self._is_root: + return "\r\n<%s xmlns=\"%s:\">" % (self.tag, self._xmlns, self.tag) + else: + return "%s<%s />" % (tabs, self.tag) + def __iter__(self): + if len(self._children) > 0: + for child in self._children: + yield child + def __str__(self): + return self.__repr__() + + + + + diff --git a/peas/pyActiveSync/utils/wbxml.py b/peas/pyActiveSync/utils/wbxml.py new file mode 100644 index 0000000..6f5ee6d --- /dev/null +++ b/peas/pyActiveSync/utils/wbxml.py @@ -0,0 +1,298 @@ +######################################################################## +# Copyright (C) 2013 Sol Birnbaum +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +######################################################################## + + +from wapxml import wapxmltree, wapxmlnode + +class wbxml_parser(object): + """WBXML Parser""" + + VERSION_BYTE = 0x03 + PUBLIC_IDENTIFIER_BYTE = 0x01 + CHARSET_BYTE = 0x6A #Currently, only UTF-8 is used by MS-ASWBXML + STRING_TABLE_LENGTH_BYTE = 0x00 #String tables are not used by MS-ASWBXML + + class GlobalTokens: + SWITCH_PAGE = 0x00 + END = 0x01 + ENTITY = 0x02 #Not used by MS-ASWBXML + STR_I = 0x03 + LITERAL = 0x04 + EXT_I_0 = 0x40 #Not used by MS-ASWBXML + EXT_I_1 = 0x41 #Not used by MS-ASWBXML + EXT_I_2 = 0x42 #Not used by MS-ASWBXML + PI = 0x43 #Not used by MS-ASWBXML + LITERAL_C = 0x44 #Not used by MS-ASWBXML + EXT_T_0 = 0x80 #Not used by MS-ASWBXML + EXT_T_1 = 0x81 #Not used by MS-ASWBXML + EXT_T_2 = 0x82 #Not used by MS-ASWBXML + STR_T = 0x83 #Not used by MS-ASWBXML + LITERAL_A = 0x84 #Not used by MS-ASWBXML + EXT_0 = 0xC0 #Not used by MS-ASWBXML + EXT_1 = 0xC1 #Not used by MS-ASWBXML + EXT_2 = 0xC2 #Not used by MS-ASWBXML + OPAQUE = 0xC3 + LITERAL_AC = 0xC4 #Not used by MS-ASWBXML + + def __init__(self, code_pages, cp_shorthand={}): + self.wapxml = None + self.wbxml = None + self.pointer = 0 + self.code_pages = code_pages + self.cp_shorthand = cp_shorthand + return + + def encode(self, inwapxml=None): + wbxml_bytes = bytearray() + + if not inwapxml: return wbxml_bytes + + #add headers + wbxml_bytes.append(self.VERSION_BYTE) + wbxml_bytes.extend(self.encode_multibyte_integer(self.PUBLIC_IDENTIFIER_BYTE)) + wbxml_bytes.extend(self.encode_multibyte_integer(self.CHARSET_BYTE)) + wbxml_bytes.extend(self.encode_multibyte_integer(self.STRING_TABLE_LENGTH_BYTE)) + + #add code_page/xmlns + wbxml_bytes.append(self.GlobalTokens.SWITCH_PAGE) + current_code_page_index = self.encode_xmlns_as_codepage(inwapxml.get_root().get_xmlns()) + wbxml_bytes.append(current_code_page_index) + self.current_code_page = self.code_pages[current_code_page_index] + self.default_code_page = self.code_pages[current_code_page_index] + + #add root token/tag + token_tag = self.current_code_page.get_token(inwapxml.get_root().tag) + if inwapxml.get_root().has_children(): + token_tag |= 0x40 + wbxml_bytes.append(token_tag) + + current_node = inwapxml.get_root() + + if current_node.has_children(): + for child in current_node.get_children(): + self.encode_node_recursive(child, wbxml_bytes) + wbxml_bytes.append(self.GlobalTokens.END) + return wbxml_bytes + + def encode_node_recursive(self, current_node, wbxml_bytes): + if ":" in current_node.tag: + split_xmlns_tag = current_node.tag.split(":") + possibly_new_code_page = self.code_pages[self.encode_xmlns_as_codepage(split_xmlns_tag[0])] + if possibly_new_code_page.index != self.current_code_page.index: + wbxml_bytes.append(self.GlobalTokens.SWITCH_PAGE) + wbxml_bytes.append(possibly_new_code_page.index) + self.current_code_page = possibly_new_code_page + token_tag = self.current_code_page.get_token(split_xmlns_tag[1]) + else: + if self.current_code_page.index != self.default_code_page.index: + wbxml_bytes.append(self.GlobalTokens.SWITCH_PAGE) + wbxml_bytes.append(self.default_code_page.index) + self.current_code_page = self.code_pages[self.default_code_page.index] + token_tag = self.current_code_page.get_token(current_node.tag) + token_tag |= 0x40 + wbxml_bytes.append(token_tag) + #text, cdata = None, None + if current_node.text: + wbxml_bytes.append(self.GlobalTokens.STR_I) + wbxml_bytes.extend(self.encode_string(current_node.text)) + elif current_node.cdata: + wbxml_bytes.append(self.GlobalTokens.OPAQUE) + if current_node.tag == "Mime": + wbxml_bytes.extend(self.encode_string_as_opaquedata(current_node.cdata.as_string())) + else: #ConversationMode or ConversationId + wbxml_bytes.extend(self.encode_hexstring_as_opaquedata(current_node.cdata)) + if current_node.has_children(): + for child in current_node.get_children(): + 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 + wbxml_bytes.append(self.GlobalTokens.END) + + def decode(self, inwbxml=None): + if inwbxml: + self.wbxml = bytearray() + self.wbxml.extend(inwbxml) + elif not self.wbxml: + 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.") + self.pointer = 0 + ver = self.decode_byte() + public_id = self.decode_multibyte_integer() + charset = self.decode_multibyte_integer() + string_table_len = self.decode_multibyte_integer() + + if charset is not 0x6A: + raise AttributeError("Currently, only UTF-8 is used by MS-ASWBXML") + return + if string_table_len > 0: + raise AttributeError("String tables are not used by MS-ASWBXML") + return + + wapxmldoc = wapxmltree() + current_element = None + first_iter = True + + byte = self.decode_byte() + if byte is not self.GlobalTokens.SWITCH_PAGE: + if self.default_code_page: + default_code_page = self.default_code_page + self.pointer-=1 + else: + raise AttributeError("No first or default code page defined.") + else: + default_code_page = self.code_pages[self.decode_byte()] + root_element = wapxmlnode("?") + current_code_page = default_code_page + root_element.set_xmlns(current_code_page.xmlns) + wapxmldoc.set_root(root_element, root_element.get_xmlns()) + current_element = root_element + + temp_xmlns = "" + + + while self.pointer < len(inwbxml): + byte = self.decode_byte() + if byte is self.GlobalTokens.SWITCH_PAGE: + current_code_page = self.code_pages[self.decode_byte()] + if current_code_page != default_code_page: + temp_xmlns = current_code_page.xmlns + ":" + else: + temp_xmlns = "" + elif byte is self.GlobalTokens.END: + if not current_element.is_root(): + current_element = current_element.get_parent() + else: + if self.pointer < len(self.wbxml): + raise EOFError("END token incorrectly placed after root node.") + else: + return wapxmldoc + elif byte is self.GlobalTokens.STR_I: + current_element.text = self.decode_string() + elif byte is self.GlobalTokens.OPAQUE: + opq_len = self.decode_byte() + opq_str = "" + if current_element.tag == "Mime": + opq_str = self.decode_string(opq_len) + else: + import binascii + opq_str = binascii.hexlify(self.decode_binary(opq_len)) + current_element.text = opq_str + else: + if byte & 0x80 > 0: + raise AttributeError("Token has attributes. MS-ASWBXML does not use attributes.") + token = byte & 0x3f + tag_token = temp_xmlns + current_code_page.get_tag(token) + if not first_iter: + new_element = wapxmlnode(tag_token, current_element) + if (byte & 0x40): #check to see if new element has children + current_element = new_element + elif current_element.is_root(): + current_element.tag = tag_token + first_iter = False + else: + raise IndexError("Missing root element.") + return wapxmldoc + + + # encode helper functions + def encode_xmlns_as_codepage(self, inxmlns_or_namespace): + lc_inxmlns = inxmlns_or_namespace.lower() + for cp_index, code_page in self.code_pages.items(): + if code_page.xmlns == lc_inxmlns: + return cp_index + if inxmlns_or_namespace in self.cp_shorthand.keys(): + lc_inxmlns = self.cp_shorthand[inxmlns_or_namespace].lower() + for cp_index, code_page in self.code_pages.items(): + if code_page.xmlns == lc_inxmlns: + return cp_index + raise IndexError("No such code page exists in current object") + + def encode_string(self, string): + string = str(string) + retarray = bytearray(string, "utf-8") + retarray.append("\x00") + return retarray + + def encode_string_as_opaquedata(self, string): + retarray = bytearray() + retarray.extend(self.encode_multibyte_integer(len(string))) + retarray.extend(bytearray(string, "utf-8")) + return retarray + + def encode_hexstring_as_opaquedata(self, hexstring): + retarray = bytearray() + retarray.extend(self.encode_multibyte_integer(len(hexstring))) + retarray.extend(hexstring) + return retarray + + def encode_multibyte_integer(self, integer): + retarray = bytearray() + if integer == 0: + retarray.append(integer) + return retarray + last = True + while integer > 0: + if last: + retarray.append( integer & 0x7f ) + last = False + else: + retarray.append( ( integer & 0x7f ) | 0x80 ) + integer = integer >> 7 + retarray.reverse() + return retarray + + # decode helper functions + def decode_codepages_as_xmlns(self): + return + + def decode_string(self, length=None): + retarray = bytearray() + if length is None: + #terminator = b"\x00" + while self.wbxml[self.pointer] != 0:#terminator: + retarray.append(self.wbxml[self.pointer]) + self.pointer += 1 + self.pointer+=1 + else: + for i in range(0, length): + retarray.append(self.wbxml[self.pointer]) + self.pointer+=1 + return str(retarray) + + def decode_byte(self): + self.pointer+=1 + return self.wbxml[self.pointer-1] + + def decode_multibyte_integer(self): + #print "indices: ", self.pointer, "of", len(self.wbxml) + if self.pointer >= len(self.wbxml): + raise IndexError("wbxml is truncated. nothing left to decode") + integer = 0 + while ( self.wbxml[self.pointer] & 0x80 ) != 0: + integer = integer << 7 + integer = integer + ( self.wbxml[self.pointer] & 0x7f ) + self.pointer += 1 + integer = integer << 7 + integer = integer + ( self.wbxml[self.pointer] & 0x7f ) + self.pointer += 1 + return integer + + def decode_binary(self, length=0): + retarray = bytearray() + for i in range(0, length): + retarray.append(self.wbxml[self.pointer]) + self.pointer+=1 + return retarray diff --git a/peas/py_activesync_helper.py b/peas/py_activesync_helper.py new file mode 100644 index 0000000..6c13167 --- /dev/null +++ b/peas/py_activesync_helper.py @@ -0,0 +1,317 @@ +######################################################################## +# Modified 2016 from code Copyright (C) 2013 Sol Birnbaum +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +########################################################################\ + +import ssl + +# https://docs.python.org/2/library/xml.html#xml-vulnerabilities +from lxml import etree as ElementTree + +from pyActiveSync.utils.as_code_pages import as_code_pages +from pyActiveSync.utils.wbxml import wbxml_parser +from pyActiveSync.client.storage import storage + +from pyActiveSync.client.FolderSync import FolderSync +from pyActiveSync.client.Sync import Sync +from pyActiveSync.client.GetItemEstimate import GetItemEstimate +from pyActiveSync.client.Provision import Provision +from pyActiveSync.client.Search import Search +from pyActiveSync.client.ItemOperations import ItemOperations + +from pyActiveSync.objects.MSASHTTP import ASHTTPConnector +from pyActiveSync.objects.MSASCMD import as_status +from pyActiveSync.objects.MSASAIRS import airsync_FilterType, airsync_Conflict, airsync_MIMETruncation, \ + airsync_MIMESupport, \ + airsync_Class, airsyncbase_Type + + +# Create WBXML parser instance. +parser = wbxml_parser(*as_code_pages.build_as_code_pages()) + + +def _parse_for_emails(res, emails): + + data = str(res) + + etparser = ElementTree.XMLParser(recover=True) + tree = ElementTree.fromstring(data, etparser) + + for item in tree.iter('{airsync:}ApplicationData'): + s = ElementTree.tostring(item) + emails.append(s) + + +def as_request(as_conn, cmd, wapxml_req): + #print "\r\n%s Request:" % cmd + #print wapxml_req + res = as_conn.post(cmd, parser.encode(wapxml_req)) + wapxml_res = parser.decode(res) + #print "\r\n%s Response:" % cmd + #print wapxml_res + return wapxml_res + + +#Provision functions +def do_apply_eas_policies(policies): + for policy in policies.keys(): + #print "Virtually applying %s = %s" % (policy, policies[policy]) + pass + return True + + +def do_provision(as_conn, device_info): + provision_xmldoc_req = Provision.build("0", device_info) + as_conn.set_policykey("0") + provision_xmldoc_res = as_request(as_conn, "Provision", provision_xmldoc_req) + status, policystatus, policykey, policytype, policydict, settings_status = Provision.parse(provision_xmldoc_res) + as_conn.set_policykey(policykey) + storage.update_keyvalue("X-MS-PolicyKey", policykey) + storage.update_keyvalue("EASPolicies", repr(policydict)) + if do_apply_eas_policies(policydict): + provision_xmldoc_req = Provision.build(policykey) + provision_xmldoc_res = as_request(as_conn, "Provision", provision_xmldoc_req) + status, policystatus, policykey, policytype, policydict, settings_status = Provision.parse(provision_xmldoc_res) + if status == "1": + as_conn.set_policykey(policykey) + storage.update_keyvalue("X-MS-PolicyKey", policykey) + + +#Sync function +def do_sync(as_conn, curs, collections, emails_out): + + as_sync_xmldoc_req = Sync.build(storage.get_synckeys_dict(curs), collections) + #print "\r\nSync Request:" + #print as_sync_xmldoc_req + res = as_conn.post("Sync", parser.encode(as_sync_xmldoc_req)) + #print "\r\nSync Response:" + if res == '': + #print "Nothing to Sync!" + pass + else: + collectionid_to_type_dict = storage.get_serverid_to_type_dict() + as_sync_xmldoc_res = parser.decode(res) + #print type(as_sync_xmldoc_res), dir(as_sync_xmldoc_res), as_sync_xmldoc_res + + _parse_for_emails(as_sync_xmldoc_res, emails_out) + + sync_res = Sync.parse(as_sync_xmldoc_res, collectionid_to_type_dict) + storage.update_items(sync_res) + return sync_res + + +#GetItemsEstimate +def do_getitemestimates(as_conn, curs, collection_ids, gie_options): + getitemestimate_xmldoc_req = GetItemEstimate.build(storage.get_synckeys_dict(curs), collection_ids, gie_options) + getitemestimate_xmldoc_res = as_request(as_conn, "GetItemEstimate", getitemestimate_xmldoc_req) + + getitemestimate_res = GetItemEstimate.parse(getitemestimate_xmldoc_res) + return getitemestimate_res + + +def getitemestimate_check_prime_collections(as_conn, curs, getitemestimate_responses, emails_out): + has_synckey = [] + needs_synckey = {} + for response in getitemestimate_responses: + if response.Status == "1": + has_synckey.append(response.CollectionId) + elif response.Status == "2": + #print "GetItemEstimate Status: Unknown CollectionId (%s) specified. Removing." % response.CollectionId + pass + elif response.Status == "3": + #print "GetItemEstimate Status: Sync needs to be primed." + pass + needs_synckey.update({response.CollectionId: {}}) + has_synckey.append( + response.CollectionId) #technically *will* have synckey after do_sync() need end of function + else: + #print as_status("GetItemEstimate", response.Status) + pass + if len(needs_synckey) > 0: + do_sync(as_conn, curs, needs_synckey, emails_out) + return has_synckey, needs_synckey + + +def sync(as_conn, curs, collections, collection_sync_params, gie_options, emails_out): + getitemestimate_responses = do_getitemestimates(as_conn, curs, collections, gie_options) + + has_synckey, just_got_synckey = getitemestimate_check_prime_collections(as_conn, curs, getitemestimate_responses, + emails_out) + + if (len(has_synckey) < collections) or (len(just_got_synckey) > 0): #grab new estimates, since they changed + getitemestimate_responses = do_getitemestimates(as_conn, curs, has_synckey, gie_options) + + collections_to_sync = {} + + for response in getitemestimate_responses: + if response.Status == "1": + if int(response.Estimate) > 0: + collections_to_sync.update({response.CollectionId: collection_sync_params[response.CollectionId]}) + else: + #print "GetItemEstimate Status (error): %s, CollectionId: %s." % (response.Status, response.CollectionId) + pass + + if len(collections_to_sync) > 0: + sync_res = do_sync(as_conn, curs, collections_to_sync, emails_out) + + if sync_res: + while True: + for coll_res in sync_res: + if coll_res.MoreAvailable is None: + del collections_to_sync[coll_res.CollectionId] + if len(collections_to_sync.keys()) > 0: + #print "Collections to sync:", collections_to_sync + sync_res = do_sync(as_conn, curs, collections_to_sync, emails_out) + else: + break + + +def disable_certificate_verification(): + + ssl._create_default_https_context = ssl._create_unverified_context + + +def extract_emails(creds): + + storage.erase_db() + storage.create_db_if_none() + + conn, curs = storage.get_conn_curs() + device_info = {"Model": "1234", "IMEI": "123457", + "FriendlyName": "My pyAS Client 2", "OS": "Python", "OSLanguage": "en-us", "PhoneNumber": "NA", + "MobileOperator": "NA", "UserAgent": "pyAS"} + + #create ActiveSync connector + as_conn = ASHTTPConnector(creds['server']) #e.g. "as.myserver.com" + as_conn.set_credential(creds['user'], creds['password']) + + #FolderSync + Provision + foldersync_xmldoc_req = FolderSync.build(storage.get_synckey("0")) + foldersync_xmldoc_res = as_request(as_conn, "FolderSync", foldersync_xmldoc_req) + changes, synckey, status = FolderSync.parse(foldersync_xmldoc_res) + if 138 < int(status) < 145: + ret = as_status("FolderSync", status) + #print ret + do_provision(as_conn, device_info) + foldersync_xmldoc_res = as_request(as_conn, "FolderSync", foldersync_xmldoc_req) + changes, synckey, status = FolderSync.parse(foldersync_xmldoc_res) + if 138 < int(status) < 145: + ret = as_status("FolderSync", status) + #print ret + raise Exception("Unresolvable provisioning error: %s. Cannot continue..." % status) + if len(changes) > 0: + storage.update_folderhierarchy(changes) + storage.update_synckey(synckey, "0", curs) + conn.commit() + + collection_id_of = storage.get_folder_name_to_id_dict() + + inbox = collection_id_of["Inbox"] + + collection_sync_params = { + inbox: + { #"Supported":"", + #"DeletesAsMoves":"1", + #"GetChanges":"1", + "WindowSize": "512", + "Options": { + "FilterType": airsync_FilterType.OneMonth, + "Conflict": airsync_Conflict.ServerReplacesClient, + "MIMETruncation": airsync_MIMETruncation.TruncateNone, + "MIMESupport": airsync_MIMESupport.SMIMEOnly, + "Class": airsync_Class.Email, + #"MaxItems":"300", #Recipient information cache sync requests only. Max number of frequently used contacts. + "airsyncbase_BodyPreference": [{ + "Type": airsyncbase_Type.HTML, + "TruncationSize": "1000000000", # Max 4,294,967,295 + "AllOrNone": "1", + # I.e. Do not return any body, if body size > tuncation size + #"Preview": "255", # Size of message preview to return 0-255 + }, + { + "Type": airsyncbase_Type.MIME, + "TruncationSize": "3000000000", # Max 4,294,967,295 + "AllOrNone": "1", + # I.e. Do not return any body, if body size > tuncation size + #"Preview": "255", # Size of message preview to return 0-255 + } + ], + #"airsyncbase_BodyPartPreference":"", + #"rm_RightsManagementSupport":"1" + }, + #"ConversationMode":"1", + #"Commands": {"Add":None, "Delete":None, "Change":None, "Fetch":None} + }, + } + + gie_options = { + inbox: + { #"ConversationMode": "0", + "Class": airsync_Class.Email, + "FilterType": airsync_FilterType.OneMonth + #"MaxItems": "" #Recipient information cache sync requests only. Max number of frequently used contacts. + }, + } + + collections = [inbox] + emails = [] + + sync(as_conn, curs, collections, collection_sync_params, gie_options, emails) + + if storage.close_conn_curs(conn): + del conn, curs + + return emails + + +def get_unc_listing(creds, unc_path, username=None, password=None): + + # Create ActiveSync connector. + as_conn = ASHTTPConnector(creds['server']) + as_conn.set_credential(creds['user'], creds['password']) + + # Perform request. + search_xmldoc_req = Search.build(unc_path, username=username, password=password) + search_xmldoc_res = as_request(as_conn, "Search", search_xmldoc_req) + + # Parse response. + status, records = Search.parse(search_xmldoc_res) + return records + + +def get_unc_file(creds, unc_path, username=None, password=None): + + # Create ActiveSync connector. + as_conn = ASHTTPConnector(creds['server']) + as_conn.set_credential(creds['user'], creds['password']) + + # Perform request. + operation = {'Name': 'Fetch', 'Store': 'DocumentLibrary', 'LinkId': unc_path} + if username is not None: + operation['UserName'] = username + if password is not None: + operation['Password'] = password + operations = [operation] + + xmldoc_req = ItemOperations.build(operations) + xmldoc_res = as_request(as_conn, "ItemOperations", xmldoc_req) + responses = ItemOperations.parse(xmldoc_res) + + # Parse response. + op, _, path, info, _ = responses[0] + data = info['Data'].decode('base64') + return data diff --git a/peas/py_eas_helper.py b/peas/py_eas_helper.py new file mode 100644 index 0000000..604af87 --- /dev/null +++ b/peas/py_eas_helper.py @@ -0,0 +1,56 @@ +__author__ = 'Adam Rutherford' + +from twisted.internet import reactor + +import eas_client.activesync + + +def body_result(result, emails, num_emails): + + emails.append(result['Properties']['Body']) + + # Stop after receiving final email. + if len(emails) == num_emails: + reactor.stop() + + +def sync_result(result, fid, async, emails): + + assert hasattr(result, 'keys') + + num_emails = len(result.keys()) + + for fetch_id in result.keys(): + + async.add_operation(async.fetch, collectionId=fid, serverId=fetch_id, + fetchType=4, mimeSupport=2).addBoth(body_result, emails, num_emails) + + +def fsync_result(result, async, emails): + + for (fid, finfo) in result.iteritems(): + if finfo['DisplayName'] == 'Inbox': + async.add_operation(async.sync, fid).addBoth(sync_result, fid, async, emails) + break + + +def prov_result(success, async, emails): + + if success: + async.add_operation(async.folder_sync).addBoth(fsync_result, async, emails) + else: + reactor.stop() + + +def extract_emails(creds): + + emails = [] + + async = eas_client.activesync.ActiveSync(creds['domain'], creds['user'], creds['password'], + creds['server'], True, device_id=creds['device_id'], verbose=False) + + async.add_operation(async.provision).addBoth(prov_result, async, emails) + + reactor.run() + + return emails diff --git a/screenshots/main.png b/screenshots/main.png new file mode 100644 index 0000000..30d13ca Binary files /dev/null and b/screenshots/main.png differ diff --git a/scripts/peas b/scripts/peas new file mode 100644 index 0000000..e8c0f3d --- /dev/null +++ b/scripts/peas @@ -0,0 +1,3 @@ +#!/bin/bash + +python2 -m peas.__main__ "$@" diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..4d4f55b --- /dev/null +++ b/setup.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python + +from distutils.core import setup + +setup(name='PEAS', + version='1.0', + description='ActiveSync Library', + author='Adam Rutherford', + author_email='adam.rutherford@mwrinfosecurity.com', + packages=['peas', 'peas.eas_client', + 'peas.pyActiveSync', 'peas.pyActiveSync.client', 'peas.pyActiveSync.objects', 'peas.pyActiveSync.utils'], + scripts=['scripts/peas'], + )