Skip to content
Snippets Groups Projects
Commit a2e68c64 authored by Thomas Schneider's avatar Thomas Schneider
Browse files

Initial import

parents
No related branches found
No related tags found
No related merge requests found
Checking pipeline status
# -*- conf -*-
[flake8]
max-line-length = 88
exclude = .venv,.git,__pycache__,.cache
config.json
### https://github.com/github/gitignore/blob/master/Python.gitignore
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
---
image: python:3.8
variables:
PIP_CACHE_DIR: '$CI_PROJECT_DIR/.cache/pip'
cache:
paths:
- .cache
before_script:
- python -V
- pip install flake8 flake8-black
test:
stage: test
script:
- flake8
#!/usr/bin/env python
import argparse
import json
from rezelist import backends
def get_backend(source):
t = source["type"]
if t == "aliases":
return backends.Aliases(source)
elif t == "db":
return backends.DB(source)
elif t == "passwd":
return backends.Passwd(source)
else:
raise ValueError(f"{t} is not a valid type")
def main():
ap = argparse.ArgumentParser("Create a list of all routable mail addresses")
ap.add_argument("--config", "-c", required=True, type=argparse.FileType("r"))
args = ap.parse_args()
config = json.load(args.config)
backends = [get_backend(s) for s in config["sources"]]
addresses = set()
for b in backends:
addresses.update(b.iter_all())
output = dict()
for d in config["domains"]:
output[d] = {a for a in addresses if a.domain is None or a.domain == d}
for k, v in output.items():
print(f"{k}:")
for a in v:
print(f" {a.localpart}")
if __name__ == "__main__":
main()
{
"output": "/var/www/reze/{domain}",
"domains": [
"example.com",
"sub.example.com",
"example.org"
],
"sources": [
{
"type": "aliases",
"path": "/etc/mail/aliases"
},
{
"type": "passwd",
"minuid": 1000
},
{
"type": "db",
"url": "mysql://user:pass@db.example.com/mail",
"table": "forwards",
"column": "source"
}
]
}
from dataclasses import dataclass
@dataclass(frozen=True)
class Address:
localpart: str
domain: str
def __str__(self) -> str:
return f"{self.localpart}@{self.domain}"
@classmethod
def from_str(cls, address: str):
parts = address.rsplit("@", maxsplit=1)
localpart = parts[0]
try:
domain = parts[1]
except IndexError:
domain = None
return cls(localpart, domain)
from .aliases import Aliases
from .db import DB
from .passwd import Passwd
__all__ = [Aliases, DB, Passwd]
from .. import Address
class Aliases:
def __init__(self, config):
self._path = config["path"]
def iter_all(self):
with open(self._path, "r") as fp:
for line in fp:
if line.startswith(("#", " ", "\t", "\n")):
continue
if line.startswith(":include:"):
raise NotImplementedError(":include")
addr = line.split(":")[0]
yield Address.from_str(addr)
from .. import Address
import sqlalchemy
class DB:
def __init__(self, config):
self._engine = sqlalchemy.create_engine(config["url"])
self._table = config["table"]
self._column = config["column"]
self._connection = None
def _conn(self):
if self._connection is None:
self._connection = self._engine.connect()
return self._connection
def iter_all(self):
metadata = sqlalchemy.MetaData()
metadata.reflect(self._conn())
tbl = metadata.tables[self._table]
col = tbl.c[self._column]
select = sqlalchemy.select([col])
result = self._conn().execute(select)
for row in result:
yield Address.from_str(row[col])
from .. import Address
import pwd
class Passwd:
def __init__(self, config):
self._minuid = config["minuid"]
def iter_all(self):
for entry in pwd.getpwall():
if entry.pw_uid >= self._minuid:
yield Address(localpart=entry.pw_name, domain=None)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment