Ejemplo n.º 1
0
    def __init__(self, conf):
        """Initzialize a new instance of out state-checker.

        :param logger logger: The logger to use.
        :param dict conf: Our StateCheckers configuration.
        """
        self.db = Factory.get('Database')(client_encoding='UTF-8')
        self.co = Factory.get('Constants')(self.db)
        self.ac = Factory.get('Account')(self.db)
        self.pe = Factory.get('Person')(self.db)
        self.gr = Factory.get('Group')(self.db)
        self.et = Factory.get('EmailTarget')(self.db)
        self.eq = EmailQuota(self.db)
        self.ea = EmailAddress(self.db)
        self.ef = EmailForward(self.db)
        self.cu = CerebrumUtils()

        self.config = conf

        self._ldap_page_size = 1000

        self._cache_randzone_users = self._populate_randzone_cache(
            self.config['randzone_unreserve_group'])
        self._cache_accounts = self._populate_account_cache(
            self.co.spread_exchange_account)
        self._cache_addresses = self._populate_address_cache()
        self._cache_local_delivery = self._populate_local_delivery_cache()
        self._cache_forwards = self._populate_forward_cache()
        self._cache_quotas = self._populate_quota_cache()
        self._cache_targets = self._populate_target_cache()
        self._cache_names = self._populate_name_cache()
        self._cache_group_names = self._populate_group_name_cache()
        self._cache_no_reservation = self._populate_no_reservation_cache()
        self._cache_primary_accounts = self._populate_primary_account_cache()
Ejemplo n.º 2
0
 def __init__(self):
     """Initialize the Utils."""
     self.db = Factory.get('Database')(client_encoding='UTF-8')
     self.en = Factory.get('Entity')(self.db)
     self.ac = Factory.get('Account')(self.db)
     self.pe = Factory.get('Person')(self.db)
     self.gr = Factory.get('Group')(self.db)
     self.co = Factory.get('Constants')(self.db)
     self.ed = EmailDomain(self.db)
     self.eq = EmailQuota(self.db)
     self.ef = EmailForward(self.db)
     self.et = Factory.get('EmailTarget')(self.db)
     self.dg = DistributionGroup(self.db)
    def __init__(self, conf):
        """Initzialize a new instance of out state-checker.

        :param logger logger: The logger to use.
        :param dict conf: Our StateCheckers configuration.
        """
        self.db = Factory.get('Database')(client_encoding='UTF-8')
        self.co = Factory.get('Constants')(self.db)
        self.ac = Factory.get('Account')(self.db)
        self.pe = Factory.get('Person')(self.db)
        self.gr = Factory.get('Group')(self.db)
        self.et = Factory.get('EmailTarget')(self.db)
        self.eq = EmailQuota(self.db)
        self.ea = EmailAddress(self.db)
        self.ef = EmailForward(self.db)
        self.cu = CerebrumUtils()

        self.config = conf

        self._ldap_page_size = 1000

        self._cache_randzone_users = self._populate_randzone_cache(
            self.config['randzone_unreserve_group'])
        self._cache_accounts = self._populate_account_cache(
            self.co.spread_exchange_account)
        self._cache_addresses = self._populate_address_cache()
        self._cache_local_delivery = self._populate_local_delivery_cache()
        self._cache_forwards = self._populate_forward_cache()
        self._cache_quotas = self._populate_quota_cache()
        self._cache_targets = self._populate_target_cache()
        self._cache_names = self._populate_name_cache()
        self._cache_group_names = self._populate_group_name_cache()
        self._cache_no_reservation = self._populate_no_reservation_cache()
        self._cache_primary_accounts = self._populate_primary_account_cache()
Ejemplo n.º 4
0
def process_servers(server_type, except_re):
    es = EmailServer(db)
    eq = EmailQuota(db)

    existing_servers = {}

    # This lists all hosts, so we need to filter on server_type later.
    for row in es.list_traits(co.trait_email_server_weight):
        existing_servers[int(row['entity_id'])] = True

    assigned = {}
    for row in es.list_email_server_ext(server_type=server_type):
        # logger.debug("Processing %r" % row.dict())
        if except_re and except_re.match(row['name']):
            logger.debug("Skipping server named '%s'" % row['name'])
            continue
        srv = int(row['server_id'])
        assigned[srv] = eq.get_quota_stats_by_server(srv)['total_quota'] or 0
        logger.debug("%s has assigned quota %d" % (row['name'], assigned[srv]))
    max_weight = max(assigned.values()) * 110 / 100

    for srv in assigned:
        es.clear()
        es.find(srv)
        # We add 1 to handle the case with only one server
        weight = max_weight - assigned[srv] + 1
        logger.debug("Assigning weight %d to server ID %d" % (weight, srv))
        es.populate_trait(co.trait_email_server_weight, numval=weight)
        es.write_db()
        if srv in existing_servers:
            del existing_servers[srv]

    for obsolete in existing_servers:
        es.clear()
        es.find(obsolete)
        if server_type and es.email_server_type != server_type:
            continue
        logger.info("Deleting old weight trait for %s" % es.name)
        es.delete_trait(co.trait_email_server_weight)
Ejemplo n.º 5
0
def process_servers(server_type, except_re):
    es = EmailServer(db)
    eq = EmailQuota(db)

    existing_servers = {}

    # This lists all hosts, so we need to filter on server_type later.
    for row in es.list_traits(co.trait_email_server_weight):
        existing_servers[int(row['entity_id'])] = True

    assigned = {}
    for row in es.list_email_server_ext(server_type=server_type):
        # logger.debug("Processing %r" % row.dict())
        if except_re and except_re.match(row['name']):
            logger.debug("Skipping server named '%s'" % row['name'])
            continue
        srv = int(row['server_id'])
        assigned[srv] = eq.get_quota_stats_by_server(srv)['total_quota'] or 0
        logger.debug("%s has assigned quota %d" % (row['name'], assigned[srv]))
    max_weight = max(assigned.values()) * 110 / 100

    for srv in assigned:
        es.clear()
        es.find(srv)
        # We add 1 to handle the case with only one server
        weight = max_weight - assigned[srv] + 1
        logger.debug("Assigning weight %d to server ID %d" % (weight, srv))
        es.populate_trait(co.trait_email_server_weight, numval=weight)
        es.write_db()
        if srv in existing_servers:
            del existing_servers[srv]

    for obsolete in existing_servers:
        es.clear()
        es.find(obsolete)
        if server_type and es.email_server_type != server_type:
            continue
        logger.info("Deleting old weight trait for %s" % es.name)
        es.delete_trait(co.trait_email_server_weight)
Ejemplo n.º 6
0
 def __init__(self):
     """Initialize the Utils."""
     self.db = Factory.get('Database')(client_encoding='UTF-8')
     self.en = Factory.get('Entity')(self.db)
     self.ac = Factory.get('Account')(self.db)
     self.pe = Factory.get('Person')(self.db)
     self.gr = Factory.get('Group')(self.db)
     self.co = Factory.get('Constants')(self.db)
     self.ed = EmailDomain(self.db)
     self.eq = EmailQuota(self.db)
     self.ef = EmailForward(self.db)
     self.et = Factory.get('EmailTarget')(self.db)
     self.dg = DistributionGroup(self.db)
