Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
b0c3dab64e
|
|||
|
33f0f06763
|
|||
|
5998535761
|
|||
|
1a368998c8
|
|||
|
8c07c02102
|
|||
|
702d22f9aa
|
|||
|
e0bf57e2d0
|
|||
|
b3e9f16e55
|
|||
|
dd3f8ac11e
|
|||
|
d93eab4d41
|
|||
|
6117ff372d
|
|||
|
782e744f08
|
|||
|
9337ac72d8
|
|||
|
ac458dade8
|
|||
|
a90e087a5d
|
@@ -1,3 +1,3 @@
|
|||||||
include LICENSE README.md
|
include LICENSE README.md
|
||||||
recursive-include docs *
|
recursive-include pyquarantine/docs *
|
||||||
recursive-include misc *
|
recursive-include pyquarantine/misc *
|
||||||
|
|||||||
@@ -17,11 +17,12 @@ import configparser
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import encodings
|
||||||
|
|
||||||
from Milter.utils import parse_addr
|
from Milter.utils import parse_addr
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from email.policy import default as default_policy
|
from email.charset import Charset
|
||||||
from email.parser import BytesHeaderParser
|
from email.header import Header, decode_header
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from itertools import groupby
|
from itertools import groupby
|
||||||
from netaddr import IPAddress, IPNetwork, AddrFormatError
|
from netaddr import IPAddress, IPNetwork, AddrFormatError
|
||||||
@@ -31,6 +32,7 @@ from pyquarantine import storages
|
|||||||
from pyquarantine import whitelists
|
from pyquarantine import whitelists
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
"make_header",
|
||||||
"Quarantine",
|
"Quarantine",
|
||||||
"QuarantineMilter",
|
"QuarantineMilter",
|
||||||
"setup_milter",
|
"setup_milter",
|
||||||
@@ -40,9 +42,47 @@ __all__ = [
|
|||||||
"notifications",
|
"notifications",
|
||||||
"storages",
|
"storages",
|
||||||
"run",
|
"run",
|
||||||
"version",
|
|
||||||
"whitelists"]
|
"whitelists"]
|
||||||
|
|
||||||
|
__version__ = "1.0.9"
|
||||||
|
|
||||||
|
|
||||||
|
################################################
|
||||||
|
# add charset alias for windows-874 encoding #
|
||||||
|
################################################
|
||||||
|
|
||||||
|
aliases = encodings.aliases.aliases
|
||||||
|
|
||||||
|
for alias in ["windows-874", "windows_874"]:
|
||||||
|
if alias not in aliases:
|
||||||
|
aliases[alias] = "cp874"
|
||||||
|
|
||||||
|
setattr(encodings.aliases, "aliases", aliases)
|
||||||
|
|
||||||
|
################################################
|
||||||
|
|
||||||
|
|
||||||
|
def make_header(decoded_seq, maxlinelen=None, header_name=None,
|
||||||
|
continuation_ws=' ', errors='strict'):
|
||||||
|
"""Create a Header from a sequence of pairs as returned by decode_header()
|
||||||
|
|
||||||
|
decode_header() takes a header value string and returns a sequence of
|
||||||
|
pairs of the format (decoded_string, charset) where charset is the string
|
||||||
|
name of the character set.
|
||||||
|
|
||||||
|
This function takes one of those sequence of pairs and returns a Header
|
||||||
|
instance. Optional maxlinelen, header_name, and continuation_ws are as in
|
||||||
|
the Header constructor.
|
||||||
|
"""
|
||||||
|
h = Header(maxlinelen=maxlinelen, header_name=header_name,
|
||||||
|
continuation_ws=continuation_ws)
|
||||||
|
for s, charset in decoded_seq:
|
||||||
|
# None means us-ascii but we can simply pass it on to h.append()
|
||||||
|
if charset is not None and not isinstance(charset, Charset):
|
||||||
|
charset = Charset(charset)
|
||||||
|
h.append(s, charset, errors=errors)
|
||||||
|
return h
|
||||||
|
|
||||||
|
|
||||||
class Quarantine(object):
|
class Quarantine(object):
|
||||||
"""Quarantine class suitable for QuarantineMilter
|
"""Quarantine class suitable for QuarantineMilter
|
||||||
@@ -64,8 +104,9 @@ class Quarantine(object):
|
|||||||
self.logger = logging.getLogger(__name__)
|
self.logger = logging.getLogger(__name__)
|
||||||
self.name = name
|
self.name = name
|
||||||
self.index = index
|
self.index = index
|
||||||
|
self.regex = regex
|
||||||
if regex:
|
if regex:
|
||||||
self.regex = re.compile(
|
self.re = re.compile(
|
||||||
regex, re.MULTILINE + re.DOTALL + re.IGNORECASE)
|
regex, re.MULTILINE + re.DOTALL + re.IGNORECASE)
|
||||||
self.storage = storage
|
self.storage = storage
|
||||||
self.whitelist = whitelist
|
self.whitelist = whitelist
|
||||||
@@ -102,7 +143,7 @@ class Quarantine(object):
|
|||||||
# pre-compile regex
|
# pre-compile regex
|
||||||
self.logger.debug(
|
self.logger.debug(
|
||||||
f"{self.name}: compiling regex '{cfg['regex']}'")
|
f"{self.name}: compiling regex '{cfg['regex']}'")
|
||||||
self.regex = re.compile(
|
self.re = re.compile(
|
||||||
cfg["regex"], re.MULTILINE + re.DOTALL + re.IGNORECASE)
|
cfg["regex"], re.MULTILINE + re.DOTALL + re.IGNORECASE)
|
||||||
|
|
||||||
self.smtp_host = cfg["smtp_host"]
|
self.smtp_host = cfg["smtp_host"]
|
||||||
@@ -264,7 +305,7 @@ class Quarantine(object):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def match(self, header):
|
def match(self, header):
|
||||||
return self.regex.search(header)
|
return self.re.search(header)
|
||||||
|
|
||||||
|
|
||||||
class QuarantineMilter(Milter.Base):
|
class QuarantineMilter(Milter.Base):
|
||||||
@@ -319,7 +360,7 @@ class QuarantineMilter(Milter.Base):
|
|||||||
if quarantine.host_in_whitelist(hostaddr):
|
if quarantine.host_in_whitelist(hostaddr):
|
||||||
self.logger.debug(
|
self.logger.debug(
|
||||||
f"host {hostaddr[0]} is in whitelist of "
|
f"host {hostaddr[0]} is in whitelist of "
|
||||||
f"quarantine {quarantine['name']}")
|
f"quarantine {quarantine.name}")
|
||||||
self.quarantines.remove(quarantine)
|
self.quarantines.remove(quarantine)
|
||||||
if not self.quarantines:
|
if not self.quarantines:
|
||||||
self.logger.debug(
|
self.logger.debug(
|
||||||
@@ -349,27 +390,32 @@ class QuarantineMilter(Milter.Base):
|
|||||||
f"{self.qid}: initializing memory buffer to save email data")
|
f"{self.qid}: initializing memory buffer to save email data")
|
||||||
# initialize memory buffer to save email data
|
# initialize memory buffer to save email data
|
||||||
self.fp = BytesIO()
|
self.fp = BytesIO()
|
||||||
|
self.headers = []
|
||||||
return Milter.CONTINUE
|
return Milter.CONTINUE
|
||||||
|
|
||||||
@Milter.noreply
|
|
||||||
def header(self, name, value):
|
def header(self, name, value):
|
||||||
try:
|
try:
|
||||||
|
# remove surrogates from value
|
||||||
|
value = value.encode(
|
||||||
|
errors="surrogateescape").decode(errors="replace")
|
||||||
|
self.logger.debug(f"{self.qid}: received header: {name}: {value}")
|
||||||
# write email header to memory buffer
|
# write email header to memory buffer
|
||||||
self.fp.write(f"{name}: {value}\r\n".encode(
|
self.fp.write(f"{name}: {value}\r\n".encode(
|
||||||
encoding="ascii", errors="replace"))
|
encoding="ascii", errors="replace"))
|
||||||
|
header = make_header(decode_header(value), errors="replace")
|
||||||
|
value = str(header).replace("\x00", "")
|
||||||
|
self.logger.debug(
|
||||||
|
f"{self.qid}: decoded header: {name}: {value}")
|
||||||
|
self.headers.append((name, value))
|
||||||
|
return Milter.CONTINUE
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.exception(
|
self.logger.exception(
|
||||||
f"an exception occured in header function: {e}")
|
f"an exception occured in header function: {e}")
|
||||||
return Milter.TEMPFAIL
|
return Milter.TEMPFAIL
|
||||||
|
|
||||||
return Milter.CONTINUE
|
|
||||||
|
|
||||||
def eoh(self):
|
def eoh(self):
|
||||||
try:
|
try:
|
||||||
self.fp.write("\r\n".encode(encoding="ascii"))
|
self.fp.write("\r\n".encode(encoding="ascii"))
|
||||||
self.fp.seek(0)
|
|
||||||
self.headers = BytesHeaderParser(
|
|
||||||
policy=default_policy).parse(self.fp).items()
|
|
||||||
self.wl_cache = whitelists.WhitelistCache()
|
self.wl_cache = whitelists.WhitelistCache()
|
||||||
|
|
||||||
# initialize dicts to set quaranines per recipient and keep matches
|
# initialize dicts to set quaranines per recipient and keep matches
|
||||||
@@ -402,7 +448,7 @@ class QuarantineMilter(Milter.Base):
|
|||||||
# check email header against quarantine regex
|
# check email header against quarantine regex
|
||||||
self.logger.debug(
|
self.logger.debug(
|
||||||
f"{self.qid}: {quarantine.name}: checking header "
|
f"{self.qid}: {quarantine.name}: checking header "
|
||||||
f"against regex '{quarantine.regex}'")
|
f"against regex '{str(quarantine.regex)}'")
|
||||||
match = quarantine.match(header)
|
match = quarantine.match(header)
|
||||||
if match:
|
if match:
|
||||||
self.logger.debug(
|
self.logger.debug(
|
||||||
|
|||||||
@@ -21,14 +21,14 @@ import sys
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
from pyquarantine import QuarantineMilter, setup_milter
|
from pyquarantine import QuarantineMilter, setup_milter
|
||||||
from pyquarantine.version import __version__ as version
|
from pyquarantine import __version__ as version
|
||||||
|
|
||||||
|
|
||||||
def _get_quarantine(quarantines, name):
|
def _get_quarantine(quarantines, name):
|
||||||
try:
|
try:
|
||||||
quarantine = next((q for q in quarantines if q.name == name))
|
quarantine = next((q for q in quarantines if q.name == name))
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
raise RuntimeError(f"invalid quarantine 'name'")
|
raise RuntimeError("invalid quarantine 'name'")
|
||||||
return quarantine
|
return quarantine
|
||||||
|
|
||||||
|
|
||||||
@@ -286,6 +286,13 @@ def delete(quarantines, args):
|
|||||||
logger.info("quarantined email deleted successfully")
|
logger.info("quarantined email deleted successfully")
|
||||||
|
|
||||||
|
|
||||||
|
def get(quarantines, args):
|
||||||
|
storage = _get_storage(quarantines, args.quarantine)
|
||||||
|
fp, _ = storage.get_mail(args.quarantine_id)
|
||||||
|
print(fp.read().decode())
|
||||||
|
fp.close()
|
||||||
|
|
||||||
|
|
||||||
class StdErrFilter(logging.Filter):
|
class StdErrFilter(logging.Filter):
|
||||||
def filter(self, rec):
|
def filter(self, rec):
|
||||||
return rec.levelno in (logging.ERROR, logging.WARNING)
|
return rec.levelno in (logging.ERROR, logging.WARNING)
|
||||||
@@ -452,6 +459,17 @@ def main():
|
|||||||
help="Delete email for all recipients.",
|
help="Delete email for all recipients.",
|
||||||
action="store_true")
|
action="store_true")
|
||||||
quarantine_delete_parser.set_defaults(func=delete)
|
quarantine_delete_parser.set_defaults(func=delete)
|
||||||
|
# quarantine get command
|
||||||
|
quarantine_get_parser = quarantine_subparsers.add_parser(
|
||||||
|
"get",
|
||||||
|
description="Get email from quarantine.",
|
||||||
|
help="Get email from quarantine",
|
||||||
|
formatter_class=formatter_class)
|
||||||
|
quarantine_get_parser.add_argument(
|
||||||
|
"quarantine_id",
|
||||||
|
metavar="ID",
|
||||||
|
help="Quarantine ID.")
|
||||||
|
quarantine_get_parser.set_defaults(func=get)
|
||||||
|
|
||||||
# whitelist command group
|
# whitelist command group
|
||||||
whitelist_parser = subparsers.add_parser(
|
whitelist_parser = subparsers.add_parser(
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
@@ -17,12 +17,12 @@ import logging
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
from cgi import escape
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from email import policy
|
from email.policy import default as default_policy
|
||||||
from email.mime.multipart import MIMEMultipart
|
from email.mime.multipart import MIMEMultipart
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
from email.mime.image import MIMEImage
|
from email.mime.image import MIMEImage
|
||||||
|
from html import escape
|
||||||
from os.path import basename
|
from os.path import basename
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
|
||||||
@@ -212,7 +212,7 @@ class EMailNotification(BaseNotification):
|
|||||||
raise RuntimeError(f"error reading image: {e}")
|
raise RuntimeError(f"error reading image: {e}")
|
||||||
else:
|
else:
|
||||||
filename = basename(img_path)
|
filename = basename(img_path)
|
||||||
img.add_header(f"Content-ID", f"<{filename}>")
|
img.add_header("Content-ID", f"<{filename}>")
|
||||||
self.embedded_imgs.append(img)
|
self.embedded_imgs.append(img)
|
||||||
|
|
||||||
def get_email_body_soup(self, qid, msg):
|
def get_email_body_soup(self, qid, msg):
|
||||||
@@ -244,7 +244,8 @@ class EMailNotification(BaseNotification):
|
|||||||
f"{qid}: content type is {content_type}, "
|
f"{qid}: content type is {content_type}, "
|
||||||
f"converting to text/html")
|
f"converting to text/html")
|
||||||
content = re.sub(r"^(.*)$", r"\1<br/>",
|
content = re.sub(r"^(.*)$", r"\1<br/>",
|
||||||
escape(content), flags=re.MULTILINE)
|
escape(content, quote=False),
|
||||||
|
flags=re.MULTILINE)
|
||||||
else:
|
else:
|
||||||
self.logger.debug(
|
self.logger.debug(
|
||||||
f"{qid}: content type is {content_type}")
|
f"{qid}: content type is {content_type}")
|
||||||
@@ -317,7 +318,7 @@ class EMailNotification(BaseNotification):
|
|||||||
|
|
||||||
# extract body from email
|
# extract body from email
|
||||||
soup = self.get_email_body_soup(
|
soup = self.get_email_body_soup(
|
||||||
qid, email.message_from_binary_file(fp, policy=policy.default))
|
qid, email.message_from_binary_file(fp, policy=default_policy))
|
||||||
|
|
||||||
# replace picture sources
|
# replace picture sources
|
||||||
image_replaced = False
|
image_replaced = False
|
||||||
@@ -353,22 +354,24 @@ class EMailNotification(BaseNotification):
|
|||||||
variables = defaultdict(
|
variables = defaultdict(
|
||||||
str,
|
str,
|
||||||
EMAIL_HTML_TEXT=sanitized_text,
|
EMAIL_HTML_TEXT=sanitized_text,
|
||||||
EMAIL_FROM=escape(headers["from"]),
|
EMAIL_FROM=escape(headers["from"], quote=False),
|
||||||
EMAIL_ENVELOPE_FROM=escape(mailfrom),
|
EMAIL_ENVELOPE_FROM=escape(mailfrom, quote=False),
|
||||||
EMAIL_ENVELOPE_FROM_URL=escape(quote(mailfrom)),
|
EMAIL_ENVELOPE_FROM_URL=escape(quote(mailfrom),
|
||||||
EMAIL_TO=escape(headers["to"]),
|
quote=False),
|
||||||
EMAIL_ENVELOPE_TO=escape(recipient),
|
EMAIL_TO=escape(headers["to"], quote=False),
|
||||||
|
EMAIL_ENVELOPE_TO=escape(recipient, quote=False),
|
||||||
EMAIL_ENVELOPE_TO_URL=escape(quote(recipient)),
|
EMAIL_ENVELOPE_TO_URL=escape(quote(recipient)),
|
||||||
EMAIL_SUBJECT=escape(headers["subject"]),
|
EMAIL_SUBJECT=escape(headers["subject"], quote=False),
|
||||||
EMAIL_QUARANTINE_ID=storage_id)
|
EMAIL_QUARANTINE_ID=storage_id)
|
||||||
|
|
||||||
if subgroups:
|
if subgroups:
|
||||||
number = 0
|
number = 0
|
||||||
for subgroup in subgroups:
|
for subgroup in subgroups:
|
||||||
variables[f"SUBGROUP_{number}"] = escape(subgroup)
|
variables[f"SUBGROUP_{number}"] = escape(subgroup,
|
||||||
|
quote=False)
|
||||||
if named_subgroups:
|
if named_subgroups:
|
||||||
for key, value in named_subgroups.items():
|
for key, value in named_subgroups.items():
|
||||||
named_subgroups[key] = escape(value)
|
named_subgroups[key] = escape(value, quote=False)
|
||||||
variables.update(named_subgroups)
|
variables.update(named_subgroups)
|
||||||
|
|
||||||
# parse template
|
# parse template
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import sys
|
|||||||
|
|
||||||
import pyquarantine
|
import pyquarantine
|
||||||
|
|
||||||
from pyquarantine.version import __version__ as version
|
from pyquarantine import __version__ as version
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
__version__ = "1.0.3"
|
|
||||||
2
setup.cfg
Normal file
2
setup.cfg
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[metadata]
|
||||||
|
version = attr: pyquarantine.__version__
|
||||||
3
setup.py
3
setup.py
@@ -4,11 +4,8 @@ def read_file(fname):
|
|||||||
with open(fname, 'r') as f:
|
with open(fname, 'r') as f:
|
||||||
return f.read()
|
return f.read()
|
||||||
|
|
||||||
version = {}
|
|
||||||
exec(read_file("pyquarantine/version.py"), version)
|
|
||||||
|
|
||||||
setup(name = "pyquarantine",
|
setup(name = "pyquarantine",
|
||||||
version = version["__version__"],
|
|
||||||
author = "Thomas Oettli",
|
author = "Thomas Oettli",
|
||||||
author_email = "spacefreak@noop.ch",
|
author_email = "spacefreak@noop.ch",
|
||||||
description = "A pymilter based sendmail/postfix pre-queue filter.",
|
description = "A pymilter based sendmail/postfix pre-queue filter.",
|
||||||
|
|||||||
Reference in New Issue
Block a user