| // 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"); |
| } |
| } |