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.
- public abstract class WebLink {
-
-
-
- public abstract string Prefix { get; }
-
-
-
-
- public abstract void ClearFields();
-
-
-
-
- public virtual void ReadLink(string link) {
- if (link == null)
- throw new ArgumentNullException("link");
-
- if (link.ToLower().StartsWith(Prefix.ToLower()) == false)
- throw new FormatException("Invalid link.");
- }
-
-
-
-
- public virtual string GenerateLink(bool includePrefix) {
- var str = string.Empty;
-
- if (includePrefix)
- str += Prefix;
-
- return str;
- }
-
-
-
-
- protected string ExcludePrefix(string link) {
- link = link.Trim();
- if (link.ToLower().StartsWith(Prefix.ToLower()))
- link = link.Substring(Prefix.Length).Trim();
- return link;
- }
-
- public override string ToString() {
- return GenerateLink(true);
- }
- }
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,
- public class MailWebLink : WebLink {
- #region Prefix
- protected static string LinkPrefix { get { return "mailto:"; } }
- public override string Prefix => LinkPrefix;
- #endregion
-
- #region Delimiters
- protected static readonly char[] MailDelimiters = new char[] { '?' };
- protected static readonly char[] RecipientDelimiters = new char[] { ',', ';' };
- protected static readonly char[] ParamDelimiters = new char[] { '&' };
- protected static readonly char[] ParamValueDelimiters = new char[] { '=' };
- #endregion
-
- #region Field Names
- protected static readonly string ToField = "to";
- protected static readonly string CcField = "cc";
- protected static readonly string BccField = "bcc";
- protected static readonly string SubjectField = "subject";
- protected static readonly string BodyField = "body";
- #endregion
-
-
- #region Fields
- public string[] To { get; set; }
- public string[] Cc { get; set; }
- public string[] Bcc { get; set; }
- public string Subject { get; set; }
- public string Body { get; set; }
- #endregion
-
- public MailWebLink() {
-
- }
- public MailWebLink(string link) {
- ReadLink(link);
- }
-
- public static bool CanHandle(string link) {
- return link.ToLower().Trim().StartsWith(LinkPrefix);
- }
-
- #region Link Loading
- public override void ClearFields() {
- To = Cc = Bcc = null;
- Subject = Body = null;
- }
-
- public override void ReadLink(string link) {
- base.ReadLink(link);
-
- try {
- ClearFields();
-
-
- link = ExcludePrefix(link);
-
-
- string tmpVal = null;
- int idx = -1;
-
- idx = link.IndexOfAny(MailDelimiters);
-
- if (idx > -1)
- tmpVal = link.Substring(0, idx);
- else
- tmpVal = link;
-
- this.To = LoadRecipients(tmpVal).ToArray();
-
- if (idx == -1)
- return;
-
- link = link.Substring(idx + 1);
-
-
- var parameters = GetParameters(link, true);
- foreach (var par in parameters) {
- if (par.Key == ToField)
- this.To = LoadRecipients(par.Value).ToArray();
- else if (par.Key == CcField)
- this.Cc = LoadRecipients(par.Value).ToArray();
- else if (par.Key == BccField)
- this.Bcc = LoadRecipients(par.Value).ToArray();
- else if (par.Key == SubjectField)
- this.Subject = par.Value;
- else if (par.Key == BodyField)
- this.Body = par.Value;
- }
- } catch {
- throw new FormatException();
- }
- }
-
-
-
-
- protected virtual IEnumerable<string> LoadRecipients(string val) {
- var items = val.Split(RecipientDelimiters, StringSplitOptions.RemoveEmptyEntries);
- return items.Select(s => s.Trim().ToLower()).Distinct();
- }
-
-
-
-
-
- protected virtual IEnumerable<KeyValuePair<string, string>> GetParameters(string val, bool skipEmpty = true) {
- var items = val.Split(ParamDelimiters, StringSplitOptions.RemoveEmptyEntries);
-
- foreach (var itm in items) {
- string key = string.Empty;
- string value = string.Empty;
-
- var delimiterIdx = itm.IndexOfAny(ParamValueDelimiters);
- if (delimiterIdx == -1)
- continue;
-
- key = itm.Substring(0, delimiterIdx).ToLower();
- value = itm.Substring(delimiterIdx + 1);
- value = UnscapeParamValue(value);
-
- if (key.Length == 0)
- continue;
-
- if (skipEmpty && value.Length == 0)
- continue;
-
- yield return new KeyValuePair<string, string>(key, value);
- }
- }
- #endregion
-
- #region Link Generation
-
- public virtual string GetLink() { return GenerateLink(true); }
-
- public override string GenerateLink(bool includePrefix) {
- string str = base.GenerateLink(includePrefix);
-
- if (this.To != null && this.To.Length > 0) {
- str += GetRecipientString(this.To);
- }
-
- str += MailDelimiters.First();
-
- if (this.Cc != null && this.Cc.Length > 0) {
- str += GetParameterString(CcField, GetRecipientString(this.Cc), false);
- str += ParamDelimiters.First();
- }
-
- if (this.Bcc != null && this.Bcc.Length > 0) {
- str += GetParameterString(BccField, GetRecipientString(this.Bcc), false);
- str += ParamDelimiters.First();
- }
-
- if (this.Subject != null && this.Subject.Length > 0) {
- str += GetParameterString(SubjectField, this.Subject, true);
- str += ParamDelimiters.First();
- }
-
- if (this.Body != null && this.Body.Length > 0) {
- str += GetParameterString(BodyField, this.Body, true);
- str += ParamDelimiters.First();
- }
-
- str = str.TrimEnd(MailDelimiters.Concat(ParamDelimiters).ToArray());
-
- return str;
- }
-
-
-
-
- protected virtual string GetRecipientString(string[] recipients) {
- return string.Join(RecipientDelimiters.First().ToString(), recipients);
- }
-
-
-
-
-
- protected virtual string GetParameterString(string key, string value, bool escapeValue) {
- return string.Format("{0}{1}{2}",
- key,
- ParamValueDelimiters.First(),
- escapeValue ? EscapeParamValue(value) : value);
- }
-
- #endregion
-
- #region Helpers
- protected static readonly Dictionary<string, string> CustomUnescapeCharacters =
- new Dictionary<string, string>() { { "+", " " } };
-
- private static string EscapeParamValue(string value) {
- return Uri.EscapeDataString(value);
- }
-
- private static string UnscapeParamValue(string value) {
- foreach (var customChar in CustomUnescapeCharacters) {
- if (value.Contains(customChar.Key))
- value = value.Replace(customChar.Key, customChar.Value);
- }
-
- return Uri.UnescapeDataString(value);
- }
- #endregion
- }
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:
tel
The tel handler is more straightforward than mailto,
- public class TelephoneWebLink : WebLink {
- #region Prefix
- protected static string LinkPrefix { get { return "tel:"; } }
- public override string Prefix => LinkPrefix;
- #endregion
-
- #region Delimiters
- protected static readonly char ExtensionDelimiter = 'p';
- #endregion
-
- #region Fields
- public string Number { get; set; }
- public string Extension { get; set; }
- #endregion
-
-
- public TelephoneWebLink() {
-
- }
- public TelephoneWebLink(string link) {
- ReadLink(link);
- }
-
- public static bool CanHandle(string link) {
- return link.ToLower().Trim().StartsWith(LinkPrefix);
- }
-
- public override void ClearFields() {
- Number = null;
- Extension = null;
- }
-
- public override void ReadLink(string link) {
- base.ReadLink(link);
-
- try {
- ClearFields();
-
-
- link = ExcludePrefix(link).Trim();
-
- Number = string.Empty;
- Extension = string.Empty;
-
- bool foundExtension = false;
- int idx = 0;
- foreach (var c in link) {
- if (idx == 0 && c == '+')
- Number += "+";
- if (c == ExtensionDelimiter)
- foundExtension = true;
- else if (char.IsDigit(c)) {
- if (foundExtension == false)
- Number += c.ToString();
- else
- Extension += c.ToString();
- }
- idx++;
- }
-
- } catch {
- throw new FormatException();
- }
- }
-
- public override string GenerateLink(bool includePrefix) {
- var str = base.GenerateLink(includePrefix);
-
- if (Number != null)
- str += Number.ToString();
-
- if (Extension != null && Extension.Length > 0)
- str += ExtensionDelimiter.ToString() + Extension;
-
- return str;
- }
- }
And here’s a list to test:
- tel:+20123456789
- 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.