2323import com .google .apphosting .api .ApiProxy ;
2424import com .google .protobuf .ByteString ;
2525import 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 ;
2646
2747/**
2848 * This class implements raw access to the mail service.
3353 */
3454class MailServiceImpl implements MailService {
3555 static final String PACKAGE = "mail" ;
56+ private final SystemEnvironmentProvider envProvider ;
57+
58+ /** Default constructor, used in production. */
59+ MailServiceImpl () {
60+ this (new SystemEnvironmentProvider ());
61+ }
62+
63+ /** Constructor for testing, allowing a mock environment provider. */
64+ MailServiceImpl (SystemEnvironmentProvider envProvider ) {
65+ this .envProvider = envProvider ;
66+ }
3667
3768 /** {@inheritDoc} */
3869 @ Override
@@ -48,13 +79,182 @@ public void send(Message message)
4879 doSend (message , false );
4980 }
5081
82+ private void sendSmtp (Message message , boolean toAdmin )
83+ throws IllegalArgumentException , IOException {
84+ String smtpHost = envProvider .getenv ("SMTP_HOST" );
85+ if (smtpHost == null || smtpHost .isEmpty ()) {
86+ throw new IllegalArgumentException ("SMTP_HOST environment variable is not set." );
87+ }
88+ Properties props = new Properties ();
89+ props .put ("mail.smtp.host" , smtpHost );
90+ props .put ("mail.smtp.port" , envProvider .getenv ("SMTP_PORT" ));
91+ props .put ("mail.smtp.auth" , "true" );
92+ if (Boolean .parseBoolean (envProvider .getenv ("SMTP_USE_TLS" ))) {
93+ props .put ("mail.smtp.starttls.enable" , "true" );
94+ }
95+
96+ Session session = Session .getInstance (props , new javax .mail .Authenticator () {
97+ protected javax .mail .PasswordAuthentication getPasswordAuthentication () {
98+ return new javax .mail .PasswordAuthentication (
99+ envProvider .getenv ("SMTP_USER" ), envProvider .getenv ("SMTP_PASSWORD" ));
100+ }
101+ });
102+
103+ try {
104+ MimeMessage mimeMessage = new MimeMessage (session );
105+ mimeMessage .setFrom (new InternetAddress (message .getSender ()));
106+
107+ List <InternetAddress > toRecipients = new ArrayList <>();
108+ List <InternetAddress > ccRecipients = new ArrayList <>();
109+ List <InternetAddress > bccRecipients = new ArrayList <>();
110+
111+ if (toAdmin ) {
112+ String adminRecipients = envProvider .getenv ("ADMIN_EMAIL_RECIPIENTS" );
113+ if (adminRecipients == null || adminRecipients .isEmpty ()) {
114+ throw new IllegalArgumentException ("Admin recipients not configured." );
115+ }
116+ toRecipients .addAll (Arrays .asList (InternetAddress .parse (adminRecipients )));
117+ } else {
118+ if (message .getTo () != null ) {
119+ toRecipients .addAll (toInternetAddressList (message .getTo ()));
120+ }
121+ if (message .getCc () != null ) {
122+ ccRecipients .addAll (toInternetAddressList (message .getCc ()));
123+ }
124+ if (message .getBcc () != null ) {
125+ bccRecipients .addAll (toInternetAddressList (message .getBcc ()));
126+ }
127+ }
128+
129+ List <Address > allTransportRecipients = new ArrayList <>();
130+ allTransportRecipients .addAll (toRecipients );
131+ allTransportRecipients .addAll (ccRecipients );
132+ allTransportRecipients .addAll (bccRecipients );
133+
134+ if (allTransportRecipients .isEmpty ()) {
135+ throw new IllegalArgumentException ("No recipients specified." );
136+ }
137+
138+ if (!toRecipients .isEmpty ()) {
139+ mimeMessage .setRecipients (RecipientType .TO , toRecipients .toArray (new Address [0 ]));
140+ }
141+ if (!ccRecipients .isEmpty ()) {
142+ mimeMessage .setRecipients (RecipientType .CC , ccRecipients .toArray (new Address [0 ]));
143+ }
144+ // Bcc recipients are not set on the MimeMessage to prevent them from being in the headers.
145+
146+ if (message .getReplyTo () != null ) {
147+ mimeMessage .setReplyTo (new Address [] {new InternetAddress (message .getReplyTo ())});
148+ }
149+
150+ mimeMessage .setSubject (message .getSubject ());
151+
152+ final boolean hasAttachments = message .getAttachments () != null && !message .getAttachments ().isEmpty ();
153+ final boolean hasHtmlBody = message .getHtmlBody () != null ;
154+ final boolean hasAmpHtmlBody = message .getAmpHtmlBody () != null ;
155+ final boolean hasTextBody = message .getTextBody () != null ;
156+
157+ // Case 1: Plain text only, no attachments. Simplest case.
158+ if (hasTextBody && !hasHtmlBody && !hasAmpHtmlBody && !hasAttachments ) {
159+ mimeMessage .setText (message .getTextBody ());
160+ } else {
161+ // Case 2: Anything more complex requires multipart.
162+ MimeMultipart topLevelMultipart = new MimeMultipart ("mixed" );
163+
164+ // The bodies (text, html, amp) are grouped in a "multipart/alternative"
165+ if (hasTextBody || hasHtmlBody || hasAmpHtmlBody ) {
166+ MimeMultipart alternativeMultipart = new MimeMultipart ("alternative" );
167+ MimeBodyPart alternativeBodyPart = new MimeBodyPart ();
168+ alternativeBodyPart .setContent (alternativeMultipart );
169+
170+ if (hasTextBody ) {
171+ MimeBodyPart textPart = new MimeBodyPart ();
172+ textPart .setText (message .getTextBody ());
173+ alternativeMultipart .addBodyPart (textPart );
174+ } else if (hasHtmlBody ) {
175+ // If there is an HTML body but no text body, add an empty text part for compatibility.
176+ MimeBodyPart textPart = new MimeBodyPart ();
177+ textPart .setText ("" );
178+ alternativeMultipart .addBodyPart (textPart );
179+ }
180+
181+ if (hasHtmlBody ) {
182+ MimeBodyPart htmlPart = new MimeBodyPart ();
183+ htmlPart .setContent (message .getHtmlBody (), "text/html" );
184+ alternativeMultipart .addBodyPart (htmlPart );
185+ }
186+ if (hasAmpHtmlBody ) {
187+ MimeBodyPart ampPart = new MimeBodyPart ();
188+ ampPart .setContent (message .getAmpHtmlBody (), "text/x-amp-html" );
189+ alternativeMultipart .addBodyPart (ampPart );
190+ }
191+ topLevelMultipart .addBodyPart (alternativeBodyPart );
192+ }
193+
194+ // Add attachments to the top-level mixed part.
195+ if (hasAttachments ) {
196+ for (Attachment attachment : message .getAttachments ()) {
197+ MimeBodyPart attachmentBodyPart = new MimeBodyPart ();
198+ DataSource source =
199+ new ByteArrayDataSource (attachment .getData (), "application/octet-stream" );
200+ attachmentBodyPart .setDataHandler (new DataHandler (source ));
201+ attachmentBodyPart .setFileName (attachment .getFileName ());
202+ if (attachment .getContentID () != null ) {
203+ attachmentBodyPart .setContentID (attachment .getContentID ());
204+ }
205+ topLevelMultipart .addBodyPart (attachmentBodyPart );
206+ }
207+ }
208+ mimeMessage .setContent (topLevelMultipart );
209+ }
210+
211+ if (message .getHeaders () != null ) {
212+ for (Header header : message .getHeaders ()) {
213+ mimeMessage .addHeader (header .getName (), header .getValue ());
214+ }
215+ }
216+
217+ // Update headers to match content, e.g., setting the Content-Type
218+ mimeMessage .saveChanges ();
219+
220+ Transport transport = session .getTransport ("smtp" );
221+ try {
222+ transport .connect ();
223+ transport .sendMessage (mimeMessage , allTransportRecipients .toArray (new Address [0 ]));
224+ } finally {
225+ if (transport != null ) {
226+ transport .close ();
227+ }
228+ }
229+
230+ } catch (MessagingException e ) {
231+ if (e instanceof javax .mail .AuthenticationFailedException ) {
232+ throw new IllegalArgumentException ("SMTP authentication failed: " + e .getMessage (), e );
233+ }
234+ throw new IOException ("Error sending email via SMTP: " + e .getMessage (), e );
235+ }
236+ }
237+
238+ private List <InternetAddress > toInternetAddressList (Collection <String > addresses )
239+ throws AddressException {
240+ List <InternetAddress > list = new ArrayList <>();
241+ for (String address : addresses ) {
242+ list .add (new InternetAddress (address ));
243+ }
244+ return list ;
245+ }
246+
51247 /**
52248 * Does the actual sending of the message.
53249 * @param message The message to be sent.
54250 * @param toAdmin Whether the message is to be sent to the admins.
55251 */
56252 private void doSend (Message message , boolean toAdmin )
57253 throws IllegalArgumentException , IOException {
254+ if ("true" .equals (envProvider .getenv ("USE_SMTP_MAIL_SERVICE" ))) {
255+ sendSmtp (message , toAdmin );
256+ return ;
257+ }
58258 // Could perform basic checks to save on RPCs in case of missing args etc.
59259 // I'm not doing this on purpose, to make sure the semantics of the two
60260 // implementations stay the same.
0 commit comments