Search code examples
asp.net-mvcvb.nettwiliomms

Retrieve file attachment from MMS via ASP.NET MVC controller


I'm trying out the Twilio service to interact with individuals via SMS/MMS. I've sorta figured out how to send MMS messages to initiate the "conversation" and that seems to be working well. However, now I'm trying to build a system to respond to the incoming messages on my SMS/MMS-enabled test number. I'm working from one of the examples I found on the Twilio documentation site to build an ASP.NET MVC web service to handle the conversation (VB.NET):

Imports System.Web.Mvc
Imports Twilio.AspNet.Common
Imports Twilio.AspNet.Mvc
Imports Twilio.TwiML

Namespace Controllers
    Public Class SMSController
        Inherits TwilioController

        ' GET: SMS
        Function Index(ByVal IncomingSMS As SmsRequest) As TwiMLResult
            Dim SMSResponse As New MessagingResponse
            Dim SMSMessage As String = IncomingSMS.Body
            Dim JediCode As String = "There is no emotion, there is peace."
            Dim SithCode As String = "Peace is a lie. There is only Passion."

            JediCode += vbCrLf & "There is no ignorance, there is knowledge."
            JediCode += vbCrLf & "There is no passion, there is serenity."
            JediCode += vbCrLf & "There is no chaos, there is harmony."
            JediCode += vbCrLf & "There is no death, there is the Force."

            SithCode += vbCrLf & "Through Passion, I gain Strength."
            SithCode += vbCrLf & "Through Strength, I gain Power."
            SithCode += vbCrLf & "Through Power, I gain Victory."
            SithCode += vbCrLf & "Through Victory my chains are Broken."
            SithCode += vbCrLf & "The Force shall free me."

            If SMSMessage IsNot Nothing Then
                If SMSMessage.ToUpper.Trim = "JEDI" Then
                    SMSResponse.Message(JediCode)
                ElseIf SMSMessage.ToUpper.Trim = "SITH" Then
                    SMSResponse.Message(SithCode)
                Else
                    SMSResponse.Message("Ahsoka? Is that you?")
                End If
            Else
                SMSResponse.Message("What did you want to know?")
            End If

            Return TwiML(SMSResponse)
        End Function
    End Class
End Namespace

Yes, this is all just "play" stuff that I'm using for testing and will eventually be replaced with something more appropriate to the purpose, but I want to try and figure it all out before I get too deep into the reality of things.

I've set up the site on my IIS server, registered the DNS, and even gotten my SSL certificate set up. Everything seems to be working great with my simple testing so far, but there are a couple of things that I still haven't been able to figure out so far and I'm hoping someone here can point me in the right direction.

I'll ask each as a separate question, but here's the first: how do I retrieve the attachment from an MMS message?

I'd like to be able to receive PDFs (and possibly other file types) and pass them along via email to an appropriate individual or department. I know how to do the emailing, but I haven't been able to find appropriate documentation for how to retrieve the attachment(s) in the MMS message to actually include it in that email process.

