[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.
Goal:
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
Requirements:
Make sure you have a functioning Asterisk box, and have installed the above dependencies before you continue.
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.
[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.
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:
[fax-inbound]
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.
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}.
[fax-outbound]
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.
Channel: SIP/${TRUNK-CONTEXT}/${FAXNUMBER}
CallerID: ${FAXSRC}
Archive: Yes
WaitTime: ${WAITTIME}
MaxRetries: ${RETRIES}
RetryTime: ${RETRYTIME}
Context: fax-outbound
Extension: ${FAXNUMBER}
Priority: 1
SetVar: FAXFILE=${TIFFSPOOLDIR}/${FAXNUMBER}/fax-${FILEPREFIX}-to-${FAXNUMBER}.tiff
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
To set up our dialplan, we need to specify the “extension” or number that will be taken as an incoming fax call.
[incoming_trunk]
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.
To receive a fax, we:
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:
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)
There are two things that we do, as part of fax receipt.
same => n,Set(PDFCONVERSION=${SHELL(${TIFF2PDF} -o ${FAXPATH}.pdf ${FAXPATH}.tiff)})
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.
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()