Ejemplo n.º 7
0
class StateChecker(object):
    """Wrapper class for state-checking functions.

    The StateChecker class wraps all the functions we need in order to
    verify and report deviances between Cerebrum and Exchange.
    """

    # Connect params
    LDAP_RETRY_DELAY = 60
    LDAP_RETRY_MAX = 5

    # Search and result params
    LDAP_COM_DELAY = 30
    LDAP_COM_MAX = 3

    def __init__(self, conf):
        """Initzialize a new instance of out state-checker.

        :param logger logger: The logger to use.
        :param dict conf: Our StateCheckers configuration.
        """
        self.db = Factory.get('Database')(client_encoding='UTF-8')
        self.co = Factory.get('Constants')(self.db)
        self.ac = Factory.get('Account')(self.db)
        self.pe = Factory.get('Person')(self.db)
        self.gr = Factory.get('Group')(self.db)
        self.et = Factory.get('EmailTarget')(self.db)
        self.eq = EmailQuota(self.db)
        self.ea = EmailAddress(self.db)
        self.ef = EmailForward(self.db)
        self.cu = CerebrumUtils()

        self.config = conf

        self._ldap_page_size = 1000

        self._cache_randzone_users = self._populate_randzone_cache(
            self.config['randzone_unreserve_group'])
        self._cache_accounts = self._populate_account_cache(
            self.co.spread_exchange_account)
        self._cache_addresses = self._populate_address_cache()
        self._cache_local_delivery = self._populate_local_delivery_cache()
        self._cache_forwards = self._populate_forward_cache()
        self._cache_quotas = self._populate_quota_cache()
        self._cache_targets = self._populate_target_cache()
        self._cache_names = self._populate_name_cache()
        self._cache_group_names = self._populate_group_name_cache()
        self._cache_no_reservation = self._populate_no_reservation_cache()
        self._cache_primary_accounts = self._populate_primary_account_cache()

    def init_ldap(self):
        """Initzialize LDAP connection."""
        self.ldap_srv = ldap.ldapobject.ReconnectLDAPObject(
            '%s://%s/' %
            (self.config['ldap_proto'], self.config['ldap_server']),
            retry_max=self.LDAP_RETRY_MAX,
            retry_delay=self.LDAP_RETRY_DELAY)

        usr = self.config['ldap_user'].split('\\')[1]
        self.ldap_srv.bind_s(self.config['ldap_user'],
                             read_password(usr, self.config['ldap_server']))

        self.ldap_lc = ldap.controls.SimplePagedResultsControl(
            True, self._ldap_page_size, '')

    def _searcher(self, ou, scope, attrs, ctrls):
        """ Perform ldap.search(), but retry in the event of an error.

        This wraps the search with error handling, so that the search is
        repeated with a delay between attempts.
        """
        for attempt in itertools.count(1):
            try:
                return self.ldap_srv.search_ext(ou,
                                                scope,
                                                attrlist=attrs,
                                                serverctrls=ctrls)
            except ldap.LDAPError as e:
                if attempt < self.LDAP_COM_MAX:
                    logger.debug('Caught %r in _searcher on attempt %d', e,
                                 attempt)
                    time.sleep(self.LDAP_COM_DELAY)
                    continue
                raise

    def _recvr(self, msgid):
        """ Perform ldap.result3(), but retry in the event of an error.

        This wraps the result fetching with error handling, so that the fetch
        is repeated with a delay between attempts.

        It also decodes all attributes and attribute text values.
        """
        for attempt in itertools.count(1):
            try:
                # return self.ldap_srv.result3(msgid)
                rtype, rdata, rmsgid, sc = self.ldap_srv.result3(msgid)
                return rtype, decode_attrs(rdata), rmsgid, sc
            except ldap.LDAPError as e:
                if attempt < self.LDAP_COM_MAX:
                    logger.debug('Caught %r in _recvr on attempt %d', e,
                                 attempt)
                    time.sleep(self.LDAP_COM_DELAY)
                    continue
                raise

    def search(self, ou, attrs, scope=ldap.SCOPE_SUBTREE):
        """Wrapper for the search- and result-calls.

        Implements paged searching.

        :param str ou: The OU to search in.
        :param list attrs: The attributes to fetch.
        :param int scope: Our search scope, default is subtree.
        """
        # Implementing paging, taken from
        # http://www.novell.com/coolsolutions/tip/18274.html
        msgid = self._searcher(ou, scope, attrs, [self.ldap_lc])

        data = []

        ctrltype = ldap.controls.SimplePagedResultsControl.controlType
        while True:
            time.sleep(1)
            rtype, rdata, rmsgid, sc = self._recvr(msgid)
            data.extend(rdata)
            pctrls = [c for c in sc if c.controlType == ctrltype]
            if pctrls:
                cookie = pctrls[0].cookie
                if cookie:
                    self.ldap_lc.cookie = cookie
                    time.sleep(1)
                    msgid = self._searcher(ou, scope, attrs, [self.ldap_lc])
                else:
                    break
            else:
                logger.warn('Server ignores RFC 2696 control.')
                break
        return data[1:]

    def close(self):
        """Close the LDAP connection."""
        self.ldap_srv.unbind_s()

    #
    # Various cache-generating functions.
    #

    def _populate_randzone_cache(self, randzone):
        self.gr.clear()
        self.gr.find_by_name(randzone)
        u = text_decoder(self.db.encoding)
        return [
            u(x['name']) for x in self.cu.get_group_members(self.gr.entity_id)
        ]

    def _populate_account_cache(self, spread):
        u = text_decoder(self.db.encoding)

        def to_dict(row):
            d = dict(row)
            d['name'] = u(row['name'])
            d['description'] = u(row['description'])
            return d

        return [to_dict(r) for r in self.ac.search(spread=spread)]

    def _populate_address_cache(self):
        u = text_decoder(self.db.encoding)
        tmp = defaultdict(list)

        # TODO: Implement fetchall?
        for addr in self.ea.list_email_addresses_ext():
            tmp[addr['target_id']].append(
                u'%s@%s' % (u(addr['local_part']), u(addr['domain'])))
        return dict(tmp)

    def _populate_local_delivery_cache(self):
        r = {}
        for ld in self.ef.list_local_delivery():
            r[ld['target_id']] = ld['local_delivery']
        return r

    def _populate_forward_cache(self):
        u = text_decoder(self.db.encoding)
        tmp = defaultdict(list)

        for fwd in self.ef.list_email_forwards():
            if fwd['enable'] == 'T':
                tmp[fwd['target_id']].append(u(fwd['forward_to']))
        return dict(tmp)

    def _populate_quota_cache(self):
        tmp = defaultdict(dict)

        # TODO: Implement fetchall?
        for quota in self.eq.list_email_quota_ext():
            tmp[quota['target_id']]['soft'] = quota['quota_soft']
            tmp[quota['target_id']]['hard'] = quota['quota_hard']
        return dict(tmp)

    def _populate_target_cache(self):
        u = text_decoder(self.db.encoding)
        tmp = defaultdict(dict)
        for targ in self.et.list_email_target_primary_addresses(
                target_type=self.co.email_target_account):
            tmp[targ['target_entity_id']]['target_id'] = targ['target_id']
            tmp[targ['target_entity_id']]['primary'] = \
                u'%s@%s' % (u(targ['local_part']), u(targ['domain']))
        return dict(tmp)

    def _populate_name_cache(self):
        u = text_decoder(self.db.encoding)
        tmp = defaultdict(dict)
        for name in self.pe.search_person_names(
                name_variant=[
                    self.co.name_first, self.co.name_last, self.co.name_full
                ],
                source_system=self.co.system_cached):
            tmp[name['person_id']][name['name_variant']] = u(name['name'])
        return dict(tmp)

    def _populate_group_name_cache(self):
        u = text_decoder(self.db.encoding)
        tmp = {}
        for eid, dom, name in self.gr.list_names(self.co.group_namespace):
            tmp[eid] = u(name)
        return tmp

    def _populate_no_reservation_cache(self):
        unreserved = []
        for r in self.pe.list_traits(self.co.trait_public_reservation,
                                     fetchall=True):
            if r['numval'] == 0:
                unreserved.append(r['entity_id'])
        return unreserved

    def _populate_primary_account_cache(self):
        primary = []
        for acc in self.ac.list_accounts_by_type(primary_only=True):
            primary.append(acc['account_id'])
        return primary

    ###
    # Mailbox related state fetching & comparison
    ###

    def collect_cerebrum_mail_info(self):
        """Collect E-mail related information from Cerebrum.

        :rtype: dict
        :return: A dict of users attributes. Uname is key.
        """
        res = {}
        for acc in self._cache_accounts:
            tmp = {}
            try:
                tid = self._cache_targets[acc['account_id']]['target_id']
            except KeyError:
                logger.warn(
                    'Could not find account with id:%d in list '
                    'of targets, skipping..', acc['account_id'])
                continue
            # Fetch addresses
            tmp[u'EmailAddresses'] = sorted(self._cache_addresses[tid])
            # Fetch primary address
            tmp[u'PrimaryAddress'] = \
                self._cache_targets[acc['account_id']]['primary']

            # Fetch names
            if acc['owner_type'] == self.co.entity_person:
                tmp[u'FirstName'] = \
                    self._cache_names[acc['owner_id']][int(self.co.name_first)]
                tmp[u'LastName'] = \
                    self._cache_names[acc['owner_id']][int(self.co.name_last)]
                tmp[u'DisplayName'] = \
                    self._cache_names[acc['owner_id']][int(self.co.name_full)]
            else:
                fn, ln, dn = self.cu.construct_group_names(
                    acc['name'],
                    self._cache_group_names.get(acc['owner_id'], None))
                tmp[u'FirstName'] = fn
                tmp[u'LastName'] = ln
                tmp[u'DisplayName'] = dn

            # Fetch quotas
            hard = self._cache_quotas[tid]['hard'] * 1024
            soft = self._cache_quotas[tid]['soft']

            tmp[u'ProhibitSendQuota'] = str(hard)
            tmp[u'ProhibitSendReceiveQuota'] = str(hard)
            tmp[u'IssueWarningQuota'] = str(int(hard * soft / 100.))

            # Randzone users will always be shown. This overrides everything
            # else.
            if acc['name'] in self._cache_randzone_users:
                hide = False
            elif acc['owner_id'] in self._cache_no_reservation and \
                    acc['account_id'] in self._cache_primary_accounts:
                hide = False
            else:
                hide = True

            tmp[u'HiddenFromAddressListsEnabled'] = hide

            # Collect local delivery status
            tmp[u'DeliverToMailboxAndForward'] = \
                self._cache_local_delivery.get(tid, False)

            # Collect forwarding address
            # We do this by doing a difference operation on the forwards and
            # the addresses, so we only end up with "external" addresses.
            s_fwds = set(self._cache_forwards.get(tid, []))
            s_addrs = set(self._cache_addresses.get(tid, []))
            ext_fwds = list(s_fwds - s_addrs)
            if ext_fwds:
                tmp[u'ForwardingSmtpAddress'] = ext_fwds[0]
            else:
                tmp[u'ForwardingSmtpAddress'] = None

            res[acc['name']] = tmp

        return res

    def collect_exchange_mail_info(self, mb_ou):
        """Collect mailbox-information from Exchange, via LDAP.

        :param str mb_ou: The OrganizationalUnit to search for mailboxes.
        :rtype: dict
        :return: A dict with the mailboxes attributes. The key is the account
            name.
        """
        attrs = [
            'proxyAddresses', 'displayName', 'givenName', 'sn',
            'msExchHideFromAddressLists', 'extensionAttribute1',
            'mDBUseDefaults', 'mDBOverQuotaLimit', 'mDBOverHardQuotaLimit',
            'mDBStorageQuota', 'deliverAndRedirect',
            'msExchGenericForwardingAddress'
        ]

        r = self.search(mb_ou, attrs)
        ret = {}
        for cn, data in r:
            if 'extensionAttribute1'in data and \
                    data['extensionAttribute1'] == ['not migrated'] or \
                    'ExchangeActiveSyncDevices' in cn:
                continue
            tmp = {}
            name = cn[3:].split(',')[0]
            for key in data:
                if key == 'proxyAddresses':
                    addrs = []
                    for addr in data[key]:
                        if addr.startswith('SMTP:'):
                            tmp[u'PrimaryAddress'] = addr[5:]
                        addrs.append(addr[5:])
                    tmp[u'EmailAddresses'] = sorted(addrs)
                elif key == 'displayName':
                    tmp[u'DisplayName'] = data[key][0]
                elif key == 'givenName':
                    tmp[u'FirstName'] = data[key][0]
                elif key == 'sn':
                    tmp[u'LastName'] = data[key][0]
                elif key == 'mDBUseDefaults':
                    tmp[u'UseDatabaseQuotaDefaults'] = (True if data[key][0]
                                                        == 'TRUE' else False)
                elif key == 'mDBOverQuotaLimit':
                    q = data[key][0]
                    tmp[u'ProhibitSendQuota'] = q
                elif key == 'mDBOverHardQuotaLimit':
                    q = data[key][0]
                    tmp[u'ProhibitSendReceiveQuota'] = q
                elif key == 'mDBStorageQuota':
                    q = data[key][0]
                    tmp[u'IssueWarningQuota'] = q

            # Non-existent attribute means that the value is false. Fuckers.
            # Collect status about if the mbox is hidden or not
            tmp[u'HiddenFromAddressListsEnabled'] = False
            if 'msExchHideFromAddressLists' in data:
                val = (True if data['msExchHideFromAddressLists'][0] == 'TRUE'
                       else False)
                tmp[u'HiddenFromAddressListsEnabled'] = val

            # Collect local delivery status
            tmp[u'DeliverToMailboxAndForward'] = False
            if 'deliverAndRedirect' in data:
                val = (True
                       if data['deliverAndRedirect'][0] == 'TRUE' else False)
                tmp[u'DeliverToMailboxAndForward'] = val

            # Collect forwarding address
            tmp[u'ForwardingSmtpAddress'] = None
            if 'msExchGenericForwardingAddress' in data:
                val = data['msExchGenericForwardingAddress'][0]
                # We split of smtp:, and store
                tmp[u'ForwardingSmtpAddress'] = val.split(':')[1]

            ret[name] = tmp
        return ret

    def compare_mailbox_state(self, ex_state, ce_state, state, config):
        """Compare the information fetched from Cerebrum and Exchange.

        This method produces a dict with the state between the systems,
        and a report that will be sent to the appropriate target system
        administrators.

        :param dict ex_state: The state in Exchange.
        :param dict ce_state: The state in Cerebrum.
        :param dict state: The previous state generated by this method.
        :param dict config: Configuration of reporting delays for various
            attributes.
        :rtype: tuple
        :return: A tuple consisting of the new difference-state and a
            human-readable report of differences.
        """
        s_ce_keys = set(ce_state.keys())
        s_ex_keys = set(ex_state.keys())
        diff_mb = {}
        diff_stale = {}
        diff_new = {}

        ##
        # Populate some structures with information we need

        # Mailboxes in Exchange, but not in Cerebrum
        stale_keys = list(s_ex_keys - s_ce_keys)
        for ident in stale_keys:
            if state and ident in state['stale_mb']:
                diff_stale[ident] = state['stale_mb'][ident]
            else:
                diff_stale[ident] = time.time()

        # Mailboxes in Cerebrum, but not in Exchange
        new_keys = list(s_ce_keys - s_ex_keys)
        for ident in new_keys:
            if state and ident in state['new_mb']:
                diff_new[ident] = state['new_mb'][ident]
            else:
                diff_new[ident] = time.time()

        # Check mailboxes that exists in both Cerebrum and Exchange for
        # difference (& is union, in case you wondered). If an attribute is not
        # in it's desired state in both this and the last run, save the
        # timestamp from the last run. This is used for calculating when we nag
        # to someone about stuff not beeing in sync.
        for key in s_ex_keys & s_ce_keys:
            for attr in ce_state[key]:
                if state and key in state['mb'] and \
                        attr in state['mb'][key]:
                    t_0 = state['mb'][key][attr][u'Time']
                else:
                    t_0 = time.time()
                diff_mb.setdefault(key, {})
                if attr not in ex_state[key]:
                    diff_mb[key][attr] = {
                        u'Exchange': None,
                        u'Cerebrum': ce_state[key][attr],
                        u'Time': t_0,
                    }
                elif ce_state[key][attr] != ex_state[key][attr]:
                    # For quotas, we only want to report mismatches if the
                    # difference is between the quotas in Cerebrum and Exchange
                    # is greater than 1% on either side. Hope this is an
                    # appropriate value to use ;)
                    try:
                        if u'Quota' in attr:
                            exq = ex_state[key][attr]
                            ceq = ce_state[key][attr]
                            diff = abs(int(exq) - int(ceq))
                            avg = (int(exq) + int(ceq)) / 2
                            one_p = avg * 0.01
                            if avg + diff < avg + one_p and \
                                    avg - diff > avg - one_p:
                                continue
                    except TypeError:
                        pass

                    diff_mb[key][attr] = {
                        u'Exchange': ex_state[key][attr],
                        u'Cerebrum': ce_state[key][attr],
                        u'Time': t_0,
                    }

        ret = {'new_mb': diff_new, 'stale_mb': diff_stale, 'mb': diff_mb}

        if not state:
            return ret, []

        now = time.time()
        # By now, we have three different dicts. Loop trough them and check if
        # we should report 'em
        report = [u'# User Attribute Since Cerebrum_value:Exchange_value']
        # Report attribute mismatches
        for key in diff_mb:
            for attr in diff_mb[key]:
                delta = (config.get(attr) if attr in config else
                         config.get('UndefinedAttribute'))
                if diff_mb[key][attr][u'Time'] < now - delta:
                    t = time.strftime(
                        u'%d%m%Y-%H:%M',
                        time.localtime(diff_mb[key][attr][u'Time']))
                    if attr == u'EmailAddresses':
                        # We report the difference for email addresses, for
                        # redability
                        s_ce_addr = set(diff_mb[key][attr][u'Cerebrum'])
                        s_ex_addr = set(diff_mb[key][attr][u'Exchange'])
                        new_addr = list(s_ce_addr - s_ex_addr)
                        stale_addr = list(s_ex_addr - s_ce_addr)
                        tmp = u'%-10s %-30s %s +%s:-%s' % (
                            key, attr, t, str(new_addr), str(stale_addr))
                    else:
                        tmp = (u'%-10s %-30s %s %s:%s' %
                               (key, attr, t,
                                repr(diff_mb[key][attr][u'Cerebrum']),
                                repr(diff_mb[key][attr][u'Exchange'])))
                    report += [tmp]

        # Report uncreated mailboxes
        report += [u'\n# Uncreated mailboxes (uname, time)']
        delta = (config.get('UncreatedMailbox') if 'UncreatedMailbox' in config
                 else config.get('UndefinedAttribute'))
        for key in diff_new:
            if diff_new[key] < now - delta:
                t = time.strftime(u'%d%m%Y-%H:%M',
                                  time.localtime(diff_new[key]))
                report += [u'%-10s uncreated_mb %s' % (key, t)]

        # Report stale mailboxes
        report += [u'\n# Stale mailboxes (uname, time)']
        delta = (config.get('StaleMailbox') if 'StaleMailbox' in config else
                 config.get('UndefinedAttribute'))
        for key in diff_stale:
            t = time.strftime(u'%d%m%Y-%H:%M', time.localtime(diff_stale[key]))
            if diff_stale[key] < now - delta:
                report += [u'%-10s stale_mb %s' % (key, t)]

        return ret, report
