Build A Fax Portal with Twilio, Python and Flask

November 19, 2019
Written by
Scott Sturdivant
Contributor
Opinions expressed by Twilio contributors are their own

Build a Fax Portal with Twilio, Python and Flask

It's nearly 2020 and can you believe that faxes are still alive?  

Without fail, it seems that when the weather turns nasty, it's inevitable that we'll be forced to drive to the local business center and pay too much per sheet for what should just be an email.

Follow along as we utilize Twilio's fax enabled numbers and a simple web portal to send our own faxes, quickly and cost effectively, and from the comfort of your residence!

Project Dependencies

We'll be using a Twilio Programmable Fax number, Python 3, Flask, and the Twilio Python Helper library as our core requirements. Optionally, we'll add in Twilio SMS to receive the status of our submissions.

Twilio

We're not going anywhere 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!

Select 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  # use this line only for Unix and Mac OS
$ \venv\Scripts\activate    # use this line only for Windows

Our project has numerous dependencies to help make our life easier. Installation of the dependencies is as simple as:

$ pip install flask twilio wtforms flask-bootstrap flask-wtf

Ngrok

Already excited for the prospect of running our code, we jump ahead of ourselves a bit by  downloading and installing ngrok by following their instructions. Ngrok will help us expose our web service to the internet, which will be required for Twilio to retrieve our PDF from us!

A 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 twilio
from pathlib import Path
from wtforms import ValidationError
from wtforms.fields import StringField
from twilio.rest import Client
from flask import Flask, request, url_for, send_from_directory, redirect, render_template, flash
from flask_bootstrap import Bootstrap
from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileRequired, FileAllowed
from wtforms.validators import DataRequired
from werkzeug import secure_filename

Next, we begin creating our application and configuring it:

app = Flask(__name__)
Bootstrap(app)
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY') or os.urandom(32)
app.config['UPLOAD_FOLDER'] = '/tmp/'
app.config['ALLOWED_EXTENSIONS'] = {'pdf',}
app.config['TWILIO_ACCOUNT_SID'] = os.getenv('TWILIO_ACCOUNT_SID')
app.config['TWILIO_ACCOUNT_TOKEN'] = os.getenv('TWILIO_ACCOUNT_TOKEN')
app.config['FAX_FROM_NUMBER'] = os.getenv('FAX_FROM_NUMBER')
app.config['SMS_TO_NUMBER'] = os.getenv('SMS_TO_NUMBER')

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 UPLOAD_FOLDER variable needs to be a valid path on the file system and accessible by the user running this program. ALLOWED_EXTENSIONS is a set which defines what file types we'll allow to be uploaded (currently Twilio only allows PDFs so don't change it). The Twilio account SID and Token are imported from environment variables, which you will set later when running the application. Next, we have the number from which our fax will appear to have been sent (this can be any of your verified numbers), and the mobile number to which the transmission status will be sent via SMS, both also imported from the environment.

Upload Form and Route

With configuration out of the way, it's time to move onto the core of this project: presenting a UI to upload the PDF and actually sending that file to Twilio!  Our web form is relatively simple as all we are interested in is the PDF to be uploaded, and to what number should it be sent to:

class FaxSubmitForm(FlaskForm):
    to = StringField(validators=[DataRequired(),])
    fax = FileField(validators=[FileRequired(), FileAllowed(app.config['ALLOWED_EXTENSIONS'], 'PDFs only!')])

    def validate_to(form, field):
        try:
            field.data = app.client.lookups.phone_numbers(field.data).fetch().phone_number
        except twilio.base.exceptions.TwilioRestException:
            raise ValidationError('Invalid phone number.')

As Twilio requires numbers to be specified in E.164 format, we take care to validate the number as best we can by utilizing the Twilio Lookup API.

Now it's time to create a route in Flask that can display the form with a GET request and handle the submission via a POST:

@app.route('/', methods=['GET', 'POST'])
def upload():
    form = FaxSubmitForm()
    if form.validate_on_submit():
        f = form.fax.data
        filename = secure_filename(f.filename)
        f.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))

        kwargs = {
            'from_': app.config['FAX_FROM_NUMBER'],
            'to': form.to.data,
            'media_url': url_for('download_file', filename=filename, _external=True),
            'status_callback': url_for('callback', _external=True),
        }
        app.client.fax.faxes.create(**kwargs)
        flash('The fax has been submitted!')
        return redirect(url_for('upload'))
    return render_template('fax.html', form=form)

