Receiving Faxes

[Ref: OpenBSD 6.1, Asterisk 13.16 | ReceiveFAX | Fax for Asterisk - No longer supported in Asterisk 13 ]

Warning: Asterisk-13.16.0 segfaults during startup. Resolution is to have an ari.conf configuration file, even if it is not being used

Introduction

Using Asterisk on OpenBSD to receive and send faxes is surprisingly complete, when you know what you're doing. This page will provides more details on some aspects of using Asterisk to Receive faxes to help you build your own solution.

From our Fax Solution Preview, we have a very basic framework for Receiving Faxes, after configuring:

  • Resource
  • Channel
  • Dialplan Context

File extract: /etc/asterisk/res_fax.conf

[general]
maxrate      = 14400       ; Maximum transmission rate
minrate      = 2400        ; Minimum transmission rate
ecm          = yes         ; Enable/Disable T.30 ECM (Error Correction Mode)

File extract: /etc/asterisk/extensions.conf

[fax-receipt]
exten => _X.,1,ReceiveFAX(/var/spool/asterisk/fax/faxfile.raw)
 same => n,Hangup()

File extract: /etc/asterisk/sip.conf

[fax-trunk](!)
type         = peer
dtmfmode     = rfc2833
directmedia  = no
videosupport = no
canreinvite  = no
qualify      = yes
disallow     = all
allow        = alaw
allow        = ulaw
sendrpid     = pai
trustrpid    = yes
deny         = 0.0.0.0/0
port         = 5060
context      = from_trunk

[carrier](fax-trunk)
bindaddr = XXX ; relevant NIC IP Address
host     = YYY ; VoIP Carrier IP Address
permit   = YYY/32

Before continuing, we set some preliminary configuratiosn into our extensions.conf globals

File extract: /etc/asterisk/extensions.conf

1 [globals]
2 COMPANY=The Company
3 FAXSPOOLDIR=/var/spool/asterisk/fax
4 
5 #include <dialplan/fax-inbound.conf>

and we'll build our dialplan context in the file dialplan/fax-inbound.conf

Requirement:

  • Python - for the fax to email script
  • Ghostscript - for conversion tools such as gs and tiff2pdf (convert FAX raw file to PDF)

Make sure you have a functioning Asterisk box, and have installed the above dependencies together with enabling the fax application module.

Receive Fax

To have our Dialplan Context be more useful, we need deal with the following:

  • Set the Filename Dynamically
  • Deal with Disconnections
  • Convert RAW Fax Files to PDF
  • Email the PDFs

Set FileName

Being able to save the fax to a file is useful, but not practical if we can't create a unique filename for each new fax. The following context gives one example of how we can dynamically generate the filename.

