Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.google.appengine.api.mail;

/**
* An interface for providing environment variables.
*/
interface EnvironmentProvider {
/**
* Gets the value of the specified environment variable.
* @param name the name of the environment variable
* @return the string value of the variable, or {@code null} if the variable is not defined
*/
String getenv(String name);

/**
* Gets the value of the specified environment variable, returning a default value if the
* variable is not defined.
* @param name the name of the environment variable
* @param defaultValue the default value to return
* @return the string value of the variable, or the default value if the variable is not defined
*/
String getenv(String name, String defaultValue);
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,26 @@
import com.google.apphosting.api.ApiProxy;
import com.google.protobuf.ByteString;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Properties;
import javax.activation.DataHandler;
import javax.activation.DataSource;
import javax.mail.Address;
import javax.mail.BodyPart;
import javax.mail.Message.RecipientType;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
import javax.mail.util.ByteArrayDataSource;

/**
* This class implements raw access to the mail service.
Expand All @@ -31,9 +51,12 @@
* convenience methods that JavaMail provides.
*
*/
class MailServiceImpl implements MailService {
class LegacyMailServiceImpl implements MailService {
static final String PACKAGE = "mail";

/** Default constructor. */
LegacyMailServiceImpl() {}

/** {@inheritDoc} */
@Override
public void sendToAdmins(Message message)
Expand All @@ -47,7 +70,7 @@ public void send(Message message)
throws IllegalArgumentException, IOException {
doSend(message, false);
}

/**
* Does the actual sending of the message.
* @param message The message to be sent.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,22 @@
*/
final class MailServiceFactoryImpl implements IMailServiceFactory {

private final EnvironmentProvider envProvider;

MailServiceFactoryImpl() {
this.envProvider = new SystemEnvironmentProvider();
}

// For testing
MailServiceFactoryImpl(EnvironmentProvider envProvider) {
this.envProvider = envProvider;
}

@Override
public MailService getMailService() {
return new MailServiceImpl();
if ("true".equals(envProvider.getenv("APPENGINE_USE_SMTP_MAIL_SERVICE"))) {
return new SmtpMailServiceImpl(envProvider);
}
return new LegacyMailServiceImpl();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
/*
* 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;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Properties;
import javax.activation.DataHandler;
import javax.activation.DataSource;
import javax.mail.Address;
import javax.mail.Message.RecipientType;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
import javax.mail.util.ByteArrayDataSource;

/**
* This class implements the MailService interface using an external SMTP server.
*/
class SmtpMailServiceImpl implements MailService {
private final EnvironmentProvider envProvider;

/**
* Constructor.
* @param envProvider The provider for environment variables.
*/
SmtpMailServiceImpl(EnvironmentProvider envProvider) {
this.envProvider = envProvider;
}

@Override
public void send(Message message) throws IOException {
sendSmtp(message, false);
}

@Override
public void sendToAdmins(Message message) throws IOException {
sendSmtp(message, true);
}

private void sendSmtp(Message message, boolean toAdmin)
throws IllegalArgumentException, IOException {
String smtpHost = envProvider.getenv("APPENGINE_SMTP_HOST");
if (smtpHost == null || smtpHost.isEmpty()) {
throw new IllegalArgumentException("SMTP_HOST environment variable is not set.");
}
Properties props = new Properties();
props.put("mail.smtp.host", smtpHost);
props.put("mail.smtp.port", envProvider.getenv("APPENGINE_SMTP_PORT"));
props.put("mail.smtp.auth", "true");
if (Boolean.parseBoolean(envProvider.getenv("APPENGINE_SMTP_USE_TLS"))) {
props.put("mail.smtp.starttls.enable", "true");
}

Session session = Session.getInstance(props, new javax.mail.Authenticator() {
protected javax.mail.PasswordAuthentication getPasswordAuthentication() {
return new javax.mail.PasswordAuthentication(
envProvider.getenv("APPENGINE_SMTP_USER"), envProvider.getenv("APPENGINE_SMTP_PASSWORD"));
}
});

try {
MimeMessage mimeMessage = new MimeMessage(session);
mimeMessage.setFrom(new InternetAddress(message.getSender()));

List<InternetAddress> toRecipients = new ArrayList<>();
List<InternetAddress> ccRecipients = new ArrayList<>();
List<InternetAddress> bccRecipients = new ArrayList<>();

if (toAdmin) {
String adminRecipients = envProvider.getenv("APPENGINE_ADMIN_EMAIL_RECIPIENTS");
if (adminRecipients == null || adminRecipients.isEmpty()) {
throw new IllegalArgumentException("Admin recipients not configured.");
}
toRecipients.addAll(Arrays.asList(InternetAddress.parse(adminRecipients)));
} else {
if (message.getTo() != null) {
toRecipients.addAll(toInternetAddressList(message.getTo()));
}
if (message.getCc() != null) {
ccRecipients.addAll(toInternetAddressList(message.getCc()));
}
if (message.getBcc() != null) {
bccRecipients.addAll(toInternetAddressList(message.getBcc()));
}
}

List<Address> allTransportRecipients = new ArrayList<>();
allTransportRecipients.addAll(toRecipients);
allTransportRecipients.addAll(ccRecipients);
allTransportRecipients.addAll(bccRecipients);

if (allTransportRecipients.isEmpty()) {
throw new IllegalArgumentException("No recipients specified.");
}

if (!toRecipients.isEmpty()) {
mimeMessage.setRecipients(RecipientType.TO, toRecipients.toArray(new Address[0]));
}
if (!ccRecipients.isEmpty()) {
mimeMessage.setRecipients(RecipientType.CC, ccRecipients.toArray(new Address[0]));
}

if (message.getReplyTo() != null) {
mimeMessage.setReplyTo(new Address[] {new InternetAddress(message.getReplyTo())});
}

mimeMessage.setSubject(message.getSubject());

final boolean hasAttachments = message.getAttachments() != null && !message.getAttachments().isEmpty();
final boolean hasHtmlBody = message.getHtmlBody() != null;
final boolean hasAmpHtmlBody = message.getAmpHtmlBody() != null;
final boolean hasTextBody = message.getTextBody() != null;

if (hasTextBody && !hasHtmlBody && !hasAmpHtmlBody && !hasAttachments) {
mimeMessage.setText(message.getTextBody());
} else {
MimeMultipart topLevelMultipart = new MimeMultipart("mixed");

if (hasTextBody || hasHtmlBody || hasAmpHtmlBody) {
MimeMultipart alternativeMultipart = new MimeMultipart("alternative");
MimeBodyPart alternativeBodyPart = new MimeBodyPart();
alternativeBodyPart.setContent(alternativeMultipart);

if (hasTextBody) {
MimeBodyPart textPart = new MimeBodyPart();
textPart.setText(message.getTextBody());
alternativeMultipart.addBodyPart(textPart);
} else if (hasHtmlBody) {
MimeBodyPart textPart = new MimeBodyPart();
textPart.setText("");
alternativeMultipart.addBodyPart(textPart);
}

if (hasHtmlBody) {
MimeBodyPart htmlPart = new MimeBodyPart();
htmlPart.setContent(message.getHtmlBody(), "text/html");
alternativeMultipart.addBodyPart(htmlPart);
}
if (hasAmpHtmlBody) {
MimeBodyPart ampPart = new MimeBodyPart();
ampPart.setContent(message.getAmpHtmlBody(), "text/x-amp-html");
alternativeMultipart.addBodyPart(ampPart);
}
topLevelMultipart.addBodyPart(alternativeBodyPart);
}

if (hasAttachments) {
for (Attachment attachment : message.getAttachments()) {
MimeBodyPart attachmentBodyPart = new MimeBodyPart();
DataSource source =
new ByteArrayDataSource(attachment.getData(), "application/octet-stream");
attachmentBodyPart.setDataHandler(new DataHandler(source));
attachmentBodyPart.setFileName(attachment.getFileName());
if (attachment.getContentID() != null) {
attachmentBodyPart.setContentID(attachment.getContentID());
}
topLevelMultipart.addBodyPart(attachmentBodyPart);
}
}
mimeMessage.setContent(topLevelMultipart);
}

if (message.getHeaders() != null) {
for (Header header : message.getHeaders()) {
mimeMessage.addHeader(header.getName(), header.getValue());
}
}

mimeMessage.saveChanges();

Transport transport = session.getTransport("smtp");
try {
transport.connect();
transport.sendMessage(mimeMessage, allTransportRecipients.toArray(new Address[0]));
} finally {
if (transport != null) {
transport.close();
}
}

} catch (MessagingException e) {
if (e instanceof javax.mail.AuthenticationFailedException) {
throw new IllegalArgumentException("SMTP authentication failed: " + e.getMessage(), e);
}
throw new IOException("Error sending email via SMTP: " + e.getMessage(), e);
}
}

private List<InternetAddress> toInternetAddressList(Collection<String> addresses)
throws AddressException {
List<InternetAddress> list = new ArrayList<>();
for (String address : addresses) {
list.add(new InternetAddress(address));
}
return list;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.google.appengine.api.mail;

/**
* A simple wrapper around {@link System} to allow for easier testing.
*/
class SystemEnvironmentProvider implements EnvironmentProvider {
/**
* Gets the value of the specified environment variable.
* @param name the name of the environment variable
* @return the string value of the variable, or {@code null} if the variable is not defined
*/
@Override
public String getenv(String name) {
return System.getenv(name);
}

/**
* Gets the value of the specified environment variable, returning a default value if the
* variable is not defined.
* @param name the name of the environment variable
* @param defaultValue the default value to return
* @return the string value of the variable, or the default value if the variable is not defined
*/
@Override
public String getenv(String name, String defaultValue) {
String value = System.getenv(name);
return value != null ? value : defaultValue;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.google.appengine.api.mail;

import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.when;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;

@RunWith(MockitoJUnitRunner.class)
public class MailServiceFactoryImplTest {

@Mock private EnvironmentProvider envProvider;

@Test
public void testGetMailService_smtp() {
when(envProvider.getenv("APPENGINE_USE_SMTP_MAIL_SERVICE")).thenReturn("true");
MailServiceFactoryImpl factory = new MailServiceFactoryImpl(envProvider);
assertTrue(factory.getMailService() instanceof SmtpMailServiceImpl);
}

@Test
public void testGetMailService_legacy() {
when(envProvider.getenv("APPENGINE_USE_SMTP_MAIL_SERVICE")).thenReturn("false");
MailServiceFactoryImpl factory = new MailServiceFactoryImpl(envProvider);
assertTrue(factory.getMailService() instanceof LegacyMailServiceImpl);
}

@Test
public void testGetMailService_legacy_null() {
when(envProvider.getenv("APPENGINE_USE_SMTP_MAIL_SERVICE")).thenReturn(null);
MailServiceFactoryImpl factory = new MailServiceFactoryImpl(envProvider);
assertTrue(factory.getMailService() instanceof LegacyMailServiceImpl);
}
}
Loading
Loading