2

I am in the process of developing an e-commerce application which naturally communiates with users through e-mail regarding transactions such as:

  • User registration
  • Email verification
  • Password resets
  • Order confirmations
  • Despatch confirmations
  • Comment notifications

And so on.

At the moment I am only sending user registration emails so I managed to keep them all in a single component called email.cfc and keep an instance of that in the application scope like such <cfset APPLICATION.EmailSender = New email.cfc() />

email.cfc just has a bunch of methods that sends out different emails like:

<cffunction name="NewUserRegistered">
  <cfmail type="html" to="#useremail#" subject="Welcome" >
    <h1>Thanks for registering #username#!</h1>
  </cfmail>
</cffunction>

<cffunction name="PasswordReset">
  <cfmail type="html" to="#useremail#" subject="Password Reset">
    <h1>Dear #username#, you requested a password reset...</h1>
  </cfmail>
</cffunction>

<cffunction name="OrderConfirmation">
  <cfmail type="html" to="#useremail#" subject="Order Confirmation #orderid#">
    <h1>Your order: #order_id# has been received...</h1>
  </cfmail>
</cffunction>

I have just realised that the amount of different email types is about to shoot up massively and I could end up with about 50 different types of emails that have to go out depending on what type of event is going on. It seems too easy to keep all these email templates in a single CFC in the Application scope which could fill up the server memory or cause some other scalability issue (whatever that might be/mean)

What would be a better way to manage sending automated transactional emails in ColdFusion? Some possible solutions:

  • Continue to keep all emails in one CFC and access them from other CFCs/CFM pages
  • Keep templates within the CFC that needs it such as shoppingcart.cfc that will fire off an Order Confirmation email at the end of a shopping session
