PageRenderTime 48ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/src/BugNET.MailboxReader/MailboxReader.cs

#
C# | 481 lines | 324 code | 83 blank | 74 comment | 57 complexity | d3d9bc164be27281924cc51f25f2f7bd MD5 | raw file
Possible License(s): LGPL-2.1, MPL-2.0-no-copyleft-exception
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Linq;
  5. using System.Text;
  6. using System.Text.RegularExpressions;
  7. using BugNET.BLL;
  8. using BugNET.BLL.Notifications;
  9. using BugNET.Common;
  10. using BugNET.Entities;
  11. using HtmlAgilityPack;
  12. using LumiSoft.Net.Log;
  13. using LumiSoft.Net.MIME;
  14. using LumiSoft.Net.Mail;
  15. using LumiSoft.Net.POP3.Client;
  16. using log4net;
  17. namespace BugNET.MailboxReader
  18. {
  19. /// <summary>
  20. /// The second version of the mailbox reader.
  21. /// </summary>
  22. public class MailboxReader
  23. {
  24. static readonly ILog Log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
  25. MailboxReaderConfig Config { get; set; }
  26. /// <summary>
  27. /// Initializes a new instance of the <see cref="MailboxReader"/> class.
  28. /// </summary>
  29. /// <param name="configuration">Options to configure the mail reader</param>
  30. public MailboxReader(IMailboxReaderConfig configuration)
  31. {
  32. Config = configuration as MailboxReaderConfig;
  33. if (configuration == null) throw new ArgumentNullException("configuration");
  34. }
  35. /// <summary>
  36. /// Reads the mail.
  37. /// </summary>
  38. public MailboxReaderResult ReadMail()
  39. {
  40. var result = new MailboxReaderResult();
  41. IList<Project> projects = new List<Project>();
  42. LogInfo("MailboxReader: Begin read mail.");
  43. try
  44. {
  45. using (var pop3Client = new POP3_Client())
  46. {
  47. // configure the logger
  48. pop3Client.Logger = new Logger();
  49. pop3Client.Logger.WriteLog += LogPop3Client;
  50. // connect to the server
  51. pop3Client.Connect(Config.Server, Config.Port, Config.UseSsl);
  52. // authenticate
  53. pop3Client.Login(Config.Username, Config.Password);
  54. // process the messages on the server
  55. foreach (POP3_ClientMessage message in pop3Client.Messages)
  56. {
  57. var mailHeader = Mail_Message.ParseFromByte(message.HeaderToByte());
  58. if (mailHeader != null)
  59. {
  60. var messageFrom = string.Empty;
  61. if (mailHeader.From.Count > 0)
  62. {
  63. messageFrom = string.Join("; ", mailHeader.From.ToList().Select(p => p.Address).ToArray()).Trim();
  64. }
  65. var recipients = mailHeader.To.Mailboxes.Select(mailbox => mailbox.Address).ToList();
  66. if (mailHeader.Cc != null)
  67. {
  68. recipients.AddRange(mailHeader.Cc.Mailboxes.Select(mailbox => mailbox.Address));
  69. }
  70. if (mailHeader.Bcc != null)
  71. {
  72. recipients.AddRange(mailHeader.Bcc.Mailboxes.Select(mailbox => mailbox.Address));
  73. }
  74. // loop through the mailboxes
  75. foreach (var address in recipients)
  76. {
  77. var pmbox = ProjectMailboxManager.GetByMailbox(address);
  78. // cannot find the mailbox skip the rest
  79. if (pmbox == null)
  80. {
  81. LogWarning(string.Format("MailboxReader: could not find project mailbox: {0} skipping.", address));
  82. continue;
  83. }
  84. var project = projects.FirstOrDefault(p => p.Id == pmbox.ProjectId);
  85. if(project == null)
  86. {
  87. project = ProjectManager.GetById(pmbox.ProjectId);
  88. // project is disabled skip
  89. if (project.Disabled)
  90. {
  91. LogWarning(string.Format("MailboxReader: Project {0} - {1} is flagged as disabled skipping.", project.Id, project.Code));
  92. continue;
  93. }
  94. projects.Add(project);
  95. }
  96. var entry = new MailboxEntry
  97. {
  98. Title = mailHeader.Subject.Trim(),
  99. From = messageFrom,
  100. ProjectMailbox = pmbox,
  101. Date = mailHeader.Date,
  102. Project = project,
  103. Content = "Email Body could not be parsed."
  104. };
  105. var mailbody = Mail_Message.ParseFromByte(message.MessageToByte());
  106. if (string.IsNullOrEmpty(mailbody.BodyHtmlText)) // no html must be text
  107. {
  108. entry.Content = mailbody.BodyText.Replace("\n\r", "<br/>").Replace("\r\n", "<br/>").Replace("\r", "");
  109. }
  110. else
  111. {
  112. //TODO: Enhancements could include regular expressions / string matching or not matching
  113. // for particular strings values in the subject or body.
  114. // strip the <body> out of the message (using code from below)
  115. var bodyExtractor = new Regex("<body.*?>(?<content>.*)</body>", RegexOptions.IgnoreCase | RegexOptions.Singleline);
  116. var match = bodyExtractor.Match(mailbody.BodyHtmlText);
  117. var emailContent = match.Success && match.Groups["content"] != null
  118. ? match.Groups["content"].Value
  119. : mailbody.BodyHtmlText;
  120. entry.Content = emailContent.Replace("&lt;", "<").Replace("&gt;", ">");
  121. entry.IsHtml = true;
  122. }
  123. if (Config.ProcessAttachments && project.AllowAttachments)
  124. {
  125. foreach (var attachment in mailbody.GetAttachments(Config.ProcessInlineAttachedPictures).Where(p => p.ContentType != null))
  126. {
  127. entry.MailAttachments.Add(attachment);
  128. }
  129. }
  130. //save this message
  131. SaveMailboxEntry(entry);
  132. // add the entry if the save did not throw any exceptions
  133. result.MailboxEntries.Add(entry);
  134. LogInfo(string.Format(
  135. "MailboxReader: Message #{0} processing finished, found [{1}] attachments, total saved [{2}].",
  136. message.SequenceNumber,
  137. entry.MailAttachments.Count, entry.AttachmentsSavedCount));
  138. // delete the message?.
  139. if (!Config.DeleteAllMessages) continue;
  140. try
  141. {
  142. message.MarkForDeletion();
  143. }
  144. catch (Exception)
  145. {
  146. }
  147. }
  148. }
  149. else
  150. {
  151. LogWarning(string.Format("pop3Client: Message #{0} header could not be parsed.", message.SequenceNumber));
  152. }
  153. }
  154. }
  155. }
  156. catch (Exception ex)
  157. {
  158. LogException(ex);
  159. result.LastException = ex;
  160. result.Status = ResultStatuses.FailedWithException;
  161. }
  162. LogInfo("MailboxReader: End read mail.");
  163. return result;
  164. }
  165. /// <summary>
  166. /// Logs an exception.
  167. /// </summary>
  168. /// <param name="ex">The exception.</param>
  169. /// <returns></returns>
  170. static void LogException(Exception ex)
  171. {
  172. if (Log == null) return;
  173. if (Log.IsErrorEnabled)
  174. Log.Error("Mailbox Reader Error", ex);
  175. }
  176. /// <summary>
  177. /// Logs an information message
  178. /// </summary>
  179. /// <param name="message">The message to log</param>
  180. static void LogInfo(string message)
  181. {
  182. if (Log == null) return;
  183. if (Log.IsInfoEnabled)
  184. Log.Info(message);
  185. }
  186. /// <summary>
  187. /// Logs a warning message
  188. /// </summary>
  189. /// <param name="message"></param>
  190. static void LogWarning(string message)
  191. {
  192. if (Log == null) return;
  193. if (Log.IsWarnEnabled)
  194. Log.Warn(message);
  195. }
  196. /// <summary>
  197. /// Saves the mailbox entry.
  198. /// </summary>
  199. /// <param name="entry">The entry.</param>
  200. void SaveMailboxEntry(MailboxEntry entry)
  201. {
  202. try
  203. {
  204. //load template
  205. var body = string.Format("<div >Sent by:{1} on: {2}<br/>{0}</div>", entry.Content.Trim(), entry.From, entry.Date);
  206. if(Config.BodyTemplate.Trim().Length > 0)
  207. {
  208. var data = new Dictionary<string, object> { { "MailboxEntry", entry } };
  209. body = NotificationManager.GenerateNotificationContent(Config.BodyTemplate, data);
  210. }
  211. var projectId = entry.ProjectMailbox.ProjectId;
  212. var mailIssue = IssueManager.GetDefaultIssueByProjectId(
  213. projectId,
  214. entry.Title.Trim(),
  215. body.Trim(),
  216. entry.ProjectMailbox.AssignToUserName,
  217. Config.ReportingUserName);
  218. if (!IssueManager.SaveOrUpdate(mailIssue)) return;
  219. entry.IssueId = mailIssue.Id;
  220. entry.WasProcessed = true;
  221. var project = ProjectManager.GetById(projectId);
  222. var projectFolderPath = Path.Combine(Config.UploadsFolderPath, project.UploadPath);
  223. var doc = new HtmlDocument();
  224. doc.LoadHtml(mailIssue.Description); // load the issue body to we can process it for inline images (if exist)
  225. //If there is an attached file present then add it to the database
  226. //and copy it to the directory specified in the web.config file
  227. foreach (MIME_Entity mimeEntity in entry.MailAttachments)
  228. {
  229. string fileName;
  230. var isInline = false;
  231. var contentType = mimeEntity.ContentType.Type.ToLower();
  232. var attachment = new IssueAttachment
  233. {
  234. Id = 0,
  235. Description = "File attached by mailbox reader",
  236. DateCreated = DateTime.Now,
  237. ContentType = mimeEntity.ContentType.TypeWithSubtype,
  238. CreatorDisplayName = Config.ReportingUserName,
  239. CreatorUserName = Config.ReportingUserName,
  240. IssueId = mailIssue.Id,
  241. ProjectFolderPath = projectFolderPath
  242. };
  243. switch (contentType)
  244. {
  245. case"application":
  246. attachment.Attachment = ((MIME_b_SinglepartBase)mimeEntity.Body).Data;
  247. break;
  248. case "attachment":
  249. case "image":
  250. case "video":
  251. case "audio":
  252. attachment.Attachment = ((MIME_b_SinglepartBase) mimeEntity.Body).Data;
  253. break;
  254. case"message":
  255. // we need to pull the actual email message out of the entity, and strip the "content type" out so that
  256. // email programs will read the file properly
  257. var messageBody = mimeEntity.ToString().Replace(mimeEntity.Header.ToString(), "");
  258. if (messageBody.StartsWith("\r\n"))
  259. {
  260. messageBody = messageBody.Substring(2);
  261. }
  262. attachment.Attachment = Encoding.UTF8.GetBytes(messageBody);
  263. break;
  264. default:
  265. LogWarning(string.Format("MailboxReader: Attachment type could not be processed {0}", mimeEntity.ContentType.Type));
  266. break;
  267. }
  268. if (contentType.Equals("attachment")) // this is an attached email
  269. {
  270. fileName = mimeEntity.ContentDisposition.Param_FileName;
  271. }
  272. else if (contentType.Equals("message")) // message has no filename so we create one
  273. {
  274. fileName = string.Format("Attached_Message_{0}.eml", entry.AttachmentsSavedCount);
  275. }
  276. else
  277. {
  278. isInline = true;
  279. fileName = string.IsNullOrWhiteSpace(mimeEntity.ContentType.Param_Name) ?
  280. string.Format("untitled.{0}", mimeEntity.ContentType.SubType) :
  281. mimeEntity.ContentType.Param_Name;
  282. }
  283. attachment.FileName = fileName;
  284. var saveFile = IsAllowedFileExtension(fileName);
  285. var fileSaved = false;
  286. // can we save the file?
  287. if(saveFile)
  288. {
  289. fileSaved = IssueAttachmentManager.SaveOrUpdate(attachment);
  290. if (fileSaved)
  291. {
  292. entry.AttachmentsSavedCount++;
  293. }
  294. else
  295. {
  296. LogWarning("MailboxReader: Attachment could not be saved, please see previous logs");
  297. }
  298. }
  299. if (!entry.IsHtml || !isInline) continue;
  300. if (string.IsNullOrWhiteSpace(mimeEntity.ContentID)) continue;
  301. var contentId = mimeEntity.ContentID.Replace("<", "").Replace(">", "").Replace("[", "").Replace("]", "");
  302. // this is pretty greedy but since people might be sending screenshots I doubt they will send in dozens of images
  303. // embedded in the email. one would hope
  304. foreach (var node in doc.DocumentNode.SelectNodes(XpathElementCaseInsensitive("img")).ToList())
  305. {
  306. var attr = node.Attributes.FirstOrDefault(p => p.Name.ToLowerInvariant() == "src");// get the src attribute
  307. if (attr == null) continue; // image has no src attribute
  308. if (!attr.Value.Contains(contentId)) continue; // is the attribute value the content id?
  309. // swap out the content of the parent node html will our link to the image
  310. var anchor = string.Format("<span class='inline-mail-attachment'>Inline Attachment: <a href='DownloadAttachment.axd?id={0}' target='_blank'>{1}</a></span>", attachment.Id, fileName);
  311. // for each image in the body if the file was saved swap out the inline link for a link to the saved attachment
  312. // otherwise blank out the content link so we don't get a missing image link
  313. node.ParentNode.InnerHtml = fileSaved ? anchor : "";
  314. }
  315. mailIssue.Description = doc.DocumentNode.InnerHtml;
  316. mailIssue.LastUpdateUserName = mailIssue.OwnerUserName;
  317. mailIssue.LastUpdate = DateTime.Now;
  318. IssueManager.SaveOrUpdate(mailIssue);
  319. }
  320. }
  321. catch (Exception ex)
  322. {
  323. LogException(ex);
  324. throw;
  325. }
  326. }
  327. /// <summary>
  328. /// Check if the file has an allowed file extension
  329. /// </summary>
  330. /// <param name="fileName">The file to check</param>
  331. /// <returns>True if the file is allowed, otherwise false</returns>
  332. bool IsAllowedFileExtension(string fileName)
  333. {
  334. fileName = fileName.Trim().ToLower();
  335. var allowedExtensions = Config.AllowedFileExtensions.ToLower().Trim();
  336. if (allowedExtensions.Length.Equals(0)) return false; // nothing saved so allow nothing
  337. var allowedFileTypes = allowedExtensions.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
  338. // allow all types
  339. if (allowedFileTypes.FirstOrDefault(p => p == "*.*") != null) return true;
  340. if (allowedFileTypes.FirstOrDefault(p => p == ".") != null) return true;
  341. // match extension in allowed extensions list
  342. var allowed = allowedFileTypes.Select(allowedFileType => allowedFileType.Replace("*", "")).Any(fileType => fileName.EndsWith(fileType));
  343. if(!allowed)
  344. {
  345. LogWarning(string.Format("MailboxReader: Attachment {0} was not one of the allowed attachment extensions {1} skipping.", fileName, Config.AllowedFileExtensions.ToLower()));
  346. }
  347. return allowed;
  348. }
  349. /// <summary>
  350. /// change the xpath element name to all uppercase
  351. /// </summary>
  352. /// <param name="elementName">The element name</param>
  353. /// <returns></returns>
  354. static string XpathElementCaseInsensitive(string elementName)
  355. {
  356. //*[translate(name(), 'abc','ABC')='ABC']"
  357. return string.Format("//*[translate(name(), '{0}', '{1}') = '{1}']", elementName.ToLower(), elementName.ToUpper());
  358. }
  359. /// <summary>
  360. /// Log the pop 3 client events
  361. /// </summary>
  362. /// <param name="sender"></param>
  363. /// <param name="e"></param>
  364. static void LogPop3Client(object sender, WriteLogEventArgs e)
  365. {
  366. try
  367. {
  368. var message = "";
  369. switch (e.LogEntry.EntryType)
  370. {
  371. case LogEntryType.Read:
  372. message = string.Format("pop3Client: {0} >> {1}", ObjectToString(e.LogEntry.RemoteEndPoint), e.LogEntry.Text);
  373. break;
  374. case LogEntryType.Write:
  375. message = string.Format("pop3Client: {0} << {1}", ObjectToString(e.LogEntry.RemoteEndPoint), e.LogEntry.Text);
  376. break;
  377. case LogEntryType.Text:
  378. message = string.Format("pop3Client: {0} xx {1}", ObjectToString(e.LogEntry.RemoteEndPoint), e.LogEntry.Text);
  379. break;
  380. }
  381. LogInfo(message);
  382. }
  383. catch (Exception ex)
  384. {
  385. LogException(ex);
  386. }
  387. }
  388. /// <summary>
  389. /// Calls obj.ToSting() if o is not null, otherwise returns "".
  390. /// </summary>
  391. /// <param name="o">Object.</param>
  392. /// <returns>Returns obj.ToSting() if o is not null, otherwise returns "".</returns>
  393. static string ObjectToString(object o)
  394. {
  395. return o == null ? "" : o.ToString();
  396. }
  397. }
  398. }