How to Make Unique Holiday e-Cards with Python and SendGrid

December 12, 2022
Written by
Reviewed by
Mia Adjei
Twilion

How to Make Unique Holiday e-Cards with Python and SendGrid

Are the holidays quickly coming around the corner, and you can’t figure out anything to do? Why not harness the power of technology to create a personalized holiday card? And while you’re at it, you can grab a budding artist, programmer, or mathematician to help you program this project.

In this tutorial, you will use Python to create a drawing program that allows you to create holiday cards. With this program, you will also generate a snowflake. Afterwards, you will use SendGrid and Python to send the card out by email.

Prerequisites

To continue with this tutorial, you will need:

After you complete the prerequisites, you are ready to proceed to the tutorial.

Set up your project environment

Before you can dive into the code, you will need to set up the project environment on your computer.

First, you’ll need to create a parent directory that holds the project. Open the terminal on your computer, navigate to a suitable directory for your project, type in the following command, and hit enter.

mkdir snowflake-card-project && cd snowflake-card-project

As a part of good practices for Python, you'll also want to create a virtual environment. If you are working on UNIX or macOS, run the following commands to create and activate a virtual environment. The first command creates the virtual environment, and the second command activates it.

python3 -m venv venv
source venv/bin/activate

However, if you are working on Windows, run these commands instead:

python -m venv venv
venv\Scripts\activate

After activating your virtual environment, you’ll need to install the following Python packages:

  • python-dotenv to manage your environmental variables.
  • pygame to create simple graphics.
  • sendgrid to interact with the Twilio SendGrid Web API.

To install these packages, run this command:

pip install python-dotenv pygame sendgrid

As a part of good programming practices, you’ll want to store sensitive information inside a secure location. To do this, you will store values in a .env file as environmental variables. In the snowflake-card-project directory, open a new file named .env (note the leading dot), and paste the following lines into the file:

SENDGRID_API_KEY=XXXXXXXXXXXXXXXX
VERIFIED_EMAIL_SENDER=XXXXXXXXXXXXXXXX

If you are using GitHub, be sure to include the .env file in your .gitignore file.

Your project environment is now set up, but you must first configure your environmental variables in the .env file. "XXXXXXXXXXXXXXXX" are simply placeholder values for their corresponding variables. The next sections will cover how to get the actual values.

Obtain a SendGrid API Key

From the SendGrid dashboard, click Settings > API Keys to navigate to the SendGrid API Keys page. Then, click on Create API Key.

SendGrid API Keys Page. An arrow is pointed to the Create API Key button

Next, enter the name (I named mine "Holiday Card") for the API Key. Then, under API Key Permissions, select Full Access. Finally, click Create & View to create the API Key.

Create API Key window. The user can set the name and permissions for the API Key

Afterwards, your API Key should pop up on the screen. Copy the key, and in the .env file you created earlier, replace the "XXXXXXXXXXXXXXXX" placeholder for SENDGRID_API_KEY with the value you copied.

Next, you need to create a verified sender to send emails from. On the left-hand side of the dashboard, navigate to Marketing > Senders to get to the Sender Management page. Click the Create New Sender button in the top right, fill out the necessary fields, and click Save. In the .env file you created earlier, replace the "XXXXXXXXXXXXXXXX" placeholder for VERIFIED_EMAIL_SENDER with the email address you used for the sender. Then, verify the sender by checking for an email from SendGrid in the sender's email inbox.

SendGrid Sender Management page displaying a verified email sender

After the sender is verified, you are now ready to begin coding the application!

Create the application

Here are the expected requirements of the application:

  • The user will draw part of a snowflake.
  • After the user clicks a button labeled CREATE, a snowflake is automatically generated.
  • The user is then able to draw on the image as they want.
  • When the user exits out, they are given an option to fill out a form to send the image via email.

Here is an example of a completed card:

Completed snowflake holiday card captioned Happy Holidays

There will be four files in the snowflake-card-project directory: settings.py, button.py, main.py, and sendMessage.py. Each section below will cover these files.

Define the settings module

The settings.py file is used as a module to store user preferences, constants, and common imports. In the snowflake-card-project directory, create a new file named settings.py and paste the following code into settings.py:

import pygame
pygame.init()

SKYBLUE = (115,215,255)
WHITE = (255,255,255)
BLACK = (0,0,0)
SWIDTH = 600 # screen width
RADIUS = 275 # radius of the snowflake
SAVENAME = "mySnowflake.png"

In the code shown above, values for several constants are defined. Feel free to change these values to match your preferences.

Define the button module

The button.py file is used as a module that defines the Button class, so you can easily create buttons for your pygame window. In the snowflake-card-project directory, create a new file named button.py and paste the following code into button.py:

from settings import *

class Button:
  def __init__(self, x, y, width, height, text=None):
    self.x = x
    self.y = y
    self.width = width
    self.height = height
    self.text = text

  def draw(self, win):
    pygame.draw.rect(win, WHITE, (self.x, self.y, self.width, self.height))
    pygame.draw.rect(win, BLACK, (self.x, self.y, self.width, self.height), 2)
    font = pygame.font.Font("freesansbold.ttf",  20)
    text = font.render(self.text, True, BLACK)
    textRect = text.get_rect()
    textRect.center = (self.x+ (self.width/2), self.y+(self.height/2))
    win.blit(text, textRect)
    pygame.draw.rect

  def clicked(self, x, y):
    if (x > self.x and x < self.x + self.width \
      and y > self.y and y < self.y + self.height):
      return True
    else:
      return False

As depicted in the code above, the draw function for the Button class renders the button, and the clicked function checks whether the mouse position is within the area defined for the button. This module will be used to make the CREATE button to generate the snowflake in main.py.

Code main.py

The code in main.py creates and renders the graphic program. This section will cover the code step-by-step below. However, you can skip to the end of this section for the completed code.

In the snowflake-card-project directory, create a new file named main.py, and paste the following code into main.py:

from settings import *
import button as button
from math import pi as PI
from math import sqrt, atan
from sendMessage import *

# Initialize the screen
screen = pygame.display.set_mode((SWIDTH, SWIDTH))
pygame.display.set_caption("Snowflake Maker")
screen.fill(SKYBLUE)

# Draw the sector
pygame.draw.arc(screen, BLACK, [(SWIDTH-2*RADIUS)/2, (SWIDTH-2*RADIUS)/2, 2*RADIUS, 2*RADIUS], PI/3, 0.5*PI, 1)
pygame.draw.line(screen, BLACK, (SWIDTH/2,SWIDTH/2), (SWIDTH/2, (SWIDTH-2*RADIUS)/2))
pygame.draw.line(screen, BLACK, (SWIDTH/2,SWIDTH/2), (0.5*(SWIDTH+RADIUS), (SWIDTH/2)-RADIUS*(sqrt(3)/2)))

# Create button
createBtn = button.Button(0.7*SWIDTH, 0.8*SWIDTH, 90, 30, "CREATE")
createBtn.draw(screen)

# Initialize surface to draw snowflake
snowflakeSurface = pygame.surface.Surface((SWIDTH, SWIDTH), pygame.SRCALPHA)
snowflakeSurface.set_colorkey(BLACK)

The code above sets up the screen to draw on. Using a bit of math, you can use pygame to create the drawing area, i.e., the sector. This sector makes up 1/12th of a circle. Additionally, the user doesn’t actually draw on the screen. Instead, snowflakeSurface is initialized for the user to draw on.

This is what the screen would look like.

Screen for drawing snowflake. The drawing area is empty.

Below are some helper functions to help you draw the snowflake. Copy and paste the following code below the lines you pasted earlier:

# Draw a circle
def drawCircle(surface, x, y):
  pygame.draw.circle(surface, WHITE, (x,y), 2)

# Check whether the mouse is inside the sector
def isInsideSector(x,y):
  center = SWIDTH/2
  relX = x-center+0.000000001
  relY = y-center
  radius = sqrt((relX)**2 + (relY)**2)
  angle = atan(-relY/relX)
  return radius < RADIUS and angle > PI/3 and angle < PI/2 and relX>0

