Fax Server

[Ref: OpenBSD 7.0, Asterisk 18.10.1 (from ports) ]


Voice over IP Telephony has been unkind to faxes. Old analog fax solutions didn’t work anymore, and Business’ had to either upgrade to more expensive solutions, or try to ignore them. We made our successful transition to VoIP and our E1/T1 analog links were replaced with a network TCP/IP data link. To get our fax servers working we leased digital to analog converters.

Eventually, the OS and Fax Server software was just too old (back in the day we all ran everything on bare metal.) We looked around for service providers who could forwarded the faxes to us as e-mail. The price was relatively high and forwarding our fax line to them introduced another layer of complexity that could compromise the service further.

Segue, queue opportunity for an Asterisk OpenBSD solution.

OpenBSD is a great platform for this service, because it works and I really don’t want to manage Operating System updates/patching on a regular basis. Asterisk supports sending and recieving faxes and there was even a local telephony provider that had, at one point, an open source fax solution based on Linux and Asterisk (Noojee).

We previously worked with Noojee and they are a great telephony provider, but we wanted to build this.

Hopefully this write-up will be useful to others.


a. Answer inbound faxes and
b. Reformat the Fax (which comes as a multipage TIFF Image) to PDF and
c. Send the PDF by e-mail


  • Asterisk PBX Server
  • Ghostscript - for tiff2pdf (convert TIFF file to PDF)
  • Python - to email the PDF to specified users.

Make sure you have a functioning Asterisk box, and have installed the above dependencies before you continue.

Outbound Faxes?

Validating the new fax service requires using an alternate Asterisk server to send faxes to our fax recieving Asterisk server. You could use a physical fax server, after running faxes through a real fax machine for a week of testing, that idea wasn’t as good as it sounded. You could use someone else’s fax sending service, but of course they charge a fee and we’re already too cheap, so we roll our own outbound fax service for the freedom, and the greater flexibility to validate different scenarios with our recieving fax service.

We need to validate that the PBX server is recieving faxes through it’s “external” network interface, so, No, we cannot perform the send/recieve from the same server.

We build a fax sending server and a fax receiving server … Unfortunately we don’t have a simple user experience to let users get their documents to the fax sending server. For this documentation we ignore providing only show how to use Asterisk to send multipage faxes.

Dialplan Applications

[Ref: ReceiveFax, SendFax]

Asterisk extends it’s functionality through “Dialplan Applications”. Essential Dialplan Applications seen in almost all configurations include: Dial, Answer, and Playback

Fax handling functionality is enabled through the two Dialplan Applications:

The two applications are very simple, and this documentation is building the environment where you send and recieve the correct file.

Recieving a Fax Request

The Asterisk Definitive Guide has a more thorough discussion Ch 18. Incoming Fax Handling

To accept, recieve a fax, you have a dialplan that includes ReceiveFax with the filename to store the fax:

exten=> _XXXXXXXX69.,1,ReceiveFAX(filepath.tiff)

The fax is created as a Multipage TIFF Image, because that’s the standard for faxes.

Overwriting the same filename is not smart, so our taks is to build the environment where we get a unique filename for each fax that makes sense for our needs.

Sending a Fax

The Asterisk Definitive Guide has a more thorough discussion Ch 19. Outgoing Fax Handling

Our strategy for sending files is to configure a DialPlan context that uses SendFax to send the fax file, ${FAXFILE}.

exten=> _x,1,SendFAX(${FAXFILE})

${FAXFILE} is the TIFF Image file to be sent through SendFax and we set this value somewhere else.

We ‘call’ the DialPlan context with Asterisk’s Call Files.

Following the above documentation, we put a callfile into the Asterisk ‘callfile’ folder (e.g. /var/spool/asterisk/callfile) within which we set the variable ${FAXFILE} and call the context: fax-outbound with the details on the fax number.

CallerID: ${FAXSRC}
Archive: Yes
WaitTime: ${WAITTIME}
MaxRetries: ${RETRIES}
RetryTime: ${RETRYTIME}

Context: fax-outbound
Extension: ${FAXNUMBER}
Priority: 1