Ejemplo n.º 8
0
class CerebrumUtils(object):
    """Utility-class containing often used functions for Exchange."""
    def __init__(self):
        """Initialize the Utils."""
        self.db = Factory.get('Database')(client_encoding='UTF-8')
        self.en = Factory.get('Entity')(self.db)
        self.ac = Factory.get('Account')(self.db)
        self.pe = Factory.get('Person')(self.db)
        self.gr = Factory.get('Group')(self.db)
        self.co = Factory.get('Constants')(self.db)
        self.clconst = Factory.get('CLConstants')(self.db)
        self.ed = EmailDomain(self.db)
        self.eq = EmailQuota(self.db)
        self.ef = EmailForward(self.db)
        self.et = Factory.get('EmailTarget')(self.db)
        self.dg = DistributionGroup(self.db)

####
# Person related methods
####
    def get_person_accounts(self, person_id, spread=None):
        """Return a list of account information.

        :type person_id: int
        :param person_id: The person id to look up by.

        :rtype: list
        :return: A list of (account_id, username) tuples."""
        ret = [(x['account_id'], x['name']) for x in
               self.ac.search(owner_id=person_id,
                              owner_type=self.co.entity_person,
                              spread=self.co.spread_exchange_account)]
        self.db.rollback()
        return ret

    def get_person_names(self, account_id=None, person_id=None):
        """Return a persons names.

        :type account_id: int
        :param account_id: The user to look up names by.

        :type person_id: int
        :param person_id: The person to look up names by.

        :rtype: tuple
        :return: (first_name, last_name, full name).
        """

        # TODO: search_name_with_language?
        if account_id:
            self.ac.clear()
            self.ac.find(account_id)
            self.pe.clear()
            self.pe.find(self.ac.owner_id)
        elif person_id:
            self.pe.clear()
            self.pe.find(person_id)
        else:
            raise AssertionError('Called w/o person or account id')

        ret = (self.pe.get_name(self.co.system_cached, self.co.name_first),
               self.pe.get_name(self.co.system_cached, self.co.name_last),
               self.pe.get_name(self.co.system_cached, self.co.name_full,))
        self.db.rollback()
        return ret

    def get_person_membership_groupnames(self, person_id):
        """List all the groups the person is a member of.

        :type person_id: int
        :param person_id: The persons entity_id.

        :rtype: list
        :return: list(string) of groupnames."""
        ret = [x['group_name'] for x in self.gr.search_members(
            member_id=person_id, indirect_members=True)]
        self.db.rollback()
        return ret

    def is_electronic_reserved(self, person_id=None, account_id=None):
        """Check if a person has reserved themself from listing.

        :type person_id: int
        :param person_id: The persons entity_id.

        :type account_id: int
        :param account_id: The accounts entity_id.

        :rtype: bool
        :return: True if reserved, False otherwise."""
        if person_id:
            self.pe.clear()
            self.pe.find(person_id)
            ret = self.pe.has_e_reservation()
        elif account_id:
            self.ac.clear()
            self.ac.find(account_id)
            ret = self.ac.owner_has_ereservation()
        # TODO: This is sane?
        else:
            ret = True
        self.db.rollback()
        return ret

