1 // Licensed to the .NET Foundation under one or more agreements. 2 // The .NET Foundation licenses this file to you under the MIT license. 3 // See the LICENSE file in the project root for more information. 4 5 using System; 6 using System.Text; 7 using System.Net.Mail; 8 using System.Globalization; 9 using System.Collections.Generic; 10 using System.Diagnostics; 11 12 namespace System.Net.Mime 13 { 14 internal static class MailBnfHelper 15 { 16 // characters allowed in atoms 17 internal static readonly bool[] Atext = CreateCharactersAllowedInAtoms(); 18 19 // characters allowed in quoted strings (not including Unicode) 20 internal static readonly bool[] Qtext = CreateCharactersAllowedInQuotedStrings(); 21 22 // characters allowed in domain literals 23 internal static readonly bool[] Dtext = CreateCharactersAllowedInDomainLiterals(); 24 25 // characters allowed in header names 26 internal static readonly bool[] Ftext = CreateCharactersAllowedInHeaderNames(); 27 28 // characters allowed in tokens 29 internal static readonly bool[] Ttext = CreateCharactersAllowedInTokens(); 30 31 // characters allowed inside of comments 32 internal static readonly bool[] Ctext = CreateCharactersAllowedInComments(); 33 34 internal static readonly int Ascii7bitMaxValue = 127; 35 internal static readonly char Quote = '\"'; 36 internal static readonly char Space = ' '; 37 internal static readonly char Tab = '\t'; 38 internal static readonly char CR = '\r'; 39 internal static readonly char LF = '\n'; 40 internal static readonly char StartComment = '('; 41 internal static readonly char EndComment = ')'; 42 internal static readonly char Backslash = '\\'; 43 internal static readonly char At = '@'; 44 internal static readonly char EndAngleBracket = '>'; 45 internal static readonly char StartAngleBracket = '<'; 46 internal static readonly char StartSquareBracket = '['; 47 internal static readonly char EndSquareBracket = ']'; 48 internal static readonly char Comma = ','; 49 internal static readonly char Dot = '.'; 50 51 private static readonly char[] s_colonSeparator = new char[] { ':' }; 52 53 // NOTE: See RFC 2822 for more detail. By default, every value in the array is false and only 54 // those values which are allowed in that particular set are then set to true. The numbers 55 // annotating each definition below are the range of ASCII values which are allowed in that definition. 56 CreateCharactersAllowedInAtoms()57 private static bool[] CreateCharactersAllowedInAtoms() 58 { 59 // atext = ALPHA / DIGIT / "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "/" / "=" / "?" / "^" / "_" / "`" / "{" / "|" / "}" / "~" 60 var atext = new bool[128]; 61 for (int i = '0'; i <= '9'; i++) { atext[i] = true; } 62 for (int i = 'A'; i <= 'Z'; i++) { atext[i] = true; } 63 for (int i = 'a'; i <= 'z'; i++) { atext[i] = true; } 64 atext['!'] = true; 65 atext['#'] = true; 66 atext['$'] = true; 67 atext['%'] = true; 68 atext['&'] = true; 69 atext['\''] = true; 70 atext['*'] = true; 71 atext['+'] = true; 72 atext['-'] = true; 73 atext['/'] = true; 74 atext['='] = true; 75 atext['?'] = true; 76 atext['^'] = true; 77 atext['_'] = true; 78 atext['`'] = true; 79 atext['{'] = true; 80 atext['|'] = true; 81 atext['}'] = true; 82 atext['~'] = true; 83 return atext; 84 } 85 CreateCharactersAllowedInQuotedStrings()86 private static bool[] CreateCharactersAllowedInQuotedStrings() 87 { 88 // fqtext = %d1-9 / %d11 / %d12 / %d14-33 / %d35-91 / %d93-127 89 var qtext = new bool[128]; 90 for (int i = 1; i <= 9; i++) { qtext[i] = true; } 91 qtext[11] = true; 92 qtext[12] = true; 93 for (int i = 14; i <= 33; i++) { qtext[i] = true; } 94 for (int i = 35; i <= 91; i++) { qtext[i] = true; } 95 for (int i = 93; i <= 127; i++) { qtext[i] = true; } 96 return qtext; 97 } 98 CreateCharactersAllowedInDomainLiterals()99 private static bool[] CreateCharactersAllowedInDomainLiterals() 100 { 101 // fdtext = %d1-8 / %d11 / %d12 / %d14-31 / %d33-90 / %d94-127 102 var dtext = new bool[128]; 103 for (int i = 1; i <= 8; i++) { dtext[i] = true; } 104 dtext[11] = true; 105 dtext[12] = true; 106 for (int i = 14; i <= 31; i++) { dtext[i] = true; } 107 for (int i = 33; i <= 90; i++) { dtext[i] = true; } 108 for (int i = 94; i <= 127; i++) { dtext[i] = true; } 109 return dtext; 110 } 111 CreateCharactersAllowedInHeaderNames()112 private static bool[] CreateCharactersAllowedInHeaderNames() 113 { 114 // ftext = %d33-57 / %d59-126 115 var ftext = new bool[128]; 116 for (int i = 33; i <= 57; i++) { ftext[i] = true; } 117 for (int i = 59; i <= 126; i++) { ftext[i] = true; } 118 return ftext; 119 } 120 CreateCharactersAllowedInTokens()121 private static bool[] CreateCharactersAllowedInTokens() 122 { 123 // ttext = %d33-126 except '()<>@,;:\"/[]?=' 124 var ttext = new bool[128]; 125 for (int i = 33; i <= 126; i++) { ttext[i] = true; } 126 ttext['('] = false; 127 ttext[')'] = false; 128 ttext['<'] = false; 129 ttext['>'] = false; 130 ttext['@'] = false; 131 ttext[','] = false; 132 ttext[';'] = false; 133 ttext[':'] = false; 134 ttext['\\'] = false; 135 ttext['"'] = false; 136 ttext['/'] = false; 137 ttext['['] = false; 138 ttext[']'] = false; 139 ttext['?'] = false; 140 ttext['='] = false; 141 return ttext; 142 } 143 CreateCharactersAllowedInComments()144 private static bool[] CreateCharactersAllowedInComments() 145 { 146 // ctext- %d1-8 / %d11 / %d12 / %d14-31 / %33-39 / %42-91 / %93-127 147 var ctext = new bool[128]; 148 for (int i = 1; i <= 8; i++) { ctext[i] = true; } 149 ctext[11] = true; 150 ctext[12] = true; 151 for (int i = 14; i <= 31; i++) { ctext[i] = true; } 152 for (int i = 33; i <= 39; i++) { ctext[i] = true; } 153 for (int i = 42; i <= 91; i++) { ctext[i] = true; } 154 for (int i = 93; i <= 127; i++) { ctext[i] = true; } 155 return ctext; 156 } 157 SkipCFWS(string data, ref int offset)158 internal static bool SkipCFWS(string data, ref int offset) 159 { 160 int comments = 0; 161 for (; offset < data.Length; offset++) 162 { 163 if (data[offset] > 127) 164 throw new FormatException(SR.Format(SR.MailHeaderFieldInvalidCharacter, data[offset])); 165 else if (data[offset] == '\\' && comments > 0) 166 offset += 2; 167 else if (data[offset] == '(') 168 comments++; 169 else if (data[offset] == ')') 170 comments--; 171 else if (data[offset] != ' ' && data[offset] != '\t' && comments == 0) 172 return true; 173 174 if (comments < 0) 175 { 176 throw new FormatException(SR.Format(SR.MailHeaderFieldInvalidCharacter, data[offset])); 177 } 178 } 179 180 //returns false if end of string 181 return false; 182 } 183 ValidateHeaderName(string data)184 internal static void ValidateHeaderName(string data) 185 { 186 int offset = 0; 187 for (; offset < data.Length; offset++) 188 { 189 if (data[offset] > Ftext.Length || !Ftext[data[offset]]) 190 throw new FormatException(SR.InvalidHeaderName); 191 } 192 if (offset == 0) 193 throw new FormatException(SR.InvalidHeaderName); 194 } 195 ReadQuotedString(string data, ref int offset, StringBuilder builder)196 internal static string ReadQuotedString(string data, ref int offset, StringBuilder builder) 197 { 198 return ReadQuotedString(data, ref offset, builder, false, false); 199 } 200 ReadQuotedString(string data, ref int offset, StringBuilder builder, bool doesntRequireQuotes, bool permitUnicodeInDisplayName)201 internal static string ReadQuotedString(string data, ref int offset, StringBuilder builder, bool doesntRequireQuotes, bool permitUnicodeInDisplayName) 202 { 203 // assume first char is the opening quote 204 if (!doesntRequireQuotes) 205 { 206 ++offset; 207 } 208 int start = offset; 209 StringBuilder localBuilder = (builder != null ? builder : new StringBuilder()); 210 for (; offset < data.Length; offset++) 211 { 212 if (data[offset] == '\\') 213 { 214 localBuilder.Append(data, start, offset - start); 215 start = ++offset; 216 } 217 else if (data[offset] == '"') 218 { 219 localBuilder.Append(data, start, offset - start); 220 offset++; 221 return (builder != null ? null : localBuilder.ToString()); 222 } 223 else if (data[offset] == '=' && 224 data.Length > offset + 3 && 225 data[offset + 1] == '\r' && 226 data[offset + 2] == '\n' && 227 (data[offset + 3] == ' ' || data[offset + 3] == '\t')) 228 { 229 //it's a soft crlf so it's ok 230 offset += 3; 231 } 232 else if (permitUnicodeInDisplayName) 233 { 234 //if data contains Unicode and Unicode is permitted, then 235 //it is valid in a quoted string in a header. 236 if (data[offset] <= Ascii7bitMaxValue && !Qtext[data[offset]]) 237 throw new FormatException(SR.Format(SR.MailHeaderFieldInvalidCharacter, data[offset])); 238 } 239 //not permitting Unicode, in which case Unicode is a formatting error 240 else if (data[offset] > Ascii7bitMaxValue || !Qtext[data[offset]]) 241 { 242 throw new FormatException(SR.Format(SR.MailHeaderFieldInvalidCharacter, data[offset])); 243 } 244 } 245 if (doesntRequireQuotes) 246 { 247 localBuilder.Append(data, start, offset - start); 248 return (builder != null ? null : localBuilder.ToString()); 249 } 250 throw new FormatException(SR.MailHeaderFieldMalformedHeader); 251 } 252 ReadParameterAttribute(string data, ref int offset, StringBuilder builder)253 internal static string ReadParameterAttribute(string data, ref int offset, StringBuilder builder) 254 { 255 if (!SkipCFWS(data, ref offset)) 256 return null; // 257 258 return ReadToken(data, ref offset, null); 259 } 260 ReadToken(string data, ref int offset, StringBuilder builder)261 internal static string ReadToken(string data, ref int offset, StringBuilder builder) 262 { 263 int start = offset; 264 for (; offset < data.Length; offset++) 265 { 266 if (data[offset] > Ascii7bitMaxValue) 267 { 268 throw new FormatException(SR.Format(SR.MailHeaderFieldInvalidCharacter, data[offset])); 269 } 270 else if (!Ttext[data[offset]]) 271 { 272 break; 273 } 274 } 275 276 if (start == offset) 277 { 278 throw new FormatException(SR.Format(SR.MailHeaderFieldInvalidCharacter, data[offset])); 279 } 280 281 return data.Substring(start, offset - start); 282 } 283 284 private static string[] s_months = new string[] { null, "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; 285 GetDateTimeString(DateTime value, StringBuilder builder)286 internal static string GetDateTimeString(DateTime value, StringBuilder builder) 287 { 288 StringBuilder localBuilder = (builder != null ? builder : new StringBuilder()); 289 localBuilder.Append(value.Day); 290 localBuilder.Append(' '); 291 localBuilder.Append(s_months[value.Month]); 292 localBuilder.Append(' '); 293 localBuilder.Append(value.Year); 294 localBuilder.Append(' '); 295 if (value.Hour <= 9) 296 { 297 localBuilder.Append('0'); 298 } 299 localBuilder.Append(value.Hour); 300 localBuilder.Append(':'); 301 if (value.Minute <= 9) 302 { 303 localBuilder.Append('0'); 304 } 305 localBuilder.Append(value.Minute); 306 localBuilder.Append(':'); 307 if (value.Second <= 9) 308 { 309 localBuilder.Append('0'); 310 } 311 localBuilder.Append(value.Second); 312 313 string offset = TimeZoneInfo.Local.GetUtcOffset(value).ToString(); 314 if (offset[0] != '-') 315 { 316 localBuilder.Append(" +"); 317 } 318 else 319 { 320 localBuilder.Append(' '); 321 } 322 323 string[] offsetFields = offset.Split(s_colonSeparator); 324 localBuilder.Append(offsetFields[0]); 325 localBuilder.Append(offsetFields[1]); 326 return (builder != null ? null : localBuilder.ToString()); 327 } 328 GetTokenOrQuotedString(string data, StringBuilder builder, bool allowUnicode)329 internal static void GetTokenOrQuotedString(string data, StringBuilder builder, bool allowUnicode) 330 { 331 int offset = 0, start = 0; 332 for (; offset < data.Length; offset++) 333 { 334 if (CheckForUnicode(data[offset], allowUnicode)) 335 { 336 continue; 337 } 338 339 if (!Ttext[data[offset]] || data[offset] == ' ') 340 { 341 builder.Append('"'); 342 for (; offset < data.Length; offset++) 343 { 344 if (CheckForUnicode(data[offset], allowUnicode)) 345 { 346 continue; 347 } 348 else if (IsFWSAt(data, offset)) // Allow FWS == "\r\n " 349 { 350 // No-op, skip these three chars 351 offset += 2; 352 } 353 else if (!Qtext[data[offset]]) 354 { 355 builder.Append(data, start, offset - start); 356 builder.Append('\\'); 357 start = offset; 358 } 359 } 360 builder.Append(data, start, offset - start); 361 builder.Append('"'); 362 return; 363 } 364 } 365 366 //always a quoted string if it was empty. 367 if (data.Length == 0) 368 { 369 builder.Append("\"\""); 370 } 371 // Token, no quotes needed 372 builder.Append(data); 373 } 374 CheckForUnicode(char ch, bool allowUnicode)375 private static bool CheckForUnicode(char ch, bool allowUnicode) 376 { 377 if (ch < Ascii7bitMaxValue) 378 { 379 return false; 380 } 381 382 if (!allowUnicode) 383 { 384 throw new FormatException(SR.Format(SR.MailHeaderFieldInvalidCharacter, ch)); 385 } 386 return true; 387 } 388 IsAllowedWhiteSpace(char c)389 internal static bool IsAllowedWhiteSpace(char c) 390 { 391 // all allowed whitespace characters 392 return c == Tab || c == Space || c == CR || c == LF; 393 } 394 HasCROrLF(string data)395 internal static bool HasCROrLF(string data) 396 { 397 for (int i = 0; i < data.Length; i++) 398 { 399 if (data[i] == '\r' || data[i] == '\n') 400 { 401 return true; 402 } 403 } 404 return false; 405 } 406 407 // Is there a FWS ("\r\n " or "\r\n\t") starting at the given index? IsFWSAt(string data, int index)408 internal static bool IsFWSAt(string data, int index) 409 { 410 Debug.Assert(index >= 0); 411 Debug.Assert(index < data.Length); 412 413 return (data[index] == MailBnfHelper.CR 414 && index + 2 < data.Length 415 && data[index + 1] == MailBnfHelper.LF 416 && (data[index + 2] == MailBnfHelper.Space 417 || data[index + 2] == MailBnfHelper.Tab)); 418 } 419 } 420 } 421