// Copyright Michael Anthony Puls II, http://shadow2531.com/ // Distributed under the Boost Software License, Version 1.0. // See http://boost.org/LICENSE_1_0.txt // Rules to follow http://shadow2531.com/opera/testcases/mailto/rfc2368-3.html // Example c++ versions of these functions are in // http://shadow2531.com/cpp/mailto_funcs1.zip // http://shadow2531.com/cpp/mailto_funcs2.zip // http://shadow2531.com/cpp/mailto_funcs3.zip // http://shadow2531.com/cpp/mailto_funcs4.zip // http://shadow2531.com/cpp/mailto_funcs5.zip // http://shadow2531.com/cpp/mailto_funcs6.zip // http://shadow2531.com/cpp/mailto_funcs7.zip // http://shadow2531.com/cpp/mailto_funcs8.zip // http://shadow2531.com/cpp/mailto_funcs9.zip // http://shadow2531.com/cpp/mailto_funcs10.zip // http://shadow2531.com/cpp/mailto_funcs11.zip // Example Javascript versions of these functions are in http://shadow2531.com/opera/testcases/mailto/mailto_funcs.js // Example Java versions of these functions are in http://shadow2531.com/java/MailtoFuncs.java // Example Ruby versions of these functions are in http://shadow2531.com/ruby/mailto_funcs.rb // Example Perl versions of these functinos are in http://shadow2531.com/perl/mailto_funcs.pl // Example Python versions of these functions are in http://shadow2531.com/py/mailto_funcs.py // Example Tcl versions of these functions are in http://shadow2531.com/tcl/mailto_funcs.tcl // Newest versions: http://shadow2531.com/opera/testcases/mailto/MailtoURIParserPack.zip import std.stdio; import std.string; class MailtoURIParser { public: this(char[] inuri) { eval(inuri); } void setURI(char[] inuri) { eval(inuri); } char[] getURI() { return keys['w']; } char[] getNormalizedURI() { return keys['n']; } char[] getEncodedTO() { return keys['T']; } char[] getDecodedTO() { return keys['t']; } char[] getEncodedSubject() { return keys['S']; } char[] getDecodedSubject() { return keys['s']; } char[] getEncodedBody() { return keys['M']; } char[] getDecodedBody() { return keys['m']; } char[] getEncodedCC() { return keys['C']; } char[] getDecodedCC() { return keys['c']; } char[] getEncodedBCC() { return keys['B']; } char[] getDecodedBCC() { return keys['b']; } // Return a copy of s with these keys (if present in s) replaced with their corresponding values. // %w = original uri // %n = normalized uri (mailto URI with just the basic hnames with no duplicate hnames) // %t = decoded TO value // %T = encoded TO value // %s = decoded Subject value // %m = deocded Body value // %M = encoded Body value // %c = decoded CC value // %C = encoded CC value // %b = decoded BCC value // %B = encoded BCC value // %% = % // An invalid %key or a % at the end of the string is treated literally. char[] resolveCommandFormatString(char[] s) { char[] ret; for (int i = 0; i < s.length; ++i) { int next = i + 1; if (s[i] == '%' && next < s.length) { if (s[next] == '%') { ret ~= '%'; ++i; continue; } if (find(keys.keys, s[next]) != -1) { ret ~= keys[s[next]]; ++i; } else { ret ~= s[i]; } } else { ret ~= s[i]; } } return ret; } private: char[][char] keys; // This replaces just the first occurence of a '?'. // The standard replace functions seem to want to replace all of them. char[] replaceFirstQuestionMark(char[] s) { char[] ret; int done = 0; foreach(char i; s) { if (i == '?') { if (done == 0) { ret ~= '&'; done = 1; } else { ret ~= i; } } else { ret ~= i; } } return ret; } // Generate a &-separated list of hname=hvalue pairs from the URI so they can be parsed. char[] prepareURIForParsing(char[] inuri) { if (ifind(inuri, "mailto:?") == 0) { return inuri[8..inuri.length]; } else if (ifind(inuri, "mailto:") == 0) { char g[] = replaceFirstQuestionMark(inuri); return "to=" ~ g[7..g.length]; } else { return ""; } } // getBody(), getAddresses() and getSubject could be implemented as one function // , but I like them separate. // Join all non-empty hvalues for the hname skey with %2C%20. (TO, CC and BCC rule) char[] getAddresses(char[] skey, char[][] hlist) { char[] ret; foreach(char[] i; hlist) { if (i == "") { continue; } int eq = find(i, "="); if (eq == -1) { continue; } char[] hname = i[0..eq]; if (icmp(hname, skey) != 0) { continue; } char[] hvalue = i[eq + 1..i.length]; if (hvalue == "") { continue; } if (ret != "") { ret ~= "%2C%20"; } ret ~= hvalue; } return ret; } // Use the last subject hvalue even if it's empty and even if a previous one is not. char[] getSubject(char[][] hlist) { char[] ret; foreach(char[] i; hlist) { if (i == "") { continue; } int eq = find(i, "="); if (eq == -1) { continue; } char[] hname = i[0..eq]; if (icmp(hname, "subject") != 0) { continue; } ret = i[eq + 1..i.length]; } return ret; } // Join the first non-empty body hvalue and all body hvalues (even if they're empty) after that with %0D%0A. char[] getBody(char[][] hlist) { char[] ret; foreach(char[] i; hlist) { if (i == "") { continue; } int eq = find(i, "="); if (eq == -1) { continue; } char[] hname = i[0..eq]; if (icmp(hname, "body") != 0) { continue; } char[] hvalue = i[eq + 1..i.length]; if (hvalue == "" && ret == "") { continue; } if (ret != "") { ret ~= "%0D%0A"; } ret ~= hvalue; } return ret; } // Decoded %HH. Treat invalid %HH literally. Do not decode + to a space because // mail clients don't do that. char[] decode(char[] s) { char[] ret = ""; for (int i = 0; i < s.length; ++i) { int two = i + 2; if (s[i] == '%' && two < s.length) { int a = ifind(hexdigits, s[two - 1]); int b = ifind(hexdigits, s[two]); if (a == -1 || b == -1) { ret ~= s[i]; } else { ret ~= cast(char)a * 16 + b; // Not sure if the case is really need, but... i += 2; } } else { ret ~= s[i]; } } // Before returning, make sure all newlines are represented as \n. return replace(replace(ret, "\r\n", "\n"), "\r", "\n"); } // Each time a new URI is set, reparse and store the data. // If the uri doesn't start with a case-insensitive mailto:, // set the keys to their default values. // The results are stored in the map to simplify replaceCommandFormatString(). void eval(char[] inuri) { char[] x = prepareURIForParsing(inuri); if (x != "") { // hlist caches the result of splitting the &-separated list of hname=hvalue // pair strings so the result doesn't have to be regenerated for each of the // encoded get calls below. // hlist is passed around below instead of having it as a private variable // and just letting the functions use it. This is because I do not know // how to clear() hlist. Just creating it here lets it go out of scope // which avoids the problem. char[][] hlist = split(x, "&"); keys['w'] = inuri; keys['T'] = getAddresses("to", hlist); keys['S'] = getSubject(hlist); keys['M'] = getBody(hlist); keys['C'] = getAddresses("cc", hlist); keys['B'] = getAddresses("bcc", hlist); // Create the normalized mailto URI with just the basic hnames and no duplicate hnames. keys['n'] = "mailto:"; keys['n'] ~= keys['T']; keys['n'] ~= "?subject="; keys['n'] ~= keys['S']; keys['n'] ~= "&body="; keys['n'] ~= keys['M']; keys['n'] ~= "&cc="; keys['n'] ~= keys['C']; keys['n'] ~= "&bcc="; keys['n'] ~= keys['B']; keys['t'] = decode(keys['T']); keys['s'] = decode(keys['S']); keys['m'] = decode(keys['M']); keys['c'] = decode(keys['C']); keys['b'] = decode(keys['B']); } else { keys['w'] = "mailto:"; keys['n'] = "mailto:"; keys['t'] = ""; keys['T'] = ""; keys['s'] = ""; keys['S'] = ""; keys['m'] = ""; keys['M'] = ""; keys['c'] = ""; keys['C'] = ""; keys['b'] = ""; keys['B'] = ""; } } } void test(int t, char[] ins, char[] outs) { if (ins == outs) { writefln("%d = pass", t); } else { writefln("%d = fail\nbefore_%s_after", t, ins); } } void main(char[][] args) { MailtoURIParser x = new MailtoURIParser(""); test(1, x.getEncodedTO(), ""); x.setURI("mailto:"); test(2, x.getEncodedTO(), ""); x.setURI("mailto:?"); test(3, x.getEncodedTO(), ""); x.setURI("mailto:email%40site.com"); test(4, x.getEncodedTO(), "email%40site.com"); x.setURI("mailto:?to=email%40site.com"); test(5, x.getEncodedTO(), "email%40site.com"); x.setURI("mailto:email1%40site.com%2C%20email2%40site.com?to=email3%40site.com%2C%20email4%40site.com"); test(6, x.getEncodedTO(), "email1%40site.com%2C%20email2%40site.com%2C%20email3%40site.com%2C%20email4%40site.com"); x.setURI("mailto:?to=email1%40site.com&to=email2%40site.com&to=email3%40site.com"); test(7, x.getEncodedTO(), "email1%40site.com%2C%20email2%40site.com%2C%20email3%40site.com"); x.setURI("mailto:?to=&to=&to=&to=&to=&to=&to=&to=&to=&to=&to=&to=&to="); test(8, x.getEncodedTO(), ""); x.setURI("mailto:?to=&to=email1%40site.com&to=&to=email2%40site.com&to="); test(9, x.getEncodedTO(), "email1%40site.com%2C%20email2%40site.com"); x.setURI("mailto:?subject=bark%20bark"); test(10, x.getEncodedSubject(), "bark%20bark"); x.setURI("mailto:?subject="); test(11, x.getEncodedSubject(), ""); x.setURI("mailto:?subject=&subject="); test(12, x.getEncodedSubject(), ""); x.setURI("mailto:?subject=1&subject=2&subject=last%20one"); test(13, x.getEncodedSubject(), "last%20one"); x.setURI("mailto:?subject=1&subject="); test(14, x.getEncodedSubject(), ""); x.setURI("mailto:?body=line1%0D%0Aline2"); test(15, x.getEncodedBody(), "line1%0D%0Aline2"); x.setURI("mailto:?body=line1&body=line2"); test(16, x.getEncodedBody(), "line1%0D%0Aline2"); x.setURI("mailto:?body=&body=&body=line1"); test(17, x.getEncodedBody(), "line1"); x.setURI("mailto:?body=&body=&body=line1&body=&body=&body=line4&body=&body="); test(18, x.getEncodedBody(), "line1%0D%0A%0D%0A%0D%0Aline4%0D%0A%0D%0A"); x.setURI("mailto:?body=line1&body="); test(19, x.getEncodedBody(), "line1%0D%0A"); x.setURI("mailto:email1%40site.com?to=&to=email2%40site.com&subject=1&subject=&subject=2&subject=&body=&body=&body=line1&body=&body=&body=line4&body=&cc=zam%40site.com&bcc=bam%40site.com"); test(20, x.getNormalizedURI(), "mailto:email1%40site.com%2C%20email2%40site.com?subject=&body=line1%0D%0A%0D%0A%0D%0Aline4%0D%0A&cc=zam%40site.com&bcc=bam%40site.com"); char[] big = "mailto:%22a%5C%5Cb%22%20%3Cemail1%40site.com%3E%2C%20email2%40site.com?to=&to=email3%40site.com%2C%20email4%40site.com&to=email5%40site.com%2C%20email6%40site.com&to=&to=&subject=You%20should%20not%20see%20me%2E&subject=last%20call%20for%20cats%20%26%20dogs%2E&body=&body=&body=&body=line1%0D%0Aline2&body=&body=&body=&body=line6&body=line7%0D%0A1+2+3+4+5%0D%0AColumn1A%09Column1B%09Column1C%09Column1D%0D%0A%E2%88%9A&cc=%22a%5C%5Cb%22%20%3Csomeone1%40site.com%3E%2C%20someone2%40site.com&cc=&cc=someone3%40site.com%2C%20someone4%40site.com%2C%20%22foo%20%5C%22bar%5C%22%22%20%3Cfoo%40bar%2Ecom%3E&cc=&cc=&bcc=%22a%5C%5Cb%22%20%3Csomeoneelse1%40site.com%3E%2C%20someoneelse2%40site.com&bcc=&bcc=someoneelse3%40site.com%2C%20someoneelse4%40site.com&bcc=&bcc="; x.setURI(big); test(21, x.getNormalizedURI(), "mailto:%22a%5C%5Cb%22%20%3Cemail1%40site.com%3E%2C%20email2%40site.com%2C%20email3%40site.com%2C%20email4%40site.com%2C%20email5%40site.com%2C%20email6%40site.com?subject=last%20call%20for%20cats%20%26%20dogs%2E&body=line1%0D%0Aline2%0D%0A%0D%0A%0D%0A%0D%0Aline6%0D%0Aline7%0D%0A1+2+3+4+5%0D%0AColumn1A%09Column1B%09Column1C%09Column1D%0D%0A%E2%88%9A&cc=%22a%5C%5Cb%22%20%3Csomeone1%40site.com%3E%2C%20someone2%40site.com%2C%20someone3%40site.com%2C%20someone4%40site.com%2C%20%22foo%20%5C%22bar%5C%22%22%20%3Cfoo%40bar%2Ecom%3E&bcc=%22a%5C%5Cb%22%20%3Csomeoneelse1%40site.com%3E%2C%20someoneelse2%40site.com%2C%20someoneelse3%40site.com%2C%20someoneelse4%40site.com"); test(22, x.resolveCommandFormatString("\"c:\\program files\\program\\program.exe\" \"%T\" \"%S\" \"%M\" \"%C\" \"%B\""), "\"c:\\program files\\program\\program.exe\" \"%22a%5C%5Cb%22%20%3Cemail1%40site.com%3E%2C%20email2%40site.com%2C%20email3%40site.com%2C%20email4%40site.com%2C%20email5%40site.com%2C%20email6%40site.com\" \"last%20call%20for%20cats%20%26%20dogs%2E\" \"line1%0D%0Aline2%0D%0A%0D%0A%0D%0A%0D%0Aline6%0D%0Aline7%0D%0A1+2+3+4+5%0D%0AColumn1A%09Column1B%09Column1C%09Column1D%0D%0A%E2%88%9A\" \"%22a%5C%5Cb%22%20%3Csomeone1%40site.com%3E%2C%20someone2%40site.com%2C%20someone3%40site.com%2C%20someone4%40site.com%2C%20%22foo%20%5C%22bar%5C%22%22%20%3Cfoo%40bar%2Ecom%3E\" \"%22a%5C%5Cb%22%20%3Csomeoneelse1%40site.com%3E%2C%20someoneelse2%40site.com%2C%20someoneelse3%40site.com%2C%20someoneelse4%40site.com\""); test(23, x.resolveCommandFormatString("%%T"), "%T"); x.setURI("mailto:?subject=%E2%88%9A"); test(24, x.getDecodedSubject(), "\xE2\x88\x9A"); x.setURI("mailto:?subject=1+2+3+4+5"); test(25, x.getDecodedSubject(), "1+2+3+4+5"); x.setURI("mailto:?subject=%G3"); test(26, x.getDecodedSubject(), "%G3"); x.setURI("mailto:?subject=%%%"); test(27, x.getDecodedSubject(), "%%%"); x.setURI("mailto:?subject=%"); test(28, x.getDecodedSubject(), "%"); x.setURI("mailto:?body=%0A%0D%0A%0D"); test(29, x.getDecodedBody(), "\n\n\n"); x.setURI("mailto:?&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&body=line1"); test(30, x.getEncodedBody(), "line1"); x.setURI(""); test(31, x.getEncodedCC(), ""); x.setURI("mailto:"); test(32, x.getEncodedCC(), ""); x.setURI("mailto:?"); test(33, x.getEncodedCC(), ""); x.setURI("mailto:?cc=email%40site.com"); test(34, x.getEncodedCC(), "email%40site.com"); x.setURI("mailto:?cc=email1%40site.com%2C%20email2%40site.com&cc=email3%40site.com%2C%20email4%40site.com"); test(35, x.getEncodedCC(), "email1%40site.com%2C%20email2%40site.com%2C%20email3%40site.com%2C%20email4%40site.com"); x.setURI("mailto:?cc=email1%40site.com&cc=email2%40site.com&cc=email3%40site.com"); test(36, x.getEncodedCC(), "email1%40site.com%2C%20email2%40site.com%2C%20email3%40site.com"); x.setURI("mailto:?cc=&cc=&cc=&cc=&cc=&cc=&cc=&cc=&cc=&cc=&cc=&cc=&cc="); test(37, x.getEncodedCC(), ""); x.setURI("mailto:?cc=&cc=email1%40site.com&cc=&cc=email2%40site.com&cc="); test(38, x.getEncodedCC(), "email1%40site.com%2C%20email2%40site.com"); x.setURI(""); test(39, x.getEncodedBCC(), ""); x.setURI("mailto:"); test(40, x.getEncodedBCC(), ""); x.setURI("mailto:?"); test(41, x.getEncodedBCC(), ""); x.setURI("mailto:?bcc=email%40site.com"); test(42, x.getEncodedBCC(), "email%40site.com"); x.setURI("mailto:?bcc=email1%40site.com%2C%20email2%40site.com&bcc=email3%40site.com%2C%20email4%40site.com"); test(43, x.getEncodedBCC(), "email1%40site.com%2C%20email2%40site.com%2C%20email3%40site.com%2C%20email4%40site.com"); x.setURI("mailto:?bcc=email1%40site.com&bcc=email2%40site.com&bcc=email3%40site.com"); test(44, x.getEncodedBCC(), "email1%40site.com%2C%20email2%40site.com%2C%20email3%40site.com"); x.setURI("mailto:?bcc=&bcc=&bcc=&bcc=&bcc=&bcc=&bcc=&bcc=&bcc=&bcc=&bcc=&bcc=&bcc="); test(45, x.getEncodedBCC(), ""); x.setURI("mailto:?bcc=&bcc=email1%40site.com&bcc=&bcc=email2%40site.com&bcc="); test(46, x.getEncodedBCC(), "email1%40site.com%2C%20email2%40site.com"); x.setURI("mailto:?subject=m%26m%09bob"); test(47, x.getDecodedSubject(), "m&m\tbob"); x.setURI("mailto:email%40site.com"); test(48, x.resolveCommandFormatString("%%%%%t"), "%%email@site.com"); x.setURI("mailto:?cc=email%40site.com"); test(49, x.getDecodedCC(), "email@site.com"); x.setURI("mailto:?to=email%40site.com"); test(50, x.getDecodedTO(), "email@site.com"); x.setURI("MaIlTo:?BcC=email%40site.com"); test(51, x.getDecodedBCC(), "email@site.com"); x.setURI(big); char[] fmt = "program_that_wants_decoded_values -to \"%t\" -subj \"%s\" -cc \"%c\" -bcc \"%b\" -body \"%m\""; test(52, x.resolveCommandFormatString(fmt), "program_that_wants_decoded_values -to \"\"a\\\\b\" , email2@site.com, email3@site.com, email4@site.com, email5@site.com, email6@site.com\" -subj \"last call for cats & dogs.\" -cc \"\"a\\\\b\" , someone2@site.com, someone3@site.com, someone4@site.com, \"foo \\\"bar\\\"\" \" -bcc \"\"a\\\\b\" , someoneelse2@site.com, someoneelse3@site.com, someoneelse4@site.com\" -body \"line1\nline2\n\n\n\nline6\nline7\n1+2+3+4+5\nColumn1A\tColumn1B\tColumn1C\tColumn1D\n\xE2\x88\x9A\""); x.setURI(""); test(53, x.getEncodedTO(), ""); test(54, x.getEncodedBCC(), ""); test(55, x.getEncodedCC(), ""); test(56, x.getEncodedBody(), ""); test(57, x.getEncodedSubject(), ""); test(58, x.getURI(), "mailto:"); test(59, x.getNormalizedURI(), "mailto:"); test(60, x.getDecodedTO(), ""); test(61, x.getDecodedBCC(), ""); test(62, x.getDecodedCC(), ""); test(63, x.getDecodedBody(), ""); test(64, x.getDecodedSubject(), ""); }