From 8cff562e6402e707ec738df9e34d1eea7e331fd5 Mon Sep 17 00:00:00 2001 From: "kuldeep.k@cisinlabs.com" Date: Thu, 3 Feb 2022 21:02:58 +0530 Subject: [PATCH 01/11] Updated code for send mail functionality --- config.ini | 9 +++ main.py | 171 +++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 18 +++++ 3 files changed, 198 insertions(+) create mode 100644 config.ini create mode 100644 main.py create mode 100644 requirements.txt diff --git a/config.ini b/config.ini new file mode 100644 index 0000000..d0d3fc1 --- /dev/null +++ b/config.ini @@ -0,0 +1,9 @@ +[server] +server_host = 0.0.0.0 +server_port = 8081 + +[app] +to_mail = test111@mailinator.com +from_mail = test@gmail.com +from_mail_password = test@123 + diff --git a/main.py b/main.py new file mode 100644 index 0000000..009898e --- /dev/null +++ b/main.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python3 +""" +Serve cloud init files +""" + +import configparser +import os +import json +import smtplib +import socket +import sys +from email.header import Header +from email.mime.text import MIMEText + +import uuid as uuid_module +from ipaddress import AddressValueError, IPv4Address, IPv6Address + +import cherrypy + +PATH = os.path.dirname(os.path.abspath(__file__)) +CONFIG = configparser.ConfigParser() +CONFIG.read(os.path.join(PATH, "config.ini")) + +TO_MAIL = CONFIG["app"].get("to_mail") +FROM_MAIL = CONFIG["app"].get("from_mail") +FROM_MAIL_PASSWORD = CONFIG["app"].get("from_mail_password") +print("TO_MAIL: ", TO_MAIL) +print("FROM_MAIL: ", FROM_MAIL) +print("FROM_MAIL_PASSWORD: ", FROM_MAIL_PASSWORD) + + +class CloudInitRequest: + """ + Request data for persistence across methods + """ + def __init__(self, request, uuid=None): + self.remoteip = None + self.hostinfo = ('localhost', ) + self.request = request + self.meta_data = None + self.meta_data_loaded = False + self.user_data = None + + try: + self.uuid = str(uuid_module.UUID('{' + uuid + '}')) + # ValueError is wrong UUID syntax + # TypeError is None + except (ValueError, TypeError): + self.uuid = None + + self._init_ip() + + def _can_ip_be_proxy(self): + """ + Assuming the connection is through a proxy, is this proxy permitted? + Can't proxy from a publicly reachable IP. + """ + self.remoteip = self.request.remote.ip + try: + ipobj = IPv4Address(self.remoteip) + except AddressValueError: + try: + ipobj = IPv6Address(self.remoteip) + except AddressValueError: + return False + return not ipobj.is_global + + def _init_ip(self): + """ + Get remote IP + """ + if self._can_ip_be_proxy(): + try: + self.remoteip = self.request.headers.get( + 'X-Real-Ip', + self.request.remote.ip + ) + except KeyError: + pass + + try: + self.hostinfo = socket.gethostbyaddr(self.remoteip) + forward_lookup = socket.gethostbyname(self.hostinfo[0]) + if forward_lookup != self.remoteip: + self.hostinfo = ('localhost', ) + except socket.herror: + self.hostinfo = ('localhost', ) + except socket.gaierror: + self.hostinfo = (self.remoteip, ) + + +class CloudInitApp: + """ + Serve cloud init files + """ + + @staticmethod + def _content_type(data): + if not data: + return "text/cloud-config" + if data.startswith("#include"): + return "text/x-include-url" + if data.startswith("## template: jinja"): + return "text/jinja2" + return "text/cloud-config" + + def _send_mail(self): + try: + # pylint: disable=deprecated-lambda + cl = cherrypy.request.headers['Content-Length'] + rawbody = cherrypy.request.body.read(int(cl)) + req_body = json.loads(rawbody) + subject = req_body['subject'] + body = req_body['body'] + client = smtplib.SMTP('smtp.gmail.com') + msg = MIMEText(body, 'plain', 'utf-8') + msg['Subject'] = Header(subject, 'utf-8') + msg['From'] = FROM_MAIL + msg['To'] = TO_MAIL + + client.ehlo() + client.starttls() + client.ehlo() + client.login(msg["From"], FROM_MAIL_PASSWORD) + client.sendmail(msg['From'], msg['To'], msg.as_string()) + client.quit() + return "mail sent successfully" + except Exception as e: + return "some error: {}".format(e) + + @cherrypy.expose + def send_mail(self): + """ + v1 api endpoint user-data + """ + return self._send_mail() + + +ROOT = CloudInitApp() + +if __name__ == "__main__": + cherrypy.server.socket_host = \ + CONFIG["server"].get("server_host", "127.0.0.1") + cherrypy.server.socket_port = \ + CONFIG["server"].getint("server_port", 8081) + ENGINE = cherrypy.engine + + CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) + CONFIG = { + '/include': { + 'tools.staticdir.on': True, + 'tools.staticdir.dir': os.path.join(CURRENT_DIR, + 'data', + 'include'), + 'tools.staticdir.content_types': { + 'yml': 'text/yaml' + } + } + } + cherrypy.tree.mount(ROOT, config=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..ac9d4e8 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,18 @@ +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 +Jinja2==3.0.3 +MarkupSafe==2.0.1 +more-itertools==8.12.0 +portend==3.1.0 +pytz==2021.3 +PyYAML==6.0 +six==1.16.0 +tempora==5.0.1 +zc.lockfile==2.0 +zipp==3.7.0 -- 2.40.1 From 9e182c4b944a19668993f32a68b4cdb3836ffc39 Mon Sep 17 00:00:00 2001 From: "kuldeep.k@cisinlabs.com" Date: Thu, 3 Feb 2022 21:03:22 +0530 Subject: [PATCH 02/11] Dockerize application --- Dockerfile | 17 +++++++++++++++++ docker-compose.yml | 15 +++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 Dockerfile create mode 100644 docker-compose.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7ae8620 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +# 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 + +# copy project +COPY . . \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..0838092 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,15 @@ +version: '3.8' + +services: + web: + build: . + command: python main.py + volumes: + - mailsend_server:/usr/src/app/ + ports: + - 8081:8081 + env_file: + - ./config.ini + +volumes: + mailsend_server: \ No newline at end of file -- 2.40.1 From 65bedd63567153ecb8b24940d2d3baa904f3274e Mon Sep 17 00:00:00 2001 From: "kuldeep.k@cisinlabs.com" Date: Fri, 4 Feb 2022 22:46:58 +0530 Subject: [PATCH 03/11] Updated code to run based on entrypoint.sh --- Dockerfile | 3 ++- docker-compose.yml | 5 ----- entrypoint.sh | 2 ++ 3 files changed, 4 insertions(+), 6 deletions(-) create mode 100755 entrypoint.sh diff --git a/Dockerfile b/Dockerfile index 7ae8620..6837854 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,4 +14,5 @@ COPY ./requirements.txt . RUN pip install -r requirements.txt # copy project -COPY . . \ No newline at end of file +COPY . . +ENTRYPOINT ["/usr/src/app/entrypoint.sh"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 0838092..631e8a5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,12 +4,7 @@ services: web: build: . command: python main.py - volumes: - - mailsend_server:/usr/src/app/ ports: - 8081:8081 env_file: - ./config.ini - -volumes: - mailsend_server: \ No newline at end of file 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 -- 2.40.1 From 7c4be18a6de926fa56fd308c8c3dca8f52927859 Mon Sep 17 00:00:00 2001 From: "kuldeep.k@cisinlabs.com" Date: Mon, 7 Feb 2022 20:58:53 +0530 Subject: [PATCH 04/11] Updated docker-compose, removed config.ini and unuseful code --- .gitignore | 129 +++++++++++++++++++++++++++++++++++++++++++++ README.md | 9 +++- config.ini | 9 ---- docker-compose.yml | 3 +- main.py | 99 ++++------------------------------ requirements.txt | 3 -- 6 files changed, 149 insertions(+), 103 deletions(-) create mode 100644 .gitignore delete mode 100644 config.ini 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/README.md b/README.md index 91d92b2..477fc09 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,10 @@ # 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 +server_host = 0.0.0.0 +server_port = 8081 +to_mail = test111@mailinator.com +from_mail = test@gmail.com +from_mail_password = test@123 \ No newline at end of file diff --git a/config.ini b/config.ini deleted file mode 100644 index d0d3fc1..0000000 --- a/config.ini +++ /dev/null @@ -1,9 +0,0 @@ -[server] -server_host = 0.0.0.0 -server_port = 8081 - -[app] -to_mail = test111@mailinator.com -from_mail = test@gmail.com -from_mail_password = test@123 - diff --git a/docker-compose.yml b/docker-compose.yml index 631e8a5..97c700b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,8 +3,7 @@ version: '3.8' services: web: build: . - command: python main.py ports: - 8081:8081 env_file: - - ./config.ini + - ./.env diff --git a/main.py b/main.py index 009898e..8271238 100644 --- a/main.py +++ b/main.py @@ -12,98 +12,14 @@ import sys from email.header import Header from email.mime.text import MIMEText -import uuid as uuid_module -from ipaddress import AddressValueError, IPv4Address, IPv6Address - import cherrypy -PATH = os.path.dirname(os.path.abspath(__file__)) -CONFIG = configparser.ConfigParser() -CONFIG.read(os.path.join(PATH, "config.ini")) - -TO_MAIL = CONFIG["app"].get("to_mail") -FROM_MAIL = CONFIG["app"].get("from_mail") -FROM_MAIL_PASSWORD = CONFIG["app"].get("from_mail_password") -print("TO_MAIL: ", TO_MAIL) -print("FROM_MAIL: ", FROM_MAIL) -print("FROM_MAIL_PASSWORD: ", FROM_MAIL_PASSWORD) - - -class CloudInitRequest: - """ - Request data for persistence across methods - """ - def __init__(self, request, uuid=None): - self.remoteip = None - self.hostinfo = ('localhost', ) - self.request = request - self.meta_data = None - self.meta_data_loaded = False - self.user_data = None - - try: - self.uuid = str(uuid_module.UUID('{' + uuid + '}')) - # ValueError is wrong UUID syntax - # TypeError is None - except (ValueError, TypeError): - self.uuid = None - - self._init_ip() - - def _can_ip_be_proxy(self): - """ - Assuming the connection is through a proxy, is this proxy permitted? - Can't proxy from a publicly reachable IP. - """ - self.remoteip = self.request.remote.ip - try: - ipobj = IPv4Address(self.remoteip) - except AddressValueError: - try: - ipobj = IPv6Address(self.remoteip) - except AddressValueError: - return False - return not ipobj.is_global - - def _init_ip(self): - """ - Get remote IP - """ - if self._can_ip_be_proxy(): - try: - self.remoteip = self.request.headers.get( - 'X-Real-Ip', - self.request.remote.ip - ) - except KeyError: - pass - - try: - self.hostinfo = socket.gethostbyaddr(self.remoteip) - forward_lookup = socket.gethostbyname(self.hostinfo[0]) - if forward_lookup != self.remoteip: - self.hostinfo = ('localhost', ) - except socket.herror: - self.hostinfo = ('localhost', ) - except socket.gaierror: - self.hostinfo = (self.remoteip, ) - class CloudInitApp: """ Serve cloud init files """ - @staticmethod - def _content_type(data): - if not data: - return "text/cloud-config" - if data.startswith("#include"): - return "text/x-include-url" - if data.startswith("## template: jinja"): - return "text/jinja2" - return "text/cloud-config" - def _send_mail(self): try: # pylint: disable=deprecated-lambda @@ -139,10 +55,17 @@ class CloudInitApp: ROOT = CloudInitApp() if __name__ == "__main__": - cherrypy.server.socket_host = \ - CONFIG["server"].get("server_host", "127.0.0.1") - cherrypy.server.socket_port = \ - CONFIG["server"].getint("server_port", 8081) + try: + SERVER_HOST = os.environ["server_host"] + SERVER_PORT = int(os.environ["server_port"]) + TO_MAIL = os.environ["to_mail"] + FROM_MAIL = os.environ["from_mail"] + FROM_MAIL_PASSWORD = os.environ["from_mail_password"] + except: + raise "Please check missing environment variables: to_mail, from_mail, from_mail_password" + + cherrypy.server.socket_host = SERVER_HOST + cherrypy.server.socket_port = SERVER_PORT ENGINE = cherrypy.engine CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) diff --git a/requirements.txt b/requirements.txt index ac9d4e8..d6e76e2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,12 +6,9 @@ jaraco.collections==3.5.1 jaraco.context==4.1.1 jaraco.functools==3.5.0 jaraco.text==3.7.0 -Jinja2==3.0.3 -MarkupSafe==2.0.1 more-itertools==8.12.0 portend==3.1.0 pytz==2021.3 -PyYAML==6.0 six==1.16.0 tempora==5.0.1 zc.lockfile==2.0 -- 2.40.1 From 5881dc7b1a300607366996b0879025d3d41738fc Mon Sep 17 00:00:00 2001 From: "kuldeep.k@cisinlabs.com" Date: Tue, 8 Feb 2022 18:36:05 +0530 Subject: [PATCH 05/11] Removed some unuseful imports, signal handler and config dict --- main.py | 25 ++++--------------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/main.py b/main.py index 8271238..af1688c 100644 --- a/main.py +++ b/main.py @@ -3,11 +3,9 @@ Serve cloud init files """ -import configparser import os import json import smtplib -import socket import sys from email.header import Header from email.mime.text import MIMEText @@ -61,31 +59,16 @@ if __name__ == "__main__": TO_MAIL = os.environ["to_mail"] FROM_MAIL = os.environ["from_mail"] FROM_MAIL_PASSWORD = os.environ["from_mail_password"] - except: - raise "Please check missing environment variables: to_mail, from_mail, from_mail_password" + except: # noqa:E722 + raise "Please check missing environment variables: to_mail, from_mail, \ + from_mail_password" cherrypy.server.socket_host = SERVER_HOST cherrypy.server.socket_port = SERVER_PORT ENGINE = cherrypy.engine - CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) - CONFIG = { - '/include': { - 'tools.staticdir.on': True, - 'tools.staticdir.dir': os.path.join(CURRENT_DIR, - 'data', - 'include'), - 'tools.staticdir.content_types': { - 'yml': 'text/yaml' - } - } - } - cherrypy.tree.mount(ROOT, config=CONFIG) + 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 -- 2.40.1 From 8488446e3728d42466b6d32517c9d22c0cac71d1 Mon Sep 17 00:00:00 2001 From: "kuldeep.k@cisinlabs.com" Date: Wed, 9 Feb 2022 22:50:16 +0530 Subject: [PATCH 06/11] Updated changes for docsrting & port updates --- README.md | 3 +-- docker-compose.yml | 4 ++-- main.py | 25 +++++++++++++------------ 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 477fc09..e188f42 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,7 @@ SMTP gateway accessible from InfluxDB for sending alerts. # create .env file with following parameters -server_host = 0.0.0.0 -server_port = 8081 +server_name = 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 index 97c700b..4f3f7f1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,7 +3,7 @@ version: '3.8' services: web: build: . - ports: - - 8081:8081 + expose: + - "8081" env_file: - ./.env diff --git a/main.py b/main.py index af1688c..18cd383 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """ -Serve cloud init files +SMTP webhook server """ import os @@ -13,20 +13,19 @@ from email.mime.text import MIMEText import cherrypy -class CloudInitApp: +class SMTPWebhookApp: """ - Serve cloud init files + SMTP webhook server """ def _send_mail(self): try: - # pylint: disable=deprecated-lambda cl = cherrypy.request.headers['Content-Length'] rawbody = cherrypy.request.body.read(int(cl)) req_body = json.loads(rawbody) subject = req_body['subject'] body = req_body['body'] - client = smtplib.SMTP('smtp.gmail.com') + client = smtplib.SMTP(SERVER_NAME) msg = MIMEText(body, 'plain', 'utf-8') msg['Subject'] = Header(subject, 'utf-8') msg['From'] = FROM_MAIL @@ -45,26 +44,28 @@ class CloudInitApp: @cherrypy.expose def send_mail(self): """ - v1 api endpoint user-data + api endpoint for send mail """ return self._send_mail() -ROOT = CloudInitApp() +ROOT = SMTPWebhookApp() if __name__ == "__main__": try: - SERVER_HOST = os.environ["server_host"] - SERVER_PORT = int(os.environ["server_port"]) + SERVER_NAME = os.environ["server_name"] TO_MAIL = os.environ["to_mail"] FROM_MAIL = os.environ["from_mail"] FROM_MAIL_PASSWORD = os.environ["from_mail_password"] - except: # noqa:E722 + # to_mail = "test111@mailinator.com" + # from_mail = "test@gmail.com" + # from_mail_password = "test@123" + except KeyError: raise "Please check missing environment variables: to_mail, from_mail, \ from_mail_password" - cherrypy.server.socket_host = SERVER_HOST - cherrypy.server.socket_port = SERVER_PORT + cherrypy.server.socket_host = "0.0.0.0" + cherrypy.server.socket_port = 8081 ENGINE = cherrypy.engine cherrypy.tree.mount(ROOT, config={}) -- 2.40.1 From 44df0eed25a3bcdc5b4706a05a37c9ba7c3d66f1 Mon Sep 17 00:00:00 2001 From: "kuldeep.k@cisinlabs.com" Date: Thu, 10 Feb 2022 21:16:00 +0530 Subject: [PATCH 07/11] Updated docker-compose entrypoint to run as entrypoint, server_port for smtp function and exception handling update --- Dockerfile | 2 +- docker-compose.yml | 2 +- main.py | 12 +++++------- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index 6837854..43bbec8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,4 +15,4 @@ RUN pip install -r requirements.txt # copy project COPY . . -ENTRYPOINT ["/usr/src/app/entrypoint.sh"] \ No newline at end of file +ENTRYPOINT ["/usr/src/app/entrypoint.sh", "--user"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 4f3f7f1..22e96bd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ version: '3.8' services: - web: + smtp-gateway: build: . expose: - "8081" diff --git a/main.py b/main.py index 18cd383..1fe6c87 100644 --- a/main.py +++ b/main.py @@ -25,7 +25,7 @@ class SMTPWebhookApp: req_body = json.loads(rawbody) subject = req_body['subject'] body = req_body['body'] - client = smtplib.SMTP(SERVER_NAME) + client = smtplib.SMTP(host=SERVER_HOST, port=SERVER_PORT) msg = MIMEText(body, 'plain', 'utf-8') msg['Subject'] = Header(subject, 'utf-8') msg['From'] = FROM_MAIL @@ -53,16 +53,14 @@ ROOT = SMTPWebhookApp() if __name__ == "__main__": try: - SERVER_NAME = os.environ["server_name"] + SERVER_HOST = os.environ["server_host"] + SERVER_PORT = os.environ["server_port"] TO_MAIL = os.environ["to_mail"] FROM_MAIL = os.environ["from_mail"] FROM_MAIL_PASSWORD = os.environ["from_mail_password"] - # to_mail = "test111@mailinator.com" - # from_mail = "test@gmail.com" - # from_mail_password = "test@123" except KeyError: - raise "Please check missing environment variables: to_mail, from_mail, \ - from_mail_password" + raise KeyError("Please check missing environment variables: to_mail, " + "from_mail, from_mail_password") cherrypy.server.socket_host = "0.0.0.0" cherrypy.server.socket_port = 8081 -- 2.40.1 From e7fe463b820efb72703ddb04960d4773f02031ad Mon Sep 17 00:00:00 2001 From: "kuldeep.k@cisinlabs.com" Date: Fri, 11 Feb 2022 22:44:20 +0530 Subject: [PATCH 08/11] Updated dockerfile and Added error handling & logging --- Dockerfile | 6 +++- main.py | 89 +++++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 70 insertions(+), 25 deletions(-) diff --git a/Dockerfile b/Dockerfile index 43bbec8..2c6d462 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,6 +13,10 @@ 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", "--user"] \ No newline at end of file +ENTRYPOINT ["/usr/src/app/entrypoint.sh"] \ No newline at end of file diff --git a/main.py b/main.py index 1fe6c87..9707add 100644 --- a/main.py +++ b/main.py @@ -7,10 +7,15 @@ 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(filename='app.log', filemode='w', + format='%(name)s - %(levelname)s - %(message)s') class SMTPWebhookApp: @@ -19,51 +24,87 @@ class SMTPWebhookApp: """ def _send_mail(self): - try: - cl = cherrypy.request.headers['Content-Length'] - rawbody = cherrypy.request.body.read(int(cl)) - req_body = json.loads(rawbody) - subject = req_body['subject'] - body = req_body['body'] - client = smtplib.SMTP(host=SERVER_HOST, port=SERVER_PORT) - msg = MIMEText(body, 'plain', 'utf-8') - msg['Subject'] = Header(subject, 'utf-8') - msg['From'] = FROM_MAIL - msg['To'] = TO_MAIL + if not cherrypy.request.headers.get('Content-Length'): + logging.error("To: {}, error: Invalid content length.".format( + TO_MAIL)) + 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("To: {}, error: body field is required.".format( + TO_MAIL)) + return {"status": 400, "message": "subject field is required."} + + if not req_body.get('body') or req_body.get('body') == '': + logging.error("To: {}, error: body field is required.".format( + TO_MAIL)) + return {"status": 400, "message": "body field is required."} + + subject = req_body['subject'] + body = req_body['body'] + + try: + client = smtplib.SMTP(host=SMTP_SERVER_HOST, port=SMTP_SERVER_PORT) + except Exception 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 "mail sent successfully" - except Exception as e: - return "some error: {}".format(e) + return response @cherrypy.expose def send_mail(self): """ 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() +SMTP_SERVER_PORT = 587 +CHERRYPY_SERVER_HOST = "0.0.0.0" +CHERRYPY_SERVER_PORT = 8081 + if __name__ == "__main__": try: - SERVER_HOST = os.environ["server_host"] - SERVER_PORT = os.environ["server_port"] - TO_MAIL = os.environ["to_mail"] - FROM_MAIL = os.environ["from_mail"] - FROM_MAIL_PASSWORD = os.environ["from_mail_password"] + SMTP_SERVER_HOST = os.environ["SMTP_SERVER_HOST"] + TO_MAIL = os.environ["TO_MAIL"] + FROM_MAIL = os.environ["FROM_MAIL"] + FROM_MAIL_PASSWORD = os.environ["FROM_MAIL_PASSWORD"] except KeyError: - raise KeyError("Please check missing environment variables: to_mail, " - "from_mail, from_mail_password") + raise KeyError("Please check missing environment variables: " + "SMTP_SERVER_HOST, TO_MAIL, FROM_MAIL, " + "FROM_MAIL_PASSWORD") - cherrypy.server.socket_host = "0.0.0.0" - cherrypy.server.socket_port = 8081 + cherrypy.server.socket_host = CHERRYPY_SERVER_HOST + cherrypy.server.socket_port = CHERRYPY_SERVER_PORT ENGINE = cherrypy.engine cherrypy.tree.mount(ROOT, config={}) -- 2.40.1 From c3830ce249b987d75aef89697725e91ba7eb2c3e Mon Sep 17 00:00:00 2001 From: "kuldeep.k@cisinlabs.com" Date: Wed, 16 Feb 2022 13:19:41 +0530 Subject: [PATCH 09/11] Updated readme file --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e188f42..84c2dfe 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ SMTP gateway accessible from InfluxDB for sending alerts. # create .env file with following parameters -server_name = smtp.gmail.com -to_mail = test111@mailinator.com -from_mail = test@gmail.com -from_mail_password = test@123 \ No newline at end of file +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 -- 2.40.1 From 1d698187183b36c88c1a05cb5b0c5d1700b579fe Mon Sep 17 00:00:00 2001 From: "kuldeep.k@cisinlabs.com" Date: Thu, 17 Feb 2022 22:31:39 +0530 Subject: [PATCH 10/11] Updated signal_handler subscription, specific exceptions & logging stream --- main.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index 9707add..1904483 100644 --- a/main.py +++ b/main.py @@ -14,7 +14,7 @@ from email.mime.text import MIMEText import cherrypy import logging -logging.basicConfig(filename='app.log', filemode='w', +logging.basicConfig(stream=sys.stdout, format='%(name)s - %(levelname)s - %(message)s') @@ -49,7 +49,7 @@ class SMTPWebhookApp: try: client = smtplib.SMTP(host=SMTP_SERVER_HOST, port=SMTP_SERVER_PORT) - except Exception as e: + 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( @@ -109,6 +109,10 @@ if __name__ == "__main__": 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 -- 2.40.1 From f4fc89c111f47a48cb582d65089661c22a92381c Mon Sep 17 00:00:00 2001 From: "kuldeep.k@cisinlabs.com" Date: Fri, 18 Feb 2022 21:40:06 +0530 Subject: [PATCH 11/11] Updated to_mail field changes & logging changes --- main.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/main.py b/main.py index 1904483..e0554d3 100644 --- a/main.py +++ b/main.py @@ -26,8 +26,7 @@ class SMTPWebhookApp: def _send_mail(self): if not cherrypy.request.headers.get('Content-Length'): - logging.error("To: {}, error: Invalid content length.".format( - TO_MAIL)) + logging.error("error: Invalid content length.") return {"status": 400, "message": "Invalid content length."} cl = cherrypy.request.headers['Content-Length'] @@ -35,30 +34,33 @@ class SMTPWebhookApp: 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)) + 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("To: {}, error: body field is required.".format( - TO_MAIL)) + 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)) + 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 + msg['To'] = to_mail try: client.ehlo() client.starttls() @@ -66,10 +68,10 @@ class SMTPWebhookApp: 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)) + 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)) + logging.error("To: {}, error: {}".format(to_mail, e)) response = {"status": 500, "message": "some error: {}".format(e)} finally: client.quit() @@ -95,12 +97,11 @@ CHERRYPY_SERVER_PORT = 8081 if __name__ == "__main__": try: SMTP_SERVER_HOST = os.environ["SMTP_SERVER_HOST"] - TO_MAIL = os.environ["TO_MAIL"] 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, TO_MAIL, FROM_MAIL, " + "SMTP_SERVER_HOST, FROM_MAIL, " "FROM_MAIL_PASSWORD") cherrypy.server.socket_host = CHERRYPY_SERVER_HOST -- 2.40.1