Skip to content

Commit 6655bb2

Browse files
committed
This is Beta work, so we need to remove it from head in Github, and we are now working of a beta branch which has the change https://github.com/GoogleCloudPlatform/appengine-java-standard/tree/2.0.39-beta-wip
PiperOrigin-RevId: 875057047 Change-Id: I4dd7bf6553ebafac540986bb2b9ad56062da6b54
1 parent 7c6dd14 commit 6655bb2

9 files changed

Lines changed: 1044 additions & 18 deletions

File tree

api/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@
139139
</dependency>
140140
<dependency>
141141
<groupId>org.mockito</groupId>
142-
<artifactId>mockito-junit-jupiter</artifactId>
142+
<artifactId>mockito-core</artifactId>
143143
<scope>test</scope>
144144
</dependency>
145145
<dependency>
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright 2021 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.appengine.api;
18+
19+
/** An interface for providing environment variables. */
20+
public interface EnvironmentProvider {
21+
/**
22+
* Gets the value of the specified environment variable.
23+
*
24+
* @param name the name of the environment variable
25+
* @return the string value of the variable, or {@code null} if the variable is not defined
26+
*/
27+
String getenv(String name);
28+
29+
/**
30+
* Gets the value of the specified environment variable, returning a default value if the variable
31+
* is not defined.
32+
*
33+
* @param name the name of the environment variable
34+
* @param defaultValue the default value to return
35+
* @return the string value of the variable, or the default value if the variable is not defined
36+
*/
37+
String getenv(String name, String defaultValue);
38+
}

api/src/main/java/com/google/appengine/api/mail/MailServiceFactoryImpl.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2021 Google LLC
2+
* Copyright 2025 Google LLC
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,13 +16,31 @@
1616

1717
package com.google.appengine.api.mail;
1818

19+
import com.google.appengine.api.EnvironmentProvider;
20+
1921
/**
2022
* Factory for creating a {@link MailService}.
2123
*/
2224
final class MailServiceFactoryImpl implements IMailServiceFactory {
2325

26+
private static final String APPENGINE_USE_SMTP_MAIL_SERVICE_ENV = "APPENGINE_USE_SMTP_MAIL_SERVICE";
27+
private final EnvironmentProvider envProvider;
28+
29+
MailServiceFactoryImpl() {
30+
this(new SystemEnvironmentProvider());
31+
}
32+
33+
// For testing
34+
MailServiceFactoryImpl(EnvironmentProvider envProvider) {
35+
this.envProvider = envProvider;
36+
}
37+
2438
@Override
39+
@SuppressWarnings("YodaCondition")
2540
public MailService getMailService() {
41+
if ("true".equals(envProvider.getenv(APPENGINE_USE_SMTP_MAIL_SERVICE_ENV))) {
42+
return new SmtpMailServiceImpl(envProvider);
43+
}
2644
return new MailServiceImpl();
2745
}
2846
}

api/src/main/java/com/google/appengine/api/mail/MailServiceImpl.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,16 @@
2525
import java.io.IOException;
2626

