Source code for blackhole.utils

# -*- coding: utf-8 -*-

# (The MIT License)
#
# Copyright (c) 2013-2021 Kura
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the 'Software'), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

"""Provides utility functionality."""

import codecs
import os
import random
import socket
import time


__all__ = ("blackhole_config_help", "mailname", "message_id", "get_version")


[docs]class Singleton(type): """Singleton.""" _instances = {} def __call__(cls, *args, **kwargs): """Override the __call__ method.""" if cls not in cls._instances: cls._instances[cls] = super(Singleton, cls).__call__( *args, **kwargs, ) return cls._instances[cls]
[docs]def mailname(mailname_file="/etc/mailname"): """ Fully qualified domain name for HELO and EHLO. :param str mailname_file: A path to the mailname file. Default: ``/etc/mailname``. :returns: A domain name. :rtype: :py:obj:`str` .. note:: Prefers content of `mailname_file`, falls back on :py:func:`socket.getfqdn` if `mailname_file` does not exist or cannot be opened for reading. """ if os.access(mailname_file, os.R_OK): with codecs.open(mailname_file, encoding="utf-8") as _mailname_file: mailname_content = _mailname_file.readlines() if len(mailname_content) == 0: return socket.getfqdn() nmailname_content = mailname_content[0].strip() if nmailname_content != "": return str(nmailname_content) return socket.getfqdn()
[docs]def message_id(domain): """ Return a string suitable for RFC 2822 compliant Message-ID. :param str domain: A fully qualified domain. :returns: An RFC 2822 compliant Message-ID. :rtype: :py:obj:`str` """ timeval = int(time.monotonic() * 100) pid = os.getpid() randint = random.getrandbits(64) return f"<{timeval}.{pid}.{randint}@{domain}>"
[docs]def get_version(): """ Extract the __version__ from a file without importing it. :return: The version that was extracted. :rtype: :py:obj:`str` :raises AssertionError: When a version cannot be determined. """ path = os.path.dirname(os.path.abspath(__file__)) filepath = os.path.join(path, "__init__.py") if not os.access(filepath, os.R_OK): raise OSError("Cannot open __init__.py file for reading") with codecs.open(filepath, encoding="utf-8") as fp: for line in fp: if line.startswith("__version__"): try: _, vers = line.split("=") except ValueError: msg = "Cannot extract version from __version__" raise AssertionError(msg) version = vers.strip().replace('"', "").replace("'", "") try: major, minor, patch = version.split(".") digits = ( major.isdigit(), minor.isdigit(), patch.isdigit(), ) if not all(digits): msg = f"{version} is not a valid version number" raise AssertionError(msg) except ValueError: msg = f"{version} is not a valid version number" raise AssertionError(msg) return version raise AssertionError("No __version__ assignment found")
[docs]class Formatter: """Terminal output formatter.""" bold = "\033[1m" under = "\033[4m" reset = "\033[0m"
formatting = Formatter() # fmt: off blackhole_config_help = '''BLACKHOLE_CONFIG(1) {f.bold}NAME{f.reset} blackhole_config - the config file format for the Blackhole MTA {f.bold}DESCRIPTION{f.reset} This manual page documents the {f.bold}Blackhole{f.reset} configuration file format and options. {f.bold}OPTIONS{f.reset} These are all available options for the configuration file, their default values and information on what the options actually do. The file format is a simple {f.under}attribute = value{f.reset} style, an example is shown below. # This is a comment. listen = :25 # This is an inline comment. user = kura group = kura {f.bold}listen{f.reset} {f.bold}Syntax{f.reset} {f.bold}listen{f.reset} = {f.under}[address]:port [mode=MODE] [delay=DELAY]{f.reset} {f.bold}Default{f.reset} 127.0.0.1:25, 127.0.0.1:587, :::25, :::587 IPv6 listeners are only enabled if IPv6 is supported. {f.bold}Optional{f.reset} {f.under}mode={f.reset} and {f.under}delay={f.reset} -- allows setting a response mode and delay per listener. The {f.under}mode={f.reset} and {f.under}delay={f.reset} flags allow specific ports to act in different ways. i.e. you could accept all mail on 10.0.0.1:25 and bounce it all on 10.0.0.2:25, as below. listen = 10.0.0.1:25 mode=accept, 10.0.0.2:25 mode=bounce The {f.under}mode={f.reset} and {f.under}delay={f.reset} flags may also be specified together, as required. listen = 10.0.0.1:25 mode=accept delay=5, 10.0.0.2:25 mode=bounce delay=10 The flags accept the same options as {f.under}dynamic-switches{f.reset}, including setting a delay range. ---- {f.bold}tls_listen{f.reset} {f.bold}Syntax{f.reset} {f.bold}tls_listen{f.reset} = {f.under}[address]:port [mode=MODE] [delay=DELAY]{f.reset} {f.bold}Default{f.reset} None {f.bold}Optional{f.reset} {f.under}mode={f.reset} and {f.under}delay={f.reset} -- allows setting a response mode and delay per listener. :465 is equivalent to listening on port 465 on all IPv4 addresses and :::465 is equivalent to listening on port 465 on all IPv6 addresses. Multiple addresses and ports can be listed on a single line. tls_listen = 10.0.0.1:465, 10.0.0.2:465, :465, :::465 The {f.under}mode={f.reset} and {f.under}delay={f.reset} flags allow specific ports to act in different ways. i.e. you could accept all mail on 10.0.0.1:465 and bounce it all on 10.0.0.2:465, as below. tls_listen = 10.0.0.1:465 mode=accept, 10.0.0.2:465 mode=bounce The {f.under}mode={f.reset} and {f.under}delay={f.reset} flags may also be specified together, as required. tls_listen = 10.0.0.1:465 mode=accept delay=5, 10.0.0.2:465 mode=bounce delay=10 The flags accept the same options as {f.under}dynamic-switches{f.reset}, including setting a delay range. ---- {f.bold}user{f.reset} {f.bold}Syntax{f.reset} {f.bold}user{f.reset} = {f.under}user{f.reset} {f.bold}Default{f.reset} The current Linux user Blackhole will set it's process owner to the value provided with this option. Ports below 1024 require sudo or root privileges, this option is available so that the process can be started, listen on privileged ports and then give up those privileges. ---- {f.bold}group{f.reset} {f.bold}Syntax{f.reset} {f.bold}group{f.reset} = {f.under}group{f.reset} {f.bold}Default{f.reset} The primary group of the current Linux user Blackhole will set it's process group to the value provided with this option. ---- {f.bold}pidfile{f.reset} {f.bold}Syntax{f.reset} {f.bold}pidfile{f.reset} = {f.under}/path/to/file.pid{f.reset} {f.bold}Default{f.reset} /tmp/blackhole.pid Blackhole will write it's Process ID to this file, allowing you to easily track the process and send signals to it. ---- {f.bold}timeout{f.reset} {f.bold}Syntax{f.reset} {f.bold}timeout{f.reset} = {f.under}seconds{f.reset} {f.bold}Default{f.reset} 60 -- Maximum value of 180 seconds. This is the amount of time to wait for a client to send data. Once the timeout value has been reached with no data being sent by the client, the connection will be terminated and a 421 Timeout message will be sent to the client. Helps mitigate DoS risks. ---- {f.bold}tls_cert{f.reset} {f.bold}Syntax{f.reset} {f.bold}tls_cert{f.reset} = {f.under}/path/to/certificate.pem{f.reset} {f.bold}Default{f.reset} None The certificate file in x509 format for wrapping a connection in SSL/TLS. ---- {f.bold}tls_key{f.reset} {f.bold}Syntax{f.reset} {f.bold}tls_key{f.reset} = {f.under}/path/to/private.key{f.reset} {f.bold}Default{f.reset} None The private key of the tls_cert. ---- {f.bold}tls_dhparams{f.reset} {f.bold}Syntax{f.reset} {f.bold}tls_dhparams{f.reset} = {f.under}/path/to/dhparams.pem{f.reset} {f.bold}Default{f.reset} None File containing Diffie Hellman ephemeral parameters for ECDH ciphers. ---- {f.bold}delay{f.reset} {f.bold}Syntax{f.reset} {f.bold}delay{f.reset} = {f.under}seconds{f.reset} {f.bold}Default{f.reset} 0 -- Maximum value of 60 seconds. Time to delay before returning a response to a completed DATA command. You can use this to delay testing or simulate lag. ---- {f.bold}mode{f.reset} {f.bold}Syntax{f.reset} {f.bold}mode{f.reset} = {f.under}accept | bounce | random{f.reset} {f.bold}Default{f.reset} accept ---- {f.bold}max_message_size{f.reset} {f.bold}Syntax{f.reset} {f.bold}max_message_size{f.reset} = {f.under}bytes{f.reset} {f.bold}Default{f.reset} 512000 Bytes (512 KB) The maximum message size for a message. This includes headers and helps mitigate a DoS risk. ---- {f.bold}dynamic_switch{f.reset} {f.bold}Syntax{f.reset} {f.bold}dynamic_switch{f.reset} = {f.under}true | false{f.reset} {f.bold}Default{f.reset} true The dynamic switch option allows you to enable or disable parsing of dynamic switches from email headers. ---- {f.bold}workers{f.reset} {f.bold}Syntax{f.reset} {f.bold}workers{f.reset} = {f.under}number{f.reset} {f.bold}Default{f.reset} 1 The workers option allows you to define how many worker processes to spawn to handle incoming mail. The absolute minimum is actually 2. Even by setting the workers value to 1, a supervisor process will always exist meaning that you would have 1 worker and a supervisor. '''.format(f=formatting) # noqa # fmt: on