Added Send mail functionality & Dockerized application #1
|
@ -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
|
|||||||
|
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 project
|
||||||
COPY . .
|
COPY . .
|
||||||
ENTRYPOINT ["/usr/src/app/entrypoint.sh", "--user"]
|
ENTRYPOINT ["/usr/src/app/entrypoint.sh"]
|
73
main.py
|
@ -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
PeterSurda
commented
class name class name
|
|||||||
|
logging.basicConfig(filename='app.log', filemode='w',
|
||||||
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:
|
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
|
|||||||
|
@ -19,51 +24,87 @@ class SMTPWebhookApp:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def _send_mail(self):
|
def _send_mail(self):
|
||||||
try:
|
|
||||||
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("To: {}, error: Invalid content length.".format(
|
||||||
PeterSurda marked this conversation as resolved
Outdated
PeterSurda
commented
port 587 port 587
|
|||||||
|
TO_MAIL))
|
||||||
|
return {"status": 400, "message": "Invalid 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
|
|||||||
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
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.
|
|||||||
|
if not req_body.get('body') or req_body.get('body') == '':
|
||||||
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
|
|||||||
|
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
PeterSurda
commented
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
PeterSurda
commented
`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
PeterSurda
commented
`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
PeterSurda
commented
`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
PeterSurda
commented
`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
PeterSurda
commented
merge with previous merge with previous `try`/`except`
|
|||||||
|
finally:
|
||||||
client.quit()
|
client.quit()
|
||||||
PeterSurda marked this conversation as resolved
Outdated
PeterSurda
commented
`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
PeterSurda
commented
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
PeterSurda
commented
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
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={})
|
cherrypy.tree.mount(ROOT, config={})
|
||||||
|
|
let's call it something else