# -*- 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")
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