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 in C#. 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 {
/// <summary>
/// Link prefix. Examples are: 'mailto:' and 'tel:'
/// </summary>
public abstract string Prefix { get; }
/// <summary>
/// Clears instance fields.
/// </summary>
public abstract void ClearFields();
/// <summary>
/// Loads link input into relevant fields.
/// </summary>
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.");
}
/// <summary>
/// Generates link from instance fields.
/// </summary>
public virtual string GenerateLink(bool includePrefix) {
var str = string.Empty;
if (includePrefix)
str += Prefix;
return str;
}
/// <summary>
/// Can be used to exclude prefix from a link string.
/// </summary>
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, adds 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();
// Exclude prefix if necessary
link = ExcludePrefix(link);
// Get mail 'To' Field
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);
// Handle rest of fields
var parameters = GetParameters(link, true);
foreach (var par in parameters) {
if (par.Key == ToField) // overrides the above code
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();
}
}
/// <summary>
/// Splits a mail string into a list of mail addresses.
/// </summary>
protected virtual IEnumerable<string> LoadRecipients(string val) {
var items = val.Split(RecipientDelimiters, StringSplitOptions.RemoveEmptyEntries);
return items.Select(s => s.Trim().ToLower()).Distinct();
}
/// <summary>
/// Splits a parameter string into a list of parameters (kay and value)
/// </summary>
/// <param name="skipEmpty">Whether to skip empty parameters.</param>
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;
}
/// <summary>
/// Joins a list of mail addresses into a string
/// </summary>
protected virtual string GetRecipientString(string[] recipients) {
return string.Join(RecipientDelimiters.First().ToString(), recipients);
}
/// <summary>
/// Joins a parameter (key and value) into a string
/// </summary>
/// <param name="escapeValue">Whether to escape value.</param>
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:
mailto:someone@yoursite.com
mailto:someone@yoursite.com?subject=Important!&body=Hi.
mailto:someone@yoursite.com?cc=someoneelse@theirsite.com,another@thatsite.com,me@mysite.com&bcc=lastperson@theirsite.com
mailto:name1@yoursite.com?cc=name2@yoursite.com&bcc=name3@yoursite.com&subject=The%20subject%20of%20the%20email&body=The%20body%20of%20the%20email
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();
// Exclude prefix if necessary
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.
Pingback: Handling ‘mailto’ and ‘tel’ Links inside Android WebView | Just Like [a] Magic