def _delete_domain_dir(self, domdir, gid): """Delete a domain's directory. Arguments: `domdir` : basestring The domain's directory (commonly DomainObj.directory) `gid` : int The domain's GID (commonly DomainObj.gid) """ assert isinstance(domdir, str) and isinstance(gid, int) if gid < MIN_GID: raise VMMError( _("GID '%(gid)u' is less than '%(min_gid)u'.") % { "gid": gid, "min_gid": MIN_GID }, DOMAINDIR_GROUP_MISMATCH, ) if domdir.count(".."): raise VMMError( _('Found ".." in domain directory path: %s') % domdir, FOUND_DOTS_IN_PATH, ) if not lisdir(domdir): self._warnings.append(_("No such directory: %s") % domdir) return dirst = os.lstat(domdir) if dirst.st_gid != gid: raise VMMError( _("Detected group mismatch in domain directory: " "%s") % domdir, DOMAINDIR_GROUP_MISMATCH, ) rmtree(domdir, ignore_errors=True)
def _make_domain_dir(self, domain): """Create a directory for the `domain` and its accounts.""" cwd = os.getcwd() hashdir, domdir = domain.directory.split(os.path.sep)[-2:] dir_created = False os.chdir(self._cfg.dget("misc.base_directory")) old_umask = os.umask(0o022) if not os.path.exists(hashdir): os.mkdir(hashdir, 0o711) os.chown(hashdir, 0, 0) dir_created = True if not dir_created and not lisdir(hashdir): raise VMMError( _("'%s' is not a directory.") % hashdir, NO_SUCH_DIRECTORY) if os.path.exists(domain.directory): raise VMMError( _("The file/directory '%s' already exists.") % domain.directory, VMM_ERROR, ) os.mkdir(os.path.join(hashdir, domdir), self._cfg.dget("domain.directory_mode")) os.chown(domain.directory, 0, domain.gid) os.umask(old_umask) os.chdir(cwd)
def _doveadmpw(password, scheme, encoding): """Communicates with Dovecot's doveadm and returns the hashed password: {scheme[.encoding]}hash """ if encoding: scheme = ".".join((scheme, encoding)) cmd_args = [ cfg_dget("bin.doveadm"), "pw", "-s", scheme, "-p", get_unicode(password), ] process = Popen(cmd_args, stdout=PIPE, stderr=PIPE) stdout, stderr = process.communicate() if process.returncode: raise VMMError(stderr.strip().decode(ENCODING), VMM_ERROR) hashed = stdout.strip().decode(ENCODING) if not hashed.startswith("{%s}" % scheme): raise VMMError( "Unexpected result from %s: %s" % (cfg_dget("bin.doveadm"), hashed), VMM_ERROR, ) return hashed
def user_password(self, emailaddress, password, scheme=None): """Wrapper for Account.update_password(...).""" if not isinstance(password, str) or not password: raise VMMError( _("Could not accept password: '******'") % password, INVALID_ARGUMENT) acc = self._get_account(emailaddress) if not acc: raise VMMError( _("The account '%s' does not exist.") % acc.address, NO_SUCH_ACCOUNT) acc.update_password(password, scheme)
def user_pwhash(self, emailaddress, pwhash): """Wrapper for Account.modify('pwhash', ...)""" scheme = extract_scheme(pwhash) if not scheme: raise VMMError(_("Missing {SCHEME} prefix from password hash."), INVALID_ARGUMENT) else: scheme, encoding = verify_scheme(scheme) # or die … acc = self._get_account(emailaddress) if not acc: raise VMMError( _("The account '%s' does not exist.") % acc.address, NO_SUCH_ACCOUNT) acc.modify("pwhash", pwhash)
def user_transport(self, emailaddress, transport): """Wrapper for Account.update_transport(Transport).""" if not isinstance(transport, str) or not transport: raise VMMError( _("Could not accept transport: '%s'") % transport, INVALID_ARGUMENT) acc = self._get_account(emailaddress) if not acc: raise VMMError( _("The account '%s' does not exist.") % acc.address, NO_SUCH_ACCOUNT) transport = (None if transport == "domain" else Transport( self._dbh, transport=transport)) acc.update_transport(transport)
def _delete_home(self, domdir, uid, gid): """Delete a user's home directory. Arguments: `domdir` : basestring The directory of the domain the user belongs to (commonly AccountObj.domain.directory) `uid` : int The user's UID (commonly AccountObj.uid) `gid` : int The user's GID (commonly AccountObj.gid) """ assert all(isinstance(xid, int) for xid in (uid, gid)) and isinstance(domdir, str) if uid < MIN_UID or gid < MIN_GID: raise VMMError( _("UID '%(uid)u' and/or GID '%(gid)u' are less " "than %(min_uid)u/%(min_gid)u.") % { "uid": uid, "gid": gid, "min_gid": MIN_GID, "min_uid": MIN_UID }, MAILDIR_PERM_MISMATCH, ) if domdir.count(".."): raise VMMError( _('Found ".." in domain directory path: %s') % domdir, FOUND_DOTS_IN_PATH, ) if not lisdir(domdir): raise VMMError( _("No such directory: %s") % domdir, NO_SUCH_DIRECTORY) os.chdir(domdir) userdir = "%s" % uid if not lisdir(userdir): self._warnings.append( _("No such directory: %s") % os.path.join(domdir, userdir)) return mdstat = os.lstat(userdir) if (mdstat.st_uid, mdstat.st_gid) != (uid, gid): raise VMMError( _("Detected owner/group mismatch in home " "directory."), MAILDIR_PERM_MISMATCH, ) rmtree(userdir, ignore_errors=True)
def user_add(self, emailaddress, password=None, note=None): """Override the parent user_add() - add the interactive password dialog. Returns the generated password, if account.random_password == True. """ acc = self._get_account(emailaddress) if acc: raise VMMError( _("The account '%s' already exists.") % acc.address, ACCOUNT_EXISTS) self._is_other_address(acc.address, TYPE_ACCOUNT) should_create_random_password = self._cfg.dget( "account.random_password") if password is None: if should_create_random_password: password = randompw(self._cfg.dget("account.password_length")) else: password = read_pass() acc.set_password(password) if note: acc.set_note(note) acc.save() self._make_account_dirs(acc) return password if should_create_random_password else None
def read_pass(): """Interactive 'password chat', returns the password in plain format. Throws a VMMError after the third failure. """ # TP: Please preserve the trailing space. readp_msg0 = _("Enter new password: "******"Retype new password: "******"Too many failures - try again later."), VMM_TOO_MANY_FAILURES) clear0 = getpass(prompt=readp_msg0) clear1 = getpass(prompt=readp_msg1) if clear0 != clear1: failures += 1 w_err(0, _("Sorry, passwords do not match.")) continue if not clear0: failures += 1 w_err(0, _("Sorry, empty passwords are not permitted.")) continue mismatched = False return clear0
def _read(self, parameter): """Ask postconf for the value of a single configuration parameter.""" stdout, stderr = Popen([self._bin, "-h", parameter], stdout=PIPE, stderr=PIPE).communicate() if stderr: raise VMMError(stderr.strip().decode(), VMM_ERROR) return stdout.strip().decode()
def user_note(self, emailaddress, note): """Wrapper for Account.modify('note', ...).""" acc = self._get_account(emailaddress) if not acc: raise VMMError( _("The account '%s' does not exist.") % acc.address, NO_SUCH_ACCOUNT) acc.modify("note", note)
def address_list(self, typelimit, pattern=None): """TODO""" llike = dlike = False lpattern = dpattern = None if pattern: parts = pattern.split("@", 2) if len(parts) == 2: # The pattern includes '@', so let's treat the # parts separately to allow for pattern search like %@domain.% lpattern = parts[0] llike = lpattern.startswith("%") or lpattern.endswith("%") dpattern = parts[1] dlike = dpattern.startswith("%") or dpattern.endswith("%") checkp = lpattern.strip("%") if llike else lpattern if len(checkp) > 0 and re.search(RE_LOCALPART, checkp): raise VMMError( _("The pattern '%s' contains invalid " "characters.") % pattern, LOCALPART_INVALID, ) else: # else just match on domains # (or should that be local part, I don't know…) dpattern = parts[0] dlike = dpattern.startswith("%") or dpattern.endswith("%") checkp = dpattern.strip("%") if dlike else dpattern if len(checkp) > 0 and not RE_DOMAIN_SEARCH.match(checkp): raise VMMError( _("The pattern '%s' contains invalid " "characters.") % pattern, DOMAIN_INVALID, ) self._db_connect() from vmm.common import search_addresses return search_addresses( self._dbh, typelimit=typelimit, lpattern=lpattern, llike=llike, dpattern=dpattern, dlike=dlike, )
def _check_parameter(self, parameter): """Check that the `parameter` looks like a configuration parameter. If not, a VMMError will be raised.""" if not self.__class__._parameter_re.match(parameter): raise VMMError( _("The value '%s' does not look like a valid " "Postfix configuration parameter name.") % parameter, VMM_ERROR, )
def user_services(self, emailaddress, *services): """Wrapper around Account.update_serviceset().""" acc = self._get_account(emailaddress) if not acc: raise VMMError( _("The account '%s' does not exist.") % acc.address, NO_SUCH_ACCOUNT) if len(services) == 1 and services[0] == "domain": serviceset = None else: kwargs = dict.fromkeys(SERVICES, False) for service in set(services): if service not in SERVICES: raise VMMError( _("Unknown service: '%s'") % service, UNKNOWN_SERVICE) kwargs[service] = True serviceset = ServiceSet(self._dbh, **kwargs) acc.update_serviceset(serviceset)
def alias_info(self, aliasaddress): """Returns an iterator object for all destinations (`EmailAddress` instances) for the `Alias` with the given *aliasaddress*.""" alias = self._get_alias(aliasaddress) if alias: return alias.get_destinations() if not self._is_other_address(alias.address, TYPE_ALIAS): raise VMMError( _("The alias '%s' does not exist.") % alias.address, NO_SUCH_ALIAS)
def user_info(self, emailaddress, details=None): """Wrapper around Account.get_info(...)""" if details not in (None, "du", "aliases", "full"): raise VMMError( _("Invalid argument: '%s'") % details, INVALID_ARGUMENT) acc = self._get_account(emailaddress) if not acc: if not self._is_other_address(acc.address, TYPE_ACCOUNT): raise VMMError( _("The account '%s' does not exist.") % acc.address, NO_SUCH_ACCOUNT) info = acc.get_info() if self._cfg.dget("account.disk_usage") or details in ("du", "full"): path = os.path.join(acc.home, acc.mail_location.directory) info["disk usage"] = self._get_disk_usage(path) if details in (None, "du"): return info if details in ("aliases", "full"): return (info, acc.get_aliases()) return info
def relocated_info(self, emailaddress): """Returns the target address of the relocated user with the given *emailaddress*.""" relocated = self._get_relocated(emailaddress) if relocated: return relocated.get_info() if not self._is_other_address(relocated.address, TYPE_RELOCATED): raise VMMError( _("The relocated user '%s' does not exist.") % relocated.address, NO_SUCH_RELOCATED, )
def _read_multi(self, parameters): """Ask postconf for multiple configuration parameters. Returns a dict parameter: value items.""" cmd = [self._bin] cmd.extend(parameter[1:] for parameter in parameters) stdout, stderr = Popen(cmd, stdout=PIPE, stderr=PIPE).communicate() if stderr: raise VMMError(stderr.strip().decode(), VMM_ERROR) par_val = {} for line in stdout.decode().splitlines(): par, val = line.split(" = ") par_val[par] = val return par_val
def verify_scheme(scheme): """Checks if the password scheme *scheme* is known and supported by the configured `misc.dovecot_version`. The *scheme* maybe a password scheme's name (e.g.: 'PLAIN') or a scheme name with a encoding suffix (e.g. 'PLAIN.BASE64'). If the scheme is known and supported by the used Dovecot version, a tuple ``(scheme, encoding)`` will be returned. The `encoding` in the tuple may be `None`. Raises a `VMMError` if the password scheme: * is unknown * depends on a newer Dovecot version * has a unknown encoding suffix """ assert isinstance(scheme, str), "Not a str: {!r}".format(scheme) scheme_encoding = scheme.upper().split(".") scheme = scheme_encoding[0] if scheme not in _scheme_info: raise VMMError( _("Unsupported password scheme: '%s'") % scheme, VMM_ERROR) if cfg_dget("misc.dovecot_version") < _scheme_info[scheme][1]: raise VMMError( _("The password scheme '%(scheme)s' requires Dovecot " ">= v%(version)s.") % { "scheme": scheme, "version": version_str(_scheme_info[scheme][1]) }, VMM_ERROR, ) if len(scheme_encoding) > 1: if scheme_encoding[1] not in ("B64", "BASE64", "HEX"): raise VMMError( _("Unsupported password encoding: '%s'") % scheme_encoding[1], VMM_ERROR) encoding = scheme_encoding[1] else: encoding = None return scheme, encoding
def user_add(self, emailaddress, password, note=None): """Wrapper around Account.set_password() and Account.save().""" acc = self._get_account(emailaddress) if acc: raise VMMError( _("The account '%s' already exists.") % acc.address, ACCOUNT_EXISTS) self._is_other_address(acc.address, TYPE_ACCOUNT) acc.set_password(password) if note: acc.set_note(note) acc.save() self._make_account_dirs(acc)
def configure(self, section=None): """Starts the interactive configuration. Configures in interactive mode options in the given ``section``. If no section is given (default) all options from all sections will be prompted. """ if section is None: self._cfg.configure(self._cfg.sections()) elif self._cfg.has_section(section): self._cfg.configure([section]) else: raise VMMError( _("Invalid section: '%s'") % section, INVALID_SECTION)
def _chkenv(self): """Make sure our base_directory is a directory and that all required executables exists and are executable. If not, a VMMError will be raised""" dir_created = False basedir = self._cfg.dget("misc.base_directory") if not os.path.exists(basedir): old_umask = os.umask(0o006) os.makedirs(basedir, 0o771) os.chown(basedir, 0, 0) os.umask(old_umask) dir_created = True if not dir_created and not lisdir(basedir): raise VMMError( _("'%(path)s' is not a directory.\n(%(cfg_file)s: " "section 'misc', option 'base_directory')") % { "path": basedir, "cfg_file": self._cfg_fname }, NO_SUCH_DIRECTORY, ) for opt, val in self._cfg.items("bin"): try: exec_ok(val) except VMMError as err: if err.code in (NO_SUCH_BINARY, NOT_EXECUTABLE): raise VMMError( err.msg + _("\n(%(cfg_file)s: section " "'bin', option '%(option)s')") % { "cfg_file": self._cfg_fname, "option": opt }, err.code, ) else: raise
def edit(self, parameter, value): """Set the `parameter`'s value to `value`. Arguments: `parameter` : str the name of a Postfix configuration parameter `value` : str the parameter's new value. """ self._check_parameter(parameter) stderr = Popen((self._bin, "-e", parameter + "=" + str(value)), stderr=PIPE).communicate()[1] if stderr: raise VMMError(stderr.strip().decode(), VMM_ERROR)
def user_quotalimit(self, emailaddress, bytes_, messages=0): """Wrapper for Account.update_quotalimit(QuotaLimit).""" acc = self._get_account(emailaddress) if not acc: raise VMMError( _("The account '%s' does not exist.") % acc.address, NO_SUCH_ACCOUNT) if bytes_ == "domain": quotalimit = None else: if not all(isinstance(i, int) for i in (bytes_, messages)): raise TypeError("'bytes_' and 'messages' have to be " "integers or longs.") quotalimit = QuotaLimit(self._dbh, bytes=bytes_, messages=messages) acc.update_quotalimit(quotalimit)
def user_password(self, emailaddress, password=None, scheme=None): """Override the parent user_password() - add the interactive password dialog.""" acc = self._get_account(emailaddress) if not acc: raise VMMError( _("The account '%s' does not exist.") % acc.address, NO_SUCH_ACCOUNT) if scheme: scheme, encoding = verify_scheme(scheme) if encoding: scheme = "%s.%s" % (scheme, encoding) if not isinstance(password, str) or not password: password = read_pass() acc.update_password(password, scheme)
def domain_list(self, pattern=None): """Wrapper around function search() from module Domain.""" from vmm.domain import search like = False if pattern and (pattern.startswith("%") or pattern.endswith("%")): like = True if not RE_DOMAIN_SEARCH.match(pattern.strip("%")): raise VMMError( _("The pattern '%s' contains invalid " "characters.") % pattern, DOMAIN_INVALID, ) self._db_connect() return search(self._dbh, pattern=pattern, like=like)
def _doveadm_create(self, mailboxes, subscribe): """Wrap around Dovecot's doveadm""" cmd_args = [ cfg_dget("bin.doveadm"), "mailbox", "create", "-u", str(self._user.address), ] if subscribe: cmd_args.append("-s") cmd_args.extend(mailboxes) process = Popen(cmd_args, stderr=PIPE) stderr = process.communicate()[1] if process.returncode: e_msg = _("Failed to create mailboxes: %r\n") % mailboxes raise VMMError(e_msg + stderr.strip().decode(ENCODING), VMM_ERROR)
def _find_cfg_file(self): """Search the CFG_FILE in CFG_PATH. Raise a VMMError when no vmm.cfg could be found. """ for path in CFG_PATH.split(":"): tmp = os.path.join(path, CFG_FILE) if os.path.isfile(tmp): self._cfg_fname = tmp break if not self._cfg_fname: raise VMMError( _("Could not find '%(cfg_file)s' in: " "'%(cfg_path)s'") % { "cfg_file": CFG_FILE, "cfg_path": CFG_PATH }, CONF_NOFILE, )
def domain_info(self, domainname, details=None): """Wrapper around Domain.get_info(), Domain.get_accounts(), Domain.get_aliase_names(), Domain.get_aliases() and Domain.get_relocated.""" if details not in [ None, "accounts", "aliasdomains", "aliases", "full", "relocated", "catchall", ]: raise VMMError( _("Invalid argument: '%s'") % details, INVALID_ARGUMENT) dom = self._get_domain(domainname) dominfo = dom.get_info() if dominfo["domain name"].startswith( "xn--") or dominfo["domain name"].count(".xn--"): dominfo["domain name"] += " (%s)" % dominfo["domain name"].encode( "utf-8").decode("idna") if details is None: return dominfo elif details == "accounts": return (dominfo, dom.get_accounts()) elif details == "aliasdomains": return (dominfo, dom.get_aliase_names()) elif details == "aliases": return (dominfo, dom.get_aliases()) elif details == "relocated": return (dominfo, dom.get_relocated()) elif details == "catchall": return (dominfo, dom.get_catchall()) else: return ( dominfo, dom.get_aliase_names(), dom.get_accounts(), dom.get_aliases(), dom.get_relocated(), dom.get_catchall(), )
def _db_connect(self): """Return a new psycopg2 connection object.""" if self._dbh is None or (isinstance(self._dbh, psycopg2.extensions.connection) and self._dbh.closed): try: self._dbh = psycopg2.connect( host=self._cfg.dget("database.host"), sslmode=self._cfg.dget("database.sslmode"), port=self._cfg.dget("database.port"), database=self._cfg.dget("database.name"), user=self._cfg.pget("database.user"), password=self._cfg.pget("database.pass"), ) self._dbh.set_client_encoding("utf8") dbc = self._dbh.cursor() dbc.execute("SET NAMES 'UTF8'") dbc.close() except psycopg2.DatabaseError as err: raise VMMError(str(err), DATABASE_ERROR)