All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.appengine.api.mail.stdimpl.GMTransport Maven / Gradle / Ivy

/*
 * Copyright 2021 Google LLC
 *
 * 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
 *
 *     https://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.appengine.api.mail.stdimpl;

import com.google.appengine.api.mail.MailService;
import com.google.appengine.api.mail.MailServiceFactory;
import com.google.common.base.Joiner;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.mail.Address;
import javax.mail.BodyPart;
import javax.mail.Header;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.SendFailedException;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.URLName;
import javax.mail.event.TransportEvent;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMessage.RecipientType;
import javax.mail.internet.MimeMultipart;

/**
 * Implementation of the 'Google Message Transport' which really just
 * connects to the exposed MailService and uses it to deliver a message.
 * 

* The special destination address "admins" results in a delivery * of the message to the owners of the application. *

* Note that most RFC822 headers are silently ignored. * * */ public class GMTransport extends Transport { private static final String FILENAME_PREVENTS_INLINING_PROPERTY = "appengine.mail.filenamePreventsInlining"; private static final String SUPPORT_EXTENDED_ATTACHMENT_ENCODINGS_PROPERTY = "appengine.mail.supportExtendedAttachmentEncodings"; private static final String ADMINS_ADDRESS = "admins"; // This field has to be in sync with the values present in the method // InitializeHeaderAllowlist of file apphosting/api/email_message_builder.cc. private static final String[] HEADERS_ALLOWLIST = { "Auto-Submitted", "In-Reply-To", "List-Id", "List-Unsubscribe", "List-Unsubscribe-Post", "On-Behalf-Of", "References", "Resent-Date", "Resent-From", "Resent-To" }; public GMTransport(Session session, URLName urlName) { super(session, urlName); } private static class SupportExtendedAttachmentEncodingsHolder { static final boolean INSTANCE = Boolean.getBoolean(SUPPORT_EXTENDED_ATTACHMENT_ENCODINGS_PROPERTY); } private static class FilenamePreventsInlininHolder { static final boolean INSTANCE = Boolean.getBoolean(FILENAME_PREVENTS_INLINING_PROPERTY); } /** {@inheritDoc} */ @Override protected boolean protocolConnect(String host, int port, String user, String password) { // dummy method, our mail transport mechanism is 'connection-less'. return true; } private boolean canInline(Message message, BodyPart bodyPart) throws MessagingException { if (!FilenamePreventsInlininHolder.INSTANCE) { return true; } // Always allow inlining of attachments explicitly set as "multipart/alternative" if (message.isMimeType("multipart/alternative")) { return true; } // If an attachment has a filename specified, we do not inline it as multipart/alternative if (bodyPart instanceof MimeBodyPart) { MimeBodyPart mimeBodyPart = (MimeBodyPart) bodyPart; return mimeBodyPart.getFileName() == null || mimeBodyPart.getFileName().isEmpty(); } return true; } /** {@inheritDoc} */ @Override public void sendMessage(Message message, Address[] addresses) throws MessagingException { MailService service = MailServiceFactory.getMailService(); MailService.Message msg = new MailService.Message(); // fill in message sender. If there is a Sender: header field we use // that, otherwise we use the From: field. String sender = null; if (message instanceof MimeMessage) { Address senderAddr = ((MimeMessage) message).getSender(); if (senderAddr != null) { sender = senderAddr.toString(); } } if (sender == null && message.getFrom() != null && message.getFrom().length > 0) { // there better be a from address, or we fail right here // in RFC822 it's okay to have multiple from addresses, but not for // Prometheus. We pick the first one only. sender = message.getFrom()[0].toString(); } // mail_service.cc does a string compare on this. so, be precise. msg.setSender(sender); // optionally fill in reply-to header verbatim, // keeping multiple addresses, if present // the default javaMail implementation returns the From: address // array, if no Reply-To: header is present, which gives us a way // to preserve multiple from addresses try { msg.setReplyTo(Joiner.on(", ").useForNull("null").join(message.getReplyTo())); } catch (NullPointerException e) { // don't do anything, the header field will stay unset } //is this intended to go to Admins? boolean toAdmins = false; Address[] allRecipients = message.getAllRecipients(); if (allRecipients != null) { for (Address addr : allRecipients) { if (ADMINS_ADDRESS.equals(addr.toString())) { toAdmins = true; } } } // fill in To:, Cc:, Bcc: header, if present. // Otherwise they will be (re-set) to null. if (!toAdmins) { Set allAddresses = new HashSet(); for (Address addr : addresses) { allAddresses.add(addr.toString()); } msg.setTo(convertAddressFields(message.getRecipients(RecipientType.TO), allAddresses)); msg.setCc(convertAddressFields(message.getRecipients(RecipientType.CC), allAddresses)); msg.setBcc(convertAddressFields(message.getRecipients(RecipientType.BCC), allAddresses)); } // subject msg.setSubject(message.getSubject()); //text and html bodies Object textObject = null; Object htmlObject = null; Object ampHtmlObject = null; String textType = null; String htmlType = null; String ampHtmlType = null; Multipart otherMessageParts = null; // headers List headers = new ArrayList(); // The message exposes all fields as headers so we need to fetch only those // that are allowlisted. @SuppressWarnings("unchecked") List

originalHeaders = Collections.list(message.getMatchingHeaders(HEADERS_ALLOWLIST)); for (Header header : originalHeaders) { headers.add(new MailService.Header(header.getName(), header.getValue())); } msg.setHeaders(headers); if (message.getContentType() == null) { // it's not defined, and it's not a MimeMessage (which would force this // to be text/plain) -- treat this as a 'plain' text body. try { textObject = message.getContent(); textType = message.getContentType(); } catch (IOException e) { throw new MessagingException("Getting typeless content failed", e); } } else if (message.isMimeType("text/html")) { try { htmlObject = message.getContent(); htmlType = message.getContentType(); } catch (IOException e) { throw new MessagingException("Getting html content failed", e); } } else if (message.isMimeType("text/*")) { // text body (could be anything, but we treat it as plain). Otherwise, // we'd have to trash all but plain explicitly, which I find rude. try { textObject = message.getContent(); textType = message.getContentType(); } catch (IOException e) { throw new MessagingException("Getting text/* content failed", e); } } else if (message.isMimeType("multipart/*")) { // ah, now the fun starts once again. lets find a first plain and/or // html part to use them as the message body. we also clone the multipart // since we don't want to modify the original input by deleting parts. // this will also give you a runtime exception if the returned object // is not really a Multipart. Multipart mp; try { mp = (Multipart) message.getContent(); for (int i = 0; i < mp.getCount(); i++) { BodyPart bp = mp.getBodyPart(i); if (bp.isMimeType("text/plain") && textObject == null && canInline(message, bp)) { textObject = bp.getContent(); textType = bp.getContentType(); } else if (bp.isMimeType("text/html") && htmlObject == null && canInline(message, bp)) { htmlObject = bp.getContent(); htmlType = bp.getContentType(); } else if (bp.isMimeType("text/x-amp-html") && ampHtmlObject == null && canInline(message, bp)) { ampHtmlObject = bp.getContent(); ampHtmlType = bp.getContentType(); } else { if (otherMessageParts == null) { String type = mp.getContentType(); assert (type.startsWith("multipart/")); otherMessageParts = new MimeMultipart( type.substring("multipart/".length())); } otherMessageParts.addBodyPart(bp); } } } catch (IOException e) { throw new MessagingException("Getting multipart content failed", e); } } if (textObject != null) { msg.setTextBody(convertAttachmentToString(textObject, textType)); } if (htmlObject != null) { msg.setHtmlBody(convertAttachmentToString(htmlObject, htmlType)); } if (ampHtmlObject != null) { msg.setAmpHtmlBody(convertAttachmentToString(ampHtmlObject, ampHtmlType)); } // convert arbitrary attachments // fail for missing filenames if (otherMessageParts != null) { msg.setAttachments(convertAttachments(otherMessageParts)); } try { if (toAdmins) { service.sendToAdmins(msg); } else { service.send(msg); } } catch (IOException e) { notifyTransportListeners( TransportEvent.MESSAGE_NOT_DELIVERED, new Address[0], addresses, new Address[0], message); throw new SendFailedException("MailService IO failed", e); } catch (IllegalArgumentException e) { throw new MessagingException("Illegal Arguments", e); } notifyTransportListeners( TransportEvent.MESSAGE_DELIVERED, addresses, new Address[0], new Address[0], message); } /** * Returns the attachments in 'multipart' as an ArrayList, converting String, byte[], InputStream * and nested attachments. * * @param multipart The input list of attachments * @return An ArrayList with all attachments * @throws MessagingException if the conversion fails due to unsupported or invalid encodings */ ArrayList convertAttachments(Multipart multipart) throws MessagingException { ArrayList result = new ArrayList<>(); convertAttachments(multipart, result); return result; } /** * Adds the attachment in "multipart" to "result", converting String, byte[], InputStream and * nested attachments. * @param multipart The input list of attachments * @param result The output ArrayList that the attachments get added to * @throws MessagingException if the conversion fails due to unsupported or invalid encodings */ private void convertAttachments(Multipart multipart, List result) throws MessagingException { for (int i = 0; i < multipart.getCount(); i++) { BodyPart bp = multipart.getBodyPart(i); String name = bp.getFileName(); byte[] data; try { Object o = bp.getContent(); if (o instanceof InputStream) { data = inputStreamToBytes((InputStream) o); } else if (o instanceof String) { data = ((String) o).getBytes(); } else if (SupportExtendedAttachmentEncodingsHolder.INSTANCE && o instanceof byte[]) { data = (byte[]) o; } else if (SupportExtendedAttachmentEncodingsHolder.INSTANCE && o instanceof Multipart) { convertAttachments((Multipart) o, result); continue; } else { throw new MessagingException("Converting attachment data failed"); } } catch (IOException e) { throw new MessagingException("Extracting attachment data failed", e); } String contentID = null; String[] contentIDHeaders = bp.getHeader("content-id"); if (contentIDHeaders != null) { contentID = contentIDHeaders[0]; } MailService.Attachment attachment = new MailService.Attachment(name, data, contentID); result.add(attachment); } } // The Apache Geronimo implementation of Transport.send() puts // transport objects in a HashMap, so we need to implement // hashCode() and equals() if we want to do the right thing. @Override public int hashCode() { return session.hashCode() * 13 + url.hashCode(); } @Override public boolean equals(Object obj) { if (obj instanceof GMTransport) { GMTransport transport = (GMTransport) obj; return session.equals(transport.session) && url.equals(transport.url); } return false; } // is there a more compact way of doing this? /** * Converts an array of addresses into a collection of strings representing * those addresses * @param targetAddrs addresses to be converted * @param allAddrs all addresses for this transport * @return A collection of strings representing the intersection * between targetAddrs and allAddrs. */ private Collection convertAddressFields(Address[] targetAddrs, Set allAddrs) { if (targetAddrs == null || targetAddrs.length == 0) { return null; } ArrayList ourAddrs = new ArrayList(targetAddrs.length); for (Address addr : targetAddrs) { String email = addr.toString(); if (allAddrs.contains(email)) { ourAddrs.add(email); } } return ourAddrs; } /** * Gets all the available data in a String, a byte[] or an InputStream and returns it as a String * using the character set specified in the type parameter. * * @param attachmentData The string, byte array or input stream to be read. * @param type The encoding type of the data. * @return A String containing the data. * @throws MessagingException if the data type of attachmentData is unsupported */ private String convertAttachmentToString(Object attachmentData, String type) throws MessagingException { String charset = null; String[] args = type.split(";"); for (String arg : args) { if (arg.trim().startsWith("charset=")) { charset = arg.split("=")[1]; break; } } try { byte[] attachmentBytes = null; if (attachmentData instanceof String) { return (String) attachmentData; } else if (SupportExtendedAttachmentEncodingsHolder.INSTANCE && attachmentData instanceof byte[]) { attachmentBytes = (byte[]) attachmentData; } else if (attachmentData instanceof InputStream) { attachmentBytes = inputStreamToBytes((InputStream) attachmentData); } else { throw new MessagingException("Converting body of type " + type + " failed"); } if (charset != null) { return new String(attachmentBytes, charset); } else { return new String(attachmentBytes); } } catch (UnsupportedEncodingException e) { throw new MessagingException("Unsupported charset: " + charset, e); } catch (IOException e) { throw new MessagingException("Stringifying body of type " + type + " failed", e); } } /** * Gets all the available data in an InputStream and returns it as a byte * array. * @param in The input stream to be read. * @return A byte array containing the data. * @throws IOException If there is a problem with the input stream. */ private byte[] inputStreamToBytes(InputStream in) throws IOException { byte[] bytes = new byte[in.available()]; /* int count = */ in.read(bytes); return bytes; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy