set up Twilio SMS to Email
tl;dr: set up a pass-through phone number that forwards to anywhere
cost: $0.0075 per inbound SMS
build time: 15 minutes (MVP)
overview
Think of all the times you've had to submit a phone number to get a promotional code, or an ebook download, or the love of your life at a bar.
In many cases, you end up subscribed to lists that are quite difficult to opt out of. Anyone who's made a political donation is familiar with the wave of unsolicited SMS's they get every election season.
In the below walkthrough, I'll detail setting up a virtual phone number with Twilio, creating a webhook handling Lambda function, and forwarding incoming SMS and phone calls to an email address. You can then create rules to filter them from there.
If you're more familiar with SendGrid and Node.js, I recommend you check out this Twilio tutorial instead.
This article assumes you've already set up AWS SES email sending. If you haven't, read this guide I put together first.
#1 - setting up Twilio
#1.1 - create an account here
#1.2 - complete 2FA auths
It will make you confirm both an existing email and existing phone number. Go through the 'ahoy' onboarding as well.
#1.3 - set up a new phone number here
The menu pathing is [Phone Numbers -> Active Number -> Get Started -> Get your first Twilio phone number]
#2 - setting up the webhook
Setting up a webhook that waits for data is as simple as standing up an API Gateway instance that can accept POSTs and attaching a Lambda to it.
#2.1 - clone the Github repo
git clone git@github.com:alecbw/Twilio-SMS-To-Email-www.alec.fyi.git
(rename the top level directory to be whatever you want)
#2.2 - (optional) change variable names in serverless.yml
You can keep the defaults or change the service (CloudFormation stack) name, AZ region, and function name and API endpoint path
service: public-facing
... truncated...
provider:
region: us-west-1
... truncated...
functions:
twilio-webhook:
handler: twilio_webhook_handler.lambda_handler
events:
- http:
path: /twilio_webhook
method: post
#2.3 - deploy the stack (change NAMEOFTOPLEVELDIRECTORY)
cd NAMEOFTOPLEVELDIRECTORY && sls deploy
#2.4 - get endpoint URL
When you deploy, the serverless CLI will output the newly generated API endpoint:
For simplicity, we are not making this API endpoint private (API Key protected). Be sure not to share or leak your endpoint address! If someone spammed it with hundreds of thousands of requests, it could get somewhat expensive (and destroy the deliverability of your email sending domain).
#2.5 - add that endpoint to Twilio
The pathing is Phone Numbers -> Manage Numbers -> Active Numbers -> [click on your number]
Scroll down to the webhook options.
#2.6 - add a balance to your Twilio account
The minimum is $20. That'll get you 2,666 inbound texts.
You can stop here if you want. Everything that follows explains how it works
#3 Lambda handler logic
Twilio passes data about the call/text in the body
of the API request. Do note: inside that data is another key:value pair called body
which holds the contents of the message.
'body': 'ToCountry=US&ToState=CA&SmsMessageSid=SM823cbdcd8c79d6c4a43b2bd9e4bc5c9e&NumMedia=0&ToCity=&FromZip=94109&SmsSid=SM82390dsc79d6c4a43b2bd9e4bc5c9e&FromState=CA&SmsStatus=received&FromCity=SAN_FRANCISCO&Body=Test+Works!&FromCountry=US&To=%2B10987654321&ToZip=&NumSegments=1&MessageSid=SM823cbdcd29306c4a43b2bd9e4bc5c9e&AccountSid=AC0f7ad2e4fbcc15f6e36bf33a57475ffb&From=%2B12345678900&ApiVersion=2010-04-01',
You can map it to a dictionary as if it were a querystring:
body_dict = {x[0]:x[1] for x in [x.split("=") for x in event["body"].split("&")]}
You'll then want to make it human readable. I suggest converting to string with pretty print
import pprint
invocation_dict = {
"Subject": f"Received SMS: {datetime.now().strftime('%m/%d/%Y, %H:%M:%S')}",
"Body": pprint.pformat(param_dict),
"Recipients": ["recipient@your-domain.com"] # Has to be a list
}
Then it's up to you how to send that with SES (the two approaches are covered below)
#4 email sending
#4.1 - if you have a separate email-sending Lambda
If you went through the earlier SES article, you should have an email sending Lambda set up. In which case, you can use Lambda <> Lambda Invoke to send the Twilio webhook data.
Below I've included a Lambda Invoke helper function that checks for and handles all conceivable errors. Feel free to use just the lambda_client
and lambda_response
lines if you prefer simplicity.
import boto3
def invoke_lambda(params, function_name, invoke_type):
lambda_client = boto3.client("lambda")
lambda_response = lambda_client.invoke(
FunctionName=function_name,
InvocationType=invoke_type,
Payload=json.dumps(params),
)
# Async Invoke returns only StatusCode
if invoke_type.title() == "Event":
return None, lambda_response.get("StatusCode", 666)
string_response = lambda_response["Payload"].read().decode("utf-8")
json_response = json.loads(string_response)
if not json_response:
return "Unknown error: no json_response. Called lambda may have timed out.", 500
elif json_response.get("errorMessage"):
return json_response.get("errorMessage"), 500
status_code = int(json_response.get("statusCode"))
json_body = json.loads(json_response.get("body"))
if json_response.get("body") and json_body.get("error"):
return json_body.get("error").get("message"), status_code
return json_body["data"], status_code
request_data, request_status = invoke_lambda(
invocation_dict,
"STACKNAME-prod-send-email",
"RequestResponse"
)
#4.2 - (alternately)
If you have set up SES but don't want to maintain a separate handler for email sending, you can add the core functionality to the webhook (instead of the Lambda Invoke)
import boto3
response = boto3.client("ses", region_name="us-west-2").send_email(
Source="hello@your-domain.com",
ReplyToAddresses=["hello@your-domain.com"],
Destination={"ToAddresses": invocation_dict["Recipients"]},
Message={
"Subject": {"Data": invocation_dict["Subject"]},
"Body": {"Text": {"Data": invocation_dict["Body"]}}
},
)
#4.3 - validate
send a text to your phone number and check your receiving email. It should be in the format of
The Github Repo with all the code in this article can be found here
If you made it this far and felt the above implementation was too involved or technical, know you can recreate the functionality in Zapier:
- Catch Hook (by Zapier)
- Send Outbound Email (by Zapier)
[it does require you to be on the $20/mo plan though]
Thanks for reading. Questions or comments? 👉🏻 alec@contextify.io