1 /**
2  * Copyright 2016 Google Inc. All Rights Reserved.
3  *
4  * <p>Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5  * except in compliance with the License. You may obtain a copy of the License at
6  *
7  * <p>http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * <p>Unless required by applicable law or agreed to in writing, software distributed under the
10  * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
11  * express or implied. See the License for the specific language governing permissions and
12  * limitations under the License.
13  */
14 package com.android.vts.util;
15 
16 import com.android.vts.entity.DeviceInfoEntity;
17 import com.android.vts.entity.TestRunEntity;
18 import com.android.vts.entity.UserFavoriteEntity;
19 import com.google.appengine.api.datastore.DatastoreService;
20 import com.google.appengine.api.datastore.DatastoreServiceFactory;
21 import com.google.appengine.api.datastore.Entity;
22 import com.google.appengine.api.datastore.Key;
23 import com.google.appengine.api.datastore.Query;
24 import com.google.appengine.api.datastore.Query.Filter;
25 import com.google.appengine.api.datastore.Query.FilterOperator;
26 import com.google.appengine.api.datastore.Query.FilterPredicate;
27 import java.io.IOException;
28 import java.io.UnsupportedEncodingException;
29 
30 import java.util.ArrayList;
31 import java.util.HashSet;
32 import java.util.List;
33 import java.util.Optional;
34 import java.util.Properties;
35 import java.util.Set;
36 import java.util.logging.Level;
37 import java.util.logging.Logger;
38 import javax.mail.Message;
39 import javax.mail.MessagingException;
40 import javax.mail.Session;
41 import javax.mail.Transport;
42 import javax.mail.internet.InternetAddress;
43 import javax.mail.internet.MimeMessage;
44 import org.apache.commons.lang.StringUtils;
45 
46 /** EmailHelper, a helper class for building and sending emails. */
47 public class EmailHelper {
48     protected static final Logger logger = Logger.getLogger(EmailHelper.class.getName());
49     protected static String DEFAULT_EMAIL;
50     protected static String EMAIL_DOMAIN;
51     protected static String SENDER_EMAIL;
52     private static final String VTS_EMAIL_NAME = "VTS Alert Bot";
53 
setPropertyValues(Properties systemConfigProp)54     public static void setPropertyValues(Properties systemConfigProp) {
55         DEFAULT_EMAIL = systemConfigProp.getProperty("appengine.defaultEmail");
56         EMAIL_DOMAIN = systemConfigProp.getProperty("appengine.emailDomain");
57         SENDER_EMAIL = systemConfigProp.getProperty("appengine.senderEmail");
58     }
59 
60     /**
61      * Create an email footer with the information from the test run.
62      *
63      * @param testRun The TestRunEntity containing test run metadata, or null.
64      * @param devices The list of devices whose fingerprints to include in the email, or null.
65      * @param link A link to the Dashboard page containing more information.
66      * @return The String email footer.
67      */
getEmailFooter( TestRunEntity testRun, List<DeviceInfoEntity> devices, String link)68     public static String getEmailFooter(
69             TestRunEntity testRun, List<DeviceInfoEntity> devices, String link) {
70         StringBuilder sb = new StringBuilder();
71         sb.append("<br><br>");
72         if (devices != null) {
73             for (DeviceInfoEntity device : devices) {
74                 sb.append("Device: " + device.getFingerprint() + "<br>");
75             }
76         }
77 
78         if (testRun != null) {
79             sb.append("VTS Build ID: " + testRun.getTestBuildId() + "<br>");
80             sb.append("Start Time: " + TimeUtil.getDateTimeZoneString(testRun.getStartTimestamp()));
81             sb.append("<br>End Time: " + TimeUtil.getDateTimeZoneString(testRun.getEndTimestamp()));
82         }
83         sb.append(
84                 "<br><br>For details, visit the"
85                         + " <a href='"
86                         + link
87                         + "'>"
88                         + "VTS dashboard.</a>");
89         return sb.toString();
90     }
91 
92     /**
93      * Fetches the list of subscriber email addresses for a test.
94      *
95      * @param testKey The key for the test for which to fetch the email addresses.
96      * @returns List of email addresses (String).
97      * @throws IOException
98      */
getSubscriberEmails(Key testKey)99     public static List<String> getSubscriberEmails(Key testKey) throws IOException {
100         DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
101         Filter testFilter =
102                 new FilterPredicate(UserFavoriteEntity.TEST_KEY, FilterOperator.EQUAL, testKey);
103         Query favoritesQuery = new Query(UserFavoriteEntity.KIND).setFilter(testFilter);
104         Set<String> emailSet = new HashSet<>();
105         if (!StringUtils.isBlank(DEFAULT_EMAIL)) {
106             emailSet.add(DEFAULT_EMAIL);
107         }
108         for (Entity favorite : datastore.prepare(favoritesQuery).asIterable()) {
109             UserFavoriteEntity favoriteEntity = UserFavoriteEntity.fromEntity(favorite);
110             // TODO this logic need to be reexamined thoroughly and improved
111             if (favoriteEntity != null
112                     && favoriteEntity.user != null
113                     && !favoriteEntity.muteNotifications) {
114                 Optional<String> userEmail = Optional.of(favoriteEntity.user.getEmail());
115                 if (userEmail.isPresent() &&
116                         userEmail.orElse("").endsWith(EMAIL_DOMAIN)) {
117                     emailSet.add(favoriteEntity.user.getEmail());
118                 }
119             }
120         }
121         return new ArrayList<>(emailSet);
122     }
123 
124     /**
125      * Sends an email to the specified email address to notify of a test status change.
126      *
127      * @param emails List of subscriber email addresses (byte[]) to which the email should be sent.
128      * @param subject The email subject field, string.
129      * @param body The html (string) body to send in the email.
130      * @returns The Message object to be sent.
131      * @throws MessagingException, UnsupportedEncodingException
132      */
composeEmail(List<String> emails, String subject, String body)133     public static Message composeEmail(List<String> emails, String subject, String body)
134             throws MessagingException, UnsupportedEncodingException {
135         if (emails.size() == 0) {
136             throw new MessagingException("No subscriber email addresses provided");
137         }
138         Properties props = new Properties();
139         Session session = Session.getDefaultInstance(props, null);
140 
141         Message msg = new MimeMessage(session);
142         for (String email : emails) {
143             try {
144                 msg.addRecipient(Message.RecipientType.TO, new InternetAddress(email, email));
145             } catch (MessagingException | UnsupportedEncodingException e) {
146                 // Gracefully continue when a subscriber email is invalid.
147                 logger.log(Level.WARNING, "Error sending email to recipient " + email + " : ", e);
148             }
149         }
150         msg.setFrom(new InternetAddress(SENDER_EMAIL, VTS_EMAIL_NAME));
151         msg.setSubject(subject);
152         msg.setContent(body, "text/html; charset=utf-8");
153         return msg;
154     }
155 
156     /**
157      * Sends an email.
158      *
159      * @param msg Message object to send.
160      * @returns true if the message sends successfully, false otherwise
161      */
send(Message msg)162     public static boolean send(Message msg) {
163         try {
164             Transport.send(msg);
165         } catch (MessagingException e) {
166             logger.log(Level.WARNING, "Error sending email : ", e);
167             return false;
168         }
169         return true;
170     }
171 
172     /**
173      * Sends a list of emails and logs any failures.
174      *
175      * @param messages List of Message objects to be sent.
176      */
sendAll(List<Message> messages)177     public static void sendAll(List<Message> messages) {
178         for (Message msg : messages) {
179             send(msg);
180         }
181     }
182 
183     /**
184      * Sends an email.
185      *
186      * @param recipients List of email address strings to which an email will be sent.
187      * @param subject The subject of the email.
188      * @param body The body of the email.
189      * @returns true if the message sends successfully, false otherwise
190      */
send(List<String> recipients, String subject, String body)191     public static boolean send(List<String> recipients, String subject, String body) {
192         try {
193             Message msg = composeEmail(recipients, subject, body);
194             return send(msg);
195         } catch (MessagingException | UnsupportedEncodingException e) {
196             logger.log(Level.WARNING, "Error composing email : ", e);
197             return false;
198         }
199     }
200 }
201