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