# Create the snowflake
def drawSnowflakeSymmetry(surface):
  angle = 60

  rotated_surface = surface
  screen.blit(rotated_surface, (0,0))
  for i in range(5):
    rotated_surface = pygame.transform.rotate(rotated_surface, angle)
    rect = rotated_surface.get_rect(center = (SWIDTH/2, SWIDTH/2))
    rotated_surface.set_colorkey(BLACK)
    screen.blit(rotated_surface, (rect.x, rect.y))
  
  rotated_surface = pygame.transform.flip(surface, True, False)
  screen.blit(rotated_surface, (0,0))
  for i in range(5):
    rotated_surface = pygame.transform.rotate(rotated_surface, angle)
    rect = rotated_surface.get_rect(center = (SWIDTH/2, SWIDTH/2))
    rotated_surface.set_colorkey(BLACK)
    screen.blit(rotated_surface, (rect.x, rect.y))

drawCircle is used to draw white circles in the sector. However, the program should only allow you to draw inside the drawable area. In order to do this, the function isInsideSector checks whether the mouse is inside the sector by checking its polar coordinates. Lastly, drawSnowflakeSymmetry is where all the magic happens. Using the snowflakeSurface, this function copies and rotates the surface every 60 degrees. It then flips the snowflakeSurface horizontally and repeats the process to create the entire snowflake.

Lastly, all of the interactions need to be implemented. Copy and paste the following code below the lines you pasted earlier for the helper functions into main.py:


# Snowflake drawing phase
def snowflake():
  isPressed = False
  while True:
    pygame.display.flip()
    for event in pygame.event.get():
      if event.type == pygame.MOUSEBUTTONDOWN:
        isPressed = True
        (x,y) = pygame.mouse.get_pos()
        if createBtn.clicked(x,y):
          screen.fill(SKYBLUE)
          drawSnowflakeSymmetry(snowflakeSurface)
          draw()
      elif event.type == pygame.MOUSEBUTTONUP:
        isPressed = False
      elif event.type == pygame.MOUSEMOTION and isPressed == True:         
        (x,y) = pygame.mouse.get_pos()
        if isInsideSector(x,y):
          drawCircle(snowflakeSurface, x, y)
        screen.blit(snowflakeSurface, (0,0))
      elif event.type == pygame.QUIT:
        pygame.quit()
        exit()

# Draw any finishing touches
def draw():
  isPressed = False
  while True:
    pygame.display.flip()
    for event in pygame.event.get():
      if event.type == pygame.MOUSEBUTTONDOWN:
        isPressed = True
      elif event.type == pygame.MOUSEBUTTONUP:
        isPressed = False
      elif event.type == pygame.MOUSEMOTION and isPressed == True:         
        (x,y) = pygame.mouse.get_pos()
        drawCircle(screen, x, y)
      elif event.type == pygame.QUIT:
        pygame.image.save(screen, SAVENAME)
        pygame.quit()
        sendEmailDialog() # implemented in sendMessage.py
        exit()

snowflake()

As depicted in the code snippet above, creating the card is split up into two phases: drawing the snowflake, and drawing elsewhere on the screen. These are split up into two functions: snowflake and draw.

The snowflake function is called first. In the function, the screen is constantly updated using an infinite while-loop. Additionally, the loop checks for an event at each iteration. If the user holds the mouse button down, they are able to draw in the sector. Conditional statements check whether the user exits the window or whether the user clicks the CREATE button. Below is an example of part of a snowflake being drawn:

A section of a snowflake is drawn on the screen

Once the create button is clicked, the drawSnowflakeSymmetry function generates the snowflake, and the program then calls the draw function to go into the drawing phase for finishing touches.

Below is an example after the CREATE button was clicked:

A generated snowflake after hitting the create button

Afterwards, the user can draw anything they want onto the screen. For example, you can write some words or draw additional images. An example is depicted below:

Snowflake card with hello written on it and a drawn heart

After the user exits out of the window, the image that they created is saved, and they are given the option to send the email by running the sendEmailDialog function from the sendMessage module. This will be covered in the next section.

