Archived Forum Post

Index of archived forum posts

Question:

Understanding IMAP Email Attachments

Dec 14 '16 at 20:17

I’ve noticed that when choosing not to download attachments when requesting an IMAP email, calling GetEmailNumAttach returns the incorrect number of attachments. If an email has no attachments then the number returned generally seems to be 2. The attachment information stored within the eml file itself appears to be correct.

Also, as the attachment information is stored in the eml data, is there a specific reason why this information is acquired via the CkoImap object and not through the CkoEmail object directly?

I’m using the latest iOS libraries (v9.5.0.51).


Answer

This is a topic that understandably causes much confusion. To understand the discrepancies, and what is a valid difference in reporting vs. what is not, I'll first describe the simpler case of downloading a full email (with attachments).

When a full email with attachments is downloaded from the IMAP server, the full MIME of the email is requested. The IMAP client receives the MIME from the server, and this should contain all of the email attachments. An "attachment" is semantically an attached file that is not there solely for the purpose of participating as an image (or style sheet) in the HTML body. These are classified as "related" items, and are accessed separately from "attachments". Detached signatures are also a special case.

When the full MIME is downloaded, the Email object's NumAttachments property returns an answer by traversing the MIME tree structure and counting the parts that qualify as attachments. (VerboseLogging may be turned on, and the LastErrorText examined after accessing the NumAttachments property to see the internal logic. (As a side note: A property access may be more expensive than one might think. It's usually a good idea to assign a property to a local primitive variable, and then use that variable in a loop rather than always re-accessing the object's property.)

Fetching an IMAP Email Without Attachments

First I'll show a snippet of C++ code as an example. The Chilkat API is identical across programming languages, so the same sequence of method calls exists in other languages -- it is only the language syntax that is different:

    // Keep an in-memory session log for what follows..
    imap.put_KeepSessionLog(true);
    // Make the LastErrorText more verbose...
    imap.put_VerboseLogging(true);
    // Don't automatically download attachments.
    imap.put_AutoDownloadAttachments(false);

// Fetch the 1st message by sequence number.
    // 
    CkEmail *email = imap.FetchSingle(1,false);
    if (email)
        {
        // Examine the SessionLog and the LastErrorText
        printf("%s\n",imap.sessionLog());
        printf("----\n");
        printf("%s\n",imap.lastErrorText());
        printf("----\n");
        printf("%s\n",imap.lastResponse());
        printf("----\n");
        printf("%s\n",email->getMime());
        delete email;
        }

To download an email from the IMAP server without attachments, the Chilkat IMAP client must first fetch information about the structure of the email. The IMAP server responds with a BODYSTRUCTURE. The BODYSTRUCTURE contains information about the email's structure, including what the IMAP server's implementation deems as attachments. For example, here's a BODYSTRUCTURE returned by GMail for an email with 1 related image, and 2 attachments:

----IMAP REQUEST----
aaaf FETCH 1 (UID BODYSTRUCTURE)
----IMAP RESPONSE----
* 1 FETCH (UID 1 BODYSTRUCTURE ((("TEXT" "PLAIN" ("CHARSET" "utf-8") NIL NIL "7bit" 36 2)
(("TEXT" "HTML" ("CHARSET" "utf-8") NIL NIL "8bit" 344 12)
("IMAGE" "JPEG" ("NAME" "starfish.jpg") "<part1.06050003.00040008@chilkatsoft.com>" NIL "base64" 8538 NIL ("inline" ("FILENAME" "starfish.jpg"))) 
"RELATED" ("BOUNDARY" "------------090406030500000101040309") NIL NIL) 
"ALTERNATIVE" ("BOUNDARY" "------------060809010805020700040701") NIL NIL)
("TEXT" "XML" ("NAME" "resp.xml") NIL NIL "7bit" 328 9 NIL ("attachment" ("FILENAME" "resp.xml")))
("IMAGE" "JPEG" ("NAME" "red.jpg") NIL NIL "base64" 278322 NIL ("attachment" ("FILENAME" "red.jpg"))) 
"MIXED" ("BOUNDARY" "------------040506060907070101020201") NIL NIL))
aaaf OK FETCH completed
Chilkat parses through this horrible mess and determines what command should be sent to fetch only the body w/ related items, but excluding the attachments. For example, in this case, Chilkat sends the following command, and the server responds with the following. (I've omitted the actual data.)
aaag FETCH 1 (FLAGS INTERNALDATE BODY[HEADER] BODY[1.MIME] BODY[1])
----IMAP RESPONSE----
* 1 FETCH (FLAGS () INTERNALDATE "29-Jun-2015 08:32:53 -0400" BODY[HEADER] {529}
(529 bytes)
 BODY[1.MIME] {88}
(88 bytes)
 BODY[1] {9617}
(9617 bytes)
)
aaag OK FETCH completed
The Email object that is returned contains the MIME without the MIME sub-parts that contain the attachments. This presents a problem, because the Email object contains a NumAttachments property, and it would be nice if, given that we only have the Email object, if the attachment information could be preserved within the Email object. Chilkat does this by adding special MIME header fields. This way, the attachment information becomes part of the MIME, and is maintained even if saved to .eml and re-loaded. In the example above, these are the header fields added to the MIME:
ckx-imap-internaldate: 29-Jun-2015 08:32:53 -0400
ckx-imap-uid: 1
ckx-imap-isUid: NO
ckx-imap-seen: YES
ckx-imap-answered: NO
ckx-imap-deleted: NO
ckx-imap-flagged: NO
ckx-imap-draft: NO
ckx-imap-flags: Seen
ckx-imap-numAttach: 2
ckx-imap-attach-nm-1: resp.xml
ckx-imap-attach-sz-1: 328
ckx-imap-attach-pt-1: 2
ckx-imap-attach-enc-1: 7bit
ckx-imap-attach-nm-2: red.jpg
ckx-imap-attach-sz-2: 278322
ckx-imap-attach-pt-2: 3
ckx-imap-attach-enc-2: base64
The header fields specific to attachments are the "ckx-imap-numAttach" header, and all header fields beginning with "ckx-imap-attach". (Note: The "ckx-" prefix is standard within Chilkat. Whenever an email is sent by Chilkat via the MailMan's SendEmail method, any headers beginning with "ckx-" are automatically removed.)

Each attachment has four headers associated with it. The headers specify the attachment filename, its size, its encoding, and its location within the MIME structure on the IMAP server. The location information will enable Chilkat to send the correct command to download the attachment separately.

The following Imap methods will get attachment information from the special "ckx-imap" headers:

GetEmailNumAttach 
GetMailAttachFilename
GetMailAttachSize
GetMailFlag
GetMailNumAttach
GetMailSize
If the special headers are not present, the methods will fall back to traversing the MIME structure to get the attachment information. When the Email.NumAttachments property is accessed, or when any of the Email methods specific to attachments are called, the special IMAP headers are not consulted. Therefore, when emails are downloaded without attachments, make sure to use the IMAP methods for accessing attachment information.

The semantics of what is an attachment vs. what is not an attachment is usually clear. However, there are cases where things can get a bit murky. For example, with things specified as "inline", not marked as an attachment, UU-encoded items, etc. The IMAP server's BODYSTRUCTURE response could possibly have a different idea of what is an attachment vs. what Chilkat considers an attachment after traversing the MIME structure of a fully downloaded email. Both of these could also differ from a human end-user's expectations, and different people may have different expectations based on different needs. "Solving" one "problem" for one user might introduce a problem for another. Therefore, to understand how to cope with a discrepency, Chilkat needs to see detailed information. Specifically, Chilkat needs to see the BODYSTRUCTURE response from the IMAP server. Chilkat would also need to have the full MIME of the email after it's been downloaded in full w/ attachments. The verbose LastErrorText would also help. This information would be used to determine if Chilkat's logic needs to be refined, or how an app might work around a certain behavior.

I hope this helps.. :)