add quarantine functionality and refactor

This commit is contained in:
2021-09-21 00:06:15 +02:00
parent 42e65848c4
commit cd470e8947
8 changed files with 157 additions and 69 deletions

View File

@@ -12,6 +12,11 @@
# along with PyMod-Milter. If not, see <http://www.gnu.org/licenses/>.
#
__all__ = [
"BaseMailStorage",
"FileMailStorage",
"Quarantine"]
import json
import logging
import os
@@ -24,7 +29,12 @@ from time import gmtime
class BaseMailStorage:
"Mail storage base class"
def __init__(self):
_headersonly = True
def __init__(self, original=False, metadata=False, metavar=None):
self.original = original
self.metadata = metadata
self.metavar = metavar
return
def add(self, data, qid, mailfrom="", recipients=[]):
@@ -54,9 +64,11 @@ class BaseMailStorage:
class FileMailStorage(BaseMailStorage):
"Storage class to store mails on filesystem."
def __init__(self, directory, original=False, skip_metadata=False,
_headersonly = False
def __init__(self, directory, original=False, metadata=False,
metavar=None):
super().__init__()
super().__init__(original, metadata, metavar)
# check if directory exists and is writable
if not os.path.isdir(directory) or \
not os.access(directory, os.W_OK):
@@ -64,9 +76,6 @@ class FileMailStorage(BaseMailStorage):
f"directory '{directory}' does not exist or is "
f"not writable")
self.directory = directory
self.original = original
self.skip_metadata = skip_metadata
self.metavar = metavar
self._metadata_suffix = ".metadata"
def get_storageid(self, qid):
@@ -96,7 +105,7 @@ class FileMailStorage(BaseMailStorage):
metafile, datafile = self._get_file_paths(storage_id)
try:
if not self.skip_metadata:
if self.metadata:
os.remove(metafile)
os.remove(datafile)
@@ -113,22 +122,22 @@ class FileMailStorage(BaseMailStorage):
# save mail
self._save_datafile(datafile, data)
if self.skip_metadata:
metafile = None
else:
# save metadata
metadata = {
"mailfrom": mailfrom,
"recipients": recipients,
"subject": subject,
"timestamp": timegm(gmtime()),
"queue_id": qid}
if not self.metadata:
return storage_id, None, datafile
try:
self._save_metafile(metafile, metadata)
except RuntimeError as e:
os.remove(datafile)
raise e
# save metadata
metadata = {
"mailfrom": mailfrom,
"recipients": recipients,
"subject": subject,
"timestamp": timegm(gmtime()),
"queue_id": qid}
try:
self._save_metafile(metafile, metadata)
except RuntimeError as e:
os.remove(datafile)
raise e
return storage_id, metafile, datafile
@@ -157,14 +166,14 @@ class FileMailStorage(BaseMailStorage):
if self.metavar:
milter.msginfo["vars"][f"{self.metavar}_ID"] = storage_id
milter.msginfo["vars"][f"{self.metavar}_DATAFILE"] = datafile
if not self.skip_metadata:
if self.metadata:
milter.msginfo["vars"][f"{self.metavar}_METAFILE"] = metafile
def get_metadata(self, storage_id):
"Return metadata of email in storage."
super(FileMailStorage, self).get_metadata(storage_id)
if self.skip_metadata:
if not self.metadata:
return None
metafile, _ = self._get_file_paths(storage_id)
@@ -191,6 +200,9 @@ class FileMailStorage(BaseMailStorage):
if isinstance(recipients, str):
recipients = [recipients]
if not self.metadata:
return {}
emails = {}
metafiles = glob(os.path.join(
self.directory, f"*{self._metadata_suffix}"))
@@ -225,7 +237,7 @@ class FileMailStorage(BaseMailStorage):
"Delete email from storage."
super(FileMailStorage, self).delete(storage_id, recipients)
if not recipients or self.skip_metadata:
if not recipients or not self.metadata:
self._remove(storage_id)
return
@@ -257,4 +269,28 @@ class FileMailStorage(BaseMailStorage):
data = open(datafile, "rb").read()
except IOError as e:
raise RuntimeError(f"unable to open email data file: {e}")
return (data, metadata)
return (metadata, data)
class Quarantine:
"Quarantine class."
_headersonly = False
def __init__(self, storage, notification=None, milter_action=None,
reject_reason="Message rejected"):
self.storage = storage.action(**storage.args, metadata=True)
self.notification = notification
if self.notification is not None:
self.notification = notification.action(**notification.args)
self.milter_action = milter_action
self.reject_reason = reject_reason
def execute(self, milter, pretend=False,
logger=logging.getLogger(__name__)):
self.storage.execute(milter, pretend, logger)
if self.notification is not None:
self.notification.execute(milter, pretend, logger)
milter.msginfo["rcpts"] = []
if self.milter_action is not None:
return (self.milter_action, self.reject_reason)