Skip to content

Commit

Permalink
Merge pull request #5 from weareasterisk/feat_email-options
Browse files Browse the repository at this point in the history
feat: additional ways to send emails
  • Loading branch information
mkhatri1 authored Dec 25, 2019
2 parents 48fc6c6 + e076b84 commit be454f5
Show file tree
Hide file tree
Showing 9 changed files with 352 additions and 128 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ config.yaml
env/
.vagrant/
.idea
.vscode/

# Elastic Beanstalk Files
.elasticbeanstalk/*
Expand Down
279 changes: 168 additions & 111 deletions Pipfile.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Procfile
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
worker: celery -A gavel:celery worker
worker: PYTHONUNBUFFERED=true celery -A gavel:celery worker -B -E --loglevel=info
web: python initialize.py && gunicorn gavel:app
72 changes: 61 additions & 11 deletions app.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,71 @@
},
"description": "An awesome judging system for hackathons",
"env": {
"ADMIN_PASSWORD": "change-this-before-deploying",
"SECRET_KEY": "randomly-generate-this-before-deploying",
"DISABLE_EMAIL": "true",
"BASE_URL": "https://example.com/",
"EMAIL_FROM": "_unused_",
"EMAIL_USER": "_unused_",
"EMAIL_PASSWORD": "_unused_",
"IGNORE_CONFIG_FILE": "true",
"EMAIL_HOST": "smtp.gmail.com",
"EMAIL_PORT": "587"
"ADMIN_PASSWORD": {
"description": "Password for the administrator account.",
"value": "change-this-before-deploying"
},
"SECRET_KEY": {
"description": "Secret key used to hash requests and keys.",
"generator": "secret"
},
"DISABLE_EMAIL": {
"description": "Email sending is disabled if set to true.",
"value": "false"
},
"BASE_URL": {
"description": "The base URL of the app.",
"value": "https://<app-name>.herokuapp.com"
},
"EMAIL_FROM": {
"description": "Who the emails are sent from. Use the format: Sender Name <[email protected]>",
"value": "_unused_"
},
"EMAIL_PROVIDER": {
"description": "What service emails are sent from. You have a choice between smtp, mailgun, and sendgrid. All services require EMAIL_FROM. SMTP requires EMAIL_USER and EMAIL_PASSWORD, alongside EMAIL_HOST and EMAIL_PORT. Mailgun requires MAILGUN_DOMAIN, and MAILGUN_API_KEY. Sendgrid requires SENDGRID_API_KEY. *Make sure that the field below is either smtp, mailgun, or sendgrid.*",
"value": "smtp"
},
"EMAIL_USER": {
"description": "Must be populated with an SMTP username if EMAIL_PROVIDER is set to smtp.",
"value": "_unused_"
},
"EMAIL_PASSWORD": {
"description": "Must be populated with an SMTP password if EMAIL_PROVIDER is set to smtp.",
"value": "_unused_"
},
"EMAIL_HOST": {
"description": "Must be populated with an SMTP host if EMAIL_PROVIDER is set to smtp. Defaults to gmail",
"value": "smtp.gmail.com"
},
"EMAIL_PORT": {
"description": "Must be populated with an SMTP port if EMAIL_PROVIDER is set to smtp.",
"value": "587"
},
"SENDGRID_API_KEY": {
"description": "Must be populated with a Sendgrid API key if EMAIL_PROVIDER is set to sendgrid.",
"value": "_unused_"
},
"MAILGUN_DOMAIN": {
"description": "Must be populated with a Mailgun domain if EMAIL_PROVIDER is set to mailgun.",
"value": "_unused_"
},
"MAILGUN_API_KEY": {
"description": "Must be populated with a Mailgun API key if EMAIL_PROVIDER is set to mailgun",
"value": "_unused_"
},
"IGNORE_CONFIG_FILE": {
"description": "MUST be set to true in order for these environment variables to work.",
"value": "true"
}
},
"website": "https://gavel.weareasterisk.com/",
"repository": "https://github.com/weareasterisk/gavel",
"logo": "https://cdn.weareasterisk.com/product-assets/gavel/icon.png",
"success_url": "/admin",
"keywords": [
"gavel",
"python",
"flask"
],
"name": "Gavel Judging System"
}
}
26 changes: 26 additions & 0 deletions config.template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,29 @@ email_body: null
# event to this list (https://github.com/anishathalye/gavel/wiki/Users), at the
# very least.
send_stats: null


# can also be specified as the 'EMAIL_PROVIDER' environment variable
# defaults to 'smtp'
#
# This setting determines the method used to send emails to judges. User currently
# has the choice between smtp, sendgrid, and mailgun
email_provider: 'smtp'

# can also be specified as the 'SENDGRID_API_KEY' environment variable
# defaults to null
#
# This setting must be populated if the email_provider is sendgrid
sendgrid_api_key: null

# can also be specified as the 'MAILGUN_DOMAIN' environment variable
# defaults to null
#
# This setting must be populated if the email_provider is mailgun
mailgun_domain: null

# can also be specified as the 'MAILGUN_API_KEY' environment variable
# defaults to null
#
# This setting must be populated if the email_provider is mailgun
mailgun_api_key: null
2 changes: 1 addition & 1 deletion gavel/controllers/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -568,4 +568,4 @@ async def email_invite_links(annotators):
body = '\n\n'.join(utils.get_paragraphs(raw_body))
emails.append((annotator.email, settings.EMAIL_SUBJECT, body))

utils.send_emails.delay(emails)
utils.send_emails.apply_async(args=[emails])
12 changes: 10 additions & 2 deletions gavel/controllers/judge.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def index():
@app.route('/vote', methods=['POST'])
@requires_open(redirect_to='index')
@requires_active_annotator(redirect_to='index')
def vote():
def vote(retries=0):
annotator = get_current_annotator()
if annotator.prev.id == int(request.form['prev_id']) and annotator.next.id == int(request.form['next_id']):
if request.form['action'] in ['Skip', 'SkipAbsent', 'SkipBusy']:
Expand All @@ -106,7 +106,15 @@ def vote():
if annotator.stop_next:
annotator.active = False
annotator.update_next(choose_next(annotator))
db.session.commit()
try:
db.session.commit()
except Exception as e:
if retries < 3:
db.session.rollback()
# Try again
vote(retries+1)
else:
raise Exception("Judging commit error: " + e)
return redirect(url_for('index'))

@app.route('/report', methods=['POST'])
Expand Down
4 changes: 4 additions & 0 deletions gavel/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,7 @@ def _list(item):
EMAIL_SUBJECT = c.get('email_subject', default=constants.DEFAULT_EMAIL_SUBJECT)
EMAIL_BODY = c.get('email_body', default=constants.DEFAULT_EMAIL_BODY)
SEND_STATS = _bool(c.get('send_stats', 'SEND_STATS', default=True))
EMAIL_PROVIDER = c.get('email_provider', 'EMAIL_PROVIDER', default="smtp")
SENDGRID_API_KEY = c.get('sendgrid_api_key','SENDGRID_API_KEY', default="")
MAILGUN_DOMAIN = c.get('mailgun_domain', 'MAILGUN_DOMAIN', default="")
MAILGUN_API_KEY = c.get('mailgun_api_key', 'MAILGUN_API_KEY', default="")
82 changes: 80 additions & 2 deletions gavel/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,26 @@
import email
import email.mime.multipart
import email.mime.text
import json
import types

import asyncio

loop = asyncio.get_event_loop()

def async_action(f):
@wraps(f)
def wrapped(*args, **kwargs):
return loop.run_until_complete(f(*args, **kwargs))
return wrapped

def async_future(f):
@wraps(f)
def wrapped(*args, **kwargs):
return asyncio.Future(f(*args, **kwargs))
return wrapped

sendgrid_url = "https://api.sendgrid.com/v3/mail/send"

def gen_secret(length):
return base64.b32encode(os.urandom(length))[:length].decode('utf8').lower()
Expand Down Expand Up @@ -51,8 +71,35 @@ def get_paragraphs(message):
paragraphs = [i.replace('\n', ' ') for i in paragraphs if i]
return paragraphs

@celery.task
@celery.task(name='utils.send_emails')
def send_emails(emails):
if settings.EMAIL_PROVIDER not in ["smtp", "sendgrid", "mailgun"]:
raise Exception("[EMAIL ERROR]: Invalid email provider. Please select one of: smtp, sendgrid, mailgun")
if settings.EMAIL_PROVIDER == "smtp":
send_smtp_emails.apply_async(args=[emails])
else:
exceptions = []
for e in emails:
to_address, subject, body = e
response = {}
to_adddress = to_address[0:]
try:
if settings.EMAIL_PROVIDER == "sendgrid":
response = loop.run_until_complete(sendgrid_send_email(to_address, subject, body))
elif settings.EMAIL_PROVIDER == "mailgun":
response = loop.run_until_complete(mailgun_send_email(to_adddress, subject, body))
if not (response.status_code == requests.codes.ok or response.status_code == requests.codes.accepted):
# all_errors = [error_obj["message"] for error_obj in response.json()["errors"]]
error_msg = to_address
exceptions.append(error_msg)

except Exception as e:
exceptions.append(e)
if exceptions:
raise Exception("Error sending some emails. Please double-check your email authentication settings.", exceptions)

@celery.task(name='utils.send_smtp_emails')
def send_smtp_emails(emails):
'''
Send a batch of emails.
Expand Down Expand Up @@ -93,6 +140,37 @@ def send_emails(emails):
if exceptions:
raise Exception('Error sending some emails: %s' % exceptions)

async def sendgrid_send_email(to_address, subject, body):
new_dict = {}
new_dict["personalizations"] = []
new_dict["personalizations"].append({"to": [{"email": to_address}], "subject": subject})
new_dict["from"] = {}
new_dict["from"]["email"] = settings.EMAIL_FROM
new_dict["subject"] = subject
new_dict["content"] = []
new_dict["content"].append({"type": "text/plain", "value": body})
headers = {
'authorization': "Bearer " + settings.SENDGRID_API_KEY,
'content-type': "application/json",
}
response = requests.post(
sendgrid_url,
data=json.dumps(new_dict),
headers=headers)
return response

async def mailgun_send_email(to_address, subject, body):
api_url = "https://api.mailgun.net/v3/" + settings.MAILGUN_DOMAIN + "/messages"
mailgun_key = settings.MAILGUN_API_KEY
response = requests.post(
api_url,
auth=("api", mailgun_key),
data={"from": settings.EMAIL_FROM,
"to": [to_address],
"subject": subject,
"text": body})
return response

def render_markdown(content):
return Markup(markdown.markdown(content))

Expand Down Expand Up @@ -127,4 +205,4 @@ def cast_row(row):
row[i] = str(int(item))
else:
row[i] = str(item)
return row
return row

0 comments on commit be454f5

Please sign in to comment.