Check in current version
This commit is contained in:
commit
dc15132da2
8 changed files with 197 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
*.pyc
|
||||||
|
__pycache__/
|
||||||
|
settings.ini
|
||||||
|
venv/
|
0
classes/__init__.py
Normal file
0
classes/__init__.py
Normal file
54
classes/config.py
Normal file
54
classes/config.py
Normal file
|
@ -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())
|
29
classes/smtp.py
Normal file
29
classes/smtp.py
Normal file
|
@ -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()
|
52
classes/vessel.py
Normal file
52
classes/vessel.py
Normal file
|
@ -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
|
1
requirements.txt
Normal file
1
requirements.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
paramiko
|
13
settings.dist.ini
Normal file
13
settings.dist.ini
Normal file
|
@ -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
|
44
worker.py
Normal file
44
worker.py
Normal file
|
@ -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}")
|
Loading…
Reference in a new issue