add option to store unparse msg and rework rules logic
This commit is contained in:
@@ -28,7 +28,7 @@ Config options in **global** section:
|
|||||||
The socket used to communicate with the MTA. If it is not specified in the config, it has to be set as command line option.
|
The socket used to communicate with the MTA. If it is not specified in the config, it has to be set as command line option.
|
||||||
* **local_addrs** (optional)
|
* **local_addrs** (optional)
|
||||||
A list of hosts and network addresses which are considered local. It is used to for the condition option [local](#Conditions).
|
A list of hosts and network addresses which are considered local. It is used to for the condition option [local](#Conditions).
|
||||||
Default: **::1/128, 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16**
|
Default: **fe80::/64, ::1/128, 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16**
|
||||||
* **loglevel** (optional)
|
* **loglevel** (optional)
|
||||||
Set the log level. This option may be overriden by any rule or action object. Possible values are:
|
Set the log level. This option may be overriden by any rule or action object. Possible values are:
|
||||||
* **error**
|
* **error**
|
||||||
@@ -115,6 +115,9 @@ Config options for **store** actions:
|
|||||||
* **storage_type**
|
* **storage_type**
|
||||||
Storage type. Possible values are:
|
Storage type. Possible values are:
|
||||||
* **file**
|
* **file**
|
||||||
|
* **original** (optional)
|
||||||
|
Default: **false**
|
||||||
|
If set to true, store the message as received by the MTA instead of storing the current state of the message, that may was modified already by other actions.
|
||||||
|
|
||||||
Config options for **file** storage:
|
Config options for **file** storage:
|
||||||
* **directory**
|
* **directory**
|
||||||
|
|||||||
@@ -32,9 +32,10 @@ import json
|
|||||||
|
|
||||||
from Milter.utils import parse_addr
|
from Milter.utils import parse_addr
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
from email import message_from_binary_file
|
||||||
from email.header import Header
|
from email.header import Header
|
||||||
from email.parser import BytesFeedParser
|
|
||||||
from email.policy import default as default_policy, SMTP
|
from email.policy import default as default_policy, SMTP
|
||||||
|
from io import BytesIO
|
||||||
from netaddr import IPNetwork, AddrFormatError
|
from netaddr import IPNetwork, AddrFormatError
|
||||||
|
|
||||||
from pymodmilter.base import CustomLogger, BaseConfig, MilterMessage
|
from pymodmilter.base import CustomLogger, BaseConfig, MilterMessage
|
||||||
@@ -92,6 +93,7 @@ class ModifyMilterConfig(BaseConfig):
|
|||||||
"should be list of strings"
|
"should be list of strings"
|
||||||
else:
|
else:
|
||||||
local_addrs = [
|
local_addrs = [
|
||||||
|
"fe80::/64",
|
||||||
"::1/128",
|
"::1/128",
|
||||||
"127.0.0.0/8",
|
"127.0.0.0/8",
|
||||||
"10.0.0.0/8",
|
"10.0.0.0/8",
|
||||||
@@ -139,12 +141,6 @@ class ModifyMilter(Milter.Base):
|
|||||||
self.logger = logging.getLogger(__name__)
|
self.logger = logging.getLogger(__name__)
|
||||||
self.logger.setLevel(ModifyMilter._loglevel)
|
self.logger.setLevel(ModifyMilter._loglevel)
|
||||||
|
|
||||||
# save rules, it must not change during runtime
|
|
||||||
self.rules = ModifyMilter._rules.copy()
|
|
||||||
|
|
||||||
self.msg = None
|
|
||||||
self._replace_body = False
|
|
||||||
|
|
||||||
def addheader(self, field, value, idx=-1):
|
def addheader(self, field, value, idx=-1):
|
||||||
value = replace_illegal_chars(Header(s=value).encode())
|
value = replace_illegal_chars(Header(s=value).encode())
|
||||||
self.logger.debug(f"milter: addheader: {field}: {value}")
|
self.logger.debug(f"milter: addheader: {field}: {value}")
|
||||||
@@ -181,28 +177,20 @@ class ModifyMilter(Milter.Base):
|
|||||||
self.addheader(field, value)
|
self.addheader(field, value)
|
||||||
|
|
||||||
def replacebody(self):
|
def replacebody(self):
|
||||||
self._replace_body = True
|
self._replacebody = True
|
||||||
|
|
||||||
def connect(self, IPname, family, hostaddr):
|
def connect(self, IPname, family, hostaddr):
|
||||||
try:
|
try:
|
||||||
if hostaddr is None:
|
if hostaddr is None:
|
||||||
self.logger.error("unable to proceed, host address is None")
|
self.logger.error(f"received invalid host address {hostaddr}, "
|
||||||
|
f"unable to proceed")
|
||||||
return Milter.TEMPFAIL
|
return Milter.TEMPFAIL
|
||||||
|
|
||||||
|
self.IP = hostaddr[0]
|
||||||
|
self.port = hostaddr[1]
|
||||||
self.logger.debug(
|
self.logger.debug(
|
||||||
f"accepted milter connection from {hostaddr[0]} "
|
f"accepted milter connection from {self.IP} "
|
||||||
f"port {hostaddr[1]}")
|
f"port {self.port}")
|
||||||
|
|
||||||
# remove rules which ignore this host
|
|
||||||
for rule in self.rules.copy():
|
|
||||||
if rule.ignores(host=hostaddr[0]):
|
|
||||||
self.rules.remove(rule)
|
|
||||||
|
|
||||||
if not self.rules:
|
|
||||||
self.logger.debug(
|
|
||||||
f"host {hostaddr[0]} is ignored by all rules, "
|
|
||||||
f"skip further processing")
|
|
||||||
return Milter.ACCEPT
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.exception(
|
self.logger.exception(
|
||||||
f"an exception occured in connect method: {e}")
|
f"an exception occured in connect method: {e}")
|
||||||
@@ -210,20 +198,21 @@ class ModifyMilter(Milter.Base):
|
|||||||
|
|
||||||
return Milter.CONTINUE
|
return Milter.CONTINUE
|
||||||
|
|
||||||
|
def hello(self, heloname):
|
||||||
|
try:
|
||||||
|
self.heloname = heloname
|
||||||
|
self.logger.debug(f"received HELO name: {heloname}")
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.exception(
|
||||||
|
f"an exception occured in hello method: {e}")
|
||||||
|
return Milter.TEMPFAIL
|
||||||
|
|
||||||
|
return Milter.CONTINUE
|
||||||
|
|
||||||
def envfrom(self, mailfrom, *str):
|
def envfrom(self, mailfrom, *str):
|
||||||
try:
|
try:
|
||||||
mailfrom = "@".join(parse_addr(mailfrom)).lower()
|
self.mailfrom = "@".join(parse_addr(mailfrom)).lower()
|
||||||
for rule in self.rules.copy():
|
self.rcpts = set()
|
||||||
if rule.ignores(envfrom=mailfrom):
|
|
||||||
self.rules.remove(rule)
|
|
||||||
|
|
||||||
if not self.rules:
|
|
||||||
self.logger.debug(
|
|
||||||
f"envelope-from address {mailfrom} is ignored by "
|
|
||||||
f"all rules, skip further processing")
|
|
||||||
return Milter.ACCEPT
|
|
||||||
|
|
||||||
self.recipients = set()
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.exception(
|
self.logger.exception(
|
||||||
f"an exception occured in envfrom method: {e}")
|
f"an exception occured in envfrom method: {e}")
|
||||||
@@ -233,7 +222,7 @@ class ModifyMilter(Milter.Base):
|
|||||||
|
|
||||||
def envrcpt(self, to, *str):
|
def envrcpt(self, to, *str):
|
||||||
try:
|
try:
|
||||||
self.recipients.add("@".join(parse_addr(to)).lower())
|
self.rcpts.add("@".join(parse_addr(to)).lower())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.exception(
|
self.logger.exception(
|
||||||
f"an exception occured in envrcpt method: {e}")
|
f"an exception occured in envrcpt method: {e}")
|
||||||
@@ -243,32 +232,25 @@ class ModifyMilter(Milter.Base):
|
|||||||
|
|
||||||
def data(self):
|
def data(self):
|
||||||
try:
|
try:
|
||||||
for rule in self.rules.copy():
|
|
||||||
if rule.ignores(envto=[*self.recipients]):
|
|
||||||
self.rules.remove(rule)
|
|
||||||
|
|
||||||
if not self.rules:
|
|
||||||
self.logger.debug(
|
|
||||||
"envelope-to addresses are ignored by all rules, "
|
|
||||||
"skip further processing")
|
|
||||||
return Milter.ACCEPT
|
|
||||||
|
|
||||||
self.qid = self.getsymval('i')
|
self.qid = self.getsymval('i')
|
||||||
self.logger = CustomLogger(self.logger, {"qid": self.qid})
|
self.logger = CustomLogger(self.logger, {"qid": self.qid})
|
||||||
self.logger.debug("received queue-id from MTA")
|
self.logger.debug("received queue-id from MTA")
|
||||||
|
|
||||||
self.fields = None
|
self.rules = []
|
||||||
self.fields_bytes = None
|
self._headersonly = True
|
||||||
self.body_data = None
|
for rule in ModifyMilter._rules:
|
||||||
|
if not rule.ignores(host=self.IP, envfrom=self.mailfrom,
|
||||||
|
envto=[*self.rcpts]):
|
||||||
|
self.rules.append(rule)
|
||||||
|
if rule.need_body():
|
||||||
|
self._headersonly = False
|
||||||
|
|
||||||
self._fp = BytesFeedParser(
|
if not self.rules:
|
||||||
_factory=MilterMessage, policy=default_policy)
|
self.logger.debug(
|
||||||
self._keep_body = False
|
"message is ignored by all rules, skip further processing")
|
||||||
for rule in self.rules:
|
return Milter.ACCEPT
|
||||||
if rule.need_body():
|
|
||||||
self._keep_body = True
|
|
||||||
break
|
|
||||||
|
|
||||||
|
self.fp = BytesIO()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.exception(
|
self.logger.exception(
|
||||||
f"an exception occured in data method: {e}")
|
f"an exception occured in data method: {e}")
|
||||||
@@ -282,7 +264,7 @@ class ModifyMilter(Milter.Base):
|
|||||||
field = field.encode("ascii", errors="replace")
|
field = field.encode("ascii", errors="replace")
|
||||||
value = value.encode("ascii", errors="replace")
|
value = value.encode("ascii", errors="replace")
|
||||||
|
|
||||||
self._fp.feed(field + b": " + value + b"\r\n")
|
self.fp.write(field + b": " + value + b"\r\n")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.exception(
|
self.logger.exception(
|
||||||
f"an exception occured in header method: {e}")
|
f"an exception occured in header method: {e}")
|
||||||
@@ -292,7 +274,7 @@ class ModifyMilter(Milter.Base):
|
|||||||
|
|
||||||
def eoh(self):
|
def eoh(self):
|
||||||
try:
|
try:
|
||||||
self._fp.feed(b"\r\n")
|
self.fp.write(b"\r\n")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.exception(
|
self.logger.exception(
|
||||||
f"an exception occured in eoh method: {e}")
|
f"an exception occured in eoh method: {e}")
|
||||||
@@ -302,8 +284,8 @@ class ModifyMilter(Milter.Base):
|
|||||||
|
|
||||||
def body(self, chunk):
|
def body(self, chunk):
|
||||||
try:
|
try:
|
||||||
if self._keep_body:
|
if not self._headersonly:
|
||||||
self._fp.feed(chunk)
|
self.fp.write(chunk)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.exception(
|
self.logger.exception(
|
||||||
f"an exception occured in body method: {e}")
|
f"an exception occured in body method: {e}")
|
||||||
@@ -313,7 +295,19 @@ class ModifyMilter(Milter.Base):
|
|||||||
|
|
||||||
def eom(self):
|
def eom(self):
|
||||||
try:
|
try:
|
||||||
self.msg = self._fp.close()
|
self.fp.seek(0)
|
||||||
|
self.msg = message_from_binary_file(
|
||||||
|
self.fp, _class=MilterMessage, policy=default_policy)
|
||||||
|
|
||||||
|
self.msg_info = defaultdict(str)
|
||||||
|
self.msg_info["ip"] = self.IP
|
||||||
|
self.msg_info["port"] = self.port
|
||||||
|
self.msg_info["heloname"] = self.heloname
|
||||||
|
self.msg_info["envfrom"] = self.mailfrom
|
||||||
|
self.msg_info["rcpts"] = self.rcpts
|
||||||
|
self.msg_info["qid"] = self.qid
|
||||||
|
|
||||||
|
self._replacebody = False
|
||||||
milter_action = None
|
milter_action = None
|
||||||
for rule in self.rules:
|
for rule in self.rules:
|
||||||
milter_action = rule.execute(self)
|
milter_action = rule.execute(self)
|
||||||
@@ -321,7 +315,7 @@ class ModifyMilter(Milter.Base):
|
|||||||
if milter_action is not None:
|
if milter_action is not None:
|
||||||
break
|
break
|
||||||
|
|
||||||
if self._replace_body:
|
if self._replacebody:
|
||||||
data = self.msg.as_bytes(policy=SMTP)
|
data = self.msg.as_bytes(policy=SMTP)
|
||||||
body_pos = data.find(b"\r\n\r\n") + 4
|
body_pos = data.find(b"\r\n\r\n") + 4
|
||||||
self.logger.debug("milter: replacebody")
|
self.logger.debug("milter: replacebody")
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ from collections import defaultdict
|
|||||||
from copy import copy
|
from copy import copy
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from email.message import MIMEPart
|
from email.message import MIMEPart
|
||||||
|
from email.policy import SMTP
|
||||||
|
|
||||||
from pymodmilter import CustomLogger, BaseConfig
|
from pymodmilter import CustomLogger, BaseConfig
|
||||||
from pymodmilter.conditions import ConditionsConfig, Conditions
|
from pymodmilter.conditions import ConditionsConfig, Conditions
|
||||||
@@ -204,7 +205,7 @@ def _patch_message_body(milter, action, text_template, html_template, logger):
|
|||||||
|
|
||||||
def _wrap_message(milter, logger):
|
def _wrap_message(milter, logger):
|
||||||
attachment = MIMEPart()
|
attachment = MIMEPart()
|
||||||
attachment.set_content(milter.msg.as_bytes(),
|
attachment.set_content(milter.msg.as_bytes(policy=SMTP),
|
||||||
maintype="plain", subtype="text",
|
maintype="plain", subtype="text",
|
||||||
disposition="attachment",
|
disposition="attachment",
|
||||||
filename=f"{milter.qid}.eml",
|
filename=f"{milter.qid}.eml",
|
||||||
@@ -317,7 +318,7 @@ def rewrite_links(milter, repl, pretend=False,
|
|||||||
milter.replacebody()
|
milter.replacebody()
|
||||||
|
|
||||||
|
|
||||||
def store(milter, directory, pretend=False,
|
def store(milter, directory, original=False, pretend=False,
|
||||||
logger=logging.getLogger(__name__)):
|
logger=logging.getLogger(__name__)):
|
||||||
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
|
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
|
||||||
store_id = f"{timestamp}_{milter.qid}"
|
store_id = f"{timestamp}_{milter.qid}"
|
||||||
@@ -326,7 +327,11 @@ def store(milter, directory, pretend=False,
|
|||||||
logger.info(f"store message in file {datafile}")
|
logger.info(f"store message in file {datafile}")
|
||||||
try:
|
try:
|
||||||
with open(datafile, "wb") as fp:
|
with open(datafile, "wb") as fp:
|
||||||
fp.write(milter.msg.as_bytes())
|
if original:
|
||||||
|
milter.fp.seek(0)
|
||||||
|
fp.write(milter.fp.read())
|
||||||
|
else:
|
||||||
|
fp.write(milter.msg.as_bytes(policy=SMTP))
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
raise RuntimeError(f"unable to store message: {e}")
|
raise RuntimeError(f"unable to store message: {e}")
|
||||||
|
|
||||||
@@ -468,6 +473,10 @@ class ActionConfig(BaseConfig):
|
|||||||
f"{self['name']}: storage_type: invalid value, " \
|
f"{self['name']}: storage_type: invalid value, " \
|
||||||
f"should be string"
|
f"should be string"
|
||||||
self["storage_type"] = cfg["storage_type"]
|
self["storage_type"] = cfg["storage_type"]
|
||||||
|
|
||||||
|
if "original" in cfg:
|
||||||
|
self.add_bool_arg(cfg, "original")
|
||||||
|
|
||||||
if self["storage_type"] == "file":
|
if self["storage_type"] == "file":
|
||||||
self.add_string_arg(cfg, "directory")
|
self.add_string_arg(cfg, "directory")
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
# Notes: A list of local hosts and networks.
|
# Notes: A list of local hosts and networks.
|
||||||
# Value: [ LIST ]
|
# Value: [ LIST ]
|
||||||
#
|
#
|
||||||
"local_addrs": ["::1/128", "127.0.0.0/8", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"],
|
"local_addrs": ["fe80::/64", "::1/128", "127.0.0.0/8", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"],
|
||||||
|
|
||||||
# Option: loglevel
|
# Option: loglevel
|
||||||
# Type: String
|
# Type: String
|
||||||
@@ -206,7 +206,14 @@
|
|||||||
# Type: String
|
# Type: String
|
||||||
# Notes: Directory used to store e-mails.
|
# Notes: Directory used to store e-mails.
|
||||||
# Value: [ file ]
|
# Value: [ file ]
|
||||||
"directory": "/mnt/messages"
|
"directory": "/mnt/messages",
|
||||||
|
|
||||||
|
# Option: original
|
||||||
|
# Type: Bool
|
||||||
|
# Notes: If set to true, store the message as received by the MTA instead of storing the current state
|
||||||
|
# of the message, that may was modified already by other actions.
|
||||||
|
# Value: [ true | false ]
|
||||||
|
"original": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user