198 lines
7.3 KiB
Python
198 lines
7.3 KiB
Python
# coding: utf-8
|
|
#
|
|
# Postfix queue control python tool (pymailq)
|
|
#
|
|
# Copyright (C) 2014 Denis Pompilio (jawa) <denis.pompilio@gmail.com>
|
|
#
|
|
# This file is part of pymailq
|
|
#
|
|
# This program is free software; you can redistribute it and/or
|
|
# modify it under the terms of the GNU General Public License
|
|
# as published by the Free Software Foundation; either version 2
|
|
# of the License, or (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, see <http://www.gnu.org/licenses/>.
|
|
|
|
import time
|
|
import subprocess
|
|
from pymailq import CONFIG, debug
|
|
|
|
|
|
class QueueControl(object):
|
|
"""
|
|
Postfix/openSMTPD queue control using postsuper/smtpctl command.
|
|
|
|
The :class:`~control.QueueControl` instance defines the following
|
|
attributes:
|
|
|
|
.. attribute:: use_sudo
|
|
|
|
Boolean to control the use of `sudo` to invoke Postfix command.
|
|
Default is ``False``
|
|
|
|
.. attribute:: postsuper_cmd
|
|
|
|
Postfix command and arguments :func:`list` for mails queue
|
|
administrative operations. Default is ``["postsuper"]``
|
|
|
|
.. attribute:: known_operations
|
|
|
|
Known Postfix administrative operations :class:`dict` to associate
|
|
operations to command arguments. Known associations are::
|
|
|
|
delete: -d
|
|
hold: -h
|
|
release: -H
|
|
requeue: -r
|
|
|
|
.. warning::
|
|
|
|
Default known associations are provided for the default mails
|
|
queue administrative command `postsuper`_.
|
|
|
|
.. seealso::
|
|
|
|
Postfix manual:
|
|
`postsuper`_ -- Postfix superintendent
|
|
"""
|
|
|
|
@staticmethod
|
|
def get_operation_cmd(operation):
|
|
"""Get operation related command from configuration
|
|
|
|
This method use Postfix administrative commands defined
|
|
in :attr:`pymailq.CONFIG` attribute under the key 'list_queue'.
|
|
Command and arguments list is build on call with the configuration data.
|
|
|
|
Command keys are built with the ``operation`` argument suffixed with
|
|
``_message``. Example: ``hold_message`` for the hold command.
|
|
|
|
:param str operation: Operation name
|
|
:return: Command and arguments as :class:`list`
|
|
|
|
:raise KeyError: Operation is unknown
|
|
|
|
.. seealso::
|
|
|
|
:ref:`pymailq-configuration`
|
|
"""
|
|
cmd = CONFIG['commands'][operation + '_message']
|
|
# Dont duplicate sudo
|
|
if CONFIG['commands']['use_sudo'] and cmd[0] != 'sudo':
|
|
cmd.insert(0, 'sudo')
|
|
return cmd
|
|
|
|
@debug
|
|
def _operate(self, operation, messages):
|
|
"""
|
|
Generic method to lead operations messages from postfix mail queue.
|
|
|
|
Operations can be one of Postfix known operations stored in
|
|
PyMailq module configuration.
|
|
|
|
:param str operation: Known operation from :attr:`pymailq.CONFIG`.
|
|
:param list messages: List of :class:`~store.Mail` objects targetted
|
|
for operation.
|
|
:return: Command's *stderr* output lines, return code of binary
|
|
:rtype: :func:`list`
|
|
"""
|
|
# validate that object's attribute "qid" exist. Raise AttributeError.
|
|
for msg in messages:
|
|
getattr(msg, "qid")
|
|
|
|
# We may modify this part to improve security.
|
|
# It should not be possible to inject commands, but who knows...
|
|
# https://www.kevinlondon.com/2015/07/26/dangerous-python-functions.html
|
|
# And consider the use of sh module: https://amoffat.github.io/sh/
|
|
if 'postfix' in CONFIG['core']['smtpd_type']:
|
|
operation_cmd = self.get_operation_cmd(operation) + ['-']
|
|
try:
|
|
child = subprocess.Popen(operation_cmd,
|
|
stdin=subprocess.PIPE,
|
|
stderr=subprocess.PIPE)
|
|
except EnvironmentError as exc:
|
|
command_str = " ".join(operation_cmd)
|
|
error_msg = "Unable to call '%s': %s" % (command_str, str(exc))
|
|
raise RuntimeError(error_msg)
|
|
|
|
# If permissions error, the postsuper process takes ~1s to teardown.
|
|
# Wait this delay and raise error message if process has stopped.
|
|
time.sleep(1.1)
|
|
child.poll()
|
|
if child.returncode:
|
|
raise RuntimeError(child.communicate()[1].strip().decode())
|
|
|
|
try:
|
|
for msg in messages:
|
|
child.stdin.write((msg.qid+'\n').encode())
|
|
stderr = child.communicate()[1].strip()
|
|
except BrokenPipeError:
|
|
raise RuntimeError("Unexpected error: child process has crashed")
|
|
|
|
return [line.strip() for line in stderr.decode().split('\n')], child.returncode
|
|
|
|
# smtpctl do not use stdin, have to call the binary for each message :-(
|
|
else:
|
|
result = []
|
|
for msg in messages:
|
|
operation_cmd = self.get_operation_cmd(operation) + [msg.qid]
|
|
try:
|
|
child = subprocess.Popen(operation_cmd,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE)
|
|
except EnvironmentError as exc:
|
|
command_str = " ".join(operation_cmd)
|
|
error_msg = "Unable to call '%s': %s" % (command_str, str(exc))
|
|
raise RuntimeError(error_msg)
|
|
|
|
stdout, stderr = child.communicate()
|
|
if not len(stdout):
|
|
return [line.strip() for line in stderr.decode().split('\n')], child.returncode
|
|
|
|
result.append(stdout.decode())
|
|
|
|
return result, child.returncode, child.returncode
|
|
|
|
|
|
def delete_messages(self, messages):
|
|
"""
|
|
Delete several messages from postfix mail queue.
|
|
|
|
This method is a :func:`~functools.partial` wrapper on
|
|
:meth:`~control.QueueControl._operate`. Passed operation is ``delete``
|
|
"""
|
|
return self._operate('delete', messages)
|
|
|
|
def hold_messages(self, messages):
|
|
"""
|
|
Hold several messages from postfix mail queue.
|
|
|
|
This method is a :func:`~functools.partial` wrapper on
|
|
:meth:`~control.QueueControl._operate`. Passed operation is ``hold``
|
|
"""
|
|
return self._operate('hold', messages)
|
|
|
|
def release_messages(self, messages):
|
|
"""
|
|
Release several messages from postfix mail queue.
|
|
|
|
This method is a :func:`~functools.partial` wrapper on
|
|
:meth:`~control.QueueControl._operate`. Passed operation is ``release``
|
|
"""
|
|
return self._operate('release', messages)
|
|
|
|
def requeue_messages(self, messages):
|
|
"""
|
|
Requeue several messages from postfix mail queue.
|
|
|
|
This method is a :func:`~functools.partial` wrapper on
|
|
:meth:`~control.QueueControl._operate`. Passed operation is ``requeue``
|
|
"""
|
|
return self._operate('requeue', messages)
|