Added Send mail functionality & Dockerized application #1
129
.gitignore
vendored
Normal 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
|
@ -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
|
|||||||
|
USER service
|
||||||
PeterSurda marked this conversation as resolved
Outdated
PeterSurda
commented
run as non-root run as non-root
PeterSurda
commented
this doesn't do anything. The 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"]
|
|
@ -1,3 +1,9 @@
|
||||||
# influx-smtp-gateway
|
# influx-smtp-gateway
|
||||||
|
|
||||||
SMTP gateway accessible from InfluxDB for sending alerts.
|
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
PeterSurda
commented
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
PeterSurda
commented
add smtp server name add smtp server name
|
|||||||
|
FROM_MAIL_PASSWORD = test@123
|
9
docker-compose.yml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
smtp-gateway:
|
||||||
PeterSurda marked this conversation as resolved
Outdated
PeterSurda
commented
`smtp-gateway:`
|
|||||||
|
build: .
|
||||||
|
expose:
|
||||||
PeterSurda marked this conversation as resolved
Outdated
PeterSurda
commented
perhaps this should be inside the perhaps this should be inside the `Dockerfile`
|
|||||||
|
- "8081"
|
||||||
PeterSurda marked this conversation as resolved
Outdated
PeterSurda
commented
If we only use this internally inside a docker service, we should use If we only use this internally inside a docker service, we should use `expose`, not `port`.
|
|||||||
|
env_file:
|
||||||
|
- ./.env
|
2
entrypoint.sh
Executable file
|
@ -0,0 +1,2 @@
|
||||||
|
#!/bin/sh
|
||||||
|
python main.py
|
122
main.py
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
SMTP webhook server
|
||||||
PeterSurda marked this conversation as resolved
Outdated
PeterSurda
commented
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
PeterSurda
commented
class name class name
|
|||||||
|
logging.basicConfig(stream=sys.stdout,
|
||||||
PeterSurda marked this conversation as resolved
Outdated
PeterSurda
commented
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
PeterSurda
commented
docstring docstring
|
|||||||
|
|
||||||
|
|
||||||
PeterSurda marked this conversation as resolved
PeterSurda
commented
this should go under this should go under `__main__`
|
|||||||
|
class SMTPWebhookApp:
|
||||||
PeterSurda marked this conversation as resolved
Outdated
PeterSurda
commented
remove 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
PeterSurda
commented
override from env variables override from env variables
|
|||||||
|
"""
|
||||||
|
|
||||||
|
def _send_mail(self):
|
||||||
|
|
||||||
PeterSurda marked this conversation as resolved
Outdated
PeterSurda
commented
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
PeterSurda
commented
port 587 port 587
|
|||||||
|
return {"status": 400, "message": "Invalid content length."}
|
||||||
|
|
||||||
|
cl = cherrypy.request.headers['Content-Length']
|
||||||
PeterSurda marked this conversation as resolved
Outdated
PeterSurda
commented
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
PeterSurda
commented
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
PeterSurda
commented
- 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
PeterSurda
commented
docstring docstring
|
|||||||
|
body = req_body['body']
|
||||||
|
to_mail = req_body['to_mail']
|
||||||
|
|
||||||
|
try:
|
||||||
PeterSurda marked this conversation as resolved
Outdated
PeterSurda
commented
`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
PeterSurda
commented
`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
PeterSurda
commented
`except KeyError`
|
|||||||
|
msg['To'] = to_mail
|
||||||
|
try:
|
||||||
|
client.ehlo()
|
||||||
|
client.starttls()
|
||||||
|
client.ehlo()
|
||||||
PeterSurda marked this conversation as resolved
Outdated
PeterSurda
commented
`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
PeterSurda
commented
merge with previous 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
PeterSurda
commented
`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
PeterSurda
commented
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
PeterSurda
commented
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
PeterSurda
commented
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
|
@ -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
|
let's call it something else