├── .atomenv.cson ├── .coveragerc ├── .editorconfig ├── .gitattributes ├── .github ├── stale.yml └── workflows │ ├── ci.yml │ ├── codeql-analysis.yml │ └── ossar-analysis.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .travis.yml ├── CHANGELOG.rst ├── CODE_OF_CONDUCT.rst ├── CONTRIBUTING.rst ├── CONTRIBUTORS.rst ├── LICENSE ├── Makefile ├── README.rst ├── THANKS.rst ├── TODO.rst ├── bash-completion └── blackhole-completion.bash ├── blackhole ├── __init__.py ├── __main__.py ├── application.py ├── child.py ├── config.py ├── control.py ├── daemon.py ├── exceptions.py ├── logs.py ├── protocols.py ├── smtp.py ├── streams.py ├── supervisor.py ├── utils.py └── worker.py ├── codecov.yml ├── docs └── source │ ├── _extra │ └── testssl.sh.html │ ├── _static │ ├── blackhole.css │ └── blackhole.png │ ├── api-application.rst │ ├── api-child.rst │ ├── api-config.rst │ ├── api-control.rst │ ├── api-daemon.rst │ ├── api-exceptions.rst │ ├── api-logs.rst │ ├── api-protocols.rst │ ├── api-smtp.rst │ ├── api-streams.rst │ ├── api-supervisor.rst │ ├── api-utils.rst │ ├── api-worker.rst │ ├── api.rst │ ├── changelog.rst │ ├── command-auth.rst │ ├── command-expn.rst │ ├── command-vrfy.rst │ ├── communicating-with-blackhole.rst │ ├── conf.py │ ├── configuration.rst │ ├── docutils.conf │ ├── dynamic-responses.rst │ ├── dynamic-switches.rst │ ├── index.rst │ ├── overview.rst │ └── todo.rst ├── example.conf ├── init.d └── debian-ubuntu │ └── blackhole ├── man ├── build │ ├── blackhole.1 │ └── blackhole_config.1 └── source │ ├── blackhole.rst │ └── blackhole_config.rst ├── poetry.lock ├── pyproject.toml ├── pytest.ini ├── requirements-dev.txt ├── requirements-extra.txt ├── scripts ├── minify.sh └── update-libuv.sh ├── setup.py ├── tests ├── __init__.py ├── _utils.py ├── conftest.py ├── test_application.py ├── test_child.py ├── test_config.py ├── test_control.py ├── test_daemon.py ├── test_logs.py ├── test_smtp.py ├── test_smtp_switches.py ├── test_streams.py ├── test_supervisor.py ├── test_utils.py ├── test_worker.py └── test_worker_child_communication.py └── tox.ini /.atomenv.cson: -------------------------------------------------------------------------------- 1 | env: 2 | PATH: "~/.tox/py36/bin:~/.tox/py36-uvloop/bin:~/.tox/py36-setproctitle/bin:~/.tox/lint/bin:~/.local/bin:/usr/bin" 3 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = 4 | blackhole 5 | tests 6 | concurrency = 7 | thread 8 | multiprocessing 9 | 10 | [paths] 11 | source = 12 | blackhole 13 | .tox/*/lib/python*/site-packages/blackhole 14 | 15 | [report] 16 | show_missing = True 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | max_line_length = 79 7 | indent_style = space 8 | indent_size = 4 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | end_of_line = lf 12 | charset = utf-8 13 | 14 | [*.{py,rst,ini}] 15 | max_line_length = 79 16 | 17 | [Makefile] 18 | indent_style = tab 19 | 20 | [.travis.yml] 21 | indent_size = 2 22 | 23 | [*.sh] 24 | max_line_length = 79 25 | indent_size = 2 26 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | 3 | *.pxd text diff=python 4 | *.py text diff=python 5 | *.py3 text diff=python 6 | *.pyw text diff=python 7 | *.pyx text diff=python 8 | *.pyz text diff=python 9 | *.pyi text diff=python 10 | 11 | *.db binary 12 | *.p binary 13 | *.pkl binary 14 | *.pickle binary 15 | 16 | *.ipynb text 17 | 18 | *.md text 19 | *.csv text 20 | *.info text 21 | *.rst text 22 | 23 | *.pdf -crlf -diff -merge 24 | *.jpg -crlf -diff -merge 25 | *.png -crlf -diff -merge 26 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kura/blackhole/5d5e6f4395a413ed99b6c0721af1a86276a6225f/.github/stale.yml -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | branches: [ main ] 7 | workflow_dispatch: 8 | 9 | jobs: 10 | unittest: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | python-version: ['3.7', '3.8', '3.9', '3.10', 'pypy-3.7', 'pyston'] 15 | steps: 16 | - uses: actions/checkout@v1 17 | - name: Set up Python 18 | if: ${{ matrix.python-version != 'pyston' }} 19 | uses: actions/setup-python@v2 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | architecture: x64 23 | - name: Set up Pyston 24 | if: ${{ matrix.python-version == 'pyston' }} 25 | run: | 26 | git clone https://github.com/pyenv/pyenv.git ~/.pyenv 27 | ~/.pyenv/bin/pyenv install pyston-2.2 28 | echo "$HOME/.pyenv/versions/pyston-2.2/bin" >> $GITHUB_PATH 29 | - uses: actions/cache@v2 30 | with: 31 | path: ~/.cache/pip 32 | key: unittest-cache-${{ matrix.python-version }} 33 | - name: Apt install 34 | run: sudo apt install shellcheck 35 | - name: Pip install 36 | run: pip install -U codecov tox tox-gh-actions 37 | - name: Build and install libuv 38 | run: | 39 | git clone https://github.com/libuv/libuv.git 40 | pushd libuv 41 | git checkout $(git describe --tags) 42 | sh autogen.sh 43 | ./configure 44 | make 45 | sudo make install 46 | popd 47 | - name: Run tests 48 | env: 49 | TOX_PARALLEL_NO_SPINNER: 1 50 | run: tox -vvp 51 | - name: Generate coverage report 52 | run: tox -e coverage-report 53 | - name: Upload coverage 54 | uses: codecov/codecov-action@v1.0.10 55 | if: github.ref == 'refs/heads/main' 56 | with: 57 | token: ${{ secrets.CODECOV_TOKEN }} 58 | file: ./coverage.xml 59 | name: blackhole 60 | flags: unittests 61 | env_vars: PYTHON 62 | fail_ci_if_error: true 63 | 64 | lint: 65 | runs-on: ubuntu-latest 66 | steps: 67 | - uses: actions/checkout@v1 68 | - name: Set up Python 69 | uses: actions/setup-python@v2 70 | with: 71 | python-version: '3.9' 72 | architecture: x64 73 | - uses: actions/cache@v2 74 | with: 75 | path: ~/.cache/pip 76 | key: lint-cache 77 | - name: Pip install 78 | run: pip install tox 79 | - name: Lint 80 | run: tox -e lint 81 | 82 | docs: 83 | runs-on: ubuntu-latest 84 | steps: 85 | - uses: actions/checkout@v1 86 | - name: Set up Python 87 | uses: actions/setup-python@v2 88 | with: 89 | python-version: '3.9' 90 | architecture: x64 91 | - uses: actions/cache@v2 92 | with: 93 | path: ~/.cache/pip 94 | key: docs-cache 95 | - name: Pip install 96 | run: pip install tox 97 | - name: Docs 98 | run: tox -e docs 99 | - name: Minify 100 | run: scripts/minify.sh 101 | - name: Deploy 102 | uses: peaceiris/actions-gh-pages@v3 103 | if: github.ref == 'refs/heads/main' 104 | with: 105 | github_token: ${{ secrets.GITHUB_TOKEN }} 106 | publish_dir: ./.tox/docs/tmp/html 107 | 108 | build: 109 | runs-on: ubuntu-latest 110 | steps: 111 | - uses: actions/checkout@v1 112 | - name: Set up Python 113 | uses: actions/setup-python@v2 114 | with: 115 | python-version: '3.9' 116 | architecture: x64 117 | - uses: actions/cache@v2 118 | with: 119 | path: ~/.cache/pip 120 | key: build-cache 121 | - name: Pip install 122 | run: pip install tox 123 | - name: Build 124 | run: tox -e build 125 | 126 | man: 127 | runs-on: ubuntu-latest 128 | steps: 129 | - uses: actions/checkout@v1 130 | - name: Set up Python 131 | uses: actions/setup-python@v2 132 | with: 133 | python-version: '3.9' 134 | architecture: x64 135 | - uses: actions/cache@v2 136 | with: 137 | path: ~/.cache/pip 138 | key: man-cache 139 | - name: Pip install 140 | run: pip install tox 141 | - name: Man 142 | run: tox -e man 143 | 144 | poetry: 145 | runs-on: ubuntu-latest 146 | steps: 147 | - uses: actions/checkout@v1 148 | - name: Set up Python 149 | uses: actions/setup-python@v2 150 | with: 151 | python-version: '3.9' 152 | architecture: x64 153 | - uses: actions/cache@v2 154 | with: 155 | path: ~/.cache/pip 156 | key: poetry-cache 157 | - name: Pip install 158 | run: pip install tox 159 | - name: Poetry 160 | run: tox -e poetry 161 | 162 | setuppy: 163 | runs-on: ubuntu-latest 164 | steps: 165 | - uses: actions/checkout@v1 166 | - name: Set up Python 167 | uses: actions/setup-python@v2 168 | with: 169 | python-version: '3.9' 170 | architecture: x64 171 | - uses: actions/cache@v2 172 | with: 173 | path: ~/.cache/pip 174 | key: setuppy-cache 175 | - name: Pip install 176 | run: pip install tox 177 | - name: setup.py 178 | run: tox -e setuppy 179 | 180 | shellcheck: 181 | runs-on: ubuntu-latest 182 | steps: 183 | - uses: actions/checkout@v1 184 | - name: Set up Python 185 | uses: actions/setup-python@v2 186 | with: 187 | python-version: '3.9' 188 | architecture: x64 189 | - uses: actions/cache@v2 190 | with: 191 | path: ~/.cache/pip 192 | key: shellcheck-cache 193 | - name: Install shellcheck 194 | run: sudo apt install shellcheck 195 | - name: Pip install 196 | run: pip install tox 197 | - name: Shellcheck 198 | run: tox -e shellcheck 199 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [main, ] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [main] 9 | schedule: 10 | - cron: '0 1 * * 5' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v2 20 | with: 21 | # We must fetch at least the immediate parents so that if this is 22 | # a pull request then we can checkout the head. 23 | fetch-depth: 2 24 | 25 | # If this run was triggered by a pull request event, then checkout 26 | # the head of the pull request instead of the merge commit. 27 | - run: git checkout HEAD^2 28 | if: ${{ github.event_name == 'pull_request' }} 29 | 30 | # Initializes the CodeQL tools for scanning. 31 | - name: Initialize CodeQL 32 | uses: github/codeql-action/init@v1 33 | # Override language selection by uncommenting this and choosing your languages 34 | # with: 35 | # languages: go, javascript, csharp, python, cpp, java 36 | 37 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 38 | # If this step fails, then you should remove it and run the build manually (see below) 39 | - name: Autobuild 40 | uses: github/codeql-action/autobuild@v1 41 | 42 | # ℹ️ Command-line programs to run using the OS shell. 43 | # 📚 https://git.io/JvXDl 44 | 45 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 46 | # and modify them (or add more) to build your code if your project 47 | # uses a compiled language 48 | 49 | #- run: | 50 | # make bootstrap 51 | # make release 52 | 53 | - name: Perform CodeQL Analysis 54 | uses: github/codeql-action/analyze@v1 55 | -------------------------------------------------------------------------------- /.github/workflows/ossar-analysis.yml: -------------------------------------------------------------------------------- 1 | # This workflow integrates a collection of open source static analysis tools 2 | # with GitHub code scanning. For documentation, or to provide feedback, visit 3 | # https://github.com/github/ossar-action 4 | name: OSSAR 5 | 6 | on: 7 | push: 8 | pull_request: 9 | 10 | jobs: 11 | OSSAR-Scan: 12 | # OSSAR runs on windows-latest. 13 | # ubuntu-latest and macos-latest support coming soon 14 | runs-on: windows-latest 15 | 16 | steps: 17 | # Checkout your code repository to scan 18 | - name: Checkout repository 19 | uses: actions/checkout@v2 20 | with: 21 | # We must fetch at least the immediate parents so that if this is 22 | # a pull request then we can checkout the head. 23 | fetch-depth: 2 24 | 25 | # If this run was triggered by a pull request event, then checkout 26 | # the head of the pull request instead of the merge commit. 27 | - run: git checkout HEAD^2 28 | if: ${{ github.event_name == 'pull_request' }} 29 | 30 | # Install dotnet, used by OSSAR 31 | - name: Install .NET 32 | uses: actions/setup-dotnet@v1 33 | with: 34 | dotnet-version: '3.1.201' 35 | 36 | # Run open source static analysis tools 37 | - name: Run OSSAR 38 | uses: github/ossar-action@v1 39 | id: ossar 40 | 41 | # Upload results to the Security tab 42 | - name: Upload OSSAR results 43 | uses: github/codeql-action/upload-sarif@v1 44 | with: 45 | sarif_file: ${{ steps.ossar.outputs.sarifFile }} 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coveraged 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *.cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # Jupyter Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # SageMath parsed files 79 | *.sage.py 80 | 81 | # Environments 82 | .env 83 | .venv 84 | env/ 85 | venv/ 86 | ENV/ 87 | 88 | # Spyder project settings 89 | .spyderproject 90 | .spyproject 91 | 92 | # Rope project settings 93 | .ropeproject 94 | 95 | # mkdocs documentation 96 | /site 97 | 98 | # mypy 99 | .mypy_cache/ 100 | 101 | .testmondata 102 | .testmondata-journal 103 | .coverage 104 | *.log 105 | *.crt 106 | *.key 107 | *.pem 108 | test.conf 109 | .codecov 110 | sendmail.py 111 | imap.py 112 | dns.py 113 | docs/build 114 | !man/build 115 | node_modules 116 | .pytest_cache 117 | blackhole-test.pid 118 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | fail_fast: true 2 | exclude: ^(\.tox/|\.coverage|poetry\.lock) 3 | repos: 4 | - repo: https://github.com/psf/black 5 | rev: 21.6b0 6 | hooks: 7 | - id: black 8 | 9 | - repo: https://gitlab.com/pycqa/flake8 10 | rev: 3.9.2 11 | hooks: 12 | - id: flake8 13 | additional_dependencies: [flake8-bugbear, flake8-isort, flake8-commas] 14 | 15 | - repo: https://github.com/timothycrosley/isort 16 | rev: 5.9.2 17 | hooks: 18 | - id: isort 19 | additional_dependencies: [toml] 20 | exclude: ^.*/tests/.*\.py$ 21 | 22 | - repo: https://github.com/pre-commit/pre-commit-hooks 23 | rev: v4.0.1 24 | hooks: 25 | - id: trailing-whitespace 26 | - id: end-of-file-fixer 27 | - id: check-added-large-files 28 | - id: check-ast 29 | - id: check-merge-conflict 30 | - id: check-symlinks 31 | - id: debug-statements 32 | - id: fix-encoding-pragma 33 | - id: check-toml 34 | - id: check-yaml 35 | - id: destroyed-symlinks 36 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | cache: pip 3 | dist: focal 4 | sudo: false 5 | group: travis_latest 6 | 7 | install: 8 | - pip install codecov tox 9 | script: tox 10 | after_success: 11 | - tox -e coverage-report 12 | - codecov 13 | 14 | matrix: 15 | fast_finish: true 16 | include: 17 | 18 | - python: "3.7" 19 | env: TOXENV=py37,py37-setproctitle,py37-uvloop 20 | sudo: true 21 | before_install: 22 | - git clone https://github.com/libuv/libuv.git 23 | - pushd libuv/ 24 | - git checkout $(git describe --tags) 25 | - sh autogen.sh 26 | - "./configure" 27 | - make 28 | - sudo make install 29 | - popd 30 | script: tox 31 | 32 | - python: "3.8" 33 | env: TOXENV=py38,py38-setproctitle,py38-uvloop 34 | sudo: true 35 | before_install: 36 | - git clone https://github.com/libuv/libuv.git 37 | - pushd libuv/ 38 | - git checkout $(git describe --tags) 39 | - sh autogen.sh 40 | - "./configure" 41 | - make 42 | - sudo make install 43 | - popd 44 | script: tox 45 | 46 | - python: "3.9" 47 | env: TOXENV=py39,py39-setproctitle,py39-uvloop 48 | sudo: true 49 | before_install: 50 | - git clone https://github.com/libuv/libuv.git 51 | - pushd libuv/ 52 | - git checkout $(git describe --tags) 53 | - sh autogen.sh 54 | - "./configure" 55 | - make 56 | - sudo make install 57 | - popd 58 | script: tox 59 | 60 | - python: "pypy3.7-7.3.5" 61 | env: TOXENV=pypy3,pypy3-setproctitle,pypy3-uvloop 62 | script: tox 63 | 64 | - python: "3.9" 65 | env: TOXENV=lint 66 | - python: "3.9" 67 | env: TOXENV=docs 68 | deploy: 69 | provider: pages 70 | skip_cleanup: true 71 | github_token: 72 | secure: "FGNxmBm5Zr2oLHK13HgqGKPFQKGrILvBUeC9F655V3hBTcOD9BXyeABwF0yvnGSS/1J/sQVNn71uRbZDfmxd/YTT8/5yK4yeuAiT1pnKcMppYaBA/691e9Uz9hB4yfivir/+ZWSlPMQJ2IiLvY6aBfJwwrNtiB5biWQiTlW+5aI=" 73 | local_dir: .tox/docs/tmp/html 74 | on: 75 | branch: master 76 | - python: "3.9" 77 | env: TOXENV=build 78 | - python: "3.9" 79 | env: TOXENV=man 80 | - python: "3.9" 81 | env: TOXENV=poetry 82 | - python: "3.9" 83 | env: TOXENV=setuppy 84 | - python: '3.9' 85 | env: TOXENV=shellcheck 86 | addons: 87 | apt: 88 | sources: 89 | - debian-sid 90 | before-script: 91 | - "sudo apt-get install cabal-install" 92 | - "cabal update" 93 | - "cabal install ShellCheck" 94 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.rst: -------------------------------------------------------------------------------- 1 | ==================================== 2 | Contributor Covenant Code of Conduct 3 | ==================================== 4 | 5 | Our Pledge 6 | ========== 7 | 8 | In the interest of fostering an open and welcoming environment, we as 9 | contributors and maintainers pledge to making participation in our project and 10 | our community a harassment-free experience for everyone, regardless of age, 11 | body size, disability, ethnicity, gender identity and expression, level of 12 | experience, nationality, personal appearance, race, religion, or sexual 13 | identity and orientation. 14 | 15 | Our Standards 16 | ============= 17 | 18 | Examples of behavior that contributes to creating a positive environment 19 | include: 20 | 21 | - Using welcoming and inclusive language 22 | - Being respectful of differing viewpoints and experiences 23 | - Gracefully accepting constructive criticism 24 | - Focusing on what is best for the community 25 | - Showing empathy towards other community members 26 | 27 | Examples of unacceptable behavior by participants include: 28 | 29 | - The use of sexualized language or imagery and unwelcome sexual attention or 30 | advances 31 | - Trolling, insulting/derogatory comments, and personal or political attacks 32 | - Public or private harassment 33 | - Publishing others' private information, such as a physical or electronic 34 | address, without explicit permission 35 | - Other conduct which could reasonably be considered inappropriate in a 36 | professional setting 37 | 38 | Our Responsibilities 39 | ==================== 40 | 41 | Project maintainers are responsible for clarifying the standards of acceptable 42 | behavior and are expected to take appropriate and fair corrective action in 43 | response to any instances of unacceptable behavior. 44 | 45 | Project maintainers have the right and responsibility to remove, edit, or 46 | reject comments, commits, code, wiki edits, issues, and other contributions 47 | that are not aligned to this Code of Conduct, or to ban temporarily or 48 | permanently any contributor for other behaviors that they deem inappropriate, 49 | threatening, offensive, or harmful. 50 | 51 | Scope 52 | ===== 53 | 54 | This Code of Conduct applies both within project spaces and in public spaces 55 | when an individual is representing the project or its community. Examples of 56 | representing a project or community include using an official project e-mail 57 | address, posting via an official social media account, or acting as an 58 | appointed representative at an online or offline event. Representation of a 59 | project may be further defined and clarified by project maintainers. 60 | 61 | Enforcement 62 | =========== 63 | 64 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 65 | reported by contacting the project team at kura@kura.gg. The project team will 66 | review and investigate all complaints, and will respond in a way that it deems 67 | appropriate to the circumstances. The project team is obligated to maintain 68 | confidentiality with regard to the reporter of an incident. Further details of 69 | specific enforcement policies may be posted separately. 70 | 71 | Project maintainers who do not follow or enforce the Code of Conduct in good 72 | faith may face temporary or permanent repercussions as determined by other 73 | members of the project's leadership. 74 | 75 | Attribution 76 | =========== 77 | 78 | This Code of Conduct is adapted from the `Contributor Covenant 79 | `__, version 1.4, available at 80 | `http://contributor-covenant.org/version/1/4 81 | `__. 82 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Contributing 3 | ============ 4 | 5 | If you're thinking about contributing, it's really quite simple. There is no need to feel daunted. You can contribute in a variety of ways: 6 | 7 | Report bugs 8 | Bug fixes 9 | New behaviour 10 | Improve the documentation 11 | Write tests 12 | Improve performance 13 | Add additional support i.e. new versions of Python or PyPy 14 | You can view a list of tasks that need work on the `todo `__ page. 15 | 16 | The `api `__ section also has a wealth of information on how the server works and how you can modify it or use parts of it. 17 | -------------------------------------------------------------------------------- /CONTRIBUTORS.rst: -------------------------------------------------------------------------------- 1 | .. _contributors: 2 | 3 | ============ 4 | Contributors 5 | ============ 6 | 7 | It's a small list. 8 | 9 | - Kura 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2013-2021 Kura 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the 'Software'), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: autodoc 2 | autodoc: 3 | pip install sphinx guzzle_sphinx_theme sphinx-autobuild 4 | sphinx-autobuild -B docs/source docs/build 5 | 6 | .PHONY: build 7 | build: 8 | rm -rf build dist 9 | pip install wheel 10 | python setup.py sdist bdist_wheel 11 | 12 | .PHONY: clean 13 | clean: 14 | find . -name "*.pyc" -delete 15 | find . -name "__pycache__" -delete 16 | rm -rf build dist docs/build man/build t coverage.xml .coverage .coverage.* .testmondata .testmondata-journal 17 | 18 | .PHONY: docs 19 | docs: clean linkcheck 20 | pip install sphinx guzzle_sphinx_theme 21 | sphinx-build -j 4 docs/source/ docs/build/ 22 | scripts/minify.sh 23 | 24 | .PHONY: install 25 | install: 26 | python setup.py install 27 | 28 | .PHONY: lint 29 | lint: 30 | tox -e lint 31 | 32 | .PHONY: man 33 | man: clean test_man 34 | mkdir -p man/build 35 | mv .tox/man/tmp/*.1 man/build 36 | 37 | .PHONY: pre-commit 38 | pre-commit: 39 | tox -e pre-commit 40 | 41 | .PHONY: publish 42 | publish: 43 | tox -e publish 44 | 45 | .PHONY: release 46 | release: publish 47 | 48 | .PHONY: shellcheck 49 | shellcheck: 50 | tox -e shellcheck 51 | 52 | .PHONY: test 53 | test: 54 | tox -vv -e py38 55 | 56 | .PHONY: test_py37 57 | test_py37: 58 | tox -vvp -e py37,py37-setproctitle,py37-uvloop 59 | 60 | .PHONY: test_py38 61 | test_py38: 62 | tox -vvp -e py38,py38-setproctitle,py38-uvloop 63 | 64 | .PHONY: test_py39 65 | test_py39: 66 | tox -vvp -e py39,py39-setproctitle,py39-uvloop 67 | 68 | .PHONY: test_py310 69 | test_py310: 70 | tox -vvp -e py310,py310-setproctitle,py310-uvloop 71 | 72 | .PHONY: test_pypy3 73 | test_pypy: 74 | tox -vvp -e pypy3,pypy3-setproctitle,py39-uvloop 75 | 76 | .PHONY: test_pyston-3 77 | test_pyston-3: 78 | tox -vvp -e pyston-3,pyston-3-setproctitle,pyston-3-uvloop 79 | 80 | .PHONY: test_build 81 | test_build: 82 | tox -e build 83 | 84 | .PHONY: test_docs 85 | test_docs: 86 | tox -e docs 87 | 88 | .PHONY: test_man 89 | test_man: 90 | tox -e man 91 | 92 | .PHONY: test_poetry 93 | test_poetry: 94 | tox -e poetry 95 | 96 | .PHONY: test_setuppy 97 | test_setuppy: 98 | tox -e setuppy 99 | 100 | .PHONY: testall 101 | testall: tox 102 | 103 | .PHONY: testssl 104 | testssl: 105 | sudo apt-get install -y aha 106 | testssl.sh --wide --colorblind blackhole.io:465 | aha | grep -v 'Start' | grep -v 'Done' | grep -v '/usr/bin/openssl' | sed 's/stdin/testssl.sh/g' | awk -v RS= -v ORS='\n\n' '1' > docs/source/_extra/testssl.sh.html 107 | 108 | .PHONY: tox 109 | tox: 110 | tox -e `tox -l | grep -v watch | tr '\n' ','` -p all 111 | 112 | .PHONY: uninstall 113 | uninstall: 114 | pip uninstall blackhole 115 | 116 | .PHONY: update-libuv 117 | update-libuv: 118 | scripts/update-libuv.sh 119 | 120 | .PHONY: watch 121 | watch: 122 | tox -e watch 123 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Blackhole 3 | ========= 4 | 5 | .. image:: https://img.shields.io/github/workflow/status/kura/blackhole/CI?style=for-the-badge&label=tests&logo=githubactions 6 | :target: https://github.com/kura/blackhole/actions/workflows/ci.yml 7 | :alt: Build status of the master branch 8 | 9 | .. image:: https://img.shields.io/codecov/c/github/kura/blackhole/master.svg?style=for-the-badge&label=coverage&logo=codecov 10 | :target: https://codecov.io/github/kura/blackhole/ 11 | :alt: Test coverage 12 | 13 | Blackhole is an `MTA (message transfer agent) 14 | `_ that (figuratively) 15 | pipes all mail to /dev/null, built on top of `asyncio 16 | `_ and utilises `async def `_ 17 | and `await 18 | `_ statements 19 | available in `Python 3.5 `_. 20 | 21 | While Blackhole is an MTA, none of the actions performed via SMTP or SMTPS are 22 | actually processed, and no email is delivered. You can tell Blackhole how to 23 | handle mail that it receives. It can accept all of it, bounce it all, or 24 | randomly do either of those two actions. 25 | 26 | Think of Blackhole sort of like a honeypot in terms of how it handles mail, but 27 | it's specifically designed with testing in mind. 28 | 29 | Python support 30 | ============== 31 | 32 | - Python 3.7+ 33 | - PyPy 3.7+ 34 | - Pyston 2.2+ 35 | 36 | Documentation 37 | ============= 38 | 39 | You can find the latest documentation `here 40 | `_. 41 | 42 | If you would like to contribute, please read the `contributors guide 43 | `_. 44 | 45 | The latest build status on GitHub ``_. 46 | 47 | And the test coverage report on `codecov 48 | `_. 49 | 50 | Changelog 51 | ========= 52 | 53 | You can find a list of changes `on the 54 | blackhole website `_. 55 | -------------------------------------------------------------------------------- /THANKS.rst: -------------------------------------------------------------------------------- 1 | .. _thanks: 2 | 3 | ====== 4 | Thanks 5 | ====== 6 | 7 | - Facebook and Ben Darnell -- for Tornado and some help, for pre-2.0.0 that used 8 | Tornado. 9 | - Daniele Varrazzo -- for setproctitle, which was used pre-2.0.0 10 | - The PyPy devs -- for PyPy and some help 11 | - Richard Noble -- for giving me the idea 12 | - David Winterbottom -- ideas/thoughts 13 | -------------------------------------------------------------------------------- /TODO.rst: -------------------------------------------------------------------------------- 1 | .. _todo: 2 | 3 | .. role:: strikethrough 4 | 5 | ==== 6 | TODO 7 | ==== 8 | 9 | Things on the todo list, in no particular order. 10 | 11 | - :strikethrough:`Make ``tests/`` flake8/isort compliant.`` 12 | - Implement logging to a file. 13 | - :strikethrough:`Add pidfile tests for config to config_test and pytest.` -- 14 | :ref:`2.0.4` 15 | - :strikethrough:`Add socket bind tests to config_test and pytest` -- 16 | :ref:`2.0.2` 17 | - :strikethrough:`Dynamic mode switch - helo, ehlo, delay verb, rcpt, mail 18 | from` -- :ref:`2.0.4` 19 | - :strikethrough:`Dynamic delay switch - min and max delay range (i.e. delay 20 | between 10 and seconds, randomly) - helo, ehlo, delay verb, rcpt, mail 21 | from` -- :ref:`2.0.4` 22 | - :strikethrough:`HELP verb` -- :ref:`2.0.2` 23 | - :strikethrough:`Improve TLS by adding load_dh_params` -- :ref:`2.0.4` 24 | - :strikethrough:`Add AUTH mechanism` -- :ref:`2.0.4` 25 | - POP & IMAP -- started, progress available at 26 | ``_ 27 | - :strikethrough:`Add SMTP Submission to default interfaces` -- :ref:`2.0.14` 28 | - :strikethrough:`Add more lists to EXPN and combine for EXPN all` -- 29 | :ref:`2.0.14` 30 | - :strikethrough:`Add pass= and fail= to more verbs` -- :ref:`2.0.14` 31 | - Properly implement ``PIPELINING`` -- build responses in a list and return in 32 | order after ``.\r\n`` 33 | - Added base level server that can be extended, i.e. ``NOT IMPLEMENTED`` most 34 | features. 35 | - Strip out :any:`blackhole.config.Config` context and make it loadable on 36 | creation -- allowing any config context to be passed. 37 | - Command injection -- move HELO, EHLO etc to separate modules, allowing them 38 | to be injected and that injection to be overridden by a user-defined method. 39 | - Add auth mechanisms as injectables, special injectables that, unlike the 40 | command inections, these are injected specially in to the auth system. 41 | 42 | Possible future features 43 | ======================== 44 | 45 | - Greylist support 46 | - DKIM / DomainKeys / SPF / Sender-ID after DATA command. 47 | -------------------------------------------------------------------------------- /bash-completion/blackhole-completion.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # (The MIT License) 4 | # 5 | # Copyright (c) 2018 Kura 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the 'Software'), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in all 15 | # copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | 26 | _blackhole_complete() { 27 | local cur_word=${COMP_WORDS[COMP_CWORD]} 28 | local options="-b -c -d -h -ls -q -t -v 29 | --background --conf --debug --help --less-secure --quiet 30 | --test --version" 31 | if [[ "$cur_word" == -* ]]; then 32 | # shellcheck disable=SC2207 33 | COMPREPLY=( $( compgen -W "$options" -- "$cur_word" ) ) 34 | fi 35 | return 0 36 | } 37 | 38 | complete -o default -F _blackhole_complete blackhole 39 | -------------------------------------------------------------------------------- /blackhole/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # (The MIT License) 4 | # 5 | # Copyright (c) 2013-2021 Kura 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the 'Software'), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | """ 26 | Blackhole is an email MTA that pipes all mail to /dev/null. 27 | 28 | Blackhole is built on top of asyncio and utilises :py:obj:`async def` and 29 | :py:obj:`await` statements on available in Python 3.5 and above. 30 | 31 | While Blackhole is an MTA (mail transport agent), none of the actions 32 | performed of SMTP or SMTPS are actually processed and no email or sent or 33 | delivered. 34 | """ 35 | 36 | from .application import __all__ as __application_all__ 37 | from .child import __all__ as __child_all__ 38 | from .config import __all__ as __config_all__ 39 | from .control import __all__ as __control_all__ 40 | from .daemon import __all__ as __daemon_all__ 41 | from .exceptions import __all__ as __exceptions_all__ 42 | from .logs import __all__ as __logs_all__ 43 | from .protocols import __all__ as __protocols_all__ 44 | from .smtp import __all__ as __smtp_all__ 45 | from .streams import __all__ as __streams_all__ 46 | from .supervisor import __all__ as __supervisor_all__ 47 | from .utils import __all__ as __utils_all__ 48 | from .worker import __all__ as __worker_all__ 49 | 50 | 51 | __author__ = "Kura" 52 | __copyright__ = "None" 53 | __credits__ = ("Kura",) 54 | __license__ = "MIT" 55 | __version__ = "2.1.19" 56 | __maintainer__ = "Kura" 57 | __email__ = "kura@kura.gg" 58 | __status__ = "Stable" 59 | 60 | 61 | __all__ = ( 62 | __application_all__ 63 | + __child_all__ 64 | + __config_all__ 65 | + __control_all__ 66 | + __daemon_all__ 67 | + __exceptions_all__ 68 | + __logs_all__ 69 | + __protocols_all__ 70 | + __smtp_all__ 71 | + __streams_all__ 72 | + __supervisor_all__ 73 | + __utils_all__ 74 | + __worker_all__ 75 | ) 76 | """Tuple all the things.""" 77 | 78 | try: 79 | import asyncio 80 | 81 | import uvloop 82 | 83 | asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) 84 | except ImportError: 85 | pass 86 | -------------------------------------------------------------------------------- /blackhole/__main__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # (The MIT License) 4 | # 5 | # Copyright (c) 2013-2021 Kura 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the 'Software'), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | """ 26 | Blackhole is an email MTA that pipes all mail to /dev/null. 27 | 28 | Blackhole is built on top of asyncio and utilises :py:obj:`async def` and 29 | :py:obj:`await` statements on available in Python 3.5 and above. 30 | 31 | While Blackhole is an MTA (mail transport agent), none of the actions 32 | performed of SMTP or SMTPS are actually processed and no email or sent or 33 | delivered. 34 | """ 35 | 36 | from .application import run 37 | 38 | 39 | if __name__ == "__main__": # pragma: no cover 40 | run() 41 | -------------------------------------------------------------------------------- /blackhole/application.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # (The MIT License) 4 | # 5 | # Copyright (c) 2013-2021 Kura 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the 'Software'), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | """Provides functionality to run the server.""" 26 | 27 | 28 | import logging 29 | import os 30 | import sys 31 | 32 | from .config import Config, config_test, parse_cmd_args, warn_options 33 | from .control import pid_permissions, setgid, setuid 34 | from .daemon import Daemon 35 | from .exceptions import ( 36 | BlackholeRuntimeException, 37 | ConfigException, 38 | DaemonException, 39 | ) 40 | from .logs import configure_logs 41 | from .supervisor import Supervisor 42 | from .utils import blackhole_config_help 43 | 44 | 45 | __all__ = ("blackhole_config", "run") 46 | """Tuple all the things.""" 47 | 48 | 49 | def blackhole_config(): 50 | """ 51 | Print the config help to the console with man-style formatting. 52 | 53 | :raises SystemExit: Exit code :py:obj:`os.EX_OK`. 54 | """ 55 | logging.basicConfig(format="%(message)s", level=logging.INFO) 56 | logging.info(blackhole_config_help) 57 | raise SystemExit(os.EX_OK) 58 | 59 | 60 | def run(): 61 | """ 62 | Create the asyncio loop and start the server. 63 | 64 | :raises SystemExit: Exit code :py:obj:`os.EX_USAGE` when a configuration 65 | error occurs, :py:obj:`os.EX_NOPERM` when a permission 66 | error occurs or :py:obj:`os.EX_OK` when the program 67 | exits cleanly. 68 | """ 69 | args = parse_cmd_args(sys.argv[1:]) 70 | configure_logs(args) 71 | logger = logging.getLogger("blackhole") 72 | if args.test: 73 | config_test(args) 74 | try: 75 | config = Config(args.config_file).load().test() 76 | config.args = args 77 | warn_options(config) 78 | daemon = Daemon(config.pidfile) 79 | supervisor = Supervisor() 80 | pid_permissions() 81 | setgid() 82 | setuid() 83 | except (ConfigException, DaemonException) as err: 84 | logger.critical(err) 85 | raise SystemExit(os.EX_USAGE) 86 | except BlackholeRuntimeException as err: 87 | logger.critical(err) 88 | raise SystemExit(os.EX_NOPERM) 89 | if args.background: 90 | try: 91 | daemon.daemonize() 92 | except DaemonException as err: 93 | supervisor.close_socks() 94 | logger.critical(err) 95 | raise SystemExit(os.EX_NOPERM) 96 | try: 97 | supervisor.run() 98 | except KeyboardInterrupt: 99 | pass 100 | raise SystemExit(os.EX_OK) 101 | -------------------------------------------------------------------------------- /blackhole/child.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # (The MIT License) 4 | # 5 | # Copyright (c) 2013-2021 Kura 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the 'Software'), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | """Provides functionality to spawn and control child processes.""" 26 | 27 | 28 | import asyncio 29 | import logging 30 | import os 31 | import signal 32 | 33 | from . import protocols 34 | from .smtp import Smtp 35 | from .streams import StreamProtocol 36 | 37 | 38 | __all__ = ("Child",) 39 | """Tuple all the things.""" 40 | 41 | 42 | logger = logging.getLogger("blackhole.child") 43 | 44 | 45 | class Child: 46 | """ 47 | A child process. 48 | 49 | Each child process maintains a list of the internal 50 | :py:class:`asyncio.Server` instances it utilises. Each child also 51 | maintains a list of all connections being managed by the child. 52 | """ 53 | 54 | _started = False 55 | servers = [] 56 | """List of :py:class:`asyncio.Server` instances.""" 57 | 58 | clients = [] 59 | """List of clients connected to this process.""" 60 | 61 | def __init__(self, up_read, down_write, socks, idx): 62 | """ 63 | Initialise a child process. 64 | 65 | :param int up_read: A file descriptor for reading. 66 | :param int down_write: A file descriptor for writing. 67 | :param list socks: A list of sockets. 68 | """ 69 | self.up_read = up_read 70 | self.down_write = down_write 71 | self.socks = socks 72 | self.idx = idx 73 | 74 | def start(self): 75 | """Start the child process.""" 76 | logger.debug(f"Starting child {self.idx}") 77 | self._started = True 78 | self.loop = asyncio.new_event_loop() 79 | asyncio.set_event_loop(self.loop) 80 | signal.signal(signal.SIGTERM, self.stop) 81 | self.heartbeat_task = asyncio.Task(self.heartbeat()) 82 | self.loop.run_forever() 83 | self.stop() 84 | os._exit(os.EX_OK) 85 | 86 | async def _start(self): 87 | """Create an asyncio server for each socket.""" 88 | for sock in self.socks: 89 | server = await self.loop.create_server( 90 | lambda: Smtp(self.clients), 91 | **sock, 92 | ) 93 | self.servers.append(server) 94 | 95 | def stop(self, *args, **kwargs): 96 | """ 97 | Stop the child process. 98 | 99 | Mark the process as being stopped, closes each client connected via 100 | this child, cancels internal communication with the supervisor and 101 | finally stops the process and exits. 102 | """ 103 | self._started = False 104 | for _ in range(len(self.clients)): 105 | client = self.clients.pop() 106 | client.close() 107 | for _ in range(len(self.servers)): 108 | server = self.servers.pop() 109 | server.close() 110 | self.heartbeat_task.cancel() 111 | self.server_task.cancel() 112 | for task in asyncio.all_tasks(self.loop): 113 | task.cancel() 114 | self.loop.stop() 115 | self._started = False 116 | os._exit(os.EX_OK) 117 | 118 | async def heartbeat(self): 119 | """ 120 | Handle heartbeat between a worker and child. 121 | 122 | If a child process stops communicating with it's worker, it will be 123 | killed, the worker managing it will also be removed and a new worker 124 | and child will be spawned. 125 | 126 | .. note:: 127 | 128 | 3 bytes are used in the communication channel. 129 | 130 | - b'x01' -- :const:`blackhole.protocols.PING` 131 | - b'x02' -- :const:`blackhole.protocols.PONG` 132 | 133 | These message values are defined in the :mod:`blackhole.protocols` 134 | schema. Documentation is available at -- 135 | https://kura.gg/blackhole/api-protocols.html 136 | """ 137 | read_fd = os.fdopen(self.up_read, "rb") 138 | r_trans, r_proto = await self.loop.connect_read_pipe( 139 | StreamProtocol, 140 | read_fd, 141 | ) 142 | write_fd = os.fdopen(self.down_write, "wb") 143 | w_trans, w_proto = await self.loop.connect_write_pipe( 144 | StreamProtocol, 145 | write_fd, 146 | ) 147 | reader = r_proto.reader 148 | writer = asyncio.StreamWriter(w_trans, w_proto, reader, self.loop) 149 | self.server_task = asyncio.Task(self._start()) 150 | 151 | while self._started: 152 | try: 153 | msg = await reader.read(3) 154 | except: # noqa 155 | break 156 | if msg == protocols.PING: 157 | logger.debug( 158 | f"child.{self.idx}.heartbeat: Ping request received from " 159 | "parent", 160 | ) 161 | writer.write(protocols.PONG) 162 | await asyncio.sleep(5) 163 | r_trans.close() 164 | w_trans.close() 165 | self.stop() 166 | -------------------------------------------------------------------------------- /blackhole/control.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # (The MIT License) 4 | # 5 | # Copyright (c) 2013-2021 Kura 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the 'Software'), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | """Provides control functionality, including socket wrappers.""" 26 | 27 | try: # pragma: no cover 28 | import ssl 29 | except ImportError: # pragma: no cover 30 | ssl = None 31 | 32 | import grp 33 | import logging 34 | import os 35 | import pwd 36 | import socket 37 | 38 | from .config import Config 39 | from .exceptions import BlackholeRuntimeException 40 | 41 | 42 | __all__ = ("pid_permissions", "server", "setgid", "setuid") 43 | """Tuple all the things.""" 44 | 45 | 46 | logger = logging.getLogger("blackhole.control") 47 | ciphers = [ 48 | "ECDHE-ECDSA-AES256-GCM-SHA384", 49 | "ECDHE-RSA-AES256-GCM-SHA384", 50 | "ECDHE-ECDSA-CHACHA20-POLY1305", 51 | "ECDHE-RSA-CHACHA20-POLY1305", 52 | "ECDHE-ECDSA-AES128-GCM-SHA256", 53 | "ECDHE-RSA-AES128-GCM-SHA256", 54 | "ECDHE-ECDSA-AES256-SHA384", 55 | "ECDHE-RSA-AES256-SHA384", 56 | "ECDHE-ECDSA-AES128-SHA256", 57 | "ECDHE-RSA-AES128-SHA256", 58 | ] 59 | """Strong default TLS ciphers.""" 60 | 61 | 62 | def _context(use_tls=False): 63 | """ 64 | Create a TLS context using the certificate, key and dhparams file. 65 | 66 | :param bool use_tls: Whether to create a TLS context or not. 67 | Default: ``False``. 68 | :returns: A TLS context or ``None``. 69 | :rtype: :py:class:`ssl.SSLContext` or :py:obj:`None`. 70 | 71 | .. note:: 72 | 73 | Created with: 74 | 75 | - :py:obj:`ssl.OP_NO_SSLv2` 76 | - :py:obj:`ssl.OP_NO_SSLv3` 77 | - :py:obj:`ssl.OP_NO_COMPRESSION` 78 | - :py:obj:`ssl.OP_CIPHER_SERVER_PREFERENCE` 79 | 80 | Also responsible for loading Diffie Hellman ephemeral parameters if 81 | they're provided -- :py:func:`ssl.SSLContext.load_dh_params` 82 | 83 | If the ``-ls`` or ``--less-secure`` option is provided, 84 | :py:obj:`ssl.OP_SINGLE_DH_USE` and :py:obj:`ssl.OP_SINGLE_ECDH_USE` 85 | will be omitted from the context. -- 86 | https://kura.gg/blackhole/configuration.html#command-line-options 87 | -- added in :ref:`2.0.13` 88 | """ 89 | if use_tls is False: 90 | return None 91 | config = Config() 92 | ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) 93 | ctx.load_cert_chain(config.tls_cert, config.tls_key) 94 | ctx.options |= ssl.OP_NO_SSLv2 95 | ctx.options |= ssl.OP_NO_SSLv3 96 | ctx.options |= ssl.OP_NO_COMPRESSION 97 | ctx.options |= ssl.OP_CIPHER_SERVER_PREFERENCE 98 | if not config.args.less_secure: 99 | ctx.options |= ssl.OP_SINGLE_DH_USE 100 | ctx.options |= ssl.OP_SINGLE_ECDH_USE 101 | ctx.set_ciphers(":".join(ciphers)) 102 | if config.tls_dhparams: 103 | ctx.load_dh_params(config.tls_dhparams) 104 | return ctx 105 | 106 | 107 | def _socket(addr, port, family): 108 | """ 109 | Create a socket, bind and listen. 110 | 111 | :param str addr: The address to use. 112 | :param int port: The port to use. 113 | :param family: The type of socket to use. 114 | :type family: :py:obj:`socket.AF_INET` or :py:obj:`socket.AF_INET6`. 115 | :returns: Bound socket. 116 | :rtype: :py:func:`socket.socket` 117 | :raises BlackholeRuntimeException: When a socket cannot be bound. 118 | """ 119 | sock = socket.socket(family, socket.SOCK_STREAM) 120 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 121 | try: 122 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) 123 | except (AttributeError, OSError): 124 | pass 125 | if family == socket.AF_INET6: 126 | sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1) 127 | try: 128 | sock.bind((addr, port)) 129 | except OSError: 130 | msg = f"Cannot bind to {addr}:{port}." 131 | logger.critical(msg) 132 | sock.close() 133 | raise BlackholeRuntimeException(msg) 134 | os.set_inheritable(sock.fileno(), True) 135 | sock.listen(1024) 136 | sock.setblocking(False) 137 | return sock 138 | 139 | 140 | def server(addr, port, family, use_tls=False): 141 | """ 142 | Socket and possibly a TLS context. 143 | 144 | Create an instance of :py:func:`socket.socket`, bind it and return a 145 | dictionary containing the socket object and a TLS context if configured. 146 | 147 | :param str addr: The address to use. 148 | :param int port: The port to use. 149 | :param family: The type of socket to use. 150 | :type family: :py:obj:`socket.AF_INET` or :py:obj:`socket.AF_INET6`. 151 | :param bool use_tls: Whether to create a TLS context or not. 152 | Default: ``False``. 153 | :returns: Bound socket, a TLS context if configured. 154 | :rtype: :py:obj:`dict` 155 | """ 156 | sock = _socket(addr, port, family) 157 | ctx = _context(use_tls=use_tls) 158 | return {"sock": sock, "ssl": ctx} 159 | 160 | 161 | def pid_permissions(): 162 | """ 163 | Change the pid file ownership. 164 | 165 | Called before :func:`blackhole.control.setgid` and 166 | :func:`blackhole.control.setuid` are called to stop 167 | :class:`blackhole.daemon.Daemon` losing permissions to modify the pid file. 168 | 169 | :raises SystemExit: With exit code :py:obj:`os.EX_USAGE` when a permissions 170 | error occurs. 171 | """ 172 | config = Config() 173 | try: 174 | user = pwd.getpwnam(config.user) 175 | group = grp.getgrnam(config.group) 176 | os.chown(config.pidfile, user.pw_uid, group.gr_gid) 177 | except (KeyError, PermissionError): 178 | logger.error("Unable to change pidfile ownership permissions.") 179 | raise SystemExit(os.EX_USAGE) 180 | 181 | 182 | def setgid(): 183 | """ 184 | Change group. 185 | 186 | Change to a less privileged group. Unless you're using it incorrectly -- 187 | in which case, don't use it. 188 | 189 | :raises SystemExit: Exit code :py:obj:`os.EX_USAGE` when a configuration 190 | error occurs or :py:obj:`os.EX_NOPERM` when a 191 | permission error occurs. 192 | 193 | .. note:: 194 | 195 | MUST be called BEFORE setuid, not after. 196 | """ 197 | config = Config() 198 | try: 199 | gid = grp.getgrnam(config.group).gr_gid 200 | os.setgid(gid) 201 | except KeyError: 202 | logger.error(f"Group '{config.group}' does not exist.") 203 | raise SystemExit(os.EX_USAGE) 204 | except PermissionError: 205 | logger.error( 206 | f"You do not have permission to switch to group '{config.group}'.", 207 | ) 208 | raise SystemExit(os.EX_NOPERM) 209 | 210 | 211 | def setuid(): 212 | """ 213 | Change user. 214 | 215 | Change to a less privileged user.Unless you're using it incorrectly -- 216 | inwhich case, don't use it. 217 | 218 | :raises SystemExit: Exit code :py:obj:`os.EX_USAGE` when a configuration 219 | error occurs or :py:obj:`os.EX_NOPERM` when a 220 | permission error occurs. 221 | 222 | .. note:: 223 | 224 | MUST be called AFTER setgid, not before. 225 | """ 226 | config = Config() 227 | try: 228 | uid = pwd.getpwnam(config.user).pw_uid 229 | os.setuid(uid) 230 | except KeyError: 231 | logger.error(f"User '{config.user}' does not exist.") 232 | raise SystemExit(os.EX_USAGE) 233 | except PermissionError: 234 | logger.error( 235 | f"You do not have permission to switch to user '{config.user}'.", 236 | ) 237 | raise SystemExit(os.EX_NOPERM) 238 | -------------------------------------------------------------------------------- /blackhole/daemon.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # (The MIT License) 4 | # 5 | # Copyright (c) 2013-2021 Kura 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the 'Software'), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | """Provides daemonisation functionality.""" 26 | 27 | 28 | import atexit 29 | import logging 30 | import os 31 | 32 | from .exceptions import DaemonException 33 | from .utils import Singleton 34 | 35 | 36 | __all__ = ("Daemon",) 37 | """Tuple all the things.""" 38 | 39 | 40 | logger = logging.getLogger("blackhole.daemon") 41 | 42 | 43 | class Daemon(metaclass=Singleton): 44 | """An object for handling daemonisation.""" 45 | 46 | def __init__(self, pidfile): 47 | """ 48 | Create an instance of :class:`Daemon`. 49 | 50 | :param str pidfile: Path to store the pid. 51 | 52 | .. note:: 53 | 54 | Registers an :py.func:`atexit.register` signal to delete the pid on 55 | exit. 56 | """ 57 | self.pidfile = pidfile 58 | self.pid = os.getpid() 59 | atexit.register(self._exit) 60 | 61 | def daemonize(self): 62 | """Daemonize the process.""" 63 | self.fork() 64 | os.chdir(os.path.sep) 65 | os.setsid() 66 | os.umask(0) 67 | self.pid = os.getpid() 68 | 69 | def _exit(self, *args, **kwargs): 70 | """Call on exit using :py:func:`atexit.register` or via a signal.""" 71 | del self.pid 72 | 73 | def fork(self): 74 | """ 75 | Fork off the process. 76 | 77 | :raises SystemExit: With code :py:obj:`os.EX_OK` when fork is 78 | successful. 79 | :raises DaemonException: When fork is unsuccessful. 80 | """ 81 | try: 82 | pid = os.fork() 83 | if pid > 0: 84 | os._exit(os.EX_OK) 85 | except OSError as err: 86 | raise DaemonException(err.strerror) 87 | 88 | @property 89 | def pid(self): 90 | """ 91 | Pid of the process, if it's been daemonised. 92 | 93 | :raises DaemonException: When pid cannot be read from the filesystem. 94 | :returns: The current pid. 95 | :rtype: :py:obj:`int` or :py:obj:`None` 96 | 97 | .. note:: 98 | 99 | The pid is retrieved from the filestem. 100 | If the pid does not exist in /proc, the pid is deleted. 101 | """ 102 | if os.path.exists(self.pidfile): 103 | try: 104 | with open(self.pidfile, "r") as pidfile: 105 | pid = pidfile.read().strip() 106 | if pid != "": 107 | return int(pid) 108 | except IOError as err: 109 | raise DaemonException(str(err)) 110 | return None 111 | 112 | @pid.setter 113 | def pid(self, pid): 114 | """ 115 | Write the pid to the filesystem. 116 | 117 | :param int pid: Process pid. 118 | :raises DaemonException: When writing to filesystem fails. 119 | """ 120 | pid = str(pid) 121 | try: 122 | with open(self.pidfile, "w+") as pidfile: 123 | pidfile.write(f"{pid}\n") 124 | except IOError as err: 125 | raise DaemonException(str(err)) 126 | 127 | @pid.deleter 128 | def pid(self): 129 | """Delete the pid from the filesystem.""" 130 | if os.path.exists(self.pidfile): 131 | os.remove(self.pidfile) 132 | -------------------------------------------------------------------------------- /blackhole/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # (The MIT License) 4 | # 5 | # Copyright (c) 2013-2021 Kura 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the 'Software'), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | """Exceptions...""" 26 | 27 | 28 | __all__ = ("ConfigException", "DaemonException", "BlackholeRuntimeException") 29 | """Tuple all the things.""" 30 | 31 | 32 | class ConfigException(Exception): # pragma: no cover 33 | """Configuration exception.""" 34 | 35 | pass 36 | 37 | 38 | class DaemonException(Exception): # pragma: no cover 39 | """A daemon exception.""" 40 | 41 | pass 42 | 43 | 44 | class BlackholeRuntimeException(Exception): # pragma: no cover 45 | """Blackhole runtime exception.""" 46 | 47 | pass 48 | -------------------------------------------------------------------------------- /blackhole/logs.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # (The MIT License) 4 | # 5 | # Copyright (c) 2013-2021 Kura 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the 'Software'), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | """Configure logging.""" 26 | 27 | 28 | import logging 29 | from logging.config import dictConfig 30 | 31 | 32 | __all__ = ("configure_logs",) 33 | """Tuple all the things.""" 34 | 35 | 36 | DEBUG_FORMAT = ( 37 | "[%(asctime)s] [%(levelname)s] blackhole.%(module)s: %(message)s" 38 | ) 39 | 40 | LOG_CONFIG = { 41 | "formatters": { 42 | "console": {"format": "%(message)s"}, 43 | "debug": {"format": DEBUG_FORMAT}, 44 | }, 45 | "handlers": {}, 46 | "loggers": {"blackhole": {"handlers": [], "level": logging.INFO}}, 47 | "version": 1, 48 | } 49 | 50 | DEBUG_HANDLER = { 51 | "class": "logging.StreamHandler", 52 | "formatter": "debug", 53 | "level": logging.DEBUG, 54 | } 55 | 56 | DEFAULT_HANDLER = { 57 | "class": "logging.StreamHandler", 58 | "formatter": "console", 59 | "level": logging.INFO, 60 | } 61 | 62 | 63 | def configure_logs(args): 64 | """ 65 | Configure the logging module. 66 | 67 | :param argparse.Namespace args: Parameters parsed from :py:mod:`argparse`. 68 | """ 69 | logger_handlers = LOG_CONFIG["loggers"]["blackhole"]["handlers"] 70 | if args.debug: 71 | LOG_CONFIG["loggers"]["blackhole"]["level"] = logging.DEBUG 72 | LOG_CONFIG["handlers"]["default_handler"] = DEBUG_HANDLER 73 | logger_handlers.append("default_handler") 74 | elif args.quiet: 75 | LOG_CONFIG["loggers"]["blackhole"]["level"] = logging.ERROR 76 | LOG_CONFIG["handlers"]["default_handler"] = DEFAULT_HANDLER 77 | LOG_CONFIG["handlers"]["default_handler"]["level"] = logging.ERROR 78 | logger_handlers.append("default_handler") 79 | else: 80 | LOG_CONFIG["loggers"]["blackhole"]["level"] = logging.INFO 81 | LOG_CONFIG["handlers"]["default_handler"] = DEFAULT_HANDLER 82 | logger_handlers.append("default_handler") 83 | dictConfig(LOG_CONFIG) 84 | -------------------------------------------------------------------------------- /blackhole/protocols.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # (The MIT License) 4 | # 5 | # Copyright (c) 2013-2021 Kura 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the 'Software'), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | """Communication protocols used by the worker and child processes.""" 26 | 27 | 28 | import asyncio 29 | import logging 30 | 31 | from .config import Config 32 | 33 | 34 | __all__ = ("StreamReaderProtocol", "PING", "PONG") 35 | """Tuple all the things.""" 36 | 37 | 38 | logger = logging.getLogger("blackhole.protocols") 39 | 40 | 41 | PING = b"x01" 42 | """Protocol message used by the worker and child processes to communicate.""" 43 | 44 | PONG = b"x02" 45 | """Protocol message used by the worker and child processes to communicate.""" 46 | 47 | 48 | class StreamReaderProtocol(asyncio.StreamReaderProtocol): 49 | """The class responsible for handling connections commands.""" 50 | 51 | def __init__(self, clients, loop=None): 52 | """ 53 | Initialise the protocol. 54 | 55 | :param list clients: A list of connected clients. 56 | :param loop: The event loop to use. 57 | :type loop: :py:obj:`None` or 58 | :py:class:`syncio.unix_events._UnixSelectorEventLoop` 59 | """ 60 | logger.debug("init") 61 | self.loop = loop if loop is not None else asyncio.get_event_loop() 62 | logger.debug("loop") 63 | super().__init__( 64 | asyncio.StreamReader(loop=self.loop), 65 | client_connected_cb=self._client_connected_cb, 66 | loop=self.loop, 67 | ) 68 | logger.debug("super") 69 | self.clients = clients 70 | self.config = Config() 71 | logger.debug(self.config) 72 | # This is not a nice way to do this but, socket.getfqdn silently fails 73 | # and crashes inbound connections when called after os.fork 74 | self.fqdn = self.config.mailname 75 | 76 | def flags_from_transport(self): 77 | """Adapt internal flags for the transport in use.""" 78 | # This has to be done here since passing it as part of init causes 79 | # flags to become garbled and mixed up. Artifact of loop.create_server 80 | sock = self.transport.get_extra_info("socket") 81 | # Ideally this would use transport.get_extra_info('sockname') but that 82 | # crashes the child process for some weird reason. Getting the socket 83 | # and interacting directly does not cause a crash, hence... 84 | sock_name = sock.getsockname() 85 | flags = self.config.flags_from_listener(sock_name[0], sock_name[1]) 86 | if len(flags.keys()) > 0: 87 | self._flags = flags 88 | self._disable_dynamic_switching = True 89 | logger.debug("Flags enabled, disabling dynamic switching") 90 | logger.debug(f"Flags for this connection: {self._flags}") 91 | 92 | def _client_connected_cb(self, reader, writer): 93 | """ 94 | Bind a stream reader and writer to the SMTP Protocol. 95 | 96 | :param asyncio.streams.StreamReader reader: An object for reading 97 | incoming data. 98 | :param asyncio.streams.StreamWriter writer: An object for writing 99 | outgoing data. 100 | """ 101 | self._reader = reader 102 | self._writer = writer 103 | self.clients.append(writer) 104 | 105 | def connection_lost(self, exc): 106 | """ 107 | Client connection is closed or lost. 108 | 109 | :param exc exc: Exception. 110 | """ 111 | logger.debug("Peer disconnected") 112 | super().connection_lost(exc) 113 | self.connection_closed, self._connection_closed = True, True 114 | try: 115 | self.clients.remove(self._writer) 116 | except ValueError: 117 | pass 118 | 119 | async def wait(self): 120 | """ 121 | Wait for data from the client. 122 | 123 | :returns: A line of received data. 124 | :rtype: :py:obj:`str` 125 | 126 | .. note:: 127 | 128 | Also handles client timeouts if they wait too long before sending 129 | data. -- https://kura.gg/blackhole/configuration.html#timeout 130 | """ 131 | while not self.connection_closed: 132 | try: 133 | line = await asyncio.wait_for( 134 | self._reader.readline(), 135 | self.config.timeout, 136 | ) 137 | except asyncio.TimeoutError: 138 | await self.timeout() 139 | return None 140 | return line 141 | 142 | async def close(self): 143 | """Close the connection from the client.""" 144 | logger.debug("Closing connection") 145 | if self._writer: 146 | try: 147 | self.clients.remove(self._writer) 148 | except ValueError: 149 | pass 150 | self._writer.close() 151 | await self._writer.drain() 152 | self._connection_closed = True 153 | 154 | async def push(self, msg): 155 | """ 156 | Write a response message to the client. 157 | 158 | :param str msg: The message for the SMTP code 159 | """ 160 | response = f"{msg}\r\n".encode("utf-8") 161 | logger.debug(f"SEND {response}") 162 | self._writer.write(response) 163 | await self._writer.drain() 164 | -------------------------------------------------------------------------------- /blackhole/streams.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # (The MIT License) 4 | # 5 | # Copyright (c) 2013-2021 Kura 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the 'Software'), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | """Provides additional stream classes.""" 26 | 27 | import asyncio 28 | import logging 29 | 30 | 31 | __all__ = ("StreamProtocol",) 32 | """Tuple all the things.""" 33 | 34 | 35 | logger = logging.getLogger("blackhole.streams") 36 | 37 | 38 | class StreamProtocol(asyncio.streams.FlowControlMixin, asyncio.Protocol): 39 | """Helper class to adapt between Protocol and StreamReader.""" 40 | 41 | def __init__(self, *, loop=None, **kwargs): 42 | """Stream protocol.""" 43 | super().__init__(loop=loop) 44 | self.transport = None 45 | self.writer = None 46 | self.reader = asyncio.StreamReader(loop=loop) 47 | 48 | def is_connected(self): 49 | """Client is connected.""" 50 | return self.transport is not None 51 | 52 | def connection_made(self, transport): 53 | """Client connection made callback.""" 54 | self.transport = transport 55 | self.reader.set_transport(transport) 56 | self.writer = asyncio.StreamWriter( 57 | transport, 58 | self, 59 | self.reader, 60 | self._loop, 61 | ) 62 | 63 | def connection_lost(self, exc): 64 | """Client connection lost callback.""" 65 | self.transport = self.writer = None 66 | self.reader._transport = None 67 | 68 | logger.debug(exc) 69 | if exc is None: 70 | self.reader.feed_eof() 71 | else: 72 | self.reader.set_exception(exc) 73 | 74 | super().connection_lost(exc) 75 | 76 | def data_received(self, data): 77 | """Client data received.""" 78 | self.reader.feed_data(data) 79 | 80 | def eof_received(self): 81 | """Client EOF received.""" 82 | self.reader.feed_eof() 83 | -------------------------------------------------------------------------------- /blackhole/supervisor.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # (The MIT License) 4 | # 5 | # Copyright (c) 2013-2021 Kura 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the 'Software'), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | """Provides functionality to create the supervisor process.""" 26 | 27 | 28 | import asyncio 29 | import logging 30 | import os 31 | import signal 32 | 33 | from .config import Config 34 | from .control import server 35 | from .exceptions import BlackholeRuntimeException 36 | from .utils import Singleton 37 | from .worker import Worker 38 | 39 | 40 | try: 41 | import setproctitle 42 | except ImportError: 43 | setproctitle = None 44 | 45 | 46 | __all__ = ("Supervisor",) 47 | """Tuple all the things.""" 48 | 49 | 50 | logger = logging.getLogger("blackhole.supervisor") 51 | 52 | 53 | class Supervisor(metaclass=Singleton): 54 | """ 55 | The supervisor process. 56 | 57 | Responsible for monitoring and controlling child processes via an 58 | internal map of workers and the children they manage. 59 | """ 60 | 61 | def __init__(self, loop=None): 62 | """ 63 | Initialise the supervisor. 64 | 65 | Loads the configuration and event loop. 66 | 67 | :param loop: The event loop to use. 68 | :type loop: :py:class:`syncio.unix_events._UnixSelectorEventLoop` or 69 | :py:obj:`None` to get the current event loop using 70 | :py:func:`asyncio.get_event_loop`. 71 | :raises BlackholeRuntimeException: When an error occurs generating 72 | servers. 73 | """ 74 | logger.debug("Initiating the supervisor") 75 | self.config = Config() 76 | self.loop = loop if loop is not None else asyncio.get_event_loop() 77 | self.socks = [] 78 | self.workers = [] 79 | if setproctitle: 80 | setproctitle.setproctitle("blackhole: master") 81 | try: 82 | self.generate_servers() 83 | except BlackholeRuntimeException: 84 | self.close_socks() 85 | raise BlackholeRuntimeException() 86 | 87 | def generate_servers(self): 88 | """Spawn all of the required sockets and TLS contexts.""" 89 | logger.debug("Attaching sockets to the supervisor") 90 | self.create_socket(self.config.listen) 91 | tls_conf = (self.config.tls_cert, self.config.tls_key) 92 | if len(self.config.tls_listen) > 0 and all(tls_conf): 93 | self.create_socket(self.config.tls_listen, use_tls=True) 94 | 95 | def create_socket(self, listeners, use_tls=False): 96 | """Create supervisor socket.""" 97 | _tls = "" 98 | if use_tls: 99 | _tls = " (TLS)" 100 | for host, port, family, flags in listeners: 101 | aserver = server(host, port, family, use_tls=use_tls) 102 | self.socks.append(aserver) 103 | logger.debug(f"Attaching {host}:{port}{_tls} with flags {flags}") 104 | 105 | def run(self): 106 | """ 107 | Start all workers and their children. 108 | 109 | Attaches signals and runs the event loop. 110 | """ 111 | self.start_workers() 112 | signal.signal(signal.SIGTERM, self.stop) 113 | signal.signal(signal.SIGINT, self.stop) 114 | self.loop.run_forever() 115 | 116 | def start_workers(self): 117 | """Start each worker and it's child process.""" 118 | logger.debug("Starting workers") 119 | for idx in range(self.config.workers): 120 | num = f"{idx + 1}" 121 | logger.debug(f"Creating worker: {num}") 122 | self.workers.append(Worker(num, self.socks, self.loop)) 123 | 124 | def stop_workers(self): 125 | """Stop the workers and their respective child process.""" 126 | logger.debug("Stopping workers") 127 | worker_num = 1 128 | for worker in self.workers: 129 | logger.debug(f"Stopping worker: {worker_num}") 130 | worker.stop() 131 | worker_num += 1 132 | 133 | def close_socks(self): 134 | """Close all opened sockets.""" 135 | for sock in self.socks: 136 | sock["sock"].close() 137 | 138 | def stop(self, *args, **kwargs): 139 | """ 140 | Terminate all of the workers. 141 | 142 | Generally should be called by a signal, nothing else. 143 | 144 | :raise SystemExit: With code :py:obj:`os.EX_OK`. 145 | """ 146 | self.stop_workers() 147 | self.close_socks() 148 | logger.debug("Stopping supervisor") 149 | try: 150 | self.loop.stop() 151 | except RuntimeError: 152 | pass 153 | raise SystemExit(os.EX_OK) 154 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | --- 2 | comment: false 3 | coverage: 4 | status: 5 | patch: 6 | default: 7 | target: "90" 8 | project: 9 | default: 10 | target: "90" 11 | -------------------------------------------------------------------------------- /docs/source/_static/blackhole.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kura/blackhole/5d5e6f4395a413ed99b6c0721af1a86276a6225f/docs/source/_static/blackhole.png -------------------------------------------------------------------------------- /docs/source/api-application.rst: -------------------------------------------------------------------------------- 1 | .. 2 | # (The MIT License) 3 | # 4 | # Copyright (c) 2013-2021 Kura 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a 7 | # copy of this software and associated documentation files (the 8 | # 'Software'), to deal in the Software without restriction, including 9 | # without limitation the rights to use, copy, modify, merge, publish, 10 | # distribute, sublicense, and/or sell copies of the Software, and to permit 11 | # persons to whom the Software is furnished to do so, subject to the 12 | # following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included 15 | # in all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS 18 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 20 | # NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 21 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 22 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 23 | # USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | ============================ 26 | :mod:`blackhole.application` 27 | ============================ 28 | 29 | .. module:: blackhole.application 30 | :platform: Unix 31 | :synopsis: Provides functionality to run the server. 32 | .. moduleauthor:: Kura 33 | 34 | Provides functionality to run the server. 35 | 36 | .. autofunction:: blackhole_config 37 | 38 | .. autofunction:: run 39 | -------------------------------------------------------------------------------- /docs/source/api-child.rst: -------------------------------------------------------------------------------- 1 | .. 2 | # (The MIT License) 3 | # 4 | # Copyright (c) 2013-2021 Kura 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a 7 | # copy of this software and associated documentation files (the 8 | # 'Software'), to deal in the Software without restriction, including 9 | # without limitation the rights to use, copy, modify, merge, publish, 10 | # distribute, sublicense, and/or sell copies of the Software, and to permit 11 | # persons to whom the Software is furnished to do so, subject to the 12 | # following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included 15 | # in all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS 18 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 20 | # NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 21 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 22 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 23 | # USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | ====================== 26 | :mod:`blackhole.child` 27 | ====================== 28 | 29 | .. module:: blackhole.child 30 | :platform: Unix 31 | :synopsis: Provides functionality to spawn and control child processes. 32 | .. moduleauthor:: Kura 33 | 34 | Provides functionality to spawn and control child processes. 35 | 36 | .. autoclass:: Child 37 | :inherited-members: 38 | :member-order: bysource 39 | -------------------------------------------------------------------------------- /docs/source/api-config.rst: -------------------------------------------------------------------------------- 1 | .. 2 | # (The MIT License) 3 | # 4 | # Copyright (c) 2013-2021 Kura 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a 7 | # copy of this software and associated documentation files (the 8 | # 'Software'), to deal in the Software without restriction, including 9 | # without limitation the rights to use, copy, modify, merge, publish, 10 | # distribute, sublicense, and/or sell copies of the Software, and to permit 11 | # persons to whom the Software is furnished to do so, subject to the 12 | # following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included 15 | # in all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS 18 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 20 | # NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 21 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 22 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 23 | # USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | ======================= 26 | :mod:`blackhole.config` 27 | ======================= 28 | 29 | .. module:: blackhole.config 30 | :platform: Unix 31 | :synopsis: Configuration structure and functionality for testing config 32 | validity. 33 | .. moduleauthor:: Kura 34 | 35 | Configuration structure and functionality for testing config validity. 36 | 37 | .. autofunction:: parse_cmd_args 38 | 39 | .. autofunction:: warn_options 40 | 41 | .. autofunction:: config_test 42 | 43 | .. autofunction:: _compare_uid_and_gid 44 | 45 | .. autoclass:: Config 46 | :inherited-members: 47 | :member-order: bysource 48 | -------------------------------------------------------------------------------- /docs/source/api-control.rst: -------------------------------------------------------------------------------- 1 | .. 2 | # (The MIT License) 3 | # 4 | # Copyright (c) 2013-2021 Kura 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a 7 | # copy of this software and associated documentation files (the 8 | # 'Software'), to deal in the Software without restriction, including 9 | # without limitation the rights to use, copy, modify, merge, publish, 10 | # distribute, sublicense, and/or sell copies of the Software, and to permit 11 | # persons to whom the Software is furnished to do so, subject to the 12 | # following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included 15 | # in all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS 18 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 20 | # NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 21 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 22 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 23 | # USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | ======================== 26 | :mod:`blackhole.control` 27 | ======================== 28 | 29 | .. module:: blackhole.control 30 | :platform: Unix 31 | :synopsis: Provides control functionality, including socket wrappers. 32 | .. moduleauthor:: Kura 33 | 34 | Provides control functionality, including socket wrappers. 35 | 36 | .. autofunction:: _context 37 | 38 | .. autofunction:: _socket 39 | 40 | .. autofunction:: server 41 | 42 | .. autofunction:: pid_permissions 43 | 44 | .. autofunction:: setgid 45 | 46 | .. autofunction:: setuid 47 | -------------------------------------------------------------------------------- /docs/source/api-daemon.rst: -------------------------------------------------------------------------------- 1 | .. 2 | # (The MIT License) 3 | # 4 | # Copyright (c) 2013-2021 Kura 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a 7 | # copy of this software and associated documentation files (the 8 | # 'Software'), to deal in the Software without restriction, including 9 | # without limitation the rights to use, copy, modify, merge, publish, 10 | # distribute, sublicense, and/or sell copies of the Software, and to permit 11 | # persons to whom the Software is furnished to do so, subject to the 12 | # following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included 15 | # in all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS 18 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 20 | # NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 21 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 22 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 23 | # USE OR OTHER DEALINGS IN THE SOFTWARE... 24 | 25 | ======================= 26 | :mod:`blackhole.daemon` 27 | ======================= 28 | 29 | .. module:: blackhole.daemon 30 | :platform: Unix 31 | :synopsis: Provides daemonisation functionality. 32 | .. moduleauthor:: Kura 33 | 34 | Provides daemonisation functionality. 35 | 36 | .. autoclass:: Daemon 37 | :inherited-members: 38 | :member-order: bysource 39 | -------------------------------------------------------------------------------- /docs/source/api-exceptions.rst: -------------------------------------------------------------------------------- 1 | .. 2 | # (The MIT License) 3 | # 4 | # Copyright (c) 2013-2021 Kura 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a 7 | # copy of this software and associated documentation files (the 8 | # 'Software'), to deal in the Software without restriction, including 9 | # without limitation the rights to use, copy, modify, merge, publish, 10 | # distribute, sublicense, and/or sell copies of the Software, and to permit 11 | # persons to whom the Software is furnished to do so, subject to the 12 | # following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included 15 | # in all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS 18 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 20 | # NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 21 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 22 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 23 | # USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | =========================== 26 | :mod:`blackhole.exceptions` 27 | =========================== 28 | 29 | .. module:: blackhole.exceptions 30 | :platform: Unix 31 | :synopsis: Exceptions... 32 | .. moduleauthor:: Kura 33 | 34 | Exceptions... 35 | 36 | .. autoexception:: ConfigException 37 | :inherited-members: 38 | :member-order: bysource 39 | 40 | .. autoexception:: DaemonException 41 | :inherited-members: 42 | :member-order: bysource 43 | 44 | .. autoexception:: BlackholeRuntimeException 45 | :inherited-members: 46 | :member-order: bysource 47 | -------------------------------------------------------------------------------- /docs/source/api-logs.rst: -------------------------------------------------------------------------------- 1 | .. 2 | # (The MIT License) 3 | # 4 | # Copyright (c) 2013-2021 Kura 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a 7 | # copy of this software and associated documentation files (the 8 | # 'Software'), to deal in the Software without restriction, including 9 | # without limitation the rights to use, copy, modify, merge, publish, 10 | # distribute, sublicense, and/or sell copies of the Software, and to permit 11 | # persons to whom the Software is furnished to do so, subject to the 12 | # following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included 15 | # in all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS 18 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 20 | # NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 21 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 22 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 23 | # USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | ===================== 26 | :mod:`blackhole.logs` 27 | ===================== 28 | 29 | .. module:: blackhole.logs 30 | :platform: Unix 31 | :synopsis: Configure logging. 32 | .. moduleauthor:: Kura 33 | 34 | Configure logging. 35 | 36 | .. autofunction:: configure_logs 37 | -------------------------------------------------------------------------------- /docs/source/api-protocols.rst: -------------------------------------------------------------------------------- 1 | .. 2 | # (The MIT License) 3 | # 4 | # Copyright (c) 2013-2021 Kura 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a 7 | # copy of this software and associated documentation files (the 8 | # 'Software'), to deal in the Software without restriction, including 9 | # without limitation the rights to use, copy, modify, merge, publish, 10 | # distribute, sublicense, and/or sell copies of the Software, and to permit 11 | # persons to whom the Software is furnished to do so, subject to the 12 | # following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included 15 | # in all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS 18 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 20 | # NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 21 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 22 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 23 | # USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | ========================== 26 | :mod:`blackhole.protocols` 27 | ========================== 28 | 29 | .. module:: blackhole.protocols 30 | :platform: Unix 31 | .. moduleauthor:: Kura 32 | 33 | .. autodata:: PING 34 | 35 | .. autodata:: PONG 36 | -------------------------------------------------------------------------------- /docs/source/api-smtp.rst: -------------------------------------------------------------------------------- 1 | .. 2 | # (The MIT License) 3 | # 4 | # Copyright (c) 2013-2021 Kura 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a 7 | # copy of this software and associated documentation files (the 8 | # 'Software'), to deal in the Software without restriction, including 9 | # without limitation the rights to use, copy, modify, merge, publish, 10 | # distribute, sublicense, and/or sell copies of the Software, and to permit 11 | # persons to whom the Software is furnished to do so, subject to the 12 | # following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included 15 | # in all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS 18 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 20 | # NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 21 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 22 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 23 | # USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | ===================== 26 | :mod:`blackhole.smtp` 27 | ===================== 28 | 29 | .. module:: blackhole.smtp 30 | :platform: Unix 31 | :synopsis: Provides the Smtp protocol wrapper. 32 | .. moduleauthor:: Kura 33 | 34 | Provides the Smtp protocol wrapper. 35 | 36 | .. autoclass:: Smtp 37 | :inherited-members: 38 | :member-order: bysource 39 | -------------------------------------------------------------------------------- /docs/source/api-streams.rst: -------------------------------------------------------------------------------- 1 | .. 2 | # (The MIT License) 3 | # 4 | # Copyright (c) 2013-2021 Kura 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a 7 | # copy of this software and associated documentation files (the 8 | # 'Software'), to deal in the Software without restriction, including 9 | # without limitation the rights to use, copy, modify, merge, publish, 10 | # distribute, sublicense, and/or sell copies of the Software, and to permit 11 | # persons to whom the Software is furnished to do so, subject to the 12 | # following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included 15 | # in all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS 18 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 20 | # NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 21 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 22 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 23 | # USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | ======================== 26 | :mod:`blackhole.streams` 27 | ======================== 28 | 29 | .. module:: blackhole.streams 30 | :platform: Unix 31 | :synopsis: Provides additional stream classes. 32 | .. moduleauthor:: Kura 33 | 34 | Provides additional stream classes. 35 | 36 | .. autoclass:: StreamProtocol 37 | :inherited-members: 38 | :member-order: bysource 39 | -------------------------------------------------------------------------------- /docs/source/api-supervisor.rst: -------------------------------------------------------------------------------- 1 | .. 2 | # (The MIT License) 3 | # 4 | # Copyright (c) 2013-2021 Kura 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a 7 | # copy of this software and associated documentation files (the 8 | # 'Software'), to deal in the Software without restriction, including 9 | # without limitation the rights to use, copy, modify, merge, publish, 10 | # distribute, sublicense, and/or sell copies of the Software, and to permit 11 | # persons to whom the Software is furnished to do so, subject to the 12 | # following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included 15 | # in all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS 18 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 20 | # NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 21 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 22 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 23 | # USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | =========================== 26 | :mod:`blackhole.supervisor` 27 | =========================== 28 | 29 | .. module:: blackhole.supervisor 30 | :platform: Unix 31 | :synopsis: Provides functionality to create the supervisor process. 32 | .. moduleauthor:: Kura 33 | 34 | Provides functionality to create the supervisor process. 35 | 36 | .. autoclass:: Supervisor 37 | :inherited-members: 38 | :member-order: bysource 39 | -------------------------------------------------------------------------------- /docs/source/api-utils.rst: -------------------------------------------------------------------------------- 1 | .. 2 | # (The MIT License) 3 | # 4 | # Copyright (c) 2013-2021 Kura 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a 7 | # copy of this software and associated documentation files (the 8 | # 'Software'), to deal in the Software without restriction, including 9 | # without limitation the rights to use, copy, modify, merge, publish, 10 | # distribute, sublicense, and/or sell copies of the Software, and to permit 11 | # persons to whom the Software is furnished to do so, subject to the 12 | # following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included 15 | # in all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS 18 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 20 | # NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 21 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 22 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 23 | # USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | ====================== 26 | :mod:`blackhole.utils` 27 | ====================== 28 | 29 | .. module:: blackhole.utils 30 | :platform: Unix 31 | :synopsis: Provides utility functionality. 32 | .. moduleauthor:: Kura 33 | 34 | Provides utility functionality. 35 | 36 | .. autoclass:: Singleton 37 | 38 | .. autofunction:: mailname 39 | 40 | .. autofunction:: message_id 41 | 42 | .. autofunction:: get_version 43 | 44 | .. autoclass:: Formatter 45 | 46 | .. py:data:: formatting 47 | 48 | An instance of `Formatter` used with `blackhole_config`. 49 | 50 | .. py:data:: blackhole_config_help 51 | 52 | The output message including formatting like bold and underline. 53 | -------------------------------------------------------------------------------- /docs/source/api-worker.rst: -------------------------------------------------------------------------------- 1 | .. 2 | # (The MIT License) 3 | # 4 | # Copyright (c) 2013-2021 Kura 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a 7 | # copy of this software and associated documentation files (the 8 | # 'Software'), to deal in the Software without restriction, including 9 | # without limitation the rights to use, copy, modify, merge, publish, 10 | # distribute, sublicense, and/or sell copies of the Software, and to permit 11 | # persons to whom the Software is furnished to do so, subject to the 12 | # following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included 15 | # in all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS 18 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 20 | # NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 21 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 22 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 23 | # USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | ======================= 26 | :mod:`blackhole.worker` 27 | ======================= 28 | 29 | .. module:: blackhole.worker 30 | :platform: Unix 31 | :synopsis: Provides functionality to manage child processes from the 32 | supervisor. 33 | .. moduleauthor:: Kura 34 | 35 | Provides functionality to manage child processes from the supervisor. 36 | 37 | .. autoclass:: Worker 38 | :inherited-members: 39 | :member-order: bysource 40 | -------------------------------------------------------------------------------- /docs/source/api.rst: -------------------------------------------------------------------------------- 1 | .. 2 | # (The MIT License) 3 | # 4 | # Copyright (c) 2013-2021 Kura 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a 7 | # copy of this software and associated documentation files (the 8 | # 'Software'), to deal in the Software without restriction, including 9 | # without limitation the rights to use, copy, modify, merge, publish, 10 | # distribute, sublicense, and/or sell copies of the Software, and to permit 11 | # persons to whom the Software is furnished to do so, subject to the 12 | # following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included 15 | # in all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS 18 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 20 | # NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 21 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 22 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 23 | # USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | .. _api: 26 | 27 | === 28 | API 29 | === 30 | 31 | .. toctree:: 32 | :maxdepth: 2 33 | 34 | api-application 35 | api-child 36 | api-config 37 | api-control 38 | api-daemon 39 | api-exceptions 40 | api-logs 41 | api-protocols 42 | api-smtp 43 | api-streams 44 | api-supervisor 45 | api-utils 46 | api-worker 47 | -------------------------------------------------------------------------------- /docs/source/changelog.rst: -------------------------------------------------------------------------------- 1 | ../../CHANGELOG.rst -------------------------------------------------------------------------------- /docs/source/command-auth.rst: -------------------------------------------------------------------------------- 1 | .. 2 | # (The MIT License) 3 | # 4 | # Copyright (c) 2013-2021 Kura 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a 7 | # copy of this software and associated documentation files (the 8 | # 'Software'), to deal in the Software without restriction, including 9 | # without limitation the rights to use, copy, modify, merge, publish, 10 | # distribute, sublicense, and/or sell copies of the Software, and to permit 11 | # persons to whom the Software is furnished to do so, subject to the 12 | # following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included 15 | # in all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS 18 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 20 | # NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 21 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 22 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 23 | # USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | .. _auth: 26 | 27 | ==== 28 | AUTH 29 | ==== 30 | 31 | AUTH PLAIN 32 | ========== 33 | 34 | :Syntax: 35 | **AUTH PLAIN** 36 | :Optional: 37 | *auth data* 38 | 39 | By default, ``AUTH PLAIN`` will succeed unless you ask it not to. 40 | 41 | Succeed 42 | ------- 43 | 44 | .. code-block:: 45 | 46 | >>> AUTH PLAIN 47 | 334 48 | >>> fail=letmein 49 | 535 5.7.8 Authentication failed 50 | 51 | .. code-block:: 52 | 53 | >>> AUTH PLAIN pass=letmein 54 | 235 2.7.0 Authentication successful 55 | 56 | Fail 57 | ---- 58 | 59 | .. code-block:: 60 | 61 | >>> AUTH PLAIN 62 | 334 63 | >>> pass=letmein 64 | 235 2.7.0 Authentication successful 65 | 66 | .. code-block:: 67 | 68 | >>> AUTH PLAIN fail=letmein 69 | 535 5.7.8 Authentication failed 70 | 71 | 72 | AUTH LOGIN 73 | ========== 74 | 75 | :Syntax: 76 | **AUTH LOGIN** 77 | 78 | By default, ``AUTH PLAIN`` will succeed unless you ask it not to. 79 | 80 | Succeed 81 | ------- 82 | 83 | .. code-block:: 84 | 85 | >>> AUTH LOGIN: 86 | 334 VXNlcm5hbWU6 87 | >>> pass=letmein 88 | 235 2.7.0 Authentication successful 89 | 90 | Fail 91 | ---- 92 | 93 | .. code-block:: 94 | 95 | >>> AUTH LOGIN: 96 | 334 VXNlcm5hbWU6 97 | >>> fail=letmein 98 | 535 5.7.8 Authentication failed 99 | 100 | AUTH CRAM-MD5 101 | ============= 102 | 103 | :Syntax: 104 | **AUTH CRAM-MD5** 105 | 106 | By default, ``AUTH PLAIN`` will succeed unless you ask it not to. 107 | 108 | Succeed 109 | ------- 110 | 111 | .. code-block:: 112 | 113 | >>> AUTH CRAM-MD5 114 | 334 PDE0NjE5MzA1OTYwMS4yMDQ5LjEyMz... 115 | >>> pass=letmein 116 | 235 2.7.0 Authentication successful 117 | 118 | Fail 119 | ---- 120 | 121 | .. code-block:: 122 | 123 | >>> AUTH CRAM-MD5 124 | 334 PDE0NjE5MzA1OTYwMS4yMDQ5LjEyMz... 125 | >>> fail=letmein 126 | 535 5.7.8 Authentication failed 127 | -------------------------------------------------------------------------------- /docs/source/command-expn.rst: -------------------------------------------------------------------------------- 1 | .. 2 | # (The MIT License) 3 | # 4 | # Copyright (c) 2013-2021 Kura 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a 7 | # copy of this software and associated documentation files (the 8 | # 'Software'), to deal in the Software without restriction, including 9 | # without limitation the rights to use, copy, modify, merge, publish, 10 | # distribute, sublicense, and/or sell copies of the Software, and to permit 11 | # persons to whom the Software is furnished to do so, subject to the 12 | # following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included 15 | # in all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS 18 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 20 | # NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 21 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 22 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 23 | # USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | .. _expn: 26 | 27 | ==== 28 | EXPN 29 | ==== 30 | 31 | The EXPN command/verb can be asked to respond in several ways. 32 | 33 | EXPN without an argument 34 | ======================== 35 | 36 | :Syntax: 37 | **EXPN** 38 | 39 | Calling the ``EXPN`` command with no arguments or an invalid mailing list name 40 | will cause it to return an error. 41 | 42 | .. code-block:: 43 | 44 | >>> EXPN 45 | 550 Not authorised 46 | 47 | .. code-block:: 48 | 49 | >>> EXPN some-list 50 | 550 Not authorised 51 | 52 | You can also explicitly tell ``EXPN`` to fail by passing ``fail=`` as part of 53 | the mailing list name. 54 | 55 | .. code-block:: 56 | 57 | >>> EXPN fail=test-list 58 | 550 Not authorised 59 | 60 | EXPN 61 | =========== 62 | 63 | :Syntax: 64 | **EXPN** *mailing-list* 65 | 66 | Three lists are built-in to Blackhole for use. 67 | 68 | ``list1``, ``list2`` and ``list3`` and can be used as follows. 69 | 70 | list1 71 | ----- 72 | 73 | .. code-block:: 74 | 75 | >>> EXPN list1 76 | 250-Shadow 77 | 250-Wednesday 78 | 250 Low-key Liesmith 79 | 80 | list2 81 | ----- 82 | 83 | .. code-block:: 84 | 85 | >>> EXPN list2 86 | 250-Jim Holden 87 | 250-Naomi Nagata 88 | 250-Alex Kamal 89 | 250 Amos Burton 90 | 91 | list3 92 | ----- 93 | 94 | .. code-block:: 95 | 96 | >>> EXPN list3 97 | 250-Takeshi Kovacs 98 | 250-Laurens Bancroft 99 | 250-Kristin Ortega 100 | 250-Quellcrist Falconer 101 | 250-Virginia Vidaura 102 | 250 Reileen Kawahara 103 | 104 | EXPN all 105 | ======== 106 | 107 | :Syntax: 108 | **EXPN** *all* 109 | 110 | The ``all`` argument combines all three lists and returns them. 111 | 112 | .. code-block:: 113 | 114 | >>> EXPN all 115 | 250-Jim Holden 116 | 250-Naomi Nagata 117 | 250-Alex Kamal 118 | 250-Amos Burton 119 | 250-Shadow 120 | 250-Wednesday 121 | 250-Low-key Liesmith 122 | 250-Takeshi Kovacs 123 | 250-Laurens Bancroft 124 | 250-Kristin Ortega 125 | 250-Quellcrist Falconer 126 | 250-Virginia Vidaura 127 | 250 Reileen Kawahara 128 | -------------------------------------------------------------------------------- /docs/source/command-vrfy.rst: -------------------------------------------------------------------------------- 1 | .. 2 | # (The MIT License) 3 | # 4 | # Copyright (c) 2013-2021 Kura 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a 7 | # copy of this software and associated documentation files (the 8 | # 'Software'), to deal in the Software without restriction, including 9 | # without limitation the rights to use, copy, modify, merge, publish, 10 | # distribute, sublicense, and/or sell copies of the Software, and to permit 11 | # persons to whom the Software is furnished to do so, subject to the 12 | # following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included 15 | # in all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS 18 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 20 | # NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 21 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 22 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 23 | # USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | .. _vrfy: 26 | 27 | ==== 28 | VRFY 29 | ==== 30 | 31 | :Syntax: 32 | **VRFY** *user@domain.tld* 33 | 34 | By default, VRFY will respond with a ``252`` response. 35 | 36 | .. code-block:: 37 | 38 | >>> VRFY user@domain.tld 39 | 252 2.0.0 Will attempt delivery 40 | 41 | You can explicitly tell it to return a ``250`` response. 42 | 43 | .. code-block:: 44 | 45 | >>> VRFY pass=user@domain.tld 46 | 250 2.0.0 OK 47 | 48 | Or explicitly tell it to fail. 49 | 50 | .. code-block:: 51 | 52 | >>> VRFY fail=user@domain.tld 53 | 550 5.7.1 unknown 54 | -------------------------------------------------------------------------------- /docs/source/communicating-with-blackhole.rst: -------------------------------------------------------------------------------- 1 | .. 2 | # (The MIT License) 3 | # 4 | # Copyright (c) 2013-2021 Kura 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a 7 | # copy of this software and associated documentation files (the 8 | # 'Software'), to deal in the Software without restriction, including 9 | # without limitation the rights to use, copy, modify, merge, publish, 10 | # distribute, sublicense, and/or sell copies of the Software, and to permit 11 | # persons to whom the Software is furnished to do so, subject to the 12 | # following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included 15 | # in all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS 18 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 20 | # NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 21 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 22 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 23 | # USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | ============================ 26 | Communicating with Blackhole 27 | ============================ 28 | 29 | With Python 30 | =========== 31 | 32 | .. code-block:: python 33 | 34 | # from smtplib import SMTP # For sending without SSL/TLS. 35 | from smtplib import SMTP_SSL 36 | 37 | server = 'blackhole.io' # Change this to your server address. 38 | msg = '''From: 39 | To: 40 | Subject: Test email 41 | 42 | Random test email. Some UTF-8 characters: ßæøþ''' 43 | 44 | # smtp = SMTP(server, 25) # For sending without SSL/TLS. 45 | smtp = SMTP_SSL(server, 465) 46 | smtp.sendmail('test@blackhole.io', 'test@blackhole.io', 47 | msg.encode('utf-8')) 48 | 49 | # We can send multiple messages using the same connection. 50 | smtp.sendmail(server, 'tset@blackhole.io', 51 | msg.encode('utf-8')) 52 | 53 | # Quit after we're done 54 | smtp.quit() 55 | 56 | 57 | .. _commands: 58 | 59 | Supported commands/verbs & parameters 60 | ===================================== 61 | 62 | - :ref:`auth` 63 | - `DATA`_ 64 | - `EHLO`_ 65 | - `ETRN`_ 66 | - :ref:`expn` 67 | - `HELO`_ 68 | - `HELP`_ 69 | - `MAIL`_ **BODY=** `7BIT`_, `8BITMIME`_, `SMTPUTF8`_ **SIZE=** `SIZE`_ 70 | - `NOOP`_ 71 | - `QUIT`_ 72 | - `RCPT`_ 73 | - `RSET`_ 74 | - :ref:`vrfy` 75 | 76 | ----- 77 | 78 | .. _DATA: 79 | 80 | DATA 81 | ---- 82 | 83 | :Syntax: 84 | **DATA** 85 | 86 | .. code-block:: 87 | 88 | >>> DATA 89 | 354 End data with . 90 | >>> some email content 91 | >>> . 92 | 93 | ----- 94 | 95 | .. _EHLO: 96 | 97 | EHLO 98 | ---- 99 | 100 | :Syntax: 101 | **EHLO** *domain.tld* 102 | 103 | .. code-block:: 104 | 105 | >>> EHLO domain.tld 106 | 250-blackhole.io 107 | 250-HELP 108 | 250-PIPELINING 109 | 250-AUTH CRAM-MD5 LOGIN PLAIN 110 | 250-SIZE 111 | 250-VRFY 112 | 250-ETRN 113 | 250-ENHANCEDSTATUSCODES 114 | 250-8BITMIME 115 | 250-SMTPUTF8 116 | 250 DSN 117 | 118 | ----- 119 | 120 | .. _ETRN: 121 | 122 | ETRN 123 | ---- 124 | 125 | :Syntax: 126 | **ETRN** 127 | 128 | .. code-block:: 129 | 130 | >>> ETRN 131 | 250 Queueing started 132 | 133 | ----- 134 | 135 | .. _HELO: 136 | 137 | HELO 138 | ---- 139 | 140 | :Syntax: 141 | **HELO** *domain.tld* 142 | 143 | .. code-block:: 144 | 145 | >>> HELO domain.tld 146 | 250 OK 147 | 148 | ----- 149 | 150 | .. _HELP: 151 | 152 | HELP 153 | ---- 154 | 155 | :Syntax: 156 | **HELP** 157 | :Optional: 158 | *COMMAND* 159 | 160 | .. code-block:: 161 | 162 | >>> HELP 163 | 250 Supported commands: AUTH DATA EHLO ETRN HELO MAIL NOOP QUIT RCPT RSET 164 | VRFY 165 | 166 | >>> HELP AUTH 167 | 250 Syntax: AUTH CRAM-MD5 LOGIN PLAIN 168 | 169 | ----- 170 | 171 | .. _MAIL: 172 | .. _7BIT: 173 | .. _8BITMIME: 174 | .. _SMTPUTF8: 175 | 176 | MAIL 177 | ---- 178 | 179 | :Syntax: 180 | **MAIL FROM:** ** 181 | :Optional: 182 | BODY= *7BIT, 8BITMIME* 183 | :Optional: 184 | *SMTPUTF8* 185 | :Optional: 186 | SIZE= *SIZE* 187 | 188 | .. code-block:: 189 | 190 | >>> MAIL FROM: 191 | 250 2.1.0 OK 192 | 193 | BODY= 194 | ~~~~~ 195 | 196 | .. code-block:: 197 | 198 | >>> MAIL FROM: BODY=7BIT 199 | 250 2.1.0 OK 200 | 201 | .. code-block:: 202 | 203 | >>> MAIL FROM: BODY=8BITMIME 204 | 250 2.1.0 OK 205 | 206 | .. code-block:: 207 | 208 | >>> MAIL FROM: SMTPUTF8 209 | 250 2.1.0 OK 210 | 211 | .. _SIZE: 212 | 213 | SIZE= 214 | ~~~~~ 215 | 216 | You can also specify the size using the ``SIZE=`` parameter. 217 | 218 | .. code-block:: 219 | 220 | >>> MAIL FROM: SIZE=82000 221 | 250 2.1.0 OK 222 | 223 | ----- 224 | 225 | .. _NOOP: 226 | 227 | NOOP 228 | ---- 229 | 230 | :Syntax: 231 | **NOOP** 232 | 233 | .. code-block:: 234 | 235 | >>> NOOP 236 | 250 2.0.0 OK 237 | 238 | ----- 239 | 240 | .. _QUIT: 241 | 242 | QUIT 243 | ---- 244 | 245 | :Syntax: 246 | **QUIT** 247 | 248 | .. code-block:: 249 | 250 | >>> QUIT 251 | 221 2.0.0 Goodbye 252 | 253 | ----- 254 | 255 | .. _RCPT: 256 | 257 | RCPT 258 | ---- 259 | 260 | :Syntax: 261 | **RCPT TO:** ** 262 | 263 | .. code-block:: 264 | 265 | >>> RCPT TO: 266 | 250 2.1.0 OK 267 | 268 | ----- 269 | 270 | .. _RSET: 271 | 272 | RSET 273 | ---- 274 | 275 | :Syntax: 276 | **RSET** 277 | 278 | .. code-block:: 279 | 280 | >>> RSET 281 | 250 2.0.0 OK 282 | 283 | 284 | .. _response-codes: 285 | 286 | Response codes 287 | ============== 288 | 289 | Accept codes 290 | ------------ 291 | 292 | :: 293 | 294 | 250: 2.0.0 OK: queued as MESSAGE-ID 295 | 296 | Bounce codes 297 | ------------ 298 | 299 | :: 300 | 301 | 450: Requested mail action not taken: mailbox unavailable 302 | 451: Requested action aborted: local error in processing 303 | 452: Requested action not taken: insufficient system storage 304 | 458: Unable to queue message 305 | 521: Machine does not accept mail 306 | 550: Requested action not taken: mailbox unavailable 307 | 551: User not local 308 | 552: Requested mail action aborted: exceeded storage allocation 309 | 553: Requested action not taken: mailbox name not allowed 310 | 571: Blocked 311 | -------------------------------------------------------------------------------- /docs/source/docutils.conf: -------------------------------------------------------------------------------- 1 | [parsers] 2 | smart_quotes: yes 3 | -------------------------------------------------------------------------------- /docs/source/dynamic-responses.rst: -------------------------------------------------------------------------------- 1 | .. 2 | # (The MIT License) 3 | # 4 | # Copyright (c) 2013-2021 Kura 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a 7 | # copy of this software and associated documentation files (the 8 | # 'Software'), to deal in the Software without restriction, including 9 | # without limitation the rights to use, copy, modify, merge, publish, 10 | # distribute, sublicense, and/or sell copies of the Software, and to permit 11 | # persons to whom the Software is furnished to do so, subject to the 12 | # following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included 15 | # in all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS 18 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 20 | # NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 21 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 22 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 23 | # USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | .. _dynamic-responses: 26 | 27 | ================= 28 | Dynamic responses 29 | ================= 30 | 31 | Some commands allow you to define how they respond. For instance, telling an 32 | ``AUTH`` command or ``VRFY`` command to fail. 33 | 34 | You can specify ``pass=`` or ``fail=`` as part of a client request to trigger 35 | it to succeed or fail, as you require. 36 | 37 | Below is a list of commands/verbs that allow you to modify their behaviour 38 | on-the-fly, including how they can be modified and what they return. 39 | 40 | .. toctree:: 41 | :maxdepth: 2 42 | 43 | command-auth 44 | command-vrfy 45 | command-expn 46 | -------------------------------------------------------------------------------- /docs/source/dynamic-switches.rst: -------------------------------------------------------------------------------- 1 | .. 2 | # (The MIT License) 3 | # 4 | # Copyright (c) 2013-2021 Kura 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a 7 | # copy of this software and associated documentation files (the 8 | # 'Software'), to deal in the Software without restriction, including 9 | # without limitation the rights to use, copy, modify, merge, publish, 10 | # distribute, sublicense, and/or sell copies of the Software, and to permit 11 | # persons to whom the Software is furnished to do so, subject to the 12 | # following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included 15 | # in all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS 18 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 20 | # NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 21 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 22 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 23 | # USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | .. _dynamic-switches: 26 | 27 | ================ 28 | Dynamic Switches 29 | ================ 30 | 31 | Dynamic switches allow you to tell blackhole how to respond to any given email. 32 | 33 | For example, with default configuration blackhole will not delay while 34 | responding to clients and will blindly accept all email. Using dynamic 35 | switches, you can tell blackhole to bounce an email instead of accepting it or 36 | delay for 10 seconds before processing the email -- without affecting any 37 | other emails you are sending to the service. 38 | 39 | Dynamic mode switches 40 | ===================== 41 | 42 | Blackhole has three response modes, you can find out more about what the modes 43 | are and the responses they send in the :ref:`mode` section. 44 | 45 | Adding a dynamic mode switch header to an email message will make the blackhole 46 | service respond to the email using that response mode. 47 | 48 | The ``X-Blackhole-Mode`` header is responsible for dynamic mode switching. 49 | 50 | .. code-block:: 51 | 52 | From: Test 53 | To: Test 54 | Subject: A test 55 | X-Blackhole-Mode: bounce 56 | 57 | This email will be bounced because of the X-Blackhole-Mode header. 58 | 59 | .. code-block:: 60 | 61 | From: Another Test 62 | To: Another Test 63 | Subject: A second test 64 | X-Blackhole-Mode: accept 65 | 66 | This email will be accepted because of the X-Blackhole-Mode header. 67 | 68 | Dynamic delay switches 69 | ====================== 70 | 71 | The ``X-Blackhole-Delay`` header is responsible for dynamic delay switching. 72 | 73 | Delay for a set amount of time 74 | ------------------------------ 75 | 76 | .. code-block:: 77 | 78 | From: Test 79 | To: Test 80 | Subject: A test 81 | X-Blackhole-Delay: 10 82 | 83 | Blackhole will delay for 10 seconds before responding. 84 | 85 | Delay using a range 86 | ------------------- 87 | 88 | .. code-block:: 89 | 90 | From: Test 91 | To: Test 92 | Subject: A test 93 | X-Blackhole-Delay: 10, 60 94 | 95 | Blackhole will delay for between 10 and 60 seconds before responding to 96 | this email. 97 | 98 | Combining dynamic switches 99 | ========================== 100 | 101 | Because dynamic switches are just email headers, they can be combined. 102 | 103 | .. code-block:: 104 | 105 | From: Test 106 | To: Test 107 | Subject: A test 108 | X-Blackhole-Mode: bounce 109 | X-Blackhole-Delay: 10 110 | 111 | Blackhole will delay for 10 seconds before bouncing this email. 112 | 113 | .. code-block:: 114 | 115 | From: Test 116 | To: Test 117 | Subject: A test 118 | X-Blackhole-Mode: accept 119 | X-Blackhole-Delay: 10, 30 120 | 121 | Blackhole will delay for between 10 and 30 seconds before accepting 122 | this email. 123 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. 2 | # (The MIT License) 3 | # 4 | # Copyright (c) 2013-2021 Kura 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a 7 | # copy of this software and associated documentation files (the 8 | # 'Software'), to deal in the Software without restriction, including 9 | # without limitation the rights to use, copy, modify, merge, publish, 10 | # distribute, sublicense, and/or sell copies of the Software, and to permit 11 | # persons to whom the Software is furnished to do so, subject to the 12 | # following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included 15 | # in all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS 18 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 20 | # NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 21 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 22 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 23 | # USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | ==================================== 26 | Blackhole |githubactions| |coverage| 27 | ==================================== 28 | 29 | Blackhole is an `MTA (message transfer agent) 30 | `_ that (figuratively) 31 | pipes all mail to /dev/null, built on top of `asyncio 32 | `_ and utilises `async def `_ and `await 33 | `_ statements 34 | available in `Python 3.5 `_. 35 | 36 | While Blackhole is an MTA, none of the actions performed via SMTP or SMTPS are 37 | actually processed, and no email is delivered. You can tell Blackhole how to 38 | handle mail that it receives. It can accept all of it, bounce it all, or 39 | randomly do either of those two actions. 40 | 41 | Think of Blackhole sort of like a honeypot in terms of how it handles mail, but 42 | it's specifically designed with testing in mind. 43 | 44 | 45 | User Guide 46 | ========== 47 | 48 | .. toctree:: 49 | :maxdepth: 3 50 | 51 | overview 52 | configuration 53 | communicating-with-blackhole 54 | dynamic-responses 55 | dynamic-switches 56 | api 57 | 58 | 59 | .. |githubactions| image:: https://img.shields.io/github/workflow/status/kura/blackhole/CI?style=for-the-badge&label=tests&logo=githubactions 60 | :target: https://github.com/kura/blackhole/actions/workflows/ci.yml 61 | :alt: Build status of the master branch 62 | 63 | .. |coverage| image:: https://img.shields.io/codecov/c/github/kura/blackhole/master.svg?style=for-the-badge&label=coverage&logo=codecov 64 | :target: https://codecov.io/github/kura/blackhole/ 65 | :alt: Test coverage 66 | -------------------------------------------------------------------------------- /docs/source/overview.rst: -------------------------------------------------------------------------------- 1 | .. 2 | # (The MIT License) 3 | # 4 | # Copyright (c) 2013-2021 Kura 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a 7 | # copy of this software and associated documentation files (the 8 | # 'Software'), to deal in the Software without restriction, including 9 | # without limitation the rights to use, copy, modify, merge, publish, 10 | # distribute, sublicense, and/or sell copies of the Software, and to permit 11 | # persons to whom the Software is furnished to do so, subject to the 12 | # following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included 15 | # in all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS 18 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 20 | # NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 21 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 22 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 23 | # USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | ======== 26 | Overview 27 | ======== 28 | 29 | .. _requirements: 30 | 31 | Requirements 32 | ============ 33 | 34 | Python support 35 | -------------- 36 | 37 | - Python 3.7+, PyPy 3.7+, or Pyston 2.2+ 38 | 39 | Extras 40 | ------ 41 | 42 | - `setproctitle` if you wish to have named processes 43 | - `uvloop` if you wish to use `libuv` 44 | 45 | .. _installation: 46 | 47 | Installation 48 | ============ 49 | 50 | PyPI 51 | ---- 52 | 53 | .. code-block:: bash 54 | 55 | pip install blackhole 56 | 57 | Installing with extas 58 | ##################### 59 | 60 | - For uvloop support 61 | 62 | .. code-block:: bash 63 | 64 | pip install blackhole[uvloop] 65 | 66 | - For setproctitle support 67 | 68 | .. code-block:: bash 69 | 70 | pip install blackhole[setproctitle] 71 | 72 | For both `uvloop` and `setproctitle` 73 | 74 | .. code-block:: bash 75 | 76 | pip install blackhole[uvloop,setproctitle] 77 | 78 | Source 79 | ------ 80 | 81 | Download the latest tarball from `PyPI 82 | `_ or `GitHub 83 | `__. Unpack and run: 84 | 85 | .. code-block:: bash 86 | 87 | python setup.py install 88 | 89 | 90 | .. _license: 91 | 92 | License 93 | ======= 94 | 95 | Blackhole is licensed under the `MIT license 96 | `_. 97 | 98 | (The MIT License) 99 | 100 | Copyright (c) 2013-2021 Kura 101 | 102 | Permission is hereby granted, free of charge, to any person obtaining a 103 | copy of this software and associated documentation files (the 104 | 'Software'), to deal in the Software without restriction, including 105 | without limitation the rights to use, copy, modify, merge, publish, 106 | distribute, sublicense, and/or sell copies of the Software, and to permit 107 | persons to whom the Software is furnished to do so, subject to the 108 | following conditions: 109 | 110 | The above copyright notice and this permission notice shall be included in 111 | all copies or substantial portions of the Software. 112 | 113 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 114 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 115 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 116 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 117 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 118 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 119 | DEALINGS IN THE SOFTWARE. 120 | 121 | 122 | .. _contributing: 123 | 124 | Contributing 125 | ============ 126 | 127 | If you're thinking about contributing, it's really quite simple. There is no 128 | need to feel daunted. You can contribute in a variety of ways: 129 | 130 | - `Report bugs `_, 131 | - `fix bugs `_, 132 | - `add new behaviour `_, 133 | - `improve the documentation 134 | `_, 135 | - `write tests `_, 136 | - improve performance, and 137 | - add additional support i.e. new versions of Python or PyPy. 138 | 139 | Please make sure that you read, understand and accept the `Code of Conduct 140 | `__. 141 | 142 | You can view a list of tasks that need work on the :ref:`todo` page. 143 | 144 | The :ref:`api` section also has a wealth of information on how the server works 145 | and how you can modify it or use parts of it. 146 | 147 | Getting started 148 | --------------- 149 | 150 | Getting started is very similar to installing blackhole yourself. You should 151 | familarise yourself with the documentation, 152 | `PEP8 `_ Python style guide and 153 | `PEP257 `_ docstring conventions. 154 | 155 | Writing some code 156 | ----------------- 157 | 158 | You'll need to fork the blackhole repository and checkout the source to your 159 | machine, much like any other project. 160 | 161 | You'll need to create a new branch for each piece of work you wish to do. 162 | 163 | .. code-block:: bash 164 | 165 | git checkout -b branch_name 166 | 167 | Once this is done, you need to run setup.py with the `develop` argument instead 168 | of `install`. 169 | 170 | .. code-block:: bash 171 | 172 | python setup.py develop 173 | 174 | Now you can hackaway. 175 | 176 | Things to do before submitting a pull request 177 | --------------------------------------------- 178 | 179 | - Make sure that you've written tests for what you have changed, or at least 180 | try to. 181 | - Add your name to the CONTRIBUTORS list, feel free to add a comment about what 182 | you did i.e. `Kura added STARTTLS support`. 183 | - Submit your pull request. 184 | 185 | .. _testing: 186 | 187 | Running tests 188 | ------------- 189 | 190 | You can find the latest tests against the source code on `GitHub `_. 191 | 192 | Running tests manually is pretty simple, there is a `Makefile` target dedicated 193 | to it. 194 | 195 | The test suite relies on `py.test `_ and is 196 | installed via the `Makefile` target and the `setup.py test` target. 197 | 198 | The test suite takes a while to run, there are a lot of parts that require 199 | communication and also use calls :any:`asyncio.sleep`, which cause the test 200 | suite to pause until the sleep is done. 201 | 202 | .. code-block:: bash 203 | 204 | make test 205 | 206 | To use the `setup.py test` target. 207 | 208 | .. code-block:: bash 209 | 210 | python setup.py test 211 | 212 | You can also test using `tox `_, 213 | when run through the `Makefile` `tox` will be installed automatically and the 214 | tests will be parallelised. 215 | 216 | .. code-block:: bash 217 | 218 | make tox 219 | 220 | Building the documentation 221 | -------------------------- 222 | 223 | The Makefile supplied also has a target for building the documentation. 224 | 225 | .. code-block:: bash 226 | 227 | make docs 228 | 229 | 230 | Upcoming/planned features 231 | ========================= 232 | 233 | .. toctree:: 234 | :maxdepth: 3 235 | 236 | todo 237 | 238 | 239 | Changelog 240 | ========= 241 | 242 | .. toctree:: 243 | :maxdepth: 3 244 | 245 | changelog 246 | -------------------------------------------------------------------------------- /docs/source/todo.rst: -------------------------------------------------------------------------------- 1 | ../../TODO.rst -------------------------------------------------------------------------------- /example.conf: -------------------------------------------------------------------------------- 1 | # 2 | # Listen for IPv4 and IPv6 connections. 3 | # 4 | # https://blackhole.io/configuration-options.html#listen 5 | # 6 | # Format:- HOST:PORT, HOST2:PORT2 7 | # Separate multiple listeners with commas. 8 | # 9 | # Default: 127.0.0.1:25, 127.0.0.1:587 10 | # 11 | # listen=:25 is equivalent to listening on all IPv4 addresses 12 | # listen=:::25 is equivalent to listen on all IPv6 addresses 13 | # 14 | # Optionally you can specify a mode and delay for each listener you define. 15 | # 16 | # listen=:25 mode=accept delay=10, :587 mode=bounce delay=5,10 17 | # 18 | # listen=0.0.0.0:25 19 | # listen=0.0.0.0:1025, fe80::a00:27ff:fe8c:9c6e:1025 20 | # 21 | listen=:25 22 | 23 | # 24 | # Listen IPv4 and IPv6 SSL/TLS connections. 25 | # 26 | # https://blackhole.io/configuration-options.html#tls_listen 27 | # 28 | # Format:- HOST:PORT, HOST2:PORT2 29 | # Separate multiple listeners with commas. 30 | # 31 | # tls_listen=:465 is equivalent to listening on all IPv4 addresses 32 | # tls_listen=:::465 is equivalent to listen on all IPv6 addresses 33 | # 34 | # Optionally you can specify a mode and delay for each listener you define. 35 | # 36 | # tls_listen=:465 mode=accept delay=10, :::465 mode=bounce delay=5,10 37 | # 38 | # 39 | # Port 465 -- while originally a recognised port for SMTP over SSL/TLS -- 40 | # is no longer advised for use. It's listed here because it's a well known 41 | # and well used port, but also because Blackhole currently does not support 42 | # STARTTLS over SMTP or SMTP Submission. -- 43 | # https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.txt 44 | # 45 | # tls_listen=0.0.0.0:465, fe80::a00:27ff:fe8c:9c6e:465 46 | 47 | # 48 | # TLS certificate location. 49 | # 50 | # https://blackhole.io/configuration-options.html#tls_cert 51 | # 52 | # Certificate should be x509 format. 53 | # 54 | # tls_cert=/etc/ssl/blackhole.crt 55 | 56 | # 57 | # TLS key file for x509 certificate. 58 | # 59 | # https://blackhole.io/configuration-options.html#tls_key 60 | # 61 | # tls_key=/etc/ssl/blackhole.key 62 | 63 | # 64 | # Diffie Hellman ephemeral parameters. 65 | # 66 | # https://blackhole.io/configuration-options.html#tls_dhparams 67 | # 68 | # openssl dhparam 4096 69 | # 70 | # tls_dhparams=/etc/ssl/blackhole.dhparams.pem 71 | 72 | # 73 | # User to run blackhole as. 74 | # 75 | # https://blackhole.io/configuration-options.html#user 76 | # 77 | # Defaults to current user. 78 | # 79 | # user=blackhole 80 | 81 | # 82 | # Group to run blackhole as. 83 | # 84 | # https://blackhole.io/configuration-options.html#group 85 | # 86 | # Defaults to current group. 87 | # 88 | # group=blackhole 89 | 90 | # 91 | # Where to store the PID. 92 | # 93 | # https://blackhole.io/configuration-options.html#pidfile 94 | # 95 | # Default: /tmp/blackhole.pid 96 | # 97 | # pidfile=/var/run/blackhole.pid 98 | pidfile=/tmp/blackhole.pid 99 | 100 | # 101 | # Timeout after no data has been received in seconds. 102 | # 103 | # https://blackhole.io/configuration-options.html#timeout 104 | # 105 | # Defaults to 60 seconds. Cannot be higher than 180 seconds for security 106 | # (denial of service). 107 | # 108 | # timeout=45 109 | # timeout=180 110 | timeout=0 111 | 112 | # 113 | # Delay for X seconds after the DATA command before sending the final 114 | # response. 115 | # 116 | # https://blackhole.io/configuration-options.html#delay 117 | # 118 | # Must be less than timeout. 119 | # Time is in seconds and cannot be set above 60 seconds for security 120 | # (denial of service). 121 | # Non-blocking - won't affect other connections. 122 | # 123 | # delay=10 124 | delay=0 125 | 126 | # 127 | # Response mode for the final response after the DATA command. 128 | # 129 | # https://blackhole.io/configuration-options.html#mode 130 | # 131 | # accept (default) - all emails are accepted with 250 code. 132 | # bounce - bounce all emails with a random code. 133 | # random - randomly accept or bounce. 134 | # 135 | # Bounce codes: 136 | # 450: Requested mail action not taken: mailbox unavailable 137 | # 451: Requested action aborted: local error in processing 138 | # 452: Requested action not taken: insufficient system storage 139 | # 458: Unable to queue message 140 | # 521: Machine does not accept mail 141 | # 550: Requested action not taken: mailbox unavailable 142 | # 551: User not local 143 | # 552: Requested mail action aborted: exceeded storage allocation 144 | # 553: Requested action not taken: mailbox name not allowed 145 | # 571: Blocked 146 | # 147 | mode=accept 148 | 149 | # 150 | # Maximum message size in bytes. 151 | # 152 | # https://blackhole.io/configuration-options.html#max_message_size 153 | # 154 | # Default 512000 bytes (512 KB). 155 | # 156 | max_message_size=512000 157 | 158 | # 159 | # Dynamic switches. 160 | # 161 | # https://blackhole.io/configuration-options.html#dynamic_switch 162 | # 163 | # Allows switching how blackhole responds to an email and delays responding 164 | # based on a header. 165 | # 166 | # https://blackhole.io/dynamic-switches.html#dynamic-switches 167 | # 168 | # Default: true 169 | # 170 | dynamic_switch=true 171 | 172 | # 173 | # workers -- added in 2.1.0 174 | # 175 | # Allows you to define how many worker processes to spawn to handle 176 | # incoming mail. The absolute minimum is actually 2. Even by setting the 177 | # workers value to 1, a supervisor process will always exist meaning 178 | # that you would have 1 worker and a supervisor. 179 | # 180 | # Default: 1 181 | # 182 | workers=1 183 | -------------------------------------------------------------------------------- /init.d/debian-ubuntu/blackhole: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | # (The MIT License) 4 | # 5 | # Copyright (c) 2013-2021 Kura 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the 'Software'), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | ### BEGIN INIT INFO 26 | # Provides: Blackhole 27 | # Required-Start: $local_fs $remote_fs $network $syslog 28 | # Required-Stop: $local_fs $remote_fs $network $syslog 29 | # Default-Start: 2 3 4 5 30 | # Default-Stop: 0 1 6 31 | # Short-Description: Starts the Blackhole MTA 32 | # Description: Starts Blackhole using start-stop-daemon 33 | ### END INIT INFO 34 | 35 | 36 | PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin 37 | CONF=/etc/blackhole.conf 38 | NAME=Blackhole 39 | DESC="Blackhole MTA" 40 | DAEMON_OPTS="" 41 | DAEMON=/usr/local/bin/blackhole 42 | pidfile=/var/run/blackhole.pid 43 | SCRIPTNAME=/etc/init.d/$NAME 44 | 45 | # Exit if the package is not installed 46 | [ -x "$DAEMON" ] || exit 1 47 | 48 | if test -f $CONF 49 | then 50 | # shellcheck source=/dev/null 51 | . $CONF 52 | else 53 | echo "Failed to open conf: $CONF" 54 | exit 1 55 | fi 56 | 57 | SDAEMON_OPTS="$DAEMON_OPTS -c $CONF -b" 58 | 59 | . /lib/lsb/init-functions 60 | 61 | start() { 62 | start-stop-daemon --start --quiet --pidfile "$pidfile" --exec "$DAEMON" -- "$SDAEMON_OPTS" 63 | } 64 | 65 | stop() { 66 | start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile "$pidfile" --exec "$DAEMON" 67 | } 68 | 69 | configtest() { 70 | $DAEMON -c $CONF -t 71 | } 72 | 73 | case "$1" in 74 | start) 75 | log_daemon_msg "Starting" "$DESC" 76 | start 77 | case "$?" in 78 | 0) log_end_msg 0 ;; 79 | 1) log_progress_msg "already started" 80 | log_end_msg 0 ;; 81 | *) log_end_msg 1 ;; 82 | esac 83 | ;; 84 | stop) 85 | log_daemon_msg "Stopping" "$DESC" 86 | stop 87 | case "$?" in 88 | 0) log_end_msg 0 ;; 89 | 1) log_progress_msg "already stopped" 90 | log_end_msg 0 ;; 91 | *) log_end_msg 1 ;; 92 | esac 93 | ;; 94 | restart) 95 | $0 stop 96 | $0 start 97 | ;; 98 | status) 99 | status_of_proc -p "$pidfile" "$DAEMON" "$NAME" && exit 0 || exit $? 100 | ;; 101 | configtest) 102 | configtest 103 | ;; 104 | *) 105 | log_action_msg "Usage: $SCRIPTNAME {start|stop|restart|status|configtest}" 106 | exit 1 107 | ;; 108 | esac 109 | -------------------------------------------------------------------------------- /man/build/blackhole.1: -------------------------------------------------------------------------------- 1 | .\" Man page generated from reStructuredText. 2 | . 3 | . 4 | .nr rst2man-indent-level 0 5 | . 6 | .de1 rstReportMargin 7 | \\$1 \\n[an-margin] 8 | level \\n[rst2man-indent-level] 9 | level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] 10 | - 11 | \\n[rst2man-indent0] 12 | \\n[rst2man-indent1] 13 | \\n[rst2man-indent2] 14 | .. 15 | .de1 INDENT 16 | .\" .rstReportMargin pre: 17 | . RS \\$1 18 | . nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] 19 | . nr rst2man-indent-level +1 20 | .\" .rstReportMargin post: 21 | .. 22 | .de UNINDENT 23 | . RE 24 | .\" indent \\n[an-margin] 25 | .\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] 26 | .nr rst2man-indent-level -1 27 | .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] 28 | .in \\n[rst2man-indent\\n[rst2man-indent-level]]u 29 | .. 30 | .TH "BLACKHOLE" 1 "" "" "" 31 | .SH NAME 32 | Blackhole \- An MTA that (figuratively) pipes all mail to /dev/null. 33 | .SH SYNOPSIS 34 | .sp 35 | blackhole [OPTIONS] 36 | .SH DESCRIPTION 37 | .sp 38 | Blackhole is an \fI\%MTA (message transfer agent)\fP that (figuratively) 39 | pipes all mail to /dev/null, built on top of \fI\%asyncio\fP and utilises \fI\%async def\fP 40 | and \fI\%await\fP 41 | statements available in \fI\%Python 3.5\fP\&. 42 | .sp 43 | While Blackhole is an MTA, none of the actions performed via SMTP or SMTPS are 44 | actually processed and no email is delivered. 45 | .sp 46 | You can tell Blackhole how to handle mail that it receives. It can accept all 47 | of it, bounce it all or randomly do either of those two actions. 48 | .sp 49 | Think of Blackhole sort of like a honeypot in terms of how it handles mail, 50 | but it\(aqs specifically designed with testing in mind. 51 | .SH OPTIONS 52 | .INDENT 0.0 53 | .TP 54 | .B \-h 55 | show this help message and exit 56 | .TP 57 | .B \-v 58 | show program\(aqs version number and exit 59 | .TP 60 | .BI \-c \ FILE 61 | override the default configuration options 62 | .TP 63 | .B \-t 64 | perform a configuration test and exit 65 | .TP 66 | .B \-d 67 | enable debugging mode 68 | .TP 69 | .B \-b 70 | run in the background 71 | .TP 72 | .BI \-l\fB s 73 | Disable \fIssl.OP_SINGLE_DH_USE\fP and \fIssl.OP_SINGLE_ECDH_USE\fP\&. 74 | Reduces CPU overhead at the expense of security. Don\(aqt use this 75 | option unless you really need to. 76 | .TP 77 | .B \-q 78 | Suppress warnings when using \-ls/\-\-less\-secure, running as root or 79 | not using \fItls_dhparams\fP option. 80 | .UNINDENT 81 | .SH SEE ALSO 82 | .INDENT 0.0 83 | .IP \(bu 2 84 | \fBblackhole_config\fP (1) 85 | .IP \(bu 2 86 | \fI\%https://kura.gg/blackhole/configuration.html\fP 87 | .UNINDENT 88 | .SH LICENSE 89 | .sp 90 | The MIT license must be distributed with this software. 91 | .SH AUTHOR(S) 92 | .sp 93 | Kura <\fI\%kura@kura.gg\fP> 94 | .\" Generated by docutils manpage writer. 95 | . 96 | -------------------------------------------------------------------------------- /man/source/blackhole.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Blackhole 3 | ========= 4 | 5 | ------------------------------------------------------- 6 | An MTA that (figuratively) pipes all mail to /dev/null. 7 | ------------------------------------------------------- 8 | 9 | :Manual section: 1 10 | 11 | SYNOPSIS 12 | ======== 13 | 14 | blackhole [OPTIONS] 15 | 16 | DESCRIPTION 17 | =========== 18 | 19 | Blackhole is an `MTA (message transfer agent) 20 | `_ that (figuratively) 21 | pipes all mail to /dev/null, built on top of `asyncio 22 | `_ and utilises `async def `_ 23 | and `await `_ 24 | statements available in `Python 3.5 25 | `_. 26 | 27 | While Blackhole is an MTA, none of the actions performed via SMTP or SMTPS are 28 | actually processed and no email is delivered. 29 | 30 | You can tell Blackhole how to handle mail that it receives. It can accept all 31 | of it, bounce it all or randomly do either of those two actions. 32 | 33 | Think of Blackhole sort of like a honeypot in terms of how it handles mail, 34 | but it's specifically designed with testing in mind. 35 | 36 | OPTIONS 37 | ======= 38 | 39 | -h show this help message and exit 40 | -v show program's version number and exit 41 | -c FILE override the default configuration options 42 | -t perform a configuration test and exit 43 | -d enable debugging mode 44 | -b run in the background 45 | -ls Disable `ssl.OP_SINGLE_DH_USE` and `ssl.OP_SINGLE_ECDH_USE`. 46 | Reduces CPU overhead at the expense of security. Don't use this 47 | option unless you really need to. 48 | -q Suppress warnings when using -ls/--less-secure, running as root or 49 | not using `tls_dhparams` option. 50 | 51 | SEE ALSO 52 | ======== 53 | 54 | - **blackhole_config** (1) 55 | - ``_ 56 | 57 | LICENSE 58 | ======= 59 | 60 | The MIT license must be distributed with this software. 61 | 62 | AUTHOR(S) 63 | ========= 64 | 65 | Kura 66 | -------------------------------------------------------------------------------- /man/source/blackhole_config.rst: -------------------------------------------------------------------------------- 1 | ================ 2 | blackhole_config 3 | ================ 4 | 5 | -------------------------------------------- 6 | the config file format for the Blackhole MTA 7 | -------------------------------------------- 8 | 9 | :Manual section: 1 10 | 11 | DESCRIPTION 12 | =========== 13 | 14 | This manual page documents the ``Blackhole`` configuration file format and 15 | options. 16 | 17 | OPTIONS 18 | ======= 19 | 20 | These are all available options for the configuration file, their default 21 | values and information on what the options actually do. 22 | 23 | The file format is a simple `attribute = value` style, an example is shown 24 | below. 25 | 26 | :: 27 | 28 | # This is a comment. 29 | listen = :25 # This is an inline comment. 30 | user = kura 31 | group = kura 32 | 33 | listen 34 | ------ 35 | 36 | :Syntax: 37 | **listen** = *[address]:port [mode=MODE] [delay=DELAY]* 38 | :Default: 39 | 127.0.0.1:25, 127.0.0.1:587, :::25, :::587 -- 25 is the recognised SMTP 40 | port, 587 is the recognised SMTP Submission port. IPv6 listeners are only 41 | enabled if IPv6 is supported. 42 | :Optional: 43 | *mode=* and *delay=* -- allows setting a response mode and delay per 44 | listener. 45 | 46 | `:25` is equivalent to listening on port 25 on all IPv4 addresses and `:::25` 47 | is equivalent to listening on port 25 on all IPv6 addresses. 48 | 49 | Multiple addresses and ports can be listed on a single line. 50 | 51 | :: 52 | 53 | listen = 10.0.0.1:25, 10.0.0.2:25, :25, :::25, :587, :::587 54 | 55 | The ``mode=`` and ``delay=`` flags allow specific ports to act in specific 56 | ways. i.e. you could accept all mail on 10.0.0.1:25 and bounce it all on 57 | 10.0.0.2:25, as below. 58 | 59 | :: 60 | 61 | listen = 10.0.0.1:25 mode=accept, 10.0.0.2:25 mode=bounce 62 | 63 | The ``mode=`` and ``delay=`` flags may also be specified together, as required. 64 | 65 | :: 66 | 67 | listen = 10.0.0.1:25 mode=accept delay=5, 10.0.0.2:25 mode=bounce delay=10 68 | 69 | The flags accept the same options as `dynamic-switches`, including setting 70 | a delay range. 71 | 72 | ----- 73 | 74 | tls_listen 75 | ---------- 76 | 77 | :Syntax: 78 | **tls_listen** = *[address]:port [mode=MODE] [delay=DELAY]* 79 | :Default: 80 | None -- 465 is the recognised SMTPS port [1]_. 81 | :Optional: 82 | *mode=* and *delay=* -- allows setting a response mode and delay per 83 | listener. 84 | :Added: 85 | 86 | `:465` is equivalent to listening on port 465 on all IPv4 addresses and 87 | `:::465` is equivalent to listening on port 465 on all IPv6 addresses. 88 | 89 | Multiple addresses and ports can be listed on a single line. 90 | 91 | :: 92 | 93 | tls_listen = 10.0.0.1:465, 10.0.0.2:465, :465, :::465 94 | 95 | The ``mode=`` and ``delay=`` flags allow specific ports to act in specific 96 | ways. i.e. you could accept all mail on 10.0.0.1:465 and bounce it all on 97 | 10.0.0.2:465, as below. 98 | 99 | :: 100 | 101 | tls_listen = 10.0.0.1:465 mode=accept, 10.0.0.2:465 mode=bounce 102 | 103 | The ``mode=`` and ``delay=`` flags may also be specified together, as required. 104 | 105 | :: 106 | 107 | tls_listen = 10.0.0.1:465 mode=accept delay=5, 10.0.0.2:465 mode=bounce delay=10 108 | 109 | The flags accept the same options as `dynamic-switches`, including setting 110 | a delay range. 111 | 112 | .. [1] Port 465 -- while originally a recognised port for SMTP over 113 | SSL/TLS -- is no longer advised for use. It's listed here because it's a 114 | well known and well used port, but also because Blackhole currently does not 115 | support ``STARTTLS`` over SMTP or SMTP Submission. -- 116 | ``_ 117 | 118 | ----- 119 | 120 | user 121 | ---- 122 | 123 | :Syntax: 124 | **user** = *user* 125 | :Default: 126 | The current Linux user 127 | 128 | Blackhole will set it's process owner to the value provided with this options. 129 | Ports below 1024 require sudo or root privileges, this option is available so 130 | that the process can be started, listen on privileged ports and then give up 131 | those privileges. 132 | 133 | ----- 134 | 135 | group 136 | ----- 137 | 138 | :Syntax: 139 | **group** = *group* 140 | :Default: 141 | The primary group of the current Linux user 142 | 143 | Blackhole will set it's process group to the value provided with this options. 144 | 145 | ----- 146 | 147 | pidfile 148 | ------- 149 | 150 | :Syntax: 151 | **pidfile** = */path/to/file.pid* 152 | :Default: 153 | /tmp/blackhole.pid 154 | 155 | Blackhole will write it's Process ID to this file, allowing you to easily track 156 | the process and send signals to it. 157 | 158 | ----- 159 | 160 | timeout 161 | ------- 162 | 163 | :Syntax: 164 | **timeout** = *seconds* 165 | :Default: 166 | 60 -- Maximum value of 180 seconds. 167 | 168 | This is the amount of time to wait for a client to send data. Once the timeout 169 | value has been reached with no data being sent by the client, the connection 170 | will be terminated and a ``421 Timeout`` message will be sent to the client. 171 | 172 | Helps mitigate DoS risks. 173 | 174 | ----- 175 | 176 | tls_cert 177 | -------- 178 | 179 | :Syntax: 180 | **tls_cert** = */path/to/certificate.pem* 181 | :Default: 182 | None 183 | 184 | The certificate file in x509 format for wrapping a connection in SSL/TLS. 185 | 186 | ----- 187 | 188 | tls_key 189 | ------- 190 | 191 | :Syntax: 192 | **tls_key** = */path/to/private.key* 193 | :Default: 194 | None 195 | 196 | ----- 197 | 198 | tls_dhparams 199 | ------------ 200 | 201 | :Syntax: 202 | **tls_dhparams** = */path/to/dhparams.pem* 203 | :Default: 204 | None 205 | 206 | 207 | File containing Diffie Hellman ephemeral parameters for ECDH ciphers. 208 | 209 | ----- 210 | 211 | delay 212 | ----- 213 | 214 | :Syntax: 215 | **delay** = *seconds* 216 | :Default: 217 | None -- Maximum value of 60 seconds. 218 | 219 | Time to delay before returning a response to a completed DATA command. You can 220 | use this to delay testing or simulate lag. 221 | 222 | ----- 223 | 224 | mode 225 | ---- 226 | 227 | :Syntax: 228 | **mode** = *accept | bounce | random* 229 | :Default: 230 | accept 231 | 232 | ----- 233 | 234 | max_message_size 235 | ---------------- 236 | 237 | :Syntax: 238 | **max_message_size** = *bytes* 239 | :Default: 240 | 512000 Bytes (512 KB) 241 | 242 | The maximum message size for a message. This includes headers and helps 243 | mitigate a DoS risk. 244 | 245 | ----- 246 | 247 | dynamic_switch 248 | -------------- 249 | 250 | :Syntax: 251 | **dynamic_switch** = *true | false* 252 | :Default: 253 | true 254 | 255 | The dynamic switch option allows you to enable or disable parsing of dynamic 256 | switches from email headers. 257 | 258 | ----- 259 | 260 | workers 261 | ------- 262 | 263 | :Syntax: 264 | **workers** = *number* 265 | :Default: 266 | 1 267 | 268 | The workers option allows you to define how many worker processes to spawn to 269 | handle incoming mail. The absolute minimum is actually 2. Even by setting the 270 | ``workers`` value to 1, a supervisor process will always exist meaning that you 271 | would have 1 worker and a supervisor. 272 | 273 | SEE ALSO 274 | ======== 275 | 276 | - **blackhole** (1) 277 | - ``_ 278 | 279 | LICENSE 280 | ======= 281 | 282 | The MIT license must be distributed with this software. 283 | 284 | AUTHOR(S) 285 | ========= 286 | 287 | Kura 288 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=41.0", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.black] 6 | line-length = 79 7 | target-version = ['py37', 'py38', 'py39'] 8 | 9 | [tool.poetry] 10 | name = "blackhole" 11 | version = "2.1.19" 12 | description = "Blackhole is an MTA (message transfer agent) that (figuratively) pipes all mail to /dev/null." 13 | authors = ["Kura "] 14 | license ="MIT" 15 | readme = "README.rst" 16 | homepage = "https://kura.gg/blackhole/" 17 | repository = "https://github.com/kura/blackhole" 18 | documentation = "https://kura.gg/blackhole/" 19 | keywords = [ 20 | "blackhole", 21 | "asyncio", 22 | "smtp", 23 | "mta", 24 | "email" 25 | ] 26 | classifiers = [ 27 | "Development Status :: 5 - Production/Stable", 28 | "Environment :: Console", 29 | "Intended Audience :: Developers", 30 | "Intended Audience :: Information Technology", 31 | "Intended Audience :: System Administrators", 32 | "License :: OSI Approved :: MIT License", 33 | "Operating System :: POSIX :: Linux", 34 | "Operating System :: Unix", 35 | "Programming Language :: Python", 36 | "Programming Language :: Python :: 3.7", 37 | "Programming Language :: Python :: 3.8", 38 | "Programming Language :: Python :: 3.9", 39 | "Programming Language :: Python :: 3 :: Only", 40 | "Programming Language :: Python :: Implementation :: CPython", 41 | "Programming Language :: Python :: Implementation :: PyPy", 42 | "Topic :: Communications :: Email", 43 | "Topic :: Communications :: Email :: Mail Transport Agents", 44 | "Topic :: Education :: Testing", 45 | "Topic :: Internet", 46 | "Topic :: Software Development", 47 | "Topic :: Software Development :: Testing", 48 | "Topic :: Software Development :: Testing :: Traffic Generation", 49 | "Topic :: System :: Networking", 50 | "Topic :: System :: Systems Administration", 51 | "Topic :: Utilities", 52 | ] 53 | include = [ 54 | "bash-completion/blackhole-completion.bash", 55 | "CHANGELOG.rst", 56 | "CODE_OF_CONDUCT.rst", 57 | "CONTRIBUTING.rst", 58 | "CONTRIBUTORS.rst", 59 | "example.conf", 60 | "LICENSE", 61 | "README.rst", 62 | "THANKS.rst", 63 | "TODO.rst" 64 | ] 65 | 66 | [tool.poetry.scripts] 67 | blackhole = "blackhole.application:run" 68 | blackhole_config = "blackhole.application:blackhole_config" 69 | 70 | [tool.poetry.dependencies] 71 | python = ">3.7, <4" 72 | uvloop = {version = "^0.14.0", optional = true} 73 | setproctitle = {version = "^1.1.10", optional = true} 74 | coverage = {version = "^5.1", optional = true} 75 | pytest = {version = "^5.4.2", optional = true} 76 | pytest-asyncio = {version = "^0.12.0", optional = true} 77 | pytest-cov = {version = "^2.8.1", optional = true} 78 | pytest-clarity = {version = "^0.3.0-alpha.0", optional = true} 79 | sphinx = {version = "^3.0.3", optional = true} 80 | guzzle_sphinx_theme = {version = "^0.7.11", optional = true} 81 | tox = {version = "^3.15.0", optional = true} 82 | pre-commit = {version = "^2.4.0", optional = true} 83 | interrogate = {version = "^1.3.0", optional = true} 84 | pyroma = {version = "^2.6", optional = true} 85 | bandit = {version = "^1.6.2", optional = true} 86 | black = {version = "^20.8b1", optional = true} 87 | isort = {version = ">=4.2.5,<5", optional = true} 88 | flake8 = {version = "^3.8.3", optional = true} 89 | flake8-bugbear = {version = "^20.1.4", optional = true} 90 | flake8-isort = {version = "^4.0.0", optional = true} 91 | flake8-commas = {version = "^2.0.0", optional = true} 92 | pydocstyle = {version = "^5.1.0", optional = true} 93 | doc8 = {version = "^0.8.1", optional = true} 94 | codespell = {version = "^2.1.0", optional = true} 95 | vulture = {version = "^2.3", optional = true} 96 | 97 | [tool.poetry.extras] 98 | uvloop = ["uvloop"] 99 | setproctitle = ["setproctitle"] 100 | docs = [ 101 | "sphinx", 102 | "guzzle_sphinx_theme" 103 | ] 104 | tests = [ 105 | "black", 106 | "coverage", 107 | "pytest", 108 | "pytest-asyncio", 109 | "pytest-clarity", 110 | "pytest-cov", 111 | "flake8", 112 | "flake8-bugbear", 113 | "flake8-isort", 114 | "flake8-commas", 115 | "interrogate", 116 | "pyroma", 117 | "bandit", 118 | "pydocstyle", 119 | "doc8", 120 | "codespell", 121 | "vulture" 122 | ] 123 | dev = [ 124 | "uvloop", 125 | "setproctitle", 126 | "sphinx", 127 | "guzzle_sphinx_theme", 128 | "black", 129 | "coverage", 130 | "pytest", 131 | "pytest-asyncio", 132 | "pytest-clarity", 133 | "pytest-cov", 134 | "flake8", 135 | "flake8-bugbear", 136 | "flake8-isort", 137 | "flake8-commas", 138 | "pydocstyle", 139 | "doc8", 140 | "interrogate", 141 | "pyroma", 142 | "bandit", 143 | "tox", 144 | "pre-commit", 145 | "codespell", 146 | "vulture" 147 | ] 148 | 149 | [tool.pytest] 150 | minversion = 3.10 151 | strict = true 152 | addopts = "-ra" 153 | testpaths = "tests" 154 | filterwarnings = "once::Warning" 155 | 156 | [tool.isort] 157 | include_trailing_comma = true 158 | lines_after_imports = 2 159 | multi_line_output = 3 160 | skip_glob = "*/tests/*.py" 161 | known_first_party = "blackhole" 162 | known_third_party = [ 163 | "guzzle_sphinx_theme", 164 | "pytest", 165 | "setproctitle", 166 | "uvloop" 167 | ] 168 | sections = "STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER" 169 | 170 | [tool.interrogate] 171 | ignore-init-method = true 172 | ignore-init-module = false 173 | ignore-magic = false 174 | ignore-semiprivate = false 175 | ignore-private = false 176 | ignore-property-decorators = true 177 | ignore-module = false 178 | fail-under = 95 179 | exclude = ["setup.py", "docs", "build"] 180 | ignore-regex = ["^get$", "^mock_.*", ".*BaseClass.*"] 181 | verbose = 0 182 | quiet = false 183 | whitelist-regex = [] 184 | color = true 185 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | markers = 3 | slow: marks tests as slow (deselect with '-m "not slow"') 4 | -------------------------------------------------------------------------------- /requirements-extra.txt: -------------------------------------------------------------------------------- 1 | setproctitle==1.2.2; python_version >= "3.6" \ 2 | --hash=sha256:9106bcbacae534b6f82955b176723f1b2ca6514518aab44dffec05a583f8dca8 \ 3 | --hash=sha256:30bc7a769a4451639a0adcbc97bdf7a6e9ac0ef3ddad8d63eb1e338edb3ebeda \ 4 | --hash=sha256:e8ef655eab26e83ec105ce79036bb87e5f2bf8ba2d6f48afdd9595ef7647fcf4 \ 5 | --hash=sha256:0df728d0d350e6b1ad8436cc7add052faebca6f4d03257182d427d86d4422065 \ 6 | --hash=sha256:5260e8700c5793d48e79c5e607e8e552e795b698491a4b9bb9111eb74366a450 \ 7 | --hash=sha256:ba1fb32e7267330bd9f72e69e076777a877f1cb9be5beac5e62d1279e305f37f \ 8 | --hash=sha256:e696c93d93c23f377ccd2d72e38908d3dbfc90e45561602b805f53f2627d42ea \ 9 | --hash=sha256:fbf914179dc4540ee6bfd8228b4cc1f1f6fb12dad66b72b5c9b955b222403220 \ 10 | --hash=sha256:28b884e1cb9a53974e15838864283f9bad774b5c7db98c9609416bd123cb9fd1 \ 11 | --hash=sha256:a11d329f33221443317e2aeaee9442f22fcae25be3aa4fb8489e4f7b1f65cdd2 \ 12 | --hash=sha256:e13a5c1d9c369cb11cdfc4b75be432b83eb3205c95a69006008ffd4366f87b9e \ 13 | --hash=sha256:c611f65bc9de5391a1514de556f71101e6531bb0715d240efd3e9732626d5c9e \ 14 | --hash=sha256:bc4393576ed3ac87ddac7d1bd0faaa2fab24840a025cc5f3c21d14cf0c9c8a12 \ 15 | --hash=sha256:17598f38be9ef499d74f2380bf76b558be72e87da75d66b153350e586649171b \ 16 | --hash=sha256:0d160d46c8f3567e0aa27b26b1f36e03122e3de475aacacc14a92b8fe45b648a \ 17 | --hash=sha256:077943272d0490b3f43d17379432d5e49c263f608fdf4cf624b419db762ca72b \ 18 | --hash=sha256:970798d948f0c90a3eb0f8750f08cb215b89dcbee1b55ffb353ad62d9361daeb \ 19 | --hash=sha256:3f6136966c81daaf5b4b010613fe33240a045a4036132ef040b623e35772d998 \ 20 | --hash=sha256:249526a06f16d493a2cb632abc1b1fdfaaa05776339a50dd9f27c941f6ff1383 \ 21 | --hash=sha256:4fc5bebd34f451dc87d2772ae6093adea1ea1dc29afc24641b250140decd23bb \ 22 | --hash=sha256:7dfb472c8852403d34007e01d6e3c68c57eb66433fb8a5c77b13b89a160d97df 23 | uvloop==0.14.0 \ 24 | --hash=sha256:08b109f0213af392150e2fe6f81d33261bb5ce968a288eb698aad4f46eb711bd \ 25 | --hash=sha256:4544dcf77d74f3a84f03dd6278174575c44c67d7165d4c42c71db3fdc3860726 \ 26 | --hash=sha256:b4f591aa4b3fa7f32fb51e2ee9fea1b495eb75b0b3c8d0ca52514ad675ae63f7 \ 27 | --hash=sha256:f07909cd9fc08c52d294b1570bba92186181ca01fe3dc9ffba68955273dd7362 \ 28 | --hash=sha256:afd5513c0ae414ec71d24f6f123614a80f3d27ca655a4fcf6cabe50994cc1891 \ 29 | --hash=sha256:e7514d7a48c063226b7d06617cbb12a14278d4323a065a8d46a7962686ce2e95 \ 30 | --hash=sha256:bcac356d62edd330080aed082e78d4b580ff260a677508718f88016333e2c9c5 \ 31 | --hash=sha256:4315d2ec3ca393dd5bc0b0089d23101276778c304d42faff5dc4579cb6caef09 \ 32 | --hash=sha256:123ac9c0c7dd71464f58f1b4ee0bbd81285d96cdda8bc3519281b8973e3a461e 33 | -------------------------------------------------------------------------------- /scripts/minify.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | pip install htmlmin cssmin jsmin 3 | 4 | find .tox/docs/tmp/html -type f -iname "*.html" | 5 | while read -r f 6 | do 7 | mv "$f" "$f.bak" 8 | htmlmin -cs "$f.bak" "$f" 9 | rm "$f.bak" 10 | done 11 | 12 | find .tox/docs/tmp/html -type f -iname "*.css" | 13 | while read -r f 14 | do 15 | mv "$f" "$f.bak" 16 | cssmin < "$f.bak" > "$f" 17 | rm "$f.bak" 18 | done 19 | 20 | find .tox/docs/tmp/html -type f -iname "*.js" | 21 | while read -r f 22 | do 23 | mv "$f" "$f.bak" 24 | python -m jsmin "$f.bak" > "$f" 25 | rm "$f.bak" 26 | done 27 | -------------------------------------------------------------------------------- /scripts/update-libuv.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | git clone https://github.com/libuv/libuv.git 3 | cd libuv || return 4 | git checkout "$(git describe --tags)" 5 | ./autogen.sh 6 | ./configure 7 | make 8 | sudo make install 9 | cd .. || return 10 | rm -rf libuv* 11 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # (The MIT License) 4 | # 5 | # Copyright (c) 2013-2021 Kura 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the 'Software'), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | """Setup file.""" 26 | 27 | import io 28 | import os 29 | import sys 30 | 31 | from setuptools import find_packages, setup 32 | from setuptools.command.test import test as TestCommand 33 | 34 | 35 | assert sys.version_info >= ( 36 | 3, 37 | 7, 38 | 0, 39 | ), "blackhole requires Python 3.7+, PyPy 3.7+, or Pyston 2.2+" 40 | 41 | 42 | class PyTest(TestCommand): 43 | """Test command.""" 44 | 45 | def finalize_options(self): 46 | """Build options.""" 47 | TestCommand.finalize_options(self) 48 | self.test_args = ["--pylama", "-q", "./blackhole", "./tests"] 49 | self.test_suite = True 50 | 51 | def run_tests(self): 52 | """Run ze tests.""" 53 | import pytest 54 | 55 | sys.exit(pytest.main(self.test_args)) 56 | 57 | 58 | def include_file(filename): 59 | """Include contents of specified file.""" 60 | fpath = os.path.join(os.path.dirname(__file__), filename) 61 | with io.open(fpath, encoding="utf-8") as f: 62 | c = f.read() 63 | return c 64 | 65 | 66 | def get_version(filepath): 67 | """Return program version.""" 68 | for line in include_file(filepath).split("\n"): 69 | if line.startswith("__version__"): 70 | _, vers = line.split("=") 71 | return vers.strip().replace('"', "").replace("'", "") 72 | 73 | 74 | __version__ = get_version("blackhole/__init__.py") 75 | 76 | entry_points = { 77 | "console_scripts": ( 78 | "blackhole = blackhole.application:run", 79 | "blackhole_config = blackhole.application:blackhole_config", 80 | ), 81 | } 82 | 83 | extras_require = { 84 | "setproctitle": ["setproctitle"], 85 | "uvloop": ["uvloop"], 86 | "docs": ["sphinx", "guzzle_sphinx_theme"], 87 | "tests": [ 88 | "bandit", 89 | "coverage", 90 | "flake8", 91 | "flake8-bugbear", 92 | "flake8-isort", 93 | "flake8-commas", 94 | "interrogate", 95 | "pydocstyle", 96 | "pyroma", 97 | "pytest", 98 | "pytest-asyncio", 99 | "pytest-cov", 100 | "pytest-clarity", 101 | "doc8", 102 | "codespell", 103 | "vulture", 104 | ], 105 | } 106 | 107 | extras_require["dev"] = ( 108 | extras_require["docs"] + extras_require["tests"] + ["tox", "pre-commit"] 109 | ) 110 | 111 | description = ( 112 | "Blackhole is an MTA (message transfer agent) that " 113 | "(figuratively) pipes all mail to /dev/null." 114 | ) 115 | 116 | keywords = ("blackhole", "asyncio", "smtp", "mta", "email") 117 | 118 | classifiers = [ 119 | "Development Status :: 5 - Production/Stable", 120 | "Environment :: Console", 121 | "Intended Audience :: Developers", 122 | "Intended Audience :: Information Technology", 123 | "Intended Audience :: System Administrators", 124 | "License :: OSI Approved :: MIT License", 125 | "Operating System :: POSIX :: Linux", 126 | "Operating System :: Unix", 127 | "Programming Language :: Python", 128 | "Programming Language :: Python :: 3.7", 129 | "Programming Language :: Python :: 3.8", 130 | "Programming Language :: Python :: 3.9", 131 | "Programming Language :: Python :: 3 :: Only", 132 | "Programming Language :: Python :: Implementation :: CPython", 133 | "Programming Language :: Python :: Implementation :: PyPy", 134 | "Topic :: Communications :: Email", 135 | "Topic :: Communications :: Email :: Mail Transport Agents", 136 | "Topic :: Education :: Testing", 137 | "Topic :: Internet", 138 | "Topic :: Software Development", 139 | "Topic :: Software Development :: Testing", 140 | "Topic :: Software Development :: Testing :: Traffic Generation", 141 | "Topic :: System :: Networking", 142 | "Topic :: System :: Systems Administration", 143 | "Topic :: Utilities", 144 | ] 145 | 146 | setup( 147 | author="Kura", 148 | author_email="kura@kura.gg", 149 | classifiers=classifiers, 150 | cmdclass={"test": PyTest}, 151 | description=description, 152 | entry_points=entry_points, 153 | extras_require=extras_require, 154 | install_requires=[], 155 | keywords=" ".join(keywords), 156 | license="MIT", 157 | long_description="\n" + include_file("README.rst"), 158 | maintainer="Kura", 159 | maintainer_email="kura@kura.gg", 160 | name="blackhole", 161 | packages=find_packages(exclude=("tests",)), 162 | platforms=["linux"], 163 | url="https://kura.gg/blackhole/", 164 | version=__version__, 165 | zip_safe=False, # this is probably not correct, but I've never tested it. 166 | ) 167 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # (The MIT License) 4 | # 5 | # Copyright (c) 2013-2021 Kura 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the 'Software'), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | -------------------------------------------------------------------------------- /tests/_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # (The MIT License) 4 | # 5 | # Copyright (c) 2013-2021 Kura 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the 'Software'), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | 26 | import logging 27 | import os 28 | import tempfile 29 | 30 | import pytest 31 | 32 | from blackhole.utils import Singleton 33 | 34 | 35 | logging.getLogger("blackhole").addHandler(logging.NullHandler()) 36 | 37 | 38 | @pytest.fixture() 39 | def cleandir(): 40 | newpath = tempfile.mkdtemp() 41 | os.chdir(newpath) 42 | 43 | 44 | @pytest.fixture() 45 | def reset(): 46 | Singleton._instances = {} 47 | 48 | 49 | def create_config(data): 50 | cwd = os.getcwd() 51 | path = os.path.join(cwd, "test.conf") 52 | with open(path, "w") as cfile: 53 | cfile.write("\n".join(data)) 54 | return path 55 | 56 | 57 | def create_file(name, data=""): 58 | cwd = os.getcwd() 59 | path = os.path.join(cwd, name) 60 | with open(path, "w") as ffile: 61 | ffile.write(str(data)) 62 | return path 63 | 64 | 65 | class Args(object): 66 | def __init__(self, args=None): 67 | if args is not None: 68 | for arg in args: 69 | setattr(self, arg[0], arg[1]) 70 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pytest 3 | 4 | 5 | @pytest.fixture 6 | def event_loop(): 7 | try: 8 | import asyncio 9 | 10 | import uvloop 11 | 12 | asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) 13 | except ImportError: 14 | pass 15 | loop = asyncio.new_event_loop() 16 | yield loop 17 | loop.close() 18 | -------------------------------------------------------------------------------- /tests/test_application.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # (The MIT License) 4 | # 5 | # Copyright (c) 2013-2021 Kura 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the 'Software'), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | 26 | import os 27 | from unittest import mock 28 | 29 | import pytest 30 | 31 | from blackhole.application import blackhole_config, run 32 | from blackhole.config import Config 33 | from blackhole.exceptions import ( 34 | BlackholeRuntimeException, 35 | ConfigException, 36 | DaemonException, 37 | ) 38 | 39 | 40 | from ._utils import ( # noqa: F401; isort:skip 41 | Args, 42 | cleandir, 43 | create_config, 44 | create_file, 45 | reset, 46 | ) 47 | 48 | 49 | @pytest.mark.usefixtures("reset", "cleandir") 50 | def test_run_test(): 51 | cfile = create_config(("",)) 52 | with mock.patch("sys.argv", ["blackhole", "-t", "-c", cfile]), mock.patch( 53 | "blackhole.config.Config.test_port", 54 | return_value=True, 55 | ), mock.patch( 56 | "blackhole.config.Config.test_pidfile", 57 | return_value=True, 58 | ), pytest.raises( 59 | SystemExit, 60 | ) as exc: 61 | run() 62 | assert exc.value.code == 0 63 | 64 | 65 | @pytest.mark.usefixtures("reset", "cleandir") 66 | def test_run_test_fails(): 67 | cfile = create_config(("listen=127.0.0.1:0",)) 68 | with mock.patch( 69 | "sys.argv", 70 | ["blackhole", "-t", "-c", cfile], 71 | ), pytest.raises(SystemExit) as exc: 72 | run() 73 | assert exc.value.code == 64 74 | 75 | 76 | @pytest.mark.usefixtures("reset", "cleandir") 77 | def test_run_load_test_fails(): 78 | cfile = create_config(("listen=127.0.0.1:0",)) 79 | with mock.patch("sys.argv", ["blackhole", "-t", "-c", cfile]), mock.patch( 80 | "blackhole.config.Config.test", 81 | side_effect=ConfigException(), 82 | ), pytest.raises(SystemExit) as exc: 83 | run() 84 | assert exc.value.code == 64 85 | 86 | 87 | @pytest.mark.usefixtures("reset", "cleandir") 88 | def test_run_foreground(): 89 | pidfile = os.path.join(os.getcwd(), "blackhole-test.pid") 90 | cfile = create_config( 91 | ("listen=127.0.0.1:9000", "pidfile={}".format(pidfile)), 92 | ) 93 | c = Config(cfile).load() 94 | 95 | with mock.patch("sys.argv", ["-c {}".format(cfile)]), mock.patch( 96 | "blackhole.config.Config.test", 97 | return_value=c, 98 | ), mock.patch("blackhole.config.warn_options"), mock.patch( 99 | "atexit.register", 100 | ), mock.patch( 101 | "os.chown", 102 | ), mock.patch( 103 | "blackhole.supervisor.Supervisor.generate_servers", 104 | ), mock.patch( 105 | "blackhole.control.pid_permissions", 106 | ), mock.patch( 107 | "blackhole.control.setgid", 108 | ), mock.patch( 109 | "blackhole.control.setuid", 110 | ), mock.patch( 111 | "blackhole.supervisor.Supervisor.run", 112 | ), pytest.raises( 113 | SystemExit, 114 | ) as exc: 115 | run() 116 | assert exc.value.code == 0 117 | 118 | 119 | @pytest.mark.usefixtures("reset", "cleandir") 120 | def test_run_foreground_pid_error(): 121 | pidfile = os.path.join(os.getcwd(), "blackhole-test.pid") 122 | cfile = create_config( 123 | ("listen=127.0.0.1:9000", "pidfile={}".format(pidfile)), 124 | ) 125 | c = Config(cfile).load() 126 | 127 | with mock.patch("sys.argv", ["-c {}".format(cfile)]), mock.patch( 128 | "blackhole.config.Config.test", 129 | return_value=c, 130 | ), mock.patch("blackhole.config.warn_options"), mock.patch( 131 | "os.getpid", 132 | return_value=1234, 133 | ), mock.patch( 134 | "atexit.register", 135 | side_effect=DaemonException, 136 | ), pytest.raises( 137 | SystemExit, 138 | ) as exc: 139 | run() 140 | assert exc.value.code == 64 141 | 142 | 143 | @pytest.mark.usefixtures("reset", "cleandir") 144 | def test_run_foreground_socket_error(): 145 | pidfile = os.path.join(os.getcwd(), "blackhole-test.pid") 146 | cfile = create_config( 147 | ("listen=127.0.0.1:9000", "pidfile={}".format(pidfile)), 148 | ) 149 | c = Config(cfile).load() 150 | 151 | with mock.patch("sys.argv", ["-c {}".format(cfile)]), mock.patch( 152 | "blackhole.config.Config.test", 153 | return_value=c, 154 | ), mock.patch("blackhole.config.warn_options"), mock.patch( 155 | "atexit.register", 156 | ), mock.patch( 157 | "blackhole.supervisor.Supervisor.close_socks", 158 | ), mock.patch( 159 | "blackhole.supervisor.Supervisor.generate_servers", 160 | side_effect=BlackholeRuntimeException, 161 | ), pytest.raises( 162 | SystemExit, 163 | ) as exc: 164 | run() 165 | assert exc.value.code == 77 166 | 167 | 168 | @pytest.mark.usefixtures("reset", "cleandir") 169 | def test_run_background(): 170 | pidfile = os.path.join(os.getcwd(), "blackhole-test.pid") 171 | cfile = create_config( 172 | ("listen=127.0.0.1:9000", "pidfile={}".format(pidfile)), 173 | ) 174 | c = Config(cfile).load() 175 | 176 | with mock.patch("sys.argv", ["-c {}".format(cfile), "-b"]), mock.patch( 177 | "blackhole.config.Config.test", 178 | return_value=c, 179 | ), mock.patch("blackhole.config.warn_options"), mock.patch( 180 | "atexit.register", 181 | ), mock.patch( 182 | "os.chown", 183 | ), mock.patch( 184 | "blackhole.supervisor.Supervisor.generate_servers", 185 | ), mock.patch( 186 | "blackhole.daemon.Daemon.daemonize", 187 | ), mock.patch( 188 | "blackhole.control.pid_permissions", 189 | ), mock.patch( 190 | "blackhole.control.setgid", 191 | ), mock.patch( 192 | "blackhole.control.setuid", 193 | ), mock.patch( 194 | "blackhole.supervisor.Supervisor.run", 195 | ), pytest.raises( 196 | SystemExit, 197 | ) as exc: 198 | run() 199 | assert exc.value.code == 0 200 | 201 | 202 | @pytest.mark.usefixtures("reset", "cleandir") 203 | def test_run_daemon_daemonize_error(): 204 | pidfile = os.path.join(os.getcwd(), "blackhole-test.pid") 205 | cfile = create_config( 206 | ("listen=127.0.0.1:9000", "pidfile={}".format(pidfile)), 207 | ) 208 | c = Config(cfile).load() 209 | 210 | with mock.patch("sys.argv", ["-c {}".format(cfile), "-b"]), mock.patch( 211 | "blackhole.application.Config.test", 212 | return_value=c, 213 | ), mock.patch("blackhole.config.warn_options"), mock.patch( 214 | "atexit.register", 215 | ), mock.patch( 216 | "os.chown", 217 | ), mock.patch( 218 | "blackhole.supervisor.Supervisor.generate_servers", 219 | ), mock.patch( 220 | "os.fork", 221 | side_effect=OSError, 222 | ), mock.patch( 223 | "blackhole.supervisor.Supervisor.close_socks", 224 | ) as mock_close, pytest.raises( 225 | SystemExit, 226 | ) as exc: 227 | run() 228 | assert exc.value.code == 77 229 | assert mock_close.called is True 230 | 231 | 232 | @pytest.mark.usefixtures("reset", "cleandir") 233 | def test_blackhole_config(): 234 | with pytest.raises(SystemExit) as exc: 235 | blackhole_config() 236 | assert exc.value.code == 0 237 | -------------------------------------------------------------------------------- /tests/test_child.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # (The MIT License) 4 | # 5 | # Copyright (c) 2013-2021 Kura 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the 'Software'), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | 26 | import asyncio 27 | import os 28 | import socket 29 | from unittest import mock 30 | 31 | import pytest 32 | 33 | from blackhole import protocols 34 | from blackhole.child import Child 35 | from blackhole.control import _socket 36 | from blackhole.streams import StreamProtocol 37 | 38 | 39 | from ._utils import ( # noqa: F401; isort:skip 40 | Args, 41 | cleandir, 42 | create_config, 43 | create_file, 44 | reset, 45 | ) 46 | 47 | 48 | try: 49 | import uvloop # noqa 50 | 51 | _LOOP = "uvloop.Loop" 52 | except ImportError: 53 | _LOOP = "asyncio.unix_events._UnixSelectorEventLoop" 54 | 55 | 56 | @pytest.mark.usefixtures("reset", "cleandir") 57 | def test_initiation(): 58 | Child("", "", [], "1") 59 | 60 | 61 | @pytest.mark.usefixtures("reset", "cleandir") 62 | def test_start(): 63 | socks = [{"sock": None, "ssl": None}, {"sock": None, "ssl": "abc"}] 64 | child = Child("", "", socks, "1") 65 | with mock.patch("asyncio.Task"), mock.patch( 66 | "blackhole.child.Child.heartbeat", 67 | ), mock.patch("{0}.run_forever".format(_LOOP)), mock.patch( 68 | "blackhole.child.Child.stop", 69 | ), mock.patch( 70 | "os._exit", 71 | ) as mock_exit: 72 | child.start() 73 | assert mock_exit.called is True 74 | 75 | 76 | @pytest.mark.usefixtures("reset", "cleandir") 77 | def test_stop(): 78 | socks = [{"sock": None, "ssl": None}, {"sock": None, "ssl": "abc"}] 79 | child = Child("", "", socks, "1") 80 | child.loop = mock.MagicMock() 81 | child.clients.append(mock.MagicMock()) 82 | child.servers.append(mock.MagicMock()) 83 | child.heartbeat_task = mock.MagicMock() 84 | child.server_task = mock.MagicMock() 85 | 86 | with mock.patch("os._exit") as mock_exit, mock.patch( 87 | "{0}.run_until_complete".format(_LOOP), 88 | ): 89 | child.stop() 90 | assert mock_exit.called is True 91 | 92 | 93 | @pytest.mark.usefixtures("reset", "cleandir") 94 | def test_stop_runtime_exception(): 95 | socks = [{"sock": None, "ssl": None}, {"sock": None, "ssl": "abc"}] 96 | child = Child("", "", socks, "1") 97 | child.loop = mock.MagicMock() 98 | child.clients.append(mock.MagicMock()) 99 | child.servers.append(mock.MagicMock()) 100 | child.heartbeat_task = mock.MagicMock() 101 | child.server_task = mock.MagicMock() 102 | 103 | with mock.patch("os._exit") as mock_exit, mock.patch( 104 | "{0}.stop".format(_LOOP), 105 | side_effect=RuntimeError, 106 | ): 107 | child.stop() 108 | assert mock_exit.called is True 109 | 110 | 111 | @pytest.mark.usefixtures("reset", "cleandir") 112 | @pytest.mark.asyncio 113 | async def test_start_child_loop(event_loop): 114 | sock = _socket("127.0.0.1", 0, socket.AF_INET) 115 | socks = ({"sock": sock, "ssl": None},) 116 | child = Child("", "", socks, "1") 117 | child.loop = event_loop 118 | await child._start() 119 | assert len(child.servers) == 1 120 | for server in child.servers: 121 | server.close() 122 | 123 | 124 | @pytest.mark.usefixtures("reset", "cleandir") 125 | @pytest.mark.asyncio 126 | async def test_child_heartbeat_not_started(event_loop): 127 | up_read, up_write = os.pipe() 128 | down_read, down_write = os.pipe() 129 | os.close(up_write) 130 | os.close(down_read) 131 | child = Child(up_read, down_write, [], "1") 132 | child.loop = event_loop 133 | child._started = False 134 | with mock.patch("asyncio.Task") as mock_task, mock.patch( 135 | "blackhole.child.Child._start", 136 | ) as mock_start, mock.patch("blackhole.child.Child.stop") as mock_stop: 137 | await child.heartbeat() 138 | assert mock_task.called is True 139 | assert mock_start.called is True 140 | assert mock_stop.called is True 141 | 142 | 143 | @pytest.mark.usefixtures("reset", "cleandir") 144 | @pytest.mark.asyncio 145 | async def test_child_heartbeat_started(event_loop): 146 | up_read, up_write = os.pipe() 147 | down_read, down_write = os.pipe() 148 | os.close(up_write) 149 | os.close(down_read) 150 | child = Child(up_read, down_write, [], "1") 151 | child.loop = event_loop 152 | child._started = True 153 | sp = StreamProtocol() 154 | sp.reader = asyncio.StreamReader() 155 | 156 | async def _reset(): 157 | sp.reader.feed_data(protocols.PING) 158 | child._started = False 159 | 160 | reset_task = asyncio.Task(_reset()) 161 | with mock.patch( 162 | "blackhole.streams.StreamProtocol", 163 | return_value=sp, 164 | ), mock.patch("asyncio.Task") as mock_task, mock.patch( 165 | "blackhole.child.Child._start", 166 | ) as mock_start, mock.patch( 167 | "blackhole.child.Child.stop", 168 | ) as mock_stop: 169 | await child.heartbeat() 170 | reset_task.cancel() 171 | assert mock_task.called is True 172 | assert mock_start.called is True 173 | assert mock_stop.called is True 174 | -------------------------------------------------------------------------------- /tests/test_daemon.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # (The MIT License) 4 | # 5 | # Copyright (c) 2013-2021 Kura 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the 'Software'), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | 26 | import os 27 | from unittest import mock 28 | 29 | import pytest 30 | 31 | from blackhole.daemon import Daemon, DaemonException 32 | 33 | 34 | from ._utils import ( # noqa: F401; isort:skip 35 | Args, 36 | cleandir, 37 | create_config, 38 | create_file, 39 | reset, 40 | ) 41 | 42 | 43 | @pytest.mark.usefixtures("reset", "cleandir") 44 | def test_instantiated_but_not_daemonised(): 45 | pid = os.path.join(os.getcwd(), "fake.pid") 46 | with mock.patch("os.getpid", return_value=666): 47 | daemon = Daemon(pid) 48 | assert daemon.pidfile == pid 49 | assert daemon.pid == 666 50 | 51 | 52 | @pytest.mark.usefixtures("reset", "cleandir") 53 | def test_set_pid_invalid_path(): 54 | with mock.patch("os.path.exists", return_value=False), mock.patch( 55 | "atexit.register", 56 | ), pytest.raises(DaemonException): 57 | Daemon("/fake/path.pid") 58 | 59 | 60 | @pytest.mark.usefixtures("reset", "cleandir") 61 | def test_set_pid_valid_path(): 62 | pid = os.path.join(os.getcwd(), "fake.pid") 63 | with mock.patch("os.getpid", return_value=666), mock.patch( 64 | "atexit.register", 65 | ): 66 | daemon = Daemon(pid) 67 | assert daemon.pidfile == pid 68 | assert daemon.pid == 666 69 | 70 | 71 | @pytest.mark.usefixtures("reset", "cleandir") 72 | def test_get_pid_file_error(): 73 | with mock.patch("os.path.exists", return_value=True): 74 | with mock.patch( 75 | "builtins.open", 76 | side_effect=FileNotFoundError, 77 | ), mock.patch("atexit.register"), pytest.raises(DaemonException): 78 | Daemon("/fake/path.pid") 79 | with mock.patch("builtins.open", side_effect=IOError), mock.patch( 80 | "atexit.register", 81 | ), pytest.raises(DaemonException): 82 | Daemon("/fake/path.pid") 83 | with mock.patch( 84 | "builtins.open", 85 | side_effect=PermissionError, 86 | ), mock.patch("atexit.register"), pytest.raises(DaemonException): 87 | Daemon("/fake/path.pid") 88 | with mock.patch("builtins.open", side_effect=OSError), mock.patch( 89 | "atexit.register", 90 | ), pytest.raises(DaemonException): 91 | Daemon("/fake/path.pid") 92 | 93 | 94 | @pytest.mark.usefixtures("reset", "cleandir") 95 | def test_get_pid(): 96 | pfile = create_file("test.pid", 123) 97 | with mock.patch("os.getpid", return_value=123), mock.patch( 98 | "atexit.register", 99 | ): 100 | daemon = Daemon(pfile) 101 | assert daemon.pid == 123 102 | 103 | 104 | @pytest.mark.usefixtures("reset", "cleandir") 105 | def test_delete_pid_no_exists(): 106 | pfile = create_file("test.pid", 123) 107 | daemon = Daemon(pfile) 108 | with mock.patch("os.remove") as mock_rm, mock.patch( 109 | "atexit.register", 110 | ), mock.patch("os.path.exists", return_value=False): 111 | del daemon.pid 112 | assert mock_rm.called is False 113 | 114 | 115 | @pytest.mark.usefixtures("reset", "cleandir") 116 | def test_delete_pid(): 117 | pfile = create_file("test.pid", 123) 118 | with mock.patch("os.getpid", return_value=123), mock.patch( 119 | "atexit.register", 120 | ): 121 | daemon = Daemon(pfile) 122 | assert daemon.pid == 123 123 | del daemon.pid 124 | assert daemon.pid is None 125 | 126 | 127 | @pytest.mark.usefixtures("reset", "cleandir") 128 | def test_delete_pid_exit(): 129 | pfile = create_file("test.pid", 123) 130 | with mock.patch("os.getpid", return_value=123), mock.patch( 131 | "atexit.register", 132 | ): 133 | daemon = Daemon(pfile) 134 | assert daemon.pid == 123 135 | daemon._exit() 136 | assert daemon.pid is None 137 | 138 | 139 | @pytest.mark.usefixtures("reset", "cleandir") 140 | def test_fork(): 141 | pfile = create_file("test.pid", 123) 142 | with mock.patch("atexit.register"): 143 | daemon = Daemon(pfile) 144 | with mock.patch("os.fork", return_value=9999) as mock_fork, mock.patch( 145 | "os._exit", 146 | ) as mock_exit: 147 | daemon.fork() 148 | assert mock_fork.called is True 149 | assert mock_exit.called is True 150 | 151 | 152 | @pytest.mark.usefixtures("reset", "cleandir") 153 | def test_fork_error(): 154 | pfile = create_file("test.pid", 123) 155 | with mock.patch("atexit.register"): 156 | daemon = Daemon(pfile) 157 | with mock.patch( 158 | "os.fork", 159 | side_effect=OSError, 160 | ) as mock_fork, pytest.raises(DaemonException): 161 | daemon.fork() 162 | assert mock_fork.called is True 163 | 164 | 165 | @pytest.mark.usefixtures("reset", "cleandir") 166 | def test_daemonise(): 167 | pfile = create_file("test.pid", 123) 168 | with mock.patch("blackhole.daemon.Daemon.fork") as mock_fork, mock.patch( 169 | "os.chdir", 170 | ) as mock_chdir, mock.patch("os.setsid") as mock_setsid, mock.patch( 171 | "os.umask", 172 | ) as mock_umask, mock.patch( 173 | "atexit.register", 174 | ) as mock_atexit, mock.patch( 175 | "os.getpid", 176 | return_value=123, 177 | ): 178 | daemon = Daemon(pfile) 179 | daemon.daemonize() 180 | assert mock_fork.called is True 181 | assert mock_chdir.called is True 182 | assert mock_setsid.called is True 183 | assert mock_umask.called is True 184 | assert mock_atexit.called is True 185 | -------------------------------------------------------------------------------- /tests/test_logs.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # (The MIT License) 4 | # 5 | # Copyright (c) 2013-2021 Kura 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the 'Software'), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | 26 | import logging 27 | 28 | import pytest 29 | 30 | from blackhole.logs import configure_logs 31 | 32 | 33 | from ._utils import ( # noqa: F401; isort:skip 34 | Args, 35 | cleandir, 36 | create_config, 37 | create_file, 38 | reset, 39 | ) 40 | 41 | 42 | @pytest.mark.usefixtures("reset", "cleandir") 43 | def test_default(): 44 | args = Args((("debug", False), ("test", False), ("quiet", False))) 45 | logger = logging.getLogger("blackhole") 46 | configure_logs(args) 47 | assert logger.handlers[0].level is logging.INFO 48 | 49 | 50 | @pytest.mark.usefixtures("reset", "cleandir") 51 | def test_debug(): 52 | args = Args((("debug", True), ("test", False), ("quiet", False))) 53 | logger = logging.getLogger("blackhole") 54 | configure_logs(args) 55 | assert logger.handlers[0].level is logging.DEBUG 56 | 57 | 58 | @pytest.mark.usefixtures("reset", "cleandir") 59 | def test_test(): 60 | args = Args((("debug", False), ("test", True), ("quiet", False))) 61 | logger = logging.getLogger("blackhole") 62 | configure_logs(args) 63 | assert logger.handlers[0].level is logging.INFO 64 | 65 | 66 | @pytest.mark.usefixtures("reset", "cleandir") 67 | def test_quiet(): 68 | args = Args((("debug", False), ("test", False), ("quiet", True))) 69 | logger = logging.getLogger("blackhole") 70 | configure_logs(args) 71 | assert logger.handlers[0].level is logging.ERROR 72 | -------------------------------------------------------------------------------- /tests/test_streams.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # (The MIT License) 4 | # 5 | # Copyright (c) 2013-2021 Kura 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the 'Software'), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | 26 | import asyncio 27 | 28 | import pytest 29 | 30 | from blackhole.streams import StreamProtocol 31 | 32 | 33 | from ._utils import ( # noqa: F401; isort:skip 34 | Args, 35 | cleandir, 36 | create_config, 37 | create_file, 38 | reset, 39 | ) 40 | 41 | 42 | @pytest.mark.usefixtures("reset", "cleandir") 43 | @pytest.mark.asyncio 44 | async def test_client_not_connected(): 45 | sp = StreamProtocol() 46 | assert sp.is_connected() is False 47 | 48 | 49 | @pytest.mark.usefixtures("reset", "cleandir") 50 | @pytest.mark.asyncio 51 | async def test_client_connected(event_loop): 52 | sp = StreamProtocol(loop=event_loop) 53 | sp.connection_made(asyncio.Transport()) 54 | assert sp.is_connected() is True 55 | 56 | 57 | # @pytest.mark.usefixtures('reset_conf', 'reset_daemon', 'reset_supervisor', 58 | # 'cleandir') 59 | # @pytest.mark.asyncio 60 | # async def test_connection_lost_exception(event_loop): 61 | # sp = StreamProtocol(loop=event_loop) 62 | # sp.connection_made(asyncio.Transport()) 63 | # assert sp.is_connected() is True 64 | # with pytest.raises(KeyError): 65 | # sp.connection_lost(KeyError) 66 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # (The MIT License) 4 | # 5 | # Copyright (c) 2013-2021 Kura 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the 'Software'), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | 26 | from io import StringIO 27 | from unittest import mock 28 | 29 | import pytest 30 | 31 | from blackhole.utils import get_version, mailname, message_id 32 | 33 | 34 | from ._utils import ( # noqa: F401; isort:skip 35 | Args, 36 | cleandir, 37 | create_config, 38 | create_file, 39 | reset, 40 | ) 41 | 42 | 43 | @pytest.mark.usefixtures("reset", "cleandir") 44 | def test_mail_name_file(): 45 | check_value = "file.blackhole.io" 46 | with mock.patch("os.access", return_value=True), mock.patch( 47 | "codecs.open", 48 | return_value=StringIO(check_value), 49 | ): 50 | mn = mailname() 51 | assert mn == check_value 52 | 53 | 54 | @pytest.mark.usefixtures("reset", "cleandir") 55 | def test_mail_name_socket(): 56 | check_value = "socket.blackhole.io" 57 | with mock.patch("os.access", return_value=False), mock.patch( 58 | "socket.getfqdn", 59 | return_value="socket.blackhole.io", 60 | ): 61 | mn = mailname() 62 | assert mn == check_value 63 | 64 | 65 | @pytest.mark.usefixtures("reset", "cleandir") 66 | def test_mail_name_file_length_0(): 67 | mnfile = create_file("mailname") 68 | check_value = "socket.blackhole.io" 69 | with mock.patch("os.access", return_value=True), mock.patch( 70 | "socket.getfqdn", 71 | return_value="socket.blackhole.io", 72 | ): 73 | mn = mailname(mnfile) 74 | assert mn == check_value 75 | 76 | 77 | @pytest.mark.usefixtures("reset", "cleandir") 78 | def test_mail_name_file_garbage(): 79 | mnfile = create_file("mailname", " \n ") 80 | check_value = "socket.blackhole.io" 81 | with mock.patch("os.access", return_value=True), mock.patch( 82 | "socket.getfqdn", 83 | return_value="socket.blackhole.io", 84 | ): 85 | mn = mailname(mnfile) 86 | assert mn == check_value 87 | 88 | 89 | @pytest.mark.usefixtures("reset", "cleandir") 90 | def test_message_id(): 91 | with mock.patch( 92 | "time.monotonic", 93 | return_value=1463290829.4173775, 94 | ) as mock_time, mock.patch( 95 | "os.getpid", 96 | return_value=9000, 97 | ) as mock_getpid, mock.patch( 98 | "random.getrandbits", 99 | return_value=17264867586200823825, 100 | ) as mock_randbits: 101 | ex_mid = "<{}.{}.{}@blackhole.io>".format( 102 | int(1463290829.4173775 * 100), 103 | 9000, 104 | 17264867586200823825, 105 | ) 106 | assert message_id("blackhole.io") == ex_mid 107 | assert mock_time.called is True 108 | assert mock_getpid.called is True 109 | assert mock_randbits.called is True 110 | 111 | 112 | @pytest.mark.usefixtures("reset", "cleandir") 113 | def test_get_version(): 114 | version_file = create_file("version.py", '__version__ = "9.9.9"') 115 | with mock.patch("os.path.join", return_value=version_file): 116 | assert get_version() == "9.9.9" 117 | 118 | 119 | @pytest.mark.usefixtures("reset", "cleandir") 120 | def test_get_version_no_access(): 121 | with mock.patch("os.access", return_value=False), pytest.raises( 122 | OSError, 123 | ) as err: 124 | get_version() 125 | assert str(err.value) == "Cannot open __init__.py file for reading" 126 | 127 | 128 | @pytest.mark.usefixtures("reset", "cleandir") 129 | def test_get_version_invalid_version_split(): 130 | version_file = create_file("version.py", "__version__") 131 | with mock.patch("os.path.join", return_value=version_file), pytest.raises( 132 | AssertionError, 133 | ) as err: 134 | get_version() 135 | assert str(err.value) == "Cannot extract version from __version__" 136 | 137 | 138 | @pytest.mark.usefixtures("reset", "cleandir") 139 | def test_get_version_invalid_version(): 140 | version_file_a = create_file("versiona.py", "__version__ = a.1") 141 | with mock.patch( 142 | "os.path.join", 143 | return_value=version_file_a, 144 | ), pytest.raises(AssertionError) as err: 145 | get_version() 146 | assert str(err.value) == "a.1 is not a valid version number" 147 | version_file_b = create_file("versionb.py", "__version__ = a.1.1") 148 | with mock.patch( 149 | "os.path.join", 150 | return_value=version_file_b, 151 | ), pytest.raises(AssertionError) as err: 152 | get_version() 153 | assert str(err.value) == "a.1.1 is not a valid version number" 154 | version_file_c = create_file("versionc.py", "__version__ = 1.a.1") 155 | with mock.patch( 156 | "os.path.join", 157 | return_value=version_file_c, 158 | ), pytest.raises(AssertionError) as err: 159 | get_version() 160 | assert str(err.value) == "1.a.1 is not a valid version number" 161 | version_file_d = create_file("versiond.py", "__version__ = 1.1.a") 162 | with mock.patch( 163 | "os.path.join", 164 | return_value=version_file_d, 165 | ), pytest.raises(AssertionError) as err: 166 | get_version() 167 | assert str(err.value) == "1.1.a is not a valid version number" 168 | 169 | 170 | @pytest.mark.usefixtures("reset", "cleandir") 171 | def test_get_version_version_not_found(): 172 | version_file = create_file("version.py", 'version = "abc"') 173 | with mock.patch("os.path.join", return_value=version_file), pytest.raises( 174 | AssertionError, 175 | ) as err: 176 | get_version() 177 | assert str(err.value) == "No __version__ assignment found" 178 | -------------------------------------------------------------------------------- /tests/test_worker.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # (The MIT License) 4 | # 5 | # Copyright (c) 2013-2021 Kura 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the 'Software'), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | 26 | import asyncio 27 | from unittest import mock 28 | 29 | import pytest 30 | 31 | from blackhole.config import Config 32 | from blackhole.worker import Worker 33 | 34 | 35 | from ._utils import ( # noqa: F401; isort:skip 36 | Args, 37 | cleandir, 38 | create_config, 39 | create_file, 40 | reset, 41 | ) 42 | 43 | 44 | @pytest.mark.usefixtures("reset", "cleandir") 45 | @pytest.mark.asyncio 46 | @pytest.mark.slow 47 | async def test_start_stop(event_loop): 48 | worker = Worker(1, [], loop=event_loop) 49 | assert worker._started is True 50 | await asyncio.sleep(10) 51 | worker.stop() 52 | assert worker._started is False 53 | 54 | 55 | @pytest.mark.usefixtures("reset", "cleandir") 56 | def test_child_start_setgid_fails_invalid_group(event_loop): 57 | cfile = create_config( 58 | ("user=fgqewgreghrehgerhehw", "group=fgqewgreghrehgerhehw"), 59 | ) 60 | Config(cfile).load() 61 | with mock.patch("os.pipe", return_value=("", "")), mock.patch( 62 | "os.fork", 63 | return_value=False, 64 | ), mock.patch("os.close"), mock.patch( 65 | "os.setgid", 66 | side_effect=KeyError, 67 | ), pytest.raises( 68 | SystemExit, 69 | ) as exc: 70 | Worker([], [], loop=event_loop) 71 | assert exc.value.code == 64 72 | 73 | 74 | @pytest.mark.usefixtures("reset", "cleandir") 75 | def test_child_start_setgid_fails_permissions(event_loop): 76 | cfile = create_config( 77 | ("user=fgqewgreghrehgerhehw", "group=fgqewgreghrehgerhehw"), 78 | ) 79 | Config(cfile).load() 80 | with mock.patch("os.pipe", return_value=("", "")), mock.patch( 81 | "os.fork", 82 | return_value=False, 83 | ), mock.patch("os.close"), mock.patch( 84 | "os.setgid", 85 | side_effect=PermissionError, 86 | ), pytest.raises( 87 | SystemExit, 88 | ) as exc: 89 | Worker([], [], loop=event_loop) 90 | assert exc.value.code == 64 91 | 92 | 93 | @pytest.mark.usefixtures("reset", "cleandir") 94 | def test_child_start_setuid_fails_invalid_user(event_loop): 95 | cfile = create_config( 96 | ("user=fgqewgreghrehgerhehw", "group=fgqewgreghrehgerhehw"), 97 | ) 98 | Config(cfile).load() 99 | with mock.patch("os.pipe", return_value=("", "")), mock.patch( 100 | "os.fork", 101 | return_value=False, 102 | ), mock.patch("os.close"), mock.patch( 103 | "os.setuid", 104 | side_effect=KeyError, 105 | ), pytest.raises( 106 | SystemExit, 107 | ) as exc: 108 | Worker([], [], loop=event_loop) 109 | assert exc.value.code == 64 110 | 111 | 112 | @pytest.mark.usefixtures("reset", "cleandir") 113 | def test_child_start_setuid_fails_permissions(event_loop): 114 | cfile = create_config( 115 | ("user=fgqewgreghrehgerhehw", "group=fgqewgreghrehgerhehw"), 116 | ) 117 | Config(cfile).load() 118 | with mock.patch("os.pipe", return_value=("", "")), mock.patch( 119 | "os.fork", 120 | return_value=False, 121 | ), mock.patch("os.close"), mock.patch( 122 | "os.setuid", 123 | side_effect=PermissionError, 124 | ), pytest.raises( 125 | SystemExit, 126 | ) as exc: 127 | Worker([], [], loop=event_loop) 128 | assert exc.value.code == 64 129 | -------------------------------------------------------------------------------- /tests/test_worker_child_communication.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # (The MIT License) 4 | # 5 | # Copyright (c) 2013-2021 Kura 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the 'Software'), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in 15 | # all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | 26 | import asyncio 27 | import socket 28 | import time 29 | 30 | import pytest 31 | 32 | from blackhole.control import server 33 | from blackhole.worker import Worker 34 | 35 | 36 | from ._utils import ( # noqa: F401; isort:skip 37 | Args, 38 | cleandir, 39 | create_config, 40 | create_file, 41 | reset, 42 | ) 43 | 44 | 45 | try: 46 | import uvloop # noqa 47 | except ImportError: 48 | pass 49 | 50 | 51 | @pytest.mark.usefixtures("reset", "cleandir") 52 | @pytest.mark.asyncio 53 | @pytest.mark.slow 54 | async def test_worker_ping_pong(unused_tcp_port, event_loop): 55 | aserver = server("127.0.0.1", unused_tcp_port, socket.AF_INET) 56 | started = time.monotonic() 57 | worker = Worker("1", [aserver], loop=event_loop) 58 | assert worker._started is True 59 | await asyncio.sleep(35) 60 | worker.stop() 61 | assert worker._started is False 62 | assert worker.ping > started 63 | assert worker.ping_count == 2 64 | aserver["sock"].close() 65 | 66 | 67 | @pytest.mark.usefixtures("reset", "cleandir") 68 | @pytest.mark.asyncio 69 | @pytest.mark.slow 70 | async def test_restart(unused_tcp_port, event_loop): 71 | aserver = server("127.0.0.1", unused_tcp_port, socket.AF_INET) 72 | started = time.monotonic() 73 | worker = Worker("1", [aserver], loop=event_loop) 74 | assert worker._started is True 75 | await asyncio.sleep(25) 76 | worker.ping = time.monotonic() - 120 77 | old_pid = worker.pid 78 | await asyncio.sleep(15) 79 | assert worker.pid is not old_pid 80 | worker.stop() 81 | assert worker._started is False 82 | assert worker.ping > started 83 | assert worker.ping_count == 0 84 | aserver["sock"].close() 85 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | pre-commit 4 | py37 5 | py37-{setproctitle,uvloop} 6 | py38 7 | py38-{setproctitle,uvloop} 8 | py39 9 | py39-{setproctitle,uvloop} 10 | py310 11 | py310-{setproctitle,uvloop} 12 | pypy3 13 | pypy3-{setproctitle,uvloop} 14 | pyston-3 15 | pyston-3-{setproctitle,uvloop} 16 | build 17 | docs 18 | lint 19 | man 20 | poetry 21 | publish 22 | setuppy 23 | shellcheck 24 | coverage-report 25 | watch 26 | isolated_build = True 27 | 28 | [gh-actions] 29 | python = 30 | 3.7: py37, py37-{setproctitle,uvloop} 31 | 3.8: py38, py38-{setproctitle,uvloop} 32 | 3.9: py39, py39-{setproctitle,uvloop} 33 | 3.10: py310, py310-{setproctitle,uvloop} 34 | pypy-3.7: pypy3, pypy3-{setproctitle,uvloop} 35 | pyston-3: pyston-3, pyston-3-{setproctitle,uvloop} 36 | 37 | [testenv] 38 | setenv = VIRTUALENV_NO_DOWNLOAD=1 39 | parallel_show_output = true 40 | deps = 41 | setproctitle: setproctitle 42 | uvloop: uvloop 43 | extras = tests 44 | commands = coverage run --parallel -m pytest \ 45 | --cache-clear \ 46 | --verbose blackhole tests 47 | 48 | [testenv:{py37,py37-setproctitle,py37-uvloop}] 49 | basepython = python3.7 50 | extras = tests 51 | setenv = PYTHONWARNINGS=d 52 | 53 | [testenv:{py38,py38-setproctitle,py38-uvloop}] 54 | basepython = python3.8 55 | extras = tests 56 | setenv = PYTHONWARNINGS=d 57 | 58 | [testenv:{py39,py39-setproctitle,py39-uvloop}] 59 | basepython = python3.9 60 | extras = tests 61 | setenv = PYTHONWARNINGS=d 62 | 63 | [testenv:{py310,py310-setproctitle,py310-uvloop}] 64 | basepython = python3.10 65 | extras = tests 66 | setenv = PYTHONWARNINGS=d 67 | 68 | [testenv:{pypy3,pypy3-setproctitle,pypy3-uvloop}] 69 | basepython = pypy3 70 | extras = tests 71 | setenv = PYTHONWARNINGS=d 72 | 73 | [testenv:{pyston-3,pyston-3-setproctitle,pyston-3-uvloop}] 74 | basepython = pyston3 75 | extras = tests 76 | setenv = PYTHONWARNINGS=d 77 | 78 | [testenv:watch] 79 | extras = tests 80 | deps = 81 | pytest-testmon 82 | pytest-watch 83 | commands = ptw -- --testmon \ 84 | --cache-clear \ 85 | --verbose blackhole tests 86 | 87 | [testenv:build] 88 | skip_install = True 89 | deps = poetry 90 | commands = poetry build 91 | 92 | [testenv:coverage-report] 93 | deps = coverage 94 | skip_install = true 95 | commands = 96 | coverage combine 97 | coverage xml 98 | coverage report 99 | 100 | [testenv:docs] 101 | extras = docs 102 | changedir = docs/source 103 | commands = 104 | sphinx-build -j 4 -d {envtmpdir}/doctrees . {envtmpdir}/html -c . 105 | 106 | [testenv:lint] 107 | skip_install = true 108 | extras = tests 109 | deps = 110 | black 111 | flake8 112 | flake8-bugbear 113 | isort>=4.2.5,<5 114 | flake8-isort 115 | flake8-commas 116 | pyroma 117 | interrogate 118 | bandit 119 | pydocstyle 120 | doc8 121 | codespell 122 | vulture 123 | commands = 124 | flake8 blackhole tests setup.py docs/source/conf.py 125 | black --check --verbose blackhole tests setup.py docs/source/conf.py 126 | interrogate blackhole 127 | pyroma . 128 | bandit -r blackhole 129 | pydocstyle blackhole 130 | doc8 docs/source 131 | codespell --skip="./.tox,./docs/source/_extra,./docs/source/_static,./.git/hooks/pre-commit" 132 | vulture --min-confidence 100 blackhole tests 133 | 134 | [testenv:man] 135 | skip_install = True 136 | deps = docutils 137 | commands = 138 | rst2man.py man/source/blackhole.rst {envtmpdir}/blackhole.1 139 | rst2man.py man/source/blackhole_config.rst {envtmpdir}/blackhole_config.1 140 | 141 | [testenv:manifest] 142 | deps = check-manifest 143 | skip_install = true 144 | commands = check-manifest 145 | 146 | [testenv:poetry] 147 | skip_install = True 148 | deps = poetry 149 | commands = 150 | poetry check 151 | poetry install 152 | 153 | [testenv:pre-commit] 154 | skip_install = true 155 | deps = pre-commit 156 | commands = pre-commit run --all-files --verbose 157 | 158 | [testenv:publish] 159 | skip_install = True 160 | deps = poetry 161 | commands = 162 | poetry build 163 | poetry publish 164 | 165 | [testenv:setuppy] 166 | deps = 167 | docutils 168 | readme_renderer 169 | skip_install = true 170 | commands = python setup.py check -r -s -m 171 | 172 | [testenv:shellcheck] 173 | skip_install = true 174 | whitelist_externals = shellcheck 175 | commands = shellcheck -x \ 176 | scripts/minify.sh \ 177 | scripts/update-libuv.sh \ 178 | bash-completion/blackhole-completion.bash \ 179 | init.d/debian-ubuntu/blackhole 180 | --------------------------------------------------------------------------------