In my application users can upload emails to a file server, these emails should then be stripped of their attachments and converted into a PDF to be saved individually. But I'm having problems correctly removing the attachments from the email.
When I'm converting an email and saving it with its attachments it works perfectly, but when I remove the attachments first and then save it, it somehow corrupts the generated PDF file making it look like this (this is just one of 4 pages generated from a 4 line email). Can anyone explain what I'm doing wrong?
This is my code to remove the attachments from the email:
public static List<(string FileName, Stream Content)> GetEmailAndAttachmentsFromEmail(Stream emailContent)
{
var email = MailMessage.Load(emailContent);
var retval = new List<(string, Stream)>
{
($"{email.Subject}.msg", emailContent)
};
var attachmentsToRemove = new List<Attachment>();
foreach (var attachment in email.Attachments)
{
retval.Add((attachment.Name, attachment.ContentStream));
attachmentsToRemove.Add(attachment);
}
foreach (var attachment in attachmentsToRemove)
{
email.Attachments.Remove(attachment);
}
return retval;
}
I've already tried multiple permutations of this code, but none worked.
Also, I'm following the official Aspose documentation on this subject and I don't see what I'm doing differently/ wrong.
It turns out I did something funky with my streams and I had to save my email without the attachments before returning it, here is my revised code:
public static List<(string FileName, Stream Content)> GetEmailAndAttachmentsFromEmail(Stream emailContent)
{
var email = MailMessage.Load(emailContent);
// I removed the prepending of the email here and moved it to the end
var retval = new List<(string, Stream)>();
var attachmentsToRemove = new List<Attachment>();
foreach (var attachment in email.Attachments)
{
retval.Add((attachment.Name, attachment.ContentStream));
attachmentsToRemove.Add(attachment);
}
foreach (var attachment in attachmentsToRemove)
{
email.Attachments.Remove(attachment);
}
// This part is new
var newEmailContent = new MemoryStream();
email.Save(newEmailContent);
newEmailContent.Seek(0, SeekOrigin.Begin);
retval = retval.Prepend(($"{email.Subject}.msg", newEmailContent)).ToList();
return retval;
}
It now works like a charm
Related
I have a RibbonXML which provide a context menu that takes an action on a received email. That email contains a spreadsheet attachment, and I want to update that spreadsheet (xlsx) and forward it to another recipient...
What is happening is that the recipient see two attachments, one being usually quite small (a few KB), and the other being the correct attachment. This does the same thing with pdf, or text files, so pretty sure it isn't the file type. It shows up in the attachments list in the email inspector, but if you try to do anything with it Outlook says the attachment cannot be found.
I built up the test from scratch and added in components of my solution until the error was found. It seems to be related to removing attachments from the new email item (resulting from the Forward() method).
public void OnTestAttachment(Office.IRibbonControl control)
{
if (control.Context is Selection)
{
Selection selection = control.Context as Selection;
if (selection.Count == 1)
{
object item = selection[1];
if (item is MailItem)
{
MailItem mailItem = item as MailItem;
var newItem = mailItem.Forward();
newItem.Recipients.Add("xxxxx#xxxxx.com");
var newAttachments = newItem.Attachments;
// remove the line below and I don't see the issue
for (int i = newAttachments.Count; i >= 1; i--) { newAttachments.Remove(i); }
{
var body = "Testing.....\r\n";
MSWord.Document document = (MSWord.Document)newItem.GetInspector.WordEditor;
MSWord.Paragraph paragraph = document.Paragraphs.Add(document.Range());
paragraph.Range.Text = body;
}
// do some things with a file..in prod I save the existing file, edit it via code and then save it back down....
// I tested using a byte stream in case that has something to do with the issue (as that's the closest
// match to what is actually going on in prod)
var fileName = #"C:\Users\<me>\Test.xlsx";
var temp = Path.Combine(Path.GetTempPath(), Path.GetFileName(fileName));
byte[] buffer = File.ReadAllBytes(fileName);
using (var stream = new FileStream(temp, FileMode.Create, FileAccess.Write))
{
stream.Write(buffer, 0, buffer.Length);
stream.Close();
}
newAttachments.Add(temp, Microsoft.Office.Interop.Outlook.OlAttachmentType.olByValue, Type.Missing, Type.Missing);
// the issues doesn't appear here...it is only once it is received that it appears
newItem.Display();
Marshal.ReleaseComObject(newItem);
Marshal.ReleaseComObject(newAttachments);
Marshal.ReleaseComObject(mailItem);
}
}
Marshal.ReleaseComObject(selection);
}
It shows up in the attachments list in the email inspector, but if you try to do anything with it Outlook says the attachment cannot be found.
The Outlook object model is not related to the issue described. The file can be overwritten with an empty data and then can be attached to the email:
using (var stream = new FileStream(temp, FileMode.Create, FileAccess.Write))
{
stream.Write(buffer, 0, buffer.Length);
stream.Close();
}
The following sentence confirms this:
What is happening is that the recipient see two attachments, one being usually quite small (a few KB), and the other being the correct attachment.
If you need to cope the file content I'd suggest saving the file using the SaveAsFile method of the Attachment class which saves the attachment to the specified path. Thus you can be sure the file content is not empty.
I have folder full of *.msg files saved from Outlook and I'm trying to convert them to Word.
There is a loop that loads each *.msg as MailItem and saves them.
public static ConversionResult ConvertEmailsToWord(this Outlook.Application app, string source, string target)
{
var word = new Word.Application();
var emailCounter = 0;
var otherCounter = 0;
var directoryTree = new PhysicalDirectoryTree();
foreach (var node in directoryTree.Walk(source))
{
foreach (var fileName in node.FileNames)
{
var currentFile = Path.Combine(node.DirectoryName, fileName);
var branch = Regex.Replace(node.DirectoryName, $"^{Regex.Escape(source)}", string.Empty).Trim('\\');
Debug.Print($"Processing file: {currentFile}");
// This is an email. Convert it to Word.
if (Regex.IsMatch(fileName, #"\.msg$"))
{
if (app.Session.OpenSharedItem(currentFile) is MailItem item)
{
if (item.SaveAs(word, Path.Combine(target, branch), fileName))
{
emailCounter++;
}
item.Close(SaveMode: OlInspectorClose.olDiscard);
}
}
// This is some other file. Copy it as is.
else
{
Directory.CreateDirectory(Path.Combine(target, branch));
File.Copy(currentFile, Path.Combine(target, branch, fileName), true);
otherCounter++;
}
}
}
word.Quit(SaveChanges: false);
return new ConversionResult
{
EmailCount = emailCounter,
OtherCount = otherCounter
};
}
The save method looks likes this:
public static bool SaveAs(this MailItem mail, Word.Application word, string path, string name)
{
Directory.CreateDirectory(path);
name = Path.Combine(path, $"{Path.GetFileNameWithoutExtension(name)}.docx");
if (File.Exists(name))
{
return false;
}
var copy = mail.GetInspector.WordEditor as Word.Document;
copy.Content.Copy();
var doc = word.Documents.Add();
doc.Content.Paste();
doc.SaveAs2(FileName: name);
doc.Close();
return true;
}
It works for most *.msg files but there are some that crash Outlook when I call copy.Content on a Word.Document.
I know you cannot tell me what is wrong with it (or maybe you do?) so I'd like to findit out by myself but the problem is that I am not able to catch the exception. Since a simple try\catch didn't work I tried it with AppDomain.CurrentDomain.UnhandledException this this didn't catch it either.
Are there any other ways to debug it?
The mail that doesn't let me get its content inside a loop doesn't cause any troubles when I open it in a new Outlook window and save it with the same method.
It makes sense to add some delays between Word calls. IO operations takes some time to finish. Also there is no need to create another document in Word for copying the content:
var copy = mail.GetInspector.WordEditor as Word.Document;
copy.Content.Copy();
var doc = word.Documents.Add();
doc.Content.Paste();
doc.SaveAs2(FileName: name);
doc.Close();
Instead, do the required modifications on the original document instance and then save it to the disk. The original mail item will remain unchanged until you call the Save method from the Outlook object model. You may call the Close method passing the olDiscard which discards any changes to the document.
Also consider using the Open XML SDK if you deal with open XML documents only, see Welcome to the Open XML SDK 2.5 for Office for more information.
Do you actually need to use Inspector.WordEditor? You can save the message in a format supported by Word (such as MHTML) using OOM alone by calling MailItem.Save(..., olMHTML) and open the file in Word programmatically to save it in the DOCX format.
Context:
I am able to copy (using CTRL-C) and paste programmatically a file from the clipboard when the file copied to the clipboard is for example a file on the desktop. This is simple enough using the following syntax:
File.Copy(Clipboard.GetFileDropList()[0], savePath)
where Clipboard.GetFileDropList()[0] returns the path of the copied file and savePath is the paste location.
However, i find that the above syntax does NOT work if the copied file (using CTRL-C) is a file attachment in an Outlook email. In that scenario, Clipboard.ContainsFileDropList() returns false and Clipboard.GetFileDropList()[0] results in the following error message:
"ArgumentOutOfRangeException: Index was out of range. Must be non-negative and less than the size of the collection. Parameter name: index"
This is despite the fact that pressing CTRL-V does successfully paste the file, confirming that the file was initially successfully copied to the clipboard.
Question:
Sorry if i missed something very basic. My question is how to programmatically paste/save an email attachment (PDF, Word, etc...) from the clipboard to a file location when that email attachment was copied into the clipboard using CTRL-C from within Outlook.
Note that i do understand that what i am trying to do can be solved by skipping the Clipboard and interacting programmatically with Outlook to access a selected email attachment. However, my goal here is to learn how to interact programmatically with the Clipboard under different scenario.
You are using the wrong DataFormat. You can always get a list of currently present data formats by calling Clipboard.GetDataObject().GetFormats().
You need to use:
"FileGroupDescriptor" to retrieve the file names
private static async Task<List<string>> GetAttachedFileNamesFromClipboardAsync(IDataObject clipboardData)
{
if (!clipboardData.GetDataPresent("FileGroupDescriptor"))
{
return new List<string>();
}
using (var descriptorStream = clipboardData.GetData("FileGroupDescriptor", true) as MemoryStream)
{
using (var streamReader = new StreamReader(descriptorStream))
{
var streamContent = await streamReader.ReadToEndAsync();
string[] fileNames = streamContent.Split(new[] { '\0' }, StringSplitOptions.RemoveEmptyEntries);
return new List<string>(fileNames.Skip(1));
}
}
}
"FileContents" to retrieve the raw file contents
// Returns the attachment file content as string
private static async Task<string> GetAttachmentFromClipboardAsync(IDataObject clipboardData)
{
if (!clipboardData.GetDataPresent("FileContents"))
{
return string.Empty;
}
using (var fileContentStream = clipboardData.GetData("FileContents", true) as MemoryStream)
{
using (var streamReader = new StreamReader(fileContentStream))
{
return await streamReader.ReadToEndAsync();
}
}
}
// Returns the attachment file content as MemoryStream
private static MemoryStream GetAttachmentFromClipboard(IDataObject clipboardData)
{
if (!clipboardData.GetDataPresent("FileContents"))
{
return null;
}
return clipboardData.GetData("FileContents", true) as MemoryStream;
}
Save attached file to disk
Since Windows only adds the first selected attachment to the system clipboard, this solution can only save a single attachment. Apparently the office clipboard is not accessible.
private static async Task SaveAttachmentFromClipboardToFileAsync(IDataObject clipboardData, string destinationFilePath)
{
if (!clipboardData.GetDataPresent("FileContents"))
{
return;
}
using (var attachedFileStream = clipboardData.GetData("FileContents", true) as MemoryStream)
{
using (var destinationFileStream = File.Open(destinationFilePath, FileMode.OpenOrCreate))
{
await attachedFileStream.CopyToAsync(destinationFileStream);
}
}
}
Save attached files to disk using the Office API
Requires to reference Microsoft.Office.Interop.Outlook.dll. This solution does not depend on the system clipboard. It just reaads the selected attachments from the currently open message item in the Outlook explorer.
You still can monitor the system clipboard to trigger the process to save the attachments.
private static void SaveSelectedAttachementsToFolder(string destinationFolderPath)
{
var outlookApplication = new Microsoft.Office.Interop.Outlook.Application();
Explorer activeOutlookExplorer = outlookApplication.ActiveExplorer();
AttachmentSelection selectedAttachments = activeOutlookExplorer.AttachmentSelection;
foreach(Attachment attachment in selectedAttachments)
{
attachment.SaveAsFile(Path.Combine(destinationFolderPath, attachment.FileName));
}
}
I am trying to send an email with an attachment by accessing it directly after saving it in the database. To do so I am following this tutorial.
What works?
Storing the attachments in the database is correct as when I go to the details page I can see the image associated with the profile.
What doesn't?
Unfortunately there seems to be a problem with how retrieving files from database works as the attachments are damaged, e.g. image stored in database shows 153328 B, but when sent turns into 117B).
The solution that actually succeeds and sends an email (damaged email) is taken from this link, but when I try to send it using the commented out stream code, the code crashes on the indicated line:
protected override void Dispose(bool disposing)
{
if (disposing)
{
db.Dispose();
}
base.Dispose(disposing); //this line
}
this is the controller code i use to save and retrieve the attachments:
public async Task<ActionResult> Create([Bind(Include = "ID,LastName,FirstMidName")] Person person, HttpPostedFileBase upload)
{
if (ModelState.IsValid)
{
if (upload != null && upload.ContentLength > 0)
{
var avatar = new File
{
FileName = System.IO.Path.GetFileName(upload.FileName),
FileType = FileType.Avatar,
ContentType = upload.ContentType
};
using (var reader = new System.IO.BinaryReader(upload.InputStream))
{
avatar.Content = reader.ReadBytes(upload.ContentLength);
}
person.Files = new List<File> { avatar };
}
db.People.Add(person);
db.SaveChanges();
//await SendEmail(person.ID);
var message = new MailMessage();
var file = db.Files.Find(person.ID);
Attachment attachment;
var stream = new MemoryStream();
try
{
stream.Write(file.Content, 0, file.Content.Length - 1);
attachment = new Attachment(stream, file.FileName);
}
catch
{
stream.Dispose();
throw;
}
//When i use this bit of code, I receive an error "Cannot access a closed stream
//using (var stream = new MemoryStream())
//{
// stream.Write(file.Content, 0, file.Content.Length - 1);
// attachment = new Attachment(stream, file.FileName);
//}
var fileSize = file.Content.Length;
message.Attachments.Add(attachment);
message.To.Add(new MailAddress("recipient#gmail.com")); // replace with valid value
message.From = new MailAddress("sender#outlook.com"); // replace with valid value
message.Subject = "Your email subject";
message.BodyEncoding = System.Text.Encoding.UTF8;
message.Body = "<p>file size: </p>" + "<p>" + fileSize + "</p>";
message.IsBodyHtml = true;
message.BodyEncoding = System.Text.Encoding.UTF8;
using (var smtp = new SmtpClient())
{
//when i try to send the mail asynchronously the view with the form just keeps showing "waiting for localhost"
//await smtp.SendMailAsync(message);
smtp.Send(message);
return RedirectToAction("Index");
}
}
return View(person);
}
Additional Question
Would it be a good idea to send the attachment inside of the save to database part?
EDIT
I have just tried sending the attachment with the below line of code:
message.Attachments.Add(new Attachment(upload.InputStream, Path.GetFileName(upload.FileName)));
added after:
person.Files = new List<File> { avatar };
But still receive damaged attachment..
EDIT 2:
I think this line
var file = db.Files.Find(person.ID)
should actually be (you were trying to get a file using a person id):
var file = db.Files.Find(avatar.ID)
but, in your case you don't need to retrieve it from the database. You already have the bytes there, so just wrap them in a MemoryStream, as you can't directly send the upload.InputStream without storing it in memory:
attachment = new Attachment(new MemoryStream(avatar.Content), file.FileName);
Looking at this quickly, I'd look at the obvious.
var file = db.Files.Find(person.ID);
Look at what this is returning. It may well be that after this object is being used, depending on what object it is, may have been disposed of already.
The reason being is you're attempting to read from the file.Content.Length which may be the very cause to the problem because it doesn't have a value or whatever.
Step through the logic, line by line. Break it down from the most simple, and build it up slowly until you get to the cause. Also, think about abstracting the logic from the controller and implementing a service that deals with this action. Check out repository pattern, unit of work and dependency injection as a side note.
Ultimately, your issue, I think it's just the fact that you're not checking all the "what if it wasn't the way you expected" type errors, which in all is why you should most probably also have some tests in place. :P
Deconstruct, start from basics and build your way up. Doing this, I'm sure you will find the problem. :)
I'm trying to learn how to use the MailKit library but I am struggling to retrieve attachments. So far my code will open a mailbox, go through each message and store data such as sender, subject, body, date etc. but I can't deal with attachments.
I have tried to use other peoples solutions found on here, on github and other sites but I still don't understand exactly what they are doing in their code and when I come close to getting a solution working it causes more bugs so I get stressed and delete all the code. I don't mean to seem lazy but I would love if somebody could explain how I can achieve this. I'm basically trying to build a mail client for a web forms app.
Below is my code, so as you can see I'm fairly clueless :)
// Open the Inbox folder
client.Inbox.Open(FolderAccess.ReadOnly, cancel.Token);
//get the full summary information to retrieve all details
var summary = client.Inbox.Fetch(0, -1, MessageSummaryItems.Full, cancel.Token);
foreach (var msg in summary)
{
//this code originally downloaded just the text from the body
var text = msg.Body as BodyPartText;
//but I tried altering it so that it will get attachments here also
var attachments = msg.Body as BodyPartBasic;
if (text == null)
{
var multipart = msg.Body as BodyPartMultipart;
if (multipart != null)
{
text = multipart.BodyParts.OfType<BodyPartText>().FirstOrDefault();
}
}
if (text == null)
continue;
//I hoped this would get the messages where the content dispositon was not null
//and let me do something like save the attachments somewhere but instead it throws exceptions
//about the object reference not set to an instance of the object so it's very wrong
if (attachments.ContentDisposition != null && attachments.ContentDisposition.IsAttachment)
{
//I tried to do the same as I did with the text here and grab the body part....... but no
var attachedpart = client.Inbox.GetBodyPart(msg.Index, attachments, cancel.Token);
}
else
{
//there is no plan b :(
}
// this will download *just* the text
var part = client.Inbox.GetBodyPart(msg.Index, text, cancel.Token);
//cast main body text to Text Part
TextPart _body = (TextPart)part;
I'm not entirely clear on what you want to accomplish, but if you just want to download the message attachments (without downloading the entire message) and save those attachments to the file system, here's how you can accomplish that:
var messages = client.Inbox.Fetch (0, -1, MessageSummaryItems.Full | MessageSummaryItems.UniqueId);
int unnamed = 0;
foreach (var message in messages) {
var multipart = message.Body as BodyPartMultipart;
var basic = message.Body as BodyPartBasic;
if (multipart != null) {
foreach (var attachment in multipart.BodyParts.OfType<BodyPartBasic> ().Where (x => x.IsAttachment)) {
var mime = (MimePart) client.Inbox.GetBodyPart (message.UniqueId.Value, attachment);
var fileName = mime.FileName;
if (string.IsNullOrEmpty (fileName))
fileName = string.Format ("unnamed-{0}", ++unnamed);
using (var stream = File.Create (fileName))
mime.ContentObject.DecodeTo (stream);
}
} else if (basic != null && basic.IsAttachment) {
var mime = (MimePart) client.Inbox.GetBodyPart (message.UniqueId.Value, basic);
var fileName = mime.FileName;
if (string.IsNullOrEmpty (fileName))
fileName = string.Format ("unnamed-{0}", ++unnamed);
using (var stream = File.Create (fileName))
mime.ContentObject.DecodeTo (stream);
}
}
Another alternative that works for me, but appears to be a little simpler:
var messages = client.Inbox.Fetch (0, -1, MessageSummaryItems.Full | MessageSummaryItems.BodyStructure | MessageSummaryItems.UniqueId);
int unnamed = 0;
foreach (var message in messages) {
foreach (var attachment in message.Attachments) {
var mime = (MimePart) client.Inbox.GetBodyPart (message.UniqueId.Value, attachment);
var fileName = mime.FileName;
if (string.IsNullOrEmpty (fileName))
fileName = string.Format ("unnamed-{0}", ++unnamed);
using (var stream = File.Create (fileName))
mime.ContentObject.DecodeTo (stream);
}
}
Note that this is asking for the BODYSTRUCTURE instead of the BODY in the Fetch statement, which seems to fix the issue of attachments not being flagged as such.