From 6f8c4a31dbf863b99d57a432a8707590ed05af80 Mon Sep 17 00:00:00 2001 From: Alex Garcia Date: Fri, 1 Apr 2022 11:47:40 -0700 Subject: [PATCH 1/8] Update mobile carrier gateway addresses --- scrubdash/asyncio_server/notification.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/scrubdash/asyncio_server/notification.py b/scrubdash/asyncio_server/notification.py index 782a9a4..14fbee3 100644 --- a/scrubdash/asyncio_server/notification.py +++ b/scrubdash/asyncio_server/notification.py @@ -17,13 +17,13 @@ HOST = "smtp.gmail.com" # Exhaustive list of carriers: https://kb.sandisk.com/app/answers/detail/a_id/17056/~/list-of-mobile-carrier-gateway-addresses CARRIER_MAP = { - "verizon": "vtext.com", + "verizon": "vzwpix.com", "tmobile": "tmomail.net", - "sprint": "messaging.sprintpcs.com", - "at&t": "txt.att.net", - "boost": "smsmyboostmobile.com", - "cricket": "sms.cricketwireless.net", - "uscellular": "email.uscc.net", + "sprint": "pm.sprint.com", + "at&t": "mms.att.net", + "boost": "myboostmobile.com", + "cricket": "mms.mycricket.com", + "uscellular": "mms.uscc.net", } From fcda45a0541854e68fa27b2c28663802410b6595 Mon Sep 17 00:00:00 2001 From: Alex Garcia Date: Fri, 1 Apr 2022 12:15:52 -0700 Subject: [PATCH 2/8] Fix cricket's gateway address --- scrubdash/asyncio_server/notification.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scrubdash/asyncio_server/notification.py b/scrubdash/asyncio_server/notification.py index 14fbee3..149eeb5 100644 --- a/scrubdash/asyncio_server/notification.py +++ b/scrubdash/asyncio_server/notification.py @@ -22,7 +22,7 @@ "sprint": "pm.sprint.com", "at&t": "mms.att.net", "boost": "myboostmobile.com", - "cricket": "mms.mycricket.com", + "cricket": "mms.cricketwireless.net", "uscellular": "mms.uscc.net", } From 9102e2cffcc2d0ae9741936f264dd63df75a34f8 Mon Sep 17 00:00:00 2001 From: Alex Garcia Date: Fri, 1 Apr 2022 12:19:46 -0700 Subject: [PATCH 3/8] Change SMS to MMS - Change function name for sending MMS texts - Change log messages when sending MMS texts --- scrubdash/asyncio_server/notification.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scrubdash/asyncio_server/notification.py b/scrubdash/asyncio_server/notification.py index 149eeb5..1718562 100644 --- a/scrubdash/asyncio_server/notification.py +++ b/scrubdash/asyncio_server/notification.py @@ -75,7 +75,7 @@ def _get_datetime(self, image_path): return (date, time) - async def send_sms(self, hostname, image_path, detected_alert_classes): + async def send_mms(self, hostname, image_path, detected_alert_classes): """ Send an SMS notification to receivers listed in the `SMS_RECEIVERS` attribute. @@ -129,9 +129,9 @@ async def send_sms(self, hostname, image_path, detected_alert_classes): start_tls=True ) res = await aiosmtplib.send(message, **send_kws) # type: ignore - msg = ("failed to send sms to {}".format(num) + msg = ("Failed to send MMS to {}".format(num) if not re.search(r"\sOK\s", res[1]) - else "succeeded to send sms to {}".format(num)) + else "Successfully sent MMS to {}".format(num)) log.info(msg) def send_email(self, hostname, image_path, detected_alert_classes): From 67f216bf24a0d59b8f1d08d6b4329daa29de65bc Mon Sep 17 00:00:00 2001 From: Alex Garcia Date: Fri, 1 Apr 2022 14:50:20 -0700 Subject: [PATCH 4/8] Fix MMS transmission - Add compression to image prior to sending MMS text - Import Pillow and IO into notification.py - Update references of SMS to MMS --- cfgs/config.yaml.example | 4 ++-- scrubdash/asyncio_server/notification.py | 21 +++++++++++++++------ scrubdash/asyncio_server/session.py | 2 +- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/cfgs/config.yaml.example b/cfgs/config.yaml.example index eb029be..8278f01 100644 --- a/cfgs/config.yaml.example +++ b/cfgs/config.yaml.example @@ -20,7 +20,7 @@ EMAIL_RECEIVERS: - receiver1@gmail.com - receiver2@gmail.com -# SMS NOTIFICATION CONFIGURATION -SMS_RECEIVERS: +# MMS NOTIFICATION CONFIGURATION +MMS_RECEIVERS: - { num: 3332221111, carrier: tmobile } - { num: 8884442222, carrier: verizon } diff --git a/scrubdash/asyncio_server/notification.py b/scrubdash/asyncio_server/notification.py index 1718562..4b98b1b 100644 --- a/scrubdash/asyncio_server/notification.py +++ b/scrubdash/asyncio_server/notification.py @@ -1,5 +1,6 @@ -"""This file contains a class for sending email and SMS notifications.""" +"""This file contains a class for sending email and MMS notifications.""" +import io import logging import re import ssl @@ -11,6 +12,7 @@ from smtplib import SMTP_SSL, SMTPResponseException import aiosmtplib +from PIL import Image log = logging.getLogger(__name__) @@ -39,7 +41,7 @@ class NotificationSender: The password for the email used to send out notifications EMAIL_RECEIVERS : list of str The list of emails notifications will be sent to - SMS_RECEIVERS: list of dict of { 'num' : int, 'carrier' : str } + MMS_RECEIVERS: list of dict of { 'num' : int, 'carrier' : str } The list of dictionaries containing phone numbers and service carriers that notifications will be sent to """ @@ -48,7 +50,7 @@ def __init__(self, self.SENDER = configs['SENDER'] self.SENDER_PASSWORD = configs['SENDER_PASSWORD'] self.EMAIL_RECEIVERS = configs['EMAIL_RECEIVERS'] - self.SMS_RECEIVERS = configs['SMS_RECEIVERS'] + self.MMS_RECEIVERS = configs['MMS_RECEIVERS'] def _get_datetime(self, image_path): """ @@ -75,9 +77,15 @@ def _get_datetime(self, image_path): return (date, time) + def compress_image(self, image_data): + image = Image.open(io.BytesIO(image_data)) + output = io.BytesIO() + image.save(output, format='JPEG', optimize=True, quality=75) + return output.getvalue() + async def send_mms(self, hostname, image_path, detected_alert_classes): """ - Send an SMS notification to receivers listed in the `SMS_RECEIVERS` + Send a MMS notification to receivers listed in the `MMS_RECEIVERS` attribute. Parameters ---------- @@ -95,7 +103,7 @@ async def send_mms(self, hostname, image_path, detected_alert_classes): """ date, time = self._get_datetime(image_path) - for receiver in self.SMS_RECEIVERS: + for receiver in self.MMS_RECEIVERS: num = receiver['num'] carrier = receiver['carrier'] @@ -113,8 +121,9 @@ async def send_mms(self, hostname, image_path, detected_alert_classes): with open(image_path, 'rb') as content_file: content = content_file.read() + image = self.compress_image(content) message.add_attachment( - content, + image, maintype='image', subtype='jpeg', filename='{}'.format(image_path.split('/')[-1]) diff --git a/scrubdash/asyncio_server/session.py b/scrubdash/asyncio_server/session.py index 375ac2b..a46e0e1 100644 --- a/scrubdash/asyncio_server/session.py +++ b/scrubdash/asyncio_server/session.py @@ -261,7 +261,7 @@ async def _send_notification_if_alert_class_detected(self, self.notification_sender.send_email(self.HOSTNAME, image_path, detected_alert_classes) - await self.notification_sender.send_sms(self.HOSTNAME, + await self.notification_sender.send_mms(self.HOSTNAME, image_path, detected_alert_classes) last_alert_time = time.time() From d09c1552b39c9a5a91b21efb6f6e07d47ab4e66b Mon Sep 17 00:00:00 2001 From: Alex Garcia Date: Fri, 1 Apr 2022 14:58:17 -0700 Subject: [PATCH 5/8] Denote helper method for image compression --- scrubdash/asyncio_server/notification.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scrubdash/asyncio_server/notification.py b/scrubdash/asyncio_server/notification.py index 4b98b1b..2ce0606 100644 --- a/scrubdash/asyncio_server/notification.py +++ b/scrubdash/asyncio_server/notification.py @@ -77,7 +77,7 @@ def _get_datetime(self, image_path): return (date, time) - def compress_image(self, image_data): + def _compress_image(self, image_data): image = Image.open(io.BytesIO(image_data)) output = io.BytesIO() image.save(output, format='JPEG', optimize=True, quality=75) From 3649f52d1f6cfdeda256e86131662fa3d93def5f Mon Sep 17 00:00:00 2001 From: Alex Garcia Date: Mon, 18 Apr 2022 11:38:41 -0700 Subject: [PATCH 6/8] Fix image compression function call --- scrubdash/asyncio_server/notification.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scrubdash/asyncio_server/notification.py b/scrubdash/asyncio_server/notification.py index 2ce0606..07e674c 100644 --- a/scrubdash/asyncio_server/notification.py +++ b/scrubdash/asyncio_server/notification.py @@ -121,7 +121,7 @@ async def send_mms(self, hostname, image_path, detected_alert_classes): with open(image_path, 'rb') as content_file: content = content_file.read() - image = self.compress_image(content) + image = self._compress_image(content) message.add_attachment( image, maintype='image', From 2705d18ad47407601a232b62062b27db2ff8a4cb Mon Sep 17 00:00:00 2001 From: Alex Garcia Date: Wed, 20 Apr 2022 16:18:45 -0700 Subject: [PATCH 7/8] Add exception handling to email sends --- scrubdash/asyncio_server/notification.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/scrubdash/asyncio_server/notification.py b/scrubdash/asyncio_server/notification.py index 07e674c..469e7f6 100644 --- a/scrubdash/asyncio_server/notification.py +++ b/scrubdash/asyncio_server/notification.py @@ -198,10 +198,11 @@ def send_email(self, hostname, image_path, detected_alert_classes): with SMTP_SSL(smtp_server, port, context=context) as server: server.login(self.SENDER, self.SENDER_PASSWORD) server.send_message(message) - except SMTPResponseException: - # Raise KeyboardInterrupt again so the asyncio server can catch - # it. Not raising the interrupt again causes only SMTP to stop, - # not the entire asyncio server. I suspect this is because SMTP - # will crash, but the asyncio server will be fine since the - # run_forever coroutine was never cancelled by an interrupt. - raise KeyboardInterrupt + except SMTPResponseException as e: + error_code = e.smtp_code + error_message = e.smtp_error.decode('utf-8') + log.info( + 'Status: Unable to send email.' + f'\n\tCode: {error_code}' + f'\n\tMessage: {error_message}' + ) From e213e6a519019912546668b2d4fc8944dff35020 Mon Sep 17 00:00:00 2001 From: Alex Garcia Date: Mon, 25 Apr 2022 12:44:52 -0700 Subject: [PATCH 8/8] Handle email and MMS exceptions and add logging - Add dictionary of the sender credentials to be used for SMTP - Add helper method that creates a text message object and returns it - Add exception handling to MMS sending - Add exception handling to email sending - Add verbose output to the terminal for email and MMS sending --- scrubdash/asyncio_server/notification.py | 108 ++++++++++++++--------- 1 file changed, 67 insertions(+), 41 deletions(-) diff --git a/scrubdash/asyncio_server/notification.py b/scrubdash/asyncio_server/notification.py index 469e7f6..88ba706 100644 --- a/scrubdash/asyncio_server/notification.py +++ b/scrubdash/asyncio_server/notification.py @@ -2,7 +2,6 @@ import io import logging -import re import ssl from email import encoders from email.message import EmailMessage @@ -45,12 +44,18 @@ class NotificationSender: The list of dictionaries containing phone numbers and service carriers that notifications will be sent to """ - def __init__(self, - configs): + def __init__(self, configs): self.SENDER = configs['SENDER'] self.SENDER_PASSWORD = configs['SENDER_PASSWORD'] self.EMAIL_RECEIVERS = configs['EMAIL_RECEIVERS'] self.MMS_RECEIVERS = configs['MMS_RECEIVERS'] + self.authentication_kwargs = dict( + username=self.SENDER, + password=self.SENDER_PASSWORD, + hostname=HOST, + port=587, + start_tls=True + ) def _get_datetime(self, image_path): """ @@ -83,6 +88,38 @@ def _compress_image(self, image_data): image.save(output, format='JPEG', optimize=True, quality=75) return output.getvalue() + def _create_text_message(self, **kwargs): + detected_alert_classes = kwargs.get('detected_art_classes') + hostname = kwargs.get('hostname') + image_path = kwargs.get('image_path') + phone_num = kwargs.get('phone_num') + to_email = kwargs.get('to_email') + + date, time = self._get_datetime(image_path) + + # Create message. + message = EmailMessage() + message['From'] = self.SENDER + message['To'] = f'{phone_num}@{to_email}' + message['Subject'] = f'New Scrubdash Image from {hostname}' + text = ( + f'At {date} {time}, we received an image from {hostname} ' + f'with the following detected classes: {detected_alert_classes}' + ) + message.set_content(text) + + with open(image_path, 'rb') as media_file: + media = media_file.read() + image = self._compress_image(media) + message.add_attachment( + image, + maintype='image', + subtype='jpeg', + filename='{}'.format(image_path.split('/')[-1]) + ) + + return message + async def send_mms(self, hostname, image_path, detected_alert_classes): """ Send a MMS notification to receivers listed in the `MMS_RECEIVERS` @@ -101,47 +138,35 @@ async def send_mms(self, hostname, image_path, detected_alert_classes): This was adapted from a post from acamso on April 2, 2021 to a github code thread here: https://gist.github.com/alexle/1294495/39d13f2d4a004a4620c8630d1412738022a4058f """ - date, time = self._get_datetime(image_path) - for receiver in self.MMS_RECEIVERS: - num = receiver['num'] + phone_num = receiver['num'] carrier = receiver['carrier'] - to_email = CARRIER_MAP[carrier] - # Create message. - message = EmailMessage() - message["From"] = self.SENDER - message["To"] = f"{num}@{to_email}" - message["Subject"] = 'New Scrubdash Image from {}'.format(hostname) - msg = ('At {} {}, we received an image from {} with the following' - ' detected classes: {}' - .format(date, time, hostname, detected_alert_classes)) - message.set_content(msg) - - with open(image_path, 'rb') as content_file: - content = content_file.read() - image = self._compress_image(content) - message.add_attachment( - image, - maintype='image', - subtype='jpeg', - filename='{}'.format(image_path.split('/')[-1]) - ) + message_kwargs = dict( + detected_alert_classes=detected_alert_classes, + hostname=hostname, + image_path=image_path, + phone_num=phone_num, + to_email=to_email + ) + + text_message = self._create_text_message(**message_kwargs) - # Send. - send_kws = dict( - username=self.SENDER, - password=self.SENDER_PASSWORD, - hostname=HOST, - port=587, - start_tls=True - ) - res = await aiosmtplib.send(message, **send_kws) # type: ignore - msg = ("Failed to send MMS to {}".format(num) - if not re.search(r"\sOK\s", res[1]) - else "Successfully sent MMS to {}".format(num)) - log.info(msg) + try: + await aiosmtplib.send( + text_message, + **self.authentication_kwargs + ) + log.debug(f'Successfully sent MMS to {phone_num}') + except aiosmtplib.errors.SMTPResponseException as e: + error_code = e.code + error_message = e.message + log.warning( + f'Failed to send MMS to {phone_num}' + f'\n\tCode: {error_code}' + f'\n\tMessage: {error_message}' + ) def send_email(self, hostname, image_path, detected_alert_classes): """ @@ -198,11 +223,12 @@ def send_email(self, hostname, image_path, detected_alert_classes): with SMTP_SSL(smtp_server, port, context=context) as server: server.login(self.SENDER, self.SENDER_PASSWORD) server.send_message(message) + log.debug('Successfully sent emails.') except SMTPResponseException as e: error_code = e.smtp_code error_message = e.smtp_error.decode('utf-8') - log.info( - 'Status: Unable to send email.' + log.warning( + 'Failed to send emails.' f'\n\tCode: {error_code}' f'\n\tMessage: {error_message}' )