2727
/**
28-
* This class implements raw access to the mail service.
29-
* Applications that don't want to make use of Sun's JavaMail
30-
* can use it directly -- but they will forego the typing and
31-
* convenience methods that JavaMail provides.
32-
*
28+
* This class implements raw access to the mail service. Applications that don't want to make use of
29+
* Sun's JavaMail can use it directly -- but they will forego the typing and convenience methods
30+
* that JavaMail provides.
3331
*/
3432
class MailServiceImpl implements MailService {
3533
static final String PACKAGE = "mail";
3634

35+
/** Default constructor. */
36+
MailServiceImpl() {}
37+
3738
/** {@inheritDoc} */
3839
@Override
3940
public void sendToAdmins(Message message)
@@ -47,7 +48,7 @@ public void send(Message message)
4748
throws IllegalArgumentException, IOException {
4849
doSend(message, false);
4950
}
50-
51+
5152
/**
5253
* Does the actual sending of the message.
5354
* @param message The message to be sent.
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
/*
2+
* Copyright 2021 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.appengine.api.mail;
18+
19+
import static com.google.common.base.Strings.isNullOrEmpty;
20+
import static com.google.common.collect.ImmutableList.toImmutableList;
21+
22+
import com.google.appengine.api.EnvironmentProvider;
23+
import com.google.common.collect.ImmutableList;
24+
import java.io.IOException;
25+
import java.util.ArrayList;
26+
import java.util.Arrays;
27+
import java.util.Collection;
28+
import java.util.List;
29+
import java.util.Properties;
30+
import javax.activation.DataHandler;
31+
import javax.activation.DataSource;
32+
import javax.mail.Address;
33+
import javax.mail.AuthenticationFailedException;
34+
import javax.mail.Authenticator;
35+
import javax.mail.Message.RecipientType;
36+
import javax.mail.MessagingException;
37+
import javax.mail.PasswordAuthentication;
38+
import javax.mail.Session;
39+
import javax.mail.Transport;
40+
import javax.mail.internet.AddressException;
41+
import javax.mail.internet.InternetAddress;
42+
import javax.mail.internet.MimeBodyPart;
43+
import javax.mail.internet.MimeMessage;
44+
import javax.mail.internet.MimeMultipart;
45+
import javax.mail.util.ByteArrayDataSource;
46+
47+
/** This class implements the MailService interface using an external SMTP server. */
48+
class SmtpMailServiceImpl implements MailService {
49+
private static final String SMTP_HOST_PROPERTY = "mail.smtp.host";
50+
private static final String SMTP_PORT_PROPERTY = "mail.smtp.port";
51+
private static final String SMTP_AUTH_PROPERTY = "mail.smtp.auth";
52+
private static final String SMTP_STARTTLS_ENABLE_PROPERTY = "mail.smtp.starttls.enable";
53+
private static final String APPENGINE_SMTP_HOST_ENV = "APPENGINE_SMTP_HOST";
54+
private static final String APPENGINE_SMTP_PORT_ENV = "APPENGINE_SMTP_PORT";
55+
private static final String APPENGINE_SMTP_USER_ENV = "APPENGINE_SMTP_USER";
56+
private static final String APPENGINE_SMTP_PASSWORD_ENV = "APPENGINE_SMTP_PASSWORD";
57+
private static final String APPENGINE_SMTP_USE_TLS_ENV = "APPENGINE_SMTP_USE_TLS";
58+
private static final String APPENGINE_ADMIN_EMAIL_RECIPIENTS_ENV =
59+
"APPENGINE_ADMIN_EMAIL_RECIPIENTS";
60+
61+
private final EnvironmentProvider envProvider;
62+
private final Session session;
63+
64+
/**
65+
* Constructor.
66+
*
67+
* @param envProvider The provider for environment variables.
68+
*/
69+
SmtpMailServiceImpl(EnvironmentProvider envProvider) {
70+
this(envProvider, createSession(envProvider));
71+
}
72+
73+
/** Constructor for testing. */
74+
SmtpMailServiceImpl(EnvironmentProvider envProvider, Session session) {
75+
this.envProvider = envProvider;
76+
this.session = session;
77+
}
78+
79+
private static Session createSession(EnvironmentProvider envProvider) {
80+
Properties props = new Properties();
81+
props.put(SMTP_HOST_PROPERTY, envProvider.getenv(APPENGINE_SMTP_HOST_ENV));
82+
props.put(SMTP_PORT_PROPERTY, envProvider.getenv(APPENGINE_SMTP_PORT_ENV));
83+
props.put(SMTP_AUTH_PROPERTY, "true");
84+
if (Boolean.parseBoolean(envProvider.getenv(APPENGINE_SMTP_USE_TLS_ENV))) {
85+
props.put(SMTP_STARTTLS_ENABLE_PROPERTY, "true");
86+
}
87+
88+
return Session.getInstance(
89+
props,
90+
new Authenticator() {
91+
@Override
92+
protected PasswordAuthentication getPasswordAuthentication() {
93+
return new PasswordAuthentication(
94+
envProvider.getenv(APPENGINE_SMTP_USER_ENV),
95+
envProvider.getenv(APPENGINE_SMTP_PASSWORD_ENV));
96+
}
97+
});
98+
}
99+
100+
@Override
101+
public void send(Message message) throws IOException {
102+
sendSmtp(message, false);
103+
}
104+
105+
@Override
106+
public void sendToAdmins(Message message) throws IOException {
107+
sendSmtp(message, true);
108+
}
109+
110+
private void sendSmtp(Message message, boolean toAdmin)
111+
throws IllegalArgumentException, IOException {
112+
String smtpHost = envProvider.getenv(APPENGINE_SMTP_HOST_ENV);
113+
if (isNullOrEmpty(smtpHost)) {
114+
throw new IllegalArgumentException("SMTP_HOST environment variable is not set.");
115+
}
116+
117+
try {
118+
MimeMessage mimeMessage = new MimeMessage(this.session);
119+
mimeMessage.setFrom(new InternetAddress(message.getSender()));
120+
121+
List<InternetAddress> toRecipients = new ArrayList<>();
122+
List<InternetAddress> ccRecipients = new ArrayList<>();
123+
List<InternetAddress> bccRecipients = new ArrayList<>();
124+
125+
if (toAdmin) {
126+
String adminRecipients = envProvider.getenv(APPENGINE_ADMIN_EMAIL_RECIPIENTS_ENV);
127+
if (adminRecipients == null || adminRecipients.isEmpty()) {
128+
throw new IllegalArgumentException("Admin recipients not configured.");
129+
}
130+
toRecipients.addAll(Arrays.asList(InternetAddress.parse(adminRecipients)));
131+
} else {
132+
if (message.getTo() != null) {
133+
toRecipients.addAll(toInternetAddressList(message.getTo()));
134+
}
135+
if (message.getCc() != null) {
136+
ccRecipients.addAll(toInternetAddressList(message.getCc()));
137+
}
138+
if (message.getBcc() != null) {
139+
bccRecipients.addAll(toInternetAddressList(message.getBcc()));
140+
}
141+
}
142+
143+
List<Address> allTransportRecipients = new ArrayList<>();
144+
allTransportRecipients.addAll(toRecipients);
145+
allTransportRecipients.addAll(ccRecipients);
146+
allTransportRecipients.addAll(bccRecipients);
147+
148+
if (allTransportRecipients.isEmpty()) {
149+
throw new IllegalArgumentException("No recipients specified.");
150+
}
151+
152+
if (!toRecipients.isEmpty()) {
153+
mimeMessage.setRecipients(RecipientType.TO, toRecipients.toArray(new Address[0]));
154+
}
155+
if (!ccRecipients.isEmpty()) {
156+
mimeMessage.setRecipients(RecipientType.CC, ccRecipients.toArray(new Address[0]));
157+
}
158+
159+
if (message.getReplyTo() != null) {
160+
mimeMessage.setReplyTo(new Address[] {new InternetAddress(message.getReplyTo())});
161+
}
162+
163+
mimeMessage.setSubject(message.getSubject());
164+
165+
final boolean hasAttachments =
166+
message.getAttachments() != null && !message.getAttachments().isEmpty();
167+
final boolean hasHtmlBody = message.getHtmlBody() != null;
168+
final boolean hasAmpHtmlBody = message.getAmpHtmlBody() != null;
169+
final boolean hasTextBody = message.getTextBody() != null;
170+
171+
if (hasTextBody && !hasHtmlBody && !hasAmpHtmlBody && !hasAttachments) {
172+
mimeMessage.setText(message.getTextBody());
173+
} else {
174+
MimeMultipart topLevelMultipart = new MimeMultipart("mixed");
175+
176+
if (hasTextBody || hasHtmlBody || hasAmpHtmlBody) {
177+
MimeMultipart alternativeMultipart = new MimeMultipart("alternative");
178+
MimeBodyPart alternativeBodyPart = new MimeBodyPart();
179+
alternativeBodyPart.setContent(alternativeMultipart);
180+
181+
if (hasTextBody) {
182+
MimeBodyPart textPart = new MimeBodyPart();
183+
textPart.setText(message.getTextBody());
184+
alternativeMultipart.addBodyPart(textPart);
185+
} else if (hasHtmlBody) {
186+
MimeBodyPart textPart = new MimeBodyPart();
187+
textPart.setText("");
188+
alternativeMultipart.addBodyPart(textPart);
189+
}
190+
191+
if (hasHtmlBody) {
192+
MimeBodyPart htmlPart = new MimeBodyPart();
193+
htmlPart.setContent(message.getHtmlBody(), "text/html");
194+
alternativeMultipart.addBodyPart(htmlPart);
195+
}
196+
if (hasAmpHtmlBody) {
197+
MimeBodyPart ampPart = new MimeBodyPart();
198+
ampPart.setContent(message.getAmpHtmlBody(), "text/x-amp-html");
199+
alternativeMultipart.addBodyPart(ampPart);
200+
}
201+
topLevelMultipart.addBodyPart(alternativeBodyPart);
202+
}
203+
204+
if (hasAttachments) {
205+
for (Attachment attachment : message.getAttachments()) {
206+
MimeBodyPart attachmentBodyPart = new MimeBodyPart();
207+
DataSource source =
208+
new ByteArrayDataSource(attachment.getData(), "application/octet-stream");
209+
attachmentBodyPart.setDataHandler(new DataHandler(source));
210+
attachmentBodyPart.setFileName(attachment.getFileName());
211+
if (attachment.getContentID() != null) {
212+
attachmentBodyPart.setContentID(attachment.getContentID());
213+
}
214+
topLevelMultipart.addBodyPart(attachmentBodyPart);
215+
}
216+
}
217+
mimeMessage.setContent(topLevelMultipart);
218+
}
219+
220+
if (message.getHeaders() != null) {
221+
for (Header header : message.getHeaders()) {
222+
mimeMessage.addHeader(header.getName(), header.getValue());
223+
}
224+
}
225+
226+
mimeMessage.saveChanges();
227+
228+
Transport transport = this.session.getTransport("smtp");
229+
try {
230+
transport.connect();
231+
transport.sendMessage(mimeMessage, allTransportRecipients.toArray(new Address[0]));
232+
} finally {
233+
if (transport != null) {
234+
transport.close();
235+
}
236+
}
237+
238+
} catch (MessagingException e) {
239+
if (e instanceof AuthenticationFailedException) {
240+
throw new IllegalArgumentException("SMTP authentication failed: " + e.getMessage(), e);
241+
}
242+
throw new IOException("Error sending email via SMTP: " + e.getMessage(), e);
243+
}
244+
}
245+
246+
private ImmutableList<InternetAddress> toInternetAddressList(Collection<String> addresses)
247+
throws IllegalArgumentException {
248+
return addresses.stream()
249+
.map(
250+
address -> {
251+
try {
252+
return new InternetAddress(address);
253+
} catch (AddressException e) {
254+
throw new IllegalArgumentException("Invalid email address: " + address, e);
255+
}
256+
})
257+
.collect(toImmutableList());
258+
}
259+
}

0 commit comments

Comments
 (0)