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