Default Configuration

I set up some global variables in the [globals] section so that I don’t have to re-type them further down in the dialplan, which for me minimises the mistakes as well as provides me a single location where I need to make changes as we expand or change use.


COMPANY=Example Widgets

The paths specified above, need to be created (and the appropriate read/write permissions set)

$ sudo mkdir -p /var/spool/asterisk/fax/in
$ sudo chown -R _asterisk:_asterisk /var/spool/asterisk/fax

Incoming Fax Number

To set up our dialplan, we need to specify the “extension” or number that will be taken as an incoming fax call.

  • Fax Number: XX XXXX XX69
exten=> _XXXXXXXX69.,1,NoOP(FAX from ${CALLERID(num)} ${STRFTIME(${EPOCH},,%c)})
  same => n,Goto(fax-rcvd,${EXTEN:0:10},1)

We recieve calls on our fax line, display a message on the console and then send execution to the [fax-rcvd] macro.

Fax Receipt Macro

To receive a fax, we:

  • configure the pseudo-fax machine response configuration using the Set function and the variable FAXOPT.
  • receive the fax
  • post-process the fax

Note, that with this example we are not doing anything special for transmission (or any other) error.

exten => _X.,1,Set(FAXCHANNEL=${CHANNEL:6:-9})
 same => n,Set(FAXTIMESTAMP=${STRFTIME(${EPOCH},,%Y-%m-%d_%H.%M.%S)})
 same => n,Verbose(*** FAXNUMBER ${EXTEN} RECEIPT from ${CALLERID(num)} ${FAXTIMESTAMP} ****)
 same => n,Set(FAXGUID=${SHELL(/usr/bin/head /dev/random | od -x   | head -1 | awk '{print $2"-"$3"-"$4"-"$5}'):0:-1});
 same => n,Set(FAXOPT(ecm)=no)
 same => n,Set(FAXOPT(headerinfo)=Received by ${COMPANY} ${STRFTIME(${EPOCH},,%Y-%m-%d %H:%M)})
 same => n,Set(FAXOPT(localstationid)=${EXTEN})
 same => n,Set(FAXOPT(maxrate)=14400)
 same => n,Set(FAXOPT(minrate)=2400)
 same => n,System(mkdir -pm 770 ${FAXSPOOL}/${EXTEN})
 same => n,ReceiveFAX(${FAXPATH}.tiff)
 same => n,Set(PDFCONVERSION=${SHELL(${TIFF2PDF} -o ${FAXPATH}.pdf ${FAXPATH}.tiff)})
 same => n,System(${FAX2EMAIL} -r ${FAXRCPT} -a ${FAXPATH}.pdf)
 same => n,Hangup()

Choosing a filename for storing the inbound fax.

We dynamically set the filename from known facts about the fax:

  • Dialled Fax Number ${EXTEN}
  • And source fax number ${CALLERID(num)}
  • The account/channel it came in on ${CHANNEL:6:-9}
  • Time of arrival ${STRFTIME(${EPOCH},,%Y-%m-%d_%H.%M.%S)}

To ensure we don’t get a scenario where two faxes get the same filename, we are also including a random file extension.

 same => n,Set(FAXGUID=${SHELL(/usr/bin/head /dev/random | od -x   | head -1 | awk '{print $2"-"$3"-"$4"-"$5}'):0:-1});

We join our new filename prefix together with the globally defined ${FAXSPOOL} path for our destination file name.


With the above filename convention, a separate directory is used for each fax number. Thus, we have to make sure the full path exists.

 same => n,System(mkdir -pm 770 ${FAXSPOOL}/${EXTEN})

and because we are going to do some post-processing of the file, we are going to recieve the file with a file extension of TIFF to represent that Fax transmissions are in G3/TIFF format.

 same => n,ReceiveFAX(${FAXPATH}.tiff)


There are two things that we do, as part of fax receipt.

  • Convert the file to PDF
  • Email the file to a recipient

Convert the file to PDF

 same => n,Set(PDFCONVERSION=${SHELL(${TIFF2PDF} -o ${FAXPATH}.pdf ${FAXPATH}.tiff)})

