public void Extract() { var utcnow = DateTime.UtcNow; // Obtain the mailbox token of the recipient var mbox = _conn.QuerySingleOrDefault <MailboxDTO>("select Id,Token,Address,ExpiresOn from MailBox where Address = @Recipient", _m); if (mbox == null) { Console.WriteLine(string.Format("Could not find mailbox for {0}", _m.Recipient)); return; } // Create mail entry var mail = new MailDTO() { IdMailBox = mbox.Id, ReceivedOn = _m.ReceivedOn, Size = _m.ContentSize }; mail.Id = _conn.QuerySingle <int>("insert into Mail (IdMailBox,ReceivedOn,Size) values (@IdMailBox,@ReceivedOn,@Size) returning Id;", mail); // Create mailbox directory structure var mailboxDir = Path.Combine(Configuration.Instance.MailboxDirectory, mbox.Token.ToString()); var mailDir = Path.Combine(mailboxDir, mail.Id.ToString(CultureInfo.InvariantCulture)); var mailInlineDir = Path.Combine(mailDir, InlineSubDirectory); var mailAttachmentDir = Path.Combine(mailDir, AttachmentSubDirectory); Directory.CreateDirectory(mailboxDir); Directory.CreateDirectory(mailDir); Directory.CreateDirectory(mailInlineDir); Directory.CreateDirectory(mailAttachmentDir); var me = new MailEntry(); var mailFilename = _m.GetContentFileName(); using (var mailStream = new FileStream(mailFilename, FileMode.Open, FileAccess.Read, FileShare.Read)) { // Parse MIME message var msg = MimeMessage.Load(ParserOptions.Default, mailStream, true); me.Id = mail.Id; me.ReceivedOn = _m.ReceivedOn; me.Sender = _m.Sender; me.Subject = msg.Subject; me.Size = _m.ContentSize; // Generate HTML preview and save inline images var preview = new HtmlPreviewVisitor(mailInlineDir, InlineSubDirectory + '/'); msg.Accept(preview); // Save sanitized mail content using (var fs = new FileStream(Path.Combine(mailDir, "mail.html"), FileMode.Create, FileAccess.Write, FileShare.None)) using (var sw = new StreamWriter(fs, Encoding.UTF8)) { var html = preview.HtmlBody; // Plug js auto sizer at end of <head>, end of <body> or just append it (sometimes, we only get HTML snippet withou body). var js = "<script src=\"/js/iframeResizer.contentWindow.min.js\" async></script>"; int idx = html.IndexOf("</head>", StringComparison.OrdinalIgnoreCase); if (idx == -1) { idx = html.IndexOf("</body>", StringComparison.OrdinalIgnoreCase); } if (idx != -1) { html = html.Insert(idx, js); sw.Write(html); } else { // sometimes all we get is an html snippet sw.Write(js); sw.Write(html); } } // Save attachments foreach (var attachment in preview.Attachments) { // TODO: cleanup filename var dirtyName = attachment.ContentDisposition.FileName; var cleanName = CleanupFilename(dirtyName); me.Attachments.Add(new MailEntry.Attachment() { OriginalFileName = dirtyName, FileName = cleanName }); using (var attachmentStream = new FileStream(Path.Combine(mailAttachmentDir, cleanName), FileMode.Create, FileAccess.Write, FileShare.None)) { if (attachment is MessagePart) { var part = (MessagePart)attachment; part.Message.WriteTo(attachmentStream); } else { var part = (MimePart)attachment; part.ContentObject.DecodeTo(attachmentStream); } } } } // Load existing mails to insert new mail var mails = new List <MailEntry>(); string jsonFile = Path.Combine(mailboxDir, "mails.json"); if (File.Exists(jsonFile)) { using (var fs = new FileStream(jsonFile, FileMode.Open, FileAccess.Read, FileShare.Read)) using (var reader = new StreamReader(fs, Encoding.UTF8)) { var content = reader.ReadToEnd(); mails = JsonConvert.DeserializeObject <List <MailEntry> >(content); } } mails.Add(me); // mail.json & mail.json.gz var json = JsonConvert.SerializeObject(mails); using (var fs = new FileStream(jsonFile, FileMode.Create, FileAccess.Write, FileShare.None)) using (var writer = new StreamWriter(fs, Encoding.UTF8)) { writer.Write(json); } Touch(jsonFile, utcnow); using (var fs = new FileStream(jsonFile + ".gz", FileMode.Create, FileAccess.Write, FileShare.None)) using (var gzip = new GZipStream(fs, CompressionLevel.Fastest)) using (var writer = new StreamWriter(gzip, Encoding.UTF8)) { writer.Write(json); } Touch(jsonFile + ".gz", utcnow); Gzipify(mailDir, utcnow); }
private void HandleCompleteLine(ArraySegment <byte> line) { var isHandled = false; var nextState = _state; byte[] response = null; ArraySegment <byte> cmd = _emptyBuffer; var tokens = new ArraySegment <byte>[] { }; if (_state != SmtpState.WaitingForEndOfData) { tokens = Bytes.Split(line, Whitespace); if (tokens.Length > 0) { Bytes.ToUpper(tokens[0]); cmd = tokens[0]; } } if (Bytes.IsSame(RSET, cmd)) { isHandled = true; if (tokens.Length == 1) { ResetMailInfo(); response = ReplyOk; nextState = SmtpState.WaitingForHelo; } else { response = ReplySyntaxErrorRset; } } else if (_state == SmtpState.WaitingForHelo && Bytes.IsSame(EHLO, cmd)) { isHandled = true; if (tokens.Length == 2) { response = ReplyEhlo; nextState = SmtpState.WaitingForMailFrom; } else { response = ReplySyntaxErrorEhlo; } } else if (_state == SmtpState.WaitingForHelo && Bytes.IsSame(HELO, cmd)) { isHandled = true; if (tokens.Length == 2) { response = ReplyHelo; nextState = SmtpState.WaitingForMailFrom; } else { response = ReplySyntaxErrorHelo; } } else if (_state == SmtpState.WaitingForMailFrom && Bytes.IsSame(MAIL, cmd)) { isHandled = true; // extract mail address between < > after : bool go = false; int lt = -1, gt = -1; for (var i = line.Offset; i < line.Offset + line.Count; i++) { byte ch = line.Array[i]; if (go) { if (lt == -1 && ch == LessThan) { lt = i; } if (lt != -1 && ch == GreaterThan) { gt = i; break; } } else if (ch == SemiColon) { go = true; } } if (lt != -1 && gt != -1) { // sender found, _sender = Encoding.ASCII.GetString(line.Array, lt + 1, gt - lt - 1).Trim(); // check for optional SIZE=nnnnnnn extension after the sender int sizeOffset = Bytes.OffsetOf(MessageSizeExtension, line.Array, gt + 1, line.Count - gt - 1); if (sizeOffset != -1) { int valueBeginOffset = sizeOffset + MessageSizeExtension.Length; int valueEndOffset = -1; for (var i = valueBeginOffset; i < line.Offset + line.Count; i++) { byte ch = line.Array[i]; byte digit = Bytes.Digits[ch]; if (Whitespace.Contains(ch)) { break; } else if (digit > 9 || digit < 0) { valueEndOffset = -1; break; } valueEndOffset = i; } if (valueEndOffset >= valueBeginOffset) { int size = 0, scale = 1; for (var i = valueEndOffset; i >= valueBeginOffset && size < MaximumMessageSize; i--, scale *= 10) { size += Bytes.Digits[line.Array[i]] * scale; } if (size > MaximumMessageSize) { response = ReplyMessageSizeTooBig; nextState = SmtpState.WaitingForRset; } else { _mailContentExpectedSize = size; response = ReplyOk; nextState = SmtpState.WaitingForRcptTo; } } else { response = ReplySyntaxErrorMail; } } else { // no size extension response = ReplyOk; nextState = SmtpState.WaitingForRcptTo; } } else { response = ReplySyntaxErrorMail; } } else if ((_state == SmtpState.WaitingForRcptTo || _state == SmtpState.WaitingForAdditionalRcptTo) && Bytes.IsSame(RCPT, cmd)) { // standard deviation: send only to the last given recipient even if not pretending so. isHandled = true; // extract mail address between < > after : bool go = false; int lt = -1, gt = -1; for (var i = line.Offset; i < line.Offset + line.Count; i++) { byte ch = line.Array[i]; if (go) { if (lt == -1 && ch == LessThan) { lt = i; } if (lt != -1 && ch == GreaterThan) { gt = i; break; } } else if (ch == SemiColon) { go = true; } } if (lt != -1 && gt != -1) { var recipient = Encoding.ASCII.GetString(line.Array, lt + 1, gt - lt - 1).Trim().ToLowerInvariant(); if (_server.MailDispatcher.IsMailboxActive(recipient)) { _lastRecipient = recipient; response = ReplyOk; nextState = SmtpState.WaitingForAdditionalRcptTo; } else { response = ReplyNoSuchUserHere; } } else { response = ReplySyntaxErrorRcpt; } } else if (_state == SmtpState.WaitingForAdditionalRcptTo && Bytes.IsSame(DATA, cmd)) { isHandled = true; if (tokens.Length == 1) { response = ReplyStartMail; if (_mailContent == null) { // presize stream to avoid garbage & extra reallocation if expected size makes sense. // add a bit of slack for size errors var size = (_mailContentExpectedSize > 0 && _mailContentExpectedSize <= MaximumMessageSize) ? _mailContentExpectedSize + 1024 : 4096; _mailContentId = Guid.NewGuid(); _mailContent = new FileStream(IncomingMailDTO.GetContentFileName(_mailContentId), FileMode.CreateNew, FileAccess.Write, FileShare.None, 16384); } nextState = SmtpState.WaitingForEndOfData; } else { response = ReplySyntaxErrorData; } } else if (_state == SmtpState.WaitingForEndOfData) { isHandled = true; if (line.Count == EndOfMail.Length && Bytes.StartsWith(EndOfMail, line)) { if (_mailContent.Length <= MaximumMessageSize) { response = ReplyOk; nextState = SmtpState.WaitingForMailFrom; _mailContent.Flush(); _server.MailDispatcher.Enqueue(new IncomingMailDTO(DateTime.UtcNow, _lastRecipient, _sender, (int)_mailContent.Length, _mailContentId)); _mailContent.Dispose(); _mailContent = null; } else { response = ReplyMessageSizeTooBig; nextState = SmtpState.WaitingForRset; } ResetMailInfo(); } else { // accumulate mail body & handle dot unstuffing (while msg size doesn't exceed maximum size, since it will be discarded later anyway). int offset = line.Offset; int count = line.Count; if (line.Array[offset] == Dot) { offset++; count--; } if (_mailContent != null && _mailContent.Length + count <= MaximumMessageSize) { _mailContent.Write(line.Array, offset, count); } } } else if (Bytes.IsSame(QUIT, cmd)) { isHandled = true; if (tokens.Length == 1) { ResetMailInfo(); response = ReplyBye; nextState = SmtpState.Disconnect; } else { response = ReplySyntaxErrorQuit; } } else if (Bytes.IsSame(VRFY, cmd) || Bytes.IsSame(EXPN, cmd)) { isHandled = true; response = ReplyCommandNotImplemented; } else if (Bytes.IsSame(HELP, cmd)) { isHandled = true; response = ReplyHelp; } else if (Bytes.IsSame(NOOP, cmd)) { isHandled = true; response = ReplyOk; } if (!isHandled) { var isKnownCommand = KnownCommands.Any((knownCmd) => Bytes.IsSame(knownCmd, cmd)); response = isKnownCommand ? ReplyBadSequence : ReplyUnknownCommand; } _state = nextState; if (response != null && response.Length > 0) { var e = CreateArgs(SendCompleted, response); if (!_socket.SendAsync(e)) { SendCompleted(_socket, e); } } }