def delete_spread(self, spread): # # Pre-remove checks # spreads = [int(r['spread']) for r in self.get_spread()] if spread not in spreads: # user doesn't have this spread return # All users in the 'ifi' NIS domain must also exist in the # 'uio' NIS domain. if (spread == self.const.spread_uio_nis_user and int(self.const.spread_ifi_nis_user) in spreads): raise self._db.IntegrityError, \ "Can't remove uio spread to an account with ifi spread." if (spread == self.const.spread_ifi_nis_user or spread == self.const.spread_uio_nis_user): self.clear_home(spread) # Remove IMAP user # TBD: It is currently a bit uncertain who and when we should # allow this. Currently it should only be used when deleting # a user. # exchange-related-jazz # this code, up to and including the TBD should be removed # when migration to Exchange is completed as it wil no longer # be needed. Jazz (2013-11) # if (spread == self.const.spread_uio_imap and int(self.const.spread_uio_imap) in spreads): et = Email.EmailTarget(self._db) et.find_by_target_entity(self.entity_id) self._UiO_order_cyrus_action(self.const.bofh_email_delete, et.email_server_id) # TBD: should we also perform a "cascade delete" from EmailTarget? # exchange-relatert-jazz # Due to the way Exchange is updated we no longer need to # register a delete request in Cerebrum. Setting target_type # to deleted should generate an appropriate event which then # may be used to remove the mailbox from Exchange in the # agreed maner (export to .pst-file, then remove-mailbox). A # clean-up job should probably be implemented to remove # email_targets that have had status deleted for a period of # time. This will however remove the e-mailaddresses assigned # to the target and make their re-use possible. Jazz (2013-11) # if spread == self.const.spread_exchange_account: et = Email.EmailTarget(self._db) et.find_by_target_entity(self.entity_id) et.email_target_type = self.const.email_target_deleted et.write_db() # (Try to) perform the actual spread removal. ret = self.__super.delete_spread(spread) return ret
def __init__(self, db): """ Cache data for this iterable object. """ co = Factory.get('Constants')(db) ac = Factory.get('Account')(db) pe = Factory.get('Person')(db) ou = Factory.get('OU')(db) et = Email.EmailTarget(db) self.db = db logger.debug("caching spreads ...") self.account_spreads = set( (row['entity_id'] for row in ac.list_all_with_spread(co.spread_oid_acc))) self.ou_spreads = set( (row['entity_id'] for row in ou.list_all_with_spread(co.spread_oid_ou))) logger.debug("... email addresses ...") self.account_id_to_email = dict( (int(row['target_entity_id']), format_email(row)) for row in et.list_email_target_primary_addresses()) logger.debug("... names ...") self.person_id_to_name = pe.getdict_persons_names( source_system=co.system_cached, name_types=(co.name_first, co.name_last)) logger.debug("... external ids ...") self.person_id_to_extid = dict( (int(row['entity_id']), row['external_id']) for row in pe.search_external_ids(id_type=co.externalid_fodselsnr, fetchall=False)) logger.debug("... auth data ...") self.account_id_to_auth = dict() for row in ac.list_account_authentication( auth_type=(co.auth_type_md4_nt, co.auth_type_plaintext)): account_id = int(row['account_id']) if row['method'] is None: continue if account_id not in self.account_id_to_auth: self.account_id_to_auth[account_id] = [ row['entity_name'], dict() ] self.account_id_to_auth[account_id][1].setdefault( int(row['method']), row['auth_data']) logger.debug("... account data ...") self.person_id_to_account_id = dict( (int(row['person_id']), int(row['account_id'])) for row in ac.list_accounts_by_type(primary_only=True)) logger.debug("... ou data ...") self.ou_id_to_name = dict((r["entity_id"], r["name"]) for r in ou.search_name_with_language( entity_type=co.entity_ou, name_variant=co.ou_name_acronym, name_language=co.language_nb)) logger.debug("... done caching data")
def delete(self): """Delete the group, along with its EmailTarget.""" et = Email.EmailTarget(self._db) ea = Email.EmailAddress(self._db) epat = Email.EmailPrimaryAddressTarget(self._db) # If there is not associated an EmailTarget with the group, call delete # from the superclass. try: et.find_by_target_entity(self.entity_id) except Errors.NotFoundError: return super(GroupUiOMixin, self).delete() # An EmailTarget exists, so we try to delete its primary address. try: epat.find(et.entity_id) epat.delete() except Errors.NotFoundError: pass # We remove all the EmailTargets addresses. try: for row in et.get_addresses(): ea.clear() ea.find(row['address_id']) ea.delete() except Errors.NotFoundError: pass # Delete the EmailTarget et.delete() # Finally! Delete the group! super(GroupUiOMixin, self).delete()
def email_rt_delete(self, operator, queuename): """ Delete RT list. """ queue, host = self._resolve_rt_name(queuename) rt_dom = self._get_email_domain_from_str(host) self.ba.can_rt_delete(operator.get_entity_id(), domain=rt_dom) et = Email.EmailTarget(self.db) ea = Email.EmailAddress(self.db) epat = Email.EmailPrimaryAddressTarget(self.db) result = [] for target_id in self.__get_all_related_rt_targets(queuename): try: et.clear() et.find(target_id) except Errors.NotFoundError: continue epat.clear() try: epat.find(et.entity_id) except Errors.NotFoundError: pass else: epat.delete() for r in et.get_addresses(): addr = '%(local_part)s@%(domain)s' % r ea.clear() ea.find_by_address(addr) ea.delete() result.append({'address': addr}) et.delete() return result
def update_email_address(address, group): et, ea = get_email_target_and_address(address) if et: if et.email_target_type != co.email_target_multi: logger.error("Wrong existing target type for <%s>", address) return if et.email_target_entity_id == group.entity_id: logger.debug("Up-to-date <%s>", address) return et.email_target_entity_id = group.entity_id logger.info("Updating <%s>", address) et.write_db() else: et = Email.EmailTarget(db) try: et.find_by_target_entity(group.entity_id) logger.info("Added address <%s>", address) except Errors.NotFoundError: et.populate(co.email_target_multi, target_entity_type=co.entity_group, target_entity_id=group.entity_id) et.write_db() logger.info("Created <%s>", address) ed = Email.EmailDomain(db) lp, dom = address.split('@') ed.find_by_domain(dom) ea = Email.EmailAddress(db) ea.populate(lp, ed.entity_id, et.entity_id) ea.write_db()
def get_email_server(account_id, local_db=db): """Return Host object for account's mail server.""" et = Email.EmailTarget(local_db) et.find_by_target_entity(account_id) server = Email.EmailServer(local_db) server.find(et.email_server_id) return server
def process_email_srv_data(uname, account_id, email_srv): if email_srv in ['NOMAIL', 'NOLOCAL']: logger.error("Bad email server %s for %s", email_srv, uname) return None email_server = Email.EmailServer(db) email_server_target = Email.EmailServerTarget(db) email_target = Email.EmailTarget(db) email_server.find_by_name(email_srv) email_server_id = email_server.entity_id try: email_target.find_by_email_target_attrs( target_entity_id=int(account_id)) except Errors.NotFoundError: logger.error("No email target for %s", uname) return None try: email_server_target.find(email_target.entity_id) logger.debug("Email-server is registered %s for %s", email_srv, uname) except Errors.NotFoundError: email_server_target.clear() email_server_target.populate(email_server_id, parent=email_target) email_server_target.write_db() logger.debug("Populated email-server %s for %s", email_srv, uname) email_server.clear() email_server_target.clear() email_target.clear()
def _get_person_owner_attributes(self, owner): """Return a dict with person tidbits for LDAP. FIXME: THIS MUST NOT FAIL with NotFoundError. """ def extract_email_from_account(account_id): try: et = Email.EmailTarget(self._db) et.find_by_target_entity(account_id) return [ "%s@%s" % (r['local_part'], r['domain']) for r in et.get_addresses(special=False) ] except Errors.NotFoundError: return [] # end extract_email_from_account result = dict() # uid try: account_id = owner.get_primary_account() acc = Factory.get("Account")(self._db) acc.find(account_id) result["uid"] = acc.account_name except Errors.NotFoundError: result["uid"] = None # ALL unames must go into 'voipSipUri'. And so must all e-mail # addresses. result["voipSipUri"] = list() for row in acc.search(owner_id=owner.entity_id): result["voipSipUri"].append(self._voipify(row["name"], None)) for address in extract_email_from_account(row["account_id"]): mangled = self._voipify(address, None) result["voipSipUri"].append(mangled) # mail - primary e-mail address. if result['uid']: try: et = Email.EmailTarget(self._db) et.find_by_target_entity(acc.entity_id) epat = Email.EmailPrimaryAddressTarget(self._db) epat.find(et.entity_id) ea = Email.EmailAddress(self._db) ea.find(epat.get_address_id()) result["mail"] = ea.get_address() except Errors.NotFoundError: result["mail"] = None # cn - grab system cached try: p_name = owner.get_name( self.const.system_cached, getattr(self.const, cereconf.DEFAULT_GECOS_NAME)) except Errors.NotFoundError: p_name = None result["cn"] = p_name return result
def delete_spread(self, spread): # # Pre-remove checks # spreads = [int(r['spread']) for r in self.get_spread()] if not spread in spreads: # user doesn't have this spread return # (Try to) perform the actual spread removal. ret = self.__super.delete_spread(spread) # Post-removal clean up if spread == self.const.spread_exchange_account: try: self.delete_trait(self.const.trait_exchange_mdb) self.write_db() except Errors.NotFoundError: pass try: et = Email.EmailTarget(self._db) et.find_by_target_entity(self.entity_id) et.email_target_type = self.const.email_target_deleted et.write_db() except Errors.NotFoundError: pass return ret
def main(): global db, co, ac, p, ou, et, logger logger = Factory.get_logger("cronjob") db = Factory.get('Database')() co = Factory.get('Constants')(db) ac = Factory.get('Account')(db) p = Factory.get('Person')(db) ou = Factory.get('OU')(db) et = Email.EmailTarget(db) txt_path = "/cerebrum/var/cache/txt" options, rest = getopt.getopt(sys.argv[1:], "t:", [ "txt-path=", ]) for option, value in options: if option in ("-t", "--txt-path"): txt_path = value # Load dicts with misc info. get_account_info() get_person_contact() # Dump OFK info f = SimilarSizeWriter("%s/ofk.txt" % txt_path, "w") f.max_pct_change = 10 users = process_txt_file(f) f.close()
def read_server(self, spread): mail_serv = Email.EmailServer(self._db) for row in mail_serv.list_email_server_ext(): self.serv_id2server[int( row['server_id'])] = [row['server_type'], row['name']] mail_targ = Email.EmailTarget(self._db) for row in mail_targ.list_email_server_targets(): self.targ2server_id[int(row['target_id'])] = int(row['server_id'])
def extract_email_from_account(account_id): try: et = Email.EmailTarget(self._db) et.find_by_target_entity(account_id) return ["%s@%s" % (r['local_part'], r['domain']) for r in et.get_addresses(special=False)] except Errors.NotFoundError: return []
def generate_email_data(db): co = Factory.get('Constants')(db) ac = Factory.get('Account')(db) u = text_decoder(db.encoding) email_spread = co.spread_email_account def get_valid_email_addrs(et): for row in et.get_addresses(): yield u'{0}@{1}'.format(u(row['local_part']), u(row['domain'])) def get_server_name(server_id): if not server_id: return None es = Email.EmailServer(db) es.find(server_id) return u(es.name) logger.debug("Fetching email data for users with email spread %s", text_type(email_spread)) for cnt, row in enumerate(ac.search(spread=email_spread), 1): if cnt % 1000 == 0: logger.debug('... account #%d ...', cnt) ac.clear() ac.find(row['account_id']) account_name = u(ac.account_name) # fetch primary address try: primary = ac.get_primary_mailaddress() except Errors.NotFoundError: logger.warn("No primary address for %s", account_name) continue # find email target for this account et = Email.EmailTarget(db) et.clear() try: et.find_by_target_entity(ac.entity_id) except Errors.NotFoundError: logger.warn("No email target for %s", account_name) continue valid_addrs = list(get_valid_email_addrs(et)) email_server_name = get_server_name(et.email_server_id) yield { 'account_name': account_name, 'default_address': primary, 'valid_addresses': valid_addrs, 'email_server_id': et.email_server_id, 'email_server_name': email_server_name, } logger.debug('Done fetching email data')
def get_email_target_and_address(address): ea = Email.EmailAddress(db) try: ea.find_by_address(address) except Errors.NotFoundError: return (None, None) et = Email.EmailTarget(db) et.find(ea.email_addr_target_id) return (et, ea)
def test_phonenumberchange_delay(self): """Fresh phone numbers should not be used if config says so.""" cereconf.INDIVIDUATION_PHONE_TYPES['system_fs']['types']['contact_mobile_phone']['delay'] = 7 cereconf.INDIVIDUATION_PHONE_TYPES['system_fs']['types']['contact_private_mobile']['delay'] = 7 ou = Factory.get('OU')(self.db) co = Factory.get('Constants')(self.db) ou.find_stedkode(0, 0, 0, 0) pe = self.createPerson(first_name='Miss', last_name='Test', studnr='007') ac = self.createAccount(pe, 'mstest') pe.populate_affiliation(source_system=co.system_fs, ou_id=ou.entity_id, affiliation=co.affiliation_student, status=co.affiliation_status_student_aktiv) pe.write_db() pe.populate_contact_info(source_system=co.system_fs, type=co.contact_mobile_phone, value='98012345') pe.write_db() # Create e-mail address for user ed = Email.EmailDomain(self.db) ed.populate('example.com', 'For testing') ed.write_db() et = Email.EmailTarget(self.db) et.populate(co.email_target_account, ac.entity_id, co.entity_account) et.write_db() ea = Email.EmailAddress(self.db) ea.populate('test', ed.entity_id, et.entity_id) ea.write_db() epa = Email.EmailPrimaryAddressTarget(self.db) epa.populate(ea.entity_id, et) epa.write_db() self.db.commit() # hack the create_date in change_log self.db.execute(""" UPDATE [:table schema=cerebrum name=change_log] SET tstamp = :tid WHERE subject_entity = :s_id AND change_type_id = :ct_id""", {'s_id': pe.entity_id, 'ct_id': co.person_create, 'tid': DateTime(2009, 10, 4),}) self.db.commit() d = self.client.callRemote('generate_token', id_type="externalid_studentnr", ext_id='007', username='******', phone_no='98012345', browser_token='a') d = self.assertFailure(d, error.Error) # TODO: test that sendmail is called with the correct to-address return d
def _has_email_target(self): """ Returns True if there is an EmailTarget for this account, False otherwise. """ et = Email.EmailTarget(self._db) try: et.find_by_target_entity(self.entity_id) except Errors.NotFoundError: return False return True
def list_email_addresses(ea): if not isinstance(ea, Email.EmailAddress): ea = find_email_address(ea) et = Email.EmailTarget(db.connection) et.find(ea.email_addr_target_id) return map( lambda (lp, dom, _a_id): { 'value': '{}@{}'.format(lp, dom), 'type': et.get_target_type() }, et.get_addresses())
def target2spread_populate(self): a = Factory.get('Account')(self._db) exspread = self.const.spread_exchange_account ttype = self.const.email_target_account et = Email.EmailTarget(self._db) exchangeaccounts = set() ret = set() for row in a.search(expire_start=None, spread=exspread): exchangeaccounts.add(int(row['account_id'])) for row in et.list_email_targets_ext(target_type=ttype): if int(row['target_entity_id']) in exchangeaccounts: ret.add(int(row['target_id'])) return ret
def _get_rt_email_target(self, queue, host): """ Get EmailTarget for an RT queue. """ et = Email.EmailTarget(self.db) try: et.find_by_alias(self._rt_pipe % { 'action': "correspond", 'queue': queue, 'host': host, }) except Errors.NotFoundError: raise CerebrumError("Unknown RT queue %s on host %s" % (queue, host)) return et
def _email_info_detail(self, acc): """ Get quotas from Cerebrum, and usage from Cyrus. """ # NOTE: Very similar to hiof and uio info = [] eq = Email.EmailQuota(self.db) # Get quota and usage try: eq.find_by_target_entity(acc.entity_id) et = Email.EmailTarget(self.db) et.find_by_target_entity(acc.entity_id) es = Email.EmailServer(self.db) es.find(et.email_server_id) if es.email_server_type == self.const.email_server_type_cyrus: used = 'N/A' limit = None pw = self.db._read_password(cereconf.CYRUS_HOST, cereconf.CYRUS_ADMIN) try: cyrus = imaplib.IMAP4(es.name) # IVR 2007-08-29 If the server is too busy, we do not want # to lock the entire bofhd. # 5 seconds should be enough cyrus.socket().settimeout(5) cyrus.login(cereconf.CYRUS_ADMIN, pw) res, quotas = cyrus.getquota("user." + acc.account_name) cyrus.socket().settimeout(None) if res == "OK": for line in quotas: try: folder, qtype, qused, qlimit = line.split() if qtype == "(STORAGE": used = str(int(qused)/1024) limit = int(qlimit.rstrip(")"))/1024 except ValueError: # line.split fails e.g. because quota isn't set # on server folder, junk = line.split() self.logger.warning( "No IMAP quota set for '%s'" % acc.account_name) used = "N/A" limit = None except (TimeoutException, socket.error): used = 'DOWN' except ConnectException, e: used = str(e) except imaplib.IMAP4.error, e: used = 'DOWN'
def create_distgroup_mailtarget(self): et = Email.EmailTarget(self._db) target_type = self.const.email_target_dl_group if self.is_expired(): raise self._db.IntegrityError( "Cannot make e-mail target for the expired group %s." % self.group_name) try: et.find_by_email_target_attrs(target_entity_id=self.entity_id) if et.email_target_type != target_type: et.email_target_type = target_type except Errors.NotFoundError: et.populate(target_type, self.entity_id, self.const.entity_group) et.write_db() self._create_distgroup_mailaddr(et)
def update_email_addresses(self): # Overriding default update_email_addresses as NIH does not require # email_server, Jazz, 2011-05-22 # Find, create or update a proper EmailTarget for this # account. et = Email.EmailTarget(self._db) target_type = self.const.email_target_account if self.is_expired() or self.is_reserved(): target_type = self.const.email_target_deleted changed = False try: et.find_by_email_target_attrs(target_entity_id=self.entity_id) if et.email_target_type != target_type: changed = True et.email_target_type = target_type except Errors.NotFoundError: # We don't want to create e-mail targets for reserved or # deleted accounts, but we do convert the type of existing # e-mail targets above. if target_type == self.const.email_target_deleted: return et.populate(target_type, self.entity_id, self.const.entity_account) et.write_db() # For deleted/reserved users, set expire_date for all of the # user's addresses, and don't allocate any new addresses. ea = Email.EmailAddress(self._db) if changed and cereconf.EMAIL_EXPIRE_ADDRESSES is not False: if target_type == self.const.email_target_deleted: seconds = cereconf.EMAIL_EXPIRE_ADDRESSES * 86400 expire_date = self._db.DateFromTicks(time.time() + seconds) else: expire_date = None for row in et.get_addresses(): ea.clear() ea.find(row['address_id']) ea.email_addr_expire_date = expire_date ea.write_db() # Active accounts shouldn't have an alias value (it is used # for failure messages) if changed and target_type == self.const.email_target_account: if et.email_target_alias is not None: et.email_target_alias = None et.write_db() if target_type == self.const.email_target_deleted: return self._update_email_address_domains(et)
def _update_email_server(self, server_name): es = Email.EmailServer(self._db) et = Email.EmailTarget(self._db) es.find_by_name(server_name) try: et.find_by_email_target_attrs(target_entity_id = self.entity_id) except Errors.NotFoundError: # Not really sure about this. it is done at UiO, but maybe it is not # right to make en email_target if one is not found?? et.clear() et.populate(self.const.email_target_account, self.entity_id, self.const.entity_account) et.write_db() et.email_server_id = es.entity_id et.write_db() return et
def fish_information(suffix, local_part, domain, listname): """Generate an entry for sympa info for the specified address. @type address: basestring @param address: Is the address we are looking for (we locate ETs based on the alias value in _sympa_addr2alias). @type et: EmailTarget instance @rtype: sequence (of dicts of basestring to basestring) @return: A sequence of dicts suitable for merging into return value from email_info_sympa. """ result = [] address = "%(local_part)s-%(suffix)s@%(domain)s" % locals() target_alias = None for a, alias in self._sympa_addr2alias: a = a % locals() if a == address: target_alias = alias % locals() break # IVR 2008-08-05 TBD Is this an error? All sympa ETs must have an # alias in email_target. if target_alias is None: return result try: # Do NOT change et's (parameter's) state. et_tmp = Email.EmailTarget(self.db) et_tmp.clear() et_tmp.find_by_alias(target_alias) except Errors.NotFoundError: return result addrs = et_tmp.get_addresses() if not addrs: return result pattern = '%(local_part)s@%(domain)s' result.append({'sympa_' + suffix + '_1': pattern % addrs[0]}) for idx in range(1, len(addrs)): result.append({'sympa_' + suffix: pattern % addrs[idx]}) return result
def deactivate_dl_mailtarget(self): """Set the associated EmailTargets type to deleted.""" et = Email.EmailTarget(self._db) # we are not supposed to remove e-mail targets because # of the danger of re-using target_type = self.const.email_target_deleted try: et.find_by_email_target_attrs(target_entity_id=self.entity_id) et.email_target_type = target_type et.write_db() except Errors.NotFoundError: # no such target, nothing to do # TBD: should we raise an exception? # I judge an exception not to be necessary # as the situation is not likely to occur at all # (Jazz, 2013-12) return
def _email_info_detail(self, acc): info = [] try: et = Email.EmailTarget(self.db) et.find_by_target_entity(acc.entity_id) homemdb = None tmp = acc.get_trait(self.const.trait_exchange_mdb) if tmp is not None: homemdb = tmp['strval'] else: homemdb = 'N/A' # should not be shown for accounts without exchange-spread, needs # fixin', Jazz 2011-02-21 info.append({'homemdb': homemdb}) except Errors.NotFoundError: pass return info
def email_move(self, operator, uname, server): acc = self._get_account(uname) self.ba.can_email_move(operator.get_entity_id(), acc) et = Email.EmailTarget(self.db) et.find_by_target_entity(acc.entity_id) old_server = et.email_server_id es = Email.EmailServer(self.db) try: es.find_by_name(server) except Errors.NotFoundError: raise CerebrumError("%r is not registered as an e-mail server" % server) if old_server == es.entity_id: raise CerebrumError("User is already at %s" % server) et.email_server_id = es.entity_id et.write_db() return "OK, updated e-mail server for %s (to %s)" % (uname, server)
def _update_email_server(self): es = Email.EmailServer(self._db) et = Email.EmailTarget(self._db) if self.is_employee(): server_name = 'postboks' else: server_name = 'student_placeholder' es.find_by_name(server_name) try: et.find_by_email_target_attrs(target_entity_id=self.entity_id) except Errors.NotFoundError: et.populate(self.const.email_target_account, self.entity_id, self.const.entity_account) et.write_db() if not et.email_server_id: et.email_server_id = es.entity_id et.write_db() return et
def _update_email_server(self, spread, force=False): """ Update `email_server` for the Account to a value based on spread. The `email_server` for the account's `EmailTarget` is set to an appropriate server, depending on the given spread. IMAP users will for instance be given an IMAP server, while Exchange 2013 users gets an Exchange 2013 server. Note: If the account doesn't have an `EmailTarget`, a new one will be created, which normally affects the mail systems. Use with care. :param _SpreadCode spread: The given spread, which could affect what server that gets chosen. :param bool force: If False, the server is not updated if it is already set. :rtype: `Email.EmailTarget` :return: The affected `EmailTarget`. """ es = Email.EmailServer(self._db) et = Email.EmailTarget(self._db) server_name = 'mail-imap2' if spread in (int(self.const.spread_exchange_account), int(self.const.spread_uia_office_365)): server_name = 'outlook.uia.no' if spread == int(self.const.spread_exchange_acc_old): server_name = 'exchkrs01.uia.no' es.find_by_name(server_name) try: et.find_by_email_target_attrs(target_entity_id=self.entity_id) except Errors.NotFoundError: # Not really sure about this. # It is done at UiO, but maybe it is not # right to make en email_target if one is not found?? et.clear() et.populate(self.const.email_target_account, self.entity_id, self.const.entity_account) et.write_db() if force or not et.email_server_id: et.email_server_id = es.entity_id et.write_db() return et
def _get_email_target_and_address(self, address): # Support DistributionGroup email target lookup try: return super(EmailCommands, self)._get_email_target_and_address(address) except CerebrumError as e: # Not found, maybe distribution group? try: dlgroup = Utils.Factory.get("DistributionGroup")(self.db) dlgroup.find_by_name(address) et = Email.EmailTarget(self.db) et.find_by_target_entity(dlgroup.entity_id) epa = Email.EmailPrimaryAddressTarget(self.db) epa.find(et.entity_id) ea = Email.EmailAddress(self.db) ea.find(epa.email_primaddr_id) return et, ea except Errors.NotFoundError: raise e