#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

""" Zimbra SOAP client pythonic abstraction

core classes for SOAP clients, there are also REST clients here, but used only
for pre-authentification.
"""

import datetime
try:
    from urllib2 import HTTPCookieProcessor, build_opener, HTTPError
except ImportError:
    from urllib.request import HTTPCookieProcessor, build_opener, HTTPError
import time
import re
import warnings

from six.moves import http_cookiejar, urllib
from six import text_type, binary_type
import pythonzimbra
import pythonzimbra.tools.auth
from pythonzimbra.communication import Communication

from zimsoap import utils
from zimsoap import zobjects


class RESTClient:
    """ Abstract Classe, RESTClient defines a REST client for some operations we
    can't do with SOAP API, such as admin preauth.
    """
    class NoPreauthKeyProvided(Exception):
        pass

    class RESTBackendError(Exception):
        def __init__(self, e):
            self.parent = e
            self.msg = 'Zimbra issued HTTP error : '+e.msg
            Exception.__init__(self, self.msg)

    def __init__(self, server_host, server_port=None, preauth_key=None):
        if server_port:
            self.preauth_url = 'https://{0}:{1}/service/preauth?'.format(
                server_host, server_port)
        else:
            self.preauth_url = 'https://{0}/service/preauth?'.format(
                server_host)

        self.set_preauth_key(preauth_key)

    def set_preauth_key(self, preauth_key):
        self.preauth_key = preauth_key

    def get_preauth_token(self, account_name, expires=0):
        if not self.preauth_key:
            raise self.NoPreauthKeyProvided

        ts = int(time.time())*1000

        preauth_str = utils.build_preauth_str(self.preauth_key, account_name,
                                              ts, expires, admin=self.isadmin)

        args = urllib.parse.urlencode({
            'account': account_name,
            'by': 'name',
            'timestamp': ts,
            'expires': expires*1000,
            'admin': "1" if self.isadmin else "0",
            'preauth': preauth_str
        })

        cj = http_cookiejar.CookieJar()
        browser = build_opener(HTTPCookieProcessor(cj))

        try:
            url = browser.open(self.preauth_url+args)
            url.read()
            value = ""
            for cookie in cj:
                if cookie.name == self.TOKEN_COOKIE:
                    value = cookie.value
            url.close()
            browser.close()
            return value
        except HTTPError as e:
            raise self.RESTBackendError(e)


class AdminRESTClient(RESTClient):
    TOKEN_COOKIE = 'ZM_ADMIN_AUTH_TOKEN'

    def __init__(self, server_host, server_port=7071, preauth_key=None):
        self.isadmin = True
        RESTClient.__init__(self, server_host, server_port, preauth_key)


class AccountRESTClient(RESTClient):
    TOKEN_COOKIE = 'ZM_AUTH_TOKEN'

    def __init__(self, *args, **kwargs):
        self.isadmin = False
        RESTClient.__init__(self, *args, **kwargs)


class MailRESTClient(RESTClient):
    TOKEN_COOKIE = 'ZM_MAIL_AUTH_TOKEN'

    def __init__(self, *args, **kwargs):
        self.isadmin = False
        RESTClient.__init__(self, *args, **kwargs)


class ZimSOAPException(Exception):
    pass


class ShouldAuthenticateFirst(ZimSOAPException):
    """ Error fired when an operation requiring auth is intented before the auth
    is done.
    """
    pass


class DomainHasNoPreAuthKey(ZimSOAPException):
    """ Error fired when the server has no preauth key
    """
    def __init__(self, domain):
        # Call the base class constructor with the parameters it needs
        self.msg = '"{0}" has no preauth key, make one first, see {1}'.format(
            domain.name,
            'http://wiki.zimbra.com/wiki/Preauth'
            '#Preparing_a_domain_for_preauth'
            )
        Exception.__init__(self)


class ZimbraSoapServerError(ZimSOAPException):
    r_soap_text = re.compile(r'<soap:Text>(.*)</soap:Text>')

    def __init__(self, request, response):
        self.request = request
        self.response = response

        fault = response.get_response()['Fault']
        self.msg = fault['Reason']['Text']
        self.code = fault['Detail']['Error']['Code']
        self.trace_url = fault['Detail']['Error']['Trace']

    def __str__(self):
        return '{0}: {1}'.format(
            self.code, self.msg)