When I try to access the properties of the IncomingSMS (SmsRequest) object, I don't find any reference to Media in any of them - no NumMedia, no MediaUri, nothing. There doesn't appear to be an MmsRequest object type (that I've found yet, anyway).

What am I overlooking here to be able to retrieve the PDF I sent to my test number for further processing? Should I change the method's definition to accept the object as a MessageResource or something?

EDIT: I forgot to mention that I checked the Twilio console and see that the message was apparently received successfully with the attachment, so I know that at least that part is working properly.

I've asked a second, related question that goes along with this one to help "finalize" some things for our goals.


Solution

  • The SmsRequest class that you're using comes from the Twilio helper library for ASP.NET which aims to make it easier to integrate Twilio into ASP.NET. However, the SmsRequest and other classes do not cover all possible webhook parameters. If there's parameters missing from the class, you can still retrieve the parameters manually instead of relying on MVC Model Binding.

    Based on this C# sample, I created a VB.NET sample to show how to receive and save incoming MMS files:

    Imports System.IO
    Imports System.Net.Http
    Imports System.Threading.Tasks
    Imports MimeTypes
    Imports Twilio.AspNet.Mvc
    Imports Twilio.TwiML
    Imports Twilio.TwiML.Messaging
    
    Public Class HomeController
        Inherits TwilioController
    
        Shared httpClient As HttpClient = New HttpClient
    
        Async Function Index() As Task(Of TwiMLResult)
            Dim response As New MessagingResponse
            Dim message As New Message
    
            Dim numMedia = Short.Parse(If(Request.Form.Get("NumMedia"), 0))
            If numMedia = 0 Then
                response.Message("No file received.")
                Return TwiML(response)
            End If
    
            For mediaIndex As Integer = 0 To numMedia - 1
                Dim mediaUrl = Request.Form.Get($"MediaUrl{mediaIndex}")
                Dim contentType = Request.Form.Get($"MediaContentType{mediaIndex}")
                Dim saveFilePath = Server.MapPath(String.Format(
                    "~/App_Data/{0}{1}",
                    Path.GetFileName(mediaUrl),
                    MimeTypeMap.GetExtension(ContentType)
                ))
                Await DownloadUrlToFileAsync(mediaUrl, saveFilePath)
            Next
    
            response.Message("File received.")
            Return TwiML(response)
        End Function
    
        Private Async Function DownloadUrlToFileAsync(mediaUrl As String, saveFilePath As String) As Task
            Dim Response = Await httpClient.GetAsync(mediaUrl)
            Dim httpStream = Await Response.Content.ReadAsStreamAsync()
            Using fileStream As Stream = IO.File.Create(saveFilePath)
                Await httpStream.CopyToAsync(fileStream)
                Await fileStream.FlushAsync()
            End Using
        End Function
    
    
    End Class
    

    You can probably simplify the code a little by assuming there's only one file going to be sent over MMS, but it's a good idea to handle other cases too.

    To get the correct file extension, I'm using this MimeTypeMap library, but you can roll your own solution.

    By default the files from the incoming MMS are publicly available via the MediaUrl{mediaIndex} URL, but it's a good idea to turn on Basic Auth for these media files.

    If you're turning on Basic Auth for media files, you'll need to add the authentication header to the HTTP requests, like this:

    Imports System.IO
    Imports System.Net.Http
    Imports System.Net.Http.Headers
    Imports System.Threading.Tasks
    Imports MimeTypes
    Imports Twilio.AspNet.Mvc
    Imports Twilio.TwiML
    Imports Twilio.TwiML.Messaging
    
    Public Class HomeController
        Inherits TwilioController
    
        Shared httpClient As HttpClient = CreateHttpClient()
        Private Shared Function CreateHttpClient() As HttpClient
            Dim client As New HttpClient
            Dim appSettings As NameValueCollection = ConfigurationManager.AppSettings
            If Boolean.Parse(If(appSettings.Get("TwilioUseBasicAuthForMedia"), False)) Then
                Dim authString = $"{appSettings.Get("TwilioAccountSid")}:{appSettings.Get("TwilioAuthToken")}"
                authString = Convert.ToBase64String(Encoding.ASCII.GetBytes(authString))
                client.DefaultRequestHeaders.Authorization = New AuthenticationHeaderValue("Basic", authString)
            End If
    
            Return client
        End Function
    
        ...
    
    End Class
    

    I'm retrieving the TwilioUseBasicAuthForMedia, TwilioAccountSid, and TwilioAuthToken from the Web.config appSettings.

    (Make sure you don't check those secrets into source control, and use UserSecretsConfigBuilder instead, to securely set the secrets).

    Here's the source code on GitHub.


    You're welcome to submit a GitHub issue and/or a PR to add support for these missing parameters to the SmsRequest. Tho, it wouldn't be as easy as normal model binding because the number of parameters increases as more files are sent over MMS.