• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008-2009 Marc Blank
3  * Licensed to The Android Open Source Project.
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 
18 package com.android.exchange;
19 
20 import android.content.ContentUris;
21 import android.content.ContentValues;
22 import android.content.Context;
23 import android.database.Cursor;
24 import android.net.TrafficStats;
25 import android.net.Uri;
26 import android.os.RemoteException;
27 import android.text.TextUtils;
28 
29 import com.android.emailcommon.TrafficFlags;
30 import com.android.emailcommon.internet.Rfc822Output;
31 import com.android.emailcommon.mail.MessagingException;
32 import com.android.emailcommon.provider.Account;
33 import com.android.emailcommon.provider.EmailContent.Body;
34 import com.android.emailcommon.provider.EmailContent.BodyColumns;
35 import com.android.emailcommon.provider.EmailContent.MailboxColumns;
36 import com.android.emailcommon.provider.EmailContent.Message;
37 import com.android.emailcommon.provider.EmailContent.MessageColumns;
38 import com.android.emailcommon.provider.EmailContent.SyncColumns;
39 import com.android.emailcommon.provider.Mailbox;
40 import com.android.emailcommon.service.EmailServiceStatus;
41 import com.android.emailcommon.utility.Utility;
42 import com.android.exchange.CommandStatusException.CommandStatus;
43 import com.android.exchange.adapter.Parser;
44 import com.android.exchange.adapter.Parser.EmptyStreamException;
45 import com.android.exchange.adapter.Serializer;
46 import com.android.exchange.adapter.Tags;
47 
48 import org.apache.http.HttpEntity;
49 import org.apache.http.HttpStatus;
50 import org.apache.http.entity.InputStreamEntity;
51 
52 import java.io.File;
53 import java.io.FileInputStream;
54 import java.io.FileOutputStream;
55 import java.io.IOException;
56 import java.io.InputStream;
57 import java.io.OutputStream;
58 
59 public class EasOutboxService extends EasSyncService {
60 
61     public static final int SEND_FAILED = 1;
62     public static final String MAILBOX_KEY_AND_NOT_SEND_FAILED =
63         MessageColumns.MAILBOX_KEY + "=? and (" + SyncColumns.SERVER_ID + " is null or " +
64         SyncColumns.SERVER_ID + "!=" + SEND_FAILED + ')';
65     public static final String[] BODY_SOURCE_PROJECTION =
66         new String[] {BodyColumns.SOURCE_MESSAGE_KEY};
67     public static final String WHERE_MESSAGE_KEY = Body.MESSAGE_KEY + "=?";
68 
69     // This is a normal email (i.e. not one of the other types)
70     public static final int MODE_NORMAL = 0;
71     // This is a smart reply email
72     public static final int MODE_SMART_REPLY = 1;
73     // This is a smart forward email
74     public static final int MODE_SMART_FORWARD = 2;
75 
76     // This needs to be long enough to send the longest reasonable message, without being so long
77     // as to effectively "hang" sending of mail.  The standard 30 second timeout isn't long enough
78     // for pictures and the like.  For now, we'll use 15 minutes, in the knowledge that any socket
79     // failure would probably generate an Exception before timing out anyway
80     public static final int SEND_MAIL_TIMEOUT = 15*MINUTES;
81 
EasOutboxService(Context _context, Mailbox _mailbox)82     public EasOutboxService(Context _context, Mailbox _mailbox) {
83         super(_context, _mailbox);
84     }
85 
86     /**
87      * Our own HttpEntity subclass that is able to insert opaque data (in this case the MIME
88      * representation of the message body as stored in a temporary file) into the serializer stream
89      */
90     private static class SendMailEntity extends InputStreamEntity {
91         private final Context mContext;
92         private final FileInputStream mFileStream;
93         private final long mFileLength;
94         private final int mSendTag;
95         private final Message mMessage;
96 
97         private static final int[] MODE_TAGS =  new int[] {Tags.COMPOSE_SEND_MAIL,
98             Tags.COMPOSE_SMART_REPLY, Tags.COMPOSE_SMART_FORWARD};
99 
SendMailEntity(Context context, FileInputStream instream, long length, int tag, Message message)100         public SendMailEntity(Context context, FileInputStream instream, long length, int tag,
101                 Message message) {
102             super(instream, length);
103             mContext = context;
104             mFileStream = instream;
105             mFileLength = length;
106             mSendTag = tag;
107             mMessage = message;
108         }
109 
110         /**
111          * We always return -1 because we don't know the actual length of the POST data (this
112          * causes HttpClient to send the data in "chunked" mode)
113          */
114         @Override
getContentLength()115         public long getContentLength() {
116             return -1;
117         }
118 
119         @Override
writeTo(OutputStream outstream)120         public void writeTo(OutputStream outstream) throws IOException {
121             // Not sure if this is possible; the check is taken from the superclass
122             if (outstream == null) {
123                 throw new IllegalArgumentException("Output stream may not be null");
124             }
125 
126             // We'll serialize directly into the output stream
127             Serializer s = new Serializer(outstream);
128             // Send the appropriate initial tag
129             s.start(mSendTag);
130             // The Message-Id for this message (note that we cannot use the messageId stored in
131             // the message, as EAS 14 limits the length to 40 chars and we use 70+)
132             s.data(Tags.COMPOSE_CLIENT_ID, "SendMail-" + System.nanoTime());
133             // We always save sent mail
134             s.tag(Tags.COMPOSE_SAVE_IN_SENT_ITEMS);
135 
136             // If we're using smart reply/forward, we need info about the original message
137             if (mSendTag != Tags.COMPOSE_SEND_MAIL) {
138                 OriginalMessageInfo info = getOriginalMessageInfo(mContext, mMessage.mId);
139                 if (info != null) {
140                     s.start(Tags.COMPOSE_SOURCE);
141                     // For search results, use the long id (stored in mProtocolSearchInfo); else,
142                     // use folder id/item id combo
143                     if (mMessage.mProtocolSearchInfo != null) {
144                         s.data(Tags.COMPOSE_LONG_ID, mMessage.mProtocolSearchInfo);
145                     } else {
146                         s.data(Tags.COMPOSE_ITEM_ID, info.mItemId);
147                         s.data(Tags.COMPOSE_FOLDER_ID, info.mCollectionId);
148                     }
149                     s.end();  // Tags.COMPOSE_SOURCE
150                 }
151             }
152 
153             // Start the MIME tag; this is followed by "opaque" data (byte array)
154             s.start(Tags.COMPOSE_MIME);
155             // Send opaque data from the file stream
156             s.opaque(mFileStream, (int)mFileLength);
157             // And we're done
158             s.end().end().done();
159         }
160     }
161 
162     private static class SendMailParser extends Parser {
163         private final int mStartTag;
164         private int mStatus;
165 
SendMailParser(InputStream in, int startTag)166         public SendMailParser(InputStream in, int startTag) throws IOException {
167             super(in);
168             mStartTag = startTag;
169         }
170 
getStatus()171         public int getStatus() {
172             return mStatus;
173         }
174 
175         /**
176          * The only useful info in the SendMail response is the status; we capture and save it
177          */
178         @Override
parse()179         public boolean parse() throws IOException {
180             if (nextTag(START_DOCUMENT) != mStartTag) {
181                 throw new IOException();
182             }
183             while (nextTag(START_DOCUMENT) != END_DOCUMENT) {
184                 if (tag == Tags.COMPOSE_STATUS) {
185                     mStatus = getValueInt();
186                 } else {
187                     skipTag();
188                 }
189             }
190             return true;
191         }
192     }
193 
194     /**
195      * For OriginalMessageInfo, we use the terminology of EAS for the serverId and mailboxId of the
196      * original message
197      */
198     protected static class OriginalMessageInfo {
199         final String mItemId;
200         final String mCollectionId;
201         final String mLongId;
202 
OriginalMessageInfo(String itemId, String collectionId, String longId)203         OriginalMessageInfo(String itemId, String collectionId, String longId) {
204             mItemId = itemId;
205             mCollectionId = collectionId;
206             mLongId = longId;
207         }
208     }
209 
sendCallback(long msgId, String subject, int status)210     private void sendCallback(long msgId, String subject, int status) {
211         try {
212             ExchangeService.callback().sendMessageStatus(msgId, subject, status, 0);
213         } catch (RemoteException e) {
214             // It's all good
215         }
216     }
217 
generateSmartSendCmd(boolean reply, OriginalMessageInfo info)218     /*package*/ String generateSmartSendCmd(boolean reply, OriginalMessageInfo info) {
219         StringBuilder sb = new StringBuilder();
220         sb.append(reply ? "SmartReply" : "SmartForward");
221         if (!TextUtils.isEmpty(info.mLongId)) {
222             sb.append("&LongId=");
223             sb.append(Uri.encode(info.mLongId, ":"));
224         } else {
225             sb.append("&ItemId=");
226             sb.append(Uri.encode(info.mItemId, ":"));
227             sb.append("&CollectionId=");
228             sb.append(Uri.encode(info.mCollectionId, ":"));
229         }
230         return sb.toString();
231     }
232 
233     /**
234      * Get information about the original message that is referenced by the message to be sent; this
235      * information will exist for replies and forwards
236      *
237      * @param context the caller's context
238      * @param msgId the id of the message we're sending
239      * @return a data structure with the serverId and mailboxId of the original message, or null if
240      * either or both of those pieces of information can't be found
241      */
getOriginalMessageInfo(Context context, long msgId)242     private static OriginalMessageInfo getOriginalMessageInfo(Context context, long msgId) {
243         // Note: itemId and collectionId are the terms used by EAS to refer to the serverId and
244         // mailboxId of a Message
245         String itemId = null;
246         String collectionId = null;
247         String longId = null;
248 
249         // First, we need to get the id of the reply/forward message
250         String[] cols = Utility.getRowColumns(context, Body.CONTENT_URI,
251                 BODY_SOURCE_PROJECTION, WHERE_MESSAGE_KEY,
252                 new String[] {Long.toString(msgId)});
253         if (cols != null) {
254             long refId = Long.parseLong(cols[0]);
255             // Then, we need the serverId and mailboxKey of the message
256             cols = Utility.getRowColumns(context, Message.CONTENT_URI, refId,
257                     SyncColumns.SERVER_ID, MessageColumns.MAILBOX_KEY,
258                     MessageColumns.PROTOCOL_SEARCH_INFO);
259             if (cols != null) {
260                 itemId = cols[0];
261                 long boxId = Long.parseLong(cols[1]);
262                 // Then, we need the serverId of the mailbox
263                 cols = Utility.getRowColumns(context, Mailbox.CONTENT_URI, boxId,
264                         MailboxColumns.SERVER_ID);
265                 if (cols != null) {
266                     collectionId = cols[0];
267                 }
268             }
269         }
270         // We need either a longId or both itemId (serverId) and collectionId (mailboxId) to process
271         // a smart reply or a smart forward
272         if (longId != null || (itemId != null && collectionId != null)){
273             return new OriginalMessageInfo(itemId, collectionId, longId);
274         }
275         return null;
276     }
277 
sendFailed(long msgId, int result)278     private void sendFailed(long msgId, int result) {
279         ContentValues cv = new ContentValues();
280         cv.put(SyncColumns.SERVER_ID, SEND_FAILED);
281         Message.update(mContext, Message.CONTENT_URI, msgId, cv);
282         sendCallback(msgId, null, result);
283     }
284 
285     /**
286      * Send a single message via EAS
287      * Note that we mark messages SEND_FAILED when there is a permanent failure, rather than an
288      * IOException, which is handled by ExchangeService with retries, backoffs, etc.
289      *
290      * @param cacheDir the cache directory for this context
291      * @param msgId the _id of the message to send
292      * @throws IOException
293      */
sendMessage(File cacheDir, long msgId)294     int sendMessage(File cacheDir, long msgId) throws IOException, MessagingException {
295         // We always return SUCCESS unless the sending error is account-specific (security or
296         // authentication) rather than message-specific; returning anything else will terminate
297         // the Outbox sync! Message-specific errors are marked in the messages themselves.
298         int result = EmailServiceStatus.SUCCESS;
299         // Say we're starting to send this message
300         sendCallback(msgId, null, EmailServiceStatus.IN_PROGRESS);
301         // Create a temporary file (this will hold the outgoing message in RFC822 (MIME) format)
302         File tmpFile = File.createTempFile("eas_", "tmp", cacheDir);
303         try {
304             // Get the message and fail quickly if not found
305             Message msg = Message.restoreMessageWithId(mContext, msgId);
306             if (msg == null) return EmailServiceStatus.MESSAGE_NOT_FOUND;
307 
308             // See what kind of outgoing messge this is
309             int flags = msg.mFlags;
310             boolean reply = (flags & Message.FLAG_TYPE_REPLY) != 0;
311             boolean forward = (flags & Message.FLAG_TYPE_FORWARD) != 0;
312             boolean includeQuotedText = (flags & Message.FLAG_NOT_INCLUDE_QUOTED_TEXT) == 0;
313 
314             // The reference message and mailbox are called item and collection in EAS
315             OriginalMessageInfo referenceInfo = null;
316             // Respect the sense of the include quoted text flag
317             if (includeQuotedText && (reply || forward)) {
318                 referenceInfo = getOriginalMessageInfo(mContext, msgId);
319             }
320             // Generally, we use SmartReply/SmartForward if we've got a good reference
321             boolean smartSend = referenceInfo != null;
322             // But we won't use SmartForward if the account isn't set up for it (currently, we only
323             // use SmartForward for EAS 12.0 or later to avoid creating eml files that are
324             // potentially difficult for the recipient to handle)
325             if (forward && ((mAccount.mFlags & Account.FLAGS_SUPPORTS_SMART_FORWARD) == 0)) {
326                 smartSend = false;
327             }
328 
329             // Write the message to the temporary file
330             FileOutputStream fileOutputStream = new FileOutputStream(tmpFile);
331             Rfc822Output.writeTo(mContext, msgId, fileOutputStream, smartSend, true);
332             fileOutputStream.close();
333 
334             // Sending via EAS14 is a whole 'nother kettle of fish
335             boolean isEas14 = (Double.parseDouble(mAccount.mProtocolVersion) >=
336                 Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE);
337 
338             // Get an input stream to our temporary file and create an entity with it
339             FileInputStream fileStream = new FileInputStream(tmpFile);
340             long fileLength = tmpFile.length();
341 
342             while (true) {
343                 // The type of entity depends on whether we're using EAS 14
344                 HttpEntity inputEntity;
345                 // For EAS 14, we need to save the wbxml tag we're using
346                 int modeTag = 0;
347                 if (isEas14) {
348                     int mode =
349                         !smartSend ? MODE_NORMAL : reply ? MODE_SMART_REPLY : MODE_SMART_FORWARD;
350                     modeTag = SendMailEntity.MODE_TAGS[mode];
351                     inputEntity =
352                         new SendMailEntity(mContext, fileStream, fileLength, modeTag, msg);
353                 } else {
354                     inputEntity = new InputStreamEntity(fileStream, fileLength);
355                 }
356                 // Create the appropriate command and POST it to the server
357                 String cmd = "SendMail";
358                 if (smartSend) {
359                     // In EAS 14, we don't send itemId and collectionId in the command
360                     if (isEas14) {
361                         cmd = reply ? "SmartReply" : "SmartForward";
362                     } else {
363                         cmd = generateSmartSendCmd(reply, referenceInfo);
364                     }
365                 }
366 
367                 // If we're not EAS 14, add our save-in-sent setting here
368                 if (!isEas14) {
369                     cmd += "&SaveInSent=T";
370                 }
371                 userLog("Send cmd: " + cmd);
372 
373                 // Finally, post SendMail to the server
374                 EasResponse resp = sendHttpClientPost(cmd, inputEntity, SEND_MAIL_TIMEOUT);
375                 try {
376                     fileStream.close();
377                     int code = resp.getStatus();
378                     if (code == HttpStatus.SC_OK) {
379                         // HTTP OK before EAS 14 is a thumbs up; in EAS 14, we've got to parse
380                         // the reply
381                         if (isEas14) {
382                             try {
383                                 // Try to parse the result
384                                 SendMailParser p =
385                                     new SendMailParser(resp.getInputStream(), modeTag);
386                                 // If we get here, the SendMail failed; go figure
387                                 p.parse();
388                                 // The parser holds the status
389                                 int status = p.getStatus();
390                                 userLog("SendMail error, status: " + status);
391                                 if (CommandStatus.isNeedsProvisioning(status)) {
392                                     result = EmailServiceStatus.SECURITY_FAILURE;
393                                 } else if (status == CommandStatus.ITEM_NOT_FOUND && smartSend) {
394                                     // This is the retry case for EAS 14; we'll send without "smart"
395                                     // commands next time
396                                     resp.close();
397                                     smartSend = false;
398                                     continue;
399                                 }
400                                 sendFailed(msgId, result);
401                                 return result;
402                             } catch (EmptyStreamException e) {
403                                 // This is actually fine; an empty stream means SendMail succeeded
404                             }
405                         }
406 
407                         // If we're here, the SendMail command succeeded
408                         userLog("Deleting message...");
409                         // Delete the message from the Outbox and send callback
410                         mContentResolver.delete(
411                                 ContentUris.withAppendedId(Message.CONTENT_URI, msgId), null, null);
412                         sendCallback(-1, msg.mSubject, EmailServiceStatus.SUCCESS);
413                         break;
414                     } else if (code == EasSyncService.INTERNAL_SERVER_ERROR_CODE && smartSend) {
415                         // This is the retry case for EAS 12.1 and below; we'll send without "smart"
416                         // commands next time
417                         resp.close();
418                         smartSend = false;
419                     } else {
420                         userLog("Message sending failed, code: " + code);
421                         if (EasResponse.isAuthError(code)) {
422                             result = EmailServiceStatus.LOGIN_FAILED;
423                         } else if (EasResponse.isProvisionError(code)) {
424                             result = EmailServiceStatus.SECURITY_FAILURE;
425                         }
426                         sendFailed(msgId, result);
427                         break;
428                     }
429                 } finally {
430                     resp.close();
431                 }
432             }
433         } catch (IOException e) {
434             // We catch this just to send the callback
435             sendCallback(msgId, null, EmailServiceStatus.CONNECTION_ERROR);
436             throw e;
437         } finally {
438             // Clean up the temporary file
439             if (tmpFile.exists()) {
440                 tmpFile.delete();
441             }
442         }
443         return result;
444     }
445 
446     @Override
run()447     public void run() {
448         setupService();
449         // Use SMTP flags for sending mail
450         TrafficStats.setThreadStatsTag(TrafficFlags.getSmtpFlags(mContext, mAccount));
451         File cacheDir = mContext.getCacheDir();
452         try {
453             mDeviceId = ExchangeService.getDeviceId(mContext);
454             // Get a cursor to Outbox messages
455             Cursor c = mContext.getContentResolver().query(Message.CONTENT_URI,
456                     Message.ID_COLUMN_PROJECTION, MAILBOX_KEY_AND_NOT_SEND_FAILED,
457                     new String[] {Long.toString(mMailbox.mId)}, null);
458             try {
459                 // Loop through the messages, sending each one
460                 while (c.moveToNext()) {
461                     long msgId = c.getLong(Message.ID_COLUMNS_ID_COLUMN);
462                     if (msgId != 0) {
463                         if (Utility.hasUnloadedAttachments(mContext, msgId)) {
464                             // We'll just have to wait on this...
465                             continue;
466                         }
467                         int result = sendMessage(cacheDir, msgId);
468                         // If there's an error, it should stop the service; we will distinguish
469                         // at least between login failures and everything else
470                         if (result == EmailServiceStatus.LOGIN_FAILED) {
471                             mExitStatus = EXIT_LOGIN_FAILURE;
472                             return;
473                         } else if (result == EmailServiceStatus.SECURITY_FAILURE) {
474                             mExitStatus = EXIT_SECURITY_FAILURE;
475                             return;
476                         } else if (result == EmailServiceStatus.REMOTE_EXCEPTION) {
477                             mExitStatus = EXIT_EXCEPTION;
478                             return;
479                         }
480                     }
481                 }
482             } finally {
483                 c.close();
484             }
485             mExitStatus = EXIT_DONE;
486         } catch (IOException e) {
487             mExitStatus = EXIT_IO_ERROR;
488         } catch (Exception e) {
489             userLog("Exception caught in EasOutboxService", e);
490             mExitStatus = EXIT_EXCEPTION;
491         } finally {
492             userLog(mMailbox.mDisplayName, ": sync finished");
493             userLog("Outbox exited with status ", mExitStatus);
494             ExchangeService.done(this);
495         }
496     }
497 
498     /**
499      * Convenience method for adding a Message to an account's outbox
500      * @param context the context of the caller
501      * @param accountId the accountId for the sending account
502      * @param msg the message to send
503      */
sendMessage(Context context, long accountId, Message msg)504     public static void sendMessage(Context context, long accountId, Message msg) {
505         Mailbox mailbox = Mailbox.restoreMailboxOfType(context, accountId, Mailbox.TYPE_OUTBOX);
506         if (mailbox != null) {
507             msg.mMailboxKey = mailbox.mId;
508             msg.mAccountKey = accountId;
509             msg.save(context);
510         }
511     }
512 }