I have this service that reads emails from a mailbox as they come and I want to extract its contents in a map structure. For now I am interested only in the actual email (no images or attachments).
I started by using an enum when I wanted something simple and then extended the enum to implement an interface when I noticed that I needed some complexity and at the end because the method extracting the content was a few lines long and the enum was getting hard to read I extracted the implementation to their own interface implementations and just assigned an instance of each in the enum.
It doesn't feel wrong, I actually think that this is what Spring looks like (very simply for conceptual purposes) when it creates singletons for autowiring but wanted to get an opinion by the community and if this is a valid approach, is there something I missed or something to consider
This is the interface of the extractor with a default method for something that is common to the implementations
public interface ContentExtractor {
Map<String, String> extract(Message mimeMessage) throws IOException, MessagingException;
default String extract(BodyPart bodyPart) {
try {
return (String)bodyPart.getContent();
}
catch (IOException | MessagingException e) {
LOGGER.warn("Couldn't cast part to a String. It's either a multipart body part or probably a content type that resolves to a stream.");
return null;
}
}
}
The abstract class for a base. I am actually divided on whether I need this, I don't actually remember why I added it...
@Getter
public abstract class MimeContentExtractor implements ContentExtractor {
private final String mimeType;
MimeContentExtractor(String mimeType) {this.mimeType = mimeType;}
static String getContentType(Part part) {
try {
return part.getContentType();
} catch (MessagingException e) {
LOGGER.warn("Couldn't get the content type of the part");
return "N/A";
}
}
}
The enum
@Getter
public enum MimeType {
MULTIPART(new MultipartContentExtractor("multipart/")),
TEXT(new TextContentExtractor("text/plain")),
HTML(new TextContentExtractor("text/html"));
private final MimeContentExtractor contentExtractor;
MimeType(MimeContentExtractor contentExtractor) {
this.contentExtractor = contentExtractor;
}
public static Optional<MimeContentExtractor> getExtractor(Part part) {
return determineMimeType(part)
.map(MimeType::getContentExtractor);
}
private static Optional<MimeType> determineMimeType(Part part) {
return Arrays.stream(values())
.filter(ct -> ct.matchesContentType(part))
.findFirst();
}
private boolean matchesContentType(Part part) {
try {
return part.getContentType().startsWith(getMimeType());
} catch (MessagingException e) {
LOGGER.warn("Failed to determine content type of {}", part);
return false;
}
}
public String getMimeType() {
return contentExtractor.getMimeType();
}
}
I have two implementations for content. The text/plain and text/html just override extract and return the content forcing it to be a string (Java mail, this is how it works) and the multipart/mixed is a bit more complex but basically goes through the parts, and uses the enum again to determine what extractor to use. For now it will recognise the text and use the aforementioned implementations.
public class MultipartContentExtractor extends MimeContentExtractor {
MultipartContentExtractor(String mimeType) {
super(mimeType);
}
@Override
public Map<String, String> extract(Message mimeMessage) throws IOException, MessagingException {
Multipart content = (Multipart)mimeMessage.getContent();
return extract(content);
}
private Map<String, String> extract(Multipart content) throws MessagingException {
int numberOfBodyParts = content.getCount();
LOGGER.info("The content has {} parts", numberOfBodyParts);
Map<String, String> bodyPartContents = new HashMap<>();
IntStream.range(0, numberOfBodyParts)
.boxed()
.peek(i -> LOGGER.info("Attempting to get body part {}", i))
.map(wrapWithOptional(content::getBodyPart))
.peek(bp -> LOGGER.info("Body part was {} retrieved", bp.isPresent() ? "" : "not"))
.filter(Optional::isPresent)
.map(Optional::get)
.forEach(bodyPart -> {
LOGGER.info("Content type of body part is {}", getContentType(bodyPart));
MimeType.getExtractor(bodyPart)
.ifPresent(ce -> bodyPartContents.put(ce.getMimeType(), ce.extract(bodyPart)));
});
return unmodifiableMap(bodyPartContents);
}
}
@Getter
public class TextContentExtractor extends MimeContentExtractor {
TextContentExtractor(String mimeType) {
super(mimeType);
}
@Override
public Map<String, String> extract(Message mimeMessage) throws IOException, MessagingException {
return singletonMap(getMimeType(), (String)mimeMessage.getContent());
}
}
Finally this is how it is used
@Component
public class BackofficeMailProcessor implements Processor {
...
@Override
public void process(Exchange exchange) {
MailMessage mailMessage = (MailMessage)exchange.getMessage();
Map<String, String> contents = getContents(mailMessage);
if (contents.isEmpty()) {
throw new BackofficeMailProcessorException("Couldn't get any of the contents of the email. Stopped processing!");
}
...
}
private Map<String, String> getContents(MailMessage mailMessage) {
final Message mailMessageBody = mailMessage.getMessage();
Optional<MimeContentExtractor> extractor = MimeType.getExtractor(mailMessageBody);
if (extractor.isPresent()) {
try {
return extractor.get().extract(mailMessageBody);
}
catch (IOException | MessagingException e) {
LOGGER.error("Failed to extract contents of email", e);
}
}
return emptyMap();
}
...
}
The fact that is an enum doesn't make it optimal for unit testing. This is part of a larger project and I did something similar for extracting reply for different kind of email clients (yahoo, gmail, etc) but there I also made a factory where I used the enum in the supplier (not a static one) and wired the factory where I would use the Enum in the above example.