volume one
  • 6,800
  • 13
  • 67
  • 146
  • 1
    Have you thought about storing the templates in a database table and then creating a generic send email function that loads up an email based on a tag or keyword? – Seanvm Feb 06 '19 at 21:04
  • @seanvm its a good idea. do you know how a large company like amazon do it? – volume one Feb 06 '19 at 21:33
  • There's no need to worry about memory, unless you have tens of thousands different e-mail functions. Storing mail templates in a plain format (database) has the disadvantage of not being able to add CFML logic into it. I'd stick with what you currently do or use [`cfmodule`](https://helpx.adobe.com/coldfusion/cfml-reference/coldfusion-tags/tags-m-o/cfmodule.html). Either way, the answer to your question is opinion based and thus the question should be closed. – Alex Feb 06 '19 at 23:43
  • 1
    Not sure I agree with the close votes for "Opinion Based". Big-picture, OP is looking for a solution to handle emailing volume. – Shawn Feb 07 '19 at 00:36
  • Why the close votes? This is a question around a solution for handling transactional emails in application architecture. Where else could I post such a question? – volume one Feb 07 '19 at 10:03
  • @volumeone There is a [separate stackexchange site](https://softwareengineering.stackexchange.com/) for general questions about how to structure software and application architecture. This stackexchange is supposed to solve concrete problems. You don't have a concrete/unresolved problem, you are asking for a "better" way to handle sending e-mails. – Alex Feb 07 '19 at 11:18
  • @alex point taken, just that rather than a suggestion it would have been nice to see some code of how it might work like shawn did – volume one Feb 07 '19 at 12:13
  • 1
    Again, I think I'd have to agree with @volumeone here. Software Engineering seems to be for much more general questions, whereas this question may seem to be general, but it's fairly ColdFusion specific. The most recent CF-tagged question over there is 5 years old, and it still asks a fairly general question. I think this one's a much more appropriate and visible question here, and I would hate to discourage other users from asking a question in a place that they'll get valid answers. But that is getting way into Meta-land. – Shawn Feb 07 '19 at 15:57

1 Answers1

0

EDIT: Added example of placeholder replacement.

I'd go with the suggestion of writing out the template in HTML and then saving that HTML into your database. Then you can just create a function that can query your db then populate and send your email. That would be pretty light-weight.

<cfscript>
    // Our mail function.
    public Void function genEmail ( required Numeric templateTypeID, required String userEmail, required Struct placeholder ) {

        // Created fake query of templates.
        emailTemplateQuery = queryNew(
            "templatetypeid,templatesubject,templatetext",
            "integer,varchar,varchar", 
            [ 
                { templatetypeid=1,templatesubject='Welcome',templatetext='<h1>Thanks for registering!</h1><p>[[p1]]</p><p>[[p2]]</p>' },
                { templatetypeid=2,templatesubject='Password Reset',templatetext='<h1>You requested a password reset.</h1><p>[[p1]]</p><p>[[p2]]</p>' },
                { templatetypeid=3,templatesubject='Another Email',templatetext='<h1>You did something.</h1><p>[[p1]]</p><p>[[p2]]</p><p>[[p3]]</p>' }
            ]
        ) ;
        ///////////////////////////////////

        // Build the query.
        local.sql = "SELECT templatesubject, templatetext FROM emailTemplateQuery WHERE templateTypeID = :templateTypeID" ;
        // Which template?
        local.params = { templateTypeID = arguments.templateTypeID };   
        // Query options?
        local.queryoptions = { 
            dbtype="query" 
            // datasource="myDSN" << Use your DSN for final query.
        } ;

        // Create a new query and execute it.
        local.emailQ = QueryExecute(local.sql, local.params, local.queryoptions ) ;

        local.finalEmailString = local.emailQ.templatetext ;

        // Let's inject our placeholder info into our email template
        for ( var p IN arguments.placeholder ) {
            local.finalEmailString = local.finalEmailString.replaceNoCase(
                "[[" & p & "]]" ,
                arguments.placeholder[p] ,
                "All"
            ) ;
        }

        // Create a new mail object.
        local.sendEmail = new mail() ;
        // Save mail body to a variable.
        savecontent variable="emailBody" {
            writeOutput(local.finalEmailString);
        }
          // Set From, To, Type, etc.
          sendEmail.setFrom("fromMe@domain.com");
          sendEmail.setTo(arguments.userEmail);
          sendEmail.setType("html");
          sendEmail.setSubject(local.emailQ.templatesubject);
          // Send the email. So uncomment next line to send.
          //sendEmail.send(body=emailBody); 

        // We don't have to return anything, but for this demo, we want to see what email will be sent.
        writeDump(local.emailQ.templatesubject);
        writeDump(local.finalEmailString);
    }



    // To send an email, just call genEmail() and pass the type and who to.
    genEmail(1,"bill@beexcellent.com",{"p1":"This is my first replacement.","p2":"This is my second replacement."}) ;
    writeoutput("<br><br>");
    genEmail(2,"ted@beexcellent.com",{"p1":"This is my third replacement.","p2":"This is my fourth replacement."}) ;
    writeoutput("<br><br>");
    genEmail(3,"rufus@beexcellent.com",{"p1":"This is my fifth replacement.","p2":"This is my sixth replacement.","p3":"This is my seventh replacement."}) ;
</cfscript>

That can be simplified a bit. The bulk of my code was setting up test data and using a Query Of Query. You'd want to use a regular call to your datasource. You can also use query results more effectively inside the cfmail tag. I would highly recommend doing a lot of filtering and validating before allowing anything to send email from your system. You could also return a status code from your email function to verify success (or other info).

You could save your email process into its own CFC and then cache it to be used throughout your application.

NOTE: I also prefer script to tags for most CF, but my logic above can be converted back to tags if you want to.

https://cffiddle.org/app/file?filepath=639e2956-a658-4676-a0d2-0efca81d7c23/ce5629c9-87e6-4bff-a9b8-86b608e9fc72/c8d38df7-f14d-481f-867d-2f7fbf3238f2.cfm

RESULTS: With my above tests, you get emails with the following HTML.

genEmail(1,"bill@beexcellent.com",{"p1":"This is my first replacement.","p2":"This is my second replacement."}) Welcome

Thanks for registering!

This is my first replacement.

This is my second replacement.

genEmail(2,"ted@beexcellent.com",{"p1":"This is my third replacement.","p2":"This is my fourth replacement."})

Password Reset

You requested a password reset.

This is my third replacement.

This is my fourth replacement.

genEmail(3,"rufus@beexcellent.com",{"p1":"This is my fifth replacement.","p2":"This is my sixth replacement.","p3":"This is my seventh replacement."})

Another Email

You did something.

This is my fifth replacement.

This is my sixth replacement.

This is my seventh replacement.

Shawn
  • 4,758
  • 1
  • 20
  • 29
  • Your example misses placeholder logic to actually make this useful. How do you generate an order e-mail with a static template like this? – Alex Feb 07 '19 at 00:41
  • @Alex If needed, that can be added fairly easily. Per the OP request, the email templates seemed very simple and primarily differed in type. If other details need to be injected into the message, those can be. – Shawn Feb 07 '19 at 00:43
  • 1
    Passing big chunks of placeholders and also taking care of plain text and HTML part can be a pain in the arse, I tell you. Done that, didn't like it, would not recommend it for business/enterprise purposes. But here we are, it's opinion based, I guess. – Alex Feb 07 '19 at 00:52
  • @Alex I would agree, but that's not really what the OP request was. That said, it's still not difficult to add replace functionality to this function, especially if we're working with a set template. We know exactly the pieces too look for that we want to replace and we know they'll be there. – Shawn Feb 07 '19 at 00:56
  • 1
    I added an example of a simple placeholder replacement. – Shawn Feb 07 '19 at 01:43
  • @Shawn this is great. I'm a little worried about what would happen if I needed to edit an email template - I would have to alter the text and variables in the DB and also modify the genEmail() function placeholder replacements and put it all back together. Not too burdensome, but are there any advantages to this over using a CFC like originally? If it was just updating the DB then thats a massive advantage (any admin person could do it) but it looks like I also have to update CF code each time a template changes. A CFC or custom tag is just one place to update right? – volume one Feb 07 '19 at 10:44
  • 1
    You shouldn't have to update CF code if you follow a pattern of placeholder text. I used `[[p#]]` to signify mine. Then you just include the text of the email you want as the template. You would pass in a struct of the text you want to replace - `""p1":"Replacement text."}`. The function would loop through any number of replacement values in the struct and take care of them. And that's only if you needed to pass in replacement values. – Shawn Feb 07 '19 at 15:46
  • And I would still recommend putting this function in its own CFC. – Shawn Feb 07 '19 at 15:47
  • Do you think this solution can also work for localising the text? I'm nowhere near that stage yet, but someone in stackexchange software engineering suggested thinking about it. – volume one Feb 13 '19 at 09:42
  • @volumeone You wouldn't do text replacement for localisation. You'd have a completely separate database row for the localized version of the text. But then this would work exactly the same for that text if you have the localized version of what needs to replace what. – Shawn Feb 13 '19 at 12:14