In FOSSASIA’s Open Event Server project, we send out emails when various different actions are performed using the API. For example, when a new user is created, he/she receives an email welcoming him to the server as well as an email verification email. Users get role invites from event organisers in the form of emails, when someone buys a ticket he/she gets a PDF link to the ticket as email. So as you can understand all the important informations that are necessary to be notified to the user are sent as an email to the user and sometimes to the organizer as well.
In FOSSASIA, we use sendgrid’s API or an SMTP server depending on the admin settings for sending emails. You can read more about how we use sendgrid’s API to send emails in FOSSASIA here. Now let’s dive into the modules that we have for sending the emails. The three main parts in the entire email sending are:
- Model – Storing the Various Actions
- Templates – Storing the HTML templates for the emails
- Email Functions – Individual functions for various different actions
Let’s go through each of these modules one by one.
Model
USER_REGISTER = 'User Registration' USER_CONFIRM = 'User Confirmation' USER_CHANGE_EMAIL = "User email" INVITE_PAPERS = 'Invitation For Papers' NEXT_EVENT = 'Next Event' NEW_SESSION = 'New Session Proposal' PASSWORD_RESET = 'Reset Password' PASSWORD_CHANGE = 'Change Password' EVENT_ROLE = 'Event Role Invitation' SESSION_ACCEPT_REJECT = 'Session Accept or Reject' SESSION_SCHEDULE = 'Session Schedule Change' EVENT_PUBLISH = 'Event Published' AFTER_EVENT = 'After Event' USER_REGISTER_WITH_PASSWORD = 'User Registration during Payment' TICKET_PURCHASED = 'Ticket(s) Purchased'
In the Model file, named as mail.py, we firstly declare the various different actions for which we send the emails out. These actions are globally used as the keys in the other modules of the email sending service. Here, we define global variables with the name of the action as strings in them. These are all constant variables, which means that there value remains throughout and never changes. For example, USER_REGISTER has the value ‘User Registration’, which essentially means that anything related to the USER_REGISTER key is executed when the User Registration action occurs. Or in other words, whenever an user registers into the system by signing up or creating a new user through the API, he/she receives the corresponding emails.
Apart from this, we have the model class which defines a table in the database. We use this model class to store the actions performed while sending emails in the database. So we store the action, the time at which the email was sent, the recipient and the sender. That way we have a record about all the emails that were sent out via our server.
class Mail(db.Model): __tablename__ = 'mails' id = db.Column(db.Integer, primary_key=True) recipient = db.Column(db.String) time = db.Column(db.DateTime(timezone=True)) action = db.Column(db.String) subject = db.Column(db.String) message = db.Column(db.String) def __init__(self, recipient=None, time=None, action=None, subject=None, message=None): self.recipient = recipient self.time = time if self.time is None: self.time = datetime.now(pytz.utc) self.action = action self.subject = subject self.message = message def __repr__(self): return '<Mail %r to %r>' % (self.id, self.recipient) def __str__(self): return unicode(self).encode('utf-8') def __unicode__(self): return 'Mail %r by %r' % (self.id, self.recipient,)
The table name in which all the information is stored is named as mails. It stores the recipient, the time at which the email is sent (timezone aware), the action which initiated the email sending, the subject of the email and the entire html body of the email. In case a datetime value is sent, we use that, else we use the current time in the time field.
HTML Templates
We store the html templates in the form of key value pairs in a file called system_mails.py inside the helpers module of the API. Inside the system_mails, we have a global dict variable named MAILS as shown below.
MAILS = { EVENT_PUBLISH: { 'recipient': 'Organizer, Speaker', 'subject': u'{event_name} is Live', 'message': ( u"Hi {email}<br/>" + u"Event, {event_name}, is up and running and ready for action. Go ahead and check it out." + u"<br/> Visit this link to view it: {link}" ) }, INVITE_PAPERS: { 'recipient': 'Speaker', 'subject': u'Invitation to Submit Papers for {event_name}', 'message': ( u"Hi {email}<br/>" + u"You are invited to submit papers for event: {event_name}" + u"<br/> Visit this link to fill up details: {link}" ) }, SESSION_ACCEPT_REJECT: { 'recipient': 'Speaker', 'subject': u'Session {session_name} has been {acceptance}', 'message': ( u"Hi {email},<br/>" + u"The session <strong>{session_name}</strong> has been <strong>{acceptance}</strong> by the organizer. " + u"<br/> Visit this link to view the session: {link}" ) }, SESSION_SCHEDULE: { 'recipient': 'Organizer, Speaker', 'subject': u'Schedule for Session {session_name} has been changed', 'message': ( u"Hi {email},<br/>" + u"The schedule for session <strong>{session_name}</strong> has been changed. " + u"<br/> Visit this link to view the session: {link}" ) },
Inside the MAILS dict, we have key-value pairs, where in keys we use the global variables from the Model to define the action related to the email template. In the value, we again have 3 different key-value pairs – recipient, subject and message. The recipient defines the group who should receive this email, the subject goes into the subject part of the email while message forms the body for the email. For subject and message we use unicode strings with named placeholders that are used later for formatting using python’s .format() function.
Email Functions
This is the most important part of the entire email sending system since this is the place where the entire email sending functionality is implemented using the above two modules. We have all these functions inside a single file namely mail.py inside the helpers module of the API. Firstly, we import two things in this file – The global dict variable MAILS defined in the template file above, and the various global action variables defined in the model. There is one main module which is used by every other individual modules for sending the emails defined as send_email(to, action, subject, html). This function takes as parameters the email to which the email is to be sent, the subject string, the html body string along with the action to store it in the database.
Firstly we ensure that the email address for the recipient is present and isn’t an empty string. After we have ensured this, we retrieve the email service as set in the admin settings. It can either be “smtp” or “sendgrid”. The email address for the sender has different formatting depending on the email service we are using. While sendgrid uses just the email say for example “medomag20@gmail.com”, smtp uses a format a little different like this: Medozonuo Suohu<medomag20@gmail.com>. So we set that as well in the email_from variable.
def send_email(to, action, subject, html): """ Sends email and records it in DB """ if not string_empty(to): email_service = get_settings()['email_service'] email_from_name = get_settings()['email_from_name'] if email_service == 'smtp': email_from = email_from_name + '<' + get_settings()['email_from'] + '>' else: email_from = get_settings()['email_from'] payload = { 'to': to, 'from': email_from, 'subject': subject, 'html': html } 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)
After this we create the payload containing the email address for the recipient, the email address of the sender, the subject of the email and the html body of the email.
For unittesting and any other testing we avoid email sending since that is really not required in the flow. So we check that the current app is not configured to run in a testing environment. After that we have two different implementation depending on the email service used.
SMTP
There are 3 kind of possible encryptions for the email that can be used with smtp server – tls, ssl and optional. We determine this based on the admin settings again. Also, from the admin settings we collect the host, username, password and port for the smtp server.
After this we start a celery task for sending the email. Since email sending to a number of clients can be time consuming so we do it using the celery queueing service without disturbing the main workflow of the entire system.
@celery.task(name='send.email.post.smtp') def send_mail_via_smtp_task(config, payload): mailer_config = { 'transport': { 'use': 'smtp', 'host': config['host'], 'username': config['username'], 'password': config['password'], 'tls': config['encryption'], 'port': config['port'] } } mailer = Mailer(mailer_config) mailer.start() message = Message(author=payload['from'], to=payload['to']) message.subject = payload['subject'] message.plain = strip_tags(payload['html']) message.rich = payload['html'] mailer.send(message) mailer.stop()
Inside the celery task, we use the Mailer and Message classes from the marrow module of python. We configure the Mailer according to the various settings received from the admin and then use the payload to send the email.
Sendgrid
For sending email using the sendgrid API, we need to set the Bearer key which is used for authenticating the email service. This key is also defined in the admin settings. After we have set the Bearer key as the authorization header, we again initiate the celery task corresponding to the sendgrid email sending service.
@celery.task(name='send.email.post') def send_email_task(payload, headers): requests.post( "https://api.sendgrid.com/api/mail.send.json", data=payload, headers=headers )
For sending the email service, all we need to do is make a POST request to the api endpoint “https://api.sendgrid.com/api/mail.send.json” with the headers which contains the Bearer Key and the data which contains the payload containing all the information related to the recipient, sender, subject of email and the body of the email.
Apart from these, this module implements all the individual functions that are called based on the various functions that occur. For example, let’s look into the email sending function in case a new session is created.
def send_email_new_session(email, event_name, link): """email for new session""" send_email( to=email, action=NEW_SESSION, subject=MAILS[NEW_SESSION]['subject'].format( event_name=event_name ), html=MAILS[NEW_SESSION]['message'].format( email=email, event_name=event_name, link=link ) )
This function is called inside the Sessions API, for every speaker of the session as well as for every organizer of the event to which the session is submitted. Inside this function, we use the send_email(). But firstly we need to create the subject of the email and the message body of the email using the templates and by replacing placeholders by actual value using python formatting. MAILS[NEW_SESSION] returns a unicode string: u’New session proposal for {event_name}’ . So what we do is use the .format() function to replace {event_name} by the actual event_name received as parameter. So it is equivalent to doing something like:
u'New session proposal for {event_name}'.format(‘FOSSASIA’)
which would give us a resulting string of the form:
u'New session proposal for FOSSASIA'
Similarly, we create the html message body using the templates and the parameters received. After this is done, we make a function call to send_email() which then sends the final email.
References:
- Read about how to send emails using Sendgrid API: https://blog.fossasia.org/sending-email-using-sendgrid-api/
- Read more about various Python String formatting: https://pyformat.info/
- Read more about Marrow mailer: https://github.com/marrow/mailer/