diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b6e4761 --- /dev/null +++ b/.gitignore @@ -0,0 +1,129 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2c6d462 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +# pull official base image +FROM python:3.9.6-alpine + +# set work directory +WORKDIR /usr/src/app + +# set environment variables +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 + +# install dependencies +RUN pip install --upgrade pip +COPY ./requirements.txt . +RUN pip install -r requirements.txt + +# add user +RUN adduser --disabled-password --gecos '' service +USER service + +# copy project +COPY . . +ENTRYPOINT ["/usr/src/app/entrypoint.sh"] \ No newline at end of file diff --git a/README.md b/README.md index 91d92b2..84c2dfe 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,9 @@ # influx-smtp-gateway -SMTP gateway accessible from InfluxDB for sending alerts. \ No newline at end of file +SMTP gateway accessible from InfluxDB for sending alerts. + +# create .env file with following parameters +SMTP_SERVER_HOST = smtp.gmail.com +TO_MAIL = test111@mailinator.com +FROM_MAIL = test@gmail.com +FROM_MAIL_PASSWORD = test@123 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..22e96bd --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,9 @@ +version: '3.8' + +services: + smtp-gateway: + build: . + expose: + - "8081" + env_file: + - ./.env diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100755 index 0000000..fe84e8b --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,2 @@ +#!/bin/sh +python main.py \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..e0554d3 --- /dev/null +++ b/main.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +""" +SMTP webhook server +""" + +import os +import json +import smtplib +import sys +import time +from email.header import Header +from email.mime.text import MIMEText + +import cherrypy +import logging + +logging.basicConfig(stream=sys.stdout, + format='%(name)s - %(levelname)s - %(message)s') + + +class SMTPWebhookApp: + """ + SMTP webhook server + """ + + def _send_mail(self): + + if not cherrypy.request.headers.get('Content-Length'): + logging.error("error: Invalid content length.") + return {"status": 400, "message": "Invalid content length."} + + cl = cherrypy.request.headers['Content-Length'] + rawbody = cherrypy.request.body.read(int(cl)) + req_body = json.loads(rawbody) + + if not req_body.get('subject') or req_body.get('subject') == '': + logging.error("error: body field is required.") + return {"status": 400, "message": "subject field is required."} + + if not req_body.get('body') or req_body.get('body') == '': + logging.error("error: body field is required.") + return {"status": 400, "message": "body field is required."} + + if not req_body.get('to_mail') or req_body.get('to_mail') == '': + logging.error("error: to_mail field is required.") + return {"status": 400, "message": "to_mail field is required."} + + subject = req_body['subject'] + body = req_body['body'] + to_mail = req_body['to_mail'] + + try: + client = smtplib.SMTP(host=SMTP_SERVER_HOST, port=SMTP_SERVER_PORT) + except (smtplib.SMTPConnectionError, TimeoutError) as e: + time.sleep(0.2) + logging.error("To: {}, error: {}".format(to_mail, e)) + return {"status": 400, "message": "SMTP client error: {}.".format( + e)} + + msg = MIMEText(body, 'plain', 'utf-8') + msg['Subject'] = Header(subject, 'utf-8') + msg['From'] = FROM_MAIL + msg['To'] = to_mail + try: + client.ehlo() + client.starttls() + client.ehlo() + client.login(msg["From"], FROM_MAIL_PASSWORD) + client.sendmail(msg['From'], msg['To'], msg.as_string()) + response = {"status": 200, "message": "mail sent successfully"} + logging.info("To: {}, mail sent successfully".format(to_mail)) + except smtplib.SMTPException as e: + time.sleep(0.2) + logging.error("To: {}, error: {}".format(to_mail, e)) + response = {"status": 500, "message": "some error: {}".format(e)} + finally: + client.quit() + return response + + @cherrypy.expose + def send_mail(self): + """ + api endpoint for send mail + """ + data = self._send_mail() + cherrypy.response.status = data["status"] + cherrypy.response.headers["Content-Type"] = "application/json" + return json.dumps(data["status"]).encode() + + +ROOT = SMTPWebhookApp() + +SMTP_SERVER_PORT = 587 +CHERRYPY_SERVER_HOST = "0.0.0.0" +CHERRYPY_SERVER_PORT = 8081 + +if __name__ == "__main__": + try: + SMTP_SERVER_HOST = os.environ["SMTP_SERVER_HOST"] + FROM_MAIL = os.environ["FROM_MAIL"] + FROM_MAIL_PASSWORD = os.environ["FROM_MAIL_PASSWORD"] + except KeyError: + raise KeyError("Please check missing environment variables: " + "SMTP_SERVER_HOST, FROM_MAIL, " + "FROM_MAIL_PASSWORD") + + cherrypy.server.socket_host = CHERRYPY_SERVER_HOST + cherrypy.server.socket_port = CHERRYPY_SERVER_PORT + ENGINE = cherrypy.engine + + cherrypy.tree.mount(ROOT, config={}) + + if hasattr(ENGINE, "signal_handler"): + ENGINE.signal_handler.subscribe() + if hasattr(ENGINE, "console_control_handler"): + ENGINE.console_control_handler.subscribe() + try: + ENGINE.start() + except Exception: # pylint: disable=broad-except + sys.exit(1) + else: + ENGINE.block() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d6e76e2 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,15 @@ +cheroot==8.6.0 +CherryPy==18.6.1 +importlib-resources==5.4.0 +jaraco.classes==3.2.1 +jaraco.collections==3.5.1 +jaraco.context==4.1.1 +jaraco.functools==3.5.0 +jaraco.text==3.7.0 +more-itertools==8.12.0 +portend==3.1.0 +pytz==2021.3 +six==1.16.0 +tempora==5.0.1 +zc.lockfile==2.0 +zipp==3.7.0