We begin by securely saving the uploaded file onto the file system, being sure to use the secure_filename helper function to prevent attacks. Next we prepare the arguments including the from and to numbers, the URL for the PDF file and the number to send the status SMS. Finally, we submit all this information to Twilio, and redirect back to the form, in case another fax needs to be sent.

PDF Download Route

You might notice that we do not submit the PDF directly, instead, we provide a URL. This is because Twilio will soon turn around and attempt to fetch the file from that URL. So in order to handle that request, we create the following route:

@app.route('/uploads/<path:filename>')
def download_file(filename):
    return send_from_directory(app.config['UPLOAD_FOLDER'], filename)

Status Callback Route

Now that Twilio has the file, they're taking care of transmitting it to the recipient. You might notice the status_callback keyword argument that we specified when creating the fax. This allows Twilio to inform us via a POST request if it was delivered successfully or not. So let's add that final route to the application:

@app.route('/callback', methods=['POST'])
def callback():
    status = request.form

    # Delete the file from the file system
    filename = Path(status['OriginalMediaUrl']).parts[-1]
    uploaded_file = Path(app.config['UPLOAD_FOLDER']) / filename
    if uploaded_file.is_file():
        uploaded_file.unlink()

    # send status SMS
    to = status['To']
    if status['Status'] == 'delivered':
        body = f'Your fax to {to} has been sent!'
    else:
        body = f'Your fax to {to} has failed. :-('
    app.client.messages.create(
        body=body, 
        from_=app.config['FAX_FROM_NUMBER'],
        to=app.config['SMS_TO_NUMBER'],
    )

    return ''  # Twilio appreciates a 200 response.

Since we know that Twilio is finished with our file, we first make sure to delete it from the file system. Now since we don't like being kept in the dark, we fire off an SMS to tell us (hopefully) that our fax was successfully sent!

HTML Template

Our routes and logic are all well and good, but we do need to display some HTML. We can do this by creating the templates/fax.html template file:

{% extends "bootstrap/base.html" %}
{% import "bootstrap/wtf.html" as wtf %}

{% block title %}
Fax Submission
{% endblock %}

{% block content %}
<div class="container">
  {% with messages = get_flashed_messages() %}
    {% if messages %}
      {% for message in messages %}
        <div class="alert alert-success" role="alert">{{ message }}</div>
      {% endfor %}
    {% endif %}
  {% endwith %}
  <form method="POST" action="/" enctype="multipart/form-data">
    <dl>
    {{ wtf.quick_form(form) }}
    <input class="btn btn-primary" type="submit" value="Send">
    </dl>
  </form>
</div>
{% endblock %}

Running the Application

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

$ export TWILIO_ACCOUNT_SID=<your_account_sid>
$ export TWILIO_ACCOUNT_TOKEN=<your_account_token>
$ export FAX_FROM_NUMBER="<your_e164_twilio_fax_number>"
$ export SMS_TO_NUMBER="<your_e164_number_for_status_updates>"
$ flask run

If you are following this tutorial on Windows, then use set instead of export to define your environment variables in the command prompt. 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 the 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 on port 80 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 - this is where you need to go to access your upload forms. Copy and paste that first Forwarding URL (it will be http://<something>.ngrok.io) into your web browser. You should be greeted with your upload form!

Fax portal screenshot before sending a fax

Populate the To field with a fax number, then select your PDF file, and finally hit send.

Don't worry about how you enter the phone number, as the care we took in validating the number will ensure that Twilio receives an E.164 formatted number regardless of how you enter it.

Fax portal screenshot after sending a fax

Congratulations, you've done it. You have bested 1970s technology and sent a fax over the internet! Sit back, relax, and wait for the SMS to arrive indicating a successful transmission.

Disclaimers / Best Practices

Neither Flask's run command nor our usage of ngrok should be considered production ready. In lieu of the flask server, consider using a combination of uWSGI and Nginx. Additionally, the route that we created to send the files from the uploaded directory can be entirely removed and replaced with a location configured within Nginx to serve the static content directly, which is going to be more efficient.

The exposure of our files to the internet is also a risk. Consider uploading them to an object store such as AWS S3 and then generating pre-signed URLs that have a short expiration time for Twilio to use.

Configure your firewall rules to expose your Nginx server on port 80 and 443, 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.

Or if you find like I do, that you need to fax once every blue moon, just fire up this application locally as shown in the previous section when you need it and stop it as soon as it completes!

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 on github.

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

Scott Sturdivant
GitHub