Skip to main content
Ben Nadel at CFCamp 2023 (Freising, Germany) with: Dörte Brosch
Ben Nadel at CFCamp 2023 (Freising, Germany) with: Dörte Brosch

Disabling Async Attribute On CFMail For At-Least-Once Delivery In Lucee CFML

By
Published in

When I'm sending emails out in a ColdFusion application, not all emails are created equal. Much of the time, if an email is lost here-and-there, it's not the end of the world. For example, a "Forgot My Password" email can always be sent a second time. In critical ColdFusion workflows, however, when losing an email is unacceptable, I track the processing of pending emails in the database; and, I make sure to set the async (Lucee CFML) / spoolEnable (Adobe ColdFusion) attribute on the CFMail tag to false.

By default (though configurable), when you send an email using the CFMail tag in ColdFusion, the ColdFusion platform queues-up the email and then attempts to send it out in the background. This allows the parent request to finish processing before the email delivery takes place (yay for performance!). And, in the vast majority of cases, those emails get sent successfully. But, there are a number of reasons why a queued-up email might fail to be delivered:

  • Server crash.
  • Server being destroyed as part of a deployment.
  • Network blip.
  • Remote mail server error.

When a spooled email fails to be delivered asynchronously, ColdFusion logs the error; but, your application doesn't know anything about it. This At-Most-Once delivery approach might be fine, depending on you the context. Or, it might be a big problem. When it's a problem, I shift into an At-Least-Once delivery workflow, where I track the email delivery in the database and err on the side of sending the same email more than once if the first delivery attempt appears to fail.

To do this, I set the async attribute (or spoolEnable in ACF) to false. This prevents the outbound email from being queued-up and delivered in the background. Instead, ColdFusion will block the current request and attempt to send the email out on the parent thread. Which means, if an error occurs, it will be thrown in the parent thread; which means, we can catch it and react to it as needed.

I use this technique to create idempotent workflows, which can be executed over-and-over again until all the pending emails have been sent successfully:

<cfscript>

	for ( pendingEmail in getPendingEmails() ) {

		try {

			mail
				to = pendingEmail.email
				from = "no-reply@example.com"
				subject = "Please review our updated Terms of Service (TOS)."
				type = "html"
				// If ASYNC / SPOOLENABLE is set to TRUE (which I believe is the default),
				// the emailer will spool the email to disk and then try to send it out in
				// the background (on a different thread). This means, if there is an
				// error sending the mail, your application logic won't know about it. By
				// setting the async attribute to FALSE, you force the mail to be sent on
				// the CURRENT THREAD, which means any error during the delivery will
				// result in an exception in this request. This is great, because it means
				// that our application will know about it and can handle it properly.
				async = false
				{

				include "./new-tos-email-body.cfm";
			}

			markPendingEmailAsSent( pendingEmail );

		} catch ( any error ) {

			// TODO: Log the error for this mail item, but move onto the next pending
			// email. This whole workflow should be idempotent and result in an AT LEAST
			// ONCE delivery of each email.

		}

	}

	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //

	/**
	* FOR DEMO: I get the emails that have been scheduled for delivery but not yet sent.
	*/
	public array function getPendingEmails() {

		return([
			{ email: "al@example.com", isSent: false },
			{ email: "sarah@example.com", isSent: false },
			{ email: "jo@example.com", isSent: false },
			{ email: "sam@example.com", isSent: false },
			{ email: "tricia@example.com", isSent: false }
		]);

	}


	/**
	* FOR DEMO: I mark the given pending email as having been successfully sent.
	*/
	public void function markPendingEmailAsSent( required struct pendingEmail ) {

		pendingEmail.isSent = true;

	}

</cfscript>

For this demo, all of the data is hard-coded; but, in a critical, real-world workflow, I'd have the emails queued-up in the database (or in some sort of message queue). Then, I'd have a task that pulls queued emails and sends them out via CFMail(async=false) one at a time. This way, I only ever mark a given email as "processed" if my control flow gets past the CFMail tag without throwing an error.

What this means, however, is that if there is an error and that error is unrelated to the mail delivery (ex, server crashes before the email gets marked as processed), my idempotent workflow may end up sending the same email more than once. I'm OK with this - for critical workflows, I'd prefer to send an occasional email more than once rather than never sending it at all.

The low-level details of the error handling will likely be different for each scenario. Maybe an automatic retry is fine; or, maybe you need a human to get involved and review the errors before re-queuing an email. In any case, setting the async attribute to false gives you much more control over your email delivery in ColdFusion.

Note on SMTP Delivery vs. HTTP Delivery

Using the CFMail tag is not the only way to deliver email within a ColdFusion application. You might be using a mail-delivery SaaS offering like PostMark, which provides both SMTP and HTTP (API) based consumption. If your ColdFusion application send an email via HTTP, the mechanics change slightly, but the overall concept is the same: you can either allow the email delivery to fail occasionally (for non-critical emails); or, you can queue them and process them using an idempotent workflow.

Want to use code from this post? Check out the license.

Reader Comments

Post A Comment — I'd Love To Hear From You!

Post a Comment

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel