If you have ever sent or received mail messages via Java, chances are high that you have used JavaMail for this task. Most of the time JavaMail does an excellent job and a lot of use cases are described in the JavaMail FAQ. But there are still some additional quirks you should be aware of when doing advanced mail operations like adding or removing attachments (or “Parts”) from existing mails retreived from some IMAP or POP3 store. This post gives a showcase for how to remove an attachment from a mail at an arbitrary level which has been obtained from an IMAP store. It points to the pitfalls which are waiting and shows some possible solutions. The principles laid out here are important for adding new attachments to a mail as well, but that’s yet another story.
Before we start manipulating mail messages it is important to understand how these are represented in the JavaMail world.
The starting point is the Message
. It has a content and a content type. The content can be any Java object representing the mail content, like a plain text (String
) or raw image data. But it can also be a Multipart
object: this is the case when a message’s content consists of more than a single item. A Multipart
object is a container which holds one ore more BodyPart
objects. These BodyPart
s, like a Message
, have a content and a content type (in fact, both Message
and BodyPart
implement the same interface Part
which carries these properties).
Beside plain content, A BodyPart
can contain another Multipart
or even another Message
, a so called nested message (e.g. a message forwarded as attachment) with content type message/rfc822
.
As you can see, the structure of a Message
can be rather heterogenous, a tree with nodes of different types. The following picture illustrates the tree structure for a sample message.
Object tree for a sample multipart mail This tree can be navigated in both directions: - *getContent()* on `Part`s like `Message` or `BodyPart` to get to the child of this node. The return type is a `java.lang.Object` and in case of a plain `BodyPart` can be quite huge. Before calling `Part.getContent()` be sure to check whether it contains a container by checking for its content type via `Part.isMimeType("multipart/*")` or `Part.isMimeType("message/rfc822")` - *getParent()* on `Multipart` or `BodyPart` returns the parent node, which is of type `BodyPart`. Note that there is no way to get from a nested `Message` to its parent `BodyPart`. If you need to traverse the tree upwards with nested messages on the way, you first have to extract the path to this node from the top down. E.g. while identifying the part to remove you could store the parent `BodyPart`s on a stack. ## First approach Back to our use case of removing an attachment at an arbitrary level within a mail. First, a `Message` from the IMAP Store needs to be obtained, e.g. by looking it up in an `IMAPFolder` via its UID:
Session session = Session.getDefaultInstance(new Properties());
Store store = session.getStore("imap");
store.connect("imap.example.com",-1,"user","password");
IMAPFolder folder = (IMAPFolder) store.getFolder("INBOX");
IMAPMessage originalMessage = (IMAPMessage) folder.getMessageByUID(42L);
Message message = new MimeMessage(originalMessage);
// Mark original message for a later expunge
originalMessage.setFlag(Flags.Flag.DELETED, true);
MimePart partToRemove = partExtractor.getPartByPartNr(message,"2.1");
Multipart parent = partToRemove.getParent();
parent.removeBodyPart(partToRemove);
// Update headers and append new message to folder
message.saveChanges();
folder.appendMessages(new Message[] { message });
// Mark as deleted and expunge
originalMessage.setFlag(Flags.Flag.DELETED, true);
folder.expunge(new Message[]{ originalMessage });
// Recursively go through and save all changes
if (message.isMimeType("multipart/*")) {
refreshRecursively((Multipart) message.getContent());
}
Multipart part = (Multipart) message.getContent();
message.setContent(part);
message.saveChanges();
...
void refreshRecursively(Multipart pPart)
throws MessagingException, IOException {
for (int i=0;i<pPart.getCount();i++) {
MimeBodyPart body = (MimeBodyPart) pPart.getBodyPart(i);
if (body.isMimeType("message/rfc822")) {
// Refresh a nested message
Message nestedMsg = (Message) body.getContent();
if (nestedMsg.isMimeType("multipart/*")) {
Multipart mPart = (Multipart) body.getContent();
refreshRecursively(mPart);
nestedMsg.setContent(mPart);
}
nestedMsg.saveChanges();
} else if (body.isMimeType("multipart/*")) {
Multipart mPart = (Multipart) body.getContent();
refreshRecursively(mPart);
}
body.setDataHandler(body.getDataHandler());
}
}
BodyPart bodyParent = null;
Multipart multipart = parent;
do {
if (multipart.getParent() instanceof BodyPart) {
bodyParent = (BodyPart) multipart.getParent();
bodyParent.setDataHandler(bodyParent.getDataHandler());
multipart = bodyParent.getParent();
} else {
// It's a Message, probably the toplevel message
// but could be a nested message, too (in which
// case we have to stop here, too)
bodyParent = null;
}
} while (bodyParent != null);
MimeMessage.saveChanges()
Stack<MimeBodyPart> parentBodys = new Stack<MimeBodyPart>();
MimePart partToRemove = partExtractor.getPartByPartNr(message,"2.1",parentBodys);
....