Creating ‘mailto’ And ‘tel’ Link Handler

Overview

 
Many business websites show their email addresses and phone numbers so their customers can contact them. In this lesson we will create wrapper classes around ‘mailto’ and ‘tel’ HTML links. Those classes will allow you to read and generate those links with ease.
 

Introduction

 
If you need an introduction to mailto and tel links please check this and this.
 

Base

 
To avoid duplication, we will start by laying out our base class.
  1. public abstract class WebLink {  
  2.   /// <summary>  
  3.   /// Link prefix. Examples are: 'mailto:' and 'tel:'  
  4.   /// </summary>  
  5.   public abstract string Prefix { get; }  
  6.   
  7.   /// <summary>  
  8.   /// Clears instance fields.  
  9.   /// </summary>  
  10.   public abstract void ClearFields();  
  11.   
  12.   /// <summary>  
  13.   /// Loads link input into relevant fields.  
  14.   /// </summary>  
  15.   public virtual void ReadLink(string link) {  
  16.     if (link == null)  
  17.       throw new ArgumentNullException("link");  
  18.   
  19.     if (link.ToLower().StartsWith(Prefix.ToLower()) == false)  
  20.       throw new FormatException("Invalid link.");  
  21.   }  
  22.   
  23.   /// <summary>  
  24.   /// Generates link from instance fields.  
  25.   /// </summary>  
  26.   public virtual string GenerateLink(bool includePrefix) {  
  27.     var str = string.Empty;  
  28.   
  29.     if (includePrefix)  
  30.       str += Prefix;  
  31.   
  32.     return str;  
  33.   }  
  34.   
  35.   /// <summary>  
  36.   /// Can be used to exclude prefix from a link string.  
  37.   /// </summary>  
  38.   protected string ExcludePrefix(string link) {  
  39.     link = link.Trim();  
  40.     if (link.ToLower().StartsWith(Prefix.ToLower()))  
  41.       link = link.Substring(Prefix.Length).Trim();  
  42.     return link;  
  43.   }  
  44.   
  45.   public override string ToString() {  
  46.     return GenerateLink(true);  
  47.   }  
  48. }  
The code is self-explanatory. Every child class will have to fill its link prefix, add some code to clear its fields, and some other code to read and write links.
 

