MimeReader.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Net.Mime;
  5. namespace Ant.Service.Utilities
  6. {
  7. /// <summary>
  8. /// This class is responsible for parsing a string array of lines
  9. /// containing a MIME message.
  10. /// </summary>
  11. public class MimeReader
  12. {
  13. private static readonly char[] HeaderWhitespaceChars = new char[] { ' ', '\t' };
  14. private Queue<string> _lines;
  15. /// <summary>
  16. /// Gets the lines.
  17. /// </summary>
  18. /// <value>The lines.</value>
  19. public Queue<string> Lines
  20. {
  21. get
  22. {
  23. return _lines;
  24. }
  25. }
  26. private MimeEntity _entity;
  27. /// <summary>
  28. /// Initializes a new instance of the <see cref="MimeReader"/> class.
  29. /// </summary>
  30. private MimeReader()
  31. {
  32. _entity = new MimeEntity();
  33. }
  34. /// <summary>
  35. /// Initializes a new instance of the <see cref="MimeReader"/> class.
  36. /// </summary>
  37. /// <param name="entity">The entity.</param>
  38. /// <param name="lines">The lines.</param>
  39. private MimeReader(MimeEntity entity, Queue<string> lines)
  40. : this()
  41. {
  42. if (entity == null)
  43. {
  44. throw new ArgumentNullException("entity");
  45. }
  46. if (lines == null)
  47. {
  48. throw new ArgumentNullException("lines");
  49. }
  50. _lines = lines;
  51. _entity = new MimeEntity(entity);
  52. }
  53. /// <summary>
  54. /// Initializes a new instance of the <see cref="MimeReader"/> class.
  55. /// </summary>
  56. /// <param name="lines">The lines.</param>
  57. public MimeReader(string[] lines)
  58. : this()
  59. {
  60. if (lines == null)
  61. {
  62. throw new ArgumentNullException("lines");
  63. }
  64. _lines = new Queue<string>(lines);
  65. }
  66. /// <summary>
  67. /// Parse headers into _entity.Headers NameValueCollection.
  68. /// </summary>
  69. private int ParseHeaders()
  70. {
  71. string lastHeader = string.Empty;
  72. string line = string.Empty;
  73. // the first empty line is the end of the headers.
  74. while (_lines.Count > 0 && !string.IsNullOrEmpty(_lines.Peek()))
  75. {
  76. line = _lines.Dequeue();
  77. //if a header line starts with a space or tab then it is a continuation of the
  78. //previous line.
  79. if (line.StartsWith(" ") || line.StartsWith(Convert.ToString('\t')))
  80. {
  81. _entity.Headers[lastHeader] = string.Concat(_entity.Headers[lastHeader], line);
  82. continue;
  83. }
  84. int separatorIndex = line.IndexOf(':');
  85. if (separatorIndex < 0)
  86. {
  87. System.Diagnostics.Debug.WriteLine("Invalid header:{0}", line);
  88. continue;
  89. } //This is an invalid header field. Ignore this line.
  90. string headerName = line.Substring(0, separatorIndex);
  91. string headerValue = line.Substring(separatorIndex + 1).Trim(HeaderWhitespaceChars);
  92. _entity.Headers.Add(headerName.ToLower(), headerValue);
  93. lastHeader = headerName;
  94. }
  95. if (_lines.Count > 0)
  96. {
  97. _lines.Dequeue();
  98. } //remove closing header CRLF.
  99. return _entity.Headers.Count;
  100. }
  101. /// <summary>
  102. /// Processes mime specific headers.
  103. /// </summary>
  104. /// <returns>A mime entity with mime specific headers parsed.</returns>
  105. private void ProcessHeaders()
  106. {
  107. foreach (string key in _entity.Headers.AllKeys)
  108. {
  109. switch (key)
  110. {
  111. case "content-description":
  112. _entity.ContentDescription = _entity.Headers[key];
  113. break;
  114. case "content-disposition":
  115. _entity.ContentDisposition = new ContentDisposition(_entity.Headers[key]);
  116. break;
  117. case "content-id":
  118. _entity.ContentId = _entity.Headers[key];
  119. break;
  120. case "content-transfer-encoding":
  121. _entity.TransferEncoding = _entity.Headers[key];
  122. _entity.ContentTransferEncoding = MimeReader.GetTransferEncoding(_entity.Headers[key]);
  123. break;
  124. case "content-type":
  125. _entity.SetContentType(MimeReader.GetContentType(_entity.Headers[key]));
  126. break;
  127. case "mime-version":
  128. _entity.MimeVersion = _entity.Headers[key];
  129. break;
  130. }
  131. }
  132. }
  133. /// <summary>
  134. /// Creates the MIME entity.
  135. /// </summary>
  136. /// <returns>A mime entity containing 0 or more children representing the mime message.</returns>
  137. public MimeEntity CreateMimeEntity()
  138. {
  139. try
  140. {
  141. ParseHeaders();
  142. ProcessHeaders();
  143. ParseBody();
  144. SetDecodedContentStream();
  145. return _entity;
  146. }
  147. catch
  148. {
  149. return null;
  150. }
  151. }
  152. /// <summary>
  153. /// Sets the decoded content stream by decoding the EncodedMessage
  154. /// and writing it to the entity content stream.
  155. /// </summary>
  156. /// <param name="entity">The entity containing the encoded message.</param>
  157. private void SetDecodedContentStream()
  158. {
  159. switch (_entity.ContentTransferEncoding)
  160. {
  161. case System.Net.Mime.TransferEncoding.Base64:
  162. _entity.Content = new MemoryStream(Convert.FromBase64String(_entity.EncodedMessage.ToString()), false);
  163. break;
  164. case System.Net.Mime.TransferEncoding.QuotedPrintable:
  165. _entity.Content = new MemoryStream(GetBytes(QuotedPrintableEncoding.Decode(_entity.EncodedMessage.ToString())), false);
  166. break;
  167. case System.Net.Mime.TransferEncoding.SevenBit:
  168. default:
  169. _entity.Content = new MemoryStream(GetBytes(_entity.EncodedMessage.ToString()), false);
  170. break;
  171. }
  172. }
  173. /// <summary>
  174. /// Gets a byte[] of content for the provided string.
  175. /// </summary>
  176. /// <param name="decodedContent">Content.</param>
  177. /// <returns>A byte[] containing content.</returns>
  178. private byte[] GetBytes(string content)
  179. {
  180. using (MemoryStream stream = new MemoryStream())
  181. {
  182. using (StreamWriter writer = new StreamWriter(stream))
  183. {
  184. writer.Write(content);
  185. }
  186. return stream.ToArray();
  187. }
  188. }
  189. /// <summary>
  190. /// Parses the body.
  191. /// </summary>
  192. private void ParseBody()
  193. {
  194. if (_entity.HasBoundary)
  195. {
  196. while (_lines.Count > 0
  197. && !string.Equals(_lines.Peek(), _entity.EndBoundary))
  198. {
  199. /*Check to verify the current line is not the same as the parent starting boundary.
  200. If it is the same as the parent starting boundary this indicates existence of a
  201. new child entity. Return and process the next child.*/
  202. if (_entity.Parent != null
  203. && string.Equals(_entity.Parent.StartBoundary, _lines.Peek()))
  204. {
  205. return;
  206. }
  207. if (string.Equals(_lines.Peek(), _entity.StartBoundary))
  208. {
  209. AddChildEntity(_entity, _lines);
  210. } //Parse a new child mime part.
  211. else if (string.Equals(_entity.ContentType.MediaType, MediaTypes.MessageRfc822, StringComparison.InvariantCultureIgnoreCase)
  212. && string.Equals(_entity.ContentDisposition.DispositionType, DispositionTypeNames.Attachment, StringComparison.InvariantCultureIgnoreCase))
  213. {
  214. /*If the content type is message/rfc822 the stop condition to parse headers has already been encountered.
  215. But, a content type of message/rfc822 would have the message headers immediately following the mime
  216. headers so we need to parse the headers for the attached message now. This is done by creating
  217. a new child entity.*/
  218. AddChildEntity(_entity, _lines);
  219. break;
  220. }
  221. else
  222. {
  223. _entity.EncodedMessage.Append(string.Concat(_lines.Dequeue(), Pop3Commands.Crlf));
  224. } //Append the message content.
  225. }
  226. } //Parse a multipart message.
  227. else
  228. {
  229. while (_lines.Count > 0)
  230. {
  231. _entity.EncodedMessage.Append(string.Concat(_lines.Dequeue(), Pop3Commands.Crlf));
  232. }
  233. } //Parse a single part message.
  234. }
  235. /// <summary>
  236. /// Adds the child entity.
  237. /// </summary>
  238. /// <param name="entity">The entity.</param>
  239. private void AddChildEntity(MimeEntity entity, Queue<string> lines)
  240. {
  241. /*if (entity == null)
  242. {
  243. return;
  244. }
  245. if (lines == null)
  246. {
  247. return;
  248. }*/
  249. MimeReader reader = new MimeReader(entity, lines);
  250. entity.Children.Add(reader.CreateMimeEntity());
  251. }
  252. /// <summary>
  253. /// Gets the type of the content.
  254. /// </summary>
  255. /// <param name="contentType">Type of the content.</param>
  256. /// <returns></returns>
  257. public static ContentType GetContentType(string contentType)
  258. {
  259. if (string.IsNullOrEmpty(contentType))
  260. {
  261. contentType = "text/plain; charset=us-ascii";
  262. }
  263. return new ContentType(contentType);
  264. }
  265. /// <summary>
  266. /// Gets the type of the media.
  267. /// </summary>
  268. /// <param name="mediaType">Type of the media.</param>
  269. /// <returns></returns>
  270. public static string GetMediaType(string mediaType)
  271. {
  272. if (string.IsNullOrEmpty(mediaType))
  273. {
  274. return "text/plain";
  275. }
  276. return mediaType.Trim();
  277. }
  278. /// <summary>
  279. /// Gets the type of the media main.
  280. /// </summary>
  281. /// <param name="mediaType">Type of the media.</param>
  282. /// <returns></returns>
  283. public static string GetMediaMainType(string mediaType)
  284. {
  285. int separatorIndex = mediaType.IndexOf('/');
  286. if (separatorIndex < 0)
  287. {
  288. return mediaType;
  289. }
  290. else
  291. {
  292. return mediaType.Substring(0, separatorIndex);
  293. }
  294. }
  295. /// <summary>
  296. /// Gets the type of the media sub.
  297. /// </summary>
  298. /// <param name="mediaType">Type of the media.</param>
  299. /// <returns></returns>
  300. public static string GetMediaSubType(string mediaType)
  301. {
  302. int separatorIndex = mediaType.IndexOf('/');
  303. if (separatorIndex < 0)
  304. {
  305. if (mediaType.Equals("text"))
  306. {
  307. return "plain";
  308. }
  309. return string.Empty;
  310. }
  311. else
  312. {
  313. if (mediaType.Length > separatorIndex)
  314. {
  315. return mediaType.Substring(separatorIndex + 1);
  316. }
  317. else
  318. {
  319. string mainType = GetMediaMainType(mediaType);
  320. if (mainType.Equals("text"))
  321. {
  322. return "plain";
  323. }
  324. return string.Empty;
  325. }
  326. }
  327. }
  328. /// <summary>
  329. /// Gets the transfer encoding.
  330. /// </summary>
  331. /// <param name="transferEncoding">The transfer encoding.</param>
  332. /// <returns></returns>
  333. /// <remarks>
  334. /// The transfer encoding determination follows the same rules as
  335. /// Peter Huber's article w/ the exception of not throwing exceptions
  336. /// when binary is provided as a transferEncoding. Instead it is left
  337. /// to the calling code to check for binary.
  338. /// </remarks>
  339. public static TransferEncoding GetTransferEncoding(string transferEncoding)
  340. {
  341. switch (transferEncoding.Trim().ToLowerInvariant())
  342. {
  343. case "7bit":
  344. case "8bit":
  345. return System.Net.Mime.TransferEncoding.SevenBit;
  346. case "quoted-printable":
  347. return System.Net.Mime.TransferEncoding.QuotedPrintable;
  348. case "base64":
  349. return System.Net.Mime.TransferEncoding.Base64;
  350. case "binary":
  351. default:
  352. return System.Net.Mime.TransferEncoding.Unknown;
  353. }
  354. }
  355. }
  356. }