Search code examples
coldfusioncfmailcoldfusion-2018

Where to store automated transactional email templates in ColdFusion


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

Solution

  • 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("[email protected]");
              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,"[email protected]",{"p1":"This is my first replacement.","p2":"This is my second replacement."}) ;
        writeoutput("<br><br>");
        genEmail(2,"[email protected]",{"p1":"This is my third replacement.","p2":"This is my fourth replacement."}) ;
        writeoutput("<br><br>");
        genEmail(3,"[email protected]",{"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,"[email protected]",{"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,"[email protected]",{"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,"[email protected]",{"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.