Welcome to the final entry in the "Mailing List" series. To be honest, all the previous entries covered what you absolutely need. This entry isn't exactly required, but comes highly recommended. Why? The previous code let anyone subscribe an email address. This is nice and simple, but it also means that I could enter your email address and you would never know it until you began to get email from my site. I did build a simple way to unsubscribe, but that isn't good enough. What I really need to add is a simple verification service. This is how I will do that:
- On subscribing, send an email to the person. The email will contain a link that must be clicked to finish the subscription.
- When sending email to the subscribers, only send to those that actually clicked the link.
Nice and simple, right? Let's start with how the application will know that a person is verified. The first thing I'm going to do is add a simple verified column to my database table. Now that this is done, let's modify the CFC's subscribe method to properly set the verified value to false. I'm not going to paste the entire method, but just the modified query:
<cfquery datasource="#variables.dsn#">
insert into subscribers(email,token,verified)
values(<cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.email#">,
<cfqueryparam cfsqltype="cf_sql_varchar" value="#createUUID()#">,
<cfqueryparam cfsqltype="cf_sql_bit" value="0">
)
</cfquery>
Next I need to send an email to the potential subscriber. I've added a new method that will fetch one subscriber from the mailing list:
<cffunction name="getSubscriber" returnType="struct" output="false" access="public"
hint="Returns a subscrier.">
<cfargument name="email" type="string" required="true">
<cfset var q = "">
<cfset var s = structNew()>
<cfset var col = "">
<cfquery name="q" datasource="#variables.dsn#">
select email, token, verified
from subscribers
where email = <cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.email#">
order by email asc
</cfquery>
<cfloop index="col" list="#q.columnlist#">
<cfset s[col] = q[col]>
</cfloop>
<cfreturn s>
</cffunction>
There isn't anything particularly interesting about this method, but you may want to pay attention to how I translate the query to a structure. I use an automatic method instead of setting each column manually. I've modified the subscribe file to handle this new logic. Let's take a look at the whole thing, and I'll then discuss what specifically changed since the first version.
<cfparam name="form.emailaddress" default="">
<cfset showForm = true>
<cfif structKeyExists(form, "subscribe")>
<cfif isValid("email", form.emailAddress)>
<cfif application.maillist.subscribe(form.emailaddress)>
<cfset token = application.maillist.getsubscriber(form.emailaddress).token>
<cfmail to="#form.emailaddress#" from="#application.maillistfrom#" subject="Mail List Verification">
You have requested to join our mailing list. To verify this subscription, please click the link below:
http://192.168.1.113/testingzone/mailinglist/verify.cfm?token=#token#
If you did not want to subcribe, please ignore this email.
</cfmail>
</cfif>
<cfset showForm = false>
<cfelse>
<cfset error = "Your email address isn't valid.">
</cfif>
</cfif>
<h2>Subscribe to Foo</h2>
<cfif showForm>
<cfif structKeyExists(variables, "error")>
<cfoutput>
<p>
<b>#error#</b>
</p>
</cfoutput>
</cfif>
<p>
<form action="subscribe.cfm" method="post">
<table>
<tr>
<td>Your Email Address</td>
<cfoutput><td><input type="text" name="emailaddress" value="#form.emailaddress#"></td></cfoutput>
</tr>
<tr>
<td> </td>
<td><input type="submit" name="subscribe" value="Subscribe"></td>
</tr>
</table>
</form>
</p>
<cfelse>
<p>
Thank you for subscribing! A verification email has been sent to your address.
</p>
</cfif>
The first thing you may notice different is that once I've subscribed a person, I check the result of that method. I didn't use to do that before. The method will return true if the email address did not exist in the list. I need to know this otherwise I'll be sending a verification email more than once (potentially). Next I call the new method, getSubscriber. This returns a structure of member information, but all I need is the token. Notice the shorthand method of grabbing the value:
<cfset token = application.maillist.getsubscriber(form.emailaddress).token>
I could have written that in two lines, but this is simpler. The last thing I do is send an email to the subscriber. Notice how I include his token in the email. This will be used to verify the user. I don't need to include his email address since the token (as a UUID) is unique enough. I made one more change as well. At the very end of the file where I had previously told the user that he was subscribed, I added a note saying that I have sent a verification email.
Now I need to build the file that will actually handle the verification:
<!--- Must have url.t --->
<cfif structKeyExists(url, "token") and isValid("uuid", url.token)>
<cfset application.maillist.verify(url.token)>
<cfoutput>
Your subscription has been verified. Thank you and have a nice day.
</cfoutput>
<cfelse>
<cfoutput>
You have <b>not</b> been verified. Please ensure that you have copied the URL correctly from your mail program.
</cfoutput>
</cfif>
This is a short file. All it does is check for the url variable "token" and if it both exists and is a valid UUID, I call the verify method of the CFC. Now let's take a look at that method:
<cffunction name="verify" returnType="void" output="false" access="public"
hint="Verifies a user.">
<cfargument name="token" type="uuid" required="true">
<cfquery datasource="#variables.dsn#">
update subscribers
set verified = 1
where token = <cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.token#">
</cfquery>
</cffunction>
All I'm doing here is setting the verified flag to true where the token matches the value passed in. Ok - so I'm almost there. The page I built for the admin, sendmail.cfm, was using getSubscribers to both get a count of how many people to mail, as well as knowing who to mail to. I've modified this template to now call getVerifiedSubscribers. This method is also rather simple as you can imagine:
<cffunction name="getVerifiedSubscribers" returnType="query" output="false" access="public"
hint="Returns a query of everyone subscribed.">
<cfset var q = "">
<cfquery name="q" datasource="#variables.dsn#">
select email, token, verified
from subscribers
where verified = 1
order by email asc
</cfquery>
<cfreturn q>
</cffunction>
That's it. I hope you enjoyed this series and sorry the last part took so long to get out. A note about the files - I used the Make Archive funtion on my Mac, and it made a zip, but I have no idea if it will play well with PCs. So let me know. I also didn't modify the DB script since the change was simple and I'm on the Mac using MySQL. I didn't want folks to get confused as to why the format changed so radically. If folks really need it I can post it later.
Archived Comments
Hi Ray, Is there supposed to be a ZIP file with this entry?
Cheers,
Michael
Can you say oops. Going to go attach now.
Fixed, thank you.
You might want to change the verify element to something like HASH(email). This is because some spam filters will block any incoming email that includes the recipients address in a url. I ran into this problem when I first started accepting newsletter subscriptions.
Also it is a good practice to include a time-stamp in the database along with the verified = true field. That way you can setup a CF task that will send out reminders to your readers after... say a week, month, and so on.
Thank you for this series. I've been developing for years and miss all the new features because I've used the same CRUD methods for eternity. It's nice to look up from the code once and awhile and see how CF has come so far when utilized properly and creatively. Your practical examples, explained practically, is the best way to learn.
How do you pass new variables into the subscribe method? I am trying to add a first and last name fields. I thought I could add them to the form and then use the CFARGUMENT tag with the same name, but I get errors saying that firstName was not passed in.
Any help would be greatly appreciated.
Steve
Well you modify the call on the public side, where you have subscribe(form var, form var, etc) and you modify it on the CFC side. Also remember that I think I cached the CFC in the Application scope, so you might have to refresh that.
I'm pretty new at CFC's and methods. Where on the public side do I call the subscribe(form var, form var, etc)? I think I have everything on the CFC side, but I still don't quite understand how to pass the form vars into the CFC.
Steve
It is in the same page that has the actual subscribe form. Check there.
I got it to work. The subscribe(form.var, etc) was in a <cfif> tag and I figured I didn't need to add my new form vars to that specific tag.
Thanks again for the great tutorial, help, and intro to CFC's!
Steve
Glad you got it. Sorry if I was a bit short - I'm on vacation till tomorrow.
Not a problem. Even Jedi need rest.
Steve
I have been trying to load the DB into MYSQL thru the MYSQL administrator and it is not working. When i load the file in it says it has 0 tables. Can someone help me out? I am running MYSQL on mc os x
Hey Ray, last question (hopefully). Do you know if this mailing list application is capable of sending html newsletters?
thanks,
Carlos
No problem on the questions - just remember my wish list. ;)
If you set the cfmail tag to use type=html, it should work just fine.
Carlos, I don't see why it can't. The cfmail tag specifies text or HTML email so set it to HTM.
First off, Thank You for a great tutorial. I've been working with CFML for three years now (though I am far from being an expert, let alone deemed proficient) and with the aid of your maillist tutorial, I'm ready to add a mailing list feature to my rare book web site. However, instead of using the mySQL, I want to try to run your tutorial using an MS Access Database on my web server (in place of the mySQL). Here’s what I’ve got so far: Your files are loaded on my web server, but every time I run subscribe.cfm, I get this error: Invalid parser construct found on line 7 at position 72. ColdFusion was looking at the following text:
.
Invalid expression format. The usual cause is an error in the expression structure.
The last successfully parsed CFML construct was a CFSET tag occupying document position (7:4) to (7:9).
The code at line 7 is:
“<cfset token =application.maillist.getsubscriber(form.emailaddress).token>”
Now I’m unclear about this but guessing that the problem above might be with the term “maillist” and that I don’t have a DSN called maillist. But correct me, and guide me if I’m wrong.
I have created a table in my Access DB called "Subscribers" and have three columns, ID, email, token. But I'm unclear as to where I would change the DSN name from your calling it "maillist" to my actual DSN that points to the Access DB. Do I have to do a find and replace throughout all the files you provided or only in certain parts? And furthermore, would replacing “maillist” with my actual DSN that points to my Access database actually make the whole thing work, or am I just spinning my wheels here? Your counsel will greatly be appreciated and I've checked out your wishlist. I don't sell any of those books on your list, but you never know when I find stuff... or buy stuff. Thanks.
No, it isn't a DSN issue. It doesn't like the syntax. What version of CF are you running?
Hi, Ray well how about to use the web schedular with the mailing list, like wanna send the mails to 5000 people so it should send 200 mails every one hour using the schedular, how this can be used?
have you have some idea in this?
You could do that. You would need to record, in the db, a status that records that last # mailed. That way you don't lose track and remail people.
So how *would* one change the DSN name? I see the question asked more than once in the comments amongst the 5 entries of this series, but no answer.
I'm an ex-programmer, started with CF in 2001 and I don't know anything about CFCs, which are new since I got out of the biz. I do have one little CF website that I still maintain pro bono, and now they desperately need an emailer... so I'm torn between my desire for code that's smart/reusable, yet not toooo time-consuming to implement. I don't mean to be lazy, but I'm a busy girl these days and don't have the bandwidth to learn CFC theory! Your code looks really well thought out but being unable to get past DSN name errors is making me feel like a rank n00b all over again... I'd be grateful for any help.
M - the DSN value is set when the CFCs are created. If you go back to step 1 (http://www.coldfusionjedi.c..., you will see the init function of the CFC takes a DSN argument. So if you define a DSN using an application variable, you can pass it in then.
I know you don't have a lot of time, but it wouldn't take you more than an hour or so to read the basic docs on CFCs and get acquainted with them.
Great mailing list article! One question: In terms of the backend view - will the lists/s of subscribers be (eventually) really long pages or do you have next and last paging in there. While I can build this sort of feature into my apps - I think I would struggle with the CFC approach.
Many thanks Jedi
Well your CFC method could simply take arguments to support pagination. Or you could do it in the view only. Doing it in the view isn't as DB efficient, but could be simpler.
Ray, I really enjoyed your series and am learning quite a bit. The app ran fine after the first lesson, but starting with the second lesson, I get this error
Message The method getMembers was not found in component foodphilosopher.assets.docs.expendable.mailinglist.maillist.
Yet, the maillist.cfc is in the directory and it does have the getMembers function. The Subscribe page works fine. Where would I look for a solution?
Ray, I just restarted the CF service. I noticed that the error was referencing the wrong path, an older one. So the restart did the trick. I'm now in the listMembers.cfm page. Thank you!
Glad you got it Sean. One word of warning. This series is VERY old now. It was written for ModelGlue 1. While it still works, note that MG3 is a significant update. If I were to write this series today there would be many changes.
Oooh, is that a hint that you might ;) - Id love it if you did!!