# -*- coding: utf-8 -*-
############################################################################
# PSHIPMGR.py
#
# Partnership manager for sync-engine.
#
# This is a rewritten version of the old Partnerships module from the
# original sync-engine. It provides considerably more flexibility in the
# assignment of partnerships over the original version and introduces
# per-partnership configurations
#
# Original partnerships.py (C) 2006  Ole André Vadla Ravnås <oleavr@gmail.com>         
#
# Dr J A Gow 29/11/07
#
############################################################################

from constants import *
import cPickle as pickle
import os
import os.path
import pyrapi2
import libxml2
import rapicontext
import xml2util
import syncdb
import logging
import util
import characteristics
import socket
import errors

class PartnershipManager:

	def __init__(self, engine):
	    
		self.logger = logging.getLogger("engine.partnerships.Partnerships")
		self.engine = engine

		self.current = None
		self.DevicePartnerships = [ None, None, None ]

	###############################
	# INTERNAL FUNCTIONS
	###############################

	#
	# _ReadDevicePartnerships
	#
	# This function retrieves the two device partnerships and inserts them into the DevicePartnerships
	# fields.
	#

	def _ReadDevicePartnerships(self):

		reg_entries = {}
		dangling_entries = []

		hklm = self.engine.rapi_session.HKEY_LOCAL_MACHINE

		# Inspect registry entries
		
		try:
		
			self.logger.debug("ReadDevicePartnerships: reading partnerships from device registry")
			partners_key = hklm.create_sub_key(r"Software\Microsoft\Windows CE Services\Partners")
		
			for pos in xrange(1, 3):
				key = partners_key.create_sub_key("P%d" % pos)
				if key.disposition == pyrapi2.REG_OPENED_EXISTING_KEY:
					try:
						id = key.query_value("PId")
						hostname = key.query_value("PName")

						self.logger.debug("ReadDevicePartnerships: read partnership ID = %s, Hostname = %s" % (id, hostname))

						if id != 0 and len(hostname) > 0:
							self.logger.debug("_read_device: Adding entry")
							reg_entries[hostname] = (pos, id)
						
					except pyrapi2.RAPIError, e:
					
						self.logger.warn("ReadDevicePartnerships: Error getting partnership key %d from device registry: %s" % (pos, e))
				key.close()
			partners_key.close()
			
		except pyrapi2.RAPIError, e:
	
			self.logger.warn("ReadDevicePartnerships: Error opening partnership keys in remote registry (%d, %s)" % (e.err_code, e))

		# Look up the synchronization data on each
		
		self.logger.debug("ReadDevicePartnerships: querying synchronization source information from device")
		
		for ctic in self.engine.rapi_session.GetConfig("Sync", "Sources").children.values():
			
			sub_ctic = self.engine.rapi_session.GetConfig("Sync.Sources", ctic.type, recursive=True)

			guid        = sub_ctic.type
			storetype   = int(sub_ctic["StoreType"])
			hostname    = sub_ctic["Server"]
			description = sub_ctic["Name"]

			self.logger.debug("ReadDevicePartnerships: read source GUID = %s, Hostname = %s, Description = %s", guid, hostname, description)
			
			if storetype == PSHMGR_STORETYPE_AS:
				
				if hostname in reg_entries:

					pos, id = reg_entries[hostname]
					del reg_entries[hostname]

					# 'pos' for AS partnerships can only exist as 1 and 2. We reserve the third slot for
					# server partnerships

               				self.logger.debug("ReadDevicePartnerships: source matches partnerhip from registry.  Initializing partnership")
					
					pship = Partnership(self.engine.config, pos, id, guid, hostname, description, storetype, self.engine.deviceName,self.engine.rapi_session)

					self.DevicePartnerships[pos-1] = pship

					self.logger.debug("ReadDevicePartnerships: querying partnerhip synchronization items (providers)")

					engine = sub_ctic.children["Engines"].children[GUID_WM5_ACTIVESYNC_ENGINE]
					for provider in engine.children["Providers"].children.values():

						self.logger.debug("ReadDevicePartnerships: found provider %s", provider["Name"])

						if int(provider["Enabled"]) != 0:
							id = None

							self.logger.debug("ReadDevicePartnerships: provider is enabled")

							if provider.type in SYNC_ITEM_ID_FROM_GUID:
								id = SYNC_ITEM_ID_FROM_GUID[provider.type]
							elif provider["Name"] == "Media":
								id = SYNC_ITEM_MEDIA
							elif provider["Name"] == "WorldMate":
								id = SYNC_ITEM_WORLDMATE

							if id == None:
								raise ValueError("Unknown GUID \"%s\" for provider with name \"%s\"" % (provider.type, provider["Name"]))

							self.logger.debug("ReadDevicePartnerships: provider ID is %d", id)

							pship.devicesyncitems.append(id)
				else:

					self.logger.debug("ReadDevicePartnerships: Found dangling sync source: GUID = %s, Hostname = %s, Description = %s",
					                  guid, hostname, description)
						  
					dangling_entries.append((guid, hostname, description))
					
					
			elif storetype == PSHMGR_STORETYPE_EXCH:
				
					pos=3;
				
					self.logger.debug("ReadDevicePartnerships: Found server sync source: GUID = %s, Hostname = %s, Description = %s",
					                  guid, hostname, description)
									
					self.DevicePartnerships[pos-1] = Partnership(self.engine.config, pos, 0, guid, hostname, description, storetype, self.engine.deviceName,self.engine.rapi_session)

			else:
				
				self.logger.debug("ReadDevicePartnerships: Found partnership with unknown storetype: GUID = %s, Hostname = %s, Description = %s",
					                  guid, hostname, description)
				self.logger.debug("ReadDevicePartnerships: - this will be ignored");

		# get rid of registry entries without a corresponding characteristic
	
		for entry in reg_entries.values():
			self.logger.info("ReadDevicePartnerships: Deleting dangling registry entry: %s", entry)
			hklm.delete_sub_key(r"Software\Microsoft\Windows CE Services\Partners\P%d" % entry[0])
		
		# Delete dangling entries that claim to be AS but do not have a registry key
		
		for entry in dangling_entries:
			self.logger.info("ReadDevicePartnerships: Deleting dangling sync source: %s", entry)
			self.engine.rapi_session.RemoveConfig("Sync.Sources", entry[0])


	######################################
	# EXPORTED FUNCTIONS
	######################################

	#
	# ClearDevicePartnerships
	#
	# Remove all recorded device partnerships.If we have a current
	# bound partnership, save it before deletion
	#
	
	def ClearDevicePartnerships(self):
		
		self.logger.info("ClearDevicePartnerships: clearing all device partnership info")

		if self.current != None:
			self.current.SaveItemDB()
		self.current = None
		del self.DevicePartnerships
		self.DevicePartnerships = [ None, None, None]

	#
	# GetDevicePartnerships (formerly get_list)
	#
	# Return a list of current device partnerships
	#

	def GetList(self):
		
		pships = []
		for pship in self.DevicePartnerships:
			if pship != None:
				pships.append(pship)
		return pships
	
	#
	# GetHostBindings
	#
	# Returns an array of PSInfo structures associated with each
	# host binding.
	
	def GetHostBindings(self):
		
		self.logger.info("GetHostBindings: reading existing host bindings")
		
		psdir = os.path.join(self.engine.config.config_user_dir,"partnerships")
		bindings = []
		
		# get all items in the dir.
		
		entries = os.listdir(psdir)
		
		for d in entries:
			
			# is it a file? - shouldn't be here so
			# ignore it.
			
			fullPath = os.path.join(psdir,d)
			if not os.path.isdir(fullPath):
				continue
			
			# it is a dir. Look for a psinfo.dat file
			
			infofp = os.path.join(fullPath,"psinfo.dat")
			
			# no psinfo, just ignore it
			
			if not os.path.isfile(infofp):
				continue
			
			# otherwise attempt to load the psinfo file
		
			try:
				f = open(infofp,"r")
				info = pickle.load(f)
				f.close()
			except Exception, e:
				self.logger.info("GetHostBindings: corrupted binding found at %s (%s), ignoring" % (infofp,e))
				continue
			
			bindings.append(info)
			
		return bindings
	
	#
	# QueryBindingConfiguration
	#
	# Returns an XML string containing the configuration block of a host binding
	
	def QueryBindingConfiguration(self,id,guid):
		
		# we need to load the config entries for a partnership from its
		# bindings
		
		psdir = os.path.join(self.engine.config.config_user_dir,"partnerships")
		pspath = os.path.join(psdir,"PS" + "-" + str(id) + "-" + str(guid))
		
		s = ""
		if os.path.isdir(pspath):
			conffile = os.path.join(pspath,"psconfig.xml")
			try:
				f=open(conffile,"r")
				s=f.read()
				f.close()
			except Exception,e:
				self.logger.info("QueryBindingConfiguration: unable to read configuration for binding %s" % pspath)
				raise Exception("corrupt binding")
		else:
			raise Exception("Binding has no configuration")
		
		return s

	#
	# SetBindingConfiguration
	#
	# Returns an XML string containing the configuration block of a host binding
	
	def SetBindingConfiguration(self,id,guid,config):
		
		# first check that we can parse the config!
		
		try:
			node = libxml2.parseDoc(config)
		except:
			self.logger.error("SetBindingConfiguration: bad XML in configuration string")
			raise Exception("bad XML in configuration string")
		
		# we need to load the config entries for a partnership from its
		# bindings
		
		psdir = os.path.join(self.engine.config.config_user_dir,"partnerships")
		pspath = os.path.join(psdir,"PS" + "-" + str(id) + "-" + str(guid))

		if os.path.isdir(pspath):
			conffile = os.path.join(pspath,"psconfig.xml")
			if os.path.isfile(conffile):
				try:
					f=open(conffile,"wb")
					f.write(config)
					f.close()
				except Exception,e:
					self.logger.info("SetBindingConfiguration: unable to write configuration for binding %s" % pspath)
					raise
			else:
				raise Exception("corrupted binding")
		else:
			raise Exception("Binding has no configuration")


	#
	# GetDevicePartnershipByID (formerly get)
	#
	# Return a device partnership by ID, if it exists
	# in the slots

	def GetDevicePartnershipByID(self, id):
				
		for pship in self.DevicePartnerships:
			if pship != None and pship.info.id == id:
				return pship
		return None

	#
	# GetCurrentPartnership (formerly get_current)
	#
	# If a current bound partnership is present, return it
	#

	def GetCurrentPartnership(self):
		return self.current

	#
	# AttemptToBind (formerly part of set_current)
	#
	# This function will load all device partnerships and then attempt to find a binding with a 
	# host partnership. If a binding is found, set the current binding and return True. If binding
	# fails, then leave the current binding open and return false. Remove any pre-existing partnership
	# info before continuing
	
	def AttemptToBind(self):
		
		self.logger.info("AttemptToBind: Reading partnerships and looking for host binding")
		
		# clear up any mess left behind
		
		self.ClearDevicePartnerships()

		# Now read the device
		
		self._ReadDevicePartnerships()
				
		partners = self.engine.rapi_session.HKEY_LOCAL_MACHINE.create_sub_key(r"Software\Microsoft\Windows CE Services\Partners")
		
		# Iterate through our partnerships and try and find a binding
		
		for i in self.DevicePartnerships[0:2]:
			if i != None:
				if i.AttemptToBind():
					self.current = i
					break
				
		if self.current == None:
			self.logger.debug("AttemptToBind: No valid host bindings found for any device partnership")
			self.logger.debug("AttemptToBind: setting current partnership to None")
			partners.set_value("PCur", 0, pyrapi2.REG_DWORD)
			return False
		else:
			idx = self.DevicePartnerships.index(self.current)
			self.logger.debug("AttemptToBind: Valid host binding found for partnership in slot %d", idx + 1)
			partners.set_value("PCur", idx + 1, pyrapi2.REG_DWORD)
			
			return True

	#
	# CreateNewPartnership (formerly add)
	#
	# Allows us to create a brand new device-host binding if there is a free slot.
	# This call will also make the new binding current.
	#
	# Add a new partnership, make it current if we do not have one
	#

	def CreateNewPartnership(self, name, sync_items):

		# Basic argument checking

		if len(name) > 20:
			raise errors.InvalidArgument("name too long (20 chars max)")

		for item in sync_items:
			if not item in SYNC_ITEMS:
				raise errors.InvalidArgument("sync item identifier %d is invalid" % item)

		if not (None in self.DevicePartnerships[0:2]):
			raise errors.NoFreeSlots("all slots are currently full")

		slot = self.DevicePartnerships[0:2].index(None) + 1

		self.logger.debug("CreateNewPartnership: attempting to create new partnership in slot %d", slot)
		pship = Partnership(self.engine.config, 
		                    slot, 
				    util.generate_id(), util.generate_guid(), 
				    socket.gethostname(), name,
				    PSHMGR_STORETYPE_AS,
				    self.engine.deviceName,
				    self.engine.rapi_session)

		for item in sync_items:
			self.logger.debug("CreateNewPartnership: adding synchronization item %s", item)
			pship.devicesyncitems.append(item)

		# Create the synchronization config data source
	
		source = characteristics.Characteristic(pship.info.guid)
		source["Name"] = pship.info.name
		source["Server"] = pship.info.hostname

		# StoreType
	
		source["StoreType"] = str(PSHMGR_STORETYPE_AS)

		ASEngines = characteristics.Characteristic("Engines")
		source.add_child(ASEngines)

		ASEngine = characteristics.Characteristic(GUID_WM5_ACTIVESYNC_ENGINE)
		ASEngines.add_child(ASEngine)

		settings = characteristics.Characteristic("Settings")
		settings["User"] = "DEFAULT"
		settings["Domain"] = "DEFAULT"
		settings["Password"] = "DEFAULT"
		settings["SavePassword"] = "1"
		settings["UseSSL"] = "0"
		settings["ConflictResolution"] = "1"
		settings["URI"] = "Microsoft-Server-ActiveSync"
		ASEngine.add_child(settings)

		providers = characteristics.Characteristic("Providers")
		ASEngine.add_child(providers)

		for item_id, item_rec in SYNC_ITEMS.items():
			item_str, item_readonly = item_rec
			item_guid = SYNC_ITEM_ID_TO_GUID[item_id]
			item_enabled = (item_id in sync_items)

			provider = characteristics.Characteristic(item_guid)
			provider["Enabled"] = str(int(item_enabled))
			provider["ReadOnly"] = str(int(item_readonly))
			provider["Name"] = item_str
			providers.add_child(provider)

		self.logger.debug("CreateNewPartnership: setting synchronization data source \n%s", source)

		self.engine.rapi_session.SetConfig("Sync.Sources", source)

		# Update the registry

		self.logger.debug("add: updating device registry")
		hklm = self.engine.rapi_session.HKEY_LOCAL_MACHINE

		partners_key = hklm.create_sub_key(r"Software\Microsoft\Windows CE Services\Partners")
		partners_key.set_value("PCur", pship.slot)

		key = partners_key.create_sub_key("P%d" % slot)
		key.set_value("PId", pship.info.id)
		key.set_value("DataSourceID", pship.info.guid)
		key.set_value("PName", pship.info.hostname)
		key.close()

		partners_key.close()

		# Store it
	
		self.logger.debug("add: storing partnership in slot %d", pship.slot)
		self.DevicePartnerships[pship.slot-1] = pship

		if pship.CreateNewBinding():
			self.logger.info("CreateNewPartnership: successfully saved host binding")
			
			# now, if we already are bound, do not try to bind. Otherwise bind
			# this partnership
			
			if self.current == None:
				if pship.AttemptToBind():
					self.logger.info("CreateNewPartnership: bind complete")
					self.current = pship
				else:
					self.logger.debug("CreateNewPartnership: failed to bind to new partnership")
					raise Exception("unable to bind to new partnership")
			else:
				self.logger.info("CreateNewPartnership: not rebinding as binding exists")
		else:
			self.logger.debug("CreateNewPartnership: Unable to create binding for new partnership")
			raise Exception("unable to create binding - check config dir")
		
		return pship
	
	#
	# DeleteDevicePartnership (formerly delete)
	#
	# Delete a partnership on the device and on the host, taking care to remove the binding as well
	#

	def DeleteDevicePartnership(self,id,guid):


		match = False
		for pship in self.DevicePartnerships:
			if pship != None:
				if pship.info.guid==guid and pship.info.id == id:

					hklm = self.engine.rapi_session.HKEY_LOCAL_MACHINE

					if self.current == pship:
						self.logger.info("DeletePartnership: setting current partnership to 0")
						partners = hklm.create_sub_key(r"Software\Microsoft\Windows CE Services\Partners")
						partners.set_value("PCur", 0, pyrapi2.REG_DWORD)
						self.current = None
		
					if pship.storetype == PSHMGR_STORETYPE_AS:
						self.logger.info("DeletePartnership: Deleting registry entry: %s", pship.slot)
						hklm.delete_sub_key(r"Software\Microsoft\Windows CE Services\Partners\P%d" % pship.slot)

					self.logger.debug("DeletePartnership: removing partnership %s from device", pship)
					self.engine.rapi_session.RemoveConfig("Sync.Sources", pship.info.guid)

					self.logger.debug("DeletePartnership: cleaning up partnership")
					pship.DeleteBinding()
					self.DevicePartnerships[pship.slot-1] = None
					match=True
					break;

		if not match:
			raise errors.InvalidArgument("Invalid partnership %s" % pship)



