Константин Рядов Инженер JVM, ушедший в инженерию ИИ. То, что я сделал.

MIME4J - парсим e-mail письма без боли

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

Подпроекты Apache James

Одна из библиотек (саб-проект) это MIME4J для работы с email-письмами в Event, DOM и прочих API-стилях.

А почему не javax.mail, спросите вы?

К сожалению, javax.mail ориентирован на работу с письмами в жесткой связке с почтовой сессией. А ваша задача, например, распарсить или, наоборот, сформировать письмо из файла / в файл, или работать с потоком, буфером.
Вот, например, разные PGP/GnuPG-based решения любят шифровать полноценный multipart/mixed в текстовом виде, в котором все сущности типа inline. А ваша задача - вытащить и разложить по полочкам. Вот тут на помощь и приходит Apache James. Все примеры ниже можно посмотреть здесь.

Ну, собственно, к коду.

  1. Создадим простое 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();
}
  1. Создадим сложное 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();
}
  1. Создадим шаблон письма и используем для создания письма на основании шаблона (код для формирования рассылки?):
// 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.
  1. Распарсим письмо с вложениями и выведем информацию об элементах, из которых оно состоит в 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.


Впервые опубликовано в ЖЖ.

Все записи