def del_destinations(self, destinations, warnings=None): """Deletes the specified ``destinations`` from the catchall alias.""" destinations = set(destinations) assert destinations and all( isinstance(dest, EmailAddress) for dest in destinations) if warnings is not None: assert isinstance(warnings, list) if not self._dests: raise AErr( _("There are no catch-all aliases defined for " "domain '%s'.") % self._domain, NO_SUCH_ALIAS, ) unknown = destinations.difference(set(self._dests)) if unknown: destinations.intersection_update(set(self._dests)) if warnings is not None: warnings.extend(unknown) if not destinations: raise AErr( _("No suitable destinations left to remove from the " "catch-all alias of domain '%s'.") % self._domain, NO_SUCH_ALIAS, ) self._delete(destinations) for destination in destinations: self._dests.remove(destination)
def del_destinations(self, destinations, warnings=None): """Delete the specified `EmailAddress`es of *destinations* from the alias's destinations. """ destinations = set(destinations) assert destinations and all( isinstance(dest, EmailAddress) for dest in destinations ) if warnings is not None: assert isinstance(warnings, list) if self._addr in destinations: destinations.remove(self._addr) if warnings is not None: warnings.append(self._addr) if not self._dests: raise AErr(_("The alias '%s' does not exist.") % self._addr, NO_SUCH_ALIAS) unknown = destinations.difference(set(self._dests)) if unknown: destinations.intersection_update(set(self._dests)) if warnings is not None: warnings.extend(unknown) if not destinations: raise AErr( _("No suitable destinations left to remove from alias " "'%s'.") % self._addr, NO_SUCH_ALIAS, ) self._delete(destinations) for destination in destinations: self._dests.remove(destination)
def save(self): """Save the new Account in the database.""" if not self._new: raise AErr( _("The account '%s' already exists.") % self._addr, ACCOUNT_EXISTS) if not self._passwd: raise AErr( _("No password set for account: '%s'") % self._addr, ACCOUNT_MISSING_PASSWORD, ) self._prepare( MailLocation( self._dbh, mbfmt=cfg_dget("mailbox.format"), directory=cfg_dget("mailbox.root"), )) dbc = self._dbh.cursor() # fmt: off dbc.execute( "INSERT INTO users (" " local_part, " " passwd, " " uid, " " gid, " " mid, " " qid, " " ssid, " " tid, " " note" ") " "VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)", ( self._addr.localpart, pwhash(self._passwd, user=self._addr), self._uid, self._domain.gid, self._mail.mid, self._qlimit.qid if self._qlimit else None, self._services.ssid if self._services else None, self._transport.tid if self._transport else None, self._note, ), ) # fmt: on self._dbh.commit() dbc.close() self._new = False
def _chk_state(self): """Raise an AccountError if the Account is new - not yet saved in the database.""" if self._new: raise AErr( _("The account '%s' does not exist.") % self._addr, NO_SUCH_ACCOUNT)
def modify(self, field, value): """Update the Account's *field* to the new *value*. Possible values for *field* are: 'name', 'note' and 'pwhash'. Arguments: `field` : str The attribute name: 'name', 'note' or 'pwhash' `value` : str The new value of the attribute. """ if field not in ("name", "note", "pwhash"): raise AErr(_("Unknown field: '%s'") % field, INVALID_ARGUMENT) if field == "pwhash": field = "passwd" self._chk_state() dbc = self._dbh.cursor() # fmt: off dbc.execute(f"UPDATE users " f"SET {field} = %s " f"WHERE uid = %s", (value, self._uid)) # fmt: on if dbc.rowcount > 0: self._dbh.commit() dbc.close()
def _check_expansion(self, count_new): """Checks the current expansion limit of the alias.""" postconf = Postconf(cfg_dget("bin.postconf")) limit = int(postconf.read("virtual_alias_expansion_limit")) dcount = len(self._dests) failed = False if dcount == limit or dcount + count_new > limit: failed = True errmsg = _( """Cannot add %(count_new)i new destination(s) to catch-all alias for domain '%(domain)s'. Currently this alias expands into %(count)i/%(limit)i recipients. %(count_new)i additional destination(s) will render this alias unusable. Hint: Increase Postfix' virtual_alias_expansion_limit""") elif dcount > limit: failed = True errmsg = _( """Cannot add %(count_new)i new destination(s) to catch-all alias for domain '%(domain)s'. This alias already exceeds its expansion limit \ (%(count)i/%(limit)i). So its unusable, all messages addressed to this alias will be bounced. Hint: Delete some destination addresses.""") if failed: raise AErr( errmsg % { "domain": self._domain, "count": dcount, "limit": limit, "count_new": count_new, }, ALIAS_EXCEEDS_EXPANSION_LIMIT, )
def __init__(self, dbh, address): """Creates a new Account instance. When an account with the given *address* could be found in the database all relevant data will be loaded. Arguments: `dbh` : psycopg2._psycopg.connection A database connection for the database access. `address` : vmm.EmailAddress.EmailAddress The e-mail address of the (new) Account. """ if not isinstance(address, EmailAddress): raise TypeError("Argument 'address' is not an EmailAddress") self._addr = address self._dbh = dbh self._domain = Domain(self._dbh, self._addr.domainname) if not self._domain.gid: # TP: Hm, what “quotation marks” should be used? # If you are unsure have a look at: # http://en.wikipedia.org/wiki/Quotation_mark,_non-English_usage raise AErr( _("The domain '%s' does not exist.") % self._addr.domainname, NO_SUCH_DOMAIN, ) self._uid = 0 self._mail = None self._qlimit = None self._services = None self._transport = None self._note = None self._passwd = None self._new = True self._load()
def get_destinations(self): """Returns an iterator for all destinations of the catchall alias.""" if not self._dests: raise AErr( _("There are no catch-all aliases defined for " "domain '%s'.") % self._domain, NO_SUCH_ALIAS, ) return iter(self._dests)
def delete(self): """Deletes all catchall destinations for the domain.""" if not self._dests: raise AErr( _("There are no catch-all aliases defined for " "domain '%s'.") % self._domain, NO_SUCH_ALIAS, ) self._delete() del self._dests[:]
def __init__(self, dbh, domain): self._domain = domain self._dbh = dbh self._gid = get_gid(self._dbh, self.domain) if not self._gid: raise AErr( _("The domain '%s' does not exist.") % self.domain, NO_SUCH_DOMAIN) self._dests = [] self._load_dests()
def set_password(self, password): """Set a password for the new Account. If you want to update the password of an existing Account use Account.modify(). Argument: `password` : basestring The password for the new Account. """ if not self._new: raise AErr( _("The account '%s' already exists.") % self._addr, ACCOUNT_EXISTS) if not isinstance(password, str) or not password: raise AErr( _("Could not accept password: '******'") % password, ACCOUNT_MISSING_PASSWORD, ) self._passwd = password
def __init__(self, dbh, address): assert isinstance(address, EmailAddress) self._addr = address self._dbh = dbh self._gid = get_gid(self._dbh, self._addr.domainname) if not self._gid: raise AErr( _("The domain '%s' does not exist.") % self._addr.domainname, NO_SUCH_DOMAIN, ) self._dests = [] self._load_dests()
def get_account_by_uid(uid, dbh): """Search an Account by its UID. This function returns a dict (keys: 'address', 'gid' and 'uid'), if an Account with the given *uid* exists. Argument: `uid` : int The Account unique ID. `dbh` : psycopg2._psycopg.connection a database connection for the database access. """ try: uid = int(uid) except ValueError: raise AErr(_("UID must be an integer."), INVALID_ARGUMENT) if uid < 1: raise AErr(_("UID must be greater than 0."), INVALID_ARGUMENT) dbc = dbh.cursor() # fmt: off dbc.execute( "SELECT local_part||'@'|| domain_name.domainname AS address, " " uid, users.gid, note " "FROM users " "LEFT JOIN domain_name ON (domain_name.gid = users.gid AND is_primary) " "WHERE uid = %s", (uid, ), ) # fmt: on info = dbc.fetchone() dbc.close() if not info: raise AErr( _("There is no account with the UID: '%d'") % uid, NO_SUCH_ACCOUNT) info = dict(list(zip(("address", "uid", "gid", "note"), info))) return info
def get_info(self): """Returns a dict with some information about the Account. The keys of the dict are: 'address', 'gid', 'home', 'imap' 'mail_location', 'name', 'pop3', 'sieve', 'smtp', transport', 'uid', 'uq_bytes', 'uq_messages', 'ql_bytes', 'ql_messages', and 'ql_domaindefault'. """ self._chk_state() dbc = self._dbh.cursor() # fmt: off dbc.execute( "SELECT (" " name, " " CASE WHEN bytes IS NULL THEN 0 ELSE bytes END, " " CASE WHEN messages IS NULL THEN 0 ELSE messages END " ") " "FROM users " "LEFT JOIN userquota USING (uid) " "WHERE users.uid = %s", (self._uid, ), ) # fmt: on info = dbc.fetchone() dbc.close() if info: info = dict(list(zip(("name", "uq_bytes", "uq_messages"), info))) info.update(self._get_info_serviceset()) info["address"] = self._addr info["gid"] = self._domain.gid info["home"] = "%s/%s" % (self._domain.directory, self._uid) info["mail_location"] = self._mail.mail_location if self._qlimit: info["ql_bytes"] = self._qlimit.bytes info["ql_messages"] = self._qlimit.messages info["ql_domaindefault"] = False else: info["ql_bytes"] = self._domain.quotalimit.bytes info["ql_messages"] = self._domain.quotalimit.messages info["ql_domaindefault"] = True info["transport"] = self._get_info_transport() info["note"] = self._note info["uid"] = self._uid return info # nearly impossible‽ raise AErr( _("Could not fetch information for account: '%s'") % self._addr, NO_SUCH_ACCOUNT, )
def _prepare(self, maillocation): """Check and set different attributes - before we store the information in the database. """ if maillocation.dovecot_version > cfg_dget("misc.dovecot_version"): raise AErr( _("The mailbox format '%(mbfmt)s' requires Dovecot " ">= v%(version)s.") % { "mbfmt": maillocation.mbformat, "version": version_str(maillocation.dovecot_version), }, INVALID_MAIL_LOCATION, ) transport = self._transport or self._domain.transport validate_transport(transport, maillocation) self._mail = maillocation self._set_uid()
def delete(self, force=False): """Delete the Account from the database. Argument: `force` : bool if *force* is `True`, all aliases, which points to the Account, will be also deleted. If there are aliases and *force* is `False`, an AccountError will be raised. """ if not isinstance(force, bool): raise TypeError("force must be a bool") self._chk_state() dbc = self._dbh.cursor() if force: # fmt: off dbc.execute("DELETE FROM users " "WHERE uid = %s", (self._uid, )) # fmt: on # delete also all aliases where the destination address is the same # as for this account. # fmt: off dbc.execute("DELETE FROM alias " "WHERE destination = %s", (str(self._addr), )) # fmt: on self._dbh.commit() else: # check first for aliases a_count = self._count_aliases() if a_count > 0: dbc.close() raise AErr( _("There are %(count)d aliases with the " "destination address '%(address)s'.") % { "count": a_count, "address": self._addr }, ALIAS_PRESENT, ) # fmt: off dbc.execute("DELETE FROM users " "WHERE uid = %s", (self._uid, )) # fmt: on self._dbh.commit() dbc.close() self._new = True self._uid = 0 self._addr = self._dbh = self._domain = self._passwd = None self._mail = self._qlimit = self._services = self._transport = None
def delete(self): """Deletes the alias with all its destinations.""" if not self._dests: raise AErr(_("The alias '%s' does not exist.") % self._addr, NO_SUCH_ALIAS) self._delete() del self._dests[:]
def get_destinations(self): """Returns an iterator for all destinations of the alias.""" if not self._dests: raise AErr(_("The alias '%s' does not exist.") % self._addr, NO_SUCH_ALIAS) return iter(self._dests)