MIME4J - парсим e-mail письма без боли
У Apache Software Foundation есть проект Apache James, суть Mail Server на Java, состоящий из набора саб-проектов (Java-библиотек для работы с электронной почтой).

Одна из библиотек (саб-проект) это MIME4J для работы с email-письмами в Event, DOM и прочих API-стилях.
А почему не javax.mail, спросите вы?
К сожалению, javax.mail ориентирован на работу с письмами в жесткой связке с почтовой сессией. А ваша задача, например, распарсить или, наоборот, сформировать письмо из файла / в файл, или работать с потоком, буфером.
Вот, например, разные PGP/GnuPG-based решения любят шифровать полноценный multipart/mixed в текстовом виде, в котором все сущности типа inline. А ваша задача - вытащить и разложить по полочкам. Вот тут на помощь и приходит Apache James. Все примеры ниже можно посмотреть здесь.
Ну, собственно, к коду.
- Создадим простое text/plain письмо:
// 1) start with an empty message
Message message = MessageBuilder.create()
// 2) set header fields
// Date and From are required fields
// Message-ID should be present
.setFrom("John Doe <jdoe@machine.example>")
.setTo("Mary Smith <mary@example.net>")
.setSubject("Saying Hello")
.setDate(new Date())
.generateMessageId(InetAddress.getLocalHost().getCanonicalHostName())
.setBody("This is a message just to say hello.\r\nSo, \"Hello\".",
Charsets.ISO_8859_1)
.build();
try {
// 4) print message to standard output
MessageWriter writer = new DefaultMessageWriter();
writer.writeMessage(message, System.out);
} finally {
// 5) message is no longer needed and should be disposed of
message.dispose();
}
- Создадим сложное multipart/mixed письмо:
// 1) start with an empty message
StorageBodyFactory bodyFactory = new StorageBodyFactory();
Message message = MessageBuilder.create()
// 2) set header fields
// Date and From are required fields
// Message-ID should be present
.setFrom("John Doe <jdoe@machine.example>")
.setTo("Mary Smith <mary@example.net>")
.setSubject("An image for you")
.setDate(new Date())
.generateMessageId(InetAddress.getLocalHost().getCanonicalHostName())
// 3) set a multipart body
.setBody(MultipartBuilder.create("mixed")
.use(bodyFactory)
// a multipart may have a preamble
.setPreamble("This is a multi-part message in MIME format.")
// first part is text/plain
.addBodyPart(BodyPartBuilder.create()
.use(bodyFactory)
.setBody("Why so serious?", Charsets.UTF_8)
.setContentTransferEncoding("quoted-printable")
.build())
// second part is image/png (image is created on the fly)
.addBodyPart(BodyPartBuilder.create()
.use(bodyFactory)
.setBody(createImageBody(bodyFactory,
renderSampleImage()))
.setContentType("image/png")
.setContentTransferEncoding("base64")
// Specify a filename in the
// Content-Disposition header
// (implicitly sets the disposition
// type to "attachment")
.setContentDisposition("attachment", "smiley.png")
.build())
.build())
// setBody also sets the Content-Type header field
.build();
try {
// 4) print message to standard output
MessageWriter writer = new DefaultMessageWriter();
writer.writeMessage(message, System.out);
} finally {
// 5) message is no longer needed and should be disposed of
message.dispose();
}
- Создадим шаблон письма и используем для создания письма на основании шаблона (код для формирования рассылки?):
// Explicitly set a strategy for storing body parts. Usually not
// necessary; for most applications the default setting is appropriate.
StorageProvider storageProvider = new TempFileStorageProvider();
StorageBodyFactory bodyFactory = new StorageBodyFactory(storageProvider,
DecodeMonitor.SILENT);
// Create a template message. It would be possible to load a message
// from an input stream but for this example a message object is created
// from scratch for demonstration purposes.
Message template = MessageBuilder.create()
.setBody(MultipartBuilder.create("mixed")
.addBodyPart(BodyPartBuilder.create()
.use(bodyFactory)
.setBody("This is the first part of the template..",
Charsets.UTF_8)
.setContentTransferEncoding("quoted-printable")
.build())
.addBodyPart(BodyPartBuilder.create()
.use(bodyFactory)
.setBody(createRandomBinary(200),
"application/octet-stream")
.setContentTransferEncoding("base64")
.build())
.addBodyPart(BodyPartBuilder.create()
.use(bodyFactory)
.setBody(createRandomBinary(300),
"application/octet-stream")
.setContentTransferEncoding("base64")
.build())
.build())
.setSubject("Template message")
.build();
// Create a new message by transforming the template.
// Create a copy of the template. The copy can be modified without
// affecting the original.
final MessageBuilder messageBuilder = MessageBuilder.createCopy(template);
// In this example we know we have a multipart message. Use
// Message#isMultipart() if uncertain.
Multipart multipart = (Multipart) messageBuilder.getBody();
// Insert a new text/plain body part after every body part of the
// template.
final int count = multipart.getCount();
for (int i = 0; i < count; i++) {
String text = "Text inserted after part " + (i + 1);
BodyPart bodyPart = BodyPartBuilder.create()
.use(bodyFactory)
.setBody(text, Charsets.UTF_8)
.setContentTransferEncoding("quoted-printable")
.build();
multipart.addBodyPart(bodyPart, 2 * i + 1);
}
// For no particular reason remove the second binary body part (now
// at index four).
Entity removed = multipart.removeBodyPart(4);
// The removed body part no longer has a parent entity it belongs to so
// it should be disposed of.
removed.dispose();
// Set some headers on the transformed message
messageBuilder.generateMessageId(HOSTNAME);
messageBuilder.setSubject("Transformed message");
messageBuilder.setDate(new Date());
messageBuilder.setFrom("John Doe <jdoe@machine.example>");
Message transformed = messageBuilder.build();
MessageWriter writer = new DefaultMessageWriter();
// Print transformed message.
System.out.println("\n\nTransformed message:\n--------------------\n");
writer.writeMessage(transformed, System.out);
// Messages should be disposed of when they are no longer needed.
// Disposing of a message also disposes of all child elements (e.g. body
// parts) of the message.
transformed.dispose();
// Print original message to illustrate that it was not affected by the
// transformation.
System.out.println("\n\nOriginal template:\n------------------\n");
writer.writeMessage(template, System.out);
// Original message is no longer needed.
template.dispose();
// At this point all temporary files have been deleted because all
// messages and body parts have been disposed of properly.
- Распарсим письмо с вложениями и выведем информацию об элементах, из которых оно состоит в stdout:
public static void printAllPartsFromFile(String fileName)
throws IOException, MimeException {
List<Entity> allPartsFromFile = getAllPartsFromStream(
new FileInputStream(fileName)
);
int i=1;
for (Entity e : allPartsFromFile) {
System.out.printf("%d. FileName=%s\n", i++, e.getFilename());
System.out.printf(" Charset=%s\n", e.getCharset());
System.out.printf(" ContentTransferEncoding=%s\n",
e.getContentTransferEncoding());
System.out.printf(" MimeType=%s\n", e.getMimeType());
System.out.printf(" DispositionType=%s\n", e.getDispositionType());
System.out.printf(" Header=%s\n\n", e.getHeader());
if("text/plain".equals(e.getMimeType())) {
IOUtils.copy(((TextBody)e.getBody()).getReader(), System.out);
}
}
}
public static List<Entity> getAllPartsFromStream(InputStream stream)
throws IOException, MimeException {
MessageBuilder builder = new DefaultMessageBuilder();
org.apache.james.mime4j.dom.Message message = builder.parseMessage(stream);
return getAllParts(message);
}
public static List<Entity> getAllParts(Entity entity) {
List<Entity> ret = new ArrayList<>();
if (entity.isMultipart()) {
List<Entity> bodyParts = ((Multipart) entity.getBody()).getBodyParts();
for (Entity bodyPart : bodyParts) {
ret.add(bodyPart);
if(bodyPart.isMultipart()) {
ret.addAll(getAllParts(bodyPart));
}
}
} else {
ret.add(entity);
}
return ret;
}
Я не затронул Event-based API парсинга, но оно есть:
MimeTokenStream stream = new MimeTokenStream();
stream.parse(new FileInputStream("mime.msg"));
for (EntityState state = stream.getState();
state != EntityState.T_END_OF_STREAM;
state = stream.next()) {
switch (state) {
case T_BODY:
System.out.println("Body detected, contents = "
+ stream.getInputStream() + ", header data = "
+ stream.getBodyDescriptor());
break;
case T_FIELD:
System.out.println("Header field detected: "
+ stream.getField());
break;
case T_START_MULTIPART:
System.out.println("Multipart message detexted,"
+ " header data = "
+ stream.getBodyDescriptor());
...
}
}
Мое резюме - для работы с почтой и письмами на Java используйте MIME4J.
Впервые опубликовано в ЖЖ.