Click here to Skip to main content
15,891,513 members
Articles / Web Development / ASP.NET

NoSpamEmailHyperlink: 1. Design

Rate me:
Please Sign up or sign in to vote.
4.90/5 (29 votes)
22 Oct 200312 min read 213.5K   3.8K   99  
Fighting back against the e-mail harvesters.
using System;
using System.Drawing;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.Design;
using System.ComponentModel;
using System.Text;
using System.Collections;
using CP.WebControls;

namespace CP.WebControls
{
	/// <summary>
	/// Control to create a mailto: link with an encoded email
	/// address which will pass any email validation routine
	/// </summary>
	[
	DefaultProperty("Email"), 
	ToolboxData("<{0}:NoSpamEmailHyperlink runat=server></{0}:NoSpamEmailHyperlink>"),
	ParseChildren(false),
	ControlBuilder(typeof(NoSpamEmailHyperlinkBuilder)),
	Designer(typeof(NoSpamEmailHyperlinkDesigner)),
	DataBindingHandler(typeof(NoSpamEmailHyperlinkDataBindingHandler))
	]
	public class NoSpamEmailHyperlink : System.Web.UI.WebControls.WebControl
	{
		/// <summary>
		/// Tag is A (hyperlink) rather than the default SPAN
		/// </summary>
		protected override HtmlTextWriterTag TagKey
		{
			get
			{
				return HtmlTextWriterTag.A;
			}
		}

		#region Text property (inner property)
		/// <summary>
		/// Text to display for link
		/// </summary>
		[
		Bindable(true), 
		Category("Data"), 
		DefaultValue(""),
		Description("Display text for hyperlink (may also be encoded)"),
		PersistenceMode(PersistenceMode.EncodedInnerDefaultProperty)
		]
		public virtual string Text 
		{
			get
			{
				return (ViewState["Text"] is string)
					? (string) ViewState["Text"]
					: String.Empty;
			}

			set
			{
				ViewState["Text"] = value;
			}
		}

		/// <summary>
		/// Handle the literal content of the control
		/// </summary>
		/// <param name="obj">Child control</param>
		protected override void AddParsedSubObject(object obj)
		{
			if (obj is LiteralControl)
			{
				// If inner text is a literal, we can
				// pick up the text at any point and
				// populate our Text property
				
				Text = ((LiteralControl)obj).Text;
			}
			else 
			{
				// If inner text is databound, we
				// need to pick up the text at render
				// time, so we'll add the control as
				// a child
				
				base.AddParsedSubObject(obj);
			}

			// NoSpamEmailHyperlinkBuilder.AppendSubBuilder
			// will throw out any other child controls at
			// parse-time, no other condition should exist
		}
		#endregion

		#region Attribute properties
		/// <summary>
		/// Email address to be encoded
		/// </summary>
		[
		Bindable(true), 
		Category("Data"), 
		DefaultValue(""),
		Description("Email address for hyperlink")
		]
		public virtual string Email
		{
			get
			{
				return (ViewState["Email"] is string)
					? (string) ViewState["Email"]
					: String.Empty;
			}

			set
			{
				ViewState["Email"] = value;
			}
		}

		/// <summary>
		/// Seed number for decoding script
		/// </summary>
		[
		Bindable(true), 
		Category("Data"), 
		DefaultValue(23),
		Description("Seed number for decoding script")
		]
		public virtual int ScrambleSeed
		{
			get
			{
				return (ViewState["ScrambleSeed"] is int)
					? (int) ViewState["ScrambleSeed"]
					: 23;
			}

			set
			{
				ViewState["ScrambleSeed"] = value;
			}
		}

		/// <summary>
		/// Set to false to avoid encoding where the email
		/// address is in the inner text
		/// </summary>
		[
		Bindable(true), 
		Category("Data"), 
		DefaultValue(true),
		Description("If the email address appears in the Text property, encode it")
		]
		public virtual bool EncodeInText
		{
			get
			{
				return (ViewState["EncodeInText"] is bool)
					? (bool) ViewState["EncodeInText"]
					: true;
			}

			set
			{
				ViewState["EncodeInText"] = value;
			}
		}

		/// <summary>
		/// Text to replace email address in the Text for Netscape pre-v6
		/// </summary>
		[
		Bindable(true), 
		Category("Data"), 
		DefaultValue("[Hidden]"),
		Description("Text to replace email address in the Text for Netscape pre-v6")
		]
		public virtual string HideText
		{
			get
			{
				return (ViewState["HideText"] is string)
					? (string) ViewState["HideText"]
					: "[Hidden]";
			}

			set
			{
				ViewState["HideText"] = value;
			}
		}
		#endregion

