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.
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.
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
I'm downloading attachments for a given mailid with the following code:
HeaderSearchQuery searchCondition = SearchQuery.HeaderContains("Message-Id", ssMailItemId);
var folder = client.GetFolder(ssFolderName);
folder.Open(FolderAccess.ReadOnly);
IList<UniqueId> ids = folder.Search(searchCondition);
foreach (UniqueId uniqueId in ids)
{
MimeMessage message = folder.GetMessage(uniqueId);
foreach (MimeEntity attachment in message.Attachments)
{
ssAttachmentsDetail.Append(fillAttachmentDetailRecord(attachment, uniqueId.Id.ToString()));
}
But both the MimeEntity.ContentDisposition.Size and MimePart.ContentDuration are null. Is there any field regarding the size of the attachment?
The ContentDisposition.Size property is only set if the Content-Disposition header has a size parameter, like this:
Content-Disposition: attachment; size=15462
But that value should probably not really be trusted anyway...
If you want the size of an attachment, the only accurate way of doing this would be to do something like this:
// specify that we want to fill the IMessageSummary.Body and IMessageSummary.UniqueId fields...
var items = folder.Fetch (ids, MessageSummaryItems.BodyStructure | MessageSummaryItems.UniqueId);
foreach (var item in items) {
foreach (var attachment in item.Attachments) {
// 'octets' is just a fancy word for "number of bytes"
var size = attachment.Octets;
// download the individual attachment
var entity = folder.GetBodyPart (item.UniqueId, attachment);
}
}
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. :)
In my project i need to attach multiple files to send mail and i am doing it as
if (fDialog.ShowDialog() == DialogResult.OK)
{
textBox6.Text += fDialog.FileName.ToString() + ";";
}
Here i am attaching the file in textbox6
I am separating the paths of the different attachment file using ";" and then i separate those paths of the attachment as follows and then send it.
System.Net.Mail.Attachment attachment;
foreach (string m in textBox6.Text.Split(';'))
{
attachment = new System.Net.Mail.Attachment(m);
message.Attachments.Add(attachment);
}
This method don't work for me. But when i send mail with single attachment with the following code it just work fine
System.Net.Mail.Attachment attachment;
attachment = new System.Net.Mail.Attachment(textBox6.Text.ToString());
message.Attachments.Add(attachment);
Someone please help. I have been working this whole day and could not figure it out.
I hope this will solve your problem completely http://archive.msdn.microsoft.com/CSharpGmail
The function should be :
foreach (string m in textBox6.Text.Split(';'))
{
System.Net.Mail.Attachment attachment = new System.Net.Mail.Attachment(m);
message.Attachments.Add(attachment);
}
This will fix your issue.
Try to use file stream instead of link on file:
message.Attachments.Add(new Attachment(attachmentFileStream, fileNameOnly));
Create a Attachmentlistbox then-
if (Attachmentlistbox.Items.Count != 0)
{
for (int i = 0; i < Attachmentlistbox.Items.Count; i++)
mailMessage.Attachments.Add(new Attachment(Attachmentlistbox.Items[i].ToString()));
}