Repository: fossasia/open-event-server

Issue:
FallBack to SMTP when SendGrid Fails/Exceeds Quota
**Describe the feature request**
We need a feature in the system where the SMTP protocol must be used when SendGrid's quota exceeds/ fails to work. 
This is essential as whenever someone orders a ticket, they expect the tickets/invoice to reach them via mail. A backup service like this would be useful.

**Additional context**
Working on it.


Discussion:
A simpler version should be implemented first. An email sender object that provides an interface like `send_email` so that we can just use it. And internally it checks the configuration and uses sendgrid or SMTP according to config present. Then it can be enhanced to add the fallback usage
Oh, okay ! @iamareebjamal , just to clarify, we have two separate tasks written out, `send_email_task` for Sendgrid and ` send_mail_via_smtp_task` for SMTP. Do you mean that we just need one method which checks for the internal config and sends it according to the enabled protocol?

Correct

Resolving patch:
diff --git a/app/api/helpers/mail.py b/app/api/helpers/mail.py
--- a/app/api/helpers/mail.py
+++ b/app/api/helpers/mail.py
@@ -1,4 +1,5 @@
 import base64
+import logging
 from datetime import datetime
 
 from flask import current_app
@@ -16,10 +17,28 @@
 from app.models.user import User
 
 
+def check_smtp_config(smtp_encryption):
+    """
+    Checks config of SMTP
+    """
+    config = {
+                'host': get_settings()['smtp_host'],
+                'username': get_settings()['smtp_username'],
+                'password': get_settings()['smtp_password'],
+                'encryption': smtp_encryption,
+                'port': get_settings()['smtp_port'],
+    }
+    for field in config:
+        if field is None:
+            return False
+    return True
+
+
 def send_email(to, action, subject, html, attachments=None):
     """
     Sends email and records it in DB
     """
+    from .tasks import send_email_task_sendgrid, send_email_task_smtp
     if not string_empty(to):
         email_service = get_settings()['email_service']
         email_from_name = get_settings()['email_from_name']
@@ -36,40 +55,41 @@ def send_email(to, action, subject, html, attachments=None):
         }
 
         if not current_app.config['TESTING']:
-            if email_service == 'smtp':
-                smtp_encryption = get_settings()['smtp_encryption']
-                if smtp_encryption == 'tls':
-                    smtp_encryption = 'required'
-                elif smtp_encryption == 'ssl':
-                    smtp_encryption = 'ssl'
-                elif smtp_encryption == 'tls_optional':
-                    smtp_encryption = 'optional'
-                else:
-                    smtp_encryption = 'none'
-
-                config = {
-                    'host': get_settings()['smtp_host'],
-                    'username': get_settings()['smtp_username'],
-                    'password': get_settings()['smtp_password'],
-                    'encryption': smtp_encryption,
-                    'port': get_settings()['smtp_port'],
-                }
-
-                from .tasks import send_mail_via_smtp_task
-                send_mail_via_smtp_task.delay(config, payload)
+            smtp_encryption = get_settings()['smtp_encryption']
+            if smtp_encryption == 'tls':
+                smtp_encryption = 'required'
+            elif smtp_encryption == 'ssl':
+                smtp_encryption = 'ssl'
+            elif smtp_encryption == 'tls_optional':
+                smtp_encryption = 'optional'
             else:
-                payload['fromname'] = email_from_name
-                key = get_settings()['sendgrid_key']
-                if not key:
-                    print('Sendgrid key not defined')
-                    return
-                headers = {
-                    "Authorization": ("Bearer " + key),
-                    "Content-Type": "application/json"
-                }
-                from .tasks import send_email_task
-                send_email_task.delay(payload, headers)
+                smtp_encryption = 'none'
+
+            smtp_config = {
+                'host': get_settings()['smtp_host'],
+                'username': get_settings()['smtp_username'],
+                'password': get_settings()['smtp_password'],
+                'encryption': smtp_encryption,
+                'port': get_settings()['smtp_port'],
+            }
+            smtp_status = check_smtp_config(smtp_encryption)
+            if smtp_status:
+                if email_service == 'smtp':
+                    send_email_task_smtp.delay(payload=payload, headers=None, smtp_config=smtp_config)
+                else:
+                    key = get_settings().get('sendgrid_key')
+                    if key:
+                        headers = {
+                            "Authorization": ("Bearer " + key),
+                            "Content-Type": "application/json"
+                        }
+                        payload['fromname'] = email_from_name
+                        send_email_task_sendgrid.delay(payload=payload, headers=headers, smtp_config=smtp_config)
+                    else:
+                        logging.exception('SMTP & sendgrid have not been configured properly')
 
+            else:
+                logging.exception('SMTP is not configured properly. Cannot send email.')
         # record_mail(to, action, subject, html)
         mail = Mail(
             recipient=to, action=action, subject=subject,
diff --git a/app/api/helpers/tasks.py b/app/api/helpers/tasks.py
--- a/app/api/helpers/tasks.py
+++ b/app/api/helpers/tasks.py
@@ -18,6 +18,7 @@
 from app.models.session import Session
 from app.models.speaker import Speaker
 
+
 """
 Define all API v2 celery tasks here
 This is done to resolve circular imports
@@ -51,45 +52,49 @@
 celery = make_celery()
 
 
-@celery.task(name='send.email.post')
-def send_email_task(payload, headers):
-    message = Mail(from_email=payload['from'],
-                   to_emails=payload['to'],
-                   subject=payload['subject'],
-                   html_content=payload["html"])
-    if payload['attachments'] is not None:
-        for attachment in payload['attachments']:
-            with open(attachment, 'rb') as f:
-                file_data = f.read()
-                f.close()
-            encoded = base64.b64encode(file_data).decode()
-            attachment = Attachment()
-            attachment.file_content = FileContent(encoded)
-            attachment.file_type = FileType('application/pdf')
-            attachment.file_name = FileName(payload['to'])
-            attachment.disposition = Disposition('attachment')
-            message.add_attachment(attachment)
-    sendgrid_client = SendGridAPIClient(get_settings()['sendgrid_key'])
-    logging.info('Sending an email regarding {} on behalf of {}'.format(payload["subject"], payload["from"]))
+@celery.task(name='send.email.post.sendgrid')
+def send_email_task_sendgrid(payload, headers, smtp_config):
     try:
+        message = Mail(from_email=payload['from'],
+                       to_emails=payload['to'],
+                       subject=payload['subject'],
+                       html_content=payload["html"])
+        if payload['attachments'] is not None:
+            for attachment in payload['attachments']:
+                with open(attachment, 'rb') as f:
+                    file_data = f.read()
+                    f.close()
+                encoded = base64.b64encode(file_data).decode()
+                attachment = Attachment()
+                attachment.file_content = FileContent(encoded)
+                attachment.file_type = FileType('application/pdf')
+                attachment.file_name = FileName(payload['to'])
+                attachment.disposition = Disposition('attachment')
+                message.add_attachment(attachment)
+        sendgrid_client = SendGridAPIClient(get_settings()['sendgrid_key'])
+        logging.info('Sending an email regarding {} on behalf of {}'.format(payload["subject"], payload["from"]))
         sendgrid_client.send(message)
         logging.info('Email sent successfully')
-    except Exception:
-        logging.exception('Error occured while sending the email')
+    except urllib.error.HTTPError as e:
+        if e.code == 429:
+            logging.warning("Sendgrid quota has exceeded")
+            send_email_task_smtp.delay(payload=payload, headers=None, smtp_config=smtp_config)
+        else:
+            logging.exception("The following error has occurred with sendgrid-{}".format(str(e)))
 
 
 @celery.task(name='send.email.post.smtp')
-def send_mail_via_smtp_task(config, payload):
+def send_email_task_smtp(payload, smtp_config, headers=None):
     mailer_config = {
-        'transport': {
-            'use': 'smtp',
-            'host': config['host'],
-            'username': config['username'],
-            'password': config['password'],
-            'tls': config['encryption'],
-            'port': config['port']
+            'transport': {
+                'use': 'smtp',
+                'host': smtp_config['host'],
+                'username': smtp_config['username'],
+                'password': smtp_config['password'],
+                'tls': smtp_config['encryption'],
+                'port': smtp_config['port']
+            }
         }
-    }
 
     mailer = Mailer(mailer_config)
     mailer.start()
@@ -97,12 +102,13 @@ def send_mail_via_smtp_task(config, payload):
     message.subject = payload['subject']
     message.plain = strip_tags(payload['html'])
     message.rich = payload['html']
-    message.attach(name=payload['attachments'])
+    if payload['attachments'] is not None:
+        for attachment in payload['attachments']:
+            message.attach(name=attachment)
     mailer.send(message)
     logging.info('Message sent via SMTP')
     mailer.stop()
 
-
 @celery.task(base=RequestContextTask, name='resize.event.images', bind=True)
 def resize_event_images_task(self, event_id, original_image_url):
     event = safe_query(db, Event, 'id', event_id, 'event_id')


Test patch: