--- /dev/null
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+@summary: The module for common used functions and global variables.
+
+@author: Frank Brehm
+@contact: frank.brehm@pixelpark.com
+@copyright: © 2024 by Frank Brehm, Berlin
+"""
+from __future__ import absolute_import
+
+# Standard modules
+import datetime
+import logging
+import re
+
+# Third party modules
+from fb_tools.common import timeinterval2delta
+
+# Own modules
+from . import DEFAULT_TZ_OFFSET
+from .errors import DateFormatError
+from .errors import WrongDateIsoformatError
+from .xlate import XLATOR
+
+HAS_GET_LOCALZONE = False
+try:
+ from .tzlocal import get_localzone
+ HAS_GET_LOCALZONE = True
+except ImportError:
+ pass
+
+_ = XLATOR.gettext
+ngettext = XLATOR.ngettext
+
+__version__ = '0.1.0'
+
+LOG = logging.getLogger(__name__)
+
+UTC = utc = datetime.timezone(datetime.timedelta(), 'UTC')
+
+PATTERN_ISODATE = r'(?P<year>\d{4})-?(?P<month>[01]\d)-?(?P<day>[0-3]\d)'
+PATTERN_ISOTIME = (
+ r'(?P<hour>[0-2]\d):?(?P<min>[0-5]\d)(?::?(?P<sec>[0-5]\d)(\.(?P<nsec>\d+))?)?')
+PATTERN_ISOTIMEZONE = r'((?P<utc>Z)|(?P<tz_hours>[+-][01]\d)(?::?(?P<tz_mins>[0-5]\d))?)?'
+
+RE_ISODATETIME = re.compile(PATTERN_ISODATE + r'[T\s]' + PATTERN_ISOTIME + PATTERN_ISOTIMEZONE)
+
+PATTERN_DEFAULT_TZ = r'^\s*(?P<sign>[+-])\s*(?P<offset>.*)'
+RE_DEFAULT_TZ = re.compile(PATTERN_DEFAULT_TZ)
+
+RE_TZ = re.compile(r'^\s*(?P<tz_hours>[01]\d)(?::?(?P<tz_mins>[0-5]\d))?')
+
+DEFAULT_TZ = UTC
+
+# =============================================================================
+def get_default_tz_from_offset(offset=None):
+ """Return the given offset in seconds as a datetime.timezone object."""
+ if offset is None:
+ offset = DEFAULT_TZ_OFFSET
+
+ default_tz = datetime.timedelta()
+
+ if not offset:
+ return default_tz
+
+ if isinstance(offset, (int, float)):
+ return datetime.timezone(datetime.timedelta(seconds=float(offset)))
+
+ offset = str(offset)
+ omatch = RE_DEFAULT_TZ.match(offset)
+ if not omatch:
+ msg = _('Could not interprete {!r} as a timezone offset.').format(offset)
+ raise ValueError(msg)
+
+ sign = omatch['sign']
+ offset_abs = omatch['offset']
+
+ tz_match = RE_TZ.match(offset_abs)
+ if tz_match:
+ res_offset = 0
+ if tz_match['tz_mins']:
+ res_offset = int(tz_match['tz_mins']) * 60
+ res_offset += int(tz_match['tz_hours']) * 3600
+ if sign == '-':
+ res_offset *= -1
+ return datetime.timezone(datetime.timedelta(seconds=res_offset))
+
+ delta = timeinterval2delta(offset_abs)
+ if sign == '-':
+ delta *= -1
+ return datetime.timezone(delta)
+
+
+# =============================================================================
+DEFAULT_TZ = get_default_tz_from_offset(DEFAULT_TZ_OFFSET)
+
+
+# =============================================================================
+def fromisoformat(datestr):
+ """Try to convert a string with an ISO-formatted timestamp into a datetime object."""
+ if hasattr(datetime.datetime, 'fromisoformat'):
+ try:
+ ret = datetime.datetime.fromisoformat(datestr)
+ return ret
+ except ValueError as e:
+ raise DateFormatError(str(e))
+
+ match_obj = RE_ISODATETIME.search(datestr)
+ if not match_obj:
+ raise WrongDateIsoformatError(datestr)
+
+ params = {
+ 'year': int(match_obj['year']),
+ 'month': int(match_obj['month']),
+ 'day': int(match_obj['day']),
+ 'minute': int(match_obj['min']),
+ }
+ if match_obj['sec'] is not None:
+ params['second'] = int(match_obj['sec'])
+
+ if match_obj['nsec'] is not None:
+ params['microsecond'] = int(round(float('0.' + match_obj['nsec']) * 1000000))
+
+ if match_obj['utc']:
+ params['tzinfo'] = UTC
+ elif match_obj['tz_hours'] is not None:
+ prefix = match_obj['tz_hours'][0]
+ offset = 0
+ if match_obj['tz_mins'] is not None:
+ offset = int(match_obj['tz_mins']) * 60
+ offset += int(match_obj['tz_hours'][1:]) * 3600
+ if prefix == '-':
+ offset *= -1
+
+ params['tzinfo'] = datetime.timezone(datetime.timedelta(seconds=offset))
+ else:
+ if HAS_GET_LOCALZONE:
+ params['tzinfo'] = get_localzone()
+ else:
+ params['tzinfo'] = DEFAULT_TZ
+
+ return datetime.datetime(**params)
+
+
+# =============================================================================
+
+if __name__ == '__main__':
+
+ pass
+
+# =============================================================================
+
+# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 list
import datetime
import ipaddress
import logging
-import re
from collections.abc import Sequence
# Third party modules
from fb_tools.common import is_sequence
from fb_tools.common import pp
-from fb_tools.common import timeinterval2delta
from fb_tools.common import to_bool
from fb_tools.errors import InvalidMailAddressError
from fb_tools.mailaddress import MailAddress
from fb_tools.obj import FbGenericBaseObject
# Own modules
-from . import DEFAULT_TZ_OFFSET
+from .common import fromisoformat
from .errors import DateFormatError
-from .errors import WrongDateIsoformatError
+from .postfix_data_pair import DataPair
+from .postfix_deliver_action import DeliverAction
from .xlate import XLATOR
-HAS_GET_LOCALZONE = False
-try:
- from .tzlocal import get_localzone
- HAS_GET_LOCALZONE = True
-except ImportError:
- pass
-
_ = XLATOR.gettext
ngettext = XLATOR.ngettext
-__version__ = '0.9.0'
+__version__ = '0.10.0'
LOG = logging.getLogger(__name__)
-UTC = utc = datetime.timezone(datetime.timedelta(), 'UTC')
-
-PATTERN_ISODATE = r'(?P<year>\d{4})-?(?P<month>[01]\d)-?(?P<day>[0-3]\d)'
-PATTERN_ISOTIME = (
- r'(?P<hour>[0-2]\d):?(?P<min>[0-5]\d)(?::?(?P<sec>[0-5]\d)(\.(?P<nsec>\d+))?)?')
-PATTERN_ISOTIMEZONE = r'((?P<utc>Z)|(?P<tz_hours>[+-][01]\d)(?::?(?P<tz_mins>[0-5]\d))?)?'
-
-RE_ISODATETIME = re.compile(PATTERN_ISODATE + r'[T\s]' + PATTERN_ISOTIME + PATTERN_ISOTIMEZONE)
-
-PATTERN_DEFAULT_TZ = r'^\s*(?P<sign>[+-])\s*(?P<offset>.*)'
-RE_DEFAULT_TZ = re.compile(PATTERN_DEFAULT_TZ)
-
-RE_TZ = re.compile(r'^\s*(?P<tz_hours>[01]\d)(?::?(?P<tz_mins>[0-5]\d))?')
-
-DEFAULT_TZ = UTC
-
-# =============================================================================
-def get_default_tz_from_offset(offset=None):
- """Return the given offset in seconds as a datetime.timezone object."""
- if offset is None:
- offset = DEFAULT_TZ_OFFSET
-
- default_tz = datetime.timedelta()
-
- if not offset:
- return default_tz
-
- if isinstance(offset, (int, float)):
- return datetime.timezone(datetime.timedelta(seconds=float(offset)))
-
- offset = str(offset)
- omatch = RE_DEFAULT_TZ.match(offset)
- if not omatch:
- msg = _('Could not interprete {!r} as a timezone offset.').format(offset)
- raise ValueError(msg)
-
- sign = omatch['sign']
- offset_abs = omatch['offset']
-
- tz_match = RE_TZ.match(offset_abs)
- if tz_match:
- res_offset = 0
- if tz_match['tz_mins']:
- res_offset = int(tz_match['tz_mins']) * 60
- res_offset += int(tz_match['tz_hours']) * 3600
- if sign == '-':
- res_offset *= -1
- return datetime.timezone(datetime.timedelta(seconds=res_offset))
-
- delta = timeinterval2delta(offset_abs)
- if sign == '-':
- delta *= -1
- return datetime.timezone(delta)
-
-
-# =============================================================================
-DEFAULT_TZ = get_default_tz_from_offset(DEFAULT_TZ_OFFSET)
-
-
-# =============================================================================
-def fromisoformat(datestr):
- """Try to convert a string with an ISO-formatted timestamp into a datetime object."""
- if hasattr(datetime.datetime, 'fromisoformat'):
- try:
- ret = datetime.datetime.fromisoformat(datestr)
- return ret
- except ValueError as e:
- raise DateFormatError(str(e))
-
- match_obj = RE_ISODATETIME.search(datestr)
- if not match_obj:
- raise WrongDateIsoformatError(datestr)
-
- params = {
- 'year': int(match_obj['year']),
- 'month': int(match_obj['month']),
- 'day': int(match_obj['day']),
- 'minute': int(match_obj['min']),
- }
- if match_obj['sec'] is not None:
- params['second'] = int(match_obj['sec'])
-
- if match_obj['nsec'] is not None:
- params['microsecond'] = int(round(float('0.' + match_obj['nsec']) * 1000000))
-
- if match_obj['utc']:
- params['tzinfo'] = UTC
- elif match_obj['tz_hours'] is not None:
- prefix = match_obj['tz_hours'][0]
- offset = 0
- if match_obj['tz_mins'] is not None:
- offset = int(match_obj['tz_mins']) * 60
- offset += int(match_obj['tz_hours'][1:]) * 3600
- if prefix == '-':
- offset *= -1
-
- params['tzinfo'] = datetime.timezone(datetime.timedelta(seconds=offset))
- else:
- if HAS_GET_LOCALZONE:
- params['tzinfo'] = get_localzone()
- else:
- params['tzinfo'] = DEFAULT_TZ
-
- return datetime.datetime(**params)
-
-
-# =============================================================================
-class DataPair(object):
- """Encapsulates a pair of to integer values in the form of 'value of total'."""
-
- re_data = re.compile(r'^\s*(?P<val>\d+)(?:\s*/\s*(?P<total>\d+))?\s*$')
-
- # -------------------------------------------------------------------------
- def __init__(self, value, total=None):
- """Initialize this object."""
- self.value = int(value)
- if self.value < 0:
- msg = _(
- 'The first value {v!r} of {c} must be greater than or equal to null.').format(
- v=value, c=self.__class__.__name__)
- raise ValueError(msg)
-
- self.total = None
- if total is not None:
- self.total = int(total)
- if self.total < self.value:
- msg = _(
- 'The total value {t!r} must be greater than or equal to the '
- 'value {v}.').format(t=total, v=self.value)
- raise ValueError(msg)
-
- # -------------------------------------------------------------------------
- @classmethod
- def from_str(cls, value):
- """Convert a string of format '2' or '0/2' into a DataPair object."""
- m = cls.re_data.match(value)
- if not m:
- msg = _('Invalid value {v!r} of a {c}.').format(v=value, c=__class__.__name__)
- raise ValueError(msg)
- pair = cls(value=m['val'], total=m['total'])
- return pair
-
- # -------------------------------------------------------------------------
- def __str__(self):
- """Typecast into a string object."""
- if self.total is None:
- return str(self.value)
- return '{}/{}'.format(self.value, self.total)
-
- # -------------------------------------------------------------------------
- def __repr__(self):
- """Typecast into a string for reproduction."""
- out = '<%s(' % (self.__class__.__name__)
-
- fields = []
- fields.append('value={!r}'.format(self.value))
- if self.total is not None:
- fields.append('total={!r}'.format(self.total))
-
- out += ', '.join(fields) + ')>'
- return out
-
- # -------------------------------------------------------------------------
- def __copy__(self):
- """Copy the current data pair into a new object."""
- return self.__class__(self.value, total=self.total)
-
-
-# ==============================================================================
-class DeliverAction(FbGenericBaseObject):
- """A class encapsulating the logged action of a Postix deliverer."""
-
- warn_on_parse_error = False
-
- attributes = (
- 'command', 'to_address', 'origin_to', 'pid', 'date', 'relay', 'delay_total',
- 'time_before_queue', 'time_in_queue', 'time_conn_setup', 'time_xmission',
- 'dsn', 'status', 'message', 'remote_id',
- )
-
- re_to_address = re.compile(r'\bto=<(?P<to>[^>]*)>(?:,\s+)', re.IGNORECASE)
- re_orig_to_address = re.compile(r'orig_to=<(?P<orig>[^>]*)>(?:,\s+)', re.IGNORECASE)
- re_relay = re.compile(r'relay=(?P<relay>\S+)(?:,\s)', re.IGNORECASE)
- re_delay = re.compile(r'delay=(?P<delay>\d+(?:\.\d*)?)(?:,\s+)', re.IGNORECASE)
-
- pat_delays = r'delays='
- pat_delays += r'(?P<p1>\d+(?:\.\d*)?)/(?P<p2>\d+(?:\.\d*)?)'
- pat_delays += r'/(?P<p3>\d+(?:\.\d*)?)/(?P<p4>\d+(?:\.\d*)?)'
- pat_delays += r'(?:,\s+)'
- re_delays = re.compile(pat_delays, re.IGNORECASE)
-
- re_dsn = re.compile(r'dsn=(?P<dsn>\S+)(?:,\s+)', re.IGNORECASE)
- re_status = re.compile(r'status=(?P<status>\S+)\s+', re.IGNORECASE)
-
- pat_remote_id = r'(?:queued\s+as\s+(?P<pf_id>[0-9a-f]+))'
- pat_remote_id += r'|(?:InternalId=(?P<jira_id>\d+))'
- pat_remote_id += r'|(?:\sid=(?P<raw_id>\S+))'
- pat_remote_id += r'|(?:Message\s+(?P<msg1_id>[0-9a-f]+)\s+accepted)'
- pat_remote_id += r'|(?:\s(?P<msg2_id>\S+)\s+(?:Message|mail)\s+accepted)'
- pat_remote_id += r'|(?:\sOK\s+\d+\s+(?P<gsmtp_id>S+)\s+-\s+gsmtp)'
- pat_remote_id += r'|(?:\sok\s+\((?P<msg3_id>\S+)\))'
- pat_remote_id += r'|(?:\sok\s+(?P<msg4_id>\S+))'
-
- re_remote_id = re.compile(pat_remote_id, re.IGNORECASE)
-
- re_relay_address = re.compile(r'\[(?P<addr>[^\]]+)\]')
-
- # -------------------------------------------------------------------------
- def __init__(self, **kwargs):
- """Initialize this object."""
- for attr in self.attributes:
- priv_name = '_' + attr
- setattr(self, priv_name, None)
-
- for attr in kwargs.keys():
- if attr not in self.attributes:
- msg = _('Unknown parameter {p!r} on calling {c}.__init__().').format(
- p=attr, c=self.__class__.__name__)
- raise AttributeError(msg)
- setattr(self, attr, kwargs[attr])
-
- # -----------------------------------------------------------
- @property
- def command(self):
- """Return the command to deliver the mail."""
- return self._command
-
- @command.setter
- def command(self, value):
- if value is None:
- self._command = None
- return
-
- val = str(value).strip()
- if val == '':
- self._command = None
- return
- self._command = val
-
- # -----------------------------------------------------------
- @property
- def date(self):
- """Return the timestamp of the delivering action."""
- return self._date
-
- @date.setter
- def date(self, value):
- if value is None:
- self._date = None
- return
-
- if isinstance(value, datetime.datetime):
- self._date = value
- return
-
- val = str(value).strip()
- if val == '':
- self._date = None
- return
-
- try:
- date = fromisoformat(val)
- except DateFormatError as e:
- msg = _('Could not interprete date {!r}:').format(val) + ' ' + str(e)
- if self.warn_on_parse_error:
- LOG.warn(msg)
- else:
- LOG.debug(msg)
- date = None
- if date:
- self._date = date
- return
- self._date = val
-
- # ----------------------------------------------------------
- @property
- def delay_total(self):
- """Return the total delay of the sended mail ion the current postfix."""
- return self._delay_total
-
- @delay_total.setter
- def delay_total(self, value):
- if value is None:
- self._delay_total = None
- return
-
- if isinstance(value, (float, int)):
- self._delay_total = float(value)
- return
- val = str(value).strip()
- if val == '':
- self._delay_total = None
- return
-
- try:
- self._delay_total = float(val)
- except ValueError as e:
- msg = _('Could not interprete total delay {a!r}: {e}').format(a=value, e=e)
- if self.warn_on_parse_error:
- LOG.warn(msg)
- else:
- LOG.debug(msg)
- self._delay_total = val
-
- # -----------------------------------------------------------
- @property
- def dsn(self):
- """Return the Delivery Status Notification (DSN) information."""
- return self._dsn
-
- @dsn.setter
- def dsn(self, value):
- if value is None:
- self._dsn = None
- return
-
- val = str(value).strip()
- if val == '':
- self._dsn = None
- return
- self._dsn = val
-
- # -----------------------------------------------------------
- @property
- def message(self):
- """Return the verbose message of the delivering transaction."""
- return self._message
-
- @message.setter
- def message(self, value):
- if value is None:
- self._message = None
- return
-
- val = str(value).strip()
- if val == '':
- self._message = None
- return
- self._message = val
-
- # -----------------------------------------------------------
- @property
- def origin_to(self):
- """Return the original RCPT TO address in the delivering dialogue envelope."""
- return self._origin_to
-
- @origin_to.setter
- def origin_to(self, value):
- if value is None:
- self._origin_to = None
- return
-
- if isinstance(value, MailAddress):
- self._origin_to = copy.copy(value)
- return
- val = str(value).strip()
- if val == '':
- self._origin_to = None
- return
-
- try:
- self._origin_to = MailAddress(val)
- except InvalidMailAddressError as e:
- msg = _('Could not interprete to address {a!r}: {e}').format(a=val, e=e)
- if self.warn_on_parse_error:
- LOG.warn(msg)
- else:
- LOG.debug(msg)
- self._origin_to = val
-
- # -----------------------------------------------------------
- @property
- def relay(self):
- """Return the socket address of the receiving relay MTA."""
- return self._relay
-
- @relay.setter
- def relay(self, value):
- if value is None:
- self._relay = None
- return
-
- val = str(value).strip()
- if val == '':
- self._relay = None
- return
- self._relay = val
-
- # -----------------------------------------------------------
- @property
- def relay_address(self):
- """Return the IP address of an existing relay for SMTP."""
- if not self.relay:
- return None
-
- m = self.re_relay_address.search(self.relay)
- if not m:
- # Did not found IP address in SMTP relay.
- return None
-
- try:
- addr = ipaddress.ip_address(m['addr'])
- except ValueError as e:
- msg = _('Could not interprete relay address {a!r}: {e}').format(a=m['addr'], e=e)
- if self.warn_on_parse_error:
- LOG.warn(msg)
- else:
- LOG.debug(msg)
- return None
-
- return addr
-
- # -----------------------------------------------------------
- @property
- def remote_id(self):
- """Return the the Mail ID of the remote (receiving) MTA."""
- return self._remote_id
-
- @remote_id.setter
- def remote_id(self, value):
- if value is None:
- self._remote_id = None
- return
-
- val = str(value).strip()
- if val == '':
- self._remote_id = None
- return
- self._remote_id = val
-
- # ----------------------------------------------------------
- @property
- def pid(self):
- """Return the process ID (PID) of the sending delivering process."""
- return self._pid
-
- @pid.setter
- def pid(self, value):
- if value is None:
- self._pid = None
- return
-
- if isinstance(value, (float, int)):
- self._pid = int(value)
- return
- val = str(value).strip()
- if val == '':
- self._pid = None
- return
-
- try:
- self._pid = int(val)
- except ValueError as e:
- msg = _('Could not interprete PID of deliverer {a!r}: {e}').format(a=value, e=e)
- if self.warn_on_parse_error:
- LOG.warn(msg)
- else:
- LOG.debug(msg)
- self._pid = val
-
- # -----------------------------------------------------------
- @property
- def status(self):
- """Return the final status of the deliverer transaction."""
- return self._status
-
- @status.setter
- def status(self, value):
- if value is None:
- self._status = None
- return
-
- val = str(value).strip()
- if val == '':
- self._status = None
- return
- self._status = val
-
- # ----------------------------------------------------------
- @property
- def time_before_queue(self):
- """Return the used time before the mail was queued."""
- return self._time_before_queue
-
- @time_before_queue.setter
- def time_before_queue(self, value):
- if value is None:
- self._time_before_queue = None
- return
-
- if isinstance(value, (float, int)):
- self._time_before_queue = float(value)
- return
- val = str(value).strip()
- if val == '':
- self._time_before_queue = None
- return
-
- try:
- self._time_before_queue = float(val)
- except ValueError as e:
- msg = _('Could not interprete time before queueing {a!r}: {e}').format(a=value, e=e)
- if self.warn_on_parse_error:
- LOG.warn(msg)
- else:
- LOG.debug(msg)
- self._time_before_queue = val
-
- # ----------------------------------------------------------
- @property
- def time_conn_setup(self):
- """
- Return the time the deliverer process needed to establish a SMTP connection.
-
- This is including DNS, HELO and TLS.
- """
- return self._time_conn_setup
-
- @time_conn_setup.setter
- def time_conn_setup(self, value):
- if value is None:
- self._time_conn_setup = None
- return
-
- if isinstance(value, (float, int)):
- self._time_conn_setup = float(value)
- return
- val = str(value).strip()
- if val == '':
- self._time_conn_setup = None
- return
-
- try:
- self._time_conn_setup = float(val)
- except ValueError as e:
- msg = _('Could not interprete smtp connection setup time {a!r}: {e}').format(
- a=value, e=e)
- if self.warn_on_parse_error:
- LOG.warn(msg)
- else:
- LOG.debug(msg)
- self._time_conn_setup = val
-
- # ----------------------------------------------------------
- @property
- def time_in_queue(self):
- """Return the time the mail was held in the queue."""
- return self._time_in_queue
-
- @time_in_queue.setter
- def time_in_queue(self, value):
- if value is None:
- self._time_in_queue = None
- return
-
- if isinstance(value, (float, int)):
- self._time_in_queue = float(value)
- return
- val = str(value).strip()
- if val == '':
- self._time_in_queue = None
- return
-
- try:
- self._time_in_queue = float(val)
- except ValueError as e:
- msg = _('Could not interprete time in queue {a!r}: {e}').format(a=value, e=e)
- if self.warn_on_parse_error:
- LOG.warn(msg)
- else:
- LOG.debug(msg)
- self._time_in_queue = val
-
- # ----------------------------------------------------------
- @property
- def time_xmission(self):
- """Return the time the smtp process needed to transmit the mail."""
- return self._time_xmission
-
- @time_xmission.setter
- def time_xmission(self, value):
- if value is None:
- self._time_xmission = None
- return
-
- if isinstance(value, (float, int)):
- self._time_xmission = float(value)
- return
- val = str(value).strip()
- if val == '':
- self._time_xmission = None
- return
-
- try:
- self._time_xmission = float(val)
- except ValueError as e:
- msg = _('Could not interprete smtp transmission time {a!r}: {e}').format(a=value, e=e)
- if self.warn_on_parse_error:
- LOG.warn(msg)
- else:
- LOG.debug(msg)
- self._time_xmission = val
-
- # -----------------------------------------------------------
- @property
- def to_address(self):
- """Return the RCPT TO address in the deliverer dialogue envelope."""
- return self._to_address
-
- @to_address.setter
- def to_address(self, value):
- if value is None:
- self._to_address = None
- return
-
- if isinstance(value, MailAddress):
- self._to_address = copy.copy(value)
- return
- val = str(value).strip()
- if val == '':
- self._to_address = None
- return
-
- try:
- self._to_address = MailAddress(val)
- except InvalidMailAddressError as e:
- msg = _('Could not interprete to address {a!r}: {e}').format(a=val, e=e)
- if self.warn_on_parse_error:
- LOG.warn(msg)
- else:
- LOG.debug(msg)
- self._to_address = val
-
- # -------------------------------------------------------------------------
- def __str__(self):
- """Typecast into a string object.
-
- @return: structure as string
- @rtype: str
- """
- return pp(self.as_dict(exportable=True))
-
- # -------------------------------------------------------------------------
- def __repr__(self):
- """Typecast into a string for reproduction."""
- out = '<%s(' % (self.__class__.__name__)
-
- fields = []
-
- attr_dict = self.as_dict(exportable=True)
- for attr in attr_dict.keys():
- value = attr_dict[attr]
- if value is None:
- continue
- fields.append(f'{attr}={value!r}')
-
- if fields:
- out += ', '.join(fields)
-
- out += ')>'
- return out
-
- # -------------------------------------------------------------------------
- def as_dict(self, short=True, exportable=False):
- """
- Transform the elements of the object into a dict.
-
- @param short: don't include local properties in resulting dict.
- @type short: bool
-
- @return: structure as dict
- @rtype: dict
- """
- if exportable:
- res = {}
- else:
- res = super(DeliverAction, self).as_dict(short=short)
-
- for attrib in self.attributes:
- if not hasattr(self, attrib):
- continue
- value = getattr(self, attrib, None)
- if value is None:
- res[attrib] = None
- continue
- if isinstance(value, ipaddress._BaseAddress):
- res[attrib] = str(value) if exportable else value
- continue
- if isinstance(value, datetime.datetime):
- res[attrib] = value.isoformat(' ') if exportable else value
- continue
- if isinstance(value, (DataPair, MailAddress)):
- res[attrib] = str(value) if exportable else value
- continue
-
- # Catch all
- res[attrib] = value
-
- if not exportable:
- res['relay_address'] = self.relay_address
-
- return res
-
- # -------------------------------------------------------------------------
- def __copy__(self):
- """Copy the current chain data into a new object."""
- params = {}
-
- for attrib in self.attributes:
- value = getattr(self, attrib, None)
- if value is None:
- continue
- params[attrib] = copy.copy(value)
-
- return self.__class__(**params)
-
- # -------------------------------------------------------------------------
- @classmethod
- def _get_remote_mailid(cls, message):
- rmatch = cls.re_remote_id.search(message)
- if not rmatch:
- return None
-
- if rmatch['pf_id'] is not None:
- return rmatch['pf_id']
- elif rmatch['jira_id'] is not None:
- return rmatch['jira_id']
- elif rmatch['raw_id'] is not None:
- return rmatch['raw_id']
- elif rmatch['msg1_id'] is not None:
- return rmatch['msg1_id']
- elif rmatch['msg2_id'] is not None:
- return rmatch['msg2_id']
- elif rmatch['gsmtp_id'] is not None:
- return rmatch['gsmtp_id']
- elif rmatch['msg3_id'] is not None:
- return rmatch['msg3_id']
- elif rmatch['msg4_id'] is not None:
- return rmatch['msg4_id']
-
- return None
-
- # -------------------------------------------------------------------------
- @classmethod
- def from_log_entry(cls, timestamp, command, pid, message, verbose=0):
- """Try to create a DeliverAction from the given Postfix log entry."""
- action = cls()
- action.date = timestamp
- action.command = command
- action.pid = pid
- cur_msg = message
-
- if verbose > 2:
- LOG.debug(f'Parsing {command} delivering line: {message}')
-
- rmatch = cls.re_to_address.search(cur_msg)
- if rmatch:
- action.to_address = rmatch['to']
- cur_msg = cls.re_to_address.sub('', cur_msg)
-
- rmatch = cls.re_orig_to_address.search(cur_msg)
- if rmatch:
- action.origin_to = rmatch['orig']
- cur_msg = cls.re_orig_to_address.sub('', cur_msg)
-
- rmatch = cls.re_relay.search(cur_msg)
- if rmatch:
- action.relay = rmatch['relay']
- cur_msg = cls.re_relay.sub('', cur_msg)
-
- rmatch = cls.re_delay.search(cur_msg)
- if rmatch:
- action.delay_total = rmatch['delay']
- cur_msg = cls.re_delay.sub('', cur_msg)
-
- rmatch = cls.re_delays.search(cur_msg)
- if rmatch:
- action.time_before_queue = rmatch['p1']
- action.time_in_queue = rmatch['p2']
- action.time_conn_setup = rmatch['p3']
- action.time_xmission = rmatch['p4']
- cur_msg = cls.re_delays.sub('', cur_msg)
-
- rmatch = cls.re_dsn.search(cur_msg)
- if rmatch:
- action.dsn = rmatch['dsn']
- cur_msg = cls.re_dsn.sub('', cur_msg)
-
- rmatch = cls.re_status.search(cur_msg)
- if rmatch:
- action.status = rmatch['status']
- cur_msg = cls.re_status.sub('', cur_msg)
-
- cur_msg = cur_msg.strip()
- if cur_msg:
- if cur_msg.startswith('(') and cur_msg.endswith(')'):
- cur_msg = cur_msg[1:-1]
- if cur_msg:
- action.message = cur_msg
- remote_id = cls._get_remote_mailid(cur_msg)
- if remote_id:
- action.remote_id = remote_id
-
- return action
# ==============================================================================
class PostfixLogchainInfo(FbGenericBaseObject):
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+@summary: Module for class DataPair, used in postfix loggings.
+
+@author: Frank Brehm
+@contact: frank.brehm@pixelpark.com
+@copyright: © 2024 by Frank Brehm, Berlin
+"""
+from __future__ import absolute_import
+
+# Standard modules
+import logging
+import re
+
+# Third party modules
+
+# Own modules
+from .xlate import XLATOR
+
+_ = XLATOR.gettext
+ngettext = XLATOR.ngettext
+
+__version__ = '0.1.0'
+
+LOG = logging.getLogger(__name__)
+
+
+# =============================================================================
+class DataPair(object):
+ """Encapsulates a pair of to integer values in the form of 'value of total'."""
+
+ re_data = re.compile(r'^\s*(?P<val>\d+)(?:\s*/\s*(?P<total>\d+))?\s*$')
+
+ # -------------------------------------------------------------------------
+ def __init__(self, value, total=None):
+ """Initialize this object."""
+ self.value = int(value)
+ if self.value < 0:
+ msg = _(
+ 'The first value {v!r} of {c} must be greater than or equal to null.').format(
+ v=value, c=self.__class__.__name__)
+ raise ValueError(msg)
+
+ self.total = None
+ if total is not None:
+ self.total = int(total)
+ if self.total < self.value:
+ msg = _(
+ 'The total value {t!r} must be greater than or equal to the '
+ 'value {v}.').format(t=total, v=self.value)
+ raise ValueError(msg)
+
+ # -------------------------------------------------------------------------
+ @classmethod
+ def from_str(cls, value):
+ """Convert a string of format '2' or '0/2' into a DataPair object."""
+ m = cls.re_data.match(value)
+ if not m:
+ msg = _('Invalid value {v!r} of a {c}.').format(v=value, c=__class__.__name__)
+ raise ValueError(msg)
+ pair = cls(value=m['val'], total=m['total'])
+ return pair
+
+ # -------------------------------------------------------------------------
+ def __str__(self):
+ """Typecast into a string object."""
+ if self.total is None:
+ return str(self.value)
+ return '{}/{}'.format(self.value, self.total)
+
+ # -------------------------------------------------------------------------
+ def __repr__(self):
+ """Typecast into a string for reproduction."""
+ out = '<%s(' % (self.__class__.__name__)
+
+ fields = []
+ fields.append('value={!r}'.format(self.value))
+ if self.total is not None:
+ fields.append('total={!r}'.format(self.total))
+
+ out += ', '.join(fields) + ')>'
+ return out
+
+ # -------------------------------------------------------------------------
+ def __copy__(self):
+ """Copy the current data pair into a new object."""
+ return self.__class__(self.value, total=self.total)
+
+
+# ==============# =============================================================================
+
+if __name__ == '__main__':
+
+ pass
+
+# =============================================================================
+
+# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 list
--- /dev/null
+# -*- coding: utf-8 -*-
+"""
+@summary: A class for encapsulating a delivering action in a Postfix transaction chain.
+
+@author: Frank Brehm
+@contact: frank.brehm@pixelpark.com
+@copyright: © 2024 by Frank Brehm, Berlin
+"""
+from __future__ import absolute_import
+
+# Standard modules
+import copy
+import datetime
+import ipaddress
+import logging
+import re
+
+# Third party modules
+from fb_tools.common import pp
+from fb_tools.errors import InvalidMailAddressError
+from fb_tools.mailaddress import MailAddress
+from fb_tools.obj import FbGenericBaseObject
+
+# Own modules
+from .common import fromisoformat
+from .errors import DateFormatError
+from .postfix_data_pair import DataPair
+from .xlate import XLATOR
+
+_ = XLATOR.gettext
+ngettext = XLATOR.ngettext
+
+__version__ = '0.1.0'
+
+LOG = logging.getLogger(__name__)
+
+
+# ==============================================================================
+class DeliverAction(FbGenericBaseObject):
+ """A class encapsulating the logged action of a Postix deliverer."""
+
+ warn_on_parse_error = False
+
+ attributes = (
+ 'command', 'to_address', 'origin_to', 'pid', 'date', 'relay', 'delay_total',
+ 'time_before_queue', 'time_in_queue', 'time_conn_setup', 'time_xmission',
+ 'dsn', 'status', 'message', 'remote_id',
+ )
+
+ re_to_address = re.compile(r'\bto=<(?P<to>[^>]*)>(?:,\s+)', re.IGNORECASE)
+ re_orig_to_address = re.compile(r'orig_to=<(?P<orig>[^>]*)>(?:,\s+)', re.IGNORECASE)
+ re_relay = re.compile(r'relay=(?P<relay>\S+)(?:,\s)', re.IGNORECASE)
+ re_delay = re.compile(r'delay=(?P<delay>\d+(?:\.\d*)?)(?:,\s+)', re.IGNORECASE)
+
+ pat_delays = r'delays='
+ pat_delays += r'(?P<p1>\d+(?:\.\d*)?)/(?P<p2>\d+(?:\.\d*)?)'
+ pat_delays += r'/(?P<p3>\d+(?:\.\d*)?)/(?P<p4>\d+(?:\.\d*)?)'
+ pat_delays += r'(?:,\s+)'
+ re_delays = re.compile(pat_delays, re.IGNORECASE)
+
+ re_dsn = re.compile(r'dsn=(?P<dsn>\S+)(?:,\s+)', re.IGNORECASE)
+ re_status = re.compile(r'status=(?P<status>\S+)\s+', re.IGNORECASE)
+
+ pat_remote_id = r'(?:queued\s+as\s+(?P<pf_id>[0-9a-f]+))'
+ pat_remote_id += r'|(?:InternalId=(?P<jira_id>\d+))'
+ pat_remote_id += r'|(?:\sid=(?P<raw_id>\S+))'
+ pat_remote_id += r'|(?:Message\s+(?P<msg1_id>[0-9a-f]+)\s+accepted)'
+ pat_remote_id += r'|(?:\s(?P<msg2_id>\S+)\s+(?:Message|mail)\s+accepted)'
+ pat_remote_id += r'|(?:\sOK\s+\d+\s+(?P<gsmtp_id>S+)\s+-\s+gsmtp)'
+ pat_remote_id += r'|(?:\sok\s+\((?P<msg3_id>\S+)\))'
+ pat_remote_id += r'|(?:\sok\s+(?P<msg4_id>\S+))'
+
+ re_remote_id = re.compile(pat_remote_id, re.IGNORECASE)
+
+ re_relay_address = re.compile(r'\[(?P<addr>[^\]]+)\]')
+
+ # -------------------------------------------------------------------------
+ def __init__(self, **kwargs):
+ """Initialize this object."""
+ for attr in self.attributes:
+ priv_name = '_' + attr
+ setattr(self, priv_name, None)
+
+ for attr in kwargs.keys():
+ if attr not in self.attributes:
+ msg = _('Unknown parameter {p!r} on calling {c}.__init__().').format(
+ p=attr, c=self.__class__.__name__)
+ raise AttributeError(msg)
+ setattr(self, attr, kwargs[attr])
+
+ # -----------------------------------------------------------
+ @property
+ def command(self):
+ """Return the command to deliver the mail."""
+ return self._command
+
+ @command.setter
+ def command(self, value):
+ if value is None:
+ self._command = None
+ return
+
+ val = str(value).strip()
+ if val == '':
+ self._command = None
+ return
+ self._command = val
+
+ # -----------------------------------------------------------
+ @property
+ def date(self):
+ """Return the timestamp of the delivering action."""
+ return self._date
+
+ @date.setter
+ def date(self, value):
+ if value is None:
+ self._date = None
+ return
+
+ if isinstance(value, datetime.datetime):
+ self._date = value
+ return
+
+ val = str(value).strip()
+ if val == '':
+ self._date = None
+ return
+
+ try:
+ date = fromisoformat(val)
+ except DateFormatError as e:
+ msg = _('Could not interprete date {!r}:').format(val) + ' ' + str(e)
+ if self.warn_on_parse_error:
+ LOG.warn(msg)
+ else:
+ LOG.debug(msg)
+ date = None
+ if date:
+ self._date = date
+ return
+ self._date = val
+
+ # ----------------------------------------------------------
+ @property
+ def delay_total(self):
+ """Return the total delay of the sended mail ion the current postfix."""
+ return self._delay_total
+
+ @delay_total.setter
+ def delay_total(self, value):
+ if value is None:
+ self._delay_total = None
+ return
+
+ if isinstance(value, (float, int)):
+ self._delay_total = float(value)
+ return
+ val = str(value).strip()
+ if val == '':
+ self._delay_total = None
+ return
+
+ try:
+ self._delay_total = float(val)
+ except ValueError as e:
+ msg = _('Could not interprete total delay {a!r}: {e}').format(a=value, e=e)
+ if self.warn_on_parse_error:
+ LOG.warn(msg)
+ else:
+ LOG.debug(msg)
+ self._delay_total = val
+
+ # -----------------------------------------------------------
+ @property
+ def dsn(self):
+ """Return the Delivery Status Notification (DSN) information."""
+ return self._dsn
+
+ @dsn.setter
+ def dsn(self, value):
+ if value is None:
+ self._dsn = None
+ return
+
+ val = str(value).strip()
+ if val == '':
+ self._dsn = None
+ return
+ self._dsn = val
+
+ # -----------------------------------------------------------
+ @property
+ def message(self):
+ """Return the verbose message of the delivering transaction."""
+ return self._message
+
+ @message.setter
+ def message(self, value):
+ if value is None:
+ self._message = None
+ return
+
+ val = str(value).strip()
+ if val == '':
+ self._message = None
+ return
+ self._message = val
+
+ # -----------------------------------------------------------
+ @property
+ def origin_to(self):
+ """Return the original RCPT TO address in the delivering dialogue envelope."""
+ return self._origin_to
+
+ @origin_to.setter
+ def origin_to(self, value):
+ if value is None:
+ self._origin_to = None
+ return
+
+ if isinstance(value, MailAddress):
+ self._origin_to = copy.copy(value)
+ return
+ val = str(value).strip()
+ if val == '':
+ self._origin_to = None
+ return
+
+ try:
+ self._origin_to = MailAddress(val)
+ except InvalidMailAddressError as e:
+ msg = _('Could not interprete to address {a!r}: {e}').format(a=val, e=e)
+ if self.warn_on_parse_error:
+ LOG.warn(msg)
+ else:
+ LOG.debug(msg)
+ self._origin_to = val
+
+ # -----------------------------------------------------------
+ @property
+ def relay(self):
+ """Return the socket address of the receiving relay MTA."""
+ return self._relay
+
+ @relay.setter
+ def relay(self, value):
+ if value is None:
+ self._relay = None
+ return
+
+ val = str(value).strip()
+ if val == '':
+ self._relay = None
+ return
+ self._relay = val
+
+ # -----------------------------------------------------------
+ @property
+ def relay_address(self):
+ """Return the IP address of an existing relay for SMTP."""
+ if not self.relay:
+ return None
+
+ m = self.re_relay_address.search(self.relay)
+ if not m:
+ # Did not found IP address in SMTP relay.
+ return None
+
+ try:
+ addr = ipaddress.ip_address(m['addr'])
+ except ValueError as e:
+ msg = _('Could not interprete relay address {a!r}: {e}').format(a=m['addr'], e=e)
+ if self.warn_on_parse_error:
+ LOG.warn(msg)
+ else:
+ LOG.debug(msg)
+ return None
+
+ return addr
+
+ # -----------------------------------------------------------
+ @property
+ def remote_id(self):
+ """Return the the Mail ID of the remote (receiving) MTA."""
+ return self._remote_id
+
+ @remote_id.setter
+ def remote_id(self, value):
+ if value is None:
+ self._remote_id = None
+ return
+
+ val = str(value).strip()
+ if val == '':
+ self._remote_id = None
+ return
+ self._remote_id = val
+
+ # ----------------------------------------------------------
+ @property
+ def pid(self):
+ """Return the process ID (PID) of the sending delivering process."""
+ return self._pid
+
+ @pid.setter
+ def pid(self, value):
+ if value is None:
+ self._pid = None
+ return
+
+ if isinstance(value, (float, int)):
+ self._pid = int(value)
+ return
+ val = str(value).strip()
+ if val == '':
+ self._pid = None
+ return
+
+ try:
+ self._pid = int(val)
+ except ValueError as e:
+ msg = _('Could not interprete PID of deliverer {a!r}: {e}').format(a=value, e=e)
+ if self.warn_on_parse_error:
+ LOG.warn(msg)
+ else:
+ LOG.debug(msg)
+ self._pid = val
+
+ # -----------------------------------------------------------
+ @property
+ def status(self):
+ """Return the final status of the deliverer transaction."""
+ return self._status
+
+ @status.setter
+ def status(self, value):
+ if value is None:
+ self._status = None
+ return
+
+ val = str(value).strip()
+ if val == '':
+ self._status = None
+ return
+ self._status = val
+
+ # ----------------------------------------------------------
+ @property
+ def time_before_queue(self):
+ """Return the used time before the mail was queued."""
+ return self._time_before_queue
+
+ @time_before_queue.setter
+ def time_before_queue(self, value):
+ if value is None:
+ self._time_before_queue = None
+ return
+
+ if isinstance(value, (float, int)):
+ self._time_before_queue = float(value)
+ return
+ val = str(value).strip()
+ if val == '':
+ self._time_before_queue = None
+ return
+
+ try:
+ self._time_before_queue = float(val)
+ except ValueError as e:
+ msg = _('Could not interprete time before queueing {a!r}: {e}').format(a=value, e=e)
+ if self.warn_on_parse_error:
+ LOG.warn(msg)
+ else:
+ LOG.debug(msg)
+ self._time_before_queue = val
+
+ # ----------------------------------------------------------
+ @property
+ def time_conn_setup(self):
+ """
+ Return the time the deliverer process needed to establish a SMTP connection.
+
+ This is including DNS, HELO and TLS.
+ """
+ return self._time_conn_setup
+
+ @time_conn_setup.setter
+ def time_conn_setup(self, value):
+ if value is None:
+ self._time_conn_setup = None
+ return
+
+ if isinstance(value, (float, int)):
+ self._time_conn_setup = float(value)
+ return
+ val = str(value).strip()
+ if val == '':
+ self._time_conn_setup = None
+ return
+
+ try:
+ self._time_conn_setup = float(val)
+ except ValueError as e:
+ msg = _('Could not interprete smtp connection setup time {a!r}: {e}').format(
+ a=value, e=e)
+ if self.warn_on_parse_error:
+ LOG.warn(msg)
+ else:
+ LOG.debug(msg)
+ self._time_conn_setup = val
+
+ # ----------------------------------------------------------
+ @property
+ def time_in_queue(self):
+ """Return the time the mail was held in the queue."""
+ return self._time_in_queue
+
+ @time_in_queue.setter
+ def time_in_queue(self, value):
+ if value is None:
+ self._time_in_queue = None
+ return
+
+ if isinstance(value, (float, int)):
+ self._time_in_queue = float(value)
+ return
+ val = str(value).strip()
+ if val == '':
+ self._time_in_queue = None
+ return
+
+ try:
+ self._time_in_queue = float(val)
+ except ValueError as e:
+ msg = _('Could not interprete time in queue {a!r}: {e}').format(a=value, e=e)
+ if self.warn_on_parse_error:
+ LOG.warn(msg)
+ else:
+ LOG.debug(msg)
+ self._time_in_queue = val
+
+ # ----------------------------------------------------------
+ @property
+ def time_xmission(self):
+ """Return the time the smtp process needed to transmit the mail."""
+ return self._time_xmission
+
+ @time_xmission.setter
+ def time_xmission(self, value):
+ if value is None:
+ self._time_xmission = None
+ return
+
+ if isinstance(value, (float, int)):
+ self._time_xmission = float(value)
+ return
+ val = str(value).strip()
+ if val == '':
+ self._time_xmission = None
+ return
+
+ try:
+ self._time_xmission = float(val)
+ except ValueError as e:
+ msg = _('Could not interprete smtp transmission time {a!r}: {e}').format(a=value, e=e)
+ if self.warn_on_parse_error:
+ LOG.warn(msg)
+ else:
+ LOG.debug(msg)
+ self._time_xmission = val
+
+ # -----------------------------------------------------------
+ @property
+ def to_address(self):
+ """Return the RCPT TO address in the deliverer dialogue envelope."""
+ return self._to_address
+
+ @to_address.setter
+ def to_address(self, value):
+ if value is None:
+ self._to_address = None
+ return
+
+ if isinstance(value, MailAddress):
+ self._to_address = copy.copy(value)
+ return
+ val = str(value).strip()
+ if val == '':
+ self._to_address = None
+ return
+
+ try:
+ self._to_address = MailAddress(val)
+ except InvalidMailAddressError as e:
+ msg = _('Could not interprete to address {a!r}: {e}').format(a=val, e=e)
+ if self.warn_on_parse_error:
+ LOG.warn(msg)
+ else:
+ LOG.debug(msg)
+ self._to_address = val
+
+ # -------------------------------------------------------------------------
+ def __str__(self):
+ """Typecast into a string object.
+
+ @return: structure as string
+ @rtype: str
+ """
+ return pp(self.as_dict(exportable=True))
+
+ # -------------------------------------------------------------------------
+ def __repr__(self):
+ """Typecast into a string for reproduction."""
+ out = '<%s(' % (self.__class__.__name__)
+
+ fields = []
+
+ attr_dict = self.as_dict(exportable=True)
+ for attr in attr_dict.keys():
+ value = attr_dict[attr]
+ if value is None:
+ continue
+ fields.append(f'{attr}={value!r}')
+
+ if fields:
+ out += ', '.join(fields)
+
+ out += ')>'
+ return out
+
+ # -------------------------------------------------------------------------
+ def as_dict(self, short=True, exportable=False):
+ """
+ Transform the elements of the object into a dict.
+
+ @param short: don't include local properties in resulting dict.
+ @type short: bool
+
+ @return: structure as dict
+ @rtype: dict
+ """
+ if exportable:
+ res = {}
+ else:
+ res = super(DeliverAction, self).as_dict(short=short)
+
+ for attrib in self.attributes:
+ if not hasattr(self, attrib):
+ continue
+ value = getattr(self, attrib, None)
+ if value is None:
+ res[attrib] = None
+ continue
+ if isinstance(value, ipaddress._BaseAddress):
+ res[attrib] = str(value) if exportable else value
+ continue
+ if isinstance(value, datetime.datetime):
+ res[attrib] = value.isoformat(' ') if exportable else value
+ continue
+ if isinstance(value, (DataPair, MailAddress)):
+ res[attrib] = str(value) if exportable else value
+ continue
+
+ # Catch all
+ res[attrib] = value
+
+ if not exportable:
+ res['relay_address'] = self.relay_address
+
+ return res
+
+ # -------------------------------------------------------------------------
+ def __copy__(self):
+ """Copy the current chain data into a new object."""
+ params = {}
+
+ for attrib in self.attributes:
+ value = getattr(self, attrib, None)
+ if value is None:
+ continue
+ params[attrib] = copy.copy(value)
+
+ return self.__class__(**params)
+
+ # -------------------------------------------------------------------------
+ @classmethod
+ def _get_remote_mailid(cls, message):
+ rmatch = cls.re_remote_id.search(message)
+ if not rmatch:
+ return None
+
+ if rmatch['pf_id'] is not None:
+ return rmatch['pf_id']
+ elif rmatch['jira_id'] is not None:
+ return rmatch['jira_id']
+ elif rmatch['raw_id'] is not None:
+ return rmatch['raw_id']
+ elif rmatch['msg1_id'] is not None:
+ return rmatch['msg1_id']
+ elif rmatch['msg2_id'] is not None:
+ return rmatch['msg2_id']
+ elif rmatch['gsmtp_id'] is not None:
+ return rmatch['gsmtp_id']
+ elif rmatch['msg3_id'] is not None:
+ return rmatch['msg3_id']
+ elif rmatch['msg4_id'] is not None:
+ return rmatch['msg4_id']
+
+ return None
+
+ # -------------------------------------------------------------------------
+ @classmethod
+ def from_log_entry(cls, timestamp, command, pid, message, verbose=0):
+ """Try to create a DeliverAction from the given Postfix log entry."""
+ action = cls()
+ action.date = timestamp
+ action.command = command
+ action.pid = pid
+ cur_msg = message
+
+ if verbose > 2:
+ LOG.debug(f'Parsing {command} delivering line: {message}')
+
+ rmatch = cls.re_to_address.search(cur_msg)
+ if rmatch:
+ action.to_address = rmatch['to']
+ cur_msg = cls.re_to_address.sub('', cur_msg)
+
+ rmatch = cls.re_orig_to_address.search(cur_msg)
+ if rmatch:
+ action.origin_to = rmatch['orig']
+ cur_msg = cls.re_orig_to_address.sub('', cur_msg)
+
+ rmatch = cls.re_relay.search(cur_msg)
+ if rmatch:
+ action.relay = rmatch['relay']
+ cur_msg = cls.re_relay.sub('', cur_msg)
+
+ rmatch = cls.re_delay.search(cur_msg)
+ if rmatch:
+ action.delay_total = rmatch['delay']
+ cur_msg = cls.re_delay.sub('', cur_msg)
+
+ rmatch = cls.re_delays.search(cur_msg)
+ if rmatch:
+ action.time_before_queue = rmatch['p1']
+ action.time_in_queue = rmatch['p2']
+ action.time_conn_setup = rmatch['p3']
+ action.time_xmission = rmatch['p4']
+ cur_msg = cls.re_delays.sub('', cur_msg)
+
+ rmatch = cls.re_dsn.search(cur_msg)
+ if rmatch:
+ action.dsn = rmatch['dsn']
+ cur_msg = cls.re_dsn.sub('', cur_msg)
+
+ rmatch = cls.re_status.search(cur_msg)
+ if rmatch:
+ action.status = rmatch['status']
+ cur_msg = cls.re_status.sub('', cur_msg)
+
+ cur_msg = cur_msg.strip()
+ if cur_msg:
+ if cur_msg.startswith('(') and cur_msg.endswith(')'):
+ cur_msg = cur_msg[1:-1]
+ if cur_msg:
+ action.message = cur_msg
+ remote_id = cls._get_remote_mailid(cur_msg)
+ if remote_id:
+ action.remote_id = remote_id
+
+ return action
+
+
+# =============================================================================
+
+if __name__ == '__main__':
+
+ pass
+
+# =============================================================================
+
+# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 list