Email the file to a recipient

 same => n,System(${FAX2EMAIL} -r ${FAXRCPT} -a ${FAXPATH}.pdf)


I found that the biggest problem I had with the fax receipt process, was ensuring that I had the right permissions et. al. with the ${FAX2EMAIL} script, so to be verbose and get some further details on what is happening.

  same => n,NoOP(Fax to e-mail: ${SYSTEMSTATUS})

We provide some diagnostic information about the success/failure of the script, from Asterisk’s perspective.

${FAX2EMAIL} Script

Below is the working Python script we use here:

#!/usr/bin/env python

"""Send the contents of a file as a MIME message."""

import os
import sys
import smtplib
# For guessing MIME type based on file name extension
import mimetypes

from optparse import OptionParser

from email import encoders
from email.message import Message
from email.mime.audio import MIMEAudio
from email.mime.base import MIMEBase
from email.mime.image import MIMEImage
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText

rfc822sender = "Fax Server <donotrespond@example.com>"

def main():
    parser = OptionParser(usage="""\
Send a file attachment in a MIME Message
Usage: %prog [options]
    parser.add_option('-a', '--attach',
                      type='string', action='store',
                      help="""Specify the file to attach.""")
    parser.add_option('-f', '--from',
                      type='string', action='store', metavar='FROM',
                      default="", dest='sender',
                      help='A FROM: header value')
    parser.add_option('-r', '--recipient',
                      type='string', action='append', metavar='RECIPIENT',
                      default=[], dest='recipients',
                      help='A To: header value (at least one required)')
    opts, args = parser.parse_args()
    if not opts.recipients or not opts.attach:
    if 'opts.sender' in locals():
        rfc822sender2 = opts.sender

    pathdir, attachment = os.path.split(opts.attach)
    if ( len(pathdir) > 0 ):
    # Create the enclosing (outer) message
    outer = MIMEMultipart()
    outer['Subject'] = 'Fax Received for you and Attached' 
    outer['To'] = COMMASPACE.join(opts.recipients)
    outer['From'] = rfc822sender
    outer.preamble = 'You will not see this in a MIME-aware mail reader.\n'

    # Text
    text = """

A fax has been received and forwarded to you as an attachment with this e-mail message
To help you identify the fax file, we use the following naming convention (using the underscore "_" as a separator):-



faxnumber  is the fax-number that that sent the fax 
date       is the date the fax was received written in the format Year, Month, Date (YYYY-MM-DD).
time       is the date the fax was received written in the format hour, minute, seconds (HH.MM.SS)

Facsimile Service

    textHtml = """
     <p>Hello,<br />
       A fax has been recieved for you and is attached with this e-mail.</p>
     <p>-- Fax Server</p>
    bodyText = MIMEText(text, 'plain')
    #bodyTextHtml = MIMEText(textHtml, 'html')
    ctype, encoding = mimetypes.guess_type(attachment)
    if ctype is None or encoding is not None:
        # No guess could be made, or the file is encoded (compressed), so
        # use a generic bag-of-bits type.
        ctype = 'application/octet-stream'
    maintype, subtype = ctype.split('/', 1)
    if maintype == 'text':
        #print "Mime:Text"
        fp = open(attachment)
        # Note: we should handle calculating the charset
        msg = MIMEText(fp.read(), _subtype=subtype)
    elif maintype == 'image':
        # print "Mime:Image"
        fp = open(attachment, 'rb')
        msg = MIMEImage(fp.read(), _subtype=subtype)
    elif maintype == 'audio':
        #print "Mime:Audio"
        fp = open(attachment, 'rb')
        msg = MIMEAudio(fp.read(), _subtype=subtype)
        #print "Mime:Base"
        fp = open(attachment, 'rb')
        msg = MIMEBase(maintype, subtype)
        # Encode the payload using Base64
    # Set the filename parameter
    msg.add_header('Content-Disposition', 'attachment', filename=attachment)

    composed = outer.as_string()
    s = smtplib.SMTP('')
    s.sendmail(rfc822sender, opts.recipients, composed)

if __name__ == '__main__':