mailto

 
Now we are going to inherit from the base class to create the mailto handler,
  1. public class MailWebLink : WebLink {  
  2.   #region Prefix  
  3.   protected static string LinkPrefix { get { return "mailto:"; } }  
  4.   public override string Prefix => LinkPrefix;  
  5.   #endregion  
  6.  
  7.   #region Delimiters  
  8.   protected static readonly char[] MailDelimiters = new char[] { '?' };  
  9.   protected static readonly char[] RecipientDelimiters = new char[] { ','';' };  
  10.   protected static readonly char[] ParamDelimiters = new char[] { '&' };  
  11.   protected static readonly char[] ParamValueDelimiters = new char[] { '=' };  
  12.   #endregion  
  13.  
  14.   #region Field Names  
  15.   protected static readonly string ToField = "to";  
  16.   protected static readonly string CcField = "cc";  
  17.   protected static readonly string BccField = "bcc";  
  18.   protected static readonly string SubjectField = "subject";  
  19.   protected static readonly string BodyField = "body";  
  20.   #endregion  
  21.  
  22.  
  23.   #region Fields  
  24.   public string[] To { getset; }  
  25.   public string[] Cc { getset; }  
  26.   public string[] Bcc { getset; }  
  27.   public string Subject { getset; }  
  28.   public string Body { getset; }  
  29.   #endregion  
  30.   
  31.   public MailWebLink() {  
  32.   
  33.   }  
  34.   public MailWebLink(string link) {  
  35.     ReadLink(link);  
  36.   }  
  37.   
  38.   public static bool CanHandle(string link) {  
  39.     return link.ToLower().Trim().StartsWith(LinkPrefix);  
  40.   }  
  41.  
  42.   #region Link Loading  
  43.   public override void ClearFields() {  
  44.     To = Cc = Bcc = null;  
  45.     Subject = Body = null;  
  46.   }  
  47.   
  48.   public override void ReadLink(string link) {  
  49.     base.ReadLink(link);  
  50.   
  51.     try {  
  52.       ClearFields();  
  53.   
  54.       // Exclude prefix if necessary  
  55.       link = ExcludePrefix(link);  
  56.   
  57.       // Get mail 'To' Field  
  58.       string tmpVal = null;  
  59.       int idx = -1;  
  60.   
  61.       idx = link.IndexOfAny(MailDelimiters);  
  62.   
  63.       if (idx > -1)  
  64.         tmpVal = link.Substring(0, idx);  
  65.       else  
  66.         tmpVal = link;  
  67.   
  68.       this.To = LoadRecipients(tmpVal).ToArray();  
  69.   
  70.       if (idx == -1)  
  71.         return;  
  72.   
  73.       link = link.Substring(idx + 1);  
  74.   
  75.       // Handle rest of fields  
  76.       var parameters = GetParameters(link, true);  
  77.       foreach (var par in parameters) {  
  78.         if (par.Key == ToField) // overrides the above code  
  79.           this.To = LoadRecipients(par.Value).ToArray();  
  80.         else if (par.Key == CcField)  
  81.           this.Cc = LoadRecipients(par.Value).ToArray();  
  82.         else if (par.Key == BccField)  
  83.           this.Bcc = LoadRecipients(par.Value).ToArray();  
  84.         else if (par.Key == SubjectField)  
  85.           this.Subject = par.Value;  
  86.         else if (par.Key == BodyField)  
  87.           this.Body = par.Value;  
  88.       }  
  89.     } catch {  
  90.       throw new FormatException();  
  91.     }  
  92.   }  
  93.   
  94.   /// <summary>  
  95.   /// Splits a mail string into a list of mail addresses.  
  96.   /// </summary>  
  97.   protected virtual IEnumerable<string> LoadRecipients(string val) {  
  98.     var items = val.Split(RecipientDelimiters, StringSplitOptions.RemoveEmptyEntries);  
  99.     return items.Select(s => s.Trim().ToLower()).Distinct();  
  100.   }  
  101.   
  102.   /// <summary>  
  103.   /// Splits a parameter string into a list of parameters (kay and value)  
  104.   /// </summary>  
  105.   /// <param name="skipEmpty">Whether to skip empty parameters.</param>  
  106.   protected virtual IEnumerable<KeyValuePair<stringstring>> GetParameters(string val, bool skipEmpty = true) {  
  107.     var items = val.Split(ParamDelimiters, StringSplitOptions.RemoveEmptyEntries);  
  108.   
  109.     foreach (var itm in items) {  
  110.       string key = string.Empty;  
  111.       string value = string.Empty;  
  112.   
  113.       var delimiterIdx = itm.IndexOfAny(ParamValueDelimiters);  
  114.       if (delimiterIdx == -1)  
  115.         continue;  
  116.   
  117.       key = itm.Substring(0, delimiterIdx).ToLower();  
  118.       value = itm.Substring(delimiterIdx + 1);  
  119.       value = UnscapeParamValue(value);  
  120.   
  121.       if (key.Length == 0)  
  122.         continue;  
  123.   
  124.       if (skipEmpty && value.Length == 0)  
  125.         continue;  
  126.   
  127.       yield return new KeyValuePair<stringstring>(key, value);  
  128.     }  
  129.   }  
  130.   #endregion  
  131.  
  132.   #region Link Generation  
  133.   
  134.   public virtual string GetLink() { return GenerateLink(true); }  
  135.   
  136.   public override string GenerateLink(bool includePrefix) {  
  137.     string str = base.GenerateLink(includePrefix);  
  138.   
  139.     if (this.To != null && this.To.Length > 0) {  
  140.       str += GetRecipientString(this.To);  
  141.     }  
  142.   
  143.     str += MailDelimiters.First();  
  144.   
  145.     if (this.Cc != null && this.Cc.Length > 0) {  
  146.       str += GetParameterString(CcField, GetRecipientString(this.Cc), false);  
  147.       str += ParamDelimiters.First();  
  148.     }  
  149.   
  150.     if (this.Bcc != null && this.Bcc.Length > 0) {  
  151.       str += GetParameterString(BccField, GetRecipientString(this.Bcc), false);  
  152.       str += ParamDelimiters.First();  
  153.     }  
  154.   
  155.     if (this.Subject != null && this.Subject.Length > 0) {  
  156.       str += GetParameterString(SubjectField, this.Subject, true);  
  157.       str += ParamDelimiters.First();  
  158.     }  
  159.   
  160.     if (this.Body != null && this.Body.Length > 0) {  
  161.       str += GetParameterString(BodyField, this.Body, true);  
  162.       str += ParamDelimiters.First();  
  163.     }  
  164.   
  165.     str = str.TrimEnd(MailDelimiters.Concat(ParamDelimiters).ToArray());  
  166.   
  167.     return str;  
  168.   }  
  169.   
  170.   /// <summary>  
  171.   /// Joins a list of mail addresses into a string  
  172.   /// </summary>  
  173.   protected virtual string GetRecipientString(string[] recipients) {  
  174.     return string.Join(RecipientDelimiters.First().ToString(), recipients);  
  175.   }  
  176.   
  177.   /// <summary>  
  178.   /// Joins a parameter (key and value) into a string  
  179.   /// </summary>  
  180.   /// <param name="escapeValue">Whether to escape value.</param>  
  181.   protected virtual string GetParameterString(string key, string value, bool escapeValue) {  
  182.     return string.Format("{0}{1}{2}",  
  183.       key,  
  184.       ParamValueDelimiters.First(),  
  185.       escapeValue ? EscapeParamValue(value) : value);  
  186.   }  
  187.  
  188.   #endregion  
  189.  
  190.   #region Helpers  
  191.   protected static readonly Dictionary<stringstring> CustomUnescapeCharacters =  
  192.     new Dictionary<stringstring>() { { "+"" " } };  
  193.   
  194.   private static string EscapeParamValue(string value) {  
  195.     return Uri.EscapeDataString(value);  
  196.   }  
  197.   
  198.   private static string UnscapeParamValue(string value) {  
  199.     foreach (var customChar in CustomUnescapeCharacters) {  
  200.       if (value.Contains(customChar.Key))  
  201.         value = value.Replace(customChar.Key, customChar.Value);  
  202.     }  
  203.   
  204.     return Uri.UnescapeDataString(value);  
  205.   }  
  206.   #endregion  
  207. }  
