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