Tuesday, June 16, 2015

Call Recordings to Email


Summary

In this article I'll be describing how create a simple call recording service that will record a message from an IVR application and then attach the resulting audio content to an email.  That email could then be routed to an agent in a contact center scenario, for instance.

Environment

Figure 1 depicts the overall architecture.  I created a simple VXML application that provides the voice interface.  That same VXML app sends the collected audio content to a Node-based web service.  The web service repackages the audio content into an email attachment.

Figure 1

Implementation

Figure 2 below depicts the voice and data flow for this architecture.

Figure 2

The web service is built as a simple Node application.  Below is high-level organization of that app.

Figure 3

Finally, Figure 4 depicts the input/output behavior of this web service.

Figure 4

Code Snippets

Voice Application



<?xml version="1.0" encoding="UTF-8"?>
<vxml version="2.1">

  <catch event="error.badfetch.http.400">
    <log label="Error" expr="'HTTP 400 - Bad Request'"/>
    <prompt>
      This request was rejected by the server due to being malformed.  Good bye.
      <break time="1000"/>
    </prompt>
  </catch>
  
  <catch event="error.badfetch.http.413">
    <log label="Error" expr="'HTTP 413 - Request Entity Too Large'"/>
    <prompt>
        This request had an upload that is larger than the server will accept.  Good bye.
        <break time="1000"/>
    </prompt>
  </catch>
  
  <catch event="error.badfetch.http.500">
    <log label="Error" expr="'HTTP 500 - Internal Server Error'"/>
    <prompt>
      The server has experienced an internal error.  Good bye.
      <break time="1000"/>
    </prompt>
  </catch>
  
  <form>
    <var name="ANI" expr="session.callerid" />
    <var name="DNIS" expr="session.calledid" />
    <block>
      <prompt>
        This is a message recording demo.
        <break time="200"/>
      </prompt>
    </block>
    
    <record  name="MSG" beep="true" maxtime="20s" dtmfterm="true" type="audio/mp3">
        <prompt timeout="5s">
          Record a message after the beep.
        </prompt>
        <noinput>
          I didn't hear anything, please try again.
         </noinput>
        <filled>
          <submit next="http://yourwebserver/upload" enctype="multipart/form-data"
            method="post" namelist="ANI DNIS MSG"/>
        </filled>
    </record>
  </form>
</vxml>
Lines 4-26:  Catch logic for HTTP error
Lines 29-30: Saving ANI and DNIS in variables for use later in the HTTP form post.
Line 38:  VXML tag used to record a caller.  Output will be in MP3 format.
Line 46:  Finally, when the recording has ended - send it and the ANI + DNIS via an HTTP multipart form.

Web Service (main body of code)

appHttp.post('/upload', function(req, res) {
          try {
            logger.debug('Entering - File: main.js, Method: appHttp.post()');
                        
            var form = new multiparty.Form();
            var ani = null;
            var dnis = null;
            var fname = null;
            var msg = null;
            var size = 0;
                        
            form.on('error', function(err, statCode) {
              logger.error('File: main.js, Method: appHttp.post(), form(), Error: ' + err.message);
              res.status(statCode || 400).end();
            });
                        
            form.on('part', function(part) {
              var data=[];
                          
              part.on('error', function(err, statCode) {
                form.emit('error', err, statCode);
              });
                          
              part.on('data', function(chunk) {
                size += chunk.length;
                if (size > properties.maxUploadSize) {
                  //covers a degenerate case of too large of an upload.  Possible DOS attempt
                  part.emit('error', new Error('Upload exceeds maximum allowed size'), 413);
                }
                else {  
                  data.push(chunk);
                }
              });
                         
              part.on('end', function() {
                switch (part.name) {
                  case 'ANI':
                    ani = data.toString();
                    break;
                  case 'DNIS':
                    dnis = data.toString();
                    break;
                  case 'MSG':
                    if (part.filename) {
                      fname = part.filename;
                      msg = Buffer.concat(data);
                    }
                    else {
                      part.emit('error', new Error('Malformed file part in form'), 400);
                    }
                    break;
                  default:
                    part.emit('error', new Error('Unrecognized part in form'), 400);
                    break;
                }      
              });
            });
                        
            form.on('close', function() {
              if (ani && dnis && fname && msg) {
                res.status(200).sendFile(__dirname + '/vxml/response.vxml');
                var mailOptions = {
                  from : properties.emailFromUser,
                  to : properties.emailToUser,
                  subject : 'Recorded Message - ANI:' + ani + ', DNIS:' + dnis,
                  text : 'The attached recorded audio message was received.',
                  attachments : [{filename : fname, content : msg}]
                };
                
                transporter.sendMail(mailOptions, function(err, info) {
                  if (err) {
                    appHttp.emit('error', err);
                  }
                  logger.debug('Exiting - File: main.js, Method: appHttp.post()');
                });    
              }
              else {
                form.emit('error', new Error('Form missing required fields'), 400);
              }             
            });
                        
            form.parse(req); 
          }
Line 1:  This is the Express route for an 'upload' POST.
Line 5:  The multiparty node module is used for processing the POST'ed form data.
Lines 24-33:  Compile the form 'chunks' that are uploaded.  If an upload is being attempted that is larger than a user-configured maximum limit, terminate the upload.  Based on my testing, simply emitting an error is enough to cause Node/Express to terminate an upload.  I saw no need for something like 'req.connection.destroy()'.
Lines 35-57:  When a form 'part' has been completely uploaded, determine which 'part' it was and save it into local variables.
Lines 59-80:  When the entire form has been completely uploaded, determine if all the expected 'parts' were included.  If so, send back a 200 OK with simple VXML response.  Then, send the 'parts' out as an email.  The ANI and DNIS are put in the subject line of the email.  The audio content is sent as an attached file.

Output

Snippet of the resulting email output below:
From: yourFromAddress@gmail.com
To: yourToAddress@gmail.com
Subject: Recorded Message - ANI:1234567890, DNIS:9876543210
X-Mailer: nodemailer (1.3.4; +http://www.nodemailer.com;
 SMTP/1.0.3[client:1.2.0])
Date: Tue, 16 Jun 2015 01:02:06 +0000
Message-Id: <1434416526890-858ca434-7a77d97c-8d53507c data-blogger-escaped-gmail.com="">
MIME-Version: 1.0

------sinikael-?=_1-14344165263510.8602459693793207
Content-Type: text/plain
Content-Transfer-Encoding: 7bit

The attached recorded audio message was received.
------sinikael-?=_1-14344165263510.8602459693793207
Content-Type: audio/mpeg
Content-Disposition: attachment; filename=MSG-1434416526064.mp3
Content-Transfer-Encoding: base64


Copyright ©1993-2024 Joey E Whelan, All rights reserved.