1 /*
2  * Copyright (C) 2010 The Android Open Source Project
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  *      http://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.android.tradefed.util;
18 
19 import com.android.ddmlib.Log;
20 
21 import java.io.BufferedInputStream;
22 import java.io.BufferedOutputStream;
23 import java.io.IOException;
24 import java.util.ArrayList;
25 import java.util.Arrays;
26 import java.util.Collection;
27 import java.util.Iterator;
28 import java.util.List;
29 
30 /**
31  * A helper class to send an email.  Note that this class is NOT PLATFORM INDEPENDENT.  It will
32  * likely fail on Windows, and possibly on Mac OS X as well.  It will fail on any machine where
33  * The binary pointed at by the {@code mailer} constant doesn't exist.
34  */
35 public class Email implements IEmail {
36     private static final String LOG_TAG = "Email";
37     private static final String[] mailer = {"/usr/sbin/sendmail", "-t", "-i"};
38     static final String CRLF = "\r\n";
39 
join(Collection<String> list, String sep)40     private static String join(Collection<String> list, String sep) {
41         StringBuilder builder = new StringBuilder();
42         Iterator<String> iter = list.iterator();
43         while (iter.hasNext()) {
44             String element = iter.next();
45             builder.append(element);
46             if(iter.hasNext()) {
47                 builder.append(sep);
48             }
49         }
50         return builder.toString();
51     }
52 
53     /**
54      * A helper method to use ProcessBuilder to create a new process.  This can't use
55      * {@link com.android.tradefed.util.IRunUtil} because that class doesn't provide a way to pass
56      * data to the stdin of the spawned process, which is the usage paradigm for most commandline
57      * mailers such as mailx and sendmail.
58      * <p/>
59      * Exposed for mocking
60      *
61      * @param cmd The {@code String[]} to pass to the {@link ProcessBuilder} constructor
62      * @return The {@link Process} returned from from {@link ProcessBuilder#start()}
63      * @throws IOException if sending email failed in a synchronously-detectable way
64      */
run(String[] cmd)65     Process run(String[] cmd) throws IOException {
66         ProcessBuilder pb = new ProcessBuilder(cmd);
67         pb.redirectErrorStream(true);
68         return pb.start();
69     }
70 
71     /**
72      * A small helper function that adds the specified header to the header list only if the value
73      * is non-null
74      */
addHeader(List<String> headers, String name, String value)75     private void addHeader(List<String> headers, String name, String value) {
76         if (name == null || value == null) return;
77         headers.add(String.format("%s: %s", name, value));
78     }
79 
80     /**
81      * A small helper function that adds the specified header to the header list only if the value
82      * is non-null
83      */
addHeaders(List<String> headers, String name, Collection<String> values)84     private void addHeaders(List<String> headers, String name, Collection<String> values) {
85         if (name == null || values == null) return;
86         if (values.isEmpty()) return;
87 
88         final String strValues = join(values, ",");
89         headers.add(String.format("%s: %s", name, strValues));
90     }
91 
92     /**
93      * {@inheritDoc}
94      */
95     @Override
send(Message msg)96     public void send(Message msg) throws IllegalArgumentException, IOException {
97         // Validity checks
98         if (msg.getTo() == null) {
99             throw new IllegalArgumentException("Message is missing a destination");
100         } else if (msg.getSubject() == null) {
101             throw new IllegalArgumentException("Message is missing a subject");
102         } else if (msg.getBody() == null) {
103             throw new IllegalArgumentException("Message is missing a body");
104         }
105 
106         // Sender, Recipients, CC, BCC, Subject are all set with appropriate email headers
107         final ArrayList<String> headers = new ArrayList<String>();
108         final String[] mailCmd;
109         if (msg.getSender() != null) {
110             addHeader(headers, "From", msg.getSender());
111 
112             // Envelope Sender (will receive any errors related to the email)
113             int cmdLen = mailer.length + 2;
114             mailCmd = Arrays.copyOf(mailer, cmdLen);
115             mailCmd[cmdLen - 2] = "-f";
116             mailCmd[cmdLen - 1] = msg.getSender();
117         } else {
118             mailCmd = mailer;
119         }
120         addHeaders(headers, "To", msg.getTo());
121         addHeaders(headers, "Cc", msg.getCc());
122         addHeaders(headers, "Bcc", msg.getBcc());
123         addHeader(headers, "Content-type", msg.getContentType());
124         addHeader(headers, "Subject", msg.getSubject());
125 
126         final StringBuilder fullMsg = new StringBuilder();
127         fullMsg.append(join(headers, CRLF));
128         fullMsg.append(CRLF);
129         fullMsg.append(CRLF);
130         fullMsg.append(msg.getBody());
131 
132         Log.d(LOG_TAG, String.format("About to send email with command: %s",
133                 Arrays.toString(mailCmd)));
134         Process mailerProc = run(mailCmd);
135         BufferedOutputStream mailerStdin = new BufferedOutputStream(mailerProc.getOutputStream());
136         /* There is no such thing as a "character" in the land of the shell; there are only bytes.
137          * Here, we convert the body from a Java string (consisting of characters) to a byte array
138          * encoding each character with UTF-8.  Each character will be represented as between one
139          * and four bytes apiece.
140          */
141         mailerStdin.write(fullMsg.toString().getBytes("UTF-8"));
142         mailerStdin.flush();
143         mailerStdin.close();
144 
145         int retValue;
146         try {
147             retValue = mailerProc.waitFor();
148         } catch (InterruptedException e) {
149             // ignore, but set retValue to something bogus
150             retValue = -12345;
151         }
152         if (retValue != 0) {
153             Log.e(LOG_TAG, String.format("Mailer finished with non-zero return value: %d", retValue));
154             BufferedInputStream mailerStdout = new BufferedInputStream(mailerProc.getInputStream());
155             StringBuilder stdout = new StringBuilder();
156             int theByte;
157             while((theByte = mailerStdout.read()) != -1) {
158                 stdout.append((char)theByte);
159             }
160             Log.e(LOG_TAG, "Mailer output was: " + stdout.toString());
161         } else {
162             Log.v(LOG_TAG, "Mailer returned successfully.");
163         }
164     }
165 }
166 
167