Here is the completed code for main.py:

from settings import *
import button as button
from math import pi as PI
from math import sqrt, atan
from sendMessage import *

# Initialize the screen
screen = pygame.display.set_mode((SWIDTH, SWIDTH))
pygame.display.set_caption("Snowflake Maker")
screen.fill(SKYBLUE)

# Draw the sector
pygame.draw.arc(screen, BLACK, [(SWIDTH-2*RADIUS)/2, (SWIDTH-2*RADIUS)/2, 2*RADIUS, 2*RADIUS], PI/3, 0.5*PI, 1)
pygame.draw.line(screen, BLACK, (SWIDTH/2,SWIDTH/2), (SWIDTH/2, (SWIDTH-2*RADIUS)/2))
pygame.draw.line(screen, BLACK, (SWIDTH/2,SWIDTH/2), (0.5*(SWIDTH+RADIUS), (SWIDTH/2)-RADIUS*(sqrt(3)/2)))

# Create button
createBtn = button.Button(0.7*SWIDTH, 0.8*SWIDTH, 90, 30, "CREATE")
createBtn.draw(screen)

# Initialize surface to draw snowflake
snowflakeSurface = pygame.surface.Surface((SWIDTH, SWIDTH), pygame.SRCALPHA)
snowflakeSurface.set_colorkey(BLACK)

# Draw a circle
def drawCircle(surface, x, y):
  pygame.draw.circle(surface, WHITE, (x,y), 2)

# Check whether the mouse is inside the sector
def isInsideSector(x,y):
  center = SWIDTH/2
  relX = x-center+0.000000001
  relY = y-center
  radius = sqrt((relX)**2 + (relY)**2)
  angle = atan(-relY/relX)
  return radius < RADIUS and angle > PI/3 and angle < PI/2 and relX>0

# Create the snowflake
def drawSnowflakeSymmetry(surface):
  angle = 60

  rotated_surface = surface
  screen.blit(rotated_surface, (0,0))
  for i in range(5):
    rotated_surface = pygame.transform.rotate(rotated_surface, angle)
    rect = rotated_surface.get_rect(center = (SWIDTH/2, SWIDTH/2))
    rotated_surface.set_colorkey(BLACK)
    screen.blit(rotated_surface, (rect.x, rect.y))
  
  rotated_surface = pygame.transform.flip(surface, True, False)
  screen.blit(rotated_surface, (0,0))
  for i in range(5):
    rotated_surface = pygame.transform.rotate(rotated_surface, angle)
    rect = rotated_surface.get_rect(center = (SWIDTH/2, SWIDTH/2))
    rotated_surface.set_colorkey(BLACK)
    screen.blit(rotated_surface, (rect.x, rect.y))

# Snowflake drawing phase
def snowflake():
  isPressed = False
  while True:
    pygame.display.flip()
    for event in pygame.event.get():
      if event.type == pygame.MOUSEBUTTONDOWN:
        isPressed = True
        (x,y) = pygame.mouse.get_pos()
        if createBtn.clicked(x,y):
          screen.fill(SKYBLUE)
          drawSnowflakeSymmetry(snowflakeSurface)
          draw()
      elif event.type == pygame.MOUSEBUTTONUP:
        isPressed = False
      elif event.type == pygame.MOUSEMOTION and isPressed == True:         
        (x,y) = pygame.mouse.get_pos()
        if isInsideSector(x,y):
          drawCircle(snowflakeSurface, x, y)
        screen.blit(snowflakeSurface, (0,0))
      elif event.type == pygame.QUIT:
        pygame.quit()
        exit()

# Draw any finishing touches
def draw():
  isPressed = False
  while True:
    pygame.display.flip()
    for event in pygame.event.get():
      if event.type == pygame.MOUSEBUTTONDOWN:
        isPressed = True
      elif event.type == pygame.MOUSEBUTTONUP:
        isPressed = False
      elif event.type == pygame.MOUSEMOTION and isPressed == True:         
        (x,y) = pygame.mouse.get_pos()
        drawCircle(screen, x, y)
      elif event.type == pygame.QUIT:
        pygame.image.save(screen, SAVENAME)
        pygame.quit()
        sendEmailDialog() # implemented in sendMessage.py
        exit()

snowflake()

Define the sendMessage module

The sendMessage.py file is used as a module to store user preferences, constants, and common imports.

In the snowflake-card-project directory, create a new file named sendMessage.py and paste the following code into sendMessage.py:

import os
from settings import *
from dotenv import load_dotenv
import base64
import sendgrid
from sendgrid.helpers.mail import Mail, Attachment, FileName, FileContent,\
  FileType, Disposition
load_dotenv()

def sendEmail(recipient, subjectLine):
  message = Mail(
    from_email=os.environ.get('VERIFIED_EMAIL_SENDER'),
    to_emails=recipient,
    subject=subjectLine,
    plain_text_content="One of a kind, just like you <3")
  try:
    sg = sendgrid.SendGridAPIClient(os.environ.get('SENDGRID_API_KEY'))

    with open(SAVENAME, 'rb') as f:
      data = f.read()
      f.close()
    encoded_file = base64.b64encode(data).decode()

    attachedFile = Attachment(
      FileContent(encoded_file),
      FileName(SAVENAME),
      FileType('image/png'),
      Disposition('attachment')
    )
    message.attachment = attachedFile

    response = sg.send(message)
    print(response.status_code, response.body, response.headers,sep='\n')
  except Exception as e:
    print(e)


def sendEmailDialog():
  if os.path.exists(SAVENAME):
    answer = input("Do you want to email your most recent snowflake? ")
    if answer.lower() == "yes":
      recipientEmail = input("Recipient Email: ")
      subjectLine = input("Subject Line: ")
      sendEmail(recipientEmail, subjectLine)
      print("Email sent.")
      answer = input("Do you want to send another email? ")
  print("Great! Happy holidays!")

In the code above, the necessary modules are imported at the top. There are two functions in this module: sendEmail and sendEmailDialog.

The sendEmail function uses SendGrid to create and send an email. First, a Mail object is initialized with the arguments. Then, the API key is assigned to the variable sg using the SendGridAPIClient() method from the SendGrid helper library. To attach an image, the saved image is converted to base64 format and added as an attachment to the message. The message is then sent, and information about the response is printed out.

The sendEmailDialog function displays all of the input prompts in the console. If an image is created, the user is prompted to answer whether they want to send the email. If they answer yes, they can fill out the recipient and subject lines. Then, the sendEmail function is called. The user can also optionally send another email. An example of the console dialog is shown below:

Console prompts for sending the card via email

Afterwards, the image should appear in the recipient’s inbox.

A snowflake holiday card is received in an email inbox

Run the application

To run the application, run this command in the snowflake-card-project directory where the virtual environment is activated.

python3 main.py

Replace python3 with python if you are using Windows.

After running the command, the screen should be opened and ready to draw on. Draw a snowflake and hit create. Afterwards, you can draw whatever you want on the screen. Once you’re ready to send out your masterpiece, exit out of the window, and the prompts to email the image should appear in the console.

Conclusion

Congratulations on building your application. First, you used Python to create a graphic program that allows you to generate snowflakes and draw on a digital holiday card. Then, using SendGrid and Python, you added additional functionality to the program by allowing the user to send the drawing as an email. Feel free to customize your application to your liking. For example, you could mess around with the layout, make the program more simple, or make it more complicated by allowing users to select colors.

Excited to learn more about what you could do with SendGrid, and Python? Check out this article on using Notion to template emails. Or perhaps you could learn how to send recurring emails with SendGrid. For more information on how to get started with SendGrid and Python, you can check out the SendGrid Email API Quickstart for Python. I can't wait to see what you build!

Postman from Klaus (2019) holding envelope

Johnny Nguyen is an intern developer turned freelancer on Twilio’s Developer Voices Team. He enjoys creating fun coding projects for others to learn and enjoy. When he’s not napping or watching TikTok, he can be reached by his LinkedIn.