Added Send mail functionality & Dockerized application #1

Merged
PeterSurda merged 11 commits from cis-kuldeep/influx-smtp-gateway:master into master 2022-02-21 07:41:14 +01:00
7 changed files with 306 additions and 1 deletions

129
.gitignore vendored Normal file
View File

@ -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/

22
Dockerfile Normal file
View File

@ -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
PeterSurda marked this conversation as resolved Outdated

let's call it something else

let's call it something else
USER service
PeterSurda marked this conversation as resolved Outdated

run as non-root

run as non-root

this doesn't do anything. The Dockerfile should create a non-privileged user and then just before ENTRYPOINT have a corresponding USER instruction.

this doesn't do anything. The `Dockerfile` should create a non-privileged user and then just before `ENTRYPOINT` have a corresponding `USER` instruction.
# copy project
COPY . .
ENTRYPOINT ["/usr/src/app/entrypoint.sh"]

View File

@ -1,3 +1,9 @@
# influx-smtp-gateway
SMTP gateway accessible from InfluxDB for sending alerts.
# create .env file with following parameters
SMTP_SERVER_HOST = smtp.gmail.com
PeterSurda marked this conversation as resolved Outdated

remove as it's matched with docker service

remove as it's matched with docker service
TO_MAIL = test111@mailinator.com
FROM_MAIL = test@gmail.com
PeterSurda marked this conversation as resolved Outdated

add smtp server name

add smtp server name
FROM_MAIL_PASSWORD = test@123

9
docker-compose.yml Normal file
View File

@ -0,0 +1,9 @@
version: '3.8'
services:
smtp-gateway:
PeterSurda marked this conversation as resolved Outdated

smtp-gateway:

`smtp-gateway:`
build: .
expose:
PeterSurda marked this conversation as resolved Outdated

perhaps this should be inside the Dockerfile

perhaps this should be inside the `Dockerfile`
- "8081"
PeterSurda marked this conversation as resolved Outdated

If we only use this internally inside a docker service, we should use expose, not port.

If we only use this internally inside a docker service, we should use `expose`, not `port`.
env_file:
- ./.env

2
entrypoint.sh Executable file
View File

@ -0,0 +1,2 @@
#!/bin/sh
python main.py

122
main.py Normal file
View File

@ -0,0 +1,122 @@
#!/usr/bin/env python3
"""
SMTP webhook server
PeterSurda marked this conversation as resolved Outdated

change docstring

change docstring
"""
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
PeterSurda marked this conversation as resolved Outdated

class name

class name
logging.basicConfig(stream=sys.stdout,
PeterSurda marked this conversation as resolved Outdated

should log to stdout or stderr (research which one is more appropriate)

should log to stdout or stderr (research which one is more appropriate)
format='%(name)s - %(levelname)s - %(message)s')
PeterSurda marked this conversation as resolved Outdated

docstring

docstring
PeterSurda marked this conversation as resolved
Review

this should go under __main__

this should go under `__main__`
class SMTPWebhookApp:
PeterSurda marked this conversation as resolved Outdated

remove CONFIG and only use environment variables
if some variable missing, display a helpful error message and quit with non-zero error code

remove `CONFIG` and only use environment variables if some variable missing, display a helpful error message and quit with non-zero error code
"""
SMTP webhook server
PeterSurda marked this conversation as resolved Outdated

override from env variables

override from env variables
"""
def _send_mail(self):
PeterSurda marked this conversation as resolved Outdated

debug we don't need

debug we don't need
if not cherrypy.request.headers.get('Content-Length'):
logging.error("error: Invalid content length.")
PeterSurda marked this conversation as resolved Outdated

port 587

port 587
return {"status": 400, "message": "Invalid content length."}
cl = cherrypy.request.headers['Content-Length']
PeterSurda marked this conversation as resolved Outdated

this whole class we don't need

this whole class we don't need
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.")
PeterSurda marked this conversation as resolved Outdated

ideally an exception per line rather than together, then you can also use more specific exceptions.

ideally an exception per line rather than together, then you can also use more specific exceptions.
return {"status": 400, "message": "body field is required."}
PeterSurda marked this conversation as resolved Outdated
  • return an error code (in addition to error text) if there's an error
  • add a small delay if there's an error before returning, say 0.2s, and try to avoid sleep, instead look for a way to do this asynchronously
- return an error code (in addition to error text) if there's an error - add a small delay if there's an error before returning, say 0.2s, and try to avoid `sleep`, instead look for a way to do this asynchronously
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']
PeterSurda marked this conversation as resolved Outdated

docstring

docstring
body = req_body['body']
to_mail = req_body['to_mail']
try:
PeterSurda marked this conversation as resolved Outdated

except (SMTPConnectionError, TimeoutError) as e:

`except (SMTPConnectionError, TimeoutError) as e:`
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(
PeterSurda marked this conversation as resolved Outdated

SERVER_PORT should default to 587 and be an integer.

`SERVER_PORT` should default to `587` and be an integer.
e)}
msg = MIMEText(body, 'plain', 'utf-8')
msg['Subject'] = Header(subject, 'utf-8')
msg['From'] = FROM_MAIL
PeterSurda marked this conversation as resolved Outdated

except KeyError

`except KeyError`
msg['To'] = to_mail
try:
client.ehlo()
client.starttls()
client.ehlo()
PeterSurda marked this conversation as resolved Outdated

except smtplib.SMTPException

`except smtplib.SMTPException`
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)
PeterSurda marked this conversation as resolved Outdated

merge with previous try/except

merge with previous `try`/`except`
logging.error("To: {}, error: {}".format(to_mail, e))
response = {"status": 500, "message": "some error: {}".format(e)}
PeterSurda marked this conversation as resolved Outdated

quit() should be in finally:

`quit()` should be in `finally:`
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()
PeterSurda marked this conversation as resolved Outdated

different name

different name
SMTP_SERVER_PORT = 587
CHERRYPY_SERVER_HOST = "0.0.0.0"
CHERRYPY_SERVER_PORT = 8081
if __name__ == "__main__":
try:
PeterSurda marked this conversation as resolved Outdated

remove

remove
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
PeterSurda marked this conversation as resolved Outdated

live data shouldn't be here, squash to get rid of it

live data shouldn't be here, squash to get rid of it
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()

15
requirements.txt Normal file
View File

@ -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