####
# Account related methods
####

    def get_account_mailaddrs(self, account_id):
        """Collect mailaddresses of an account.

        :type account_id: int
        :param account_id: The account_id representing the object.

        :rtype: list
        :return: A list of addresses."""
        self.et.clear()
        self.et.find_by_target_entity(account_id)
        addrs = ['%s@%s' % (x['local_part'], x['domain'])
                 for x in self.et.get_addresses()]
        self.db.rollback()
        return addrs

    def get_account_forwards(self, account_id):
        """Collect an accounts forward addresses.

        :param int account_id: The account_id representing the object.
        :return: A list of forward addresses."""
        self.ef.clear()
        self.ef.find_by_target_entity(account_id)
        r = []
        for fwd in self.ef.get_forward():
            # Need to do keys() for now, db_row is stupid.
            if 'enable' in fwd.keys() and fwd['enable'] == 'T':
                r.append(fwd['forward_to'])
        self.db.rollback()
        return r

    def get_account_local_delivery(self, account_id):
        """Check if this account should have local delivery.

        :param int account_id: The account_id representing the object.
        :rtype: bool
        :return: Local delivery on or off."""
        self.ef.clear()
        self.ef.find_by_target_entity(account_id)
        return self.ef.local_delivery

    def get_account_name(self, account_id):
        """Return information about the account.

        :type account_id: int
        :param acccount_id: The accounts entity id.

        :rtype: string
        :return: The accounts name."""
        self.ac.clear()
        self.ac.find(account_id)
        ret = self.ac.account_name
        self.db.rollback()
        return ret

    def get_account_owner_info(self, account_id):
        """Return the type and id of the accounts owner.

        :type account_id: int
        :param acccount_id: The accounts entity id.

        :rtype: tuple
        :return: (entity_type, entity_id)."""
        self.ac.clear()
        self.ac.find(account_id)
        ret = (self.ac.owner_type, self.ac.owner_id)
        self.db.rollback()
        return ret

    def get_account_spreads(self, account_id):
        """Return the accounts spread codes.

        :type account_id: int
        :param acccount_id: The accounts entity id.

        :rtype: list
        :return: A list of the accounts spread codes."""
        self.ac.clear()
        self.ac.find(account_id)
        ret = [x['spread'] for x in self.ac.get_spread()]
        self.db.rollback()
        return ret

    def get_account_primary_email(self, account_id):
        """Return the accounts primary address.

        :type account_id: int
        :param acccount_id: The accounts entity id.

        :rtype: str
        :return: The accounts primary email-address."""
        self.ac.clear()
        self.ac.find(account_id)
        ret = self.ac.get_primary_mailaddress()
        self.db.rollback()
        return ret

    def get_account_id(self, uname):
        """Get an accounts entity id.

        :type uname: str
        :param uname: The users name.

        :rtype: int
        :return: The entity id of the account."""
        self.ac.clear()
        self.ac.find_by_name(uname)
        ret = self.ac.entity_id
        self.db.rollback()
        return ret

    def get_primary_account(self, person_id):
        """Get a persons primary account.

        :type person_id: int
        :param person_id: The persons entity id.

        :rtype: int
        :return: The primary accounts entity_id or None if no primary."""
        self.pe.clear()
        self.pe.find(person_id)
        ret = self.pe.get_primary_account()
        self.db.rollback()
        return ret

    def get_account_group_memberships(self, uname, group_spread):
        """Get a list of group names, which the user (and users owner, if
        person), is a member of.

        :type uname: string
        :param uname: The accounts name.

        :type group_spread: _SpreadCode
        :param group_spread: Spread to filter groups by.

        :rtype: list(tuple)
        :return: A list of tuples contining group-name and -id."""
        self.ac.clear()
        self.ac.find_by_name(uname)

        groups = []

        # Fetch the groups the person is a member of
        if self.ac.owner_type == self.co.entity_person:
            for group in self.gr.search(member_id=self.ac.owner_id,
                                        spread=group_spread,
                                        indirect_members=True):
                groups.append((group['name'], group['group_id']))

        # Fetch the groups the account is a member of
        for group in self.gr.search(member_id=self.ac.entity_id,
                                    spread=group_spread,
                                    indirect_members=True):
            groups.append((group['name'], group['group_id']))

        self.db.rollback()
        return groups

####
# Group related methods
####

    def construct_group_names(self, uname, gname):
        """Construct Exchange related group names.

        :param str uname: The users username.
        :param str gname: The owning groups groupname.

        :rtype: tuple(str)
        :return: A tuple consisting of FirstName, LastName and DisplayName."""
        fn = uname
        ln = '(owner: %s)' % gname
        dn = '%s (owner: %s)' % (uname, gname)
        return (fn, ln, dn)

    def get_group_information(self, group_id):
        """Get a groups name and description.

        :type group_id: int
        :param group_id: The groups entity id.

        :rtype: tuple
        :return: The groups name and description."""
        # This function is kinda generic. We don't care
        # (yet) if it's a distribution or security group.
        r = self.gr.search(group_id=group_id)[0]
        ret = (r['name'], r['description'])
        self.db.rollback()
        return ret

    def get_group_id(self, group_name):
        """Get a groups entity_id.

        :type group_name: str
        :param group_name: The groups name.

        :rtype: int
        :return: The groups entity_id."""
        self.gr.clear()
        self.gr.find_by_name(group_name)
        ret = self.gr.entity_id
        self.db.rollback()
        return ret

    def get_group_spreads(self, group_id):
        """Return the groupss spread codes.

        :type group_id: int
        :param group_id: The accounts entity id.

        :rtype: list
        :return: A list of the accounts spread codes."""
        self.gr.clear()
        self.gr.find(group_id)
        ret = [x['spread'] for x in self.gr.get_spread()]
        self.db.rollback()
        return ret

    def get_group_members(self, group_id, spread=None, filter_spread=None):
        """Collect a list of the usernames of all users in a group.

        :type group_id: int
        :param group_id: The groups entity_id.

        :type spread: _SpreadCode
        :param spread: The spread to filter by.

        :type filter_spread: _SpreadCode
        :param filter_spread: A spread that the user must also have.

        :rtype: list
        :return: A list of usernames who are members of group_id."""
        found_accounts = []
        r = []
        for x in self.gr.search_members(group_id=group_id,
                                        indirect_members=True,
                                        member_spread=spread,
                                        member_type=self.co.entity_account):
            if x['member_id'] not in found_accounts:
                if filter_spread in self.get_account_spreads(x['member_id']):
                    r.append({'name': self.get_account_name(x['member_id']),
                              'account_id': x['member_id']})
                    found_accounts.append(x['member_id'])

        spreads = []
        if spread:
            spreads += [int(spread)]
        if filter_spread:
            spreads += [int(filter_spread)]

        for x in self.gr.search_members(group_id=group_id,
                                        indirect_members=True,
                                        member_type=self.co.entity_person):
            self.pe.clear()
            self.pe.find(x['member_id'])
            aid = self.pe.get_primary_account()
            if aid:
                self.ac.clear()
                self.ac.find(aid)
                if self.ac.entity_id not in found_accounts:
                    # If/elif used to allow usage without filter and
                    # filter_spread params
                    if spreads and \
                        set(spreads).issubset(
                            set([x['spread'] for x in self.ac.get_spread()])):
                        r.append({'name': self.ac.account_name,
                                  'account_id': self.ac.entity_id})
                        found_accounts.append(self.ac.entity_id)
                    elif not spreads:
                        r.append({'name': self.ac.account_name,
                                  'account_id': self.ac.entity_id})
                        found_accounts.append(self.ac.entity_id)
        self.db.rollback()
        return r

    def get_parent_groups(self, id, spread=None, name_prefix=None):
        """Return all groups that the group is an indirect member of. Filter
        by spread and the start of the group name.

        :type id: int
        :param id: The entity_id.

        :type spread: _SpreadCode
        :param _SpreadCode: The spread code to filter by, default is None.

        :type name_prefix: str
        :param name_prefix: Check if the group starts with this, default None.

        :rtype: list
        :return: A list of appropriate groups."""
        if name_prefix:
            np = '%s%%' % name_prefix
        else:
            np = None
        groups = []
        for group in self.gr.search(member_id=id, indirect_members=True,
                                    spread=spread, name=np):
            groups.append(group['name'])
        self.db.rollback()
        return groups

####
# Other utility methods
####

    def load_params(self, event):
        """Get the change params of an event.

        :type event: dbrow
        :param event: The db row returned by Change- or EventLog.

        :rtype: dict or None
        :return: The change params."""
        params = event['change_params']
        if isinstance(params, basestring):
            return json.loads(params)
        return params

    def get_entity_type(self, entity_id):
        """Fetch the entity type code of an entity.

        :type entity_id: int
        :param entity_id: The entity id.

        :rtype: long
        :return: The entity type code."""
        self.en.clear()
        self.en.find(entity_id)
        ret = self.en.entity_type
        self.db.rollback()
        return ret

    def log_event(self, event, trigger):
        """Utility method used to create an event only in the EventLog.

        :type event: dict
        :param event: Dict representing an event (as returned from get_event).

        :param trigger: str or list or tuple
        :param trigger: The change type code we want to associate with the
            event. Only the first value will be used."""
        if isinstance(trigger, (list, tuple)):
            trigger = trigger[0]
        trigger = trigger.split(':')
        ct = self.clconst.ChangeType(trigger[0], trigger[1])

        params = self.load_params(event)

        self.db.log_change(event['subject_entity'],
                           int(ct),
                           event['dest_entity'],
                           change_params=params,
                           skip_change=True,
                           skip_publish=True)
        self.db.commit()

    def log_event_receipt(self, event, trigger):
        """Log the "receipt" of a completed event in ChangeLog.

        :type event: dict
        :param event: Dict representing an event (as returned from get_event).

        :param trigger: str or list or tuple
        :param trigger: The change type code we want to associate with the
            event. Only the first value will be used."""
        # TODO: Set change_program from a sensible source to something smart
        if isinstance(trigger, (list, tuple)):
            trigger = trigger[0]
        trigger = trigger.split(':')
        ct = self.clconst.ChangeType(trigger[0], trigger[1])
        parm = {'change_program': 'ExchangeIntegration',
                'skip_event': True,
                'skip_publish': True}

        # Only log params if they actually contain something.
        param = self.load_params(event)
        if param:
            parm['change_params'] = param

        self.db.log_change(event['subject_entity'],
                           int(ct),
                           event['dest_entity'],
                           **parm)
        self.db.commit()

