Michael Okoko Linux and Sci-Fi ➕ = ❤️

Receiving emails with Bref PHP and SendGrid

5 min read 1402

Bref is a composer package that makes it easy to run serverless PHP applications on AWS Lambda. It achieves this by providing the required layers needed to run PHP applications since they are not supported natively on Lambda.

In this tutorial, we will be building and deploying a serverless PHP application that processes incoming emails programmatically using SendGrid Inbound Parse.

Prerequisites

To follow along, you will need:

  • PHP >= 7.2 (to use the latest version of Bref)
  • Composer, npm, and the serverless CLI installed
  • SendGrid Inbound Parse configured for your domain/subdomain (so that SendGrid can handle all incoming emails for such domain)
  • Ngrok installed (to expose your Bref application while developing locally)

Installing Bref and application dependencies

To get started, create a folder for your application (I am naming mine bref-email-watch) and enter into the directory with the command below:

$ mkdir bref-email-watch && cd bref-email-watch

Install the application dependencies which comprise the bref CLI, phpdotenv to enable us to load environment variables from a .env file, and nexylan/slack to interact with Slack’s API:

$ composer require bref/bref vlucas/phpdotenv nexylan/slack php-http/discovery

With our dependencies installed, initialize Bref by running ./vendor/bin/bref in the project directory and select the HTTP option from the interactive menu.

Output of “bref init” command
Output of “bref init” command

The command will create a serverless.yml file which acts as the manifest for how the serverless framework will deploy your application as well as an index.php file to serve as an entry point into the application.

Next, create a .env file in the project folder and add the Slack hook URL:

SLACK_HOOK_URL="HERE_LIVES_YOUR_SLACK_HOOK_URL"

Parsing incoming emails from SendGrid

The application works by receiving JSON payloads (in the form of HTTP post requests) from SendGrid each time there is a new mail on the configured domain. We will modify the generated index.php file to parse these payloads, extract the sender and the recipient (using regex and PHP’s preg_match()), and send a Slack message to the relevant channel containing the extracted data.

Open the index.php file and replace its content with the code block below:

try {
    if (strtoupper($_SERVER['REQUEST_METHOD'] != 'POST')) {
        throw new Exception("Received non-post request on webhook handler");
    }

    if (json_last_error() != JSON_ERROR_NONE) {
        $em = "Error while parsing payload: ".json_last_error_msg();
        throw new Exception($em);
    }

    $from = $_POST['from'];
    $to = $_POST['to'];

    preg_match("#<(.*?)>#", $from, $sender);
    preg_match("#<(.*?)>#", $to, $recipient);
    $senderAddr = $sender[1];
    $recipientAddr = $recipient[1];

    $message = "*You've got mail!*\n";
    $message .= "*To:* ".$recipientAddr."\n";
    $message .= "*From:* ".$senderAddr;

    notifyOnSlack($message, true);

    // send OK back to SendGrid so they stop bothering our webhook
    header("Content-type: application/json; charset=utf-8");
    echo json_encode(["message" => "OK"]);
    exit(0);
} catch (Exception $e) {
    notifyOnSlack($e->getMessage());
    header("Content-type: application/json; charset=utf-8");
    http_response_code(400);
    echo json_encode(["message" => $e->getMessage()]);
    exit(0);
}

Sending slack notifications for new emails

In the previous code block, we referenced a notifyOnSlack function that doesn’t exist yet. This function is responsible for sending the $message parameter it receives to Slack. To implement it, load the variables declared in the .env file into your application by adding the following code to the top of the index.php file (just before the try block):

require_once './vendor/autoload.php';
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();

Next, wire up the function implementation, like this:

function notifyOnSlack($message, $markdown = false)
{
    $slackHookUrl = $_ENV["SLACK_HOOK_URL"];
    $options = [
        "channel" => "#general",
        "allow_markdown" => $markdown,
        "username" => "bref-email-watch",
    ];
    $client = new Nexy\Slack\Client(
        \Http\Discovery\Psr18ClientDiscovery::find(),
        \Http\Discovery\Psr17FactoryDiscovery::findRequestFactory(),
        \Http\Discovery\Psr17FactoryDiscovery::findStreamFactory(),
        $slackHookUrl,
        $options
    );
    $client->send($message);
}

The function loads the SLACK_HOOK_URL environment variable from the .env file and then sets up the options, which includes the channel the message is to be sent to, to then be passed to the Slack client. We also instantiate the client by passing in the HTTPlug discovery services which allow it to find and use any HTTP client that conforms to the PSR standard.

Testing the serverless functions locally

Now that our application is set up, start the built-in PHP server on port 3000 and open an ngrok tunnel on the same port:

$ php -S localhost:3000
$ ngrok http 3000

