| // Copyright (C) 2016 The Android Open Source Project | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| package com.google.gerrit.mail; | |
| import static java.nio.charset.StandardCharsets.UTF_8; | |
| import com.google.common.base.Strings; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.common.collect.ImmutableSet; | |
| import com.google.common.io.CharStreams; | |
| import com.google.common.primitives.Ints; | |
| import com.google.gerrit.entities.Address; | |
| import java.io.ByteArrayInputStream; | |
| import java.io.IOException; | |
| import java.io.InputStreamReader; | |
| import java.time.Instant; | |
| import java.util.Locale; | |
| import org.apache.james.mime4j.MimeException; | |
| import org.apache.james.mime4j.dom.Entity; | |
| import org.apache.james.mime4j.dom.Message; | |
| import org.apache.james.mime4j.dom.MessageBuilder; | |
| import org.apache.james.mime4j.dom.Multipart; | |
| import org.apache.james.mime4j.dom.TextBody; | |
| import org.apache.james.mime4j.dom.address.Mailbox; | |
| import org.apache.james.mime4j.message.DefaultMessageBuilder; | |
| /** Parses raw email content received through POP3 or IMAP into an internal {@link MailMessage}. */ | |
| public class RawMailParser { | |
| private static final ImmutableSet<String> MAIN_HEADERS = | |
| ImmutableSet.of("to", "from", "cc", "date", "message-id", "subject", "content-type"); | |
| private RawMailParser() {} | |
| /** | |
| * Parses a MailMessage from a string. | |
| * | |
| * @param raw {@link String} payload as received over the wire | |
| * @return parsed {@link MailMessage} | |
| * @throws MailParsingException in case parsing fails | |
| */ | |
| public static MailMessage parse(String raw) throws MailParsingException { | |
| MailMessage.Builder messageBuilder = MailMessage.builder(); | |
| messageBuilder.rawContentUTF(raw); | |
| Message mimeMessage; | |
| try { | |
| MessageBuilder builder = new DefaultMessageBuilder(); | |
| mimeMessage = builder.parseMessage(new ByteArrayInputStream(raw.getBytes(UTF_8))); | |
| } catch (IOException | MimeException e) { | |
| throw new MailParsingException("Can't parse email", e); | |
| } | |
| // Add general headers | |
| if (mimeMessage.getMessageId() != null) { | |
| messageBuilder.id(mimeMessage.getMessageId()); | |
| } | |
| if (mimeMessage.getSubject() != null) { | |
| messageBuilder.subject(mimeMessage.getSubject()); | |
| } | |
| if (mimeMessage.getDate() != null) { | |
| @SuppressWarnings("JdkObsolete") | |
| Instant mimeMessageInstant = mimeMessage.getDate().toInstant(); | |
| messageBuilder.dateReceived(mimeMessageInstant); | |
| } | |
| // Add From, To and Cc | |
| if (mimeMessage.getFrom() != null && !mimeMessage.getFrom().isEmpty()) { | |
| Mailbox from = mimeMessage.getFrom().get(0); | |
| messageBuilder.from(Address.create(from.getName(), from.getAddress())); | |
| } | |
| if (mimeMessage.getTo() != null) { | |
| for (Mailbox m : mimeMessage.getTo().flatten()) { | |
| messageBuilder.addTo(Address.create(m.getName(), m.getAddress())); | |
| } | |
| } | |
| if (mimeMessage.getCc() != null) { | |
| for (Mailbox m : mimeMessage.getCc().flatten()) { | |
| messageBuilder.addCc(Address.create(m.getName(), m.getAddress())); | |
| } | |
| } | |
| // Add additional headers | |
| mimeMessage.getHeader().getFields().stream() | |
| .filter(f -> !MAIN_HEADERS.contains(f.getName().toLowerCase(Locale.US))) | |
| .forEach(f -> messageBuilder.addAdditionalHeader(f.getName() + ": " + f.getBody())); | |
| // Add text and html body parts | |
| StringBuilder textBuilder = new StringBuilder(); | |
| StringBuilder htmlBuilder = new StringBuilder(); | |
| try { | |
| handleMimePart(mimeMessage, textBuilder, htmlBuilder); | |
| } catch (IOException e) { | |
| throw new MailParsingException("Can't parse email", e); | |
| } | |
| messageBuilder.textContent(Strings.emptyToNull(textBuilder.toString())); | |
| messageBuilder.htmlContent(Strings.emptyToNull(htmlBuilder.toString())); | |
| try { | |
| // build() will only succeed if all required attributes were set. We wrap | |
| // the IllegalStateException in a MailParsingException indicating that | |
| // required attributes are missing, so that the caller doesn't fall over. | |
| return messageBuilder.build(); | |
| } catch (IllegalStateException e) { | |
| throw new MailParsingException("Missing required attributes after email was parsed", e); | |
| } | |
| } | |
| /** | |
| * Parses a MailMessage from an array of characters. Note that the character array is int-typed. | |
| * This method is only used by POP3, which specifies that all transferred characters are US-ASCII | |
| * (RFC 6856). When reading the input in Java, io.Reader yields ints. These can be safely | |
| * converted to chars as all US-ASCII characters fit in a char. If emails contain non-ASCII | |
| * characters, such as UTF runes, these will be encoded in ASCII using either Base64 or | |
| * quoted-printable encoding. | |
| * | |
| * @param chars Array as received over the wire | |
| * @return Parsed {@link MailMessage} | |
| * @throws MailParsingException in case parsing fails | |
| */ | |
| public static MailMessage parse(int[] chars) throws MailParsingException { | |
| StringBuilder b = new StringBuilder(chars.length); | |
| for (int c : chars) { | |
| b.append((char) c); | |
| } | |
| MailMessage.Builder messageBuilder = parse(b.toString()).toBuilder(); | |
| messageBuilder.rawContent(ImmutableList.copyOf(Ints.asList(chars))); | |
| return messageBuilder.build(); | |
| } | |
| /** | |
| * Traverses a mime tree and parses out text and html parts. All other parts will be dropped. | |
| * | |
| * @param part {@code MimePart} to parse | |
| * @param textBuilder {@link StringBuilder} to append all plaintext parts | |
| * @param htmlBuilder {@link StringBuilder} to append all html parts | |
| * @throws IOException in case of a failure while transforming the input to a {@link String} | |
| */ | |
| private static void handleMimePart( | |
| Entity part, StringBuilder textBuilder, StringBuilder htmlBuilder) throws IOException { | |
| if (isPlainOrHtml(part.getMimeType()) && !isAttachment(part.getDispositionType())) { | |
| TextBody tb = (TextBody) part.getBody(); | |
| String result = | |
| CharStreams.toString(new InputStreamReader(tb.getInputStream(), tb.getMimeCharset())); | |
| if (part.getMimeType().equals("text/plain")) { | |
| textBuilder.append(result); | |
| } else if (part.getMimeType().equals("text/html")) { | |
| htmlBuilder.append(result); | |
| } | |
| } else if (isMultipart(part.getMimeType())) { | |
| Multipart multipart = (Multipart) part.getBody(); | |
| for (Entity e : multipart.getBodyParts()) { | |
| handleMimePart(e, textBuilder, htmlBuilder); | |
| } | |
| } | |
| } | |
| private static boolean isPlainOrHtml(String mimeType) { | |
| return (mimeType.equals("text/plain") || mimeType.equals("text/html")); | |
| } | |
| private static boolean isMultipart(String mimeType) { | |
| return mimeType.startsWith("multipart/"); | |
| } | |
| private static boolean isAttachment(String dispositionType) { | |
| return dispositionType != null && dispositionType.equals("attachment"); | |
| } | |
| } |