Receive Faxes with Twilio, Python, and Flask

December 17, 2019
Written by
Scott Sturdivant
Contributor
Opinions expressed by Twilio contributors are their own

Receive Faxes with Twilio, Python and Flask

Wouldn’t you know it.  As soon as we have sent our first fax, someone now wants to respond! Follow along as we quickly setup the means to receive faxes delivered as a PDF attachment via email.

Project Dependencies

We'll be using a Twilio Programmable Fax number, Python 3, Flask, and the Twilio Python Helper library as our core requirements.

Twilio

It’ll be difficult to receive faxes if we don't have a Twilio Fax-Enabled number, so if you do not already have one, head on over and sign up for a free account.

Once you have registered, buy a phone number in the console. Be sure to select the Fax capability!

Phone number capabilities screenshot

Search for a number that meets your region criteria then press buy.

Python

To keep our libraries separate from the system libraries, we follow Python best practices and create a virtualenv and activate it:

$ python3 -m venv venv
$ source venv/bin/activate  # for Unix and Mac OS
$ \venv\Scripts\activate    # for Windows

Our project has several Python dependencies. Installation is as simple as:

$ pip install flask twilio requests flask-mail

Ngrok

Already excited for the prospect of running our code, we jump ahead of ourselves a bit and download and install ngrok by following their instructions. Ngrok will help us expose our web service to the internet, which will be required for Twilio to inform us that we have received a fax!

Flask Skeleton

Begin by creating a file called app.py. At the top, we'll list out all of the import statements we'll be using:

import os
import requests
import twilio
from twilio.rest import Client
from twilio.request_validator import RequestValidator
from flask import Flask, request, url_for, Response, abort
from flask_mail import Mail, Message
from functools import wraps

Flask Configuration

Next up, we create and configure our Flask application::

app = Flask(__name__)
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY') or os.urandom(32)
app.config['TWILIO_ACCOUNT_SID'] = os.getenv('TWILIO_ACCOUNT_SID')
app.config['TWILIO_ACCOUNT_TOKEN'] = os.getenv('TWILIO_ACCOUNT_TOKEN')
app.config['MAIL_SERVER'] = os.getenv('MAIL_SERVER')
app.config['MAIL_PORT'] = os.getenv('MAIL_PORT')
app.config['MAIL_USE_TLS'] = os.getenv('MAIL_USE_TLS', False)
app.config['MAIL_USE_SSL'] = os.getenv('MAIL_USE_SSL', False)
app.config['MAIL_USERNAME'] = os.getenv('MAIL_USERNAME')
app.config['MAIL_PASSWORD'] = os.getenv('MAIL_PASSWORD')
app.config['MAIL_TO'] = os.getenv('MAIL_TO') or app.config['MAIL_USERNAME']
app.config['MAIL_FROM'] = os.getenv('MAIL_FROM') or app.config['MAIL_USERNAME']
mail = Mail(app)

app.client = Client(
    app.config['TWILIO_ACCOUNT_SID'],
    app.config['TWILIO_ACCOUNT_TOKEN']
)

The SECRET_KEY variable is used by Flask to create secure user sessions. In a production deployment, it is a good idea to set the environment variable of the same name to a random string of characters, but during development you can leave that variable undefined and a new random key will be generated each time you run the application. The Twilio account SID and Token are imported from environment variables, which you will set later when running the application.

As we intend to email ourselves the incoming fax, we must configure our mail server settings!  The MAIL_SERVER variable should be the fully qualified domain name (FQDN) of our SMTP server to which we submit our emails. The MAIL_PORT should be the port number on which that server is listening.  Should our SMTP server support any type of secure communications, we may set the MAIL_USE_TLS or MAIL_USE_SSL variables to True. Next, we have our server’s login credentials which can be set with MAIL_USERNAME and MAIL_PASSWORD.

Finally if we wanted to send the fax to or from a different account, we could set the MAIL_TO and MAIL_FROM variables, otherwise the username will be used for both. If your email account login is not a full email address, then you will need to set these variables.

Fax Received Routes

We will be configuring our Twilio Fax number to send an HTTP POST request to a webhook informing us of this incoming message. There are two stages to this reception process:

  1. Twilio informs us via the HTTP POST request that a fax is being sent to our number. We may either accept or reject it.
  2. Assuming we accept it, Twilio sends us the details about the Fax in a follow-up POST request.

For the first request, Twilio expects a response in their TwiML format, which is based on the XML standard.  We may either accept the fax with a <Receive> tag, or reject it with <Reject> tag, both enclosed in a top-level <Response> tag.  In this initial POST request, Twilio includes To and From variables as form data, so we can use that information as the basis for our acceptance criteria if we so desired.  The <Receive> tag must include an action with a link to our second route so that Twilio knows where to send the fax details to.

@app.route('/fax/incoming', methods=['POST'])
def fax_incoming():
    accept_url = url_for('fax_receive', _external=True)
    twiml = '<Response><Receive action="{}"/></Response>'.format(accept_url)
    return Response(twiml, mimetype='text/xml')

Considering we’ve told Twilio that we’re more than happy to accept the incoming fax, we now setup the route that receives it:

@app.route('/fax/receive', methods=['POST'])
def fax_receive():

    # Fetch the fax from Twilio's server
    fax = requests.get(request.form.get('MediaUrl'))

    # Send the received fax file as an email attachment
    msg = Message(
        'Fax from {} received.'.format(request.form.get('From')),
        sender=app.config['MAIL_FROM'],
        recipients=[app.config['MAIL_TO']]
    )
    msg.attach('fax.pdf', 'application/pdf', fax.content)
    mail.send(msg)

    # Finally, we can cleanup and remove the fax from Twilio's servers.
    app.client.fax.faxes(request.form.get('FaxSid')).delete()

    return ''  # Twilio appreciates a 200 response.

We begin by fetching the PDF file that Twilio has stored on their server.  Next, we create an email message and add to it our PDF file as an attachment.  We then submit this message via our configured mail server.  Finally, we remove the Fax from Twilio using their Fax Resource.

Webhook Security

Currently we are accepting POST requests from anyone.  We will blindly attempt to download a file and email it to a recipient.  This is a gaping security hole and one that could be fixed by validating that the requests that are sent to our webhooks are in fact originating from Twilio servers.

Twilio includes a cryptographic signature in all requests that it sends.  For an indepth look at how the signature is created, consider reading Securing Your Twilio Webhooks in Python. This signature must be validated to ensure that the origin is legitimate.  Because both of our routes need this verification, we may factor this logic out into a decorator that will then be applied to both routes. Make sure the following code appears before the two routes in app.py:

def twilio_originating(f):
    """This decorator ensures that we only accept requests from Twilio."""
    @wraps(f)
    def decorated_function(*args, **kwargs):
        validator = RequestValidator(app.config['TWILIO_ACCOUNT_TOKEN'])
        if not validator.validate(request.url, request.form, request.headers.get('X-Twilio-Signature', '')):
            abort(401)
        return f(*args, **kwargs)
    return decorated_function

If the request fails the signature check, it will abort with a 401 error code which means Unauthorized. Now that we have our validating decorator, we can apply it to both routes as such:

@app.route('/fax/incoming', methods=['POST'])
@twilio_originating
def fax_incoming():
    # ...

And:

@app.route('/fax/receive', methods=['POST'])
@twilio_originating
def fax_receive():
    # ...

Running the Application

In our existing terminal window with our activated virtualenv, it is time to run the flask application. Define all of the environment variables we referenced in the configuration section by dropping them in the appropriate places here, then start the application:

$ export TWILIO_ACCOUNT_SID="<your_twilio_account_sid>"
$ export TWILIO_ACCOUNT_TOKEN="<your_twilio_account_token>"
$ export MAIL_SERVER="<your_email_server_host_fqdn>"
$ export MAIL_PORT=<your_email_server_submission_port>
$ export MAIL_USE_TLS=True    # if your email server requires TLS
$ export MAIL_USE_SSL=True    # if your email server requires SSL
$ export MAIL_USERNAME="<your_email_server_username>"
$ export MAIL_PASSWORD="<your_email_server_password>"
$ export MAIL_TO="<who_should_receive_the_fax_email>"
$ export MAIL_FROM="<who_is_sending_the_fax_email>"
$ flask run

If you are following this tutorial on Windows, then use set instead of export to define your environment variables.

If you intend to use Gmail as your email provider, you will first need to generate an App Password.  Once you have an app password to be used by this application, the following settings will allow you to send emails through that account:

$ export TWILIO_ACCOUNT_SID="<your_twilio_account_sid>"
$ export TWILIO_ACCOUNT_TOKEN="<your_twilio_account_token>"
$ export MAIL_SERVER="smtp.gmail.com"
$ export MAIL_PORT=465
$ export MAIL_USE_SSL=True
$ export MAIL_USERNAME="<your_gmail_account>"
$ export MAIL_PASSWORD="<your_gmail_account_app_password>"
$ flask run

Assuming all went well, you will see the flask server doing its thing on port 5000:

 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

Now because it's only listening on the localhost and because you're most likely behind a firewall or NAT of some type, it is time to launch the ngrok utility that we downloaded earlier.

In another terminal window, navigate to where you downloaded and extracted ngrok, then run:

$ ngrok http 5000

This exposes an http tunnel to the internet, meaning that traffic that hits the URL created by ngrok is forwarded to port 5000 on your local machine, which just so happens to be where your flask server is listening!

Take note of the Forwarding URL that ngrok displays!

ngrok screenshot

Twilio Configuration

We must now configure our fax number within Twilio to send requests to our webhooks when a fax is received.  Do this from the Twilio Console, by navigating to your number configuration. Change the “Accept Incoming” option to “Faxes”, and the “Configure With” option to “Webhooks, TwiML Bins, Functions, Studio, or Proxy”. Finally, in the “A Fax Comes In” section, select a “Webhook” as the primary handler with the Ngrok URL above with /fax/incoming added as a suffix.

Fax number configuration screenshot

Once you have saved the configuration, you are now able to receive faxes!

Disclaimers and Best Practices

Neither Flask's run command nor our usage of ngrok should be considered production ready. In lieu of the flask server, for a production deployment consider using a combination of uWSGI and Nginx.

Configure your firewall rules to expose your Nginx server on ports 80 and 443, and setup a domain name and a free SSL certificate from Let’s Encrypt for an easy to remember and secure URL.

Finally, you may want to setup systemd service unit files to make sure that all of the components (uWSGI, nginx) are enabled at boot and are always running.

Conclusion

Hopefully you've seen a portion of the power that Twilio, their Python Helper Library, and tools like Flask can provide.

All code can be found in github.

Thanks for your time, and I look forward to seeing what you build with Twilio!

Scott Sturdivant

GitHub