Skip to content

Commit 3e06a59

Browse files
committed
refactor(mail): Implement Strategy pattern for MailService
Refactored the MailService to use the Strategy pattern, addressing reviewer feedback to create a more modular and testable design. The mail sending logic is now split into two distinct strategies: the legacy App Engine API and the new SMTP implementation. Key changes: - Renamed the original to to clarify that it uses the traditional App Engine API. - Created a new to encapsulate all logic for sending email via an external SMTP server. - Updated to act as a factory. It now checks the environment variable to determine which implementation ( or ) to provide. - Reorganized the tests to match the new structure: - Renamed to to focus exclusively on the SMTP implementation. - Created to verify the factory's selection logic. This new structure improves adherence to the Single Responsibility and Open/Closed principles, making the system more scalable and easier to maintain.
1 parent d6717b4 commit 3e06a59

5 files changed

Lines changed: 229 additions & 137 deletions

File tree

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
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 com.google.appengine.api.mail.MailServicePb.MailAttachment;
20+
import com.google.appengine.api.mail.MailServicePb.MailHeader;
21+
import com.google.appengine.api.mail.MailServicePb.MailMessage;
22+
import com.google.appengine.api.mail.MailServicePb.MailServiceError.ErrorCode;
23+
import com.google.apphosting.api.ApiProxy;
24+
import com.google.protobuf.ByteString;
25+
import java.io.IOException;
26+
import java.util.ArrayList;
27+
import java.util.Arrays;
28+
import java.util.Collection;
29+
import java.util.List;
30+
import java.util.Properties;
31+
import javax.activation.DataHandler;
32+
import javax.activation.DataSource;
33+
import javax.mail.Address;
34+
import javax.mail.BodyPart;
35+
import javax.mail.Message.RecipientType;
36+
import javax.mail.MessagingException;
37+
import javax.mail.Multipart;
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+
/**
48+
* This class implements raw access to the mail service.
49+
* Applications that don't want to make use of Sun's JavaMail
50+
* can use it directly -- but they will forego the typing and
51+
* convenience methods that JavaMail provides.
52+
*
53+
*/
54+
class LegacyMailServiceImpl implements MailService {
55+
static final String PACKAGE = "mail";
56+
57+
/** Default constructor. */
58+
LegacyMailServiceImpl() {}
59+
60+
/** {@inheritDoc} */
61+
@Override
62+
public void sendToAdmins(Message message)
63+
throws IllegalArgumentException, IOException {
64+
doSend(message, true);
65+
}
66+
67+
/** {@inheritDoc} */
68+
@Override
69+
public void send(Message message)
70+
throws IllegalArgumentException, IOException {
71+
doSend(message, false);
72+
}
73+
74+
/**
75+
* Does the actual sending of the message.
76+
* @param message The message to be sent.
77+
* @param toAdmin Whether the message is to be sent to the admins.
78+
*/
79+
private void doSend(Message message, boolean toAdmin)
80+
throws IllegalArgumentException, IOException {
81+
// Could perform basic checks to save on RPCs in case of missing args etc.
82+
// I'm not doing this on purpose, to make sure the semantics of the two
83+
// implementations stay the same.
84+
// The benefit of not doing basic checks here is that we will pick up
85+
// changes in semantics (ie from address can now also be the logged-in user)
86+
// for free.
87+
88+
MailMessage.Builder msgProto = MailMessage.newBuilder();
89+
if (message.getSender() != null) {
90+
msgProto.setSender(message.getSender());
91+
}
92+
if (message.getTo() != null) {
93+
msgProto.addAllTo(message.getTo());
94+
}
95+
if (message.getCc() != null) {
96+
msgProto.addAllCc(message.getCc());
97+
}
98+
if (message.getBcc() != null) {
99+
msgProto.addAllBcc(message.getBcc());
100+
}
101+
if (message.getReplyTo() != null) {
102+
msgProto.setReplyTo(message.getReplyTo());
103+
}
104+
if (message.getSubject() != null) {
105+
msgProto.setSubject(message.getSubject());
106+
}
107+
if (message.getTextBody() != null) {
108+
msgProto.setTextBody(message.getTextBody());
109+
}
110+
if (message.getHtmlBody() != null) {
111+
msgProto.setHtmlBody(message.getHtmlBody());
112+
}
113+
if (message.getAmpHtmlBody() != null) {
114+
msgProto.setAmpHtmlBody(message.getAmpHtmlBody());
115+
}
116+
if (message.getAttachments() != null) {
117+
for (Attachment attach : message.getAttachments()) {
118+
MailAttachment.Builder attachProto = MailAttachment.newBuilder();
119+
attachProto.setFileName(attach.getFileName());
120+
attachProto.setData(ByteString.copyFrom(attach.getData()));
121+
String contentId = attach.getContentID();
122+
if (contentId != null) {
123+
attachProto.setContentID(contentId);
124+
}
125+
msgProto.addAttachment(attachProto);
126+
}
127+
}
128+
if (message.getHeaders() != null) {
129+
for (Header header : message.getHeaders()) {
130+
msgProto.addHeader(
131+
MailHeader.newBuilder().setName(header.getName()).setValue(header.getValue()));
132+
}
133+
}
134+
135+
byte[] msgBytes = msgProto.buildPartial().toByteArray();
136+
try {
137+
// Returns VoidProto -- just ignore the return value.
138+
if (toAdmin) {
139+
ApiProxy.makeSyncCall(PACKAGE, "SendToAdmins", msgBytes);
140+
} else {
141+
ApiProxy.makeSyncCall(PACKAGE, "Send", msgBytes);
142+
}
143+
} catch (ApiProxy.ApplicationException ex) {
144+
// Pass all the error details straight through (same as python).
145+
switch (ErrorCode.forNumber(ex.getApplicationError())) {
146+
case BAD_REQUEST:
147+
throw new IllegalArgumentException("Bad Request: " +
148+
ex.getErrorDetail());
149+
case UNAUTHORIZED_SENDER:
150+
throw new IllegalArgumentException("Unauthorized Sender: " +
151+
ex.getErrorDetail());
152+
case INVALID_ATTACHMENT_TYPE:
153+
throw new IllegalArgumentException("Invalid Attachment Type: " +
154+
ex.getErrorDetail());
155+
case INVALID_HEADER_NAME:
156+
throw new IllegalArgumentException("Invalid Header Name: " +
157+
ex.getErrorDetail());
158+
case INTERNAL_ERROR:
159+
default:
160+
throw new IOException(ex.getErrorDetail());
161+
}
162+
}
163+
}
164+
}

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,22 @@
2121
*/
2222
final class MailServiceFactoryImpl implements IMailServiceFactory {
2323

24+
private final EnvironmentProvider envProvider;
25+
26+
MailServiceFactoryImpl() {
27+
this.envProvider = new SystemEnvironmentProvider();
28+
}
29+
30+
// For testing
31+
MailServiceFactoryImpl(EnvironmentProvider envProvider) {
32+
this.envProvider = envProvider;
33+
}
34+
2435
@Override
2536
public MailService getMailService() {
26-
return new MailServiceImpl();
37+
if ("true".equals(envProvider.getenv("APPENGINE_USE_SMTP_MAIL_SERVICE"))) {
38+
return new SmtpMailServiceImpl(envProvider);
39+
}
40+
return new LegacyMailServiceImpl();
2741
}
2842
}

0 commit comments

Comments
 (0)