The code is fairly simple. One thing to note is that Uri.UnescapeDataString cannot convert ‘+’ to a space. That’s why we added a dictionary of custom un-escape characters.
Now here’s a list of input to test back and forth:
  1. mailto:[email protected]  
  2. mailto:[email protected]?subject=Important!&body=Hi.  
  3. mailto:[email protected][email protected],[email protected],[email protected]&[email protected]  
  4. mailto:[email protected][email protected]&[email protected]&subject=The%20subject%20of%20the%20email&body=The%20body%20of%20the%20email  

tel

 
The tel handler is more straightforward than mailto,
  1. public class TelephoneWebLink : WebLink {  
  2.   #region Prefix  
  3.   protected static string LinkPrefix { get { return "tel:"; } }  
  4.   public override string Prefix => LinkPrefix;  
  5.   #endregion  
  6.  
  7.   #region Delimiters  
  8.   protected static readonly char ExtensionDelimiter = 'p';  
  9.   #endregion  
  10.  
  11.   #region Fields  
  12.   public string Number { getset; }  
  13.   public string Extension { getset; }  
  14.   #endregion  
  15.   
  16.   
  17.   public TelephoneWebLink() {  
  18.   
  19.   }  
  20.   public TelephoneWebLink(string link) {  
  21.     ReadLink(link);  
  22.   }  
  23.   
  24.   public static bool CanHandle(string link) {  
  25.     return link.ToLower().Trim().StartsWith(LinkPrefix);  
  26.   }  
  27.   
  28.   public override void ClearFields() {  
  29.     Number = null;  
  30.     Extension = null;  
  31.   }  
  32.   
  33.   public override void ReadLink(string link) {  
  34.     base.ReadLink(link);  
  35.   
  36.     try {  
  37.       ClearFields();  
  38.   
  39.       // Exclude prefix if necessary  
  40.       link = ExcludePrefix(link).Trim();  
  41.   
  42.       Number = string.Empty;  
  43.       Extension = string.Empty;  
  44.   
  45.       bool foundExtension = false;  
  46.       int idx = 0;  
  47.       foreach (var c in link) {  
  48.         if (idx == 0 && c == '+')  
  49.           Number += "+";  
  50.         if (c == ExtensionDelimiter)  
  51.           foundExtension = true;  
  52.         else if (char.IsDigit(c)) {  
  53.           if (foundExtension == false)  
  54.             Number += c.ToString();  
  55.           else  
  56.             Extension += c.ToString();  
  57.         }  
  58.         idx++;  
  59.       }  
  60.   
  61.     } catch {  
  62.       throw new FormatException();  
  63.     }  
  64.   }  
  65.   
  66.   public override string GenerateLink(bool includePrefix) {  
  67.     var str = base.GenerateLink(includePrefix);  
  68.   
  69.     if (Number != null)  
  70.       str += Number.ToString();  
  71.   
  72.     if (Extension != null && Extension.Length > 0)  
  73.       str += ExtensionDelimiter.ToString() + Extension;  
  74.   
  75.     return str;  
  76.   }  
  77. }  
And here’s a list to test:
  1. tel:+20123456789  
  2. tel:+20123456789p113  

What’s next?

 
You can use the same mechanism for any other special link. Adrian Ber wrote a very useful blog post about those links. Moreover, in a future post we will see how this mechanism can be integrated into Android WebView to allow your application to respond to mailto and tel links.
 
Many refactoring patterns and fixes can be applied to the code. Please feel free to comment with your updated code.