		#region Hidden properties for inheritors
		/// <summary>
		/// A base string for scrambling text
		/// </summary>
		/// <remarks>
		/// This should contain as many alpha-numeric characters
		/// as possible but each character should only appear once.
		/// Avoid using punctuation ('@', '.', '_', '-') which can
		/// make the address invalid.
		/// </remarks>
		[
		DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)
		]
		protected virtual string CodeKey
		{
			get
			{
				return "yJzdeB4CcDnmEFbZtvuHlI1hA8SiLo9MwfN3O6Y5QaRqKTjUpxVk2WgXrP7Gs0";
			}
		}

		/// <summary>
		/// Unique name for registering Link Array
		/// </summary>
		[
		DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)
		]
		protected virtual string LinkArrayName
		{
			get
			{
				return GetType().Name + "_LinkNames";
			}
		}

		/// <summary>
		/// Unique name for registering Seed Array
		/// </summary>
		[
		DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)
		]
		protected virtual string SeedArrayName
		{
			get
			{
				return GetType().Name + "_Seeded";
			}
		}

		/// <summary>
		/// Unique name for registering function script
		/// </summary>
		[
		DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)
		]
		protected virtual string FuncScriptName
		{
			get
			{
				return GetType().Name + "_DecodeScript";
			}
		}

		/// <summary>
		/// Unique name for registering call script
		/// </summary>
		[
		DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)
		]
		protected virtual string CallScriptName
		{
			get
			{
				return GetType().Name + "_DecodeScriptCall";
			}
		}

		/// <summary>
		/// Variable name used for CodeKey string
		/// </summary>
		[
		DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)
		]
		protected virtual string CodeKeyName
		{
			get
			{
				return "ky";
			}
		}

		/// <summary>
		/// If a broswer cannot set the innerHTML, this should identify it
		/// </summary>
		[
		DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)
		]
		protected virtual bool BrowserNeedsHide
		{
			get
			{
				// If the Browser is Netscape (v4.x or less), we cannot change
				// the innerHTML at run time, so we'll just hide it

				HttpBrowserCapabilities bc = Page.Request.Browser;
				Version bv = new Version(bc.Version);

				return (bc.Browser.ToLower().IndexOf("netscape") > -1 
					&& bv.Major < 5);
			}
		}
		#endregion

		#region Script building methods
		/// <summary>
		/// Build the decoder function
		/// </summary>
		protected virtual string GetFuncScript()
		{
#if DEBUG
			// Formatted script text in debug version
			JavaScriptBuilder jsb = new JavaScriptBuilder(true);
#else
			// Compress script text in release version
			JavaScriptBuilder jsb = new JavaScriptBuilder();
#endif

			jsb.AddLine("function ", FuncScriptName, "(link, seed)");
			jsb.OpenBlock(); // function()
			jsb.AddCommentLine("This is the decoding key for all ", LinkArrayName, " objects");
			jsb.AddLine("var ", CodeKeyName, " = \"", CodeKey, "\";");
			jsb.AddLine();

			if (!BrowserNeedsHide)
			{
				jsb.AddCommentLine("Store the innerHTML so that it doesn't get");
				jsb.AddCommentLine("distorted when updating the href later");
				jsb.AddLine("var storeText = link.innerHTML;");
				jsb.AddLine();
			}

			jsb.AddCommentLine("Initialize variables");
			jsb.AddLine("var baseNum = parseInt(seed);");
			jsb.AddLine("var atSym = link.href.indexOf(\"@\");");
			jsb.AddLine("if (atSym == -1) atSym = 0;");
			jsb.AddLine("var dotidx = link.href.indexOf(\".\", atSym);");
			jsb.AddLine("if (dotidx == -1) dotidx = link.href.length;");
			jsb.AddLine("var scramble = link.href.substring(7, dotidx);");
			jsb.AddLine("var unscramble = \"\";");
			jsb.AddLine("var su = true;");
			jsb.AddLine();
			jsb.AddCommentLine("Go through the scrambled section of the address");
			jsb.AddLine("for (i=0; i < scramble.length; i++)");
			jsb.OpenBlock(); // for (i = 0; i < scramble.length; i++)
			jsb.AddCommentLine("Find each character in the scramble key string");
			jsb.AddLine("var ch = scramble.substring(i,i + 1);");
			jsb.AddLine("var idx = ", CodeKeyName, ".indexOf(ch);");
			jsb.AddLine();
			jsb.AddCommentLine("If it isn't there then add the character");
			jsb.AddCommentLine("directly to the unscrambled email address");
			jsb.AddLine("if (idx < 0)");
			jsb.OpenBlock(); // if (idx < 0)
			jsb.AddLine("unscramble = unscramble + ch;");
			jsb.AddLine("continue;");
			jsb.CloseBlock(); // if (idx < 0)
			jsb.AddLine();
			jsb.AddCommentLine("Decode the character");
			jsb.AddLine("idx -= (su ? -baseNum : baseNum);");
			jsb.AddLine("baseNum -= (su ? -i : i);");
			jsb.AddLine("while (idx < 0) idx += ", CodeKeyName, ".length;");
			jsb.AddLine("idx %= ", CodeKeyName, ".length;");
			jsb.AddLine();
			jsb.AddCommentLine("... and add it to the unscrambled email address");
			jsb.AddLine("unscramble = unscramble + ", CodeKeyName, ".substring(idx,idx + 1);");
			jsb.AddLine("su = !su;");
			jsb.CloseBlock(); // for (i = 0; i < scramble.length; i++)
			jsb.AddLine();
			jsb.AddCommentLine("Adjust the href property of the link");
			jsb.AddLine("var emAdd = unscramble + link.href.substring(dotidx, link.href.length + 1);");
			jsb.AddLine("link.href = \"mailto:\" + emAdd;");
			jsb.AddLine();

			if (!BrowserNeedsHide)
			{
				jsb.AddCommentLine("If the scrambled email address is also in the text");
				jsb.AddCommentLine("of the hyperlink, replace it");
				jsb.AddLine("var findEm = storeText.indexOf(scramble);");
				jsb.AddLine("while (findEm > -1)");
				jsb.OpenBlock(); // while (findEm > -1)
				jsb.AddLine("storeText = storeText.substring(0, findEm) + emAdd + storeText.substring(findEm + emAdd.length, storeText.length);");
				jsb.AddLine("findEm = storeText.indexOf(scramble);");
				jsb.CloseBlock(); // while (findEm > -1)
				jsb.AddLine();
				jsb.AddLine("link.innerHTML = storeText;");
			}

			jsb.CloseBlock(); // function()

			return jsb.ToString();
		}

		/// <summary>
		/// Build the call script text
		/// </summary>
		/// <remarks>
		/// Use the CallScriptName property to create variable names,
		/// so that we do not clash with similar (or even completely
		/// different) controls.
		/// </remarks>
		[
		DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)
		]
		protected string GetCallScript()
		{
#if DEBUG
			// Formatted script text in debug version
			JavaScriptBuilder jsb = new JavaScriptBuilder(true);
#else
			// Compress script text in release version
			JavaScriptBuilder jsb = new JavaScriptBuilder();
#endif

			jsb.AddCommentLine("Run through all links in this page");

			if (Page.Request.Browser.Browser.IndexOf("ie") > -1)
			{
				jsb.AddLine("for (", CallScriptName, "_idx = 0; ", CallScriptName,	"_idx < ", LinkArrayName, ".length; ", CallScriptName,	"_idx++)");
				jsb.OpenBlock();
				jsb.AddLine(FuncScriptName, "(document.links.item(", LinkArrayName, "[", CallScriptName, "_idx]), ", SeedArrayName, "[", CallScriptName,"_idx]);");
				jsb.CloseBlock();
			}
			else
			{
				jsb.AddLine("for (", CallScriptName, "_idx = 0; ", CallScriptName, "_idx < document.links.length; ", CallScriptName, "_idx++)");
				jsb.OpenBlock();
				jsb.AddLine("for (", FuncScriptName, "_idx = 0; ", FuncScriptName, "_idx < ", LinkArrayName, ".length; ", FuncScriptName, "_idx++)");
				jsb.OpenBlock();
				jsb.AddLine("if (document.links[", CallScriptName, "_idx].id == ", LinkArrayName, "[", FuncScriptName, "_idx])");
				jsb.OpenBlock();
				jsb.AddLine(FuncScriptName, "(document.links[", CallScriptName, "_idx], ", SeedArrayName, "[", FuncScriptName,"_idx]);");
				jsb.CloseBlock();
				jsb.CloseBlock();
				jsb.CloseBlock();
			}

			return jsb.ToString();
		}
		#endregion

		#region Encoding Algorithm
		protected string Encode (string Unencoded, string ToEncode)
		{
			// Cannot String.Replace a zero-length string
			if (ToEncode.Length == 0) return Unencoded;

			// Encode all occurences of ToEncode in Unencoded
			return Unencoded.Replace(ToEncode, Encode(ToEncode));
		}

		protected virtual string Encode (string Unencoded)
		{
			// Convert string to char[]
			char[] scramble = Email.ToCharArray();

			// Initialize variables
			int baseNum = ScrambleSeed;
			bool subtract = true;

			// Find the @ symbol and the following .
			// if either don't exist then we don't have a
			// valid email address and should return it unencoded
			int atSymbol = Array.IndexOf(scramble, '@');
			if (atSymbol == -1) atSymbol = 0;
			int stopAt = Array.IndexOf(scramble, '.', atSymbol);
			if (stopAt == -1) stopAt = scramble.Length;

			// Go through the section of the address to be scrambled
			for (int i=0; i < stopAt; i++)
			{
				// Find each character in the scramble key string
				char ch = scramble[i];
				int idx = CodeKey.IndexOf(ch);

				// If it isn't there then ignore the character
				if (idx < 0) continue;

				// Encode the character
				idx += (subtract ? -baseNum : baseNum);
				baseNum -= (subtract ? -i : i);
				while (idx < 0) idx += CodeKey.Length;
				idx %= CodeKey.Length;
				scramble[i] = CodeKey[idx];
				subtract = !subtract;
			}

			// Return the encoded string
			return new string(scramble);
		}
		#endregion

		#region Rendering functionality
		/// <summary>
		/// Add the href atribute to the main tag
		/// </summary>
		/// <param name="writer">The HtmlTextWriter for the page</param>
		protected override void AddAttributesToRender(HtmlTextWriter writer)
		{
			if (Email.Length > 0)
			{
				writer.AddAttribute(HtmlTextWriterAttribute.Href, "mailto:" + Encode(Email));
			}

			base.AddAttributesToRender (writer);
		}

		/// <summary>
		/// Declare arrays and scripts before rendering
		/// </summary>
		/// <param name="e">EventArgs</param>
		protected override void OnPreRender(EventArgs e)
		{
			base.OnPreRender (e);

			if (Email.Length > 0)
			{
				// Register the Control's ID and Decode seed in scripted arrays
				Page.RegisterArrayDeclaration(
					LinkArrayName, String.Format("\"{0}\"", ClientID)
					);
				Page.RegisterArrayDeclaration(
					SeedArrayName, String.Format("\"{0}\"", ScrambleSeed)
					);

				// Register the decoder function script block
				if (!Page.IsClientScriptBlockRegistered(FuncScriptName))
					Page.RegisterClientScriptBlock(FuncScriptName, GetFuncScript());

				// Register the calling script block
				if (!Page.IsStartupScriptRegistered(CallScriptName))
					Page.RegisterStartupScript(CallScriptName, GetCallScript());
			}
		}

		/// <summary> 
		/// Render this control to the output parameter specified.
		/// </summary>
		/// <param name="output">The HtmlTextWriter for the page</param>
		protected override void Render(HtmlTextWriter output)
		{
			// If we don't have anything to display don't even render
			// the start and end blocks.  This assures that the page shows
			// nothing but the VS.NET designer has a selectable label
			if (Email.Length == 0 && Text.Length == 0 && Controls.Count == 0)
				return;

			base.Render(output);
		}

		protected override void RenderContents(HtmlTextWriter writer)
		{
			// The only controls that should be left at this
			// point should be DataBoundLiteralControls.  In theory,
			// if there are any then there should be exactly one 
			// and nothing else.  But we'll leave in a bit of
			// flexibility, in case the framework changes later.
			string displayText = null;

			if (Controls.Count > 0 && Controls[0] is DataBoundLiteralControl)
			{
				displayText = ((DataBoundLiteralControl)Controls[0]).Text;
			}
			else
			{
				// If there is some text, use it.  If not then display the
				// encoded email address or the hyperlink will not be
				// visible
				displayText = (Text.Length == 0)
					? Email
					: Text;
			}

			// If the EncodeInText flag is set and the email address
			// is somewhere in the Text, encode it.
			if (EncodeInText && Email.Length > 0)
			{
				int idx = displayText.IndexOf(Email);

				if (idx > -1) displayText = BrowserNeedsHide ? HideText : Encode(displayText, Email);
			}

			// Html encode any text that remains.
			writer.Write(HttpUtility.HtmlEncode(displayText));
		}
		#endregion
	}
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
United Kingdom United Kingdom
Paul lives in the heart of En a backwater village in the middle of England. Since writing his first Hello World on an Oric 1 in 1980, Paul has become a programming addict, got married and lost most of his hair (these events may or may not be related in any number of ways).

Since writing the above, Paul got divorced and moved to London. His hair never grew back.

Paul's ambition in life is to be the scary old guy whose house kids dare not approach except at halloween.

Comments and Discussions