Inbound Fax Server

Inbound Fax Server

[Ref: OpenBSD 5.1, Asterisk 1.8.11 (from ports) | AST Fax | ReceiveFAX | Doxygen Docs | Extension Names and Patterns | Extensions.conf - the Dial Plan ]

Table of Contents

  • Introduction
  • Dialplan
    • Default Config
    • Incoming Fax Number
    • Macro Fax Receipt
      • Diagnostics
    • ${FAX2EMAIL} script

Requirement:

  • Python - for the fax to email script
  • Ghostscript - for tiff2pdf (convert FAX raw file to PDF)

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

Dialplan

[Ref: ReceiveFax]

Asterisk can handle faxes through the Dialplan application RecieveFax

File extracts: /etc/asterisk/extensions.conf

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.

[globals]

COMPANY=Example Widgets
FAXSPOOL=/var/spool/asterisk/fax/in
TIFF2PDF=/usr/local/bin/tiff2pdf
FAX2EMAIL=/usr/local/sbin/fax2email.py
FAXRCPT=samt@example.com

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
[incoming_itsp]
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.

[fax-rcvd]
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(FILEPREFIX=${EXTEN}/${CALLERID(num)}_on_${FAXCHANNEL}_${FAXTIMESTAMP}_${FAXGUID})
 same => n,Set(FAXPATH=${FAXSPOOL}/${FILEPREFIX})
 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)}
same => n,Set(FILEPREFIX=fax_${EXTEN}_at_${FAXTIMESTAMP}_from_${FAXCHANNEL}-${CALLERID(num)}-${FAXGUID})
same => n,Set(FILEPREFIX=${EXTEN}/${CALLERID(num)}_on_${FAXCHANNEL}_${FAXTIMESTAMP}_${FAXGUID})

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.

 same => n,Set(FAXPATH=${FAXSPOOL}/${FILEPREFIX})

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)

Post-Processing

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)

Diagnostics

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>"
COMMASPACE = ', '


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:
        parser.print_help()
        sys.exit(1)
    if 'opts.sender' in locals():
        rfc822sender2 = opts.sender

    pathdir, attachment = os.path.split(opts.attach)
    if ( len(pathdir) > 0 ):
        os.chdir(pathdir)
 
    # 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 = """
Hello,


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_on_XXXX_date_time_XXXX-XXXX-XXXX.pdf

Where:

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 = """
<html>
   <head></head>
   <body>
     <p>Hello,<br />
       A fax has been recieved for you and is attached with this e-mail.</p>
     <p>-- Fax Server</p>
   </body>
</html>
"""
    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)
        fp.close()
    elif maintype == 'image':
        # print "Mime:Image"
        fp = open(attachment, 'rb')
        msg = MIMEImage(fp.read(), _subtype=subtype)
        fp.close()
    elif maintype == 'audio':
        #print "Mime:Audio"
        fp = open(attachment, 'rb')
        msg = MIMEAudio(fp.read(), _subtype=subtype)
        fp.close()
    else:
        #print "Mime:Base"
        fp = open(attachment, 'rb')
        msg = MIMEBase(maintype, subtype)
        msg.set_payload(fp.read())
        fp.close()
        # Encode the payload using Base64
        encoders.encode_base64(msg)
    # Set the filename parameter
    msg.add_header('Content-Disposition', 'attachment', filename=attachment)
    outer.attach(bodyText)
    #outer.attach(bodyTextHtml)
    outer.attach(msg)

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


if __name__ == '__main__':
    main()