commit dc15132da2c50d56070007336e33be63bbee7c5b Author: Kumi Date: Sun Aug 21 09:46:00 2022 +0000 Check in current version diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..520949f --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.pyc +__pycache__/ +settings.ini +venv/ \ No newline at end of file diff --git a/classes/__init__.py b/classes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/classes/config.py b/classes/config.py new file mode 100644 index 0000000..dcbb39c --- /dev/null +++ b/classes/config.py @@ -0,0 +1,54 @@ +from configparser import ConfigParser +from json import loads +from pathlib import Path + +import socket + +from .vessel import Vessel + + +class Config: + @classmethod + def fromFile(cls, path): + parser = ConfigParser() + parser.read(path) + return cls(parser) + + def __init__(self, config): + self._config = config + + @property + def vessels(self): + out = list() + + for section in filter(lambda x: x.startswith("Vessel "), self._config.sections()): + out.append(Vessel.fromConfig(self._config[section])) + + return out + + def getTempDir(self): + return Path(self._config["FILEMAILER"].get("TempDir", fallback="/tmp/filemailer/")) + + def getMailServer(self): + return self._config["FILEMAILER"].get("Server", fallback="localhost") + + def getMailPort(self): + return int(self._config["FILEMAILER"].get("Port", fallback=0)) + + def getMailSSL(self): + return bool(int(self._config["FILEMAILER"].get("SSL", fallback=0))) + + def getMailUsername(self): + return self._config["FILEMAILER"].get("Username") + + def getMailPassword(self): + return self._config["FILEMAILER"].get("Password") + + def getMailSender(self): + return self._config["FILEMAILER"].get("Sender") + + def getBCC(self): + return loads(self._config.get("FILEMAILER", "BCC", fallback="[]")) + + def getHostname(self): + return self._config.get("FILEMAILER", "Hostname", fallback=socket.gethostname()) \ No newline at end of file diff --git a/classes/smtp.py b/classes/smtp.py new file mode 100644 index 0000000..086d7b7 --- /dev/null +++ b/classes/smtp.py @@ -0,0 +1,29 @@ +import smtplib + + +class SMTP: + @classmethod + def fromConfig(cls, config): + return cls(config.getMailServer(), config.getMailUsername(), config.getMailPassword(), config.getMailSender(), config.getMailPort(), config.getMailSSL()) + + def __init__(self, host, username=None, password=None, sender=None, port=None, ssl=None): + port = 0 if port is None else port + ssl = bool(ssl) + + smtpclass = smtplib.SMTP_SSL if ssl else smtplib.SMTP + + self.connection = smtpclass(host, port) + self.connection.login(username, password) + + self.sender = sender + + def send_message(self, message, *args, **kwargs): + if not message.get("From"): + message["From"] = self.sender + elif message["From"] == "None": + message.replace_header("From", self.sender) + + self.connection.send_message(message, *args, **kwargs) + + def __del__(self): + self.connection.close() \ No newline at end of file diff --git a/classes/vessel.py b/classes/vessel.py new file mode 100644 index 0000000..07b9cff --- /dev/null +++ b/classes/vessel.py @@ -0,0 +1,52 @@ +from configparser import SectionProxy +from typing import Optional, Union +from pathlib import Path + +from paramiko.client import SSHClient + +import time + + +class Vessel: + @classmethod + def fromConfig(cls, config: SectionProxy): + name = config.name + host = config["Host"] + username = config.get("Username") + sourcedir = config.get("SourceDir") + return cls(name, host, username, sourcedir) + + def __init__(self, name: str, host: str, username: Optional[str] = None, sourcedir: Optional[Union[str, Path]] = None): + self.name = name + self.host = host + self.username = username or "filemailer" + self.sourcedir = str(sourcedir) if sourcedir else "/var/filemailer" + + self._ssh = None + self._sftp = None + + def connect(self): + self._ssh = SSHClient() + self._ssh.load_system_host_keys() + + try: + self._ssh.connect(self.host, username=self.username) + self._sftp = self._ssh.open_sftp() + except Exception as e: + raise Exception(f"Could not connect to {self.name} ({self.host}): {e}") + + def fetch(self, destination, retry=True): + try: + self._sftp.chdir(self.sourcedir) + files = self._sftp.listdir() + + time.sleep(3) # Make sure write operations are complete + + for f in files: + self._sftp.get(f, str(destination / f.split("/")[-1])) + self._sftp.remove(f) + + except Exception as e: + if retry: + return self.fetch(destination, False) + raise \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8608c1b --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +paramiko diff --git a/settings.dist.ini b/settings.dist.ini new file mode 100644 index 0000000..cd77ba3 --- /dev/null +++ b/settings.dist.ini @@ -0,0 +1,13 @@ +[FILEMAILER] +Hostname = FileMailerServer +TempDir = /tmp/FileMailer/ +Sender = filemailer@example.com +Server = kumi.email +Username = filemailer@example.com +Password = Pa$$w0rd! +SSL = 1 +Port = 465 +BCC = ["stalker@example.com"] + +[Vessel TestVessel] +Host = 10.11.12.13 diff --git a/worker.py b/worker.py new file mode 100644 index 0000000..0a8181e --- /dev/null +++ b/worker.py @@ -0,0 +1,44 @@ +from json import loads +from email.parser import Parser as EmailParser +from email.utils import formatdate + +from classes.config import Config +from classes.smtp import SMTP + + +config = Config.fromFile("settings.ini") + +path = config.getTempDir() +path.mkdir(exist_ok=True) + +smtp = SMTP.fromConfig(config) + +for vessel in config.vessels: + try: + vessel.connect() + vessel.fetch(path) + except Exception as e: + print(f"SFTP operations failed on {vessel.host}: {e}") + + +for eml in sorted(filter(lambda x: x.with_suffix(".json").exists(), path.glob("*.eml")), key=lambda d: d.stat().st_mtime): + try: + with open(eml.resolve()) as contentfile: + content = contentfile.read() + with open(eml.with_suffix(".json").resolve()) as metafile: + meta = loads(metafile.read()) + + message = EmailParser().parsestr(content) + message.add_header("Received", f"by {config.getHostname()} (Kumi Systems FileMailer); {formatdate()}") + + for bcc in config.getBCC(): + if not bcc in meta["recipients"]: + meta["recipients"].append(bcc) + + smtp.send_message(message, from_addr=meta["sender"], to_addrs=meta["recipients"]) + + eml.with_suffix(".json").unlink() + eml.unlink() + + except Exception as e: + print(f"Could not process file {eml.resolve()}: {e}")