Compare commits
1 Commits
v0.2
...
debian/0.1
Author | SHA1 | Date | |
---|---|---|---|
ec3ec224f7 |
11
debian/changelog
vendored
Normal file
11
debian/changelog
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
pqm (0.1-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* Initial release.
|
||||||
|
|
||||||
|
-- Stefan Bühler <stefan.buehler@tik.uni-stuttgart.de> Thu, 12 Jan 2023 10:14:06 +0100
|
||||||
|
|
||||||
|
pqm (0.1-0) unstable; urgency=medium
|
||||||
|
|
||||||
|
* Initial release (fake ITP).
|
||||||
|
|
||||||
|
-- Stefan Bühler <stefan.buehler@tik.uni-stuttgart.de> Thu, 12 Jan 2023 10:14:05 +0100
|
21
debian/control
vendored
Normal file
21
debian/control
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
Source: pqm
|
||||||
|
Section: mail
|
||||||
|
Priority: optional
|
||||||
|
Maintainer: Stefan Bühler <stefan.buehler@tik.uni-stuttgart.de>
|
||||||
|
Rules-Requires-Root: no
|
||||||
|
Build-Depends:
|
||||||
|
debhelper-compat (= 13),
|
||||||
|
Standards-Version: 4.6.1
|
||||||
|
Homepage: https://git-nks-public.tik.uni-stuttgart.de/mail/pqm
|
||||||
|
|
||||||
|
Package: pqm
|
||||||
|
Architecture: all
|
||||||
|
Depends:
|
||||||
|
python3:any,
|
||||||
|
python3-trio,
|
||||||
|
python3-pyparsing,
|
||||||
|
python3-yaml,
|
||||||
|
${misc:Depends},
|
||||||
|
Description: postfix (cluster) queue manager
|
||||||
|
CLI tool to manage postfix queues (optionally across multiple hosts through
|
||||||
|
ssh).
|
28
debian/copyright
vendored
Normal file
28
debian/copyright
vendored
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||||
|
Source: https://git-nks-public.tik.uni-stuttgart.de/mail/pqm
|
||||||
|
Upstream-Name: pqm
|
||||||
|
|
||||||
|
Files:
|
||||||
|
*
|
||||||
|
Copyright:
|
||||||
|
2023 Universität Stuttgart <no-support-by-mail@uni-stuttgart.de>
|
||||||
|
License: MIT
|
||||||
|
|
||||||
|
License: MIT
|
||||||
|
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.
|
5
debian/gbp.conf
vendored
Normal file
5
debian/gbp.conf
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
[DEFAULT]
|
||||||
|
debian-branch = debian
|
||||||
|
upstream-branch = main
|
||||||
|
upstream-tag = v%(version)s
|
||||||
|
pristine-tar = False
|
2
debian/pqm.docs
vendored
Normal file
2
debian/pqm.docs
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
README.md
|
||||||
|
pqm.example.yaml
|
1
debian/pqm.install
vendored
Normal file
1
debian/pqm.install
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
pqm /usr/bin
|
1
debian/pqm.lintian-overrides
vendored
Normal file
1
debian/pqm.lintian-overrides
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
pqm: no-manual-page [usr/bin/pqm]
|
4
debian/rules
vendored
Executable file
4
debian/rules
vendored
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
#!/usr/bin/make -f
|
||||||
|
|
||||||
|
%:
|
||||||
|
dh $@
|
1
debian/source/format
vendored
Normal file
1
debian/source/format
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
3.0 (quilt)
|
1
debian/source/options
vendored
Normal file
1
debian/source/options
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
extend-diff-ignore = "(^|/)(venv|.mypy_cache)(/|$)"
|
64
pqm
64
pqm
@ -33,7 +33,7 @@ import yaml
|
|||||||
T = typing.TypeVar('T')
|
T = typing.TypeVar('T')
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass(slots=True)
|
@dataclasses.dataclass
|
||||||
class Config:
|
class Config:
|
||||||
# if not remote_sources are configured -> use local queue
|
# if not remote_sources are configured -> use local queue
|
||||||
remote_sources: dict[str, str] = dataclasses.field(default_factory=dict)
|
remote_sources: dict[str, str] = dataclasses.field(default_factory=dict)
|
||||||
@ -157,7 +157,7 @@ async def trio_parallel_ordered(
|
|||||||
await tpo.close()
|
await tpo.close()
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass(slots=True)
|
@dataclasses.dataclass
|
||||||
class Recipient:
|
class Recipient:
|
||||||
"""Recipient in a postfix mail"""
|
"""Recipient in a postfix mail"""
|
||||||
address: str
|
address: str
|
||||||
@ -184,22 +184,7 @@ class QueueName(enum.Enum):
|
|||||||
ALL_QUEUE_NAMES: set[QueueName] = set(QueueName)
|
ALL_QUEUE_NAMES: set[QueueName] = set(QueueName)
|
||||||
|
|
||||||
|
|
||||||
def json_decode_stream(data: str):
|
@dataclasses.dataclass
|
||||||
decoder = json.JSONDecoder()
|
|
||||||
data_len = len(data)
|
|
||||||
data_pos = 0
|
|
||||||
while data_len > data_pos and data[data_len-1].isspace():
|
|
||||||
data_len -= 1
|
|
||||||
while True:
|
|
||||||
while data_pos < data_len and data[data_pos].isspace():
|
|
||||||
data_pos += 1
|
|
||||||
if data_pos >= data_len:
|
|
||||||
return
|
|
||||||
obj, data_pos = decoder.raw_decode(data, data_pos)
|
|
||||||
yield obj
|
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass(slots=True)
|
|
||||||
class Mail:
|
class Mail:
|
||||||
"""Metadata for mail in postfix queue"""
|
"""Metadata for mail in postfix queue"""
|
||||||
queue_name: QueueName
|
queue_name: QueueName
|
||||||
@ -224,7 +209,11 @@ class Mail:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def read_postqueue_json(data: str, id_prefix: str = '') -> list[Mail]:
|
def read_postqueue_json(data: str, id_prefix: str = '') -> list[Mail]:
|
||||||
queue = []
|
queue = []
|
||||||
for obj in json_decode_stream(data):
|
decoder = json.JSONDecoder()
|
||||||
|
data = data.strip()
|
||||||
|
while data:
|
||||||
|
obj, end = decoder.raw_decode(data, 0)
|
||||||
|
data = data[end:].lstrip()
|
||||||
mail = Mail.from_json(obj)
|
mail = Mail.from_json(obj)
|
||||||
mail.queue_id = id_prefix + mail.queue_id
|
mail.queue_id = id_prefix + mail.queue_id
|
||||||
queue.append(mail)
|
queue.append(mail)
|
||||||
@ -233,8 +222,6 @@ class Mail:
|
|||||||
|
|
||||||
# abstract collection/cluster of postfix nodes (or just a single one)
|
# abstract collection/cluster of postfix nodes (or just a single one)
|
||||||
class Source(abc.ABC):
|
class Source(abc.ABC):
|
||||||
__slots__ = ()
|
|
||||||
|
|
||||||
# list of server names in collection
|
# list of server names in collection
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def server_list(self) -> list[str]:
|
def server_list(self) -> list[str]:
|
||||||
@ -333,8 +320,6 @@ class Source(abc.ABC):
|
|||||||
|
|
||||||
|
|
||||||
class LocalSource(Source):
|
class LocalSource(Source):
|
||||||
__slots__ = ()
|
|
||||||
|
|
||||||
def server_list(self) -> list[str]:
|
def server_list(self) -> list[str]:
|
||||||
return [socket.gethostname()]
|
return [socket.gethostname()]
|
||||||
|
|
||||||
@ -392,8 +377,6 @@ class LocalSource(Source):
|
|||||||
|
|
||||||
|
|
||||||
class RemoteSource(Source):
|
class RemoteSource(Source):
|
||||||
__slots__ = ('remotes',)
|
|
||||||
|
|
||||||
def __init__(self, remotes: dict[str, str]) -> None:
|
def __init__(self, remotes: dict[str, str]) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.remotes = remotes
|
self.remotes = remotes
|
||||||
@ -526,8 +509,6 @@ class RemoteSource(Source):
|
|||||||
|
|
||||||
|
|
||||||
class UserActivityStats:
|
class UserActivityStats:
|
||||||
__slots__ = ('count_mails', 'related_mails', 'cut_off')
|
|
||||||
|
|
||||||
# find most active address for `health` command
|
# find most active address for `health` command
|
||||||
|
|
||||||
def __init__(self, cut_off: int = 10) -> None:
|
def __init__(self, cut_off: int = 10) -> None:
|
||||||
@ -596,8 +577,6 @@ class UserActivityStats:
|
|||||||
class Filter(abc.ABC):
|
class Filter(abc.ABC):
|
||||||
"""abstract base class for filter expressions"""
|
"""abstract base class for filter expressions"""
|
||||||
|
|
||||||
__slots__ = ()
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -660,14 +639,11 @@ class Filter(abc.ABC):
|
|||||||
|
|
||||||
# abstract base class for filters that don't need (...) around in representations of members in combined filters
|
# abstract base class for filters that don't need (...) around in representations of members in combined filters
|
||||||
class SingleExprFilter(Filter):
|
class SingleExprFilter(Filter):
|
||||||
__slots__ = ()
|
pass
|
||||||
|
|
||||||
|
|
||||||
class FalseFilter(SingleExprFilter):
|
class FalseFilter(SingleExprFilter):
|
||||||
"""constant false - matches no mail"""
|
"""constant false - matches no mail"""
|
||||||
|
|
||||||
__slots__ = ()
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return '0'
|
return '0'
|
||||||
|
|
||||||
@ -693,9 +669,6 @@ class FalseFilter(SingleExprFilter):
|
|||||||
|
|
||||||
class TrueFilter(SingleExprFilter):
|
class TrueFilter(SingleExprFilter):
|
||||||
"""constant true - matches all mail"""
|
"""constant true - matches all mail"""
|
||||||
|
|
||||||
__slots__ = ()
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return '1'
|
return '1'
|
||||||
|
|
||||||
@ -724,8 +697,6 @@ TRUE_FILTER = TrueFilter()
|
|||||||
|
|
||||||
|
|
||||||
class AndFilter(Filter):
|
class AndFilter(Filter):
|
||||||
__slots__ = ('expressions',)
|
|
||||||
|
|
||||||
def __init__(self, expressions: list[Filter] | tuple[()] = ()) -> None:
|
def __init__(self, expressions: list[Filter] | tuple[()] = ()) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.expressions: list[Filter] = expressions or []
|
self.expressions: list[Filter] = expressions or []
|
||||||
@ -779,8 +750,6 @@ class AndFilter(Filter):
|
|||||||
|
|
||||||
|
|
||||||
class OrFilter(Filter):
|
class OrFilter(Filter):
|
||||||
__slots__ = ('expressions',)
|
|
||||||
|
|
||||||
def __init__(self, expressions: list[Filter] | tuple[()] = ()) -> None:
|
def __init__(self, expressions: list[Filter] | tuple[()] = ()) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.expressions: list[Filter] = expressions or []
|
self.expressions: list[Filter] = expressions or []
|
||||||
@ -834,8 +803,6 @@ class OrFilter(Filter):
|
|||||||
|
|
||||||
|
|
||||||
class NotFilter(SingleExprFilter):
|
class NotFilter(SingleExprFilter):
|
||||||
__slots__ = ('expression',)
|
|
||||||
|
|
||||||
def __init__(self, expression: Filter) -> None:
|
def __init__(self, expression: Filter) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.expression = expression
|
self.expression = expression
|
||||||
@ -873,9 +840,6 @@ class NotFilter(SingleExprFilter):
|
|||||||
|
|
||||||
class QueueFilter(SingleExprFilter):
|
class QueueFilter(SingleExprFilter):
|
||||||
"""match mails based on queue they are in"""
|
"""match mails based on queue they are in"""
|
||||||
|
|
||||||
__slots__ = ('select',)
|
|
||||||
|
|
||||||
def __init__(self, select: list[QueueName]) -> None:
|
def __init__(self, select: list[QueueName]) -> None:
|
||||||
self.select = set(select)
|
self.select = set(select)
|
||||||
|
|
||||||
@ -934,8 +898,6 @@ class AddressSelector(enum.Enum):
|
|||||||
|
|
||||||
|
|
||||||
class BaseAddressPattern(abc.ABC):
|
class BaseAddressPattern(abc.ABC):
|
||||||
__slots__ = ()
|
|
||||||
|
|
||||||
# abstract base class of patterns to use to match a mail address
|
# abstract base class of patterns to use to match a mail address
|
||||||
# subclasses match either: full address, domain part, regex
|
# subclasses match either: full address, domain part, regex
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
@ -968,8 +930,6 @@ class BaseAddressPattern(abc.ABC):
|
|||||||
|
|
||||||
|
|
||||||
class AddressPattern(BaseAddressPattern):
|
class AddressPattern(BaseAddressPattern):
|
||||||
__slots__ = ('address',)
|
|
||||||
|
|
||||||
# match full address exactly
|
# match full address exactly
|
||||||
def __init__(self, address: str) -> None:
|
def __init__(self, address: str) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@ -992,8 +952,6 @@ class AddressPattern(BaseAddressPattern):
|
|||||||
|
|
||||||
|
|
||||||
class AddressDomainPattern(BaseAddressPattern):
|
class AddressDomainPattern(BaseAddressPattern):
|
||||||
__slots__ = ('domain',)
|
|
||||||
|
|
||||||
# match address by domain
|
# match address by domain
|
||||||
def __init__(self, domain: str) -> None:
|
def __init__(self, domain: str) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@ -1011,8 +969,6 @@ class AddressDomainPattern(BaseAddressPattern):
|
|||||||
|
|
||||||
|
|
||||||
class AddressRegexMatch(BaseAddressPattern):
|
class AddressRegexMatch(BaseAddressPattern):
|
||||||
__slots__ = ('address',)
|
|
||||||
|
|
||||||
# match address by regex
|
# match address by regex
|
||||||
def __init__(self, address: str | re.Pattern[str]) -> None:
|
def __init__(self, address: str | re.Pattern[str]) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@ -1034,8 +990,6 @@ class AddressRegexMatch(BaseAddressPattern):
|
|||||||
|
|
||||||
|
|
||||||
class AddressFilter(SingleExprFilter):
|
class AddressFilter(SingleExprFilter):
|
||||||
__slots__ = ('selector', 'patterns')
|
|
||||||
|
|
||||||
# match mails by address
|
# match mails by address
|
||||||
def __init__(self, selector: AddressSelector, patterns: list[BaseAddressPattern]) -> None:
|
def __init__(self, selector: AddressSelector, patterns: list[BaseAddressPattern]) -> None:
|
||||||
self.selector = selector
|
self.selector = selector
|
||||||
|
Reference in New Issue
Block a user