def test_fault_non_existing_folder_genrequest_invalidurl(self): """ Request a non existing folder, so we get a fitting fault """ config = get_config() if config.getboolean('fault_test', 'enabled'): comm = Communication(config.get('fault_test', 'url') + "1234") token = auth.authenticate(config.get('fault_test', 'url'), config.get('fault_test', 'account'), config.get('fault_test', 'preauthkey'), config.get('fault_test', 'account_by')) request = comm.gen_request(token=token) request.add_request( "GetFolderRequest", { "folder": { "path": config.get('fault_test', 'folder') } }, "urn:zimbraMail" ) # A 404 error should by raised as an exception. self.assertRaises( Exception, comm.send_request, request )
def test_genrequest_default(self): """ Create a request only using the Communication-object """ config = get_config() if config.getboolean("genrequest_test", "enabled"): # Run only if enabled comm = Communication(config.get("genrequest_test", "url")) token = authenticate(config.get("genrequest_test", "url"), config.get("genrequest_test", "account"), config.get("genrequest_test", "preauthkey")) self.assertNotEqual(token, None, "Cannot authenticate.") request = comm.gen_request(token=token) request.add_request("NoOpRequest", {}, "urn:zimbraMail") response = comm.send_request(request) if response.is_fault(): self.fail( "Reponse failed: (%s) %s" % (response.get_fault_code(), response.get_fault_message()))
def getDlData(name, url, token): comm = Communication(url) info_request = comm.gen_request(token=token) info_request.add_request('GetDistributionListRequest', {'dl': { '_content': name, 'by': 'name' }}, 'urn:zimbraAdmin') info_response = comm.send_request(info_request) data = {} data['members'] = [] if not info_response.is_fault(): zimbra_data = info_response.get_response( )['GetDistributionListResponse'] data['settings'] = dict.zimbra_to_python(zimbra_data['dl']['a'], key_attribute='n', content_attribute='_content') if 'dlm' in zimbra_data['dl']: for i in zimbra_data['dl']['dlm']: data['members'].append(i['_content']) data['id'] = zimbra_data['dl']['id'] data['name'] = zimbra_data['dl']['name'] return data else: print(info_response.get_fault_message()) sys.exit(1)
def test_fault_non_existing_folder_genrequest_xml(self): """ Request a non existing folder, so we get a fitting fault """ config = get_config() if config.getboolean('fault_test', 'enabled'): comm = Communication(config.get('fault_test', 'url')) token = auth.authenticate(config.get('fault_test', 'url'), config.get('fault_test', 'account'), config.get('fault_test', 'preauthkey'), config.get('fault_test', 'account_by')) request = comm.gen_request(request_type="xml", token=token) request.add_request( "GetFolderRequest", { "folder": { "path": config.get('fault_test', 'folder') } }, "urn:zimbraMail" ) response = comm.send_request(request) self.check_response( response )
def __init__(self, server_host, server_port, *args, **kwargs): loc = 'https://%s:%s/%s' % (server_host, server_port, self.LOCATION) self.com = Communication(loc) self._server_host = server_host self._server_port = server_port self._session = ZimbraAPISession(self)
def request(self, name, data, urn): comm = Communication(self.url) account_request = RequestXml() account_request.set_auth_token(self.getToken()) account_request.add_request(name, data, urn) account_response = ResponseXml() comm.send_request(account_request, account_response) return account_response.get_response()
class ZimbraAbstractClient(object): """ Factorized abstract code for SOAP API access. Provides common ground for zimbraAdmin and zimbraAccount. """ def __init__(self, server_host, server_port, *args, **kwargs): loc = 'https://%s:%s/%s' % (server_host, server_port, self.LOCATION) self.com = Communication(loc) self._server_host = server_host self._server_port = server_port self._session = ZimbraAPISession(self) def request(self, name, content={}, namespace=None): """ Do a SOAP request and returns the result. Simple wrapper arround pythonzimbra functions :param name: ex: 'Auth' for performing an 'AuthRequest' :param content: a dict formatted pythonzimbra-style for request :param namespace: (optional), the namespace, if different from the client's :returns: a dict with response """ if not namespace: namespace = self.NAMESPACE req_name = name+'Request' resp_name = name+'Response' req = auth_request = pythonzimbra.request_xml.RequestXml() resp = pythonzimbra.response_xml.ResponseXml() if self._session.is_logged_in(): req.set_auth_token(self._session.authToken) req.add_request(req_name, content, namespace) try: self.com.send_request(req, resp) except urllib2.HTTPError, e: if resp : raise ZimbraSoapServerError(e.req, e.resp) else: raise try: resp_content = resp.get_response() return resp_content[resp_name] except KeyError: if resp_content.has_key('Fault'): raise ZimbraSoapServerError(req, resp) raise ZimbraSoapUnexpectedResponse( req, resp, 'Cannot find {} in response "{}"'.format( resp_name, resp.get_response())) return resp_content
def run_request(self, creds, req, req_args={}): token = self.get_token( creds['url'], creds['user'], creds['pwd'], creds['admin_auth']) comm = Communication(creds['url']) zmreq = comm.gen_request(token=token, request_type="xml") zmreq.add_request(req, req_args, creds['urn']) resp = comm.send_request(zmreq) if resp.is_fault(): print(resp.get_fault_code()) ret = resp.get_response() return json.loads(ret['response']['content'])
def connect(self): url = os.environ['ZIMBRA_URL'] account = os.environ['ZIMBRA_ACCOUNT'] password = os.environ['ZIMBRA_PASSWORD'] try: self.comm = Communication(url) self.token = auth.authenticate(url, account, password, admin_auth=True) except: log.exception("locals=%s", locals()) if not self.token: raise Exception("Authentication failed: locals=%s", locals())
def connect(self): try: self.comm = Communication(self.url) self.__token = auth.authenticate(self.url, self.usr, self.__password, admin_auth=True) self.request = self.comm.gen_request(token=self.__token, set_batch=True) return 'Conectado' except urllib.error.URLError as E: # catastrophic error. bail. return f'Erro na conexão: {E}'
def test_autoresponse_xml(self): """ Create an XML-request and pass this to send_request expection a xml response. """ config = get_config() if config.getboolean("autoresponse_test", "enabled"): # Run only if enabled token = authenticate( config.get("autoresponse_test", "url"), config.get("autoresponse_test", "account"), config.get("autoresponse_test", "preauthkey") ) self.assertNotEqual( token, None, "Cannot authenticate." ) request = RequestXml() request.set_auth_token(token) request.add_request( "NoOpRequest", { }, "urn:zimbraMail" ) comm = Communication(config.get("autoresponse_test", "url")) response = comm.send_request(request) if response.is_fault(): self.fail( "Reponse failed: (%s) %s" % ( response.get_fault_code(), response.get_fault_message() ) ) self.assertEqual( response.response_type, "xml", "Invalid response type %s" % response.response_type )
def test_genrequest_xml(self): """ Create a request only using the Communication-object """ config = get_config() if config.getboolean("genrequest_test", "enabled"): # Run only if enabled comm = Communication(config.get("genrequest_test", "url")) token = authenticate( config.get("genrequest_test", "url"), config.get("genrequest_test", "account"), config.get("genrequest_test", "preauthkey") ) self.assertNotEqual( token, None, "Cannot authenticate." ) request = comm.gen_request(request_type="xml", token=token) request.add_request( "NoOpRequest", { }, "urn:zimbraMail" ) response = comm.send_request(request) if response.is_fault(): self.fail( "Reponse failed: (%s) %s" % ( response.get_fault_code(), response.get_fault_message() ) ) self.assertEqual( response.response_type, "xml", "Invalid response type %s" % response.response_type )
def test_genrequest_fail(self): """ Create a request only using the Communication-object """ config = get_config() if config.getboolean("genrequest_test", "enabled"): # Run only if enabled comm = Communication(config.get("genrequest_test", "url")) token = authenticate( config.get("genrequest_test", "url"), config.get("genrequest_test", "account"), config.get("genrequest_test", "preauthkey") ) self.assertNotEqual( token, None, "Cannot authenticate." ) self.assertRaises( UnknownRequestType, comm.gen_request, request_type="INVALID", token=token ) request = comm.gen_request(token=token) request.add_request( "NoOpRequest", { }, "urn:zimbraMail" ) # Deliberately break the request request.request_type = "INVALID" self.assertRaises( UnknownRequestType, comm.send_request, request )
def test_genrequest_check_response_xml(self): """ Create a request only using the Communication-object, send it and check the response """ config = get_config() if config.getboolean("genrequest_test", "enabled"): # Run only if enabled comm = Communication(config.get("genrequest_test", "url")) token = authenticate( config.get("genrequest_test", "url"), config.get("genrequest_test", "account"), config.get("genrequest_test", "preauthkey") ) self.assertNotEqual( token, None, "Cannot authenticate." ) request = comm.gen_request(request_type="xml", token=token) request.add_request( "GetInfoRequest", { }, "urn:zimbraAccount" ) response = comm.send_request(request) if response.is_fault(): self.fail( "Reponse failed: (%s) %s" % ( response.get_fault_code(), response.get_fault_message() ) ) self.assertEqual( response.get_response()["GetInfoResponse"]["name"], config.get("genrequest_test", "account"), "Request returned unexpected response" )
def test_fault_non_existing_folder_batch_json(self): """ Request a non existing folder multiple times to get multiple faults """ config = get_config() if config.getboolean('fault_test', 'enabled'): comm = Communication(config.get('fault_test', 'url')) token = auth.authenticate(config.get('fault_test', 'url'), config.get('fault_test', 'account'), config.get('fault_test', 'preauthkey'), config.get('fault_test', 'account_by')) request = RequestJson() request.set_auth_token(token) request.enable_batch() request.add_request( "GetFolderRequest", { "folder": { "path": config.get('fault_test', 'folder') } }, "urn:zimbraMail" ) request.add_request( "GetFolderRequest", { "folder": { "path": config.get('fault_test', 'folder') } }, "urn:zimbraMail" ) response = ResponseJson() comm.send_request(request, response) self.check_response( response )
def __init__(self, admin_url, admin_user, admin_pass): self.admin_url = admin_url self.admin_user = admin_user self.admin_pass = admin_pass self.admin_account_by = 'name' self.request = None self.token = authenticate(self.admin_url, self.admin_user, self.admin_pass, self.admin_account_by, admin_auth=True, request_type="json") self.comm = Communication(self.admin_url)
def delMembers(dlId, member, url, token): if options.verbose: print('deleting member: ', member) comm = Communication(url) info_request = comm.gen_request(token=token) info_request.add_request('RemoveDistributionListMemberRequest', { 'id': dlId, 'dlm': { '_content': member } }, 'urn:zimbraAdmin') info_response = comm.send_request(info_request) if not info_response.is_fault(): zimbra_data = info_response.get_response( )['RemoveDistributionListMemberResponse'] else: print(info_response.get_fault_message()) sys.exit(1)
def __init__( self , cfg ): """ Initialise l'accès à l'API en lisant les paramètres de connexion dans la configuration et en créant l'instance de communication. """ self.url_ = cfg.get( 'bss' , 'zimbra-url' , 'https://webmail.partage.renater.fr/service/soap' ) self.domain_ = cfg.get( 'bss' , 'domain' ) self.dkey_ = cfg.get( 'bss' , 'token' ) self.fake_it_ = cfg.has_flag( 'bss' , 'simulate' ) self.user_ = None self.token_ = None # On lit le timeout depuis la configuration, s'il est défini. Sinon, # on utilise 10s par défaut. timeout_cfg = cfg.get( 'bss' , 'zimbra-timeout' , '10' ) try: timeout_cfg = int( timeout_cfg ) except ValueError as e: Logging( 'zimbra' ).error( 'Paramètre zimbra-timeout invalide' ) raise FatalError( 'Erreur de configuration' , e ) from pythonzimbra.communication import Communication self.comm_ = Communication( self.url_ , timeout = timeout_cfg )
def __init__(self, admin_url, admin_user, admin_pass): self.admin_url = admin_url self.admin_user = admin_user self.admin_pass = admin_pass self.admin_account_by = 'name' self.request = None self.token = authenticate( self.admin_url, self.admin_user, self.admin_pass, self.admin_account_by, admin_auth=True, request_type="json" ) self.comm = Communication(self.admin_url)
class ZimbraAbstractClient(object): """ Factorized abstract code for SOAP API access. Provides common ground for zimbraAdmin, zimbraAccount and zimbraMail. """ def __init__(self, server_host, server_port, *args, **kwargs): loc = 'https://%s:%s/%s' % (server_host, server_port, self.LOCATION) self.com = Communication(loc) self._server_host = server_host self._server_port = server_port self._session = ZimbraAPISession(self) def request(self, name, content={}, namespace=None): """ Do a SOAP request and returns the result. Simple wrapper arround pythonzimbra functions :param name: ex: 'Auth' for performing an 'AuthRequest' :param content: a dict formatted pythonzimbra-style for request :param namespace: (optional), the namespace, if different from the client's :returns: a dict with response """ if not namespace: namespace = self.NAMESPACE req_name = name+'Request' resp_name = name+'Response' req = pythonzimbra.request_xml.RequestXml() resp = pythonzimbra.response_xml.ResponseXml() if self._session.is_logged_in(): req.set_auth_token(self._session.authToken) req.add_request(req_name, content, namespace) try: self.com.send_request(req, resp) except HTTPError as e: if resp: raise ZimbraSoapServerError(e.req, e.resp) else: raise try: resp_content = resp.get_response() return resp_content[resp_name] except KeyError: if 'Fault' in resp_content: raise ZimbraSoapServerError(req, resp) raise ZimbraSoapUnexpectedResponse( req, resp, 'Cannot find {} in response "{}"'.format( resp_name, resp.get_response())) return resp_content def request_single(self, name, content={}): """ Simple wrapper arround request to extract a single response :returns: the first tag in the response body """ resp = self.request(name, content) # We stop on the first non-attribute (attributes are unicode/str) # If it's a list, we only return the first one. for i in resp.values(): if type(i) == list: return i[0] elif type(i) == dict: return i return None def request_list(self, name, content={}): """ Simple wrapper arround request to extract a list of response :returns: the list of tags with same name or empty list """ resp = self.request(name, content) # We stop on the first non-attribute (attributes are unicode/str) # If it's a list, we only return the first one. for i in resp.values(): if type(i) == list: return i elif type(i) == dict: return [i] return [] def login(self, user, password): self._session.login(user, password) def login_with_authToken(self, authToken, lifetime=None): self._session.import_session(authToken) if lifetime: self._session.set_end_date(int(lifetime)) def get_logged_in_by(self, login, parent_zc, duration=0): """Use another client to get logged in via preauth mechanism by an already logged in admin. It required the domain of the admin user to have preAuthKey The preauth key cannot be created by API, do it with zmprov : zmprov gdpak <domain> """ domain_name = zobjects.Account(name=login).get_domain() preauth_key = parent_zc.get_domain(domain_name)['zimbraPreAuthKey'] rc = self.REST_PREAUTH( self._server_host, parent_zc._server_port, preauth_key=preauth_key) authToken = rc.get_preauth_token(login) self.login_with_authToken(authToken) def delegated_login(self, login, admin_zc, duration=0): """Use another client to get logged in via delegated_auth mechanism by an already logged in admin. :param login: the user login (or email) you want to log as :param admin_zc: An already logged-in admin client :type admin_zc: ZimbraAdminClient """ # a duration of zero is interpretted literaly by the API... selector = zobjects.Account(name=login).to_selector() delegate_args = {'account': selector} if duration: delegate_args['duration': duration] resp = admin_zc.request('DelegateAuth', delegate_args) lifetime = resp['lifetime'] authToken = resp['authToken'] self.login_account = login self.login_with_authToken(authToken, lifetime) def is_session_valid(self): # some classes may need to overload it return self._session.is_session_valid() def get_host(self): return self._server_host
# Calculate point in times for appointment expansion expand_start = datetime.datetime.now() - datetime.timedelta(options.start) expand_end = datetime.datetime.now() + datetime.timedelta(options.end) # Calculate the epoch-timestamps for Zimbra (in milliseconds) expand_start_epoch = calendar.timegm(expand_start.utctimetuple()) * 1000 expand_end_epoch = calendar.timegm(expand_end.utctimetuple()) * 1000 # Build up Zimbra communication url = "https://%s:7071/service/admin/soap" % server_name user_url = "https://%s/service/soap" % server_name comm = Communication(url) user_comm = Communication(user_url) token = auth.authenticate(url, admin_account, admin_password, admin_auth=True) if token is None: logging.error("Cannot login into zimbra with the supplied credentials.") exit(1) # Search for groupcal dictionaries search_request = comm.gen_request(token=token)
def authenticate(url, account, key, by='name', expires=0, timestamp=None, timeout=None, request_type="xml", admin_auth=False, use_password=False, raise_on_error=False): """ Authenticate to the Zimbra server :param url: URL of Zimbra SOAP service :param account: The account to be authenticated against :param key: The preauth key of the domain of the account or a password (if admin_auth or use_password is True) :param by: If the account is specified as a name, an ID or a ForeignPrincipal :param expires: When the token expires (or 0 for default expiration) :param timestamp: When the token was requested (None for "now") :param timeout: Timeout for the communication with the server. Defaults to the urllib2-default :param request_type: Which type of request to use ("xml" (default) or "json") :param admin_auth: This request should authenticate and generate an admin token. The "key"-parameter therefore holds the admin password (implies use_password) :param use_password: The "key"-parameter holds a password. Do a password- based user authentication. :param raise_on_error: Should I raise an exception when an authentication error occurs or just return None? :return: The authentication token or None :rtype: str or None or unicode """ if timestamp is None: timestamp = int(time.time()) * 1000 pak = "" if not admin_auth: pak = preauth.create_preauth(account, key, by, expires, timestamp) if request_type == 'xml': auth_request = RequestXml() else: auth_request = RequestJson() request_data = {'account': {'by': by, '_content': account}} ns = "urn:zimbraAccount" if admin_auth: ns = "urn:zimbraAdmin" request_data['password'] = key elif use_password: request_data['password'] = {"_content": key} else: request_data['preauth'] = { 'timestamp': timestamp, 'expires': expires, '_content': pak } auth_request.add_request('AuthRequest', request_data, ns) server = Communication(url, timeout) if request_type == 'xml': response = ResponseXml() else: response = ResponseJson() server.send_request(auth_request, response) if response.is_fault(): if raise_on_error: raise AuthenticationFailed( "Cannot authenticate user: (%s) %s" % (response.get_fault_code(), response.get_fault_message())) return None return response.get_response()['AuthResponse']['authToken']['_content']
help='webmail URL', required=True) parser.add_argument('-S', '--send-mail', dest='send_mail', nargs='+', metavar=('email', 'smtp-server'), help='Add this if you want to send output by email') argslist = parser.parse_args() url = "https://" + argslist.hostname + ":7071/service/admin/soap" admin = argslist.admin password = argslist.password domain = argslist.domain comm = Communication(url) sendm = argslist.send_mail def multipleReplace(text, wordDict): for key in wordDict: text = text.replace(key, wordDict[key]) return text def zimbraAttributes(): return [ 'displayName', 'description', 'zimbraAccountStatus', 'zimbraCOSId', 'zimbraLastLogonTimestamp' ]
class ZimbraRequest(object): def __init__(self, admin_url, admin_user, admin_pass): self.admin_url = admin_url self.admin_user = admin_user self.admin_pass = admin_pass self.admin_account_by = 'name' self.request = None self.token = authenticate( self.admin_url, self.admin_user, self.admin_pass, self.admin_account_by, admin_auth=True, request_type="json" ) self.comm = Communication(self.admin_url) def getAllAdminAccounts(self, domain_name): self.cleanUp() self.request.add_request( request_name = 'GetAllAccountsRequest', request_dict = { "domain": { "_content" : domain_name, "by": "name", }, "a" : { "n" : "zimbraIsAdminAccount", "_content" : "TRUE" } }, namespace = 'urn:zimbraAdmin' ) response = self.comm.send_request(self.request) if response.is_fault(): raise ZimbraRequestError( message = 'Error getting all admin accounts. Error: %s' % response.get_fault_message(), response = response ) return response.get_response() def createDomain(self, domain_name, attrs=list()): """ Create a new domain :param domain_name: The name of the domain :param attrs: List of tuple attributes of domain (zmprov desc domain) """ if not type(attrs) == list: raise TypeError('attrs must be a list') self.cleanUp() request_attrs = [] for attr in attrs: zattr, value = attr request_attrs.append({ 'n' : zattr, '_content' : value }) request_dict = { 'name' : domain_name } if request_attrs: request_dict['a'] = request_attrs self.request.add_request( request_name = 'CreateDomainRequest', request_dict = request_dict, namespace = 'urn:zimbraAdmin' ) response = self.comm.send_request(self.request) if response.is_fault(): raise ZimbraRequestError( message = 'Error creating domain %s Error: %s' % (domain_name, response.get_fault_message()), response = response ) return response.get_response() def deleteDomain(self, domain_id): """ Delete a domain :param domain_id: The zimbraId of the domain """ self.cleanUp() self.request.add_request( request_name = 'DeleteDomainRequest', request_dict = { 'id' : domain_id }, namespace = 'urn:zimbraAdmin' ) response = self.comm.send_request(self.request) if response.is_fault(): raise ZimbraRequestError( message = 'Error deleting domain %s Error: %s' % (domain_id, response.get_fault_message()), response = response ) return response.get_response() def getDomain(self, domain, attrs): """ TODO - Implement list of attributes to get. - Raise exception when an error occours. """ """ Returns the attributes requested in a json format. "GetDomainResponse": { "domain": { "a": [ { "_content": "externalLdapAutoComplete", "n": "zimbraGalAutoCompleteLdapFilter" }, { "_content": "FALSE", "n": "zimbraAdminConsoleDNSCheckEnabled" }, . . . https://files.zimbra.com/docs/soap_api/8.6.0/api-reference/zimbraAdmin/GetDomain.html """ if not type(attrs) == list: raise TypeError('attrs must be a list') self.cleanUp() attrs = ','.join(attrs) self.request.add_request( request_name = "GetDomainRequest", request_dict = { "attrs" : attrs, "domain": { "_content" : domain, "by": "name", }, }, namespace = "urn:zimbraAdmin" ) response = self.comm.send_request(self.request) if response.is_fault(): raise ZimbraRequestError( message = 'Error getting domain %s Error: %s' % (domain, response.get_fault_message()), response = response ) return response.get_response() def modifyDomain(self, domain_id, attrs): """ Modify an attribute of a domain This method is idempotent, it will not change the result executing multiple times :param domain_id: The zimbraID of the domain :param attrs: A list of tuple containing the zimbra attribute with the corresponding value: [(zattr, value), ...] """ self.cleanUp() request_attrs = [] for attr in attrs: zattr, value = attr request_attrs.append({ 'n' : zattr, '_content' : value }) self.request.add_request( request_name = "ModifyDomainRequest", request_dict = { "id": { "_content": domain_id, }, "a": request_attrs }, namespace = "urn:zimbraAdmin" ) response = self.comm.send_request(self.request) if response.is_fault(): raise ZimbraRequestError( message = 'Error modifying domain %s Error: %s' % (domain_id, response.get_fault_message()), response = response ) return response.get_response() def getAccount(self, account, attrs): """ TODO - Implement a list of attributes to get. - Implement raise exception. """ """ Returns a json containing all attributes of an account or an exception: "GetAccountResponse": { "account": { "a": [ { "_content": "FALSE", "n": "zimbraPrefCalendarReminderMobile" }, { "_content": "TRUE", "n": "zimbraPrefIMLogChats" }, . . . https://files.zimbra.com/docs/soap_api/8.6.0/api-reference/zimbraAdmin/GetAccount.html """ if not type(attrs) == list: raise TypeError('attrs must be a list') attrs = ','.join(attrs) self.cleanUp() self.request.add_request( "GetAccountRequest", { "attrs" : attrs, "account": { "_content": account, "by": "name", }, }, "urn:zimbraAdmin" ) response = self.comm.send_request(self.request) if response.is_fault(): raise ZimbraRequestError("Reponse failed: (%s) %s" % (response.get_fault_code(), response.get_fault_message())) return(response.get_response()) def createAccount(self, account, password=None, attrs=list()): """ Create a new account into Zimbra system :param account: The target account :param password: The given for the account :param attrs: A list of tuple containing the zimbra attribute with the corresponding value: [(zattr, value), ...] """ request_attrs = [] for attr in attrs: zattr, value = attr request_attrs.append({ 'n' : zattr, '_content' : value }) self.cleanUp() if not password: password = hmac.new(str(uuid.uuid4), str(uuid.uuid4()), hashlib.md5).hexdigest() self.request.add_request( request_name = "CreateAccountRequest", request_dict = { "name" : account, "password" : password, "a" : request_attrs }, namespace = "urn:zimbraAdmin" ) response = self.comm.send_request(self.request) if response.is_fault(): raise ZimbraRequestError( message = 'Error creating account %s. Error: %s' % (account, response.get_fault_message()), response = response ) return response.get_response() def cleanUp(self): """ Clean up after one step to leave a dedicated result for the other test cases. """ self.setUp() def setUp(self): """ Setup everything required to make a request to zimbra server. """ self.request = RequestJson() self.request = self.comm.gen_request(token=self.token) def getDomainId(self, domain): """ Returns the zimbraId of a domain. Useful to modify a domain with ModifyDomainRequest for instance. domain_id = self.getDomainId(inova.net) equal: domain_id = '4af850c7-7e44-452e-ad25-c70fda58f9bf' """ self.cleanUp() self.request.add_request( "GetDomainInfoRequest", { "domain": { "_content": domain, "by": "name", }, }, "urn:zimbraAdmin" ) response = self.comm.send_request(self.request) if response.is_fault(): raise ZimbraRequestError("Reponse failed: (%s) %s" % (response.get_fault_code(), response.get_fault_message())) return response.get_response()['GetDomainInfoResponse']['domain']['id'] def getDomainQuotaUsage(self,domain): """ Returns quota usage of all users of a specific domain { "GetQuotaUsageResponse": { "searchTotal": 1294, "account": [ { "used": 0, "limit": 0, "name": "*****@*****.**", "id": "63b128d6-b7f2-466d-ac86-7b253e62a7ed" }, { "used": 28, "limit": 26843545600, "name": "*****@*****.**", "id": "5b4832d1-b642-4778-ab7d-3056ebcefada" }, . . . https://files.zimbra.com/docs/soap_api/8.6.0/api-reference/zimbraAdmin/GetQuotaUsage.html """ self.cleanUp() self.request.add_request( "GetQuotaUsageRequest", { "domain": domain, "allServers": "1", "sortBy": "percentUsed", "sortAscending": "1", }, "urn:zimbraAdmin" ) response = self.comm.send_request(self.request) if response.is_fault(): raise ZimbraRequestError("Reponse failed: (%s) %s" % (response.get_fault_code(), response.get_fault_message())) return(response.get_response()) def getCos(self, cos_name): """ Get COS by it's name :param cos_name: The name of the COS """ self.cleanUp() self.request.add_request( request_name = 'GetCosRequest', request_dict = { 'cos' : { 'by' : 'name', '_content' : cos_name } }, namespace = 'urn:zimbraAdmin' ) response = self.comm.send_request(self.request) if response.is_fault(): raise ZimbraRequestError( message = 'Error getting COS %s. Error: %s' % (cos_name, response.get_fault_message()), response = response ) return response.get_response() def createCos(self, cos_name, features=dict()): """ Create a new cos. :param cos_name: The name of the COS :param features: A dict representing the feature->value """ if type(features) is not dict: raise TypeError('Wrong type found for features, must be a dict.') features_req = [] for feature, value in features.items(): features_req.append({ 'n' : feature , '_content' : value }) self.cleanUp() self.request.add_request( request_name = 'CreateCosRequest', request_dict = { 'name' : { '_content' : cos_name }, 'a' : features_req }, namespace = 'urn:zimbraAdmin' ) response = self.comm.send_request(self.request) if response.is_fault(): raise ZimbraRequestError( message = 'Error creating COS %s Error: %s' % (cos_name, response.get_fault_message()), response = response ) return response.get_response() def modifyCos(self, zimbra_cos_id, features): """ Update a cos. :param zimbra_cos_id: The zimbraID of the COS :param features: A dict representing the feature->value """ if type(features) is not dict: raise TypeError('Wrong type found for features, must be a dict') features_req = [] for feature, value in features.items(): features_req.append({ 'n' : feature, '_content' : value }) self.cleanUp() self.request.add_request( request_name = 'ModifyCosRequest', request_dict = { 'id' : { '_content' : zimbra_cos_id }, 'a' : features_req }, namespace = 'urn:zimbraAdmin' ) response = self.comm.send_request(self.request) if response.is_fault(): raise ZimbraRequestError( message = 'Error creating COS %s Error: %s' % (cos_name, response.get_fault_message()), response = response ) return response.get_response() def deleteCos(self, zimbra_cos_id): """ Delete a specific COS :param zimbra_cos_id: The zimbraID of the COS """ self.cleanUp() self.request.add_request( request_name = 'DeleteCosRequest', request_dict = { 'id' : { '_content' : zimbra_cos_id } }, namespace = 'urn:zimbraAdmin' ) response = self.comm.send_request(self.request) if response.is_fault(): raise ZimbraRequestError( message = 'Error creating COS %s Error: %s' % (cos_name, response.get_fault_message()), response = response ) return response.get_response() def getComputeAggregateQuotaUsage(self): """ This method get all quota usage of all domains in a zimbra system. This may take a while depending how many domains and servers you have. Use wisely :P """ self.cleanUp() self.request.add_request( "ComputeAggregateQuotaUsageRequest", { }, "urn:zimbraAdmin" ) response = self.comm.send_request(self.request) if response.is_fault(): raise ZimbraRequestError("Reponse failed: (%s) %s" % (response.get_fault_code(), response.get_fault_message())) return(response.get_response()) def createDistributionList(self, dlist, attrs=list()): """ This method create zimbra distribution list. A list of attributes may be given, we will handle it for you. :param dlist: The target distribution list :param attrs: List of tuple attributes of distribution list """ if not type(attrs) == list: raise TypeError('attrs must be a list') request_attrs = [] for attr in attrs: zattr, value = attr # If it's a list, then it's a multi-value attribute if type(value) == list: for multi_attr in value: request_attrs.append({ 'n' : zattr, '_content' : multi_attr }) else: request_attrs.append({ 'n' : zattr, '_content' : value }) self.cleanUp() self.request.add_request( request_name = "CreateDistributionListRequest", request_dict = { "name": dlist, "a": request_attrs }, namespace = "urn:zimbraAdmin" ) response = self.comm.send_request(self.request) if response.is_fault(): raise ZimbraRequestError( message = 'Error creating DL %s Error: %s' % (dlist, response.get_fault_message()), response = response ) return response.get_response() def getDistributionList(self, dlist): """ Gets information about a distribution list. :param dlist: The target distribution list Obs: Tested with "a" attribute does not have effect on result """ self.cleanUp() self.request.add_request( request_name = "GetDistributionListRequest", request_dict = { "dl": { "_content": dlist, "by": "name", }, }, namespace = "urn:zimbraAdmin" ) response = self.comm.send_request(self.request) if response.is_fault(): raise ZimbraRequestError( message = 'Error getting DL: %s Error: %s' % (dlist, response.get_fault_message()), response = response ) return response.get_response() def deleteDistributionList(self, dlist_zimbra_id): """ Deletes distribution list :param dlist_zimbra_id: Distribution List zimbraID """ self.cleanUp() self.request.add_request( request_name = "DeleteDistributionListRequest", request_dict = { "id" : dlist_zimbra_id }, namespace = "urn:zimbraAdmin" ) response = self.comm.send_request(self.request) if response.is_fault(): raise ZimbraRequestError( message = 'Error deleting DL: %s Error: %s' % (dlist, response.get_fault_message()), response = response ) return response.get_response() def addDistributionListMember(self, dlist_zimbra_id, members): """ This method adds members to a zimbra distribution list. A list of members must be sent. This method is idempotent, it will not change the result executing multiple times :param dlist_zimbra_id: The target distribution list zimbraId :param members: List containing the account members """ if not type(members) == list: raise TypeError('members must be a list') zmembers = [] for member in members: zmembers.append({'_content': member}) self.cleanUp() self.request.add_request( request_name = "AddDistributionListMemberRequest", request_dict = { "id": dlist_zimbra_id, "dlm": zmembers }, namespace = "urn:zimbraAdmin" ) response = self.comm.send_request(self.request) if response.is_fault(): raise ZimbraRequestError( message = 'Error adding members to dlist %s Error: %s' % (dlist_zimbra_id, response.get_fault_message()), response = response ) return response.get_response() def grantRight(self, target_name, target_type, grantee_name, grantee_type, right, deny=0): """ Grant a right on a target to an individual or group grantee. This method is idempotent, it will not change the result executing multiple times :param target_name: The target for applying the right (by name). E.g.: 'inova.net'. External docs: /target :param target_type: The type of the target. E.g.: 'domain'. External docs: /target@type :param grantee_name: Grantee selector. E.g.: 'grp', 'dlist'. External docs: /grantee :param grantee_type: The type of the grantee. E.g.: 'grp', 'dlist'. External docs: /grantee@type :param right: The name of the right. E.g.: getDomainQuotaUsage, domainAdminConsoleRights. External docs: /right :param deny: Either to deny or grant the permission. Default is 0. External docs: /right@deny Ref. Docs: https://files.zimbra.com/docs/soap_api/8.6.0/api-reference/zimbraAdmin/GrantRight.html """ self.cleanUp() self.request.add_request( request_name = "GrantRightRequest", request_dict = { "target": { "type": target_type, "by": "name", "_content": target_name }, "grantee": { "type": grantee_type, "by": "name", "_content": grantee_name }, "right": { "_content": right, "deny": deny } }, namespace = "urn:zimbraAdmin" ) response = self.comm.send_request(self.request) if response.is_fault(): raise ZimbraRequestError( message = 'Error adding grant to target_name %s Error: %s' % (target_name, response.get_fault_message()), response = response ) return response.get_response()
def test_genrequest_check_response_batch_xml(self): """ Create a batch-request only using the Communication-object """ config = get_config() if config.getboolean("genrequest_test", "enabled"): # Run only if enabled comm = Communication(config.get("genrequest_test", "url")) token = authenticate( config.get("genrequest_test", "url"), config.get("genrequest_test", "account"), config.get("genrequest_test", "preauthkey") ) self.assertNotEqual( token, None, "Cannot authenticate." ) request = comm.gen_request( request_type="xml", token=token, set_batch=True ) self.assertEqual( type(request), RequestXml, "Generated request wasn't an json-request, which should be " "the default." ) request.add_request( "NoOpRequest", { }, "urn:zimbraMail" ) request.add_request( "GetInfoRequest", { }, "urn:zimbraAccount" ) response = comm.send_request(request) if response.is_fault(): self.fail( "Reponse failed: (%s) %s" % ( response.get_fault_code(), response.get_fault_message() ) ) self.assertEqual( response.get_response(2)["GetInfoResponse"]["name"], config.get("genrequest_test", "account"), "Request returned unexpected response" )
def run_admin_test(self, request_type): """ Actually do the work """ config = get_config() if config.getboolean("admin_request_test", "enabled"): # Run only if enabled token = authenticate( config.get("admin_request_test", "admin_url"), config.get("admin_request_test", "admin_account"), config.get("admin_request_test", "admin_password"), config.get("admin_request_test", "admin_account_by"), admin_auth=True, request_type=request_type ) if token is None: self.fail("Authentication with the configured settings " "was not successful") # Create an account comm = Communication(config.get("admin_request_test", "admin_url")) if request_type == "xml": request = RequestXml() else: request = RequestJson() request.set_auth_token(token) test_account = config.get("admin_request_test", "test_account") if "TEMP" in test_account: # Generate a random number and add it to the test account random.seed() temp_account = random.randint(1000000, 5000000) test_account = test_account.replace("TEMP", str(temp_account)) test_displayname = config.get( "admin_request_test", "test_displayname" ) if sys.version < '3': # Create unicode string for py2 test_displayname = test_displayname.decode("utf-8") request.add_request( "CreateAccountRequest", { "name": test_account, "password": config.get( "admin_request_test", "test_password" ), "a": { "n": "displayName", "_content": test_displayname } }, "urn:zimbraAdmin" ) if request_type == "xml": response = ResponseXml() else: response = ResponseJson() comm.send_request(request, response) if response.is_fault(): self.fail( "CreateAccount faulted. (%s) %s" % ( response.get_fault_code(), response.get_fault_message() ) ) account_id = response.get_response( )["CreateAccountResponse"]["account"]["id"] # Get account from database and compare display name to the setting request.clean() request.set_auth_token(token) response.clean() request.add_request( "GetAccountRequest", { "account": { "by": "name", "_content": test_account } }, "urn:zimbraAdmin" ) comm.send_request(request, response) if response.is_fault(): self.fail( "GetAccount faulted. (%s) %s" % ( response.get_fault_code(), response.get_fault_message() ) ) returned_name = get_value( response.get_response()["GetAccountResponse"]["account"]["a"], "displayName" ) self.assertEqual( returned_name, test_displayname, "Zimbra didn't save the display name as requested." ) # Try to log in as the new account user_token = authenticate( config.get("admin_request_test", "url"), test_account, config.get("admin_request_test", "test_password"), "name", request_type=request_type, use_password=True ) if user_token is None: self.fail("Cannot log in as the test user.") # Remove account request.clean() response.clean() request.set_auth_token(token) request.add_request( "DeleteAccountRequest", { "id": account_id }, "urn:zimbraAdmin" ) comm.send_request(request, response) if response.is_fault(): self.fail( "Cannot remove test account: (%s) %s" % ( response.get_fault_code(), response.get_fault_message() ) )
class Zimbra(): def __init__(self): self.comm = None self.token = None def connect(self): url = os.environ['ZIMBRA_URL'] account = os.environ['ZIMBRA_ACCOUNT'] password = os.environ['ZIMBRA_PASSWORD'] try: self.comm = Communication(url) self.token = auth.authenticate(url, account, password, admin_auth=True) except: log.exception("locals=%s", locals()) if not self.token: raise Exception("Authentication failed: locals=%s", locals()) def request(self, request_name, params=None, context=None, urn=None, opts={}): """ Send a single request to Zimbra; return the response as native python data. """ if 'xml' in opts and opts['xml']: request, response = RequestXml(), ResponseXml() else: request, response = RequestJson(), ResponseJson() try: if urn == None: urn = urn_for_request(request_name) request.add_request(request_name, params or {}, "urn:"+urn) if context: request.set_context_params(context) request.set_auth_token(self.token) except: log.exception("failed to build request: request_name=%s, params=%s, context=%s", request_name, params, context) return None if opts['debug']: if isinstance(request, RequestXml): print xml.dom.minidom.parseString(request.get_request()).toprettyxml(indent=". ") else: pprint.pprint(yaml.load(request.get_request())) try: self.comm.send_request(request, response) except urllib2.HTTPError as e: requestbody = request.get_request() responsebody = e.read() log.error('''send_request HTTP Error %s: %s; request="%s" response="%s"''', e.code, e.reason, requestbody, responsebody) if isinstance(request, RequestXml): requestdoc = xml.dom.minidom.parseString(requestbody) print "REQUEST=", requestdoc.toprettyxml(indent=". ") else: print "REQUEST=\n", pprint.pformat(yaml.load(requestbody)) if isinstance(response, ResponseXml): responsedoc = xml.dom.minidom.parseString(responsebody) print "RESPONSE=", responsedoc.toprettyxml(indent=". ") else: print "RESPONSE=\n", pprint.pformat(yaml.load(responsebody)) return None except Exception as e: log.exception("send_request failed (%s): request=%s", type(e), request.get_request()) return None if response.is_fault(): log.error("send_request returned fault: request=%s, response=%s", request.get_request(), response.get_response()) return None info = response.get_response() return info
def _get_devices(self, user): self.app.log.debug("Fetching devices of user %s" % user) (local_part, domain_part) = user.split("@") if domain_part not in self.preauth_cache: # No preauth key cached. Fetch one self.app.log.debug("Fetch preauth key for domain %s" % domain_part) comm = Communication(self.url) preauthkey_request = comm.gen_request(token=self.token) preauthkey_request.add_request( "GetDomainRequest", { "domain": { "by": "name", "_content": domain_part } }, "urn:zimbraAdmin" ) preauthkey_response = comm.send_request(preauthkey_request) if preauthkey_response.is_fault(): self.app.log.fatal( "Cannot fetch preauth key for domain %s" % domain_part, preauthkey_response.get_fault_code(), preauthkey_response.get_fault_message(), ) exit(1) preauth = get_value( preauthkey_response.get_response()["GetDomainResponse"][ "domain"]["a"], "zimbraPreAuthKey" ) if preauth is None: self.app.log.fatal( "Domain %s has no preauthkey. Please use zmprov gdpak " "<domain> first." % domain_part ) exit(1) self.preauth_cache[domain_part] = preauth else: preauth = self.preauth_cache[domain_part] user_token = auth.authenticate( self.user_url, user, preauth ) if user_token is None: self.app.log.fatal("Cannot login as user %s" % user) exit(1) user_comm = Communication(self.user_url) get_device_status_request = user_comm.gen_request(token=user_token) get_device_status_request.add_request( "GetDeviceStatusRequest", {}, "urn:zimbraSync" ) get_device_status_response = user_comm.send_request( get_device_status_request) if get_device_status_response.is_fault(): self.app.log.fatal( "Cannot fetch devices for user %s: (%s) %s" % ( user, get_device_status_response.get_fault_code(), get_device_status_response.get_fault_message() ) ) exit(1) devices = [] if "device" in get_device_status_response.get_response()[ "GetDeviceStatusResponse"]: devices = get_device_status_response.get_response()[ "GetDeviceStatusResponse"]["device"] if type(devices) == dict: devices = [devices] return devices
def _get_scope(self): """ Build up a list of users for the specified scope """ self.app.log.debug("Building up user list of scope definition") if self.app.pargs.scope == "": # Set the scope to the admin user's domain (local_part, domain_part) = self.app.pargs.user.split("@") scope_config = "DOMAIN=%s" % domain_part else: scope_config = self.app.pargs.scope (scope, scope_value) = scope_config.split("=") if scope not in ("DOMAIN", "LIST", "USER"): self.app.log.fatal("Scope not correctly configured. Please use " "DOMAIN, LIST or USER") exit(1) userlist = [] if scope == "DOMAIN": # Fetch all users in a domain self.app.log.debug( "Searching for accounts in domain %s" % scope_value ) comm = Communication(self.url) search_account_request = comm.gen_request(token=self.token) search_account_request.add_request( "SearchAccountsRequest", { "query": "", "domain": scope_value }, "urn:zimbraAdmin" ) search_account_response = comm.send_request(search_account_request) if search_account_response.is_fault(): self.app.log.fatal( "Cannot search for accounts in the specified domain %s: " "(%s) %s" % ( scope_value, search_account_response.get_fault_code(), search_account_response.get_fault_message() ) ) for account in search_account_response.get_response()[ "SearchAccountsResponse"]["account"]: userlist.append(account["name"]) elif scope == "LIST": # Fetch all users in a distribution list self.app.log.debug( "Searching for users in distribution list %s" % scope_value ) comm = Communication(self.url) get_distributionlist_request = comm.gen_request(token=self.token) get_distributionlist_request.add_request( "GetDistributionListRequest", { "dl": { "by": "name", "_content": scope_value } }, "urn:zimbraAdmin" ) get_distributionlist_response = comm.send_request( get_distributionlist_request ) if get_distributionlist_response.is_fault(): self.app.log.fatal( "Cannot search for accounts in the specified list %s: " "(%s) %s" % ( scope_value, get_distributionlist_response.get_fault_code(), get_distributionlist_response.get_fault_message() ) ) for member in get_distributionlist_response.get_response()[ "GetDistributionListResponse"]["dl"]["dlm"]: if type(member) == dict: userlist.append(member["_content"]) else: userlist.append(member) elif scope == "USER": # Just a single user userlist.append(scope_value) self.app.log.debug("Found these users:\n %s" % userlist) return userlist
def authenticate(url, account, key, by='name', expires=0, timestamp=None, timeout=None, request_type="xml", admin_auth=False, use_password=False, raise_on_error=False): """ Authenticate to the Zimbra server :param url: URL of Zimbra SOAP service :param account: The account to be authenticated against :param key: The preauth key of the domain of the account or a password (if admin_auth or use_password is True) :param by: If the account is specified as a name, an ID or a ForeignPrincipal :param expires: When the token expires (or 0 for default expiration) :param timestamp: When the token was requested (None for "now") :param timeout: Timeout for the communication with the server. Defaults to the urllib2-default :param request_type: Which type of request to use ("xml" (default) or "json") :param admin_auth: This request should authenticate and generate an admin token. The "key"-parameter therefore holds the admin password (implies use_password) :param use_password: The "key"-parameter holds a password. Do a password- based user authentication. :param raise_on_error: Should I raise an exception when an authentication error occurs or just return None? :return: The authentication token or None :rtype: str or None or unicode """ if timestamp is None: timestamp = int(time.time()) * 1000 pak = "" if not admin_auth: pak = preauth.create_preauth(account, key, by, expires, timestamp) if request_type == 'xml': auth_request = RequestXml() else: auth_request = RequestJson() request_data = { 'account': { 'by': by, '_content': account } } ns = "urn:zimbraAccount" if admin_auth: ns = "urn:zimbraAdmin" request_data['password'] = key elif use_password: request_data['password'] = { "_content": key } else: request_data['preauth'] = { 'timestamp': timestamp, 'expires': expires, '_content': pak } auth_request.add_request( 'AuthRequest', request_data, ns ) server = Communication(url, timeout) if request_type == 'xml': response = ResponseXml() else: response = ResponseJson() server.send_request(auth_request, response) if response.is_fault(): if raise_on_error: raise AuthenticationFailed( "Cannot authenticate user: (%s) %s" % ( response.get_fault_code(), response.get_fault_message() ) ) return None return response.get_response()['AuthResponse']['authToken']
def test_genrequest_batch_invalid_xml(self): """ Create a batchrequest only using the Communication-object, send it and request an invalid request id (xml) """ config = get_config() if config.getboolean("genrequest_test", "enabled"): # Run only if enabled comm = Communication(config.get("genrequest_test", "url")) token = authenticate( config.get("genrequest_test", "url"), config.get("genrequest_test", "account"), config.get("genrequest_test", "preauthkey") ) self.assertNotEqual( token, None, "Cannot authenticate." ) request = comm.gen_request( request_type="xml", token=token, set_batch=True ) self.assertEqual( type(request), RequestXml, "Generated request wasn't an json-request, which should be " "the default." ) request.add_request( "NoOpRequest", { }, "urn:zimbraMail" ) request.add_request( "NoOpRequest", { }, "urn:zimbraMail" ) response = comm.send_request(request) if response.is_fault(): self.fail( "Reponse failed: (%s) %s" % ( response.get_fault_code(), response.get_fault_message() ) ) self.assertIsNone( response.get_response(3), "Querying an invalid requestId didn't return None" )
if options.quiet: logging.basicConfig(level=logging.ERROR) elif options.debug: logging.basicConfig(level=logging.DEBUG) else: logging.basicConfig(level=logging.INFO) logging.debug("Starting followup-agent.") logging.debug("Authenticating as administrator to get users and domain " "preauth") server_url = "https://%s:7071/service/admin/soap" % server_name comm = Communication(server_url) token = auth.authenticate(server_url, admin_account, admin_password, admin_auth=True) users = [] if options.distlist: logging.debug("Retrieving distribution list members from list %s" % ( options.distlist )) get_members_request = comm.gen_request(token=token) get_members_request.add_request( "GetDistributionListRequest",
def run_admin_test(self, request_type): """ Actually do the work """ config = get_config() if config.getboolean("admin_request_test", "enabled"): # Run only if enabled token = authenticate( config.get("admin_request_test", "url"), config.get("admin_request_test", "account"), config.get("admin_request_test", "password"), config.get("admin_request_test", "account_by"), admin_auth=True, request_type=request_type ) if token is None: self.fail("Authentication with the configured settings " "was not successful") # Create an account comm = Communication(config.get("admin_request_test", "url")) if request_type == "xml": request = RequestXml() else: request = RequestJson() request.set_auth_token(token) request.add_request( "CreateAccountRequest", { "name": config.get("admin_request_test", "test_account"), "password": config.get( "admin_request_test", "test_password" ) }, "urn:zimbraAdmin" ) if request_type == "xml": response = ResponseXml() else: response = ResponseJson() comm.send_request(request, response) if response.is_fault(): self.fail( "CreateAccount faulted. %s" % (response.get_response()) ) account_id = response.get_response( )["CreateAccountResponse"]["account"]["id"] # Try to log in as the new account user_token = authenticate( config.get("admin_request_test", "user_url"), config.get("admin_request_test", "test_account"), config.get("admin_request_test", "test_password"), "name", request_type=request_type, use_password=True ) if user_token is None: self.fail("Cannot log in as the test user.") # Remove account request.clean() response.clean() request.set_auth_token(token) request.add_request( "DeleteAccountRequest", { "id": account_id }, "urn:zimbraAdmin" ) comm.send_request(request, response) if response.is_fault(): self.fail( "Cannot remove test account. %s" % response.get_response() )
def get_com(self): if self.comm is None: self.comm = Communication(self.url) return self.comm
class ZimbraRequest(object): def __init__(self, admin_url, admin_user, admin_pass): self.admin_url = admin_url self.admin_user = admin_user self.admin_pass = admin_pass self.admin_account_by = 'name' self.request = None self.token = authenticate(self.admin_url, self.admin_user, self.admin_pass, self.admin_account_by, admin_auth=True, request_type="json") self.comm = Communication(self.admin_url) def getAllAdminAccounts(self, domain_name): self.cleanUp() self.request.add_request(request_name='GetAllAccountsRequest', request_dict={ "domain": { "_content": domain_name, "by": "name", }, "a": { "n": "zimbraIsAdminAccount", "_content": "TRUE" } }, namespace='urn:zimbraAdmin') response = self.comm.send_request(self.request) if response.is_fault(): raise ZimbraRequestError( message='Error getting all admin accounts. Error: %s' % response.get_fault_message(), response=response) return response.get_response() def createDomain(self, domain_name, attrs=list()): """ Create a new domain :param domain_name: The name of the domain :param attrs: List of tuple attributes of domain (zmprov desc domain) """ if not type(attrs) == list: raise TypeError('attrs must be a list') self.cleanUp() request_attrs = [] for attr in attrs: zattr, value = attr request_attrs.append({'n': zattr, '_content': value}) request_dict = {'name': domain_name} if request_attrs: request_dict['a'] = request_attrs self.request.add_request(request_name='CreateDomainRequest', request_dict=request_dict, namespace='urn:zimbraAdmin') response = self.comm.send_request(self.request) if response.is_fault(): raise ZimbraRequestError( message='Error creating domain %s Error: %s' % (domain_name, response.get_fault_message()), response=response) return response.get_response() def deleteDomain(self, domain_id): """ Delete a domain :param domain_id: The zimbraId of the domain """ self.cleanUp() self.request.add_request(request_name='DeleteDomainRequest', request_dict={'id': domain_id}, namespace='urn:zimbraAdmin') response = self.comm.send_request(self.request) if response.is_fault(): raise ZimbraRequestError( message='Error deleting domain %s Error: %s' % (domain_id, response.get_fault_message()), response=response) return response.get_response() def getDomain(self, domain, attrs): """ TODO - Implement list of attributes to get. - Raise exception when an error occours. """ """ Returns the attributes requested in a json format. "GetDomainResponse": { "domain": { "a": [ { "_content": "externalLdapAutoComplete", "n": "zimbraGalAutoCompleteLdapFilter" }, { "_content": "FALSE", "n": "zimbraAdminConsoleDNSCheckEnabled" }, . . . https://files.zimbra.com/docs/soap_api/8.6.0/api-reference/zimbraAdmin/GetDomain.html """ if not type(attrs) == list: raise TypeError('attrs must be a list') self.cleanUp() attrs = ','.join(attrs) self.request.add_request(request_name="GetDomainRequest", request_dict={ "attrs": attrs, "domain": { "_content": domain, "by": "name", }, }, namespace="urn:zimbraAdmin") response = self.comm.send_request(self.request) if response.is_fault(): raise ZimbraRequestError( message='Error getting domain %s Error: %s' % (domain, response.get_fault_message()), response=response) return response.get_response() def modifyDomain(self, domain_id, attrs): """ Modify an attribute of a domain This method is idempotent, it will not change the result executing multiple times :param domain_id: The zimbraID of the domain :param attrs: A list of tuple containing the zimbra attribute with the corresponding value: [(zattr, value), ...] """ self.cleanUp() request_attrs = [] for attr in attrs: zattr, value = attr request_attrs.append({'n': zattr, '_content': value}) self.request.add_request(request_name="ModifyDomainRequest", request_dict={ "id": { "_content": domain_id, }, "a": request_attrs }, namespace="urn:zimbraAdmin") response = self.comm.send_request(self.request) if response.is_fault(): raise ZimbraRequestError( message='Error modifying domain %s Error: %s' % (domain_id, response.get_fault_message()), response=response) return response.get_response() def getAccount(self, account, attrs): """ TODO - Implement a list of attributes to get. - Implement raise exception. """ """ Returns a json containing all attributes of an account or an exception: "GetAccountResponse": { "account": { "a": [ { "_content": "FALSE", "n": "zimbraPrefCalendarReminderMobile" }, { "_content": "TRUE", "n": "zimbraPrefIMLogChats" }, . . . https://files.zimbra.com/docs/soap_api/8.6.0/api-reference/zimbraAdmin/GetAccount.html """ if not type(attrs) == list: raise TypeError('attrs must be a list') attrs = ','.join(attrs) self.cleanUp() self.request.add_request("GetAccountRequest", { "attrs": attrs, "account": { "_content": account, "by": "name", }, }, "urn:zimbraAdmin") response = self.comm.send_request(self.request) if response.is_fault(): raise ZimbraRequestError( "Reponse failed: (%s) %s" % (response.get_fault_code(), response.get_fault_message())) return (response.get_response()) def createAccount(self, account, password=None, attrs=list()): """ Create a new account into Zimbra system :param account: The target account :param password: The given for the account :param attrs: A list of tuple containing the zimbra attribute with the corresponding value: [(zattr, value), ...] """ request_attrs = [] for attr in attrs: zattr, value = attr request_attrs.append({'n': zattr, '_content': value}) self.cleanUp() if not password: password = hmac.new(str(uuid.uuid4), str(uuid.uuid4()), hashlib.md5).hexdigest() self.request.add_request(request_name="CreateAccountRequest", request_dict={ "name": account, "password": password, "a": request_attrs }, namespace="urn:zimbraAdmin") response = self.comm.send_request(self.request) if response.is_fault(): raise ZimbraRequestError( message='Error creating account %s. Error: %s' % (account, response.get_fault_message()), response=response) return response.get_response() def cleanUp(self): """ Clean up after one step to leave a dedicated result for the other test cases. """ self.setUp() def setUp(self): """ Setup everything required to make a request to zimbra server. """ self.request = RequestJson() self.request = self.comm.gen_request(token=self.token) def getDomainId(self, domain): """ Returns the zimbraId of a domain. Useful to modify a domain with ModifyDomainRequest for instance. domain_id = self.getDomainId(inova.net) equal: domain_id = '4af850c7-7e44-452e-ad25-c70fda58f9bf' """ self.cleanUp() self.request.add_request("GetDomainInfoRequest", { "domain": { "_content": domain, "by": "name", }, }, "urn:zimbraAdmin") response = self.comm.send_request(self.request) if response.is_fault(): raise ZimbraRequestError( "Reponse failed: (%s) %s" % (response.get_fault_code(), response.get_fault_message())) return response.get_response()['GetDomainInfoResponse']['domain']['id'] def getDomainQuotaUsage(self, domain): """ Returns quota usage of all users of a specific domain { "GetQuotaUsageResponse": { "searchTotal": 1294, "account": [ { "used": 0, "limit": 0, "name": "*****@*****.**", "id": "63b128d6-b7f2-466d-ac86-7b253e62a7ed" }, { "used": 28, "limit": 26843545600, "name": "*****@*****.**", "id": "5b4832d1-b642-4778-ab7d-3056ebcefada" }, . . . https://files.zimbra.com/docs/soap_api/8.6.0/api-reference/zimbraAdmin/GetQuotaUsage.html """ self.cleanUp() self.request.add_request( "GetQuotaUsageRequest", { "domain": domain, "allServers": "1", "sortBy": "percentUsed", "sortAscending": "1", }, "urn:zimbraAdmin") response = self.comm.send_request(self.request) if response.is_fault(): raise ZimbraRequestError( "Reponse failed: (%s) %s" % (response.get_fault_code(), response.get_fault_message())) return (response.get_response()) def getCos(self, cos_name): """ Get COS by it's name :param cos_name: The name of the COS """ self.cleanUp() self.request.add_request( request_name='GetCosRequest', request_dict={'cos': { 'by': 'name', '_content': cos_name }}, namespace='urn:zimbraAdmin') response = self.comm.send_request(self.request) if response.is_fault(): raise ZimbraRequestError( message='Error getting COS %s. Error: %s' % (cos_name, response.get_fault_message()), response=response) return response.get_response() def createCos(self, cos_name, features=dict()): """ Create a new cos. :param cos_name: The name of the COS :param features: A dict representing the feature->value """ if type(features) is not dict: raise TypeError('Wrong type found for features, must be a dict.') features_req = [] for feature, value in features.items(): features_req.append({'n': feature, '_content': value}) self.cleanUp() self.request.add_request(request_name='CreateCosRequest', request_dict={ 'name': { '_content': cos_name }, 'a': features_req }, namespace='urn:zimbraAdmin') response = self.comm.send_request(self.request) if response.is_fault(): raise ZimbraRequestError( message='Error creating COS %s Error: %s' % (cos_name, response.get_fault_message()), response=response) return response.get_response() def modifyCos(self, zimbra_cos_id, features): """ Update a cos. :param zimbra_cos_id: The zimbraID of the COS :param features: A dict representing the feature->value """ if type(features) is not dict: raise TypeError('Wrong type found for features, must be a dict') features_req = [] for feature, value in features.items(): features_req.append({'n': feature, '_content': value}) self.cleanUp() self.request.add_request(request_name='ModifyCosRequest', request_dict={ 'id': { '_content': zimbra_cos_id }, 'a': features_req }, namespace='urn:zimbraAdmin') response = self.comm.send_request(self.request) if response.is_fault(): raise ZimbraRequestError( message='Error creating COS %s Error: %s' % (cos_name, response.get_fault_message()), response=response) return response.get_response() def deleteCos(self, zimbra_cos_id): """ Delete a specific COS :param zimbra_cos_id: The zimbraID of the COS """ self.cleanUp() self.request.add_request( request_name='DeleteCosRequest', request_dict={'id': { '_content': zimbra_cos_id }}, namespace='urn:zimbraAdmin') response = self.comm.send_request(self.request) if response.is_fault(): raise ZimbraRequestError( message='Error creating COS %s Error: %s' % (cos_name, response.get_fault_message()), response=response) return response.get_response() def getComputeAggregateQuotaUsage(self): """ This method get all quota usage of all domains in a zimbra system. This may take a while depending how many domains and servers you have. Use wisely :P """ self.cleanUp() self.request.add_request("ComputeAggregateQuotaUsageRequest", {}, "urn:zimbraAdmin") response = self.comm.send_request(self.request) if response.is_fault(): raise ZimbraRequestError( "Reponse failed: (%s) %s" % (response.get_fault_code(), response.get_fault_message())) return (response.get_response()) def createDistributionList(self, dlist, attrs=list()): """ This method create zimbra distribution list. A list of attributes may be given, we will handle it for you. :param dlist: The target distribution list :param attrs: List of tuple attributes of distribution list """ if not type(attrs) == list: raise TypeError('attrs must be a list') request_attrs = [] for attr in attrs: zattr, value = attr # If it's a list, then it's a multi-value attribute if type(value) == list: for multi_attr in value: request_attrs.append({'n': zattr, '_content': multi_attr}) else: request_attrs.append({'n': zattr, '_content': value}) self.cleanUp() self.request.add_request(request_name="CreateDistributionListRequest", request_dict={ "name": dlist, "a": request_attrs }, namespace="urn:zimbraAdmin") response = self.comm.send_request(self.request) if response.is_fault(): raise ZimbraRequestError(message='Error creating DL %s Error: %s' % (dlist, response.get_fault_message()), response=response) return response.get_response() def getDistributionList(self, dlist): """ Gets information about a distribution list. :param dlist: The target distribution list Obs: Tested with "a" attribute does not have effect on result """ self.cleanUp() self.request.add_request(request_name="GetDistributionListRequest", request_dict={ "dl": { "_content": dlist, "by": "name", }, }, namespace="urn:zimbraAdmin") response = self.comm.send_request(self.request) if response.is_fault(): raise ZimbraRequestError(message='Error getting DL: %s Error: %s' % (dlist, response.get_fault_message()), response=response) return response.get_response() def deleteDistributionList(self, dlist_zimbra_id): """ Deletes distribution list :param dlist_zimbra_id: Distribution List zimbraID """ self.cleanUp() self.request.add_request(request_name="DeleteDistributionListRequest", request_dict={"id": dlist_zimbra_id}, namespace="urn:zimbraAdmin") response = self.comm.send_request(self.request) if response.is_fault(): raise ZimbraRequestError( message='Error deleting DL: %s Error: %s' % (dlist, response.get_fault_message()), response=response) return response.get_response() def addDistributionListMember(self, dlist_zimbra_id, members): """ This method adds members to a zimbra distribution list. A list of members must be sent. This method is idempotent, it will not change the result executing multiple times :param dlist_zimbra_id: The target distribution list zimbraId :param members: List containing the account members """ if not type(members) == list: raise TypeError('members must be a list') zmembers = [] for member in members: zmembers.append({'_content': member}) self.cleanUp() self.request.add_request( request_name="AddDistributionListMemberRequest", request_dict={ "id": dlist_zimbra_id, "dlm": zmembers }, namespace="urn:zimbraAdmin") response = self.comm.send_request(self.request) if response.is_fault(): raise ZimbraRequestError( message='Error adding members to dlist %s Error: %s' % (dlist_zimbra_id, response.get_fault_message()), response=response) return response.get_response() def grantRight(self, target_name, target_type, grantee_name, grantee_type, right, deny=0): """ Grant a right on a target to an individual or group grantee. This method is idempotent, it will not change the result executing multiple times :param target_name: The target for applying the right (by name). E.g.: 'inova.net'. External docs: /target :param target_type: The type of the target. E.g.: 'domain'. External docs: /target@type :param grantee_name: Grantee selector. E.g.: 'grp', 'dlist'. External docs: /grantee :param grantee_type: The type of the grantee. E.g.: 'grp', 'dlist'. External docs: /grantee@type :param right: The name of the right. E.g.: getDomainQuotaUsage, domainAdminConsoleRights. External docs: /right :param deny: Either to deny or grant the permission. Default is 0. External docs: /right@deny Ref. Docs: https://files.zimbra.com/docs/soap_api/8.6.0/api-reference/zimbraAdmin/GrantRight.html """ self.cleanUp() self.request.add_request(request_name="GrantRightRequest", request_dict={ "target": { "type": target_type, "by": "name", "_content": target_name }, "grantee": { "type": grantee_type, "by": "name", "_content": grantee_name }, "right": { "_content": right, "deny": deny } }, namespace="urn:zimbraAdmin") response = self.comm.send_request(self.request) if response.is_fault(): raise ZimbraRequestError( message='Error adding grant to target_name %s Error: %s' % (target_name, response.get_fault_message()), response=response) return response.get_response()
class Zimbra: """ Classe utilitaire pour l'accès à l'API de Zimbra. """ def __init__( self , cfg ): """ Initialise l'accès à l'API en lisant les paramètres de connexion dans la configuration et en créant l'instance de communication. """ self.url_ = cfg.get( 'bss' , 'zimbra-url' , 'https://webmail.partage.renater.fr/service/soap' ) self.domain_ = cfg.get( 'bss' , 'domain' ) self.dkey_ = cfg.get( 'bss' , 'token' ) self.fake_it_ = cfg.has_flag( 'bss' , 'simulate' ) self.user_ = None self.token_ = None # On lit le timeout depuis la configuration, s'il est défini. Sinon, # on utilise 10s par défaut. timeout_cfg = cfg.get( 'bss' , 'zimbra-timeout' , '10' ) try: timeout_cfg = int( timeout_cfg ) except ValueError as e: Logging( 'zimbra' ).error( 'Paramètre zimbra-timeout invalide' ) raise FatalError( 'Erreur de configuration' , e ) from pythonzimbra.communication import Communication self.comm_ = Communication( self.url_ , timeout = timeout_cfg ) def terminate( self ): """ Termine la session actuelle, s'il y en a une, en envoyant une requête 'EndSession'. """ if self.user_ is None: return Logging( 'zimbra' ).debug( 'Déconnexion de {}'.format( self.user_ ) ) try: self.send_request_( 'Account' , 'EndSession' ) finally: self.user_ = None self.token_ = None def set_user( self , user_name ): """ Tente de se connecter à l'API Zimbra avec un utilisateur donné. Si le nom d'utilisateur n'inclut pas un domaine, il sera rajouté. :param str user_name: le nom d'utilisateur :raises ZimbraError: une erreur de communication s'est produite """ if '@' not in user_name: user_name = '{}@{}'.format( user_name , self.domain_ ) if self.user_ is not None and self.user_ == user_name: return self.terminate( ) # ...Yes dear, you can self-terminate. from pythonzimbra.tools import auth from pythonzimbra.exceptions.auth import AuthenticationFailed Logging( 'zimbra' ).debug( 'Connexion pour {}'.format( user_name ) ) try: ttok = auth.authenticate( self.url_ , user_name , self.dkey_ , raise_on_error = True ) except AuthenticationFailed as e: Logging( 'zimbra' ).error( 'Échec de la connexion: {}'.format( str( e ) ) ) raise ZimbraConnectionError( e ) #FIXME Logging( 'zimbra' ).debug( 'Connexion réussie; jeton: {}'.format( ttok ) ) assert ttok is not None self.user_ = user_name self.token_ = ttok def get_folder( self , path = '/' , uuid = None , recursive = True ): """ Lit les informations d'un dossier (au sens Zimbra du terme) et éventuellement de ses sous-dossiers. :param str path: le chemin absolu du dossier sur lequel on veut \ des informations; sera ignoré si un UUID est spécifié :param str uuid: l'UUID du dossier sur lequel on veut des informations :param bool recursive: les informations des sous-dossiers seront \ également lues si ce paramètre est True :raises ZimbraError: une erreur de communication ou de requête \ s'est produite """ Logging( 'zimbra' ).debug( 'Récupération{} du dossier {}'.format( ' récursive' if recursive else '' , path ) ) folder_spec = dict( ) if uuid is None: folder_spec[ 'path' ] = path else: folder_spec[ 'uuid' ] = uuid ls = self.send_request_( 'Mail' , 'GetFolder' , { 'folder' : folder_spec , 'depth' : -1 if recursive else 0 } ) return ls[ 'folder' ] if 'folder' in ls else None def extract_acl( self , folder_data ): """ Extrait les entrées d'ACL d'un dossier vers un dictionnaire. :param folder_data: les informations d'un dossier :return: un dictionnaire associant à chaque EPPN disposant de droits \ sur un dossier un sous-dictionnaire contenant les permissions \ et l'identifiant de l'entrée dans ses champs 'perm' et 'zid', \ respectivement """ if 'acl' not in folder_data or 'grant' not in folder_data[ 'acl' ]: return dict( ) rv = dict( ) for entry in folder_data[ 'acl' ][ 'grant' ]: if 'd' not in entry or 'perm' not in entry or 'zid' not in entry: continue rv[ entry[ 'd' ] ] = { 'zid' : entry[ 'zid' ] , 'perm' : entry[ 'perm' ] , } return rv def create_folder( self , name , parent_id , folder_type , color = None , url = None , flags = None , others = None ): """ Demande la création d'un dossier. Si le mode simulé est activé, l'appel ne sera pas effectué. :param str name: le nom du dossier à créer :param parent_id: l'identifiant du dossier parent :param str folder_type: le type de dossier à créer (conversation, \ message, contact, appointment, task, wiki, document) :param str color: la couleur, sous la forme d'un code RGB hexadécimal \ ('#RRGGBB') :param str url: l'URL de synchronisation, s'il y en a une :param str flags: les drapeaux Zimbra du dossier :param dict others: un dictionnaire contenant des valeurs \ supplémentaires à associer au dossier :raises ZimbraError: une erreur de communication ou de requête \ s'est produite """ Logging( 'zimbra' ).debug( 'Création{} du dossier {}, parent {}, type {}'.format( ' simulée' if self.fake_it_ else '' , name , parent_id , folder_type ) ) if self.fake_it_: return {} data = { 'name' : name , 'view' : folder_type , 'l' : parent_id , 'fie' : '1' , } if others is not None: data.update( others ) if 'acl' not in data: data[ 'acl' ] = {} if color is not None: data[ 'rgb' ] = color if url is not None: data[ 'url' ] = url if flags is not None: data[ 'f' ] = flags return self.send_request_( 'Mail' , 'CreateFolder' , { 'folder' : data } ) def mount( self , source_eppn , source_id , name , parent_id , folder_type , color = None , flags = None , others = None ): """ Demande le montage d'un dossier. Si le mode simulé est activé, l'appel ne sera pas effectué. :param source_eppn: l'EPPN de l'utilisateur depuis le compte duquel \ on veut monter un dossier :param source_id: l'identifiant Zimbra du dossier à monter :param str name: le nom du dossier à créer :param parent_id: l'identifiant du dossier parent :param str folder_type: le type de dossier à créer (conversation, \ message, contact, appointment, task, wiki, document) :param str color: la couleur, sous la forme d'un code RGB hexadécimal \ ('#RRGGBB') :param str flags: les drapeaux Zimbra du dossier :param dict others: un dictionnaire contenant des valeurs \ supplémentaires à associer au dossier :raises ZimbraError: une erreur de communication ou de requête \ s'est produite """ Logging( 'zimbra' ).debug( ( 'Montage{} du dossier #{} de {} dans le dossier {} ' + '(parent {}, type {})' ).format( ' simulé' if self.fake_it_ else '' , source_id , source_eppn , name , parent_id , folder_type ) ) if self.fake_it_: return {} data = { 'name' : name , 'view' : folder_type , 'l' : parent_id , 'owner' : source_eppn , 'rid' : source_id , } if others is not None: data.update( others ) if color is not None: data[ 'rgb' ] = color if flags is not None: data[ 'f' ] = flags return self.send_request_( 'Mail' , 'CreateMountpoint' , { 'link' : data } ) def move_folder( self , folder_id , to_id ): """ Demande le déplacement d'un dossier. Si le mode simulé est activé, l'appel ne sera pas effectué. :param folder_id: l'identifiant du dossier à déplacer :param to_id: l'identifiant du nouveau dossier parent :raises ZimbraError: une erreur de communication ou de requête \ s'est produite """ Logging( 'zimbra' ).debug( 'Déplacement{} du dossier #{} vers dossier #{}'.format( ' simulé' if self.fake_it_ else '' , folder_id , to_id ) ) if self.fake_it_: return {} return self.send_request_( 'Mail' , 'FolderAction' , { 'action' : { 'op' : 'move' , 'id' : folder_id , 'l' : to_id , } } ) def rename_folder( self , folder_id , name ): """ Demande à l'API de renommer un dossier. Si le mode simulé est activé, l'appel ne sera pas effectué. :param folder_id: l'identifiant du dossier à renommer :param str name: le nouveau nom du dossier :raises ZimbraError: une erreur de communication ou de requête \ s'est produite """ Logging( 'zimbra' ).debug( 'Renommage{} du dossier #{}; nouveau nom "{}"'.format( ' simulé' if self.fake_it_ else '' , folder_id , name ) ) if self.fake_it_: return {} return self.send_request_( 'Mail' , 'FolderAction' , { 'action' : { 'op' : 'rename' , 'id' : folder_id , 'name' : name , } } ) def delete_folder( self , folder_id ): """ Demande à l'API de supprimer un dossier. Si le mode simulé est activé, l'appel ne sera pas effectué. :param folder_id: l'identifiant du dossier à supprimer :raises ZimbraError: une erreur de communication ou de requête \ s'est produite """ Logging( 'zimbra' ).debug( 'Suppression{} du dossier #{}'.format( ' simulée' if self.fake_it_ else '' , folder_id ) ) if self.fake_it_: return {} return self.send_request_( 'Mail' , 'FolderAction' , { 'action' : { 'op' : 'delete' , 'id' : folder_id , } } ) def set_grant( self , folder_id , eppn , perm ): """ Demande à l'API de donner des droits sur un dossier à un utilisateur. :param folder_id: l'identifiant du dossier sur lequel des droits \ doivent être positionnés :param eppn: l'EPPN de l'utilisateur pour lequel des droits doivent \ être positionnés :param perm: les permissions à attribuer à l'utilisateur :raises ZimbraError: une erreur de communication ou de requête \ s'est produite """ Logging( 'zimbra' ).debug( 'Dossier #{}: mise en place des droits "{}" pour {}'.format( folder_id , perm , eppn ) ) if self.fake_it_: return {} return self.send_request_( 'Mail' , 'FolderAction' , { 'action' : { 'op' : 'grant' , 'id' : folder_id , 'grant' : { 'perm' : perm , 'gt' : 'usr' , 'd' : eppn , } , } , } ) def remove_grant( self , folder_id , zid ): """ Demande à l'API de supprimer des droits sur un dossier. :param folder_id: l'identifiant du dossier pour lequel les droits \ doivent être modifiés :param zid: l'identifiant de l'entrée d'ACL à supprimer :raises ZimbraError: une erreur de communication ou de requête \ s'est produite """ Logging( 'zimbra' ).debug( 'Dossier #{}: suppression de l\'entrée ACL {}'.format( folder_id , zid ) ) if self.fake_it_: return {} return self.send_request_( 'Mail' , 'FolderAction' , { 'action': { 'op': '!grant' , 'id': folder_id , 'zid' : zid } } ) def get_data_sources( self ): """ Récupère la liste des sources de données externes associées à un compte Zimbra. :return: le résultat de la requête :raises ZimbraError: une erreur de communication ou de requête \ s'est produite """ Logging( 'zimbra' ).debug( 'Liste des sources de données' ) return self.send_request_( 'Mail' , 'GetDataSources' , {} ) def modify_data_source( self , ds_type , ds_id , **kwargs ): """ Met à jour les paramètres d'une source de données Zimbra. Cette méthode est essentiellement un wrapper autour de la requête SOAP ZimbraMail::ModifyDataSource. Les paramètres supplémentaires seront passés tels quels à l'API Zimbra. :param ds_type: le type de la source de données à modifier :param ds_id: l'identifiant de la source de données à modifier :raises ZimbraError: une erreur de communication ou de requête \ s'est produite """ assert len( kwargs ) != 0 data = dict( kwargs ) data.update({ 'id' : ds_id }) Logging( 'zimbra' ).debug( 'Modification de source de données {}: {}'.format( ds_type , repr( data ) ) ) if self.fake_it_: return self.send_request_( 'Mail' , 'ModifyDataSource' , { ds_type : data } ) def send_request_( self , namespace , request , data = None ): """ Envoie une requête au serveur Zimbra :param str namespace: l'espace de nommage de l'appel, par exemple Mail :param str request: le nom de la requête, par exemple GetFolder :return: la réponse renvoyée par le serveur :raises ZimbraRequestError: une erreur de communication ou de requête \ s'est produite """ assert self.token_ is not None if data is None: data = dict() Logging( 'zimbra.request' ).debug( 'Requête {}.{}( {} )'.format( namespace , request , repr( data ) ) ) req = self.comm_.gen_request( token = self.token_ ) req.add_request( request + 'Request' , data , 'urn:zimbra' + namespace ) response = self.comm_.send_request( req ) if response.is_fault( ): raise ZimbraRequestError( namespace , request , response ) rv = response.get_response( ) return rv[ request + 'Response' ]
class GetZimbra(): def __init__(self, hostname, user, password, domain, pathzm, atributo, add_domain, quota, path_archive): self.url = f'https://{hostname}:7071/service/admin/soap' self.usr = user self.__password = password self.domain = domain.split(',') self.__atributo = atributo self.quota = quota self.__add_domain = add_domain self.__path_archive = path_archive self.today = datetime.datetime.now().date() self.colum = ['name'] def connect(self): try: self.comm = Communication(self.url) self.__token = auth.authenticate(self.url, self.usr, self.__password, admin_auth=True) self.request = self.comm.gen_request(token=self.__token, set_batch=True) return 'Conectado' except urllib.error.URLError as E: # catastrophic error. bail. return f'Erro na conexão: {E}' def getAllDomains(self): """Retornar todos os dominios do ambiente""" self.request.add_request( 'GetAllDomainsRequest', { }, 'urn:zimbraAdmin' ) response = self.comm.send_request(self.request) if not response.is_fault(): alldomains = response.get_response(1)['GetAllDomainsResponse']['domain'] return [{k: v for k, v in cos.items() if k == 'name'} for cos in alldomains] # Retornar somente name do dominio else: return f'Error CountAccount: {response.get_fault_message()}' def countAccount(self): """Limite de COS por dominio""" for dom in self.domain: self.request.add_request( 'CountAccountRequest', { "domain": { "_content": dom, "by": "name" } }, 'urn:zimbraAdmin' ) response = self.comm.send_request(self.request) if not response.is_fault(): allcount = response.get_response(1)['CountAccountResponse'] l_domains = [] d = 0 if len(self.domain) > 1: for a in allcount: cos = {} cos.update({'name': self.domain[d]}) d =+ 1 for v in a['cos']: cos.update({v['name']: v['_content']}) if v['name'] not in self.colum: self.colum.append(v['name']) l_domains.append(cos) else: cos = {} cos.update({'name': self.domain[d]}) for a in allcount['cos']: cos.update({a['name']: a['_content']}) if a['name'] not in self.colum: self.colum.append(a['name']) l_domains.append(cos) return l_domains else: return f'Error CountAccount: {response.get_fault_message()}' def getAllAccounts(self): """Consulta todas as contas por dominio""" if self.__add_domain: if 'name' in self.__atributo: self.__atributo.insert(1, 'domain') else: return "Só é possivel adicionar o dominio se adicionar o atributo 'name'" self.request.add_request( "GetAllAccountsRequest", { "domain": { "_content": self.domain[0], "by": "name" } }, "urn:zimbraAdmin" ) self.request.add_request( "GetAllCosRequest", { }, "urn:zimbraAdmin" ) if self.quota: self.request.add_request( "GetQuotaUsageRequest", { "domain": self.domain[0], "allServers": "1" }, "urn:zimbraAdmin" ) response = self.comm.send_request(self.request) if not response.is_fault(): allAccounts = [response.get_response(1)['GetAllAccountsResponse']['account'],response.get_response(2)['GetAllCosResponse']['cos']] if self.quota: allAccounts.append(response.get_response(3)['GetQuotaUsageResponse']['account']) return allAccounts else: return f'Error getAllAccounts: {response.get_fault_message()}' def getFormatted(self, func=None): quota = '' lista_dict = [] for account in func[0]: a_atr = {} a_atr['name'] = account['name'] for atr in account['a']: if atr['n'] in self.__atributo: a_atr[atr['n']] = atr['_content'] lista_dict.append(a_atr) if 'zimbraCOSId' in self.__atributo: allcos = [{k: v for k, v in cos.items() if k == 'name' or k == 'id'} for cos in func[1]] if self.quota: quota = func[2] for account in quota: account['used'] = f"{int(account['used'] / 1024 / 1024)} MB" account['limit'] = f"{int(account['limit'] / 1024 / 1024)} MB" [self.__atributo.append(i) for i in ['used', 'limit']] with open(f'{self.__path_archive}Relatório_{self.domain[0]}.csv', 'w') as archive: data_csv = DictWriter(archive, fieldnames=self.__atributo) data_csv.writeheader() for account in lista_dict: dict_comprehension = {chave: valor for chave, valor in account.items()} if self.__add_domain: dict_comprehension['domain'] = dict_comprehension['name'].split('@')[1] if self.quota: # Configurar quota com as contas for i in quota: if i['name'] == account['name']: dict_comprehension['used'] = i['used'] dict_comprehension['limit'] = i['limit'] if 'zimbraLastLogonTimestamp' in dict_comprehension.keys(): # Configurar data do ultimo login data = dict_comprehension['zimbraLastLogonTimestamp'] date_br = datetime.datetime( int(data[:4]), int(data[4:6]), int(data[6:8]), int(data[8:10]), int(data[10:12]), int(data[12:14]) ).strftime('%d/%m/%Y %H:%M') dict_comprehension['zimbraLastLogonTimestamp'] = date_br if 'zimbraCreateTimestamp' in dict_comprehension.keys(): # Configurar data de criação data1 = dict_comprehension['zimbraCreateTimestamp'] date1_br = datetime.datetime( int(data1[:4]), int(data1[4:6]), int(data1[6:8]), int(data1[8:10]), int(data1[10:12]), int(data1[12:14]) ).strftime('%d/%m/%Y %H:%M') dict_comprehension['zimbraCreateTimestamp'] = date1_br if 'zimbraCOSId' in dict_comprehension.keys(): # Configurar nome do COS cosid = dict_comprehension['zimbraCOSId'] cosname = [cos['name'] for cos in allcos if cosid == cos['id']] dict_comprehension['zimbraCOSId'] = cosname[0] data_csv.writerow(dict_comprehension) return f'Planilha criada com SUCESSO! \nRelatório_{self.today}' def formatedPlan(self, func): list_domains = func with open(f'{self.__path_archive}Relatório_{self.today}.csv', 'w') as archive: data_csv = DictWriter(archive, fieldnames=self.colum) data_csv.writeheader() for domain_count in list_domains: list_domains = {chave: valor for chave, valor in domain_count.items()} data_csv.writerow(list_domains) return f'Planilha criada com SUCESSO! \nRelatório_{self.today}'
def authenticate(url, account, key, by='name', expires=0, timestamp=None, timeout=None, request_type="xml"): """ Authenticate to the Zimbra server :param url: URL of Zimbra SOAP service :param account: The account to be authenticated against :param key: The preauth key of the domain of the account :param by: If the account is specified as a name, an ID or a ForeignPrincipal :param expires: When the token expires (or 0 for default expiration) :param timestamp: When the token was requested (None for "now") :param timeout: Timeout for the communication with the server. Defaults to the urllib2-default :return: The authentication token :rtype: str or None or unicode """ if timestamp is None: timestamp = int(datetime.now().strftime("%s")) * 1000 pak = preauth.create_preauth(account, key, by, expires, timestamp) if request_type == 'xml': auth_request = RequestXml() else: auth_request = RequestJson() auth_request.add_request( 'AuthRequest', { 'account': { 'by': by, '_content': account }, 'preauth': { 'timestamp': timestamp, 'expires': expires, '_content': pak } }, 'urn:zimbraAccount' ) server = Communication(url, timeout) if request_type == 'xml': response = ResponseXml() else: response = ResponseJson() try: server.send_request(auth_request, response) except HTTPError: # A HTTPError (which is an AuthError in most cases) occured. Simply # return nothing return None return response.get_response()['AuthResponse']['authToken']['_content']
def run_admin_test(self, request_type): """ Actually do the work """ config = get_config() if config.getboolean("admin_request_test", "enabled"): # Run only if enabled token = authenticate(config.get("admin_request_test", "admin_url"), config.get("admin_request_test", "admin_account"), config.get("admin_request_test", "admin_password"), config.get("admin_request_test", "admin_account_by"), admin_auth=True, request_type=request_type) if token is None: self.fail("Authentication with the configured settings " "was not successful") # Create an account comm = Communication(config.get("admin_request_test", "admin_url")) if request_type == "xml": request = RequestXml() else: request = RequestJson() request.set_auth_token(token) request.add_request( "CreateAccountRequest", { "name": config.get("admin_request_test", "test_account"), "password": config.get("admin_request_test", "test_password") }, "urn:zimbraAdmin") if request_type == "xml": response = ResponseXml() else: response = ResponseJson() comm.send_request(request, response) if response.is_fault(): self.fail( "CreateAccount faulted. (%s) %s" % (response.get_fault_code(), response.get_fault_message())) account_id = response.get_response( )["CreateAccountResponse"]["account"]["id"] # Try to log in as the new account user_token = authenticate(config.get("admin_request_test", "url"), config.get("admin_request_test", "test_account"), config.get("admin_request_test", "test_password"), "name", request_type=request_type, use_password=True) if user_token is None: self.fail("Cannot log in as the test user.") # Remove account request.clean() response.clean() request.set_auth_token(token) request.add_request("DeleteAccountRequest", {"id": account_id}, "urn:zimbraAdmin") comm.send_request(request, response) if response.is_fault(): self.fail( "Cannot remove test account: (%s) %s" % (response.get_fault_code(), response.get_fault_message()))
def test_genrequest_batch_xml(self): """ Create a batch-request only using the Communication-object ( xml-version) """ config = get_config() if config.getboolean("genrequest_test", "enabled"): # Run only if enabled comm = Communication(config.get("genrequest_test", "url")) token = authenticate( config.get("genrequest_test", "url"), config.get("genrequest_test", "account"), config.get("genrequest_test", "preauthkey") ) self.assertNotEqual( token, None, "Cannot authenticate." ) request = comm.gen_request( request_type="xml", token=token, set_batch=True ) self.assertEqual( type(request), RequestXml, "Generated request wasn't an xml-request" ) request.add_request( "NoOpRequest", { }, "urn:zimbraMail" ) request.add_request( "NoOpRequest", { }, "urn:zimbraMail" ) response = comm.send_request(request) if response.is_fault(): self.fail( "Reponse failed: (%s) %s" % ( response.get_fault_code(), response.get_fault_message() ) ) self.assertEqual( response.is_batch(), True, "Batch-request didn't return a Batch response." ) expected_batch = { 'nameToId': { 'NoOpResponse': [ '1', '2' ] }, 'hasFault': False, 'idToName': { '1': 'NoOpResponse', '2': 'NoOpResponse' } } self.assertEqual( response.get_batch(), expected_batch, "Batch-dictionary wasn't expected" )