Exploring Postmark Tags For Grouping Related Transactional Emails In Lucee CFML 5.3.6.61
At InVision, we use Postmark to send both our outbound transactional emails and handle inbound reply emails. We've been using them for over 8-years and it's been just a seamless, outstanding experience. In those 8-years, however, Postmark has added a number of features that we have yet to take advantage of. The other day, I looked at injecting debugging meta-data using SMTP headers. Today, I wanted to look at using Tags to group our transaction emails using the CFMail
and CFMailParam
tags in Lucee CFML 5.3.6.61.
Just as with the SMTP meta-data, a Tag is defined using CFMailParam
and an SMTP header: X-PM-Tag
. I couldn't seem to find much information about the limitations of the tag value, such as how long it's allowed to be. However, only one tag can be provided per transactional email.
To explore the use of Tags - and how they manifest themselves in the Postmark application UI (User Interface) - I created a dummy ColdFusion component to send out some sample emails. This ColdFusion component contains three methods to send out three emails:
sendDailyDigestEmail()
sendPasswordResetEmail()
sendWelcomeEmail()
And, each of these emails is going to contain a unique X-PM-Tag
SMTP header, respectively:
X-PM-Tag
=DailyDigestEmail
X-PM-Tag
=PasswordResetEmail
X-PM-Tag
=WelcomeEmail
Here's the code - remember, these are just dummy emails, don't pay attention to the actual email body (even though it uses the amazing tag islands feature available in Lucee CFML):
component
output = false
hint = "I provide methods for sending out transactional system emails."
{
/**
* I initialize the emailer with the given SMTP settings.
*
* @smtpServer I am the SMTP server address.
* @smtpPort I am the SMTP server port.
* @postmarkApiToken I am the Postmark API token used to authenticate emails.
*/
public void function init(
required string smtpServer,
required string smtpPort,
required string postmarkApiToken
) {
variables.smtpServer = arguments.smtpServer;
variables.smtpPort = arguments.smtpPort;
variables.postmarkApiToken = arguments.postmarkApiToken;
// Postmark can only send from a predefined set of signatures.
variables.systemEmail = "ben@bennadel.com";
// All outbound CFMail tags will share this set of base attributes.
variables.baseMailAttributes = {
from: systemEmail,
type: "html",
// For the sake of the demo, I don't want to spool emails, I want to see
// errors immediately in the response if they fail.
async: false,
server: smtpServer,
port: smtpPort,
username: postmarkApiToken,
password: postmarkApiToken
};
}
// ---
// PUBLIC METHODS.
// ---
/**
* I send the user a digest of recent activity related to their account.
*
* @toEmail I am the user to which the email is being sent.
* @digestActivityItems I am the collection of activity items to report.
*/
public void function sendDailyDigestEmail(
required string toEmail,
required array digestActivityItems
) {
mail
to = toEmail
subject = "Your InVision daily digest"
attributeCollection = baseMailAttributes
{
mailparam
name = "X-PM-Tag"
value = "DailyDigestEmail"
;
```
<cfoutput>
<h1>
You have been a busy bee!
</h1>
<p>
Here are you #encodeForHtml( digestActivityItems.len() )# items: ...
</p>
</cfoutput>
```
}
}
/**
* I send the user an email that contains a password-reset call-to-action (CTA).
*
* @toEmail I am the user to which the email is being sent.
* @passwordResetUrl I am the password reset URL (CTA).
*/
public void function sendPasswordResetEmail(
required string toEmail,
required string passwordResetUrl
) {
mail
to = toEmail
subject = "Reset your InVision password"
attributeCollection = baseMailAttributes
{
mailparam
name = "X-PM-Tag"
value = "PasswordResetEmail"
;
```
<cfoutput>
<h1>
Please use the following link to reset your InVision password
</h1>
<p>
<a href="#encodeForHtmlAttribute( passwordResetUrl )#">Reset password</a>
</p>
</cfoutput>
```
}
}
/**
* I send the user the InVision welcome email.
*
* @toEmail I am the user to which the email is being sent.
*/
public void function sendWelcomeEmail( required string toEmail ) {
mail
to = toEmail
subject = "Welcome to InVision!"
attributeCollection = baseMailAttributes
{
mailparam
name = "X-PM-Tag"
value = "WelcomeEmail"
;
```
<cfoutput>
<h1>
Welcome to InVision!
</h1>
<p>
This is going to be awesome. Are you ready to begin?
</p>
</cfoutput>
```
}
}
}
As you can see, each email contains a CFMailParam
tag through which we are defining an SMTP header. This SMTP header contains our Postmark tag, which is then incorporated into the Activity stream within the Postmark UI.
ASIDE: Unlike with the Postmark meta-data headers, the
X-PM-Tag
SMTP header is not stripped out of the outbound email. As such, these tag values can be seen in the source within the user's email client. Don't use tags to pass around private information - use theX-PM-Metadata-*
headers for private information.
Now, to try sending out a few of these emails:
<cfscript>
emailer = new Emailer(
smtpServer = "smtp.postmarkapp.com",
smtpPort = "2525",
postmarkApiToken = request.postmarkApiToken
);
// Send out test emails with embedded Postmark Tags.
emailer.sendWelcomeEmail( getRandomEmail() );
emailer.sendPasswordResetEmail( getRandomEmail(), "https://reset.my.password" );
emailer.sendDailyDigestEmail( getRandomEmail(), [ {}, {}, {} ] );
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
/**
* I return a random test email to send-to.
*/
public string function getRandomEmail() {
var emails = [
"ben+doria@bennadel.com", "ben+timmy@bennadel.com", "ben+steve@bennadel.com",
"ben+jojo@bennadel.com", "ben+sonia@bennadel.com", "ben+clay@bennadel.com",
"ben+anna@bennadel.com", "ben+roland@bennadel.com", "ben+dave@bennadel.com"
];
return( emails[ randRange( 1, emails.len() ) ] );
}
</cfscript>
Once we send out some ColdFusion emails using CFMail
and CFMailParam
with out SMTP headers, if we pop over to the Postmark application console, we can see our Tags start to show up in the activity stream:
And, what's even cooler is that we can then filter by tags by either clicking on one of the tags in the activity stream; or, by selecting a tag from the Filter drop-down menu:
Seeing this functionality in action, I feel silly saying that, historically, I've had to find groups of emails by searching based on Subject line value. Which is to say, a "fuzzy match" at best, especially since our Subject lines tend to be dynamic based on user. This Tag functionality is really going to make finding a particular email much easier for our Product and Support Engineers!
For 8-years, I've been extremely happy with Postmark as both an outbound and inbound email SaaS (Software as a Service) provider. And now, with things like custom meta-data headers and Tagging, things are only looking better! Plus, I love how easy these features are to leverage using their SMTP API within Lucee CFML 5.3.6.61.
Want to use code from this post? Check out the license.
Reader Comments
But why continue to use the SMTP API instead of the REST API? We were delighted with our move to the Postmark API with email templates; it just made transaction emails a lot easier all round.
@Geoff,
Purely historical momentum. We have a lot of code that uses the SMTP approach. That said, it is all mostly encapsulated in a ColdFusion component (
EmailService.cfc
); as such, it would be fairly straightforward to start converting<cfmail>
tags to HTTP calls. At the end of the day, though, I am not sure that we're actually feeling any "pain points" around the SMTP approach.I will admit, though, I didn't even know Postmark_ had email templates! I swear, they quietly snuck all of this stuff in there over the last 8-years and I was completely oblivious to it :D I'll take a look at that stuff, it sounds cool. I've been "hand crafting" emails for the last decade and it never gets any easier.