####
# Email* utility methods
####

    def get_email_target_info(self, target_id=None, target_entity=None):
        """Return the entity_type and entity_id of the entity the EmailTarget
        points at. Look up by _either_ target_id or target_entity.

        :type target_id: int
        :param target_id: The EmailTargets id.

        :type target_entity: int
        :param target_entity: The targets target entity id.

        :rtype: typle
        :return: (entity_id, target_entity_id, target_entity_type, hard_quota,
            soft_quota)."""
        self.et.clear()
        self.eq.clear()
        if target_id:
            self.et.find(target_id)
            try:
                self.eq.find(target_id)
            except Errors.NotFoundError:
                self.eq.clear()
        elif target_entity:
            self.et.find_by_target_entity(target_entity)
            try:
                self.eq.find(self.et.entity_id)
            except Errors.NotFoundError:
                self.eq.clear()
        else:
            self.db.rollback()
            raise Errors.ProgrammingError(
                'Must define either target_id og target_entity')
        ret = (self.et.entity_id,
               self.et.email_target_entity_id,
               self.et.email_target_entity_type,
               self.eq.get_quota_hard(),
               self.eq.get_quota_soft())
        self.db.rollback()
        return ret

    def get_email_domain_info(self, email_domain_id=None,
                              email_domain_name=None):
        """Return info about an EmailDomain.

        :type email_domain_id: int
        :param email_domain_id: The email domains id.

        :rtype: dict
        :return: {name: EmailDomain name, id: EmailDomain id}."""
        self.ed.clear()
        if email_domain_id:
            self.ed.find(email_domain_id)
        elif email_domain_name:
            self.ed.find_by_domain(email_domain_name)
        ret = {'name': self.ed.email_domain_name, 'id': self.ed.entity_id}
        self.db.rollback()
        return ret

####
# *Group utility methods
####

    def get_distgroup_attributes_and_targetdata(self, subj_id):
        """Collect DistributionGroup specific information.

        :type subj_id: int
        :param subj_id: The entity-id of the DistributionGroup.

        :rtype: dict
        :return: A dict with DistributionGroup-specific data."""
        # TODO: This might crash if someone where to add a spread, on a group
        # which is not a DistributionGroup in Cerebrum
        self.dg.clear()
        self.dg.find(subj_id)
        rl = True if self.dg.roomlist == 'T' else False
        ret = self.dg.get_distgroup_attributes_and_targetdata(roomlist=rl)
        self.db.rollback()
        return ret

    def get_distgroup_displayname(self, subj_id):
        """Get DisplayName for a DistributionGroup.

        :type subj_id: int
        :param subj_id: The DistributionGroups entity-id.

        @rtype: Cerebrum.extlib.db_row.row
        @return: Rows as returned by Entity.search_name_with_language()."""
        self.dg.clear()
        self.dg.find(subj_id)
        ret = self.dg.search_name_with_language(
            entity_id=subj_id,
            name_variant=self.co.dl_group_displ_name,
            name_language=self.co.language_nb)
        self.db.rollback()
        return ret
Ejemplo n.º 9
0
class CerebrumUtils(object):
    """Utility-class containing often used functions for Exchange."""
    def __init__(self):
        """Initialize the Utils."""
        self.db = Factory.get('Database')(client_encoding='UTF-8')
        self.en = Factory.get('Entity')(self.db)
        self.ac = Factory.get('Account')(self.db)
        self.pe = Factory.get('Person')(self.db)
        self.gr = Factory.get('Group')(self.db)
        self.co = Factory.get('Constants')(self.db)
        self.ed = EmailDomain(self.db)
        self.eq = EmailQuota(self.db)
        self.ef = EmailForward(self.db)
        self.et = Factory.get('EmailTarget')(self.db)
        self.dg = DistributionGroup(self.db)

####
# Person related methods
####

    def get_person_accounts(self, person_id, spread=None):
        """Return a list of account information.

        :type person_id: int
        :param person_id: The person id to look up by.

        :rtype: list
        :return: A list of (account_id, username) tuples."""
        ret = [(x['account_id'], x['name'])
               for x in self.ac.search(owner_id=person_id,
                                       owner_type=self.co.entity_person,
                                       spread=self.co.spread_exchange_account)]
        self.db.rollback()
        return ret

    def get_person_names(self, account_id=None, person_id=None):
        """Return a persons names.

        :type account_id: int
        :param account_id: The user to look up names by.

        :type person_id: int
        :param person_id: The person to look up names by.

        :rtype: tuple
        :return: A tuple consisting of first_name, last_name and the full name."""

        # TODO: search_name_with_language?
        if account_id:
            self.ac.clear()
            self.ac.find(account_id)
            self.pe.clear()
            self.pe.find(self.ac.owner_id)
        elif person_id:
            self.pe.clear()
            self.pe.find(person_id)
        else:
            raise AssertionError('Called w/o person or account id')

        ret = (self.pe.get_name(self.co.system_cached, self.co.name_first),
               self.pe.get_name(self.co.system_cached, self.co.name_last),
               self.pe.get_name(
                   self.co.system_cached,
                   self.co.name_full,
               ))
        self.db.rollback()
        return ret

    def get_person_membership_groupnames(self, person_id):
        """List all the groups the person is a member of.

        :type person_id: int
        :param person_id: The persons entity_id.

        :rtype: list
        :return: list(string) of groupnames."""
        ret = [
            x['group_name']
            for x in self.gr.search_members(member_id=person_id,
                                            indirect_members=True)
        ]
        self.db.rollback()
        return ret

    def is_electronic_reserved(self, person_id=None, account_id=None):
        """Check if a person has reserved themself from listing.

        :type person_id: int
        :param person_id: The persons entity_id.

        :type account_id: int
        :param account_id: The accounts entity_id.

        :rtype: bool
        :return: True if reserved, False otherwise."""
        if person_id:
            self.pe.clear()
            self.pe.find(person_id)
            ret = self.pe.has_e_reservation()
        elif account_id:
            self.ac.clear()
            self.ac.find(account_id)
            ret = self.ac.owner_has_ereservation()
        # TODO: This is sane?
        else:
            ret = True
        self.db.rollback()
        return ret

####
# Account related methods
####

    def get_account_mailaddrs(self, account_id):
        """Collect mailaddresses of an account.

        :type account_id: int
        :param account_id: The account_id representing the object.

        :rtype: list
        :return: A list of addresses."""
        self.et.clear()
        self.et.find_by_target_entity(account_id)
        addrs = [
            '%s@%s' % (x['local_part'], x['domain'])
            for x in self.et.get_addresses()
        ]
        self.db.rollback()
        return addrs

    def get_account_forwards(self, account_id):
        """Collect an accounts forward addresses.

        :param int account_id: The account_id representing the object.
        :return: A list of forward addresses."""
        self.ef.clear()
        self.ef.find_by_target_entity(account_id)
        r = []
        for fwd in self.ef.get_forward():
            # Need to do keys() for now, db_row is stupid.
            if 'enable' in fwd.keys() and fwd['enable'] == 'T':
                r.append(fwd['forward_to'])
        self.db.rollback()
        return r

    def get_account_local_delivery(self, account_id):
        """Check if this account should have local delivery.

        :param int account_id: The account_id representing the object.
        :rtype: bool
        :return: Local delivery on or off."""
        self.ef.clear()
        self.ef.find_by_target_entity(account_id)
        return self.ef.local_delivery

    def get_account_name(self, account_id):
        """Return information about the account.

        :type account_id: int
        :param acccount_id: The accounts entity id.

        :rtype: string
        :return: The accounts name."""
        self.ac.clear()
        self.ac.find(account_id)
        ret = self.ac.account_name
        self.db.rollback()
        return ret

    def get_account_owner_info(self, account_id):
        """Return the type and id of the accounts owner.

        :type account_id: int
        :param acccount_id: The accounts entity id.

        :rtype: tuple
        :return: (entity_type, entity_id)."""
        self.ac.clear()
        self.ac.find(account_id)
        ret = (self.ac.owner_type, self.ac.owner_id)
        self.db.rollback()
        return ret

    def get_account_spreads(self, account_id):
        """Return the accounts spread codes.

        :type account_id: int
        :param acccount_id: The accounts entity id.

        :rtype: list
        :return: A list of the accounts spread codes."""
        self.ac.clear()
        self.ac.find(account_id)
        ret = [x['spread'] for x in self.ac.get_spread()]
        self.db.rollback()
        return ret

    def get_account_primary_email(self, account_id):
        """Return the accounts primary address.

        :type account_id: int
        :param acccount_id: The accounts entity id.

        :rtype: str
        :return: The accounts primary email-address."""
        self.ac.clear()
        self.ac.find(account_id)
        ret = self.ac.get_primary_mailaddress()
        self.db.rollback()
        return ret

    def get_account_id(self, uname):
        """Get an accounts entity id.

        :type uname: str
        :param uname: The users name.

        :rtype: int
        :return: The entity id of the account."""
        self.ac.clear()
        self.ac.find_by_name(uname)
        ret = self.ac.entity_id
        self.db.rollback()
        return ret

    def get_primary_account(self, person_id):
        """Get a persons primary account.

        :type person_id: int
        :param person_id: The persons entity id.

        :rtype: int
        :return: The primary accounts entity_id or None if no primary."""
        self.pe.clear()
        self.pe.find(person_id)
        ret = self.pe.get_primary_account()
        self.db.rollback()
        return ret

    def get_account_group_memberships(self, uname, group_spread):
        """Get a list of group names, which the user (and users owner, if
        person), is a member of.

        :type uname: string
        :param uname: The accounts name.

        :type group_spread: _SpreadCode
        :param group_spread: Spread to filter groups by.

        :rtype: list(tuple)
        :return: A list of tuples contining group-name and -id."""
        self.ac.clear()
        self.ac.find_by_name(uname)

        groups = []

        # Fetch the groups the person is a member of
        if self.ac.owner_type == self.co.entity_person:
            for group in self.gr.search(member_id=self.ac.owner_id,
                                        spread=group_spread,
                                        indirect_members=True):
                groups.append((group['name'], group['group_id']))

        # Fetch the groups the account is a member of
        for group in self.gr.search(member_id=self.ac.entity_id,
                                    spread=group_spread,
                                    indirect_members=True):
            groups.append((group['name'], group['group_id']))

        self.db.rollback()
        return groups