File extract: /etc/asterisk/dialplan/fax-inbound.conf

 1 [fax-inbound]
 2 exten => _X.,1,Set(TIMESTAMP=${STRFTIME(${EPOCH},,%Y%m%d_%H.%M.%S)})
 3  same => n,Set(SPOOLFAX=${FAXSPOOLDIR}/new)
 4  same => n,System(mkdir -pm 770 \"${SPOOLFAX}\") 
 5  same => n,Set(FilePrefix=${CALLERID(num)}_${TIMESTAMP})
 6  same => n,Set(FRAW=${FilePrefix}.raw)
 7  same => n,Set(FAXOPT(headerinfo)=Receiving at ${COMPANY}, Time: ${TIMESTAMP})
 8  same => n,Set(FAXOPT(localstationid)=${EXTEN})
 9  same => n,Set(CHANNEL(hangup_handler_push)=faxin-hangup_handler,${EXTEN},1(${FRAW}))
10  same => n,ReceiveFAX(${SPOOLFAX}/${FRAW},f)
11  same => n,Set(CHANNEL(hangup_handler_pop)=)
12  same => n,Goto(faxin-hangup_handler,${EXTEN},1(${FRAW}))
13  same => n,Hangup()

Set the directory for storing the new fax (SPOOLFAX) based on ${FAXSPOOLDIR}/new and make sure it's created with the correct permissions:

  • mkdir -pm 770 "${SPOOLFAX}"

Generate the filename for the fax file using:

  • ${SPOOLFAX} - the specified FAXSPOOLDIR (/var/spool/asterisk/fax) with "/new"
  • ${FRAW} - (e.g. 0212345678_20170812_12.45.22.raw)
    • ${CALLERID(num)} - The inbound caller-id.
    • ${TIMESTAMP} - current time
    • ".raw" - text ".raw"

Example Filename?

  • 0212345678_20170812_12.45.22.raw

Deal with Disconnections

hangup_handlers were introduced with Asterisk 11, and we use it in our DialPlan because we need to handle occurrences of hangup, more importantly because scoping of extensions in Asterisk can get hard to follow.

When a Fax is hungup (within the transmission ReceiveFAX) then the channel is closed and the context goes to the hangup extension (h in earlier Asterisk version.) If Asterisk doesn't detect a hangupe the dialplan continues? Did the fax complete or it may not?

To alleviate this seeming discrepenacy, we deal with success/failure with the same "subroutine" [faxin-hangup_handler] and we explicitly close (hangup) afterwards.

15 [faxin-hangup_handler]
16 exten => _X.,1,Set(CHANNEL(hangup_handler_pop)=)
17  same => n,Goto(faxrcvd-${FAXOPT(status)},1)
18 
19 exten => faxrcvd-SUCCESS,1,NoOp(SUCCESS - ${FRAW})
20  same => n,Hangup()
21 
22 exten => faxrcvd-FAILED,1,NoOp(FAXOPT(error)     : ${FAXOPT(error)})
23  same => n,Hangup()
24 
25 exten => faxrcvd-.,1,NoOp(FAXOPT(error)      : ${FAXOPT(error)})
26  same => n,Hangup()

Convert FAX to PDF

Once we confirm that the above configuration works, you can recieve faxes to a RAW Fax File, we look at making the file easier to give to our users. res_fax_spandsp reads and saves RAW files as multi-page TIFF Image Files.

There are alternative tools for making the conversion, we will be using tiff2pdf which comes with ghostscript. Install ghostscript as you install OpenBSD packages.

To simplify things we will set a global variable for tiff2pdf in extensions.conf:

File extract: /etc/asterisk/extensions.conf

1 [globals]
2 COMPANY=The Company
3 FAXSPOOLDIR=/var/spool/asterisk/fax
4 TIFF2PDF=/usr/local/bin/tiff2pdf
5 
6 #include <dialplan/fax-inbound.conf>

File extract: /etc/asterisk/dialplan/fax-inbound.conf

 1 [fax-inbound]
 2 exten => _X.,1,Set(TIMESTAMP=${STRFTIME(${EPOCH},,%Y%m%d_%H.%M.%S)})
 3  same => n,Set(SPOOLFAX=${FAXSPOOLDIR}/new)
 4  same => n,Set(SPOOLTMP=${FAXSPOOLDIR}/tmp)
 5  same => n,System(mkdir -pm 770 \"${SPOOLFAX}\") 
 6  same => n,System(mkdir -pm 770 \"${SPOOLTMP}\") 
 7  same => n,Set(FilePrefix=${CALLERID(num)}_${TIMESTAMP})
 8  same => n,Set(FRAW=${FilePrefix}.raw)
 9  same => n,Set(FAXOPT(headerinfo)=Receiving at ${COMPANY}, Time: ${TIMESTAMP})
10  same => n,Set(FAXOPT(localstationid)=${EXTEN})
11  same => n,Set(CHANNEL(hangup_handler_push)=faxin-hangup_handler,${EXTEN},1(${FRAW}))
12  same => n,ReceiveFAX(${SPOOLFAX}/${FRAW},f)
13  same => n,Set(CHANNEL(hangup_handler_pop)=)
14  same => n,Goto(faxin-hangup_handler,${EXTEN},1(${FRAW}))
15  same => n,Hangup()

New to the context is creating the directory to put the PDFs ${FAXSPOOLDIR}/tmp and make sure it's created with the correct permissions:

  • mkdir -pm 770 "${SPOOLTMP}"
17 [faxin-hangup_handler]
18 exten => _X.,1,Set(CHANNEL(hangup_handler_pop)=)
19  same => n,Goto(faxrcvd-${FAXOPT(status)},1)
20 
21 exten => faxrcvd-SUCCESS,1,NoOp(SUCCESS - ${FRAW})
22  same => n,Set(Fpdf=fax-${FilePrefix}_${FAXOPT(pages)}Pages.pdf)
23  same => n,TrySystem(${TIFF2PDF} -o \"${SPOOLTMP}/${Fpdf}\" \"${SPOOLFAX}/${FRAW}\")
24  same => n,Goto(tiff2pdf-${SYSTEMSTATUS},1)

Our Hangup Handler now sets the PDF Filename using Set(Fpdf=fax-${FilePrefix}_${FAXOPT(pages)}Pages.pdf)

  • "fax-" - the text "fax-"
  • ${FilePrefix} discussed earlier for the Raw Fax filename
  • ${FAXOPT(pages)} - Number of successful pages received.
  • "Pages.pdf" - the text "Pages.pdf"

Example Filename?

  • fax-0212345678_20170812_12.45.22_12Pages.pdf

We now have the minimal infrastructure to receive the fax, and convert the fax to a PDF file.

Email

With the user readable PDF created, we can send that file as attachment by email.

1 [globals]
2 COMPANY=The Company
3 FAXSPOOLDIR=/var/spool/asterisk/fax
4 TIFF2PDF=/usr/local/bin/tiff2pdf
5 FAX2EMAIL=/usr/local/sbin/fax2email.py
6 
7 #include <dialplan/fax-inbound.conf>

Add to extensions.conf the location of our fax2email.py Python script.

 1 [fax-inbound]
 2 exten => _X.,1,Set(TIMESTAMP=${STRFTIME(${EPOCH},,%Y%m%d_%H.%M.%S)})
 3  same => n,Set(FAXSOURCE=${CALLERID(num)})
 4  same => n,Set(SPOOLFAX=${FAXSPOOLDIR}/new)
 5  same => n,Set(SPOOLTMP=${FAXSPOOLDIR}/tmp)
 6  same => n,System(mkdir -pm 770 \"${SPOOLFAX}\") 
 7  same => n,System(mkdir -pm 770 \"${SPOOLTMP}\") 
 8  same => n,Set(FilePrefix=${FAXSOURCE}_${TIMESTAMP})
 9  same => n,Set(FRAW=${FilePrefix}.raw)
10  same => n,Set(FAXOPT(headerinfo)=Receiving at ${COMPANY}, Time: ${TIMESTAMP})
11  same => n,Set(FAXOPT(localstationid)=${EXTEN})
12  same => n,Set(CHANNEL(hangup_handler_push)=faxin-hangup_handler,${EXTEN},1(${FRAW}))
13  same => n,ReceiveFAX(${SPOOLFAX}/${FRAW},f)
14  same => n,Set(CHANNEL(hangup_handler_pop)=)
15  same => n,Goto(faxin-hangup_handler,${EXTEN},1(${FRAW}))
16  same => n,Hangup()

Inside [fax-inbound] we:

  • Set a variable ${FAXSOURCE} for the source fax (to be used later)
18 [faxin-hangup_handler]
19 exten => _X.,1,Set(CHANNEL(hangup_handler_pop)=)
20  same => n,Goto(faxrcvd-${FAXOPT(status)},1)
21 
22 exten => faxrcvd-SUCCESS,1,NoOp(SUCCESS - ${FRAW})
23  same => n,TrySystem(${TIFF2PDF} -o \"${SPOOLTMP}/${Fpdf}\" \"${SPOOLFAX}/${FRAW}\")
24  same => n,Goto(tiff2pdf-${SYSTEMSTATUS},1)
25 
26 exten => tiff2pdf-SUCCESS,1,NoOp(${TIFF2PDF} : ${SYSTEMSTATUS})
27  same => n,System(${FAX2EMAIL} -d ${EXTEN} -s ${FAXSOURCE} -t ${TIMESTAMP} -a \"${SPOOLTMP}/${Fpdf}\" -p ${FAXOPT(pages)})
28  same => n,Goto(fax2email-${SYSTEMSTATUS},1)

The hangup handler sends an email, after it has successfully converted the RAW TIFF file to PDF.

The Source

Below you some of what we've implemented, on a few machines to be comfortable that it works. Not detailed are issues that you should consider as part of your Fax Processing:

  • Monitoring
  • Action on faxes that are not processed
  • Reporting

Dialplan extensions.conf

[globals]
COMPANY=The Company
FAXSPOOLDIR=/var/fax

TIFF2PDF=/usr/local/bin/tiff2pdf
FAX2EMAIL=/usr/local/sbin/fax2email.py

#include "dialplan/fax-inbound.conf"
#include "dialplan/fax-outbound.conf"

Dialplan fax-inbound.conf

Below is a copy of my working context with a few more fiddles.

  • LOGging
  • Set Maximum Simultaneous Faxes
  • Differentiate Faxes by Destination Fax Number
  • Move processed Faxes somewhere else
    • raw faxes in ./new to ./old after successfully being converted to PDF
    • pdf files in ./tmp to ./pdf after successfully being e-mailed.

Was there something else in there?

[fax-inbound]
exten => _X.,1,Set(GROUP()=ActiveFaxSessions)
 same => n,Set(FAXCHANNEL=${CHANNEL:4:-9})
 same => n,Verbose(*** FAX ${GROUP_COUNT(ActiveFaxSessions)} for ${EXTEN} from ${CALLERID(num)} on ${FAXCHANNEL} ****)
 ; 
 ; Default values - Configurable in extensions.conf [global]
 ;
 same => n,Set(MAXCONCURRENTFAXES=${IF($[ 0${GLOBAL(MAXCONCURRENTFAXES)} > 0 ]?${GLOBAL(MAXCONCURRENTFAXES)}:10)})
 same => n,GotoIf($[${GROUP_COUNT(ActiveFaxSessions)}>${MAXCONCURRENTFAXES}]?aboveconcurrentfaxes)
 same => n,Set(GLOBAL(FAXCOUNTER)=${IF($[ "${GLOBAL(FAXCOUNTER)}" = "" ]?1:$[0${GLOBAL(FAXCOUNTER)} + 1])})
 same => n,Set(FAXCOUNTER=${GLOBAL(FAXCOUNTER)})
 same => n,Set(ASTSPOOLDIR=${IF($[ "${GLOBAL(ASTSPOOLDIR)}" != "" ]?${GLOBAL(ASTSPOOLDIR)}:/var/spool/asterisk)})
 same => n,Set(FAXSPOOLDIR=${IF($[ "${GLOBAL(FAXSPOOLDIR)}" != "" ]?${GLOBAL(FAXSPOOLDIR)}:${ASTSPOOLDIR}/fax)})
 same => n,Set(COMPANY=${IF($[ "${COMPANY}" != "" ]?${COMPANY}:FAX TEST)})
 same => n,Set(TIMESTAMP=${STRFTIME(${EPOCH},,%Y%m%d_%H.%M.%S)})
 same => n,Set(FAXSOURCE=${CALLERID(num)})
 same => n,Set(FAXEXTEN=${EXTEN})
 same => n,Set(SPOOLFAX=${FAXSPOOLDIR}/new/${FAXEXTEN})
 same => n,Set(SPOOLOLD=${FAXSPOOLDIR}/old/${FAXEXTEN})
 same => n,Set(SPOOLTMP=${FAXSPOOLDIR}/tmp/${FAXEXTEN})
 same => n,Set(SPOOLPDF=${FAXSPOOLDIR}/pdf/${FAXEXTEN})
 same => n,Set(Flog=${FAXSPOOLDIR}/message.log)
 ;WARNING: SHELL was inconsistent for me with my preferred GUID code (sometimes hang, sometimes works beautifully)
 ;         if you use it, make sure to test it over multiple cycles/config reloads.
 ;same => n,Set(GUID=${SHELL(/usr/local/bin/uuidgen | awk -F- '{ print $1 }'):0:-1})
 same => n,Set(GUID=XiiX${FAXCOUNTER})
 same => n,Set(FilePrefix=${FAXSOURCE}_${TIMESTAMP})
 same => n,Set(Ftiff=${FilePrefix}-${GUID}.tiff)
 same => n,Set(FAXOPT(headerinfo)=Receiving at ${COMPANY}, Time: ${TIMESTAMP})
 same => n,Set(FAXOPT(localstationid)=${FAXEXTEN})
 same => n,TrySystem(mkdir -pm 770 \"${SPOOLFAX}\")
 same => n,TrySystem(echo "${TIMESTAMP} ${GUID} RECEIVING START for ${FAXEXTEN} from ${FAXSOURCE} on ${FAXCHANNEL}" >> \"${Flog}\")
 same => n,Set(CHANNEL(hangup_handler_push)=hangup_handler,${FAXEXTEN},1(${Ftiff}))
 same => n,ReceiveFAX(${SPOOLFAX}/${Ftiff},f)
 same => n,Set(CHANNEL(hangup_handler_pop)=)
 same => n,Goto(hangup_handler,${FAXEXTEN},1(${Ftiff}))
 same => n(aboveconcurrentfaxes),Verbose(1,*** Concurrent [${GROUP_COUNT(ActiveFaxSessions)}] faxes, exceed limit [${MAXCONCURRENTFAXES}])
 same => n(end),Hangup()

[hangup_handler]
exten => _X.,1,Set(CHANNEL(hangup_handler_pop)=)
 same => n,Set(FINISHTIME=${STRFTIME(${EPOCH},,%Y%m%d_%H.%M.%S)})
 same => n,Goto(faxrcvd-${FAXOPT(status)},1)

exten => faxrcvd-SUCCESS,1,NoOp(SUCCESS - ${Ftiff})
 same => n,TrySystem(mkdir -pm 770 \"${SPOOLPDF}\")
 same => n,TrySystem(mkdir -pm 770 \"${SPOOLOLD}\")
 same => n,TrySystem(mkdir -pm 770 \"${SPOOLTMP}\")
 same => n,TrySystem(echo "${FINISHTIME} ${GUID} RECEIVING SUCCESS for ${FAXEXTEN}/${Ftiff} ${FAXOPT(pages)} pages" >> \"${Flog}\")
 same => n,TrySystem(echo "${FINISHTIME} ${GUID} PDF Conversion START for ${FAXEXTEN}/${Ftiff}" >> \"${Flog}\")
 same => n,Set(Fpdf=fax-${FilePrefix}_${FINISHTIME}-(${GUID})-${FAXOPT(pages)}Pages.pdf)
 same => n,TrySystem(${TIFF2PDF} -o \"${SPOOLTMP}/${Fpdf}\" \"${SPOOLFAX}/${Ftiff}\")
 same => n,Set(FINISHTIME=${STRFTIME(${EPOCH},,%Y%m%d_%H.%M.%S)})
 same => n,Goto(tiff2pdf-${SYSTEMSTATUS},1)

exten => faxrcvd-FAILED,1,NoOp(FAXOPT(error)     : ${FAXOPT(error)})
 same => n,TrySystem(mkdir -pm 770 \"${SPOOLPDF}\")
 same => n,TrySystem(mkdir -pm 770 \"${SPOOLOLD}\")
 same => n,TrySystem(mkdir -pm 770 \"${SPOOLTMP}\")
 same => n,TrySystem(echo "${FINISHTIME} ${GUID} RECEIVING FAILED for ${FAXEXTEN} from ${FAXSOURCE} ${FAXOPT(pages)} pages [${FAXOPT(error)}]" >> \"${Flog}\")
 same => n,GotoIf($[0${FAXOPT(pages)} > 0]?SomePagesRcvd:NoPagesRcvd)
 same => n(SomePagesRcvd),NoOp()
 same => n,Set(Fpdf=fax-${FilePrefix}_${FINISHTIME}-(${GUID})-${FAXOPT(pages)}Pages-ERROR-${FAXOPT(error)}.pdf)
 same => n,TrySystem(echo "${FINISHTIME} ${GUID} PDF Conversion START for ${FAXEXTEN}/${Ftiff} COMMS ERROR" >> \"${Flog}\")
 same => n,TrySystem(${TIFF2PDF} -o \"${SPOOLTMP}/${Fpdf}\" \"${SPOOLFAX}/${Ftiff}\")
 same => n,Set(FINISHTIME=${STRFTIME(${EPOCH},,%Y%m%d_%H.%M.%S)})
 same => n,Goto(tiff2pdf-${SYSTEMSTATUS},1)
 same => n(NoPagesRcvd),NoOp(** FAX FAIL, No Pages recieved **)
 same => n,Hangup()

exten => faxrcvd-.,1,NoOp(FAXOPT(error)      : ${FAXOPT(error)})
 same => n,TrySystem(echo "${FINISHTIME} ${GUID} RECEIVING ${FAXOPT(error)} for ${FAXEXTEN}/${Ftiff} ${FAXOPT(pages)} pages ${FAXOPT(error)}" >> \"${Flog}\")
 same => n,Hangup()

exten => tiff2pdf-SUCCESS,1,NoOp(${TIFF2PDF} : ${SYSTEMSTATUS})
 same => n,System(echo "${FINISHTIME} ${GUID} PDF Conversion SUCCESS for ${Fpdf}" >> \"${Flog}\")
 same => n,System(mv \"${SPOOLFAX}/${Ftiff}\" \"${SPOOLOLD}\")
 same => n,System(echo "${FINISHTIME} ${GUID} mv ${SYSTEMSTATUS} ${Ftiff}" >> \"${Flog}\")
 same => n,System(echo "${FINISHTIME} ${GUID} EMAIL START ${SPOOLPDF} :: ${Fpdf}" >> \"${Flog}\")
 same => n,System(${FAX2EMAIL} -d ${FAXEXTEN} -s ${FAXSOURCE} -t ${TIMESTAMP} -e \"${FAXOPT(error)}\" -a \"${SPOOLTMP}/${Fpdf}\" -p ${FAXOPT(pages)} -z \"${SPOOLPDF}\")
 same => n,Goto(fax2email-${SYSTEMSTATUS},1)

exten => tiff2pdf-FAILED,1,NoOp(${TIFF2PDF} : ${SYSTEMSTATUS})
 same => n,TrySystem(echo "${FINISHTIME} ${GUID} PDF Conversion FAILED for ${FAXEXTEN}/${Ftiff} REMOVE ${SPOOLTMP}/${Fpdf}" >> \"${Flog}\")
 same => n,TrySystem(rm -f \"${SPOOLTMP}/${Fpdf}\")
 same => n,TrySystem(mv \"${SPOOLFAX}/${Ftiff}\" \"${SPOOLOLD}\")
 same => n,Set(MVSTATUS=${SYSTEMSTATUS})
 same => n,TrySystem(echo "${FINISHTIME} ${GUID} mv ${SYSTEMSTATUS} ${Ftiff}" >> \"${Flog}\")
 same => n,TrySystem(echo "${FINISHTIME} ${GUID} EMAIL ${FAXEXTEN}/${Ftiff}" >> \"${Flog}\")
 same => n,TrySystem(${FAX2EMAIL} -d ${FAXEXTEN} -s ${FAXSOURCE} -t ${TIMESTAMP} -e \"PDF Conversion\" -a \"${SPOOLOLD}/${Ftiff}\" -p ${FAXOPT(pages)})
 same => n,Goto(fax2email-${MVSTATUS},1)

exten => tiff2pdf-APPERROR,1,NoOp(${TIFF2PDF} : ${SYSTEMSTATUS})
 same => n,TrySystem(echo "${FINISHTIME} ${GUID} PDF Conversion APPERROR for ${FAXEXTEN}/${Ftiff}" >> \"${Flog}\")
 same => n,Hangup()

exten => tiff2pdf-.,1,NoOp(${TIFF2PDF} : ${SYSTEMSTATUS})
 same => n,TrySystem(echo "${FINISHTIME} ${GUID} PDF Conversion ${SYSTEMSTATUS} for ${FAXEXTEN}/${Ftiff}" >> \"${Flog}\")
 same => n,Hangup()

exten => fax2email-SUCCESS,1,Verbose(FAX 2 Email success)
 same => n,TrySystem(echo "${FINISHTIME} ${GUID} EMAIL SUCCESS for ${FAXEXTEN} ${Fpdf}" >> \"${Flog}\")
 same => n,TrySystem(mv \"${SPOOLTMP}/${Fpdf}\"  \"${SPOOLPDF}\")
 same => n,TrySystem(echo "${FINISHTIME} ${GUID} mv ${SYSTEMSTATUS} ${Fpdf}" >> \"${Flog}\")
 same => n,Hangup()

exten => fax2email-FAILED,1,Verbose(FAX 2 Email failed)
 same => n,TrySystem(echo "${FINISHTIME} ${GUID} EMAIL FAILED for ${FAXEXTEN} ${Fpdf}" >> \"${Flog}\")
 same => n,Hangup()

exten => fax2email-APPERROR,1,Verbose(FAX 2 Email ${SYSTEMSTATUS})
 same => n,TrySystem(echo "${FINISHTIME} ${GUID} EMAIL ${SYSTEMSTATUS} for ${FAXEXTEN} ${Fpdf}" >> \"${Flog}\")
 same => n,Hangup()

exten => fax2email-.,1,Verbose(FAX 2 Email ${SYSTEMSTATUS})
 same => n,TrySystem(echo "${FINISHTIME} ${GUID} EMAIL ${SYSTEMSTATUS} for ${FAXEXTEN} ${Fpdf}" >> \"${Flog}\")
 same => n,Hangup()

Python Script: fax2email.py

Below is the Python Soure Code, available at: fax2email.py

#!/usr/local/bin/python
# Be explicit with the python pat as within sh environment the file may not
# be in your executables $PATH.

"""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.application import MIMEApplication
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
# from email.encoders import encode_base64

rfc822sender = "EXAMPLE Fax Service <donotrespond@example.com>"
COMMASPACE = ', '
fax2emailList = {
                    "0212345678": "departmentA@example.com",
                    "0287654321": "GroupA@example.com"
                }
errorList = {
    "Connection failed":
        """There was no receiver on the other end of the call
        to connect to.""",
    "Document generation error":
        """There was an error while generating the document. This could be due
            to the file being deleted before sending.""",
    "Failed to train with any of the compatible modems":
        """A remote fax machine was detected, but the sending and receiving
        modems could not establish communication.""",
    "Fax transmission not established":
        """We could not detect a remote fax
        machine. This could be due to there being no fax machine on the
        receiving end, or a lack of call clarity.""",
    "Log entry lost": """The call started, but the log entry for the call was
        lost / incomplete. In practice, this should be rare and may indicate a
        system problem.""",
    "No response after sending a page": """The remote fax machine did not
        acknowledge that a page of the fax was received. Depending on the
        remote machine's behaviour, it may have still printed the page and
        any preceding pages.""",
    "Received no response to DCS or TCF":
        """The bulk fax service could not successfully determine the remote
        machine's fax capabilities.""",
    "The call dropped prematurely":
        """The call dropped due to a non-fax
        transmission error. It is likely that the receiver hung up.""",
    "The HDLC carrier did not stop in a timely manner":
        """The fax service initiated a fax transmission with the receiver,
        but there was a synchronization (timing) error that could not
        be resolved.""",
    "Timed out waiting for initial communication":
        """A call was established with the receiver and the bulk fax service
        attempted to establish a fax session, but there was no fax response
        from the receiver. This could be due to there being no fax machine
        on the receiving end, or a lack of call clarity.""",
    "Timed out waiting for the first message":
        """A call was established with the receiver and the fax service
        attempted to establish a fax session, but there was no fax response
        from the receiver. This could be due to there being no fax machine
        on the receiving end, or a lack of call clarity.""",
    "Unexpected DCN while waiting for DCS or DIS":
        """The remote fax machine unexpectedly sent a disconnect message when
        the fax service asked for the remote fax machine's fax capabilities.
        """,
    "Received bad response to DCS or training":
        """An unexpected message was received when the bulk fax service asked
        for the remote fax machine's fax capabilities.""",
    "Invalid response after sending a page":
        """An unexpected message was received after successfully sending a
        page. Depending on the remote machine's behaviour, it may have still
        printed the sent page and any preceding pages.""",
    "Disconnected after permitted retries":
        """The fax service attempted to send the same message multiple times
        unsuccessfully. This may be due to a call clarity issue.""",
    "Far end cannot receive at the resolution of the image":
        """The remote fax machine does not support receiving faxes sent at the
        resolution that our service sends at.""",
    "Received a DCN from remote after sending a page":
        """The remote fax machine responded with a disconnect message after a
        page was sent successfully. Depending on the remote machine's behaviour
        , it may have still printed the sent page and any preceding pages.""",
    "Unexpected message received":
        """The fax service received a message that it did not expect given the
        current context. If this problem persists, it can indicate a
        compatibility problem between the sender's machine with FAX over VoIP.
        """,
    "Received other than DIS while waiting for DIS":
        """The fax service expected to receive the remote fax machine's fax
        capabilities, but received an unexpected message instead.""",
    "Unexpected DCN after EOM or MPS sequence":
        """The remotefax machine disconnected unexpectedly after receiving a
        page of a multipage fax. Depending on the remote machine's behaviour,
        it may have still printed the sent page and any preceding pages.""",
    "Document loading error.":
        """The fax service attempted to generate the message to send, but a
        document was missing.""",
    "Invalid ECM response received from receiver":
        """The fax service received an invalid error correction message from
        the remote fax machine.""",
    "Unexpected DCN after RR/RNR sequence":
        """The remote fax machine disconnected unexpectedly after indicating
        that it was not ready to initiate a fax session.""",
    "PDF Conversion":
        """Converting the fax image to a PDF document failed. The attachment
        is included as the RAW Fax Image
        (also known as Tagged Image File, TIFF.)""",
    "Processing":
        """The Inbound Fax failed whilst recieving and sending this file.
        The attachment is what has been recieved."""
}


def getCommandLine():
    parser = OptionParser(usage="""\
        Send an Attachment by email given the recipient fax number.

        Usage: %prog [options]

        """)
    parser.add_option('-a', '--attach',
                      type='string', action='store',
                      help="""Specify the file to attach.""")
    parser.add_option('-t', '--datetime',
                      type='string', action='store',
                      default="", dest='datetime',
                      help='Date and Time of Receipt')
    parser.add_option('-s', '--faxsource',
                      type='string', action='store',
                      default="", dest='faxsource',
                      help='The fax recipient Phone Number')
    parser.add_option('-d', '--faxdest',
                      type='string', action='store',
                      default="", dest='faxdest',
                      help='The fax recipient Phone Number')
    parser.add_option('-e', '--error',
                      type='string', action='store',
                      default="", dest='faxerror',
                      help='Fax Error Message')
    parser.add_option('-p', '--pages',
                      type='int', action='store',
                      default=0, dest='numpages',
                      help='Number of Pages')
    opts, args = parser.parse_args()
    if not opts.faxdest or not opts.attach:
        parser.print_help()
        sys.exit(1)

    return opts.faxdest, opts.attach, opts.faxsource, opts.datetime, opts.faxerror, opts.numpages


def initEnvelope(faxnumber):
    recipient = getEmailRecipient(faxnumber)
    if (recipient is not None):
        # Create the enclosing (outer) message
        mpMessage = MIMEMultipart()
        mpMessage['Subject'] = "Fax Received for {} and Attached".format(faxnumber)
        mpMessage['To'] = recipient
        mpMessage['From'] = rfc822sender
        mpMessage.preamble = 'You will not see this in a MIME-aware mail reader.\n'
        return mpMessage, recipient
    return None, recipient


def msgAttachment(attachpath):
    pathdir, attachment = os.path.split(attachpath)
    if (len(pathdir) > 0):
        os.chdir(pathdir)

    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 == 'pdf':
        # print "Mime:Application"
        fp = open(attachment, 'rb')
        msg = MIMEApplication(fp.read(), _subtype=subtype, _encoder=encode_base64)
        fp.close()
    elif maintype == 'tif' or maintype == 'tiff':
        # print "Mime:Image"
        fp = open(attachment, 'rb')
        msg = MIMEApplication(fp.read(), _subtype=subtype, _encoder=encode_base64)
        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)
    msg.add_header('Content-Disposition', 'attachment', filename=attachment)
    return msg


def getFaxErrorDescription(error):
    ErrorMsg = ""
    if (errorList.has_key(error)):
        ErrorMsg = "<p>" + errorList[error] + "<p>"
    if ("DCN" in error or "DIS" in error or "DTC" in error):
        acronyms = """
            <p>
                Common Acronyms used in Error Message include:
            </p>
            <table>
                <tr><th class="green">Acronym</th>
                    <th class="green">Expanded</th>
                </tr>
                <tr><td>DCN</td>
                    <td>Disconnect Command is sent from FAX transmitter to FAX
                    receiver to indicate call disconnection. If the
                    transmitting station follows EOP with the Disconnect
                    Command (DCN), it and the receiving end will then both
                    hang-up.</td>
                </tr>
                <tr><td>EOM</td>
                    <td>End of Message</td>
                </tr>
                <tr><td>EOP</td>
                    <td>End of Page is sent from FAX transmitter to FAX
                    receiver to indicate the end of the current page
                    transmission. But no more page is coming (that's all).
                </td>
                </tr>
                <tr><td>DIS</td>
                    <td>Digital Identification Signal is used by the called
                    fax machine to indicate its capabilities on scanning and
                    printing resolutions and modem capabilities etc.
                    <ul>
                        <li>Group?</li>
                        <li>Data rate?</li>
                        <li>Vertical resolution? (7.7 lines/mm yes/no)</li>
                        <li>Two-dimensional encoding? (y/n)</li>
                        <li>Page-width capabilities</li>
                        <li>Maximum page-length capability</li>
                        <li>Handshake speed</li>
                        <li>Error-correcting mode (y/n)</li>
                        <li>And so on</li>
                    </ul>
                    </td>
                </tr>
                <tr><td>DCS</td>
                    <td>Digital Command Signal is the calling terminal's
                    response to the DIS. It informs the called terminal what
                    type of modem and what type of coding and resolution will
                    be used. </td>
                </tr>
                <tr><td>DTC</td>
                    <td>Digital Transmission Control. To initiate a poll, the
                    calling station sends a Digital Transmit Command (DTC). As
                    with DIS, the DTC can be sent with station ID and the non-
                    standard field. The non-standard field can be used to
                    identify the particular fax to be sent (the caller wishes
                    to receive) as  well as the polling station's password, if
                    required.
                    If polled, the called station assumes control of the
                    session. If it has nothing to transmit it sends a
                    disconnect to the caller and hangs up. Therefore, the
                    calling station should always transmit any queued documents
                    before polling. If the polled station has a document to
                    transmit, it sends DCS, the command to receive. Note that,
                    as with the other control fields, the transmitter can send
                    its station ID, as well as the ID of the individual
                    recipient.</td>
                </tr>
                <tr><td>TCF</td>
                    <td>Training Check Field. This command is sent through the
                    T.4 modulation system to verify training and to give a
                    first indication of the acceptability of the channel
                    for this data rate. </p>
                    <p>The training check sequence that is formed by feeding
                    continuous zero bits into the modem scrambler and
                    modulating the output bits for 1.5s. If the calling gateway
                    receives a fail to train (FTT) message in response to the
                    training check (TCF) test pattern generated at 14.4 kbps,
                    the calling fax machine may fall back to the next lower
                    rate and then it sends a new digital command signal
                    (DCS) message followed by a TCF test pattern at the new
                    data rate. This process is continued until a TCF test
                    pattern is received OK or all data rates have been
                    attempted. In some implementations, gateways are falling
                    back to the pass-through mode by sending the SIP re-INVITE
                    request using the G.711 codec if continuously the FTT
                    message is received in response to the TCF test pattern
                    more than three times.</p>
                    </td>
                </tr>
            </table>
        """
        ErrorMsg = ErrorMsg + acronyms
    return ErrorMsg


def msgBody(mpMime, destfax, attachpath, faxsource, datetime, faxerror, pages):
    filepath, file_extension = os.path.splitext(attachpath)
    errorMessage = getFaxErrorDescription(faxerror)
    textHtmlHeader = """
        <html>
        <head>
            <style>
                body {
                    font-family:"Calibri",sans-serif;
                }
                h1 {
                    color: blue;
                    font-family:"Calibri",sans-serif;
                    font-size: 18pt;
                }
                table {
                    border-collapse: collapse;
                    width: 100%;
                }
                th.green {
                    background-color: #4CAF50;
                    color: white;
                }
                th.myblue {
                    background-color: #00CCEE;
                    color: white;
                }
                th.warning {
                    background-color: #CC0000;
                    color: white;
                }
                th td {
                    padding: 8px;
                    vertical-align: top;
                    border-bottom: 1px solid #ddd;
                }
                tr {
                    vertical-align: top;
                }
                tr:nth-child(even){
                    background-color: #f2f2f2
                }
                tr:hover {
                    background-color: #f5f5f5
                }
                p, li, th, td, table
                {
                    font-size:13.0pt;
                    font-family:"Calibri",sans-serif;
                    text-align: left;
                    margin-bottom: 0.5em;
                    margin-top: 0.5em;
                }
                p.footer, li.footer
                {
                    margin-top: 0in;
                    margin-bottom:.0001pt;
                    font-size:11.0pt;
                    line-height:1.5pt;
                }
                p.monospace, li.monospace
                {
                    font-family: "Courier New", Monospace;
                    font-size: 10pt;
                }
                p.error, td.error {
                    vertical-align: top;
                    color: red
                }

            </style>
        </head>
        """

    textHtmlBodyHeader = """
        <body>
            <p>Hello,</p>

            <p>A fax has been received for <strong>{faxnumber}</strong> and is
            forwarded to you as an attachment with this e-mail message</p>

        """.format(faxnumber=destfax)
    textHtmlErrorComms1 = """
            <p>&nbsp;</p>
            <p><span style="color:red">Notice:</span> The attached fax may be
            incomplete </p>
            <p>
            A communication error occurred during receipt of the fax and we
            cannot confirm whether all the pages for the fax has been received.
            </p>
            <p>What has been received before the communication error is
            attached. The originating fax may resend (at which point you will
            receive another e-mail with another attachment) or if the
            originating fax is not going to retry, you may need to contact
            the originator to confirm what has been received is complete.
            </p>
        """
    textHtmlErrorComms2 = """
        <p>
            The Fax Communications Error Message is described below:
        </p>
        <table>
            <tr><th class="warning">Error Message</th>
                <th class="warning">Description</th>
            </tr>
            <tr><td class="error">{faxerror}</td>
                <td>{ErrorMessage}</td>
            </tr>
        </table>
        """.format(
                faxerror=faxerror,
                ErrorMessage=errorMessage)

    textHtmlErrorImageConversion = """
            <p><span style="color:red">Notice:</span> The attached image is a
            multipage TIFF Image file<br />
            An error occurred while trying to convert the fax to a PDF.
            </p>
            <p>If Windows Photo Viewer is unable to open the file. Use either
            of the following image views
            to verify whether the facsimile was received correctly:
                <ul>
                    <li><a href="http://irfanview.com">IrfanView</a></li>
                    <li><a href="http://xnview.com">XnView</a></li>
                </ul>
            </p>
            <p>If the Image is not viewable in any of the above applications,
            then please ask the original
            sender to re-send the fax.</p>
        """
    successPages = pages
    if (pages == 0):
        successPages = "N/A"
    textHtmlBodyFaxMeta = """
            <p>
            The following details summarise the attached document.
            </p>
            <table>
                <tr>
                    <th class="myblue">Item</th>
                    <th class="myblue">Details</th>
                </tr>
                <tr><td>Our Fax Number:
                    <td><strong>{faxnumber}</strong></td>
                </tr>
                <tr><td>Source Fax Number:
                    <td><strong>{faxsource}</strong></td>
                </tr>
                <tr><td>Date and Time</td>
                    <td><strong>{datetime}</strong><br />
                    the date and time the fax receipt was <i>started</i> <br/>
                    (in the format YYYYMMDD_HH.mm.ss)</td>
                </tr>
                <tr><td>Pages</td>
                    <td><strong>{numPages}</strong><br />
                    The number of pages successfully received.</td>
                </tr>
            </table>
        """.format(
                    faxnumber=destfax,
                    faxsource=faxsource,
                    datetime=datetime,
                    numPages=successPages
                    )
    textHtmlBodySalutation = """
            <p>&nbsp</p>
            <p>&nbsp</p>
            <p>Please submit any questions about this fax to
                <a href="mailto:servicedesk@example.com">
                    servicedesk@example.com</a>
            </p>
            <p>&nbsp</p>
            <p><strong>EXAMPLE Facsimile Service</strong></p>
            <p class="footer">EXAMPLE COMPANY<br/>
                Level 7, 800 Walk Street, Sydney NSW 2000<br/>
                <span style='color:gray'>T:</span>&nbsp;1800-1234-5678<br/>
                <span style='color:gray'>E:</span>&nbsp;
                    <a href="mailto:servicedesk@example.com">
                        servicedesk@example.com</a> <br />
                <span style='color:gray'>W:</span>&nbsp;
                <a href="https://www.example.com">www.example.com</a>
            </p>
        """
    textHtmlBodyFooter = """
        </body>
        </html>
        """
    textHtml = textHtmlHeader + textHtmlBodyHeader
    if (faxerror != ""):
        textHtml = textHtml + textHtmlErrorComms1
    if ("tif" in file_extension):
        textHtml = textHtml + textHtmlErrorImageConversion

    textHtml = textHtml + textHtmlBodyFaxMeta + textHtmlBodySalutation

    if (faxerror != ""):
        textHtml = textHtml + textHtmlErrorComms2

    textHtml = textHtml + textHtmlBodyFooter

    bodyTextHtml = MIMEText(textHtml, 'html')
    mpMime.attach(bodyTextHtml)
    return mpMime


def closeEnvelope(outer, msg):
    outer.attach(msg)
    return outer.as_string()


def getEmailRecipient(faxdestination):
    if (fax2emailList.has_key(faxdestination)):
        return fax2emailList[faxdestination]
    else:
        return None


def main():
    destFax, attachpath, faxsource, datetime, faxerror, numpages = getCommandLine()
    mpMime, emailRecipient = initEnvelope(destFax)
    if mpMime is not None:
        mpMime.attach(msgAttachment(attachpath))
        mpMime = msgBody(
                        mpMime, destFax, attachpath, faxsource, datetime,
                        faxerror, numpages)

        s = smtplib.SMTP('mailrelay.emia.com.au')
        s.sendmail(rfc822sender, emailRecipient, mpMime.as_string())
        s.quit()
    else:
        print "Note: No email recipient for this fax number"


if __name__ == '__main__':
    main()

If you get this far, thank you for reading my diatribe.

I've put more "features" into fax2email.py because I've seen the Dialplan context "drop out" of the System call and the rest of the context, not processing anything afterwards.