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
2 changed files with 70 additions and 25 deletions
Showing only changes of commit e7fe463b82 - Show all commits

View File

@ -13,6 +13,10 @@ RUN pip install --upgrade pip
COPY ./requirements.txt . COPY ./requirements.txt .
RUN pip install -r 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 project
COPY . . COPY . .
ENTRYPOINT ["/usr/src/app/entrypoint.sh", "--user"] ENTRYPOINT ["/usr/src/app/entrypoint.sh"]

73
main.py
View File

@ -7,10 +7,15 @@ import os
import json import json
import smtplib import smtplib
import sys import sys
import time
from email.header import Header from email.header import Header
from email.mime.text import MIMEText from email.mime.text import MIMEText
import cherrypy import cherrypy
import logging
PeterSurda marked this conversation as resolved Outdated

class name

class name
logging.basicConfig(filename='app.log', filemode='w',
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: 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
@ -19,51 +24,87 @@ class SMTPWebhookApp:
""" """
def _send_mail(self): def _send_mail(self):
try:
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("To: {}, error: Invalid content length.".format(
PeterSurda marked this conversation as resolved Outdated

port 587

port 587
TO_MAIL))
return {"status": 400, "message": "Invalid content length."}
PeterSurda marked this conversation as resolved Outdated

this whole class we don't need

this whole class we don't need
cl = cherrypy.request.headers['Content-Length'] cl = cherrypy.request.headers['Content-Length']
rawbody = cherrypy.request.body.read(int(cl)) rawbody = cherrypy.request.body.read(int(cl))
req_body = json.loads(rawbody) req_body = json.loads(rawbody)
if not req_body.get('subject') or req_body.get('subject') == '':
logging.error("To: {}, error: body field is required.".format(
TO_MAIL))
return {"status": 400, "message": "subject 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.
if not req_body.get('body') or req_body.get('body') == '':
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
logging.error("To: {}, error: body field is required.".format(
TO_MAIL))
return {"status": 400, "message": "body field is required."}
subject = req_body['subject'] subject = req_body['subject']
body = req_body['body'] body = req_body['body']
PeterSurda marked this conversation as resolved Outdated

docstring

docstring
client = smtplib.SMTP(host=SERVER_HOST, port=SERVER_PORT)
try:
client = smtplib.SMTP(host=SMTP_SERVER_HOST, port=SMTP_SERVER_PORT)
except Exception as e:
PeterSurda marked this conversation as resolved Outdated

except (SMTPConnectionError, TimeoutError) as e:

`except (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)}
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.
msg = MIMEText(body, 'plain', 'utf-8') msg = MIMEText(body, 'plain', 'utf-8')
msg['Subject'] = Header(subject, 'utf-8') msg['Subject'] = Header(subject, 'utf-8')
msg['From'] = FROM_MAIL msg['From'] = FROM_MAIL
msg['To'] = TO_MAIL msg['To'] = TO_MAIL
try:
PeterSurda marked this conversation as resolved Outdated

except KeyError

`except KeyError`
client.ehlo() client.ehlo()
client.starttls() client.starttls()
client.ehlo() client.ehlo()
client.login(msg["From"], FROM_MAIL_PASSWORD) client.login(msg["From"], FROM_MAIL_PASSWORD)
client.sendmail(msg['From'], msg['To'], msg.as_string()) client.sendmail(msg['From'], msg['To'], msg.as_string())
PeterSurda marked this conversation as resolved Outdated

except smtplib.SMTPException

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

merge with previous try/except

merge with previous `try`/`except`
finally:
client.quit() client.quit()
PeterSurda marked this conversation as resolved Outdated

quit() should be in finally:

`quit()` should be in `finally:`
return "mail sent successfully" return response
except Exception as e:
return "some error: {}".format(e)
@cherrypy.expose @cherrypy.expose
def send_mail(self): def send_mail(self):
""" """
api endpoint for send mail api endpoint for send mail
""" """
return self._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() ROOT = SMTPWebhookApp()
SMTP_SERVER_PORT = 587
CHERRYPY_SERVER_HOST = "0.0.0.0"
PeterSurda marked this conversation as resolved Outdated

different name

different name
CHERRYPY_SERVER_PORT = 8081
if __name__ == "__main__": if __name__ == "__main__":
try: try:
SERVER_HOST = os.environ["server_host"] SMTP_SERVER_HOST = os.environ["SMTP_SERVER_HOST"]
SERVER_PORT = os.environ["server_port"] TO_MAIL = os.environ["TO_MAIL"]
PeterSurda marked this conversation as resolved Outdated

remove

remove
TO_MAIL = os.environ["to_mail"] FROM_MAIL = os.environ["FROM_MAIL"]
FROM_MAIL = os.environ["from_mail"] FROM_MAIL_PASSWORD = os.environ["FROM_MAIL_PASSWORD"]
FROM_MAIL_PASSWORD = os.environ["from_mail_password"]
except KeyError: except KeyError:
raise KeyError("Please check missing environment variables: to_mail, " raise KeyError("Please check missing environment variables: "
"from_mail, from_mail_password") "SMTP_SERVER_HOST, TO_MAIL, FROM_MAIL, "
"FROM_MAIL_PASSWORD")
cherrypy.server.socket_host = "0.0.0.0" cherrypy.server.socket_host = CHERRYPY_SERVER_HOST
cherrypy.server.socket_port = 8081 cherrypy.server.socket_port = CHERRYPY_SERVER_PORT
ENGINE = cherrypy.engine 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={}) cherrypy.tree.mount(ROOT, config={})