####
# Group related methods
####

    def construct_group_names(self, uname, gname):
        """Construct Exchange related group names.

        :param str uname: The users username.
        :param str gname: The owning groups groupname.

        :rtype: tuple(str)
        :return: A tuple consisting of FirstName, LastName and DisplayName."""
        fn = uname
        ln = '(owner: %s)' % gname
        dn = '%s (owner: %s)' % (uname, gname)
        return (fn, ln, dn)

    def get_group_information(self, group_id):
        """Get a groups name and description.

        :type group_id: int
        :param group_id: The groups entity id.

        :rtype: tuple
        :return: The groups name and description."""
        # This function is kinda generic. We don't care
        # (yet) if it's a distribution or security group.
        r = self.gr.search(group_id=group_id)[0]
        ret = (r['name'], r['description'])
        self.db.rollback()
        return ret

    def get_group_id(self, group_name):
        """Get a groups entity_id.

        :type group_name: str
        :param group_name: The groups name.

        :rtype: int
        :return: The groups entity_id."""
        self.gr.clear()
        self.gr.find_by_name(group_name)
        ret = self.gr.entity_id
        self.db.rollback()
        return ret

    def get_group_spreads(self, group_id):
        """Return the groupss spread codes.

        :type group_id: int
        :param group_id: The accounts entity id.

        :rtype: list
        :return: A list of the accounts spread codes."""
        self.gr.clear()
        self.gr.find(group_id)
        ret = [x['spread'] for x in self.gr.get_spread()]
        self.db.rollback()
        return ret

    def get_group_members(self, group_id, spread=None, filter_spread=None):
        """Collect a list of the usernames of all users in a group.

        :type group_id: int
        :param group_id: The groups entity_id.

        :type spread: _SpreadCode
        :param spread: The spread to filter by.

        :type filter_spread: _SpreadCode
        :param filter_spread: A spread that the user must also have.

        :rtype: list
        :return: A list of usernames who are members of group_id."""
        found_accounts = []
        r = []
        for x in self.gr.search_members(group_id=group_id,
                                        indirect_members=True,
                                        member_spread=spread,
                                        member_type=self.co.entity_account):
            if x['member_id'] not in found_accounts:
                if filter_spread in self.get_account_spreads(x['member_id']):
                    r.append({
                        'name': self.get_account_name(x['member_id']),
                        'account_id': x['member_id']
                    })
                    found_accounts.append(x['member_id'])

        spreads = []
        if spread:
            spreads += [int(spread)]
        if filter_spread:
            spreads += [int(filter_spread)]

        for x in self.gr.search_members(group_id=group_id,
                                        indirect_members=True,
                                        member_type=self.co.entity_person):
            self.pe.clear()
            self.pe.find(x['member_id'])
            aid = self.pe.get_primary_account()
            if aid:
                self.ac.clear()
                self.ac.find(aid)
                if self.ac.entity_id not in found_accounts:
                    # If/elif used to allow usage without filter and
                    # filter_spread params
                    if spreads and \
                        set(spreads).issubset(
                            set([x['spread'] for x in self.ac.get_spread()])):
                        r.append({
                            'name': self.ac.account_name,
                            'account_id': self.ac.entity_id
                        })
                        found_accounts.append(self.ac.entity_id)
                    elif not spreads:
                        r.append({
                            'name': self.ac.account_name,
                            'account_id': self.ac.entity_id
                        })
                        found_accounts.append(self.ac.entity_id)
        self.db.rollback()
        return r

    def get_parent_groups(self, id, spread=None, name_prefix=None):
        """Return all groups that the group is an indirect member of. Filter
        by spread and the start of the group name.

        :type id: int
        :param id: The entity_id.

        :type spread: _SpreadCode
        :param _SpreadCode: The spread code to filter by, default is None.

        :type name_prefix: str
        :param name_prefix: Check if the group starts with this, default None.

        :rtype: list
        :return: A list of appropriate groups."""
        if name_prefix:
            np = '%s%%' % name_prefix
        else:
            np = None
        groups = []
        for group in self.gr.search(member_id=id,
                                    indirect_members=True,
                                    spread=spread,
                                    name=np):
            groups.append(group['name'])
        self.db.rollback()
        return groups

####
# Other utility methods
####

    def unpickle_event_params(self, event):
        """Unpickle the change params of an event.

        :type event: dbrow
        :param event: The db row returned by Change- or EventLog.

        :rtype: string
        :return: The change params."""
        # Hopefully, evertyhin will use UTF in the end
        try:
            return pickle.loads(event['change_params'])
        except:
            return pickle.loads(event['change_params'].encode('ISO_8859_15'))

    def get_entity_type(self, entity_id):
        """Fetch the entity type code of an entity.

        :type entity_id: int
        :param entity_id: The entity id.

        :rtype: long
        :return: The entity type code."""
        self.en.clear()
        self.en.find(entity_id)
        ret = self.en.entity_type
        self.db.rollback()
        return ret

    def log_event(self, event, trigger):
        """Utility method used to create an event only in the EventLog.

        :type event: dict
        :param event: Dict representing an event (as returned from get_event).

        :param trigger: str or list or tuple
        :param trigger: The change type code we want to associate with the
            event. Only the first value will be used."""
        if isinstance(trigger, (list, tuple)):
            trigger = trigger[0]
        trigger = trigger.split(':')
        ct = self.co.ChangeType(trigger[0], trigger[1])

        param = self.unpickle_event_params(event)

        self.db.log_change(event['subject_entity'],
                           int(ct),
                           event['dest_entity'],
                           change_params=param,
                           skip_change=True,
                           skip_publish=True)
        self.db.commit()

    def log_event_receipt(self, event, trigger):
        """Log the "receipt" of a completed event in ChangeLog.

        :type event: dict
        :param event: Dict representing an event (as returned from get_event).

        :param trigger: str or list or tuple
        :param trigger: The change type code we want to associate with the
            event. Only the first value will be used."""
        # TODO: Set change_program from a sensible source to something smart
        if isinstance(trigger, (list, tuple)):
            trigger = trigger[0]
        trigger = trigger.split(':')
        ct = self.co.ChangeType(trigger[0], trigger[1])
        parm = {
            'change_program': 'ExchangeIntegration',
            'skip_event': True,
            'skip_publish': True
        }

        # Only log params if they actually contain something.
        param = self.unpickle_event_params(event)
        if param:
            parm['change_params'] = param

        self.db.log_change(event['subject_entity'], int(ct),
                           event['dest_entity'], **parm)
        self.db.commit()

####
# Email* utility methods
####

    def get_email_target_info(self, target_id=None, target_entity=None):
        """Return the entity_type and entity_id of the entity the EmailTarget
        points at. Look up by _either_ target_id or target_entity.

        :type target_id: int
        :param target_id: The EmailTargets id.

        :type target_entity: int
        :param target_entity: The targets target entity id.

        :rtype: typle
        :return: (entity_id, target_entity_id, target_entity_type, hard_quota,
            soft_quota)."""
        self.et.clear()
        self.eq.clear()
        if target_id:
            self.et.find(target_id)
            try:
                self.eq.find(target_id)
            except Errors.NotFoundError:
                self.eq.clear()
        elif target_entity:
            self.et.find_by_target_entity(target_entity)
            try:
                self.eq.find(self.et.entity_id)
            except Errors.NotFoundError:
                self.eq.clear()
        else:
            self.db.rollback()
            raise Errors.ProgrammingError(
                'Must define either target_id og target_entity')
        ret = (self.et.entity_id, self.et.email_target_entity_id,
               self.et.email_target_entity_type, self.eq.get_quota_hard(),
               self.eq.get_quota_soft())
        self.db.rollback()
        return ret

    def get_email_domain_info(self,
                              email_domain_id=None,
                              email_domain_name=None):
        """Return info about an EmailDomain.

        :type email_domain_id: int
        :param email_domain_id: The email domains id.

        :rtype: dict
        :return: {name: EmailDomain name, id: EmailDomain id}."""
        self.ed.clear()
        if email_domain_id:
            self.ed.find(email_domain_id)
        elif email_domain_name:
            self.ed.find_by_domain(email_domain_name)
        ret = {'name': self.ed.email_domain_name, 'id': self.ed.entity_id}
        self.db.rollback()
        return ret

####
# *Group utility methods
####

    def get_distgroup_attributes_and_targetdata(self, subj_id):
        """Collect DistributionGroup specific information.

        :type subj_id: int
        :param subj_id: The entity-id of the DistributionGroup.

        :rtype: dict
        :return: A dict with DistributionGroup-specific data."""
        # TODO: This might crash if someone where to add a spread, on a group
        # which is not a DistributionGroup in Cerebrum
        self.dg.clear()
        self.dg.find(subj_id)
        rl = True if self.dg.roomlist == 'T' else False
        ret = self.dg.get_distgroup_attributes_and_targetdata(roomlist=rl)
        self.db.rollback()
        return ret

    def get_distgroup_displayname(self, subj_id):
        """Get DisplayName for a DistributionGroup.

        :type subj_id: int
        :param subj_id: The DistributionGroups entity-id.

        @rtype: Cerebrum.extlib.db_row.row
        @return: Rows as returned by Entity.search_name_with_language()."""
        self.dg.clear()
        self.dg.find(subj_id)
        ret = self.dg.search_name_with_language(
            entity_id=subj_id,
            name_variant=self.co.dl_group_displ_name,
            name_language=self.co.language_nb)
        self.db.rollback()
        return ret
class StateChecker(object):

    """Wrapper class for state-checking functions.

    The StateChecker class wraps all the functions we need in order to
    verify and report deviances between Cerebrum and Exchange.
    """

    # Connect params
    LDAP_RETRY_DELAY = 60
    LDAP_RETRY_MAX = 5

    # Search and result params
    LDAP_COM_DELAY = 30
    LDAP_COM_MAX = 3

    def __init__(self, conf):
        """Initzialize a new instance of out state-checker.

        :param logger logger: The logger to use.
        :param dict conf: Our StateCheckers configuration.
        """
        self.db = Factory.get('Database')(client_encoding='UTF-8')
        self.co = Factory.get('Constants')(self.db)
        self.ac = Factory.get('Account')(self.db)
        self.pe = Factory.get('Person')(self.db)
        self.gr = Factory.get('Group')(self.db)
        self.et = Factory.get('EmailTarget')(self.db)
        self.eq = EmailQuota(self.db)
        self.ea = EmailAddress(self.db)
        self.ef = EmailForward(self.db)
        self.cu = CerebrumUtils()

        self.config = conf

        self._ldap_page_size = 1000

        self._cache_randzone_users = self._populate_randzone_cache(
            self.config['randzone_unreserve_group'])
        self._cache_accounts = self._populate_account_cache(
            self.co.spread_exchange_account)
        self._cache_addresses = self._populate_address_cache()
        self._cache_local_delivery = self._populate_local_delivery_cache()
        self._cache_forwards = self._populate_forward_cache()
        self._cache_quotas = self._populate_quota_cache()
        self._cache_targets = self._populate_target_cache()
        self._cache_names = self._populate_name_cache()
        self._cache_group_names = self._populate_group_name_cache()
        self._cache_no_reservation = self._populate_no_reservation_cache()
        self._cache_primary_accounts = self._populate_primary_account_cache()

    def init_ldap(self):
        """Initzialize LDAP connection."""
        self.ldap_srv = ldap.ldapobject.ReconnectLDAPObject(
            '%s://%s/' % (self.config['ldap_proto'],
                          self.config['ldap_server']),
            retry_max=self.LDAP_RETRY_MAX,
            retry_delay=self.LDAP_RETRY_DELAY)

        usr = self.config['ldap_user'].split('\\')[1]
        self.ldap_srv.bind_s(
            self.config['ldap_user'],
            read_password(usr, self.config['ldap_server']))

        self.ldap_lc = ldap.controls.SimplePagedResultsControl(
            True, self._ldap_page_size, '')

    def _searcher(self, ou, scope, attrs, ctrls):
        """ Perform ldap.search(), but retry in the event of an error.

        This wraps the search with error handling, so that the search is
        repeated with a delay between attempts.
        """
        for attempt in itertools.count(1):
            try:
                return self.ldap_srv.search_ext(
                    ou, scope, attrlist=attrs, serverctrls=ctrls)
            except ldap.LDAPError as e:
                if attempt < self.LDAP_COM_MAX:
                    logger.debug('Caught %r in _searcher on attempt %d',
                                 e, attempt)
                    time.sleep(self.LDAP_COM_DELAY)
                    continue
                raise

    def _recvr(self, msgid):
        """ Perform ldap.result3(), but retry in the event of an error.

        This wraps the result fetching with error handling, so that the fetch
        is repeated with a delay between attempts.

        It also decodes all attributes and attribute text values.
        """
        for attempt in itertools.count(1):
            try:
                # return self.ldap_srv.result3(msgid)
                rtype, rdata, rmsgid, sc = self.ldap_srv.result3(msgid)
                return rtype, decode_attrs(rdata), rmsgid, sc
            except ldap.LDAPError as e:
                if attempt < self.LDAP_COM_MAX:
                    logger.debug('Caught %r in _recvr on attempt %d',
                                 e, attempt)
                    time.sleep(self.LDAP_COM_DELAY)
                    continue
                raise

    def search(self, ou, attrs, scope=ldap.SCOPE_SUBTREE):
        """Wrapper for the search- and result-calls.

        Implements paged searching.

        :param str ou: The OU to search in.
        :param list attrs: The attributes to fetch.
        :param int scope: Our search scope, default is subtree.
        """
        # Implementing paging, taken from
        # http://www.novell.com/coolsolutions/tip/18274.html
        msgid = self._searcher(ou, scope, attrs, [self.ldap_lc])

        data = []

        ctrltype = ldap.controls.SimplePagedResultsControl.controlType
        while True:
            time.sleep(1)
            rtype, rdata, rmsgid, sc = self._recvr(msgid)
            data.extend(rdata)
            pctrls = [c for c in sc if c.controlType == ctrltype]
            if pctrls:
                cookie = pctrls[0].cookie
                if cookie:
                    self.ldap_lc.cookie = cookie
                    time.sleep(1)
                    msgid = self._searcher(ou, scope, attrs, [self.ldap_lc])
                else:
                    break
            else:
                logger.warn('Server ignores RFC 2696 control.')
                break
        return data[1:]

    def close(self):
        """Close the LDAP connection."""
        self.ldap_srv.unbind_s()

    #
    # Various cache-generating functions.
    #

    def _populate_randzone_cache(self, randzone):
        self.gr.clear()
        self.gr.find_by_name(randzone)
        u = text_decoder(self.db.encoding)
        return [u(x['name'])
                for x in self.cu.get_group_members(self.gr.entity_id)]

    def _populate_account_cache(self, spread):
        u = text_decoder(self.db.encoding)

        def to_dict(row):
            d = dict(row)
            d['name'] = u(row['name'])
            d['description'] = u(row['description'])
            return d

        return [to_dict(r) for r in self.ac.search(spread=spread)]

    def _populate_address_cache(self):
        u = text_decoder(self.db.encoding)
        tmp = defaultdict(list)

        # TODO: Implement fetchall?
        for addr in self.ea.list_email_addresses_ext():
            tmp[addr['target_id']].append(
                u'%s@%s' % (u(addr['local_part']), u(addr['domain'])))
        return dict(tmp)

    def _populate_local_delivery_cache(self):
        r = {}
        for ld in self.ef.list_local_delivery():
            r[ld['target_id']] = ld['local_delivery']
        return r

    def _populate_forward_cache(self):
        u = text_decoder(self.db.encoding)
        tmp = defaultdict(list)

        for fwd in self.ef.list_email_forwards():
            if fwd['enable'] == 'T':
                tmp[fwd['target_id']].append(u(fwd['forward_to']))
        return dict(tmp)

    def _populate_quota_cache(self):
        tmp = defaultdict(dict)

        # TODO: Implement fetchall?
        for quota in self.eq.list_email_quota_ext():
            tmp[quota['target_id']]['soft'] = quota['quota_soft']
            tmp[quota['target_id']]['hard'] = quota['quota_hard']
        return dict(tmp)

    def _populate_target_cache(self):
        u = text_decoder(self.db.encoding)
        tmp = defaultdict(dict)
        for targ in self.et.list_email_target_primary_addresses(
                target_type=self.co.email_target_account):
            tmp[targ['target_entity_id']]['target_id'] = targ['target_id']
            tmp[targ['target_entity_id']]['primary'] = \
                u'%s@%s' % (u(targ['local_part']), u(targ['domain']))
        return dict(tmp)

    def _populate_name_cache(self):
        u = text_decoder(self.db.encoding)
        tmp = defaultdict(dict)
        for name in self.pe.search_person_names(
            name_variant=[self.co.name_first,
                          self.co.name_last,
                          self.co.name_full],
                source_system=self.co.system_cached):
            tmp[name['person_id']][name['name_variant']] = u(name['name'])
        return dict(tmp)

    def _populate_group_name_cache(self):
        u = text_decoder(self.db.encoding)
        tmp = {}
        for eid, dom, name in self.gr.list_names(self.co.group_namespace):
            tmp[eid] = u(name)
        return tmp

    def _populate_no_reservation_cache(self):
        unreserved = []
        for r in self.pe.list_traits(self.co.trait_public_reservation,
                                     fetchall=True):
            if r['numval'] == 0:
                unreserved.append(r['entity_id'])
        return unreserved

    def _populate_primary_account_cache(self):
        primary = []
        for acc in self.ac.list_accounts_by_type(primary_only=True):
            primary.append(acc['account_id'])
        return primary

    ###
    # Mailbox related state fetching & comparison
    ###

    def collect_cerebrum_mail_info(self):
        """Collect E-mail related information from Cerebrum.

        :rtype: dict
        :return: A dict of users attributes. Uname is key.
        """
        res = {}
        for acc in self._cache_accounts:
            tmp = {}
            try:
                tid = self._cache_targets[acc['account_id']]['target_id']
            except KeyError:
                logger.warn('Could not find account with id:%d in list '
                            'of targets, skipping..', acc['account_id'])
                continue
            # Fetch addresses
            tmp[u'EmailAddresses'] = sorted(self._cache_addresses[tid])
            # Fetch primary address
            tmp[u'PrimaryAddress'] = \
                self._cache_targets[acc['account_id']]['primary']

            # Fetch names
            if acc['owner_type'] == self.co.entity_person:
                tmp[u'FirstName'] = \
                    self._cache_names[acc['owner_id']][int(self.co.name_first)]
                tmp[u'LastName'] = \
                    self._cache_names[acc['owner_id']][int(self.co.name_last)]
                tmp[u'DisplayName'] = \
                    self._cache_names[acc['owner_id']][int(self.co.name_full)]
            else:
                fn, ln, dn = self.cu.construct_group_names(
                    acc['name'], self._cache_group_names.get(acc['owner_id'],
                                                             None))
                tmp[u'FirstName'] = fn
                tmp[u'LastName'] = ln
                tmp[u'DisplayName'] = dn

            # Fetch quotas
            hard = self._cache_quotas[tid]['hard'] * 1024
            soft = self._cache_quotas[tid]['soft']

            tmp[u'ProhibitSendQuota'] = str(hard)
            tmp[u'ProhibitSendReceiveQuota'] = str(hard)
            tmp[u'IssueWarningQuota'] = str(int(hard * soft / 100.))

            # Randzone users will always be shown. This overrides everything
            # else.
            if acc['name'] in self._cache_randzone_users:
                hide = False
            elif acc['owner_id'] in self._cache_no_reservation and \
                    acc['account_id'] in self._cache_primary_accounts:
                hide = False
            else:
                hide = True

            tmp[u'HiddenFromAddressListsEnabled'] = hide

            # Collect local delivery status
            tmp[u'DeliverToMailboxAndForward'] = \
                self._cache_local_delivery.get(tid, False)

            # Collect forwarding address
            # We do this by doing a difference operation on the forwards and
            # the addresses, so we only end up with "external" addresses.
            s_fwds = set(self._cache_forwards.get(tid, []))
            s_addrs = set(self._cache_addresses.get(tid, []))
            ext_fwds = list(s_fwds - s_addrs)
            if ext_fwds:
                tmp[u'ForwardingSmtpAddress'] = ext_fwds[0]
            else:
                tmp[u'ForwardingSmtpAddress'] = None

            res[acc['name']] = tmp

        return res

    def collect_exchange_mail_info(self, mb_ou):
        """Collect mailbox-information from Exchange, via LDAP.

        :param str mb_ou: The OrganizationalUnit to search for mailboxes.
        :rtype: dict
        :return: A dict with the mailboxes attributes. The key is the account
            name.
        """
        attrs = ['proxyAddresses',
                 'displayName',
                 'givenName',
                 'sn',
                 'msExchHideFromAddressLists',
                 'extensionAttribute1',
                 'mDBUseDefaults',
                 'mDBOverQuotaLimit',
                 'mDBOverHardQuotaLimit',
                 'mDBStorageQuota',
                 'deliverAndRedirect',
                 'msExchGenericForwardingAddress']

        r = self.search(mb_ou, attrs)
        ret = {}
        for cn, data in r:
            if 'extensionAttribute1'in data and \
                    data['extensionAttribute1'] == ['not migrated'] or \
                    'ExchangeActiveSyncDevices' in cn:
                continue
            tmp = {}
            name = cn[3:].split(',')[0]
            for key in data:
                if key == 'proxyAddresses':
                    addrs = []
                    for addr in data[key]:
                        if addr.startswith('SMTP:'):
                            tmp[u'PrimaryAddress'] = addr[5:]
                        addrs.append(addr[5:])
                    tmp[u'EmailAddresses'] = sorted(addrs)
                elif key == 'displayName':
                    tmp[u'DisplayName'] = data[key][0]
                elif key == 'givenName':
                    tmp[u'FirstName'] = data[key][0]
                elif key == 'sn':
                    tmp[u'LastName'] = data[key][0]
                elif key == 'mDBUseDefaults':
                    tmp[u'UseDatabaseQuotaDefaults'] = (
                        True if data[key][0] == 'TRUE' else False)
                elif key == 'mDBOverQuotaLimit':
                    q = data[key][0]
                    tmp[u'ProhibitSendQuota'] = q
                elif key == 'mDBOverHardQuotaLimit':
                    q = data[key][0]
                    tmp[u'ProhibitSendReceiveQuota'] = q
                elif key == 'mDBStorageQuota':
                    q = data[key][0]
                    tmp[u'IssueWarningQuota'] = q

            # Non-existent attribute means that the value is false. Fuckers.
            # Collect status about if the mbox is hidden or not
            tmp[u'HiddenFromAddressListsEnabled'] = False
            if 'msExchHideFromAddressLists' in data:
                val = (True if data['msExchHideFromAddressLists'][0] == 'TRUE'
                       else False)
                tmp[u'HiddenFromAddressListsEnabled'] = val

            # Collect local delivery status
            tmp[u'DeliverToMailboxAndForward'] = False
            if 'deliverAndRedirect' in data:
                val = (True if data['deliverAndRedirect'][0] == 'TRUE'
                       else False)
                tmp[u'DeliverToMailboxAndForward'] = val

            # Collect forwarding address
            tmp[u'ForwardingSmtpAddress'] = None
            if 'msExchGenericForwardingAddress' in data:
                val = data['msExchGenericForwardingAddress'][0]
                # We split of smtp:, and store
                tmp[u'ForwardingSmtpAddress'] = val.split(':')[1]

            ret[name] = tmp
        return ret

    def compare_mailbox_state(self, ex_state, ce_state, state, config):
        """Compare the information fetched from Cerebrum and Exchange.

        This method produces a dict with the state between the systems,
        and a report that will be sent to the appropriate target system
        administrators.

        :param dict ex_state: The state in Exchange.
        :param dict ce_state: The state in Cerebrum.
        :param dict state: The previous state generated by this method.
        :param dict config: Configuration of reporting delays for various
            attributes.
        :rtype: tuple
        :return: A tuple consisting of the new difference-state and a
            human-readable report of differences.
        """
        s_ce_keys = set(ce_state.keys())
        s_ex_keys = set(ex_state.keys())
        diff_mb = {}
        diff_stale = {}
        diff_new = {}

        ##
        # Populate some structures with information we need

        # Mailboxes in Exchange, but not in Cerebrum
        stale_keys = list(s_ex_keys - s_ce_keys)
        for ident in stale_keys:
            if state and ident in state['stale_mb']:
                diff_stale[ident] = state['stale_mb'][ident]
            else:
                diff_stale[ident] = time.time()

        # Mailboxes in Cerebrum, but not in Exchange
        new_keys = list(s_ce_keys - s_ex_keys)
        for ident in new_keys:
            if state and ident in state['new_mb']:
                diff_new[ident] = state['new_mb'][ident]
            else:
                diff_new[ident] = time.time()

        # Check mailboxes that exists in both Cerebrum and Exchange for
        # difference (& is union, in case you wondered). If an attribute is not
        # in it's desired state in both this and the last run, save the
        # timestamp from the last run. This is used for calculating when we nag
        # to someone about stuff not beeing in sync.
        for key in s_ex_keys & s_ce_keys:
            for attr in ce_state[key]:
                if state and key in state['mb'] and \
                        attr in state['mb'][key]:
                    t_0 = state['mb'][key][attr][u'Time']
                else:
                    t_0 = time.time()
                diff_mb.setdefault(key, {})
                if attr not in ex_state[key]:
                    diff_mb[key][attr] = {
                        u'Exchange': None,
                        u'Cerebrum': ce_state[key][attr],
                        u'Time': t_0,
                    }
                elif ce_state[key][attr] != ex_state[key][attr]:
                    # For quotas, we only want to report mismatches if the
                    # difference is between the quotas in Cerebrum and Exchange
                    # is greater than 1% on either side. Hope this is an
                    # appropriate value to use ;)
                    try:
                        if u'Quota' in attr:
                            exq = ex_state[key][attr]
                            ceq = ce_state[key][attr]
                            diff = abs(int(exq) - int(ceq))
                            avg = (int(exq) + int(ceq)) / 2
                            one_p = avg * 0.01
                            if avg + diff < avg + one_p and \
                                    avg - diff > avg - one_p:
                                continue
                    except TypeError:
                        pass

                    diff_mb[key][attr] = {
                        u'Exchange': ex_state[key][attr],
                        u'Cerebrum': ce_state[key][attr],
                        u'Time': t_0,
                    }

        ret = {'new_mb': diff_new, 'stale_mb': diff_stale, 'mb': diff_mb}

        if not state:
            return ret, []

        now = time.time()
        # By now, we have three different dicts. Loop trough them and check if
        # we should report 'em
        report = [u'# User Attribute Since Cerebrum_value:Exchange_value']
        # Report attribute mismatches
        for key in diff_mb:
            for attr in diff_mb[key]:
                delta = (config.get(attr) if attr in config else
                         config.get('UndefinedAttribute'))
                if diff_mb[key][attr][u'Time'] < now - delta:
                    t = time.strftime(u'%d%m%Y-%H:%M', time.localtime(
                        diff_mb[key][attr][u'Time']))
                    if attr == u'EmailAddresses':
                        # We report the difference for email addresses, for
                        # redability
                        s_ce_addr = set(diff_mb[key][attr][u'Cerebrum'])
                        s_ex_addr = set(diff_mb[key][attr][u'Exchange'])
                        new_addr = list(s_ce_addr - s_ex_addr)
                        stale_addr = list(s_ex_addr - s_ce_addr)
                        tmp = u'%-10s %-30s %s +%s:-%s' % (key, attr, t,
                                                           str(new_addr),
                                                           str(stale_addr))
                    else:
                        tmp = (u'%-10s %-30s %s %s:%s' %
                               (key, attr, t,
                                repr(diff_mb[key][attr][u'Cerebrum']),
                                repr(diff_mb[key][attr][u'Exchange']))
                               )
                    report += [tmp]

        # Report uncreated mailboxes
        report += [u'\n# Uncreated mailboxes (uname, time)']
        delta = (config.get('UncreatedMailbox') if 'UncreatedMailbox' in config
                 else config.get('UndefinedAttribute'))
        for key in diff_new:
            if diff_new[key] < now - delta:
                t = time.strftime(u'%d%m%Y-%H:%M', time.localtime(
                    diff_new[key]))
                report += [u'%-10s uncreated_mb %s' % (key, t)]

        # Report stale mailboxes
        report += [u'\n# Stale mailboxes (uname, time)']
        delta = (config.get('StaleMailbox') if 'StaleMailbox' in config else
                 config.get('UndefinedAttribute'))
        for key in diff_stale:
            t = time.strftime(u'%d%m%Y-%H:%M', time.localtime(
                diff_stale[key]))
            if diff_stale[key] < now - delta:
                report += [u'%-10s stale_mb %s' % (key, t)]

        return ret, report