###############################################################################
# PSInfo
#
# Class containing stored information about a partnership. This is designed 
# for ease of storage
#
###############################################################################

class PSInfo:
	
	def __init__(self):

		folder_items = ( (0, "Inbox", 2),
				 (0, "Drafts", 3),
				 (0, "Deleted Items", 4),
				 (0, "Sent Items", 5),
				 (0, "Outbox", 6),
				 (0, "Tasks", 7),
				 (0, "Calendar", 8),
				 (0, "Contacts", 9),
				 (0, "Notes", 10),
				 (0, "Journal", 11),
				 (0, "Junk E-mail", 12),
				)
		
		self.id   = None
		self.guid = None
		self.name = ""
		self.hostname = ""
		self.devicename = ""
		
		self.lastSyncItems = []
		
		self.folders = {}		# used by Airsync

		for item in folder_items:
			id = util.generate_guid()
			self.folders[id] = item
	
	#
	# isEqual
	#
	# Allows us to compare the core items of a partnership to determine if it is equal

	def isEqual(self, b):
		
		if self.id == b.id and self.guid == b.guid and self.name == b.name:
			return True
		else:
			return False
 	
	#
	# Debug dumps...
	#
	
	def __str__(self):
		return "id=%#x, guid=\"%s\", hostname=\"%s\", name=\"%s\"" % (self.id, self.guid, self.hostname,self.name)

	
