In our last post we created a feedback form that updated Azure DevOps with a new work item (feature) based on content from a static web form.
Next we would like to expand on that to include sending emails. We have a few options from Azure Logic Apps to Sendgrid to AWS SES. Let's start with SES on this since we will be well within the free tier.
Setting up SES
If you have not already, verify a domain in SES. It was easy to do with one already hosted in Route53
Next. we need to verify an email address
which sends an email
Email and confirmation
Once verified, we can see that as a result
In testing, I was required to validate both my from and to addresses. I did some digging and initially we are fixed to a "sandbox" account, albeit that is shown oddly under metrics.
$ cat destination.json
{
"ToAddresses": ["isaac.johnson@gmail.com"],
"CcAddresses": [],
"BccAddresses": []
}
$ cat message.json
{
"Subject": {
"Data": "Test email sent using SES",
"Charset": "UTF-8"
},
"Body": {
"Text": {
"Data": "You are Great! Mostly....",
"Charset": "UTF-8"
},
"Html": {
"Data": "<H1>You are Great!</H1><p>Mostly...</p>",
"Charset": "UTF-8"
}
}
}
$ aws ses send-email --from isaac@freshbrewed.science --destination file://destination.json --message file://message.json
An error occurred (MessageRejected) when calling the SendEmail operation: Email address is not verified. The following identities failed the check in region EU-CENTRAL-1: isaac@freshbrewed.science, isaac.johnson@gmail.com
For this test, I went ahead and verified the destination address
Once verified, that worked
$ aws ses send-email --from isaac@freshbrewed.science --destination file://destination.json --message file://message.json
{
"MessageId": "0100017a3de1238c-1c58e528-50af-429b-b7dc-f521d3a70726-000000"
}
Result:
Secure SMTP
The other way we could go about it is with secure SMTP.
Go to SES and get your secure SMTP settings:
To use Secure SMTP, i tend to use s-nail:
With ubuntu/debian one can use
sudo apt-get update
sudo apt-get install -y s-nail || true
With mac, just use the brew formulae (gripe: Big Sur made homebrew update a lot so be patient)
brew install s-nail
On a container with ubuntu we can use snail :
root@my-shell:/# echo "TEsting" | s-nail -s "Welcome" -M "text/html" -S smtp=email-smtp.us-east-1.amazonaws.com:587 -S smtp-use-starttls -S ssl-verify=ignore -S ssl-verify=ignore -S smtp-auth=login -S smtp-auth-user=AKAAAAAAAAAAAAAAA -S smtp-auth-password=BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -c isaac.johnson@gmail.com -r isaac@freshbrewed.science isaac.johnson@gmail.com
Or on the Mac
$ echo "TEsting" | s-nail -s "Welcome" -M "text/html" -S smtp=smtp://email-smtp.us-east-1.amazonaws.com:587 -S smtp-use-starttls -S ssl-verify=ignore -S ssl-verify=ignore -S smtp-auth=login -S smtp-auth-user=AAAAAAAAAAAAAAAAAAAAAA -S smtp-auth-password=BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -c isaac.johnson@gmail.com -r isaac@freshbrewed.science isaac.johnson@gmail.com
s-nail: Warning: variable superseded or obsoleted: smtp
s-nail: Warning: variable superseded or obsoleted: ssl-verify
s-nail: Warning: variable superseded or obsoleted: smtp-auth-user
s-nail: Warning: variable superseded or obsoleted: smtp-auth-password
s-nail: Obsoletion warning: please do not use *smtp*, instead assign a smtp:// URL to *mta*!
s-nail: Obsoletion warning: Use of old-style credentials, which will vanish in v15!
s-nail: Please read the manual section "On URL syntax and credential lookup"
Both send emails
Updating the Pipeline to send mails
Lastly we can update our form processor to send emails.
We do this by adding the SMTP Credentials to our Group Vars library
Then we can add to the pipeline like this
- script: |
sudo apt-get update
sudo apt-get install -y s-nail || true
set -x
export WIID=`cat azresp.json | jq -r '.id' | tr -d '\n'`
cat rawSummary |sed ':a;N;$!ba;s/\n/ /g' | sed 's/"/\\"/g' > emailSummary
echo "<h1>New Feature Requested</h1><p>user ${{ parameters.feedbackForm.userId }} has requested "`cat emailSummary`"</p><p>https://dev.azure.com/princessking/ghost-blog/_workitems/edit/$WIID/</p><br/><br/>Kind Regards,<br/>Isaac Johnson" | s-nail -s "Blog: Feature $WIID Requested" -M "text/html" -S smtp=email-smtp.us-east-1.amazonaws.com:587 -S smtp-use-starttls -S ssl-verify=ignore -S smtp-auth=login -S smtp-auth-user=$(SMTPAUTHUSER) -S smtp-auth-password=$(SMPTAUTHPASS) -c isaac.johnson@gmail.com -r isaac@freshbrewed.science isaac.johnson@gmail.com
displayName: 'Email Me'
Moving out of SES sandbox
When you are ready, go to the "Sending Statistics" section in SES to move yourself out of sandbox.
We use "Edit your account details" to put in a request for Production use
Until we get approved, expect to see errors going to unverified emails:
2021-06-24T12:39:47.7129853Z + echo '<h1>New Feature Requested</h1><p>user isaac.johnson@mycompany.com has requested "`cat inputSummary`"</p><p>https://dev.azure.com/princessking/ghost-blog/_workitems/edit/$WIID/</p><br/><br/>Kind Regards,<br/>Isaac Johnson'
2021-06-24T12:39:47.7132219Z + s-nail -s 'Blog: Feature 109 Requested' -M text/html -S smtp=email-smtp.us-east-1.amazonaws.com:587 -S smtp-use-starttls -S ssl-verify=ignore -S smtp-auth=login -S smtp-auth-user=*** -S smtp-auth-password=*** -c isaac.johnson@gmail.com -r isaac@freshbrewed.science isaac.johnson@gmail.com
2021-06-24T12:39:48.4526656Z + echo '<h1>New Feature Requested</h1><p>user isaac.johnson@mycompany.com has requested "`cat inputSummary`"</p><p>https://dev.azure.com/princessking/ghost-blog/_workitems/edit/$WIID/</p><br/><br/>Kind Regards,<br/>Isaac Johnson'
2021-06-24T12:39:48.4529918Z + s-nail -s 'Blog: Feature 109 Requested' -M text/html -S smtp=email-smtp.us-east-1.amazonaws.com:587 -S smtp-use-starttls -S ssl-verify=ignore -S smtp-auth=login -S smtp-auth-user=*** -S smtp-auth-password=*** -c isaac.johnson@mycompany.com -r isaac@freshbrewed.science isaac.johnson@gmail.com
2021-06-24T12:39:49.0259121Z s-nail: SMTP server: 554 Message rejected: Email address is not verified. The following identities failed the check in region US-EAST-1: isaac.johnson@mycompany.com
2021-06-24T12:39:49.0583946Z /home/vsts/dead.letter 13/574
2021-06-24T12:39:49.0584784Z s-nail: ... message not sent
Once approved, you can see you are now in the Production System:
Getting approved actually required a few back and forth exchanges. I am guessing they are keen to stop spammers from abusing SES. So be thorough and verbose in your request verbiage.
The pipeline step added with the CC requestor commented out (for now)
- script: |
sudo apt-get update
sudo apt-get install -y s-nail || true
set -x
export WIID=`cat azresp.json | jq -r '.id' | tr -d '\n'`
cat rawSummary |sed ':a;N;$!ba;s/\n/ /g' | sed 's/"/\\"/g' > emailSummary
echo "<h1>New Feature Requested</h1><p>user ${{ parameters.feedbackForm.userId }} has requested "`cat emailSummary`"</p><p>https://dev.azure.com/princessking/ghost-blog/_workitems/edit/$WIID/</p><br/><br/>Kind Regards,<br/>Isaac Johnson" | s-nail -s "Blog: Feature $WIID Requested" -M "text/html" -S smtp=email-smtp.us-east-1.amazonaws.com:587 -S smtp-use-starttls -S ssl-verify=ignore -S smtp-auth=login -S smtp-auth-user=$(SMTPAUTHUSER) -S smtp-auth-password=$(SMPTAUTHPASS) -c isaac.johnson@gmail.com -r isaac@freshbrewed.science isaac.johnson@gmail.com
# may not work
# echo "<h1>New Feature Requested</h1><p>user ${{ parameters.feedbackForm.userId }} has requested "`cat emailSummary`"</p><p>https://dev.azure.com/princessking/ghost-blog/_workitems/edit/$WIID/</p><br/><br/>Kind Regards,<br/>Isaac Johnson" | s-nail -s "Blog: Feature $WIID Requested" -M "text/html" -S smtp=email-smtp.us-east-1.amazonaws.com:587 -S smtp-use-starttls -S ssl-verify=ignore -S smtp-auth=login -S smtp-auth-user=$(SMTPAUTHUSER) -S smtp-auth-password=$(SMPTAUTHPASS) -c ${{ parameters.feedbackForm.userId }} -r isaac@freshbrewed.science isaac.johnson@gmail.com
displayName: 'Email Me'
Verification
we can request a new feature from the webform
which triggers the pipeline
and we of course get an email now
Azure DevOps Dashboards
One more change would be to add some reports we can see on the Dashboard of our project.
From there we can add some Query results to the Project Dashboard
Now when we add a feature:
We see a nice results page in our Dashboard:
As well as email
and update to slack
Production SES Enabled
with production SES enabled, let's circle back and CC users if they ask for it
- script: |
sudo apt-get update
sudo apt-get install -y s-nail || true
set -x
export WIID=`cat azresp.json | jq -r '.id' | tr -d '\n'`
cat rawSummary |sed ':a;N;$!ba;s/\n/ /g' | sed 's/"/\\"/g' > emailSummary
export USERTLD=`echo "${{ parameters.feedbackForm.userId }}" | sed 's/^.*@//'`
if [[ "$USERTLD" == "dontemailme.com" ]]; then
# do not CC user
echo "<h1>New Feature Requested</h1><p>user ${{ parameters.feedbackForm.userId }} has requested "`cat emailSummary`"</p><p>https://dev.azure.com/princessking/ghost-blog/_workitems/edit/$WIID/</p><br/><br/>Kind Regards,<br/>Isaac Johnson" | s-nail -s "Blog: Feature $WIID Requested" -M "text/html" -S smtp=email-smtp.us-east-1.amazonaws.com:587 -S smtp-use-starttls -S ssl-verify=ignore -S smtp-auth=login -S smtp-auth-user=$(SMTPAUTHUSER) -S smtp-auth-password=$(SMPTAUTHPASS) -c isaac.johnson@gmail.com -r isaac@freshbrewed.science isaac.johnson@gmail.com
else
# may not work
echo "<h1>New Feature Requested</h1><p>user ${{ parameters.feedbackForm.userId }} has requested "`cat emailSummary`"</p><p>https://dev.azure.com/princessking/ghost-blog/_workitems/edit/$WIID/</p><br/><br/>Kind Regards,<br/>Isaac Johnson" | s-nail -s "Blog: Feature $WIID Requested" -M "text/html" -S smtp=email-smtp.us-east-1.amazonaws.com:587 -S smtp-use-starttls -S ssl-verify=ignore -S smtp-auth=login -S smtp-auth-user=$(SMTPAUTHUSER) -S smtp-auth-password=$(SMPTAUTHPASS) -c ${{ parameters.feedbackForm.userId }} -r isaac@freshbrewed.science isaac.johnson@gmail.com
fi
displayName: 'Email Me'
I'll try using my corporate email
Which was delivered to my corp email
Azure Logic Apps
Let's do the same thing but this time with Azure Logic Apps.
Go to the portal and create a Logic App. I tend to use Consumption plan.
Next we give it a name and Resource Group
Then create it
Once created we need to design a basic emailer. I generally start with a webhook trigger
If you click "use a sample payload" it can be easy to create that first step
For instance, we could use a payload such as:
{
"requestorEmail": "anonymous@dontemailme.com",
"emailSummary": "make a feature",
"workitemID": "12345",
"workitemURL": "https://dev.azure.com/princessking/ghost-blog/_workitems/edit/12345/",
"desitinationEmail": "isaac.johnson@gmail.com"
}
which when we click done creates
{
"type": "object",
"properties": {
"requestorEmail": {
"type": "string"
},
"emailSummary": {
"type": "string"
},
"workitemID": {
"type": "string"
},
"workitemURL": {
"type": "string"
},
"desitinationEmail": {
"type": "string"
}
}
}
Next largely depends on if you are using sendgrid or an o365 connection.
Sendgrid
Sendgrid will need an API key
Here we can use the dynamic content and fill in the fields
Before we go further, recall that we want to CC the user if it's not a "dontemailme" email address.
We can add a condition to check on this:
When done we should have a condition that forks logic with the only difference being that if they specified a real email address, we will CC it
Once we save the Logic App, we can go to the first step to get a URL for our Logic App
Validation
We can use curl in our shell to test
$ cat valuesLA.json
{
"requestorEmail": "anonymous@dontemailme.com",
"emailSummary": "make a feature",
"workitemID": "12345",
"workitemURL": "https://dev.azure.com/princessking/ghost-blog/_workitems/edit/12345/",
"desitinationEmail": "isaac.johnson@gmail.com"
}
$ curl -v -X POST -d @valuesLA.json -H "Content-type: application/json" "https://prod-34.eastus.logic.azure.com:443/workflows/851f197339fa4f09b1517ebf712efdcc/triggers/manual/paths/invoke?api-version=2016-10-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=uSUo-SNU_TZqorFctkDEVaiA0fCo92HFS-XLAR3N5V8"
Note: Unnecessary use of -X or --request, POST is already inferred.
* Trying 137.135.106.54...
* TCP_NODELAY set
* Connected to prod-34.eastus.logic.azure.com (137.135.106.54) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
* CAfile: /etc/ssl/cert.pem
CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384
* ALPN, server accepted to use h2
* Server certificate:
* subject: C=US; ST=WA; L=Redmond; O=Microsoft Corporation; CN=eastus.logic.azure.com
* start date: Apr 14 21:57:31 2021 GMT
* expire date: Apr 9 21:57:31 2022 GMT
* subjectAltName: host "prod-34.eastus.logic.azure.com" matched cert's "*.eastus.logic.azure.com"
* issuer: C=US; O=Microsoft Corporation; CN=Microsoft Azure TLS Issuing CA 02
* SSL certificate verify ok.
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x7faa8180d600)
> POST /workflows/851f197339fa4f09b1517ebf712efdcc/triggers/manual/paths/invoke?api-version=2016-10-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=uSUo-SNU_TZqorFctkDEVaiA0fCo92HFS-XLAR3N5V8 HTTP/2
> Host: prod-34.eastus.logic.azure.com
> User-Agent: curl/7.64.1
> Accept: */*
> Content-type: application/json
> Content-Length: 255
>
* Connection state changed (MAX_CONCURRENT_STREAMS == 100)!
* We are completely uploaded and fine
< HTTP/2 202
< cache-control: no-cache
< pragma: no-cache
< expires: -1
< x-ms-workflow-run-id: 08585766400244707146123513754CU79
< x-ms-correlation-id: 6e044971-7197-4420-b651-be08fae9b7ec
< x-ms-client-tracking-id: 08585766400244707146123513754CU79
< x-ms-trigger-history-name: 08585766400244707146123513754CU79
< x-ms-execution-location: eastus
< x-ms-workflow-system-id: /locations/eastus/scaleunits/prod-34/workflows/851f197339fa4f09b1517ebf712efdcc
< x-ms-workflow-id: 851f197339fa4f09b1517ebf712efdcc
< x-ms-workflow-version: 08585766402382171437
< x-ms-workflow-name: FeatureRequestedEmail
< x-ms-tracking-id: 6e044971-7197-4420-b651-be08fae9b7ec
< x-ms-ratelimit-burst-remaining-workflow-writes: 4999
< x-ms-ratelimit-remaining-workflow-download-contentsize: 357913600
< x-ms-ratelimit-remaining-workflow-upload-contentsize: 357913345
< x-ms-ratelimit-time-remaining-directapirequests: 33333257
< x-ms-request-id: eastus:6e044971-7197-4420-b651-be08fae9b7ec
< strict-transport-security: max-age=31536000; includeSubDomains
< date: Tue, 29 Jun 2021 11:41:00 GMT
< content-length: 0
<
* Connection #0 to host prod-34.eastus.logic.azure.com left intact
* Closing connection 0
Note: if you get an error like
<
* Connection #0 to host prod-34.eastus.logic.azure.com left intact
{"error":{"code":"DirectApiAuthorizationRequired","message":"The request must be authenticated only by Shared Access scheme."}}* Closing connection 0
Chances are you forgot to put the URL in double quotes (i do this all the time)
I almost immediately got an email:
Office 365 / Outlook
If you instead want to use O365 instead of Sendgrid, that is just as easy.
We use the "Office 365 Outlook" task
These steps fill out just the same as Sendgrid albeit you cannot specify the "from" email address (Outlook determines this for you)
While i'll avoid displaying my corp email address, you can see O365 spewed in the standard boilerplate legal footer
Adding Logic App Invokation to Pipeline
To add to our azure pipeline is even easier than the SES step as we can assume the Azure Logic App will do the logic on CCing
...snip...
cat >valuesLA.json <<EOLA
{
"requestorEmail": "${{ parameters.feedbackForm.userId }}",
"emailSummary": "`cat emailSummary`",
"workitemID": "$WIID",
"workitemURL": "https://dev.azure.com/princessking/ghost-blog/_workitems/edit/$WIID/",
"desitinationEmail": "isaac.johnson@gmail.com"
}
EOLA
curl -v -X POST -d @valuesLA.json -H "Content-type: application/json" "https://prod-34.eastus.logic.azure.com:443/workflows/851f197339fa4f09b1517ebf712efdcc/triggers/manual/paths/invoke?api-version=2016-10-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=uSUo-SNU_TZqorFctkDEVaiA0fCo92HFS-XLAR3N5V8"
displayName: 'Email Me'
Validation
Create a feedback ticket
which then on submit confirms
and we see the feedback form pipeline kick in
and completes
And i can see the Logic App results
O365:
Sendgrid:
and AWS SES:
In the Azure portal we can see the runs of this app:
And because I signed in as myself, I, interestingly enough, see the emails in my Sent box of Outlook:
I have found Logic Apps can add up costwise over time so I generally only use them when I want to leverage the Microsoft Graph to pull in user details from Exchange.
For instance, pulling in the users First and Last name when i just have their userid (which i pulled from git or AzDO)
Note, you can regenerate the SIG (key) for the logic app from the portal, say, you posted a blog with the real URL
Which now if using the posted URL would error
<
* Connection #0 to host prod-34.eastus.logic.azure.com left intact
{"error":{"code":"AuthorizationFailed","message":"The authentication credentials are not valid."}}* Closing connection 0
Lastly for cleanup, i commented out the CURL line in my pipeline yaml (as i'm fine with SES).
Summary
It was easy to extend our pipeline to send emails out using secure SMTP. We showed using the AWS command line client and secure SMTP via s-nail. Additionally we covered how to apply for production SES access and update our Azure DevOps Dashboard for improved visibility.
We then looked into Azure Logic Apps using both Sendgrid and O365 to send form email and implemented conditional logic in the Logic App itself. Our final tests included creating a form driven feature request that emailed us three ways: SES, Azure Logic App to Sendgrid and Azure Logic App to Office 365.
While we have thoroughly covered multiple ways to send out emails from pipelines for user feature request ingestion, we might want to start to think next about how we might use SES to ingest emails. What if the requestor has more details or feedback? Could we automate a system that processes emails with SES and Dapr in a cluster? We'll tackle that next week as we dive into SES, SNS, SQS, Dapr and Mime parsing.