class ZimbraSoapUnexpectedResponse(ZimSOAPException):
    def __init__(self, request, response, msg=''):
        self.request = request
        self.response = response
        self.msg = msg

    def __str__(self):
        if self.msg:
            return self.msg
        else:
            return 'Unexpected Response from Zimbra Server'


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 = 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 admin_zc: An already logged-in admin client
        :type admin_zc: ZimbraAdminClient
        :param login: the user login (or email) you want to log as
        """
        # 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


class ZimbraAccountClient(ZimbraAbstractClient):
    """ Specialized Soap client to access zimbraAccount webservice.

    API ref is
    http://files.zimbra.com/docs/soap_api/8.0.4/soap-docs-804/api-reference/zimbraAccount/service-summary.html
    """
    NAMESPACE = 'urn:zimbraAccount'
    LOCATION = 'service/soap'
    REST_PREAUTH = AccountRESTClient

    def __init__(self, server_host, server_port='443', *args, **kwargs):
        super(ZimbraAccountClient, self).__init__(
            server_host, server_port,
            *args, **kwargs)

    # Share

    def get_share_info(self, grantee_type=None, grantee_id=None,
                       grantee_name=None, owner=None, owner_type='name'):
        """
        :returns: list of dict representing shares informations
        """
        params = {}
        if grantee_type:
            if 'grantee' not in params.keys():
                params['grantee'] = {}
            params['grantee'].update({'type': grantee_type})
        if grantee_id:
            if 'grantee' not in params.keys():
                params['grantee'] = {}
            params['grantee'].update({'id': grantee_id})
        if grantee_name:
            if 'grantee' not in params.keys():
                params['grantee'] = {}
            params['grantee'].update({'name': grantee_name})
        if owner:
            params['owner'] = {'by': owner_type, '_content': owner}

        try:
            resp = self.request('GetShareInfo', params)
        # if user never logged in, no mailbox was created
        except ZimbraSoapServerError as e:
            if 'mailbox not found for account' in str(e):
                return []
            else:
                raise e
        if resp and isinstance(resp['share'], list):
            return resp['share']
        elif resp and isinstance(resp['share'], dict):
            return [resp['share']]
        else:
            return []

    # Signature

    def create_signature(self, name, content, contenttype="text/html"):
        """
        :param:  name        verbose name of the signature
        :param:  content     content of the signature, in html or plain-text
        :param:  contenttype can be "text/html" (default) or "text/plain"
        :returns: a zobjects.Signature object
        """
        s = zobjects.Signature(name=name)
        s.set_content(content, contenttype)

        resp = self.request('CreateSignature', {'signature': s.to_creator()})
        return zobjects.Signature.from_dict(resp['signature'])

    def get_signatures(self):
        """ Get all signatures for the current user

        :returns: a list of zobjects.Signature
        """
        signatures = self.request_list('GetSignatures')

        return [zobjects.Signature.from_dict(i) for i in signatures]

    def get_signature(self, signature):
        """Retrieve one signature, discriminated by name or id.

        Note that signature name is not case sensitive.

        :param: a zobjects.Signature describing the signature
               like "Signature(name='my-sig')"

        :returns: a zobjects.Signature object, filled with the signature if no
                 signature is matching, returns None.
        """
        resp = self.request_list('GetSignatures')

        # GetSignature does not allow to filter the results, so we do it by
        # hand...
        if resp and (len(resp) > 0):
            for sig_dict in resp:
                sig = zobjects.Signature.from_dict(sig_dict)
                if hasattr(signature, 'id'):
                    its_this_one = (sig.id == signature.id)
                elif hasattr(signature, 'name'):
                    its_this_one = (sig.name.upper() == signature.name.upper())
                else:
                    raise ValueError('should mention one of id,name')
                if its_this_one:
                    return sig
        else:
            return None

    def delete_signature(self, signature):
        """ Delete a signature by name or id

        :param: signature a Signature object with name or id defined
        """
        self.request('DeleteSignature', {'signature': signature.to_selector()})

    def modify_signature(self, signature):
        """ Modify an existing signature

        Can modify the content, contenttype and name. An unset attribute will
        not delete the attribute but leave it untouched.
        :param: signature a zobject.Signature object, with modified
                         content/contentype/name, the id should be present and
                          valid, the name does not allows to identify the
                         signature for that operation.
        """
        # if no content is specified, just use a selector (id/name)
        dic = signature.to_creator(for_modify=True)

        self.request('ModifySignature', {'signature': dic})

    def get_preferences(self):
        """ Gets all the preferences of the current user

        :returns: a dict presenting the preferences by name, values are
                 typed to str/bool/int/float regarding their content.
        """
        pref_list = self.request('GetPrefs')['pref']

        out = {}
        for pref in pref_list:
            out[pref['name']] = utils.auto_type(pref['_content'])

        return out

    def get_preference(self, pref_name):
        """ Gets a single named preference

        :returns: the value, typed to str/bool/int/float regarding its content.
        """
        resp = self.request_single('GetPrefs', {'pref': {'name': pref_name}})
        return utils.auto_type(resp['_content'])

    def create_identity(self, name, attrs=[]):
        """ Create an Identity

        :param: name identity name
        :param: attrs list of dict of attributes (zimsoap format)
        :returns: a zobjects.Identity object
        """
        params = {
            'name': name,
            'a': attrs
        }
        resp = self.request('CreateIdentity', {'identity': params})
        return zobjects.Identity.from_dict(resp['identity'])

    def get_identities(self, identity=None, attrs=None):
        """ Get identities matching name and attrs
        of the user, as a list

        :param: zobjects.Identity or identity name (string)
        :param: attrs dict of attributes to return only identities matching
        :returns: list of zobjects.Identity
        """
        resp = self.request('GetIdentities')

        if 'identity' in resp:
            identities = resp['identity']
            if type(identities) != list:
                identities = [identities]

            if identity or attrs:
                wanted_identities = []

                for u_identity in [
                        zobjects.Identity.from_dict(i) for i in identities]:
                    if identity:
                        if isinstance(identity, zobjects.Identity):
                            if u_identity.name == identity.name:
                                return [u_identity]
                        else:
                            if u_identity.name == identity:
                                return [u_identity]

                    elif attrs:
                        for attr, value in attrs.items():
                            if (attr in u_identity._a_tags and
                                    u_identity._a_tags[attr] == value):
                                wanted_identities.append(u_identity)
                return wanted_identities
            else:
                return [zobjects.Identity.from_dict(i) for i in identities]
        else:
            return []

    def modify_identity(self, identity, **kwargs):
        """ Modify some attributes of an identity or its name.

        :param: identity a zobjects.Identity with `id` set (mandatory). Also
               set items you want to modify/set and/or the `name` attribute to
               rename the identity.
               Can also take the name in string and then attributes to modify
        :returns: zobjects.Identity object
        """

        if isinstance(identity, zobjects.Identity):
            self.request('ModifyIdentity', {'identity': identity._full_data})
            return self.get_identities(identity=identity.name)[0]
        else:
            attrs = []
            for attr, value in kwargs.items():
                attrs.append({
                    'name': attr,
                    '_content': value
                })
            self.request('ModifyIdentity', {
                'identity': {
                    'name': identity,
                    'a': attrs
                }
            })
            return self.get_identities(identity=identity)[0]

    def delete_identity(self, identity):
        """ Delete an identity from its name or id

        :param: a zobjects.Identity object with name or id defined or a string
        of the identity's name
        """
        if isinstance(identity, zobjects.Identity):
            self.request(
                'DeleteIdentity', {'identity': identity.to_selector()})
        else:
            self.request('DeleteIdentity', {'identity': {'name': identity}})
    # Whitelists and Blacklists

    def get_white_black_lists(self):
        return self.request('GetWhiteBlackList')

    def add_to_blacklist(self, values):
        param = {'blackList': {'addr': []}}
        for value in values:
            param['blackList']['addr'].append({'op': '+', '_content': value})

        self.request('ModifyWhiteBlackList', param)

    def remove_from_blacklist(self, values):
        param = {'blackList': {'addr': []}}
        for value in values:
            param['blackList']['addr'].append({'op': '-', '_content': value})

        self.request('ModifyWhiteBlackList', param)

    def add_to_whitelist(self, values):
        param = {'whiteList': {'addr': []}}
        for value in values:
            param['whiteList']['addr'].append({'op': '+', '_content': value})

        self.request('ModifyWhiteBlackList', param)

    def remove_from_whitelist(self, values):
        param = {'whiteList': {'addr': []}}
        for value in values:
            param['whiteList']['addr'].append({'op': '-', '_content': value})

        self.request('ModifyWhiteBlackList', param)


class ZimbraAdminClient(ZimbraAbstractClient):
    """ Specialized Soap client to access zimbraAdmin webservice, handling auth.

    API ref is
    http://files.zimbra.com/docs/soap_api/8.0.4/soap-docs-804/api-reference/zimbraAdmin/service-summary.html
    """
    NAMESPACE = 'urn:zimbraAdmin'
    LOCATION = 'service/admin/soap'
    REST_PREAUTH = AdminRESTClient

    def __init__(self, server_host, server_port='7071',
                 *args, **kwargs):
        super(ZimbraAdminClient, self).__init__(
            server_host, server_port,
            *args, **kwargs)

    def get_quota_usage(self, domain=None, all_servers=None,
                        limit=None, offset=None, sort_by=None,
                        sort_ascending=None, refresh=None):
        content = {}
        if domain:
            content['domain'] = domain
        if all_servers:
            content['allServers'] = all_servers
        if limit:
            content['limit'] = limit
        if sort_by:
            content['sortBy'] = sort_by
        if sort_ascending:
            content['sortAscending'] = sort_ascending
        if refresh:
            content['refresh'] = refresh

        resp = self.request_list('GetQuotaUsage', content)

        return resp

    def get_all_config(self):
        resp = self.request_list('GetAllConfig')
        config = {}
        for attr in resp:
            # If there is multiple attributes with the same name
            if attr['n'] in config:
                if isinstance(config[attr['n']], str):
                    config[attr['n']] = [config[attr['n']], attr['_content']]
                else:
                    config[attr['n']].append(attr['_content'])
            else:
                config[attr['n']] = attr['_content']
        return config

    def get_config(self, attr):
        resp = self.request_list('GetConfig', {'a': {'n': attr}})
        if len(resp) > 1:
            config = {attr: []}
            for a in resp:
                config[attr].append(a['_content'])
        elif len(resp) == 1:
            config = {attr: resp[0]['_content']}
        else:
            raise KeyError('{} not found'.format(attr))
        return config

    def modify_config(self, attr, value):
        self.request('ModifyConfig', {
            'a': {
                'n': attr,
                '_content': value
            }})
        if attr[0] == '-' or attr[0] == '+':
            attr = attr[1::]
        return self.get_config(attr)

    def _get_or_fetch_id(self, zobj, fetch_func):
        """ Returns the ID of a Zobject wether it's already known or not

        If zobj.id is not known (frequent if zobj is a selector), fetches first
        the object and then returns its ID.

        :type zobj:       a zobject subclass
        :type fetch_func: the function to fetch the zobj from server if its id
                          is undefined.
        :returns:         the object id
        """

        try:
            return zobj.id
        except AttributeError:
            try:
                return fetch_func(zobj).id
            except AttributeError:
                raise ValueError('Unqualified Resource')

    def get_all_domains(self):
        resp = self.request_list('GetAllDomains')
        return [zobjects.Domain.from_dict(d) for d in resp]

    def get_all_accounts(self, domain=None, server=None,
                         include_system_accounts=False,
                         include_admin_accounts=True,
                         include_virtual_accounts=True):
        selectors = {}
        if domain:
            selectors['domain'] = domain.to_selector()
        if server:
            selectors['server'] = server.to_selector()

        dict_accounts = self.request_list('GetAllAccounts', selectors)

        accounts = []
        for i in dict_accounts:
            account = zobjects.Account.from_dict(i)

            if not (
                not include_system_accounts and account.is_system() or
                not include_admin_accounts and account.is_admin() or
                not include_virtual_accounts and account.is_virtual()
            ):
                accounts.append(account)

        return accounts

    # Calendar resources

    def get_all_calendar_resources(self, domain=None, server=None,):
        selectors = {}
        if domain:
            selectors['domain'] = domain.to_selector()
        if server:
            selectors['server'] = server.to_selector()

        dict_calres = self.request_list('GetAllCalendarResources', selectors)

        resources = []
        for i in dict_calres:
            calres = zobjects.CalendarResource.from_dict(i)
            resources.append(calres)

        return resources

    def get_calendar_resource(self, cal_resource):
        """ Fetches an calendar resource with all its attributes.

        :param account: a CalendarResource, with either id or
                        name attribute set.
        :returns: a CalendarResource object, filled.
        """
        selector = cal_resource.to_selector()
        resp = self.request_single('GetCalendarResource',
                                   {'calresource': selector})
        return zobjects.CalendarResource.from_dict(resp)

    def create_calendar_resource(self, name, password=None, attrs={}):
        """
        :param: attrs a dict of attributes, must specify the displayName and
                     zimbraCalResType
        """
        args = {
            'name': name,
            'a': [{'n': k, '_content': v} for k, v in attrs.items()]
            }
        if password:
            args['password'] = password
        resp = self.request_single('CreateCalendarResource', args)
        return zobjects.CalendarResource.from_dict(resp)

    def delete_calendar_resource(self, calresource):
        self.request('DeleteCalendarResource', {
            'id': self._get_or_fetch_id(calresource,
                                        self.get_calendar_resource),
        })

    def modify_calendar_resource(self, calres, attrs):
        """
        :param calres: a zobjects.CalendarResource
        :param attrs:    a dictionary of attributes to set ({key:value,...})
        """
        attrs = [{'n': k, '_content': v} for k, v in attrs.items()]
        self.request('ModifyCalendarResource', {
            'id': self._get_or_fetch_id(
                calres, self.get_calendar_resource),
            'a': attrs
        })

    def rename_calendar_resource(self, r_description, new_r_name):
        """
        :param r_description : a CalendarResource specifying either :
                   - id:   the ressource ID
                   - r_description: the name of the ressource
        :param new_r_name: new name of the list
        :return: a zobjects.CalendarResource
        """
        resp = self.request('RenameCalendarResource', {
            'id': self._get_or_fetch_id(r_description,
                                        self.get_calendar_resource),
            'newName': new_r_name
        })

        return zobjects.CalendarResource.from_dict(resp['calresource'])

    # Mailbox stats

    def get_mailbox_stats(self):
        """ Get global stats about mailboxes

        Parses <stats numMboxes="6" totalSize="141077"/>

        :returns: dict with stats
        """
        resp = self.request_single('GetMailboxStats')
        ret = {}
        for k, v in resp.items():
            ret[k] = int(v)

        return ret

    def count_account(self, domain):
        """ Count the number of accounts for a given domain, sorted by cos

        :returns: a list of pairs <ClassOfService object>,count
        """
        selector = domain.to_selector()
        cos_list = self.request_list('CountAccount', {'domain': selector})
        ret = []

        for i in cos_list:
            count = int(i['_content'])
            ret.append((zobjects.ClassOfService.from_dict(i),  count))

        return list(ret)

    def get_all_mailboxes(self):
        resp = self.request_list('GetAllMailboxes')

        return [zobjects.Mailbox.from_dict(i) for i in resp]

    def get_account_mailbox(self, account_id):
        """ Returns a Mailbox corresponding to an account. Usefull to get the
        size (attribute 's'), and the mailbox ID, returns nothing appart from
        that.
        """
        selector = zobjects.Mailbox(id=account_id).to_selector()
        resp = self.request_single('GetMailbox', {'mbox': selector})

        return zobjects.Mailbox.from_dict(resp)

    def get_account_cos(self, account):
        """ Fetch the cos for a given account

        Quite different from the original request which returns COS + various
        URL + COS + zimbraMailHost... But all other informations are accessible
        through get_account.

        :type account: zobjects.Account
        :rtype: zobjects.COS
        """
        resp = self.request(
            'GetAccountInfo', {'account': account.to_selector()})
        return zobjects.COS.from_dict(resp['cos'])

    def create_domain(self, name):
        """
        :param name: A string, NOT a zObject
        :return: a zobjects.Domain
        """
        args = {'name': name}
        resp = self.request_single('CreateDomain', args)

        return zobjects.Domain.from_dict(resp)

    def delete_domain(self, domain):
        self.request('DeleteDomain', {
            'id': self._get_or_fetch_id(domain, self.get_domain)
        })

    def delete_domain_forced(self, domain):
        # Remove aliases and accounts
        # we take all accounts because there might be an alias
        # for an account of an other domain
        accounts = self.get_all_accounts()
        for a in accounts:
            if 'zimbraMailAlias' in a._a_tags:
                aliases = a._a_tags['zimbraMailAlias']
                if isinstance(aliases, list):
                    for alias in aliases:
                        if alias.split('@')[1] == domain.name:
                            self.remove_account_alias(a, alias)
                else:
                    if aliases.split('@')[1] == domain.name:
                        self.remove_account_alias(a, aliases)
            if a.name.split('@')[1] == domain.name:
                self.delete_account(a)

        # Remove resources
        resources = self.get_all_calendar_resources(domain=domain)
        for r in resources:
            self.delete_calendar_resource(r)

        # Remove distribution lists
        dls = self.get_all_distribution_lists(domain)
        for dl in dls:
            self.delete_distribution_list(dl)

        self.request('DeleteDomain', {
            'id': self._get_or_fetch_id(domain, self.get_domain)
        })

    def get_domain(self, domain):
        selector = domain.to_selector()
        resp = self.request_single('GetDomain', {'domain': selector})
        return zobjects.Domain.from_dict(resp)

    def modify_domain(self, domain, attrs):
        """
        :type domain: a zobjects.Domain
        :param attrs: attributes to modify
        :type attrs dict
        """
        attrs = [{'n': k, '_content': v} for k, v in attrs.items()]
        self.request('ModifyDomain', {
            'id': self._get_or_fetch_id(domain, self.get_domain),
            'a': attrs
        })

    def add_distribution_list_alias(self, distribution_list, alias):
        """
        :param distribution_list:  a distribution list object to be used as
         a selector
        :param alias:     email alias address
        :returns:         None (the API itself returns nothing)
        """
        self.request('AddDistributionListAlias', {
            'id': self._get_or_fetch_id(
                distribution_list, self.get_distribution_list
                ),
            'alias': alias,
        })

    def remove_distribution_list_alias(self, distribution_list, alias):
        """
        :param distribution_list:  an distribution list object to be used as
        a selector
        :param alias:     email alias address
        :returns:         None (the API itself returns nothing)
        """
        self.request('RemoveDistributionListAlias', {
            'id': self._get_or_fetch_id(
                distribution_list, self.get_distribution_list
            ),
            'alias': alias,
        })

    def get_all_distribution_lists(self, domain=None):
        if domain:
            selectors = {'domain': domain.to_selector()}
        else:
            selectors = {}

        got = self.request_list('GetAllDistributionLists', selectors)
        return [zobjects.DistributionList.from_dict(i) for i in got]

    def get_distribution_list(self, dl_description):
        """
        :param:   dl_description : a DistributionList specifying either :
                   - id:   the account_id
                   - name: the name of the list
        :returns: the DistributionList
        """
        selector = dl_description.to_selector()

        resp = self.request_single('GetDistributionList', {'dl': selector})
        dl = zobjects.DistributionList.from_dict(resp)
        return dl

    def create_distribution_list(self, name, dynamic=0):
        """

        :param name: A string, NOT a zObject
        :param dynamic:
        :return: a zobjects.DistributionList
        """
        args = {'name': name, 'dynamic': str(dynamic)}
        resp = self.request_single('CreateDistributionList', args)

        return zobjects.DistributionList.from_dict(resp)

    def modify_distribution_list(self, dl_description, attrs):
        """
        :param dl_description : a DistributionList specifying either :
                   - id:   the dl_list_id
                   - dl_description: the name of the list
        :param attrs  : a dictionary of attributes to set ({key:value,...})
        """
        attrs = [{'n': k, '_content': v} for k, v in attrs.items()]
        self.request('ModifyDistributionList', {
            'id': self._get_or_fetch_id(dl_description,
                                        self.get_distribution_list),
            'a': attrs
        })

    def rename_distribution_list(self, dl_description, new_dl_name):
        """
        :param dl_description : a DistributionList specifying either :
                   - id:   the dl_list_id
                   - dl_description: the name of the list
        :param new_dl_name: new name of the list
        :return: a zobjects.DistributionList
        """
        resp = self.request('RenameDistributionList', {
            'id': self._get_or_fetch_id(dl_description,
                                        self.get_distribution_list),
            'newName': new_dl_name
        })

        return zobjects.DistributionList.from_dict(resp['dl'])

    def delete_distribution_list(self, dl):
        self.request('DeleteDistributionList', {
            'id': self._get_or_fetch_id(dl, self.get_distribution_list)
        })

    def add_distribution_list_member(self, distribution_list, members):
        """ Adds members to the distribution list

        :type distribution_list: zobjects.DistributionList
        :param members:          list of email addresses you want to add
        :type members:           list of str
        """
        members = [{'_content': v} for v in members]
        resp = self.request_single('AddDistributionListMember', {
            'id': self._get_or_fetch_id(distribution_list,
                                        self.get_distribution_list),
            'dlm': members
        })
        return resp

    def remove_distribution_list_member(self, distribution_list, members):
        """ Removes members from the distribution list

        :type distribution_list: zobjects.DistributionList
        :param members:          list of email addresses you want to remove
        :type members:           list of str
        """
        members = [{'_content': v} for v in members]
        resp = self.request_single('RemoveDistributionListMember', {
            'id': self._get_or_fetch_id(distribution_list,
                                        self.get_distribution_list),
            'dlm': members
        })
        return resp

    def get_account(self, account):
        """ Fetches an account with all its attributes.

        :param account: an account object, with either id or name attribute set
        :returns: a zobjects.Account object, filled.
        """
        selector = account.to_selector()
        resp = self.request_single('GetAccount', {'account': selector})
        return zobjects.Account.from_dict(resp)

    def rename_account(self, account, new_name):
        """ Rename an account.

        :param account: a zobjects.Account
        :param new_name: a string of new account name
        """
        self.request('RenameAccount', {
            'id': self._get_or_fetch_id(account, self.get_account),
            'newName': new_name
        })

    def modify_account(self, account, attrs):
        """
        :param account: a zobjects.Account
        :param attrs  : a dictionary of attributes to set ({key:value,...})
        """
        attrs = [{'n': k, '_content': v} for k, v in attrs.items()]
        self.request('ModifyAccount', {
            'id': self._get_or_fetch_id(account, self.get_account),
            'a': attrs
        })

    def set_password(self, account, password):
        """
        :param account: a zobjects.Account
        :param password: new password to set
        """
        self.request('SetPassword', {
            'id': account.id,
            'newPassword': password
        })

    def create_account(self, email, password=None, attrs={}):
        """
        :param email:    Full email with domain eg: login@domain.com
        :param password: Password for local auth
        :param attrs:    a dictionary of attributes to set ({key:value,...})
        :returns:        the created zobjects.Account
        """
        attrs = [{'n': k, '_content': v} for k, v in attrs.items()]

        params = {'name': email, 'a': attrs}

        if password:
            params['password'] = password

        resp = self.request_single('CreateAccount', params)

        return zobjects.Account.from_dict(resp)

    def delete_account(self, account):
        """
        :param account: an account object to be used as a selector
        """
        self.request('DeleteAccount', {
            'id': self._get_or_fetch_id(account, self.get_account),
        })

    def add_account_alias(self, account, alias):
        """
        :param account:  an account object to be used as a selector
        :param alias:     email alias address
        :returns:         None (the API itself returns nothing)
        """
        self.request('AddAccountAlias', {
            'id': self._get_or_fetch_id(account, self.get_account),
            'alias': alias,
        })

    def remove_account_alias(self, account, alias):
        """
        :param account:  an account object to be used as a selector
        :param alias:     email alias address
        :returns:         None (the API itself returns nothing)
        """
        self.request('RemoveAccountAlias', {
            'id': self._get_or_fetch_id(account, self.get_account),
            'alias': alias,
        })

    def mk_auth_token(self, account, admin=False, duration=0):
        """ Builds an authentification token, using preauth mechanism.

        See http://wiki.zimbra.com/wiki/Preauth

        :param duration: in seconds defaults to 0, which means "use account
               default"

        :param account: an account object to be used as a selector
        :returns:       the auth string
        """
        domain = account.get_domain()
        try:
            preauth_key = self.get_domain(domain)['zimbraPreAuthKey']
        except KeyError:
            raise DomainHasNoPreAuthKey(domain)
        timestamp = int(time.time())*1000
        expires = duration*1000
        return utils.build_preauth_str(preauth_key, account.name, timestamp,
                                       expires, admin)

    def delegate_auth(self, account):
        """ Uses the DelegateAuthRequest to provide a ZimbraAccountClient
        already logged with the provided account.

        It's the mechanism used with the "view email" button in admin console.
        """
        warnings.warn("delegate_auth() on parent client is deprecated,"
                      " use delegated_login() on child client instead",
                      DeprecationWarning)
        selector = account.to_selector()
        resp = self.request('DelegateAuth', {'account': selector})

        lifetime = resp['lifetime']
        authToken = resp['authToken']

        zc = ZimbraAccountClient(self._server_host)
        zc.login_with_authToken(authToken, lifetime)
        return zc

    def get_account_authToken(self, account=None, account_name=''):
        """ Use the DelegateAuthRequest to provide a token and his lifetime
        for the provided account.

        If account is provided we use it,
        else we retreive the account from the provided account_name.
        """
        if account is None:
            account = self.get_account(zobjects.Account(name=account_name))
        selector = account.to_selector()

        resp = self.request('DelegateAuth', {'account': selector})

        authToken = resp['authToken']
        lifetime = int(resp['lifetime'])

        return authToken, lifetime

    def delegated_login(self, *args, **kwargs):
        raise NotImplementedError(
            'zimbraAdmin do not support to get logged-in by delegated auth')

    def search_directory(self, **kwargs):
        """
        SearchAccount is deprecated, using SearchDirectory

        :param query: Query string - should be an LDAP-style filter
        string (RFC 2254)
        :param limit: The maximum number of accounts to return
        (0 is default and means all)
        :param offset: The starting offset (0, 25, etc)
        :param domain: The domain name to limit the search to
        :param applyCos: applyCos - Flag whether or not to apply the COS
        policy to account. Specify 0 (false) if only requesting attrs that
        aren't inherited from COS
        :param applyConfig: whether or not to apply the global config attrs to
        account. specify 0 (false) if only requesting attrs that aren't
        inherited from global config
        :param sortBy: Name of attribute to sort on. Default is the account
        name.
        :param types: Comma-separated list of types to return. Legal values
        are: accounts|distributionlists|aliases|resources|domains|coses
        (default is accounts)
        :param sortAscending: Whether to sort in ascending order. Default is
        1 (true)
        :param countOnly: Whether response should be count only. Default is
        0 (false)
        :param attrs: Comma-seperated list of attrs to return ("displayName",
        "zimbraId", "zimbraAccountStatus")
        :return: dict of list of "account" "alias" "dl" "calresource" "domain"
        "cos"
        """

        search_response = self.request('SearchDirectory', kwargs)

        result = {}
        items = {
            "account": zobjects.Account.from_dict,
            "domain": zobjects.Domain.from_dict,
            "dl": zobjects.DistributionList.from_dict,
            "cos": zobjects.COS.from_dict,
            "calresource": zobjects.CalendarResource.from_dict
            # "alias": TODO,
        }

        for obj_type, func in items.items():
            if obj_type in search_response:
                if isinstance(search_response[obj_type], list):
                    result[obj_type] = [
                        func(v) for v in search_response[obj_type]]
                else:
                    result[obj_type] = func(search_response[obj_type])
        return result


class ZimbraMailClient(ZimbraAbstractClient):
    """ Specialized Soap client to access zimbraMail webservice.

    API ref is
    http://files.zimbra.com/docs/soap_api/8.0.4/soap-docs-804/api-reference/zimbraMail/service-summary.html
    """
    NAMESPACE = 'urn:zimbraMail'
    LOCATION = 'service/soap'
    REST_PREAUTH = MailRESTClient

    def __init__(self, server_host, server_port='443', *args, **kwargs):
        super(ZimbraMailClient, self).__init__(
            server_host, server_port,
            *args, **kwargs)

    def _return_comma_list(self, l):
        """ get a list and return a string with comma separated list values
        Examples ['to', 'ta'] will return 'to,ta'.
        """
        if isinstance(l, (text_type, int)):
            return l

        if not isinstance(l, list):
            raise TypeError(l, ' should be a list of integers, \
not {0}'.format(type(l)))

        str_ids = ','.join(str(i) for i in l)

        return str_ids

    def is_session_valid(self):
        # zimbraMail do not have by itself an Auth request, so create a
        # zimbraAccount client for that check.
        zac = ZimbraAccountClient(self._server_host, self._server_port)
        zac._session.import_session(self._session.authToken)
        return zac.is_session_valid()

    def login(self, user, password):
        # !!! We need to authenticate with the 'urn:zimbraAccount' namespace
        self._session.login(user, password, 'urn:zimbraAccount')

    # Permissions
    def get_permissions(self, rights=[]):
        """
        :param rights: list of rights. Possible values : 'sendAs',
        'sendOnBehalfOf'
        :return: dict with key ace with a list of rights
        """
        aces = []
        if rights:
            for right in rights:
                ace = self.request(
                    'GetPermission',
                    {'ace': {
                        'right': {'_content': right}}})

                if 'ace' in ace.keys() and isinstance(ace, list):
                    aces.extend(ace['ace'])
                elif 'ace' in ace.keys() and isinstance(ace, dict):
                    aces.append(ace['ace'])
            return {'ace': aces}

        else:
            ace = self.request('GetPermission', {})
            if 'ace' in ace.keys() and isinstance(ace['ace'], list):
                return ace
            elif 'ace' in ace.keys() and isinstance(ace['ace'], dict):
                return ace
            else:
                return {'ace': []}

    def grant_permission(self, right, zid=None, grantee_name=None, gt='usr'):
        params = {'ace': {
            'gt': gt,
            'right': right
        }}

        if grantee_name:
            params['ace']['d'] = grantee_name
        elif zid:
            params['ace']['zid'] = zid
        else:
            raise TypeError('at least zid or grantee_name should be set')

        return self.request('GrantPermission', params)

    def revoke_permission(self, right, zid=None, grantee_name=None, gt='usr'):
        params = {'ace': {
            'gt': gt,
            'right': right
        }}

        if grantee_name:
            params['ace']['d'] = grantee_name
        elif zid:
            params['ace']['zid'] = zid
        else:
            raise TypeError('missing zid or grantee_name')

        self.request('RevokePermission', params)

    # Ranking action
    def reset_ranking(self):
        """Reset the contact ranking table for the account
        """
        self.request('RankingAction', {'action': {'op': 'reset'}})

    def delete_ranking(self, email):
        """Delete a specific address in the auto-completion of the users

        :param email: the address to remove
        """
        self.request('RankingAction', {'action': {'op': 'reset',
                                                  'email': email
                                                  }})

    # Task

    def create_task(self, subject, desc):
        """Create a task

        :param subject: the task's subject
        :param desc: the task's content in plain-text
        :returns: the task's id
        """
        task = zobjects.Task()
        task_creator = task.to_creator(subject, desc)
        resp = self.request('CreateTask', task_creator)
        task_id = resp['calItemId']
        return task_id

    def get_task(self, task_id):
        """Retrieve one task, discriminated by id.

        :param: task_id: the task id

        :returns: a zobjects.Task object ;
                  if no task is matching, returns None.
        """
        task = self.request_single('GetTask', {'id': task_id})

        if task:
            return zobjects.Task.from_dict(task)
        else:
            return None

    # Contact

    def create_contact(self, attrs, members=None, folder_id=None, tags=None):
        """Create a contact

        Does not include VCARD nor group membership yet

        XML example :
        <cn l="7> ## ContactSpec
            <a n="lastName">MARTIN</a>
            <a n="firstName">Pierre</a>
            <a n="email">pmartin@example.com</a>
        </cn>
        Which would be in zimsoap : attrs = { 'lastname': 'MARTIN',
                                        'firstname': 'Pierre',
                                        'email': 'pmartin@example.com' }
                                    folder_id = 7

        :param folder_id: a string of the ID's folder where to create
        contact. Default '7'
        :param tags:     comma-separated list of tag names
        :param attrs:   a dictionary of attributes to set ({key:value,...}). At
        least one attr is required
        :returns:       the created zobjects.Contact
        """
        cn = {}
        if folder_id:
            cn['l'] = str(folder_id)
        if tags:
            tags = self._return_comma_list(tags)
            cn['tn'] = tags
        if members:
            cn['m'] = members

        attrs = [{'n': k, '_content': v} for k, v in attrs.items()]
        cn['a'] = attrs
        resp = self.request_single('CreateContact', {'cn': cn})

        return zobjects.Contact.from_dict(resp)

    def get_contacts(self, ids=None, **kwargs):
        """ Get all contacts for the current user

        :param l: string of a folder id
        :param ids: An coma separated list of contact's ID to look for

        :returns: a list of zobjects.Contact
        """
        params = {}
        if ids:
            ids = self._return_comma_list(ids)
            params['cn'] = {'id': ids}

        for key, value in kwargs.items():
            if key in ['a', 'ma']:
                params[key] = {'n': value}
            else:
                params[key] = value

        contacts = self.request_list('GetContacts', params)

        return [zobjects.Contact.from_dict(i) for i in contacts]

    def modify_contact(self, contact_id, attrs=None, members=None, tags=None):
        """
        :param contact_id: zimbra id of the targetd contact
        :param attrs  : a dictionary of attributes to set ({key:value,...})
        :param members: list of dict representing contacts and
        operation (+|-|reset)
        :param tags:    comma-separated list of tag names
        :returns:       the modified zobjects.Contact
        """
        cn = {}
        if tags:
            tags = self._return_comma_list(tags)
            cn['tn'] = tags
        if members:
            cn['m'] = members
        if attrs:
            attrs = [{'n': k, '_content': v} for k, v in attrs.items()]
            cn['a'] = attrs

        cn['id'] = contact_id
        resp = self.request_single('ModifyContact', {'cn': cn})

        return zobjects.Contact.from_dict(resp)

    def delete_contacts(self, ids):
        """ Delete selected contacts for the current user

        :param ids: list of ids
        """

        str_ids = self._return_comma_list(ids)
        self.request('ContactAction', {'action': {'op': 'delete',
                                                  'id': str_ids}})

    def create_group(self, attrs, members, folder_id=None, tags=None):
        """Create a contact group

        XML example :
        <cn l="7> ## ContactSpec
            <a n="lastName">MARTIN</a>
            <a n="firstName">Pierre</a>
            <a n="email">pmartin@example.com</a>
        </cn>
        Which would be in zimsoap : attrs = { 'lastname': 'MARTIN',
                                        'firstname': 'Pierre',
                                        'email': 'pmartin@example.com' }
                                    folder_id = 7

        :param folder_id: a string of the ID's folder where to create
        contact. Default '7'
        :param tags:     comma-separated list of tag names
        :param members:  list of dict. Members with their type. Example
        {'type': 'I', 'value': 'manual_addresse@example.com'}.
        :param attrs:   a dictionary of attributes to set ({key:value,...}). At
        least one attr is required
        :returns:       the created zobjects.Contact
        """
        cn = {}
        cn['m'] = members

        if folder_id:
            cn['l'] = str(folder_id)
        if tags:
            cn['tn'] = tags

        attrs = [{'n': k, '_content': v} for k, v in attrs.items()]
        attrs.append({'n': 'type', '_content': 'group'})
        cn['a'] = attrs
        resp = self.request_single('CreateContact', {'cn': cn})

        return zobjects.Contact.from_dict(resp)

    # Folder

    def create_folder(self, name, parent_id='1'):
        params = {'folder': {
            'name': name,
            'l': parent_id
        }}

        return self.request('CreateFolder', params)['folder']

    def create_mountpoint(self, **kwargs):
        """ Create mountpoint according to attributes definied in soap
        documentation.
        """

        params = {'link': kwargs}

        return self.request('CreateMountpoint', params)['link']

    def delete_folders(self, paths=None, folder_ids=None, f_type='folder'):
        """
        :param folder_ids: list of ids
        :param path: list of folder's paths
        """
        if folder_ids:
            f_ids = folder_ids
        elif paths:
            f_ids = []
            for path in paths:
                folder = self.get_folder(path=path)
                f_ids.append(folder[f_type]['id'])

        comma_ids = self._return_comma_list(f_ids)

        params = {'action': {
            'id': comma_ids,
            'op': 'delete'
        }}

        self.request('FolderAction', params)

    def delete_mountpoints(self, paths=None, folder_ids=None):
        """
        :param folder_ids: list of ids
        :param path: list of folder's paths
        """
        self.delete_folders(paths=paths, folder_ids=folder_ids, f_type='link')

    def get_mountpoint(self, mp_id=None, path=None, uuid=None):
        return self.get_folder(f_id=mp_id, path=path, uuid=uuid)

    def get_folder(self, f_id=None, path=None, uuid=None):
        request = {'folder': {}}
        if f_id:
            request['folder']['l'] = str(f_id)
        if uuid:
            request['folder']['uuid'] = str(uuid)
        if path:
            request['folder']['path'] = str(path)

        return self.request('GetFolder', request)

    def get_folder_grant(self, **kwargs):
        folder = self.get_folder(**kwargs)
        if 'acl' in folder['folder']:
            return folder['folder']['acl']
        else:
            return None

    def modify_folder_grant(
        self,
        folder_ids,
        perm,
        zid=None,
        grantee_name=None,
        gt='usr',
        flags=None
    ):
        """
        :param folder_ids: list of ids
        :param perm: permission to grant to the user on folder(s)
        :param zid: id of user to grant rights
        :param grantee_name: email address of user to grant rights
        :param flags: folder's flags
        """
        f_ids = self._return_comma_list(folder_ids)

        params = {'action': {
            'id': f_ids,
            'op': 'grant',
            'grant': {'perm': perm, 'gt': gt}
        }}

        if perm == 'none':
            params['action']['op'] = '!grant'
            params['action']['zid'] = zid
            # Remove key to raise Zimsoap exception if no zid provided
            if not zid:
                params['action'].pop('zid', None)

        if grantee_name:
            params['action']['grant']['d'] = grantee_name
        elif zid:
            params['action']['grant']['zid'] = zid
        else:
            raise TypeError('missing zid or grantee_name')

        self.request('FolderAction', params)

    def modify_folders(
        self, folder_ids, color=None, flags=None, parent_folder=None,
        name=None, num_days=None, rgb=None, tags=None, view=None
    ):
        """
        :param folder_ids: list of ids
        :param color: color numeric; range 0-127; defaults to 0 if not present;
        client can display only 0-7
        :param flags: flags
        :param parent_folder: id of new location folder
        :param name: new name for the folder
        :param tags: list of tag names
        :param view: list of tag view
        """
        f_ids = self._return_comma_list(folder_ids)

        params = {'action': {
            'id': f_ids,
            'op': 'update',
        }}

        if color:
            params['action']['color'] = color
        if flags:
            params['action']['f'] = flags
        if parent_folder:
            params['action']['l'] = parent_folder
        if name:
            params['action']['name'] = name
        if tags:
            tn = self._return_comma_list(tags)
            params['action']['tn'] = tn
        if view:
            params['action']['view'] = view

        self.request('FolderAction', params)

    # Conversation

    def get_conversation(self, conv_id, **kwargs):
        content = {'c': kwargs}
        content['c']['id'] = int(conv_id)

        return self.request('GetConv', content)

    def delete_conversations(self, ids):
        """ Delete selected conversations

        :params ids: list of ids
        """

        str_ids = self._return_comma_list(ids)
        self.request('ConvAction', {'action': {'op': 'delete',
                                               'id': str_ids
                                               }})

    def move_conversations(self, ids, folder):
        """ Move selected conversations to an other folder

        :params ids: list of ids
        :params folder: folder id
        """

        str_ids = self._return_comma_list(ids)
        self.request('ConvAction', {'action': {'op': 'move',
                                               'id': str_ids,
                                               'l': str(folder)}})

    # Messages

    def add_message(self, msg_content, folder, **kwargs):
        """ Inject a message

        :params string msg_content: The entire message's content.
        :params string folder: Folder pathname (starts with '/') or folder ID
        """
        content = {'m': kwargs}
        content['m']['l'] = str(folder)
        content['m']['content'] = {'_content': msg_content}

        return self.request('AddMsg', content)

    def get_message(self, msg_id, **kwargs):
        content = {'m': kwargs}
        content['m']['id'] = str(msg_id)

        return self.request('GetMsg', content)

    def move_messages(self, ids, folder_id):
        """ Move selected messages to an other folder

        :param msg_ids: list of message's ids to move
        :param folder_id: folder's id where to move messages
        """
        str_ids = self._return_comma_list(ids)
        params = {'action': {
            'id': str_ids,
            'op': 'move',
            'l': folder_id
        }}

        self.request('MsgAction', params)

    def update_messages_flag(self, ids, flag):
        """
        List of flags :
        u -> unread                 f -> flagged
        a -> has attachment         s -> sent by me
        r -> replied                w -> forwarded
        d -> draft                  x -> deleted
        n -> notification sent

        by default a message priority is "normal" otherwise:
        ! -> priority high          ? -> priority low
        """
        str_ids = self._return_comma_list(ids)
        params = {'action': {
            'id': str_ids,
            'op': 'update',
            'f': flag
        }}

        self.request('MsgAction', params)

    def delete_messages(self, ids):
        """ Delete selected messages for the current user

        :param ids: list of ids
        """
        str_ids = self._return_comma_list(ids)
        return self.request('MsgAction', {'action': {'op': 'delete',
                                                     'id': str_ids}})

    # Search
    def search(self, query, **kwargs):
        """ Search object in account

        :returns: a dic where value c contains the list of results (if there
        is any). Example : {
            'more': '0',
            'offset': '0',
            'sortBy': 'dateDesc',
            'c': [
                {
                    'id': '-261',
                    'm': {'id': '261',
                          's': '2556',
                          'l': '2'},
                    'u': '0', 'd': '1450714720000',
                    'sf': '1450714720000',
                    'e': {'t': 'f',
                          'd': 'kokopu',
                          'a': 'kokopu@zimbratest3.example.com'},
                    'n': '1',
                    'fr': {'_content': 'Hello there !'},
                    'su': {'_content': 'The subject is cool'}
                }
            ]
        """

        content = kwargs
        content['query'] = {'_content': query}

        return self.request('Search', content)

    # DataSource

    def create_data_source(self, data_source, dest_folder):
        """ Create data source from a dict
        data_source example =
        {
            'pop3': {
                'leaveOnServer': "(0|1)", 'id': 'data-source-id',
                'name': 'data-source-name',
                'isEnabled': '(0|1)', 'importOnly': '(0|1)',
                'host': 'data-source-server', 'port': 'data-source-port',
                'connectionType': '(cleartext|ssl|tls|tls_is_available)',
                'username': 'data-source-username',
                'password': 'data-source-password',
                'emailAddress': 'data-source-address',
                'useAddressForForwardReply': '(0|1)',
                'defaultSignature': 'default-signature-id',
                'forwardReplySignature': 'forward-reply-signature-id',
                'fromDisplay': 'data-source-from-display',
                'replyToAddress': 'data-source-replyto-address',
                'replyToDisplay': 'data-source-replyto-display',
                'importClass': 'data-import-class',
                'failingSince': 'data-source-failing-since'
            }
        }
        """
        folder = self.create_folder(dest_folder)
        for type_source, source_config in data_source.items():
            data_source[type_source]['l'] = folder['id']
        return self.request('CreateDataSource', data_source)

    def get_data_sources(self, types=[], source_addresses=[], source_id=None):
        all_data_sources = self.request('GetDataSources')

        data_sources = {}
        if types and source_addresses:
            for t in types:
                data_sources = {t: []}
                if t in all_data_sources and isinstance(all_data_sources[t],
                                                        list):
                    for data_source in all_data_sources[t]:
                        if data_source['emailAddress'] in source_addresses:
                            data_sources[t].append(data_source)
                elif t in all_data_sources and isinstance(all_data_sources[t],
                                                          dict):
                    if all_data_sources[t]['emailAddress'] in source_addresses:
                        data_sources[t].append(all_data_sources[t])

        elif types and not source_addresses:
            for t in types:
                data_sources = {t: []}
                if t in all_data_sources and isinstance(all_data_sources[t],
                                                        list):
                    for data_source in all_data_sources[t]:
                        data_sources[t].append(data_source)
                elif t in all_data_sources and isinstance(all_data_sources[t],
                                                          dict):
                    data_sources[t].append(all_data_sources[t])

        elif source_addresses and not types:
            for t in all_data_sources.keys():
                if isinstance(all_data_sources[t], list):
                    for data_source in all_data_sources[t]:
                        if data_source['emailAddress'] in source_addresses:
                            try:
                                data_sources[t].append(data_source)
                            except KeyError:
                                data_sources = {t: []}
                                data_sources[t].append(data_source)
                elif isinstance(all_data_sources[t], dict):
                    if all_data_sources[t]['emailAddress'] in source_addresses:
                        try:
                            data_sources[t].append(all_data_sources[t])
                        except KeyError:
                            data_sources = {t: []}
                            data_sources[t].append(all_data_sources[t])

        elif source_id:
            for t in all_data_sources.keys():
                data_sources = {t: []}
                if isinstance(all_data_sources[t], list):
                    for data_source in all_data_sources[t]:
                        if data_source['id'] == source_id:
                            data_sources[t].append(data_source)
                elif isinstance(all_data_sources[t], dict):
                    if all_data_sources[t]['id'] == source_id:
                        data_sources[t].append(all_data_sources[t])

        else:
            return all_data_sources

        return data_sources

    def modify_data_source(self, data_source):
        """ Modify data source from a dict
        data_source example =
        {
            'pop3': {
                'leaveOnServer': "(0|1)", 'id': 'data-source-id',
                'name': 'data-source-name', 'l': 'data-source-folder-id',
                'isEnabled': '(0|1)', 'importOnly': '(0|1)',
                'host': 'data-source-server', 'port': 'data-source-port',
                'connectionType': '(cleartext|ssl|tls|tls_is_available)',
                'username': 'data-source-username',
                'password': 'data-source-password',
                'emailAddress': 'data-source-address',
                'useAddressForForwardReply': '(0|1)',
                'defaultSignature': 'default-signature-id',
                'forwardReplySignature': 'forward-reply-signature-id',
                'fromDisplay': 'data-source-from-display',
                'replyToAddress': 'data-source-replyto-address',
                'replyToDisplay': 'data-source-replyto-display',
                'importClass': 'data-import-class',
                'failingSince': 'data-source-failing-since'
            }
        }
        """
        return self.request('ModifyDataSource', data_source)

    def delete_data_source(self, data_source):
        """
        Delete data source with it's name or ID.
        data_source = { 'imap': {'name': 'data-source-name'}}
        or
        data_source = { 'pop3': {'id': 'data-source-id'}}
        """
        source_type = [k for k in data_source.keys()][0]
        complete_source = self.get_data_sources(
            source_id=data_source[source_type]['id'])
        folder_id = complete_source[source_type][0]['l']
        self.delete_folders(folder_ids=[folder_id])
        return self.request('DeleteDataSource', data_source)

    # Filter

    def add_filter_rule(
            self, name, condition, filters, actions, active=1, way='in'):
        """
        :param: name filter name
        :param: condition allof or anyof
        :param: filters dict of filters
        :param: actions dict of actions
        :param: way string discribing if filter is for 'in' or 'out' messages
        :returns: list of user's zobjects.FilterRule
        """

        filters['condition'] = condition

        new_rule = {
            'name': name,
            'active': active,
            'filterTests': filters,
            'filterActions': actions
        }

        new_rules = [zobjects.FilterRule.from_dict(new_rule)]

        prev_rules = self.get_filter_rules(way=way)

        # if there is already some rules
        if prev_rules:
            for rule in prev_rules:
                # don't add rule if it already exist
                if rule.name == new_rules[0].name:
                    raise ZimSOAPException(
                        'filter %s already exists' % rule.name)
            new_rules = new_rules + prev_rules

        content = {
            'filterRules': {
                'filterRule': [r._full_data for r in new_rules]
            }
        }
        if way == 'in':
            self.request('ModifyFilterRules', content)
        elif way == 'out':
            self.request('ModifyOutgoingFilterRules', content)
        return new_rules

    def get_filter_rule(self, _filter, way='in'):
        """ Return the filter rule

        :param: _filter a zobjects.FilterRule or the filter name
        :param: way string discribing if filter is for 'in' or 'out' messages
        :returns: a zobjects.FilterRule"""
        if isinstance(_filter, zobjects.FilterRule):
            _filter = _filter.name
        for f in self.get_filter_rules(way=way):
            if f.name == _filter:
                return f
        return None

    def get_filter_rules(self, way='in'):
        """
        :param: way string discribing if filter is for 'in' or 'out' messages
        :returns: list of zobjects.FilterRule
        """
        try:
            if way == 'in':
                filters = self.request(
                    'GetFilterRules')['filterRules']['filterRule']
            elif way == 'out':
                filters = self.request(
                    'GetOutgoingFilterRules')['filterRules']['filterRule']

            # Zimbra return a dict if there is only one instance
            if isinstance(filters, dict):
                filters = [filters]

            return [zobjects.FilterRule.from_dict(f) for f in filters]
        except KeyError:
            return []

    def apply_filter_rule(self, _filter, query='in:inbox', way='in'):
        """
        :param: _filter _filter a zobjects.FilterRule or the filter name
        :param: query on what will the filter be applied
        :param: way string discribing if filter is for 'in' or 'out' messages
        :returns: list of impacted message's ids
        """
        if isinstance(_filter, zobjects.FilterRule):
            _filter = _filter.name

        content = {
            'filterRules': {
                'filterRule': {'name': _filter}
                },
            'query': {'_content': query}
        }
        if way == 'in':
            ids = self.request('ApplyFilterRules', content)
        elif way == 'out':
            ids = self.request('ApplyOutgoingFilterRules', content)

        if ids:
            return [int(m) for m in ids['m']['ids'].split(',')]
        else:
            return []

    def delete_filter_rule(self, _filter, way='in'):
        """ delete a filter rule

        :param: _filter a zobjects.FilterRule or the filter name
        :param: way string discribing if filter is for 'in' or 'out' messages
        :returns: a list of zobjects.FilterRule
        """
        updated_rules = []
        rules = self.get_filter_rules(way=way)

        if isinstance(_filter, zobjects.FilterRule):
            _filter = _filter.name

        if rules:
            for rule in rules:
                if not rule.name == _filter:
                    updated_rules.append(rule)

        if rules != updated_rules:
            content = {
                'filterRules': {
                    'filterRule': [f._full_data for f in updated_rules]
                }
            }
            if way == 'in':
                self.request('ModifyFilterRules', content)
            elif way == 'out':
                self.request('ModifyOutgoingFilterRules', content)

        return updated_rules


class ZimbraAPISession:
    """Handle the login, the session expiration and the generation of the
       authentification header.
    """
    def __init__(self, client):
        self.client = client
        self.authToken = None

    def set_end_date(self, lifetime):
        """Computes and store an absolute end_date session according to the
        lifetime of the session"""
        self.end_date = (datetime.datetime.now() +
                         datetime.timedelta(0, lifetime))

    def login(self, username, password, namespace=None):
        """ Performs the login against zimbra
        (sends AuthRequest, receives AuthResponse).

        :param namespace: if specified, the namespace used for authetication
                         (if the client namespace is not suitable for
                         authentication).
        """

        if namespace is None:
            namespace = self.client.NAMESPACE

        data = self.client.request(
            'Auth',
            {
                'account': zobjects.Account(name=username).to_selector(),
                'password': {'_content': password}
            },
            namespace)
        self.authToken = data['authToken']
        lifetime = int(data['lifetime'])

        self.authToken = str(self.authToken)
        self.set_end_date(lifetime)

    def import_session(self, auth_token):
        if not isinstance(auth_token, (binary_type, text_type)):
            raise TypeError('auth_token should be a string, not {0}'.format(
                type(auth_token)))
        self.authToken = auth_token

    def is_logged_in(self, force_check=False):
        if not self.authToken:
            return False

        # if it's logged-in by preauth, we can't know the exp. date for sure
        try:
            return self.end_date >= datetime.datetime.now()
        except AttributeError:
            return True

    def is_session_valid(self):
        try:
            self.client.request('Auth',
                                {'authToken': {'_content': self.authToken}})
            return True
        except ZimbraSoapServerError:
            return False