###############################################################################
#
# Partnership
#
# This is an internal representation of a partnership. It may or may not
# be bound (a link set up between host and device). A partnership is latent
# if not bound, and can not be used in a sync.

class Partnership:
	
	def __init__(self, config, slot, id, guid, hostname, name, storetype, devicename, rapisession):
	
		self.logger = logging.getLogger("engine.partnerships.Partnership")

		# Partnership identification

		self.info            = PSInfo()
		self.info.id         = id
		self.info.guid       = guid
		self.info.name       = name
		self.info.hostname   = hostname
		self.info.devicename = devicename

		# is the host-device relationship established? 
		# is the itemDB loaded?

		self.isBound = False
		self.itemDBLoaded = False
		
		# store type
		
		self.storetype = storetype
		
		# configuration (established during bind)
		
		self.config = None
		self.xpc = None
		self.rapisession = rapisession

		# sync data
		
		self.slot = slot		# slot in which partnership is held?
		self.devicesyncitems = []	# formerly sync_items
		self.deviceitemdbs = {}		# item databases

		self.psdir       = os.path.join(config.config_user_dir,"partnerships")
		self.pshippath   = os.path.join(self.psdir, "PS" + "-" + str(self.info.id) + "-" + str(self.info.guid))
		self.configpath  = os.path.join(self.pshippath, "psconfig.xml")
		self.infopath    = os.path.join(self.pshippath, "psinfo.dat")
		self.idbpath     = os.path.join(self.pshippath, "IDB")
		
		# make sure we have a partnerships dir!
		
		if not os.path.isdir(self.psdir):
			try:
				os.makedirs(self.psdir)
			except:
				self.logger.error("Unable to create partnership repository in config")

	#
	# _RunPartnershipConfig
	#
	# Run the per-partnership configuration
	#
	
	def _RunPartnershipConfig(self):
		
		# 1. Do we need to sync the time to the PC?
		
		doSyncTime = self.QueryConfig("/syncpartner-config/General/SyncTimeToPc[position() = 1]","0")
		if doSyncTime == "1":
			self.logger.info("synchronization of time to host is enabled")
			self.rapisession.SyncTimeToPc()
		else:
			self.logger.info("synchronization of time to host is disabled")
			
		# 2. Set up DTPT
		
		allowDTPTMH = self.QueryConfig("/syncpartner-config/DTPT/EnableMultihoming[position() = 1]","0")
		if allowDTPTMH == "1":
			value = 1
			self.logger.info("DTPT multihoming is enabled")
		else:
			value = 0
			self.logger.info("DTPT multihoming is disabled")
			
		cticset = characteristics.Characteristic("Settings",None)
		cticset.params["AllowDTPTMultihoming"]=str(value)
		self.rapisession.SetConfig("Sync",cticset)
		
		# config done

	#
	# __str__
	#
	# Debug dumping
	#

	def __str__(self):
		
		str = ""
		for id in self.devicesyncitems:
			if str:
				str += ", "
			else:
				str = "[ "
			str += SYNC_ITEMS[id][0]
		str += " ]"

		return "P%d: id=%#x, guid=\"%s\", hostname=\"%s\", name=\"%s\", sync_items=%s" % \
			(self.slot, self.info.id, self.info.guid, self.info.hostname, self.info.name, str)


	####################################
	## INTERNAL FUNCTIONS
	####################################
	
	#
	# _CreateNewConfig
	#
	# Create a new config structure for the partnership
	#
	
	def _CreateNewConfig(self):
		
		self.config = libxml2.newDoc("1.0")
		confnode = self.config.newChild(None,"syncpartner-config",None)
		generalnode = confnode.newChild(None,"General",None)
		synctime = generalnode.newChild(None,"SyncTimeToPc", "0")
		filepath = generalnode.newChild(None,"LocalFilePath", self.info.name)
		dtptnode = confnode.newChild(None,"DTPT",None)
		dtptnode.newChild(None,"Enabled","1")
		dtptnode.newChild(None,"EnableMultihoming","0")
		
		if not self._SaveConfigFile():
			self.logger.warning("unable to save config info for partnership %s", str(self.id))
	
	#
	# _LoadConfigFile
	#
	# Load the configuration for a partnership when binding. If no file exists, create a 
	# default one.
	#

	def _LoadConfigFile(self):
	
		try:
			self.config = libxml2.parseFile(self.configpath)
		except:
			self.logger.info("load_config: No previous config file available, creating default")
			self._CreateNewConfig()
		
		self.xpc = self.config.xpathNewContext()

	#
	# _SaveConfigFile
	#
	# Attempts to save the config data as an XML file
	#

	def _SaveConfigFile(self):
		
		try:
			f = open(self.configpath,"wb")
			f.write(self.config.serialize("utf-8",0))
			f.close()
		except Exception, e:
			self.logger.warning("failed to save file for partnership configuration")
			return False
	
		return True

	#####################################
	## EXPORTED FUNCTIONS
	#####################################

	#
	# AttemptToBind
	#
	# This function takes an unbound partnership and looks to see if we have a previously
	# established host-device relationship. If one is found, the bind information is 
	# loaded from the config and the partnership initialized. At this point it is said
	# to be 'bound' and can be used to sync.
	#
	# If no host-device relationship can be found, the function will return
	#
	# If a corrupted binding is found on the host side, then the host side binding
	# is deleted and the function continues to search.
	
	
	def AttemptToBind(self):
		
		# Partnerships with a store-type of PSHMGR_STORETYPE_EXCH can not be
		# bound by sync-engine
		
		if self.storetype == PSHMGR_STORETYPE_EXCH:
			self.logger.info("unable to bind Exchange Server partnership %s" % hostname);
			return False
		
		# The path will have already been constructed by the initializer. Check
		# if it exists. If not, we have no binding.
		
		if not os.path.isdir(self.pshippath):
			return False
		
		# See if we can open the info file.
		
		try:
			f=open(self.infopath,"r")
			bindinfo = pickle.load(f)
			f.close()
		except:
			self.logger.error("corrupt/nonexistent partnership info file at %s - deleting directory" % self.infopath)
			util.deltree(self.pshippath)
			return False

		
		# we have the info file. Check it!
		
		if self.info.isEqual(bindinfo):
			
			# the binding looks good!. Load the config file
			
			self._LoadConfigFile()
			
			# Bind the partnership
			
			self.info = bindinfo
			
			# ItemDBs are loaded when needed. The tendency for Airsync to send 
			# everything down the wire when an item is deselected then subsequently
			# selected is handled in the itemDB

			self.info.lastSyncItems = self.devicesyncitems

			# Binding is now complete
			
			self.isBound = True
			
			# Now save out all our info
			
			try:
				f=open(self.infopath,"wb")
				pickle.dump(self.info, f, pickle.HIGHEST_PROTOCOL)
				f.close()
			except Exception, e:
				self.logger.error("AttemptToBind: Failed to save sync info: ", e)

			self.logger.info("AttemptToBind : bound partnership %s - running config" % str(self))

			# Now the partnership is bound, run the per-partnership config
			
			self._RunPartnershipConfig()

			return True

		else:
			# if we get here, the partnership was not ours, but the id/guid combination
			# has placed us in this path. There is something very wrong here, but we can
			# fix it by removing the broken host binding info and reporting a failed bind.
		
			util.deltree(self.pshippath)
			self.logger.warning("bind: broken partnership (%d) configuration info found on host - removing traces", self.info.id)
			
			return False

	#
	# CreateNewBinding
	#
	# Used when we create a new partnership. This creates and updates a new binding
	#
	
	def CreateNewBinding(self):
		
		if self.storetype != PSHMGR_STORETYPE_AS:
			self.logger.error("CreateNewBinding: Exchange Server partnerships can not be bound")
			return False
		
		# The path will have already been constructed by the initializer. Check
		# if it exists. If so, we must clean it up ready for the new binding. If not, 
		# create it.
		
		if os.path.isdir(self.pshippath):
			deltree(self.pshippath)
			
		try:
			os.makedirs(self.pshippath)
		except Exception,e:
			self.logger.error("CreateNewBinding: unable to create bind info directory (%s)",e)
			return False
					
		# Generate a config.
		
		self._CreateNewConfig()
		
		# save out our info
					
		try:
			f=open(self.infopath,"wb")
			pickle.dump(self.info, f, pickle.HIGHEST_PROTOCOL)
			f.close()
		except Exception, e:
			print "CreateNewBinding: Failed to save sync info: ", e
			return False
		
		# mark us as successfully bound.
		
		self.isBound = True
		
		return True

	#
	# DeleteBinding
	#
	# Remove a host binding for this partnership
	
	def DeleteBinding(self):
		
		if self.storetype != PSHMGR_STORETYPE_AS:
			self.logger.info("DeleteBinding: Exchange Server partnerships have no binding - doing nothing")
			return False
		
		self.logger.info("DeleteBinding: deleting host binding for partnership %s",self.info.name)
		
		if self.isBound:
			self.isBound=False
		
		util.deltree(self.pshippath)


	#
	# QueryIsBound
	#
	# Returns true or false depending on whether the partnership is currently bound
	
	def QueryIsBound(self):
		return self.isBound
	

	#
	# QueryItemDB
	#
	# If an itemDB is available for a specific sync item, then  return it
	
	def QueryItemDB(self, itemid):
		
		if self.deviceitemdbs.has_key[itemid]:
			return self.deviceitemdbs[itemid]
		else:
			return None

	#
	# QueryConfig
	#
	# Used to query the content of a config file entry
	#

	def QueryConfig(self,path,default):

		nodelist = self.xpc.xpathEval(path)
		for n in nodelist:
			if n.type == "element":
				if n.content != None:
					return n.content
				else:
					return default
		return default
	
	#
	# Sync state information is now retained in the individual itemDBs. These are demand-loaded
	#

	#
	# LoadItemDB
	#
	# Load the item database for the items we have

	def LoadItemDB(self):
		
		if self.isBound:
			for item in self.devicesyncitems:
				self.logger.info("loading itemDB for item %d" % item)
				itemfile = self.idbpath + str(item)
				try:
					f = open(itemfile,"r")
					self.deviceitemdbs[item] = pickle.load(f)
					f.close()
				except:
					# if we can't open an existing one, create a new one.
					self.logger.info("LoadItemDB: no DB available for item %d",item)
					self.deviceitemdbs[item] = syncdb.ItemDB(item)
					
			self.itemDBLoaded = True
			return True
					
		else:
			self.logger.error("LoadItemDB: Partnership is unbound")
			return False

	#
	# SaveItemDB
	#
	# Save the item database for the items we have

	def SaveItemDB(self):
		if self.isBound:
			for itemid in self.deviceitemdbs.keys():
				self.deviceitemdbs[itemid].Save(self.idbpath)
			return True
		else:
			
			self.logger.error("SaveItemDB: Partnership is unbound")
			return False

	#
	# FlushItemDB
	#
	# Clears the item database from memory between syncs

	def FlushItemDB(self):
		
		self.logger.info("FlushItemDB: Flushing all itemDBs")
		self.itemDBLoaded = False
		del self.deviceitemdbs
		self.deviceitemdbs = {}


