Search code examples
c#gmailimapattachmentmailkit

Problems with reading some attachments using IMAP protocol in C #


I would like to ask you the following question:

I have two codes to read emails with their respective attachments that have:

The first one, I use the S22.IMAP dll:

using (ImapClient Client = new ImapClient(imap, 993, usuario, psw, AuthMethod.Login, true))
{
    IEnumerable<uint> uids = Client.Search(SearchCondition.Unseen());//Correos no leídos

    IEnumerable<MailMessage> messages = Client.GetMessages(uids, FetchOptions.Normal); 

    conexion.stringconeccion = stringconeccion;
    conexion.conectar();

    String ts = "start transaction";

    MySqlCommand datos_ts = new MySqlCommand(ts, conexion.con);
    datos_ts.ExecuteScalar();

    DataTable dt_existeXML = new DataTable();

    int insercion = 0;

    foreach (MailMessage msg in messages)
    {
        foreach (Attachment atc in msg.Attachments)
        {
            if (System.IO.Path.GetExtension(msg.Attachments[0].Name) == ".xml")
            {
                String archivoXML_texto = "";
                byte[] allBytes = new byte[msg.Attachments[0].ContentStream.Length];
                int bytesRead = msg.Attachments[0].ContentStream.Read(allBytes, 0, (int)msg.Attachments[0].ContentStream.Length);

                using (MemoryStream memory = new MemoryStream(allBytes))
                {
                    StreamReader archivoXML = new StreamReader(memory);

                    archivoXML_texto = archivoXML.ReadToEnd();
                    archivoXML.Close();

                    memory.Dispose();
                }
            }
        }
    }

second code, using the MailKit DLL:

using (var client = new ImapClient ()) {
    client.Connect ("imap.gmail.com", 993, SecureSocketOptions.SslOnConnect);
    client.Authenticate ("[email protected]", "clave");
    client.Inbox.Open (FolderAccess.ReadOnly);

    var uids = client.Inbox.Search(SearchQuery.NotSeen);

    foreach (var uid in uids)
    {
        var message = client.Inbox.GetMessage(uid);
        foreach (var attachment in message.Attachments.OfType<MimePart>())
        {   
                byte[] allBytes = new byte[attachment.Content.Stream.Length];
                int bytesRead = attachment.Content.Stream.Read(allBytes, 0, (int)attachment.Content.Stream.Length);
                string texto_definitivo = "";
                String archivoXML_textoBase64 = "";
                using (MemoryStream memory = new MemoryStream(allBytes))
                {
                    StreamReader archivoXML = new StreamReader(memory);
                    archivoXML_textoBase64 = archivoXML.ReadToEnd();
                    byte[] temp_backToBytes = Convert.FromBase64String(archivoXML_textoBase64);
                    texto_definitivo = Encoding.ASCII.GetString(temp_backToBytes);
                    archivoXML.Close();
                    memory.Dispose();
                }   
        }
    }
    client.Disconnect (true);
}

But I realize that there are attachments that do not read them, but also, I have noticed something in common of the attachments that are not read, do not have the attachment icon. But when I open the mail, I see that they have attachments:

enter image description here

For example the image that is marked with red, the attachment icon to its right is not shown. But when I open it, I verify that it has an attached document. Just those documents, either of the two codes, do not read the attachments.

My question are:

How could I also read those attachments? Should I configure or enable any option in the mail? What and how? Is it a mistake to send mail? How could it be solved?

UPDATE

I hope I have understood. The following code tried to obtain the text / content of the attachment for the cases that it could. I would like it if I'm wrong, please correct me.

    public static void DownloadBodyParts ()
    {
        using (var client = new ImapClient ()) {
            client.Connect ("imap.gmail.com", 993, SecureSocketOptions.SslOnConnect);             
            client.Authenticate("[email protected]", "clave");
            client.Inbox.Open(FolderAccess.ReadWrite);
            var uids = client.Inbox.Search(SearchQuery.NotSeen);          

            foreach (var uid in uids)
            {
                var message = client.Inbox.GetMessage(uid);                    
                var attachments = message.BodyParts.OfType<MimePart>().Where(part => !string.IsNullOrEmpty(part.FileName));

                foreach (MimePart atch in attachments)
                {
                    using (var memory = new MemoryStream())
                    {
                        atch.Content.DecodeTo(memory);
                        var buffer = memory.ToArray();
                        var text = Encoding.UTF8.GetString(buffer);
                    }
                }
             }
        client.Disconnect (true);
         }

Solution

  • From the MailKit FAQ:

    Q: How can I tell if a message has attachments?

    In most cases, a message with a body that has a MIME-type of multipart/mixed containing more than a single part probably has attachments. As illustrated above, the first part of a multipart/mixed is typically the textual body of the message, but it is not always quite that simple.

    In general, MIME attachments will have a Content-Disposition header with a value of attachment. To get the list of body parts matching this criteria, you can use the MimeMessage.Attachments property.

    Unfortunately, not all mail clients follow this convention and so you may need to write your own custom logic. For example, you may wish to treat all body parts having a name or filename parameter set on them:

    var attachments = message.BodyParts.OfType<MimePart> ().Where (part => !string.IsNullOrEmpty (part.FileName));
    

    A more sophisticated approach is to treat body parts not referenced by the main textual body part of the message as attachments. In other words, treat any body part not used for rendering the message as an attachment. For an example on how to do this, consider the following code snippets:

    /// <summary>
    /// Visits a MimeMessage and generates HTML suitable to be rendered by a browser control.
    /// </summary>
    class HtmlPreviewVisitor : MimeVisitor
    {
        List<MultipartRelated> stack = new List<MultipartRelated> ();
        List<MimeEntity> attachments = new List<MimeEntity> ();
        readonly string tempDir;
        string body;
    
        /// <summary>
        /// Creates a new HtmlPreviewVisitor.
        /// </summary>
        /// <param name="tempDirectory">A temporary directory used for storing image files.</param>
        public HtmlPreviewVisitor (string tempDirectory)
        {
            tempDir = tempDirectory;
        }
    
        /// <summary>
        /// The list of attachments that were in the MimeMessage.
        /// </summary>
        public IList<MimeEntity> Attachments {
            get { return attachments; }
        }
    
        /// <summary>
        /// The HTML string that can be set on the BrowserControl.
        /// </summary>
        public string HtmlBody {
            get { return body ?? string.Empty; }
        }
    
        protected override void VisitMultipartAlternative (MultipartAlternative alternative)
        {
            // walk the multipart/alternative children backwards from greatest level of faithfulness to the least faithful
            for (int i = alternative.Count - 1; i >= 0 && body == null; i--)
                alternative[i].Accept (this);
        }
    
        protected override void VisitMultipartRelated (MultipartRelated related)
        {
            var root = related.Root;
    
            // push this multipart/related onto our stack
            stack.Add (related);
    
            // visit the root document
            root.Accept (this);
    
            // pop this multipart/related off our stack
            stack.RemoveAt (stack.Count - 1);
        }
    
        // look up the image based on the img src url within our multipart/related stack
        bool TryGetImage (string url, out MimePart image)
        {
            UriKind kind;
            int index;
            Uri uri;
    
            if (Uri.IsWellFormedUriString (url, UriKind.Absolute))
                kind = UriKind.Absolute;
            else if (Uri.IsWellFormedUriString (url, UriKind.Relative))
                kind = UriKind.Relative;
            else
                kind = UriKind.RelativeOrAbsolute;
    
            try {
                uri = new Uri (url, kind);
            } catch {
                image = null;
                return false;
            }
    
            for (int i = stack.Count - 1; i >= 0; i--) {
                if ((index = stack[i].IndexOf (uri)) == -1)
                    continue;
    
                image = stack[i][index] as MimePart;
                return image != null;
            }
    
            image = null;
    
            return false;
        }
    
        // Save the image to our temp directory and return a "file://" url suitable for
        // the browser control to load.
        // Note: if you'd rather embed the image data into the HTML, you can construct a
        // "data:" url instead.
        string SaveImage (MimePart image, string url)
        {
            string fileName = url.Replace (':', '_').Replace ('\\', '_').Replace ('/', '_');
    
            string path = Path.Combine (tempDir, fileName);
    
            if (!File.Exists (path)) {
                using (var output = File.Create (path))
                    image.Content.DecodeTo (output);
            }
    
            return "file://" + path.Replace ('\\', '/');
        }
    
        // Replaces <img src=...> urls that refer to images embedded within the message with
        // "file://" urls that the browser control will actually be able to load.
        void HtmlTagCallback (HtmlTagContext ctx, HtmlWriter htmlWriter)
        {
            if (ctx.TagId == HtmlTagId.Image && !ctx.IsEndTag && stack.Count > 0) {
                ctx.WriteTag (htmlWriter, false);
    
                // replace the src attribute with a file:// URL
                foreach (var attribute in ctx.Attributes) {
                    if (attribute.Id == HtmlAttributeId.Src) {
                        MimePart image;
                        string url;
    
                        if (!TryGetImage (attribute.Value, out image)) {
                            htmlWriter.WriteAttribute (attribute);
                            continue;
                        }
    
                        url = SaveImage (image, attribute.Value);
    
                        htmlWriter.WriteAttributeName (attribute.Name);
                        htmlWriter.WriteAttributeValue (url);
                    } else {
                        htmlWriter.WriteAttribute (attribute);
                    }
                }
            } else if (ctx.TagId == HtmlTagId.Body && !ctx.IsEndTag) {
                ctx.WriteTag (htmlWriter, false);
    
                // add and/or replace oncontextmenu="return false;"
                foreach (var attribute in ctx.Attributes) {
                    if (attribute.Name.ToLowerInvariant () == "oncontextmenu")
                        continue;
    
                    htmlWriter.WriteAttribute (attribute);
                }
    
                htmlWriter.WriteAttribute ("oncontextmenu", "return false;");
            } else {
                // pass the tag through to the output
                ctx.WriteTag (htmlWriter, true);
            }
        }
    
        protected override void VisitTextPart (TextPart entity)
        {
            TextConverter converter;
    
            if (body != null) {
                // since we've already found the body, treat this as an attachment
                attachments.Add (entity);
                return;
            }
    
            if (entity.IsHtml) {
                converter = new HtmlToHtml {
                    HtmlTagCallback = HtmlTagCallback
                };
            } else if (entity.IsFlowed) {
                var flowed = new FlowedToHtml ();
                string delsp;
    
                if (entity.ContentType.Parameters.TryGetValue ("delsp", out delsp))
                    flowed.DeleteSpace = delsp.ToLowerInvariant () == "yes";
    
                converter = flowed;
            } else {
                converter = new TextToHtml ();
            }
    
            body = converter.Convert (entity.Text);
        }
    
        protected override void VisitTnefPart (TnefPart entity)
        {
            // extract any attachments in the MS-TNEF part
            attachments.AddRange (entity.ExtractAttachments ());
        }
    
        protected override void VisitMessagePart (MessagePart entity)
        {
            // treat message/rfc822 parts as attachments
            attachments.Add (entity);
        }
    
        protected override void VisitMimePart (MimePart entity)
        {
            // realistically, if we've gotten this far, then we can treat this as an attachment
            // even if the IsAttachment property is false.
            attachments.Add (entity);
        }
    }
    And the way you'd use this visitor might look something like this:
    
    void Render (MimeMessage message)
    {
        var tmpDir = Path.Combine (Path.GetTempPath (), message.MessageId);
        var visitor = new HtmlPreviewVisitor (tmpDir);
    
        Directory.CreateDirectory (tmpDir);
    
        message.Accept (visitor);
    
        DisplayHtml (visitor.HtmlBody);
        DisplayAttachments (visitor.Attachments);
    }
    

    Once you've rendered the message using the above technique, you'll have a list of attachments that were not used, even if they did not match the simplistic criteria used by the MimeMessage.Attachments property.

    Update:

    Here's how to get the text content of a MimePart (assuming that the MimePart cannot be cast to a TextPart which makes this process dead simple).

    using (var memory = new MemoryStream ()) {
        mimePart.Content.DecodeTo (memory);
    
        var buffer = memory.ToArray ();
        var text = Encoding.UTF8.GetString (buffer);
    }
    

    Of course, if the MimePart can be cast to a TextPart, then this is even simpler:

    var textPart = (TextPart) mimePart;
    var text = textPart.Text;