MimeKit.cs 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883
  1. #nullable disable
  2. #pragma warning disable CA1510
  3. namespace Jint.Native.Date;
  4. // This file contains code extracted from excellent MimeKit library
  5. // https://github.com/jstedfast/MimeKit , see above copyright which applies to all code
  6. // Jint version has adjusted namespaces and made members visible / removed unused ones
  7. // Author: Jeffrey Stedfast <[email protected]>
  8. //
  9. // Copyright (c) 2013-2022 .NET Foundation and Contributors
  10. //
  11. // Permission is hereby granted, free of charge, to any person obtaining a copy
  12. // of this software and associated documentation files (the "Software"), to deal
  13. // in the Software without restriction, including without limitation the rights
  14. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  15. // copies of the Software, and to permit persons to whom the Software is
  16. // furnished to do so, subject to the following conditions:
  17. //
  18. // The above copyright notice and this permission notice shall be included in
  19. // all copies or substantial portions of the Software.
  20. //
  21. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  22. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  23. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  24. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  25. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  26. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  27. // THE SOFTWARE.
  28. //
  29. using System;
  30. using System.Text;
  31. using System.Collections.Generic;
  32. using System.Runtime.InteropServices;
  33. [Flags]
  34. internal enum DateTokenFlags : byte
  35. {
  36. None = 0,
  37. NonNumeric = (1 << 0),
  38. NonWeekday = (1 << 1),
  39. NonMonth = (1 << 2),
  40. NonTime = (1 << 3),
  41. NonAlphaZone = (1 << 4),
  42. NonNumericZone = (1 << 5),
  43. HasColon = (1 << 6),
  44. HasSign = (1 << 7),
  45. }
  46. [StructLayout(LayoutKind.Auto)]
  47. internal readonly struct DateToken
  48. {
  49. public DateToken(DateTokenFlags flags, int start, int length)
  50. {
  51. Flags = flags;
  52. Start = start;
  53. Length = length;
  54. }
  55. public DateTokenFlags Flags { get; }
  56. public int Start { get; }
  57. public int Length { get; }
  58. public bool IsNumeric
  59. {
  60. get { return (Flags & DateTokenFlags.NonNumeric) == DateTokenFlags.None; }
  61. }
  62. public bool IsWeekday
  63. {
  64. get { return (Flags & DateTokenFlags.NonWeekday) == DateTokenFlags.None; }
  65. }
  66. public bool IsMonth
  67. {
  68. get { return (Flags & DateTokenFlags.NonMonth) == DateTokenFlags.None; }
  69. }
  70. public bool IsTimeOfDay
  71. {
  72. get { return (Flags & DateTokenFlags.NonTime) == DateTokenFlags.None && (Flags & DateTokenFlags.HasColon) != DateTokenFlags.None; }
  73. }
  74. public bool IsNumericZone
  75. {
  76. get { return (Flags & DateTokenFlags.NonNumericZone) == DateTokenFlags.None && (Flags & DateTokenFlags.HasSign) != DateTokenFlags.None; }
  77. }
  78. public bool IsAlphaZone
  79. {
  80. get { return (Flags & DateTokenFlags.NonAlphaZone) == DateTokenFlags.None; }
  81. }
  82. public bool IsTimeZone
  83. {
  84. get { return IsNumericZone || IsAlphaZone; }
  85. }
  86. }
  87. /// <summary>
  88. /// Utility methods to parse and format rfc822 date strings.
  89. /// </summary>
  90. /// <remarks>
  91. /// Utility methods to parse and format rfc822 date strings.
  92. /// </remarks>
  93. internal static class DateUtils
  94. {
  95. private const string MonthCharacters = "JanuaryFebruaryMarchAprilMayJuneJulyAugustSeptemberOctoberNovemberDecember";
  96. private const string WeekdayCharacters = "SundayMondayTuesdayWednesdayThursdayFridaySaturday";
  97. private const string AlphaZoneCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
  98. private const string NumericZoneCharacters = "+-0123456789";
  99. private const string NumericCharacters = "0123456789";
  100. private const string TimeCharacters = "0123456789:";
  101. private static readonly string[] Months = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
  102. private static readonly string[] WeekDays = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
  103. private static readonly Dictionary<string, int> timezones;
  104. private static readonly DateTokenFlags[] datetok;
  105. static DateUtils()
  106. {
  107. timezones = new Dictionary<string, int>(StringComparer.Ordinal)
  108. {
  109. { "UT", 0 },
  110. { "UTC", 0 },
  111. { "GMT", 0 },
  112. { "EDT", -400 },
  113. { "EST", -500 },
  114. { "CDT", -500 },
  115. { "CST", -600 },
  116. { "MDT", -600 },
  117. { "MST", -700 },
  118. { "PDT", -700 },
  119. { "PST", -800 },
  120. // Note: rfc822 got the signs backwards for the military
  121. // timezones so some sending clients may mistakenly use the
  122. // wrong values.
  123. { "A", 100 },
  124. { "B", 200 },
  125. { "C", 300 },
  126. { "D", 400 },
  127. { "E", 500 },
  128. { "F", 600 },
  129. { "G", 700 },
  130. { "H", 800 },
  131. { "I", 900 },
  132. { "K", 1000 },
  133. { "L", 1100 },
  134. { "M", 1200 },
  135. { "N", -100 },
  136. { "O", -200 },
  137. { "P", -300 },
  138. { "Q", -400 },
  139. { "R", -500 },
  140. { "S", -600 },
  141. { "T", -700 },
  142. { "U", -800 },
  143. { "V", -900 },
  144. { "W", -1000 },
  145. { "X", -1100 },
  146. { "Y", -1200 },
  147. { "Z", 0 },
  148. };
  149. datetok = new DateTokenFlags[256];
  150. var any = new char[2];
  151. for (int c = 0; c < 256; c++)
  152. {
  153. if (c >= 0x41 && c <= 0x5a)
  154. {
  155. any[1] = (char) (c + 0x20);
  156. any[0] = (char) c;
  157. }
  158. else if (c >= 0x61 && c <= 0x7a)
  159. {
  160. any[0] = (char) (c - 0x20);
  161. any[1] = (char) c;
  162. }
  163. #pragma warning disable CA2249
  164. if (NumericZoneCharacters.IndexOf((char) c) == -1)
  165. datetok[c] |= DateTokenFlags.NonNumericZone;
  166. if (AlphaZoneCharacters.IndexOf((char) c) == -1)
  167. datetok[c] |= DateTokenFlags.NonAlphaZone;
  168. if (WeekdayCharacters.IndexOfAny(any) == -1)
  169. datetok[c] |= DateTokenFlags.NonWeekday;
  170. if (NumericCharacters.IndexOf((char) c) == -1)
  171. datetok[c] |= DateTokenFlags.NonNumeric;
  172. if (MonthCharacters.IndexOfAny(any) == -1)
  173. datetok[c] |= DateTokenFlags.NonMonth;
  174. if (TimeCharacters.IndexOf((char) c) == -1)
  175. datetok[c] |= DateTokenFlags.NonTime;
  176. #pragma warning restore CA2249
  177. }
  178. datetok[':'] |= DateTokenFlags.HasColon;
  179. datetok['+'] |= DateTokenFlags.HasSign;
  180. datetok['-'] |= DateTokenFlags.HasSign;
  181. }
  182. private static bool TryGetWeekday(in DateToken token, byte[] text, out DayOfWeek weekday)
  183. {
  184. weekday = DayOfWeek.Sunday;
  185. if (!token.IsWeekday || token.Length < 3)
  186. return false;
  187. var name = Encoding.ASCII.GetString(text, token.Start, 3);
  188. for (int day = 0; day < WeekDays.Length; day++)
  189. {
  190. if (WeekDays[day].Equals(name, StringComparison.OrdinalIgnoreCase))
  191. {
  192. weekday = (DayOfWeek) day;
  193. return true;
  194. }
  195. }
  196. return false;
  197. }
  198. private static bool TryGetDayOfMonth(in DateToken token, byte[] text, out int day)
  199. {
  200. int endIndex = token.Start + token.Length;
  201. int index = token.Start;
  202. day = 0;
  203. if (!token.IsNumeric)
  204. return false;
  205. if (!ParseUtils.TryParseInt32(text, ref index, endIndex, out day))
  206. return false;
  207. if (day <= 0 || day > 31)
  208. return false;
  209. return true;
  210. }
  211. private static bool TryGetMonth(in DateToken token, byte[] text, out int month)
  212. {
  213. month = 0;
  214. if (!token.IsMonth || token.Length < 3)
  215. return false;
  216. var name = Encoding.ASCII.GetString(text, token.Start, 3);
  217. for (int i = 0; i < Months.Length; i++)
  218. {
  219. if (Months[i].Equals(name, StringComparison.OrdinalIgnoreCase))
  220. {
  221. month = i + 1;
  222. return true;
  223. }
  224. }
  225. return false;
  226. }
  227. private static bool TryGetYear(in DateToken token, byte[] text, out int year)
  228. {
  229. int endIndex = token.Start + token.Length;
  230. int index = token.Start;
  231. year = 0;
  232. if (!token.IsNumeric)
  233. return false;
  234. if (!ParseUtils.TryParseInt32(text, ref index, endIndex, out year))
  235. return false;
  236. if (year < 100)
  237. year += (year < 70) ? 2000 : 1900;
  238. return year >= 1969;
  239. }
  240. private static bool TryGetTimeOfDay(in DateToken token, byte[] text, out int hour, out int minute, out int second)
  241. {
  242. int endIndex = token.Start + token.Length;
  243. int index = token.Start;
  244. hour = minute = second = 0;
  245. if (!token.IsTimeOfDay)
  246. return false;
  247. if (!ParseUtils.TryParseInt32(text, ref index, endIndex, out hour) || hour > 23)
  248. return false;
  249. if (index >= endIndex || text[index++] != (byte) ':')
  250. return false;
  251. if (!ParseUtils.TryParseInt32(text, ref index, endIndex, out minute) || minute > 59)
  252. return false;
  253. // Allow just hh:mm (i.e. w/o the :ss?)
  254. if (index >= endIndex || text[index++] != (byte) ':')
  255. return true;
  256. if (!ParseUtils.TryParseInt32(text, ref index, endIndex, out second) || second > 59)
  257. return false;
  258. return index == endIndex;
  259. }
  260. private static bool TryGetTimeZone(in DateToken token, byte[] text, out int tzone)
  261. {
  262. tzone = 0;
  263. if (token.IsNumericZone)
  264. {
  265. int endIndex = token.Start + token.Length;
  266. int index = token.Start;
  267. int sign;
  268. if (text[index] == (byte) '-')
  269. sign = -1;
  270. else if (text[index] == (byte) '+')
  271. sign = 1;
  272. else
  273. return false;
  274. index++;
  275. if (!ParseUtils.TryParseInt32(text, ref index, endIndex, out tzone) || index != endIndex)
  276. return false;
  277. tzone *= sign;
  278. }
  279. else if (token.IsAlphaZone)
  280. {
  281. if (token.Length > 3)
  282. return false;
  283. var name = Encoding.ASCII.GetString(text, token.Start, token.Length);
  284. if (!timezones.TryGetValue(name, out tzone))
  285. return false;
  286. }
  287. else if (token.IsNumeric)
  288. {
  289. int endIndex = token.Start + token.Length;
  290. int index = token.Start;
  291. if (!ParseUtils.TryParseInt32(text, ref index, endIndex, out tzone) || index != endIndex)
  292. return false;
  293. }
  294. if (tzone < -1200 || tzone > 1400)
  295. return false;
  296. return true;
  297. }
  298. private static bool IsTokenDelimeter(byte c)
  299. {
  300. return c == (byte) '-' || c == (byte) '/' || c == (byte) ',' || c.IsWhitespace();
  301. }
  302. private static IEnumerable<DateToken> TokenizeDate(byte[] text, int startIndex, int length)
  303. {
  304. int endIndex = startIndex + length;
  305. int index = startIndex;
  306. DateTokenFlags mask;
  307. int start;
  308. while (index < endIndex)
  309. {
  310. if (!ParseUtils.SkipCommentsAndWhiteSpace(text, ref index, endIndex, false))
  311. break;
  312. if (index >= endIndex)
  313. break;
  314. // get the initial mask for this token
  315. if ((mask = datetok[text[index]]) != DateTokenFlags.None)
  316. {
  317. start = index++;
  318. // find the end of this token
  319. while (index < endIndex && !IsTokenDelimeter(text[index]))
  320. mask |= datetok[text[index++]];
  321. yield return new DateToken(mask, start, index - start);
  322. }
  323. // skip over the token delimeter
  324. index++;
  325. }
  326. yield break;
  327. }
  328. private static bool TryParseStandardDateFormat(List<DateToken> tokens, byte[] text, out DateTimeOffset date)
  329. {
  330. //bool haveWeekday;
  331. int n = 0;
  332. date = new DateTimeOffset();
  333. // we need at least 5 tokens, 6 if we have a weekday
  334. if (tokens.Count < 5)
  335. return false;
  336. // Note: the weekday is not required
  337. if (TryGetWeekday(tokens[n], text, out _))
  338. {
  339. if (tokens.Count < 6)
  340. return false;
  341. //haveWeekday = true;
  342. n++;
  343. }
  344. if (!TryGetDayOfMonth(tokens[n++], text, out int day))
  345. return false;
  346. if (!TryGetMonth(tokens[n++], text, out int month))
  347. return false;
  348. if (!TryGetYear(tokens[n++], text, out int year))
  349. return false;
  350. if (!TryGetTimeOfDay(tokens[n++], text, out int hour, out int minute, out int second))
  351. return false;
  352. if (!TryGetTimeZone(tokens[n], text, out int tzone))
  353. tzone = 0;
  354. int minutes = tzone % 100;
  355. int hours = tzone / 100;
  356. var offset = new TimeSpan(hours, minutes, 0);
  357. try
  358. {
  359. date = new DateTimeOffset(year, month, day, hour, minute, second, offset);
  360. }
  361. catch (ArgumentOutOfRangeException)
  362. {
  363. return false;
  364. }
  365. return true;
  366. }
  367. #pragma warning disable CA1859
  368. private static bool TryParseUnknownDateFormat(IList<DateToken> tokens, byte[] text, out DateTimeOffset date)
  369. #pragma warning restore CA1859
  370. {
  371. int? day = null, month = null, year = null, tzone = null;
  372. int hour = 0, minute = 0, second = 0;
  373. bool numericMonth = false;
  374. bool haveWeekday = false;
  375. bool haveTime = false;
  376. TimeSpan offset;
  377. for (int i = 0; i < tokens.Count; i++)
  378. {
  379. int value;
  380. if (!haveWeekday && TryGetWeekday(tokens[i], text, out _))
  381. {
  382. haveWeekday = true;
  383. continue;
  384. }
  385. if ((month == null || numericMonth) && TryGetMonth(tokens[i], text, out value))
  386. {
  387. if (numericMonth)
  388. {
  389. numericMonth = false;
  390. day = month;
  391. }
  392. month = value;
  393. continue;
  394. }
  395. if (!haveTime && TryGetTimeOfDay(tokens[i], text, out hour, out minute, out second))
  396. {
  397. haveTime = true;
  398. continue;
  399. }
  400. // Limit TryGetTimeZone to alpha and numeric timezone tokens (do not allow numeric tokens as they are handled below).
  401. if (tzone == null && tokens[i].IsTimeZone && TryGetTimeZone(tokens[i], text, out value))
  402. {
  403. tzone = value;
  404. continue;
  405. }
  406. if (tokens[i].IsNumeric)
  407. {
  408. if (tokens[i].Length == 4)
  409. {
  410. if (year == null)
  411. {
  412. if (TryGetYear(tokens[i], text, out value))
  413. year = value;
  414. }
  415. else if (tzone == null)
  416. {
  417. if (TryGetTimeZone(tokens[i], text, out value))
  418. tzone = value;
  419. }
  420. continue;
  421. }
  422. if (tokens[i].Length > 2)
  423. continue;
  424. // Note: we likely have either YYYY[-/]MM[-/]DD or MM[-/]DD[-/]YY
  425. int endIndex = tokens[i].Start + tokens[i].Length;
  426. int index = tokens[i].Start;
  427. #pragma warning disable CA1806
  428. ParseUtils.TryParseInt32(text, ref index, endIndex, out value);
  429. #pragma warning restore CA1806
  430. if (month == null && value > 0 && value <= 12)
  431. {
  432. numericMonth = true;
  433. month = value;
  434. continue;
  435. }
  436. if (day == null && value > 0 && value <= 31)
  437. {
  438. day = value;
  439. continue;
  440. }
  441. if (year == null && value >= 69)
  442. {
  443. year = 1900 + value;
  444. continue;
  445. }
  446. }
  447. // WTF is this??
  448. }
  449. if (year == null || month == null || day == null)
  450. {
  451. date = new DateTimeOffset();
  452. return false;
  453. }
  454. if (!haveTime)
  455. hour = minute = second = 0;
  456. if (tzone != null)
  457. {
  458. int minutes = tzone.Value % 100;
  459. int hours = tzone.Value / 100;
  460. offset = new TimeSpan(hours, minutes, 0);
  461. }
  462. else
  463. {
  464. offset = new TimeSpan(0);
  465. }
  466. try
  467. {
  468. date = new DateTimeOffset(year.Value, month.Value, day.Value, hour, minute, second, offset);
  469. }
  470. catch (ArgumentOutOfRangeException)
  471. {
  472. date = new DateTimeOffset();
  473. return false;
  474. }
  475. return true;
  476. }
  477. /// <summary>
  478. /// Try to parse the given input buffer into a new <see cref="System.DateTimeOffset"/> instance.
  479. /// </summary>
  480. /// <remarks>
  481. /// Parses an rfc822 date and time from the supplied buffer starting at the given index
  482. /// and spanning across the specified number of bytes.
  483. /// </remarks>
  484. /// <returns><c>true</c>, if the date was successfully parsed, <c>false</c> otherwise.</returns>
  485. /// <param name="buffer">The input buffer.</param>
  486. /// <param name="startIndex">The starting index of the input buffer.</param>
  487. /// <param name="length">The number of bytes in the input buffer to parse.</param>
  488. /// <param name="date">The parsed date.</param>
  489. /// <exception cref="System.ArgumentNullException">
  490. /// <paramref name="buffer"/> is <c>null</c>.
  491. /// </exception>
  492. /// <exception cref="System.ArgumentOutOfRangeException">
  493. /// <paramref name="startIndex"/> and <paramref name="length"/> do not specify
  494. /// a valid range in the byte array.
  495. /// </exception>
  496. public static bool TryParse(byte[] buffer, int startIndex, int length, out DateTimeOffset date)
  497. {
  498. if (buffer == null)
  499. throw new ArgumentNullException(nameof(buffer));
  500. if (startIndex < 0 || startIndex > buffer.Length)
  501. throw new ArgumentOutOfRangeException(nameof(startIndex));
  502. if (length < 0 || length > (buffer.Length - startIndex))
  503. throw new ArgumentOutOfRangeException(nameof(length));
  504. var tokens = new List<DateToken>(TokenizeDate(buffer, startIndex, length));
  505. if (TryParseStandardDateFormat(tokens, buffer, out date))
  506. return true;
  507. if (TryParseUnknownDateFormat(tokens, buffer, out date))
  508. return true;
  509. date = new DateTimeOffset();
  510. return false;
  511. }
  512. /// <summary>
  513. /// Try to parse the given input buffer into a new <see cref="System.DateTimeOffset"/> instance.
  514. /// </summary>
  515. /// <remarks>
  516. /// Parses an rfc822 date and time from the specified text.
  517. /// </remarks>
  518. /// <returns><c>true</c>, if the date was successfully parsed, <c>false</c> otherwise.</returns>
  519. /// <param name="text">The input text.</param>
  520. /// <param name="date">The parsed date.</param>
  521. /// <exception cref="System.ArgumentNullException">
  522. /// <paramref name="text"/> is <c>null</c>.
  523. /// </exception>
  524. public static bool TryParse(string text, out DateTimeOffset date)
  525. {
  526. if (text == null)
  527. throw new ArgumentNullException(nameof(text));
  528. var buffer = Encoding.UTF8.GetBytes(text);
  529. return TryParse(buffer, 0, buffer.Length, out date);
  530. }
  531. }
  532. internal static class ParseUtils
  533. {
  534. public static bool TryParseInt32(byte[] text, ref int index, int endIndex, out int value)
  535. {
  536. int startIndex = index;
  537. value = 0;
  538. while (index < endIndex && text[index] >= (byte) '0' && text[index] <= (byte) '9')
  539. {
  540. int digit = text[index] - (byte) '0';
  541. if (value > int.MaxValue / 10)
  542. {
  543. // integer overflow
  544. return false;
  545. }
  546. if (value == int.MaxValue / 10 && digit > int.MaxValue % 10)
  547. {
  548. // integer overflow
  549. return false;
  550. }
  551. value = (value * 10) + digit;
  552. index++;
  553. }
  554. return index > startIndex;
  555. }
  556. public static bool SkipWhiteSpace(byte[] text, ref int index, int endIndex)
  557. {
  558. int startIndex = index;
  559. while (index < endIndex && text[index].IsWhitespace())
  560. index++;
  561. return index > startIndex;
  562. }
  563. public static bool SkipComment(byte[] text, ref int index, int endIndex)
  564. {
  565. bool escaped = false;
  566. int depth = 1;
  567. index++;
  568. while (index < endIndex && depth > 0)
  569. {
  570. if (text[index] == (byte) '\\')
  571. {
  572. escaped = !escaped;
  573. }
  574. else if (!escaped)
  575. {
  576. if (text[index] == (byte) '(')
  577. depth++;
  578. else if (text[index] == (byte) ')')
  579. depth--;
  580. escaped = false;
  581. }
  582. else
  583. {
  584. escaped = false;
  585. }
  586. index++;
  587. }
  588. return depth == 0;
  589. }
  590. public static bool SkipCommentsAndWhiteSpace(byte[] text, ref int index, int endIndex, bool throwOnError)
  591. {
  592. SkipWhiteSpace(text, ref index, endIndex);
  593. while (index < endIndex && text[index] == (byte) '(')
  594. {
  595. int startIndex = index;
  596. if (!SkipComment(text, ref index, endIndex))
  597. {
  598. if (throwOnError)
  599. {
  600. #pragma warning disable MA0015
  601. throw new ArgumentException($"Incomplete comment token at offset {startIndex}");
  602. #pragma warning restore MA0015
  603. }
  604. return false;
  605. }
  606. SkipWhiteSpace(text, ref index, endIndex);
  607. }
  608. return true;
  609. }
  610. }
  611. [Flags]
  612. internal enum CharType : ushort
  613. {
  614. None = 0,
  615. IsAscii = (1 << 0),
  616. IsAtom = (1 << 1),
  617. IsAttrChar = (1 << 2),
  618. IsBlank = (1 << 3),
  619. IsControl = (1 << 4),
  620. IsDomainSafe = (1 << 5),
  621. IsEncodedPhraseSafe = (1 << 6),
  622. IsEncodedWordSafe = (1 << 7),
  623. IsQuotedPrintableSafe = (1 << 8),
  624. IsSpace = (1 << 9),
  625. IsSpecial = (1 << 10),
  626. IsTokenSpecial = (1 << 11),
  627. IsWhitespace = (1 << 12),
  628. IsXDigit = (1 << 13),
  629. IsPhraseAtom = (1 << 14)
  630. }
  631. internal static class ByteExtensions
  632. {
  633. private const string AtomSafeCharacters = "!#$%&'*+-/=?^_`{|}~";
  634. private const string AttributeSpecials = "*'%"; // attribute specials from rfc2184/rfc2231
  635. private const string DomainSpecials = "[]\\\r \t"; // not allowed in domains
  636. private const string EncodedWordSpecials = "()<>@,;:\"/[]?.=_"; // rfc2047 5.1
  637. private const string EncodedPhraseSpecials = "!*+-/=_"; // rfc2047 5.3
  638. private const string Specials = "()<>[]:;@\\,.\""; // rfc5322 3.2.3
  639. private const string TokenSpecials = "()<>@,;:\\\"/[]?="; // rfc2045 5.1
  640. private const string Whitespace = " \t\r\n";
  641. private static readonly CharType[] table = new CharType[256];
  642. private static void RemoveFlags(string values, CharType bit)
  643. {
  644. for (int i = 0; i < values.Length; i++)
  645. table[(byte) values[i]] &= ~bit;
  646. }
  647. private static void SetFlags(string values, CharType bit, CharType bitcopy, bool remove)
  648. {
  649. int i;
  650. if (remove)
  651. {
  652. for (i = 0; i < 128; i++)
  653. table[i] |= bit;
  654. for (i = 0; i < values.Length; i++)
  655. table[values[i]] &= ~bit;
  656. // Note: not actually used...
  657. //if (bitcopy != CharType.None) {
  658. // for (i = 0; i < 256; i++) {
  659. // if ((table[i] & bitcopy) != 0)
  660. // table[i] &= ~bit;
  661. // }
  662. //}
  663. }
  664. else
  665. {
  666. for (i = 0; i < values.Length; i++)
  667. table[values[i]] |= bit;
  668. if (bitcopy != CharType.None)
  669. {
  670. for (i = 0; i < 256; i++)
  671. {
  672. if ((table[i] & bitcopy) != CharType.None)
  673. table[i] |= bit;
  674. }
  675. }
  676. }
  677. }
  678. static ByteExtensions()
  679. {
  680. for (int i = 0; i < 256; i++)
  681. {
  682. if (i < 127)
  683. {
  684. if (i < 32)
  685. table[i] |= CharType.IsControl;
  686. if (i > 32)
  687. table[i] |= CharType.IsAttrChar;
  688. if ((i >= 33 && i <= 60) || (i >= 62 && i <= 126) || i == 32)
  689. table[i] |= (CharType.IsQuotedPrintableSafe | CharType.IsEncodedWordSafe);
  690. if ((i >= '0' && i <= '9') || (i >= 'a' && i <= 'z') || (i >= 'A' && i <= 'Z'))
  691. table[i] |= CharType.IsEncodedPhraseSafe | CharType.IsAtom | CharType.IsPhraseAtom;
  692. if ((i >= '0' && i <= '9') || (i >= 'a' && i <= 'f') || (i >= 'A' && i <= 'F'))
  693. table[i] |= CharType.IsXDigit;
  694. table[i] |= CharType.IsAscii;
  695. }
  696. else
  697. {
  698. if (i == 127)
  699. table[i] |= CharType.IsAscii;
  700. else
  701. table[i] |= CharType.IsAtom | CharType.IsPhraseAtom;
  702. table[i] |= CharType.IsControl;
  703. }
  704. }
  705. table['\t'] |= CharType.IsQuotedPrintableSafe | CharType.IsBlank;
  706. table[' '] |= CharType.IsSpace | CharType.IsBlank;
  707. SetFlags(Whitespace, CharType.IsWhitespace, CharType.None, false);
  708. SetFlags(AtomSafeCharacters, CharType.IsAtom | CharType.IsPhraseAtom, CharType.None, false);
  709. SetFlags(TokenSpecials, CharType.IsTokenSpecial, CharType.IsControl, false);
  710. SetFlags(Specials, CharType.IsSpecial, CharType.None, false);
  711. SetFlags(DomainSpecials, CharType.IsDomainSafe, CharType.None, true);
  712. RemoveFlags(Specials, CharType.IsAtom | CharType.IsPhraseAtom);
  713. RemoveFlags(EncodedWordSpecials, CharType.IsEncodedWordSafe);
  714. RemoveFlags(AttributeSpecials + TokenSpecials, CharType.IsAttrChar);
  715. SetFlags(EncodedPhraseSpecials, CharType.IsEncodedPhraseSafe, CharType.None, false);
  716. // Note: Allow '[' and ']' in the display-name of a mailbox address
  717. table['['] |= CharType.IsPhraseAtom;
  718. table[']'] |= CharType.IsPhraseAtom;
  719. }
  720. public static bool IsWhitespace(this byte c)
  721. {
  722. return (table[c] & CharType.IsWhitespace) != CharType.None;
  723. }
  724. }