• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package com.android.exchange.eas;
2 
3 import android.content.ContentUris;
4 import android.content.Context;
5 import android.net.Uri;
6 import android.text.format.DateUtils;
7 import android.util.Log;
8 
9 import com.android.emailcommon.internet.MimeUtility;
10 import com.android.emailcommon.internet.Rfc822Output;
11 import com.android.emailcommon.provider.Account;
12 import com.android.emailcommon.provider.Mailbox;
13 import com.android.emailcommon.provider.EmailContent.Attachment;
14 import com.android.emailcommon.provider.EmailContent.Body;
15 import com.android.emailcommon.provider.EmailContent.BodyColumns;
16 import com.android.emailcommon.provider.EmailContent.MailboxColumns;
17 import com.android.emailcommon.provider.EmailContent.Message;
18 import com.android.emailcommon.provider.EmailContent.MessageColumns;
19 import com.android.emailcommon.provider.EmailContent.SyncColumns;
20 import com.android.emailcommon.utility.Utility;
21 import com.android.exchange.CommandStatusException;
22 import com.android.exchange.Eas;
23 import com.android.exchange.EasResponse;
24 import com.android.exchange.CommandStatusException.CommandStatus;
25 import com.android.exchange.adapter.SendMailParser;
26 import com.android.exchange.adapter.Serializer;
27 import com.android.exchange.adapter.Tags;
28 import com.android.exchange.adapter.Parser.EmptyStreamException;
29 import com.android.mail.utils.LogUtils;
30 
31 import org.apache.http.HttpEntity;
32 import org.apache.http.HttpStatus;
33 import org.apache.http.entity.InputStreamEntity;
34 
35 import java.io.ByteArrayOutputStream;
36 import java.io.File;
37 import java.io.FileInputStream;
38 import java.io.FileNotFoundException;
39 import java.io.FileOutputStream;
40 import java.io.IOException;
41 import java.io.OutputStream;
42 import java.util.ArrayList;
43 
44 public class EasOutboxSync extends EasOperation {
45 
46     // Value for a message's server id when sending fails.
47     public static final int SEND_FAILED = 1;
48     // This needs to be long enough to send the longest reasonable message, without being so long
49     // as to effectively "hang" sending of mail.  The standard 30 second timeout isn't long enough
50     // for pictures and the like.  For now, we'll use 15 minutes, in the knowledge that any socket
51     // failure would probably generate an Exception before timing out anyway
52     public static final long SEND_MAIL_TIMEOUT = 15 * DateUtils.MINUTE_IN_MILLIS;
53 
54     public static final int RESULT_OK = 1;
55     public static final int RESULT_IO_ERROR = -100;
56     public static final int RESULT_ITEM_NOT_FOUND = -101;
57     public static final int RESULT_SEND_FAILED = -102;
58 
59     private final Message mMessage;
60     private final boolean mIsEas14;
61     private final File mCacheDir;
62     private final SmartSendInfo mSmartSendInfo;
63     private final int mModeTag;
64     private File mTmpFile;
65     private FileInputStream mFileStream;
66 
EasOutboxSync(final Context context, final Account account, final Message message, final boolean useSmartSend)67     public EasOutboxSync(final Context context, final Account account, final Message message,
68             final boolean useSmartSend) {
69         super(context, account);
70         mMessage = message;
71         mIsEas14 = (Double.parseDouble(mAccount.mProtocolVersion) >=
72                 Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE);
73         mCacheDir = context.getCacheDir();
74         if (useSmartSend) {
75             mSmartSendInfo = SmartSendInfo.getSmartSendInfo(mContext, mAccount, mMessage);
76         } else {
77             mSmartSendInfo = null;
78         }
79         mModeTag = getModeTag(mSmartSendInfo);
80     }
81 
82     @Override
getCommand()83     protected String getCommand() {
84         String cmd = "SendMail";
85         if (mSmartSendInfo != null) {
86             // In EAS 14, we don't send itemId and collectionId in the command
87             if (mIsEas14) {
88                 cmd = mSmartSendInfo.isForward() ? "SmartForward" : "SmartReply";
89             } else {
90                 cmd = mSmartSendInfo.generateSmartSendCmd();
91             }
92         }
93         // If we're not EAS 14, add our save-in-sent setting here
94         if (!mIsEas14) {
95             cmd += "&SaveInSent=T";
96         }
97         return cmd;
98     }
99 
100     @Override
getRequestEntity()101     protected HttpEntity getRequestEntity() throws IOException, MessageInvalidException {
102         try {
103             mTmpFile = File.createTempFile("eas_", "tmp", mCacheDir);
104         } catch (final IOException e) {
105             LogUtils.w(LOG_TAG, "IO error creating temp file");
106             throw new IllegalStateException("Failure creating temp file");
107         }
108 
109         if (!writeMessageToTempFile(mTmpFile, mMessage, mSmartSendInfo)) {
110             // There are several reasons this could happen, possibly the message is corrupt (e.g.
111             // the To header is null) or the disk is too full to handle the temporary message.
112             // We can't send this message, but we don't want to abort the entire sync. Returning
113             // this error code will let the caller recognize that this operation failed, but we
114             // should continue on with the rest of the sync.
115             LogUtils.w(LOG_TAG, "IO error writing to temp file");
116             throw new MessageInvalidException("Failure writing to temp file");
117         }
118 
119         try {
120             mFileStream = new FileInputStream(mTmpFile);
121         } catch (final FileNotFoundException e) {
122             LogUtils.w(LOG_TAG, "IO error creating fileInputStream");
123             throw new IllegalStateException("Failure creating fileInputStream");
124         }
125           final long fileLength = mTmpFile.length();
126           final HttpEntity entity;
127           if (mIsEas14) {
128               entity = new SendMailEntity(mFileStream, fileLength, mModeTag, mMessage,
129                       mSmartSendInfo);
130           } else {
131               entity = new InputStreamEntity(mFileStream, fileLength);
132           }
133 
134           return entity;
135     }
136 
137     @Override
handleHttpError(int httpStatus)138     protected int handleHttpError(int httpStatus) {
139         if (httpStatus == HttpStatus.SC_INTERNAL_SERVER_ERROR && mSmartSendInfo != null) {
140             // Let's retry without "smart" commands.
141             return RESULT_ITEM_NOT_FOUND;
142         } else {
143             return RESULT_OTHER_FAILURE;
144         }
145     }
146 
147     @Override
onRequestMade()148     protected void onRequestMade() {
149         try {
150             mFileStream.close();
151         } catch (IOException e) {
152             LogUtils.w(LOG_TAG, "IOException closing fileStream %s", e);
153         }
154         if (mTmpFile != null && mTmpFile.exists()) {
155             mTmpFile.delete();
156         }
157     }
158 
159     @Override
handleResponse(EasResponse response)160     protected int handleResponse(EasResponse response) throws IOException, CommandStatusException {
161         if (mIsEas14) {
162             try {
163                 // Try to parse the result
164                 final SendMailParser p = new SendMailParser(response.getInputStream(), mModeTag);
165                 // If we get here, the SendMail failed; go figure
166                 p.parse();
167                 // The parser holds the status
168                 final int status = p.getStatus();
169                 if (CommandStatus.isNeedsProvisioning(status)) {
170                     LogUtils.w(LOG_TAG, "Needs provisioning sending mail");
171                     return RESULT_PROVISIONING_ERROR;
172                 } else if (status == CommandStatus.ITEM_NOT_FOUND &&
173                         mSmartSendInfo != null) {
174                     // Let's retry without "smart" commands.
175                     LogUtils.w(LOG_TAG, "Needs provisioning sending mail");
176                     return RESULT_ITEM_NOT_FOUND;
177                 }
178 
179                 // TODO: Set syncServerId = SEND_FAILED in DB?
180                 LogUtils.d(LOG_TAG, "General failure sending mail");
181                 return RESULT_SEND_FAILED;
182             } catch (final EmptyStreamException e) {
183                 // This is actually fine; an empty stream means SendMail succeeded
184                 LogUtils.d(LOG_TAG, "empty response sending mail");
185                 // Don't return here, fall through so that we'll delete the sent message.
186             } catch (final IOException e) {
187                 // Parsing failed in some other way.
188                 LogUtils.w(LOG_TAG, "IOException sending mail");
189                 return RESULT_IO_ERROR;
190             }
191         } else {
192             // FLAG: Do we need to parse results for earlier versions?
193         }
194         mContext.getContentResolver().delete(
195             ContentUris.withAppendedId(Message.CONTENT_URI, mMessage.mId), null, null);
196         return RESULT_OK;
197     }
198 
199     /**
200      * Writes message to the temp file.
201      * @param tmpFile The temp file to use.
202      * @param message The {@link Message} to write.
203      * @param smartSendInfo The {@link SmartSendInfo} for this message send attempt.
204      * @return Whether we could successfully write the file.
205      */
writeMessageToTempFile(final File tmpFile, final Message message, final SmartSendInfo smartSendInfo)206     private boolean writeMessageToTempFile(final File tmpFile, final Message message,
207             final SmartSendInfo smartSendInfo) {
208         final FileOutputStream fileStream;
209         try {
210             fileStream = new FileOutputStream(tmpFile);
211             Log.d(LogUtils.TAG, "created outputstream");
212         } catch (final FileNotFoundException e) {
213             Log.e(LogUtils.TAG, "Failed to create message file", e);
214             return false;
215         }
216         try {
217             final boolean smartSend = smartSendInfo != null;
218             final ArrayList<Attachment> attachments =
219                     smartSend ? smartSendInfo.mRequiredAtts : null;
220             Rfc822Output.writeTo(mContext, message, fileStream, smartSend, true, attachments);
221         } catch (final Exception e) {
222             Log.e(LogUtils.TAG, "Failed to write message file", e);
223             return false;
224         } finally {
225             try {
226                 fileStream.close();
227             } catch (final IOException e) {
228                 // should not happen
229                 Log.e(LogUtils.TAG, "Failed to close file - should not happen", e);
230             }
231         }
232         return true;
233     }
234 
getModeTag(final SmartSendInfo smartSendInfo)235     private int getModeTag(final SmartSendInfo smartSendInfo) {
236         if (mIsEas14) {
237             if (smartSendInfo == null) {
238                 return Tags.COMPOSE_SEND_MAIL;
239             } else if (smartSendInfo.isForward()) {
240                 return Tags.COMPOSE_SMART_FORWARD;
241             } else {
242                 return Tags.COMPOSE_SMART_REPLY;
243             }
244         }
245         return 0;
246     }
247 
248     /**
249      * Information needed for SmartReply/SmartForward.
250      */
251     private static class SmartSendInfo {
252         public static final String[] BODY_SOURCE_PROJECTION =
253                 new String[] {BodyColumns.SOURCE_MESSAGE_KEY};
254         public static final String WHERE_MESSAGE_KEY = Body.MESSAGE_KEY + "=?";
255 
256         final String mItemId;
257         final String mCollectionId;
258         final boolean mIsReply;
259         final ArrayList<Attachment> mRequiredAtts;
260 
SmartSendInfo(final String itemId, final String collectionId, final boolean isReply,ArrayList<Attachment> requiredAtts)261         private SmartSendInfo(final String itemId, final String collectionId,
262                 final boolean isReply,ArrayList<Attachment> requiredAtts) {
263             mItemId = itemId;
264             mCollectionId = collectionId;
265             mIsReply = isReply;
266             mRequiredAtts = requiredAtts;
267         }
268 
generateSmartSendCmd()269         public String generateSmartSendCmd() {
270             final StringBuilder sb = new StringBuilder();
271             sb.append(isForward() ? "SmartForward" : "SmartReply");
272             sb.append("&ItemId=");
273             sb.append(Uri.encode(mItemId, ":"));
274             sb.append("&CollectionId=");
275             sb.append(Uri.encode(mCollectionId, ":"));
276             return sb.toString();
277         }
278 
isForward()279         public boolean isForward() {
280             return !mIsReply;
281         }
282 
283         /**
284          * See if a given attachment is among an array of attachments; it is if the locations of
285          * both are the same (we're looking to see if they represent the same attachment on the
286          * server. Note that an attachment that isn't on the server (e.g. an outbound attachment
287          * picked from the  gallery) won't have a location, so the result will always be false.
288          *
289          * @param att the attachment to test
290          * @param atts the array of attachments to look in
291          * @return whether the test attachment is among the array of attachments
292          */
amongAttachments(final Attachment att, final Attachment[] atts)293         private static boolean amongAttachments(final Attachment att, final Attachment[] atts) {
294             final String location = att.mLocation;
295             if (location == null) return false;
296             for (final Attachment a: atts) {
297                 if (location.equals(a.mLocation)) {
298                     return true;
299                 }
300             }
301             return false;
302         }
303 
304         /**
305          * If this message should use SmartReply or SmartForward, return an object with the data
306          * for the smart send.
307          *
308          * @param context the caller's context
309          * @param account the Account we're sending from
310          * @param message the Message being sent
311          * @return an object to support smart sending, or null if not applicable.
312          */
getSmartSendInfo(final Context context, final Account account, final Message message)313         public static SmartSendInfo getSmartSendInfo(final Context context,
314                 final Account account, final Message message) {
315             final int flags = message.mFlags;
316             // We only care about the original message if we include quoted text.
317             if ((flags & Message.FLAG_NOT_INCLUDE_QUOTED_TEXT) != 0) {
318                 return null;
319             }
320             final boolean reply = (flags & Message.FLAG_TYPE_REPLY) != 0;
321             final boolean forward = (flags & Message.FLAG_TYPE_FORWARD) != 0;
322             // We also only care for replies or forwards.
323             if (!reply && !forward) {
324                 return null;
325             }
326             // Just a sanity check here, since we assume that reply and forward are mutually
327             // exclusive throughout this class.
328             if (reply && forward) {
329                 return null;
330             }
331             // If we don't support SmartForward and it's a forward, then don't proceed.
332             if (forward && (account.mFlags & Account.FLAGS_SUPPORTS_SMART_FORWARD) == 0) {
333                 return null;
334             }
335 
336             // Note: itemId and collectionId are the terms used by EAS to refer to the serverId and
337             // mailboxId of a Message
338             String itemId = null;
339             String collectionId = null;
340 
341             // First, we need to get the id of the reply/forward message
342             String[] cols = Utility.getRowColumns(context, Body.CONTENT_URI, BODY_SOURCE_PROJECTION,
343                     WHERE_MESSAGE_KEY, new String[] {Long.toString(message.mId)});
344             long refId = 0;
345             // TODO: We can probably just write a smarter query to do this all at once.
346             if (cols != null && cols[0] != null) {
347                 refId = Long.parseLong(cols[0]);
348                 // Then, we need the serverId and mailboxKey of the message
349                 cols = Utility.getRowColumns(context, Message.CONTENT_URI, refId,
350                         SyncColumns.SERVER_ID, MessageColumns.MAILBOX_KEY,
351                         MessageColumns.PROTOCOL_SEARCH_INFO);
352                 if (cols != null) {
353                     itemId = cols[0];
354                     final long boxId = Long.parseLong(cols[1]);
355                     // Then, we need the serverId of the mailbox
356                     cols = Utility.getRowColumns(context, Mailbox.CONTENT_URI, boxId,
357                             MailboxColumns.SERVER_ID);
358                     if (cols != null) {
359                         collectionId = cols[0];
360                     }
361                 }
362             }
363             // We need either a longId or both itemId (serverId) and collectionId (mailboxId) to
364             // process a smart reply or a smart forward
365             if (itemId != null && collectionId != null) {
366                 final ArrayList<Attachment> requiredAtts;
367                 if (forward) {
368                     // See if we can really smart forward (all reference attachments must be sent)
369                     final Attachment[] outAtts =
370                             Attachment.restoreAttachmentsWithMessageId(context, message.mId);
371                     final Attachment[] refAtts =
372                             Attachment.restoreAttachmentsWithMessageId(context, refId);
373                     for (final Attachment refAtt: refAtts) {
374                         // If an original attachment isn't among what's going out, we can't be smart
375                         if (!amongAttachments(refAtt, outAtts)) {
376                             return null;
377                         }
378                     }
379                     requiredAtts = new ArrayList<Attachment>();
380                     for (final Attachment outAtt: outAtts) {
381                         // If an outgoing attachment isn't in original message, we must send it
382                         if (!amongAttachments(outAtt, refAtts)) {
383                             requiredAtts.add(outAtt);
384                         }
385                     }
386                 } else {
387                     requiredAtts = null;
388                 }
389                 return new SmartSendInfo(itemId, collectionId, reply, requiredAtts);
390             }
391             return null;
392         }
393     }
394 
395     @Override
getRequestContentType()396     public String getRequestContentType() {
397         // When using older protocols, we need to use a different MIME type for sending messages.
398         if (getProtocolVersion() < Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE) {
399             return MimeUtility.MIME_TYPE_RFC822;
400         } else {
401             return super.getRequestContentType();
402         }
403     }
404 
405     /**
406      * Our own HttpEntity subclass that is able to insert opaque data (in this case the MIME
407      * representation of the message body as stored in a temporary file) into the serializer stream
408      */
409     private static class SendMailEntity extends InputStreamEntity {
410         private final FileInputStream mFileStream;
411         private final long mFileLength;
412         private final int mSendTag;
413         private final Message mMessage;
414         private final SmartSendInfo mSmartSendInfo;
415 
SendMailEntity(final FileInputStream instream, final long length, final int tag, final Message message, final SmartSendInfo smartSendInfo)416         public SendMailEntity(final FileInputStream instream, final long length, final int tag,
417                 final Message message, final SmartSendInfo smartSendInfo) {
418             super(instream, length);
419             mFileStream = instream;
420             mFileLength = length;
421             mSendTag = tag;
422             mMessage = message;
423             mSmartSendInfo = smartSendInfo;
424         }
425 
426         /**
427          * We always return -1 because we don't know the actual length of the POST data (this
428          * causes HttpClient to send the data in "chunked" mode)
429          */
430         @Override
getContentLength()431         public long getContentLength() {
432             final ByteArrayOutputStream baos = new ByteArrayOutputStream();
433             try {
434                 // Calculate the overhead for the WBXML data
435                 writeTo(baos, false);
436                 // Return the actual size that will be sent
437                 return baos.size() + mFileLength;
438             } catch (final IOException e) {
439                 // Just return -1 (unknown)
440             } finally {
441                 try {
442                     baos.close();
443                 } catch (final IOException e) {
444                     // Ignore
445                 }
446             }
447             return -1;
448         }
449 
450         @Override
writeTo(final OutputStream outstream)451         public void writeTo(final OutputStream outstream) throws IOException {
452             writeTo(outstream, true);
453         }
454 
455         /**
456          * Write the message to the output stream
457          * @param outstream the output stream to write
458          * @param withData whether or not the actual data is to be written; true when sending
459          *   mail; false when calculating size only
460          * @throws IOException
461          */
writeTo(final OutputStream outstream, final boolean withData)462         public void writeTo(final OutputStream outstream, final boolean withData)
463                 throws IOException {
464             // Not sure if this is possible; the check is taken from the superclass
465             if (outstream == null) {
466                 throw new IllegalArgumentException("Output stream may not be null");
467             }
468 
469             // We'll serialize directly into the output stream
470             final Serializer s = new Serializer(outstream);
471             // Send the appropriate initial tag
472             s.start(mSendTag);
473             // The Message-Id for this message (note that we cannot use the messageId stored in
474             // the message, as EAS 14 limits the length to 40 chars and we use 70+)
475             s.data(Tags.COMPOSE_CLIENT_ID, "SendMail-" + System.nanoTime());
476             // We always save sent mail
477             s.tag(Tags.COMPOSE_SAVE_IN_SENT_ITEMS);
478 
479             // If we're using smart reply/forward, we need info about the original message
480             if (mSendTag != Tags.COMPOSE_SEND_MAIL) {
481                 if (mSmartSendInfo != null) {
482                     s.start(Tags.COMPOSE_SOURCE);
483                     // For search results, use the long id (stored in mProtocolSearchInfo); else,
484                     // use folder id/item id combo
485                     if (mMessage.mProtocolSearchInfo != null) {
486                         s.data(Tags.COMPOSE_LONG_ID, mMessage.mProtocolSearchInfo);
487                     } else {
488                         s.data(Tags.COMPOSE_ITEM_ID, mSmartSendInfo.mItemId);
489                         s.data(Tags.COMPOSE_FOLDER_ID, mSmartSendInfo.mCollectionId);
490                     }
491                     s.end();  // Tags.COMPOSE_SOURCE
492                 }
493             }
494 
495             // Start the MIME tag; this is followed by "opaque" data (byte array)
496             s.start(Tags.COMPOSE_MIME);
497             // Send opaque data from the file stream
498             if (withData) {
499                 s.opaque(mFileStream, (int)mFileLength);
500             } else {
501                 s.opaqueWithoutData((int)mFileLength);
502             }
503             // And we're done
504             s.end().end().done();
505         }
506     }
507 }
508