Source code for univention.testing.mail
#
# UCS test
#
# SPDX-FileCopyrightText: 2013-2025 Univention GmbH
# SPDX-License-Identifier: AGPL-3.0-only
import os
import pwd
import subprocess
import sys
import time
from types import TracebackType
from typing import Self
[docs]
class MailSinkGuard:
"""
This class is a simple context manager that stops all attached mail sinks
if the context is left.
with MaiLSinkGuard() as msg:
sink = MailSink(......)
msg.add(sink)
....use sink....
"""
def __init__(self) -> None:
self.mail_sinks: set[MailSink] = set()
[docs]
def add(self, sink: 'MailSink') -> None:
self.mail_sinks.add(sink)
def __enter__(self) -> Self:
return self
def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, etraceback: TracebackType | None) -> None:
for mail_sink in self.mail_sinks:
mail_sink.stop()
[docs]
class MailSink:
"""
This class starts an SMTP sink on the specified address/port.
Each incoming mail will be written to a single file if target_dir is used.
To write all incoming mails into one file, use filename.
>>> ms = MailSink('127.0.0.1', 12345, target_dir='/tmp/')
>>> ms.start()
<do some stuff>
>>> ms.stop()
>>> ms = MailSink('127.0.0.1', 12345, filename='/tmp/sinkfile.eml')
>>> ms.start()
<do some stuff>
>>> ms.stop()
>>> with MailSink('127.0.0.1', 12345, filename='/tmp/sinkfile.eml') as ms:
>>> <do some stuff>
"""
def __init__(self, address: str, port: int, filename: str | None = None, target_dir: str | None = None, fqdn: str | None = None) -> None:
self.address = address
self.port = port
self.filename = filename
self.target_dir = target_dir
self.process: subprocess.Popen | None = None
self.fqdn = fqdn
def __enter__(self) -> Self:
self.start()
return self
def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, etraceback: TracebackType | None) -> None:
self.stop()
[docs]
def start(self) -> None:
print(f'*** Starting SMTPSink at {self.address}:{self.port}')
cmd = ['/usr/sbin/smtp-sink'] # use postfix' smtp-sink tool
if self.filename is not None:
cmd.extend(['-D', self.filename])
elif self.target_dir is not None:
cmd.extend(['-d', os.path.join(self.target_dir, '%Y%m%d-%H%M%S.')])
else:
cmd.extend(['-d', os.path.join('./%Y%m%d-%H%M%S.')])
if self.fqdn:
cmd.extend(['-h', self.fqdn])
if os.geteuid() == 0:
cmd.extend(['-u', pwd.getpwuid(os.getuid()).pw_name])
cmd.append(f'{self.address}:{self.port}')
cmd.append('10')
print(f'*** {cmd!r}')
self.process = subprocess.Popen(cmd, stderr=sys.stdout, stdout=sys.stdout)
[docs]
def stop(self) -> None:
if self.process is not None:
self.process.terminate()
time.sleep(1)
self.process.kill()
print(f'*** SMTPSink at {self.address}:{self.port} stopped')
self.process = None
if __name__ == '__main__':
# ms = MailSink('127.0.0.1', 12345, target_dir='/tmp/')
ms = MailSink('127.0.0.1', 12345, filename='/tmp/sink.eml')
print('Starting sink')
ms.start()
print('Waiting')
time.sleep(25)
print('Stopping sink')
ms.stop()
print('Waiting')
time.sleep(5)