The ngrok command generates Forwarding URL, like this:

ngrok command output

Copy the URL and visit your SendGrid’s Inbound Parse settings page. Now, click on the Add Host & URL button and paste in the copied URL in the Destination URL field.

Inbound Parse webhook settings

You may want to set up a proper subdomain since SendGrid will notify your webhook of EVERY email that comes to the domain name (irrespective of the username).

Next, send an email to an email address on the domain you specified and the notification should show up on Slack like this:

sample slack notification
sample slack notification

Configuring AWS credentials

Setting up an IAM role

For our application to be successfully deployed, Bref and the Serverless CLI need access to the following AWS resources:

  • Lambda
  • IAM
  • APIGateway
  • S3
  • CloudFormation
  • CloudWatch Logs

If you have an IAM user with these permissions, you can go ahead and use their AWS access keys and secrets, else:

Screenshot of IAM users’ home page

  • On the new user page, specify a username to help you remember the purpose of the user e.g bref-sendgrid-inbound, then Enable Programmatic Access and click Next to proceed to the permissions page:

Screenshot of adding a new IAM user

Select the Attach existing policies directly tab and click the Create policy button. This will open a new browser tab for you to set up a new permissions policy.

Attaching an existing policy to the IAM user

Select the JSON tab on the Create policy page and paste in the code block below:

{
        "Version": "2012-10-17",
        "Statement": [
            {
                "Sid": "VisualEditor0",
                "Effect": "Allow",
                "Action": [
                    "iam:*",
                    "s3:*",
                    "apigateway:*",
                    "lambda:*",
                    "cloudformation:*",
                    "logs:*"
                ],
                "Resource": "*"
            }
        ]
}

Give the policy a descriptive name, review it, and complete the policy creation process.

Creating a new policy from a JSON policy document

  • Return to the Add user page and attach the new policy by selecting it from the list. Note, you may have to refresh the list for your changes to be reflected.

Attaching the newly created policy to an IAM user

Click through the Next button at the bottom of the page to finish creating your IAM user. View and copy the user’s Access Key ID as well as the Secret Key to a temporary place.

Credentials page of the new IAM user

Using the AWS credentials

Back in your terminal, set up the copied credentials using the serverless config command:

$ serverless config credentials --provider aws --key  AWS_ACCESS_KEY_ID  --secret AWS_SECRET --profile bref-sendgrid-inbound

The above command will add a new entry in the file holding your AWS credentials. Remember to update the provider section in your serverless.yml file to match the profile specified above as well as your IAM user’s region. Below is an example of the modified serverless.yml config:

service: app

provider:
    name: aws
    region: us-west-2
    runtime: provided
    # "profile" should match the profile specified while configuring the serverless CLI
    profile: bref-sendgrid-inbound

plugins:
    - ./vendor/bref/bref

functions:
    api:
        handler: index.php
        description: ''
        timeout: 28 # in seconds (API Gateway has a timeout of 29 seconds)
        layers:
            - ${bref:layer.php-73-fpm}
        events:
            -   http: 'ANY /'
            -   http: 'ANY /{proxy+}'

# Exclude files from deployment
package:
    exclude:
        - 'node_modules/**'
        - 'tests/**'

 

Deploying to Lambda

We can now deploy our application by running the command below from the project directory.

$ serverless deploy

The command generates an application URL e.g https://XXXXXXX.execute-api.us-west-2.amazonaws.com/dev when it has finished the deployments. You can then update the Destination URL on your Inbound Parse settings page to match this generated URL.

Inbound Parse webhook settings using the Lambda function URL

Test the application again by sending an email to [email protected]_DOMAIN.COM and you should get a Slack message similar to the one below:

Sample Slack Message

Conclusion

Working with emails can be fun and though the focus is usually on sending them, we can have as much fun receiving them via code. In this tutorial, we saw exactly how to do that and explored the Bref serverless library while at it. You can find the complete project on GitHub.

The Bref and serverless framework documentation are good places if you are looking to further explore them. Also, you can further learn to restrict access to your lambda functions by using features like Lambda Authorizers.

Get setup with LogRocket's modern error tracking in minutes:

  1. Visit https://logrocket.com/signup/ to get an app ID.
  2. Install LogRocket via NPM or script tag. LogRocket.init() must be called client-side, not server-side.
  3. $ npm i --save logrocket 

    // Code:

    import LogRocket from 'logrocket';
    LogRocket.init('app/id');
    Add to your HTML:

    <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
    <script>window.LogRocket && window.LogRocket.init('app/id');</script>
  4. (Optional) Install plugins for deeper integrations with your stack:
    • Redux middleware
    • ngrx middleware
    • Vuex plugin
Get started now
Michael Okoko Linux and Sci-Fi ➕ = ❤️

Leave a Reply