package com.fsck.k9.mail.store; import android.app.Application; import android.content.ContentValues; import android.content.SharedPreferences; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; import android.net.Uri; import android.util.Log; import com.fsck.k9.Account; import com.fsck.k9.K9; import com.fsck.k9.Preferences; import com.fsck.k9.controller.MessageRemovalListener; import com.fsck.k9.controller.MessageRetrievalListener; import com.fsck.k9.helper.Regex; import com.fsck.k9.helper.Utility; import com.fsck.k9.mail.*; import com.fsck.k9.mail.Message.RecipientType; import com.fsck.k9.mail.filter.Base64OutputStream; import com.fsck.k9.mail.internet.*; import com.fsck.k9.provider.AttachmentProvider; import org.apache.commons.io.IOUtils; import java.io.*; import java.net.URI; import java.net.URLEncoder; import java.util.*; import java.util.regex.Matcher; /** *
* Implements a SQLite database backed local store for Messages. **/ public class LocalStore extends Store implements Serializable { /** * Immutable empty {@link String} array */ private static final String[] EMPTY_STRING_ARRAY = new String[0]; private static final int DB_VERSION = 35; private static final Flag[] PERMANENT_FLAGS = { Flag.DELETED, Flag.X_DESTROYED, Flag.SEEN, Flag.FLAGGED }; private String mPath; private SQLiteDatabase mDb; private File mAttachmentsDir; private Application mApplication; private String uUid = null; private static Set
"; } else { return ""; } } private String htmlifyMessageFooter() { if (K9.messageViewFixedWidthFont()) { return ""; } else { return ""; } } public String convertEmoji2ImgForDocomo(String html) { StringReader reader = new StringReader(html); StringBuilder buff = new StringBuilder(html.length() + 512); int c = 0; try { while ((c = reader.read()) != -1) { switch (c) { // Emoji case 0xE63E: // Fine buff.append(""); break; case 0xE63F: // Cloudy buff.append(""); break; case 0xE640: // Rain buff.append(""); break; case 0xE641: // Snow buff.append(""); break; case 0xE642: // Thunder buff.append(""); break; case 0xE643: // Typhoon buff.append(""); break; case 0xE644: // Fog buff.append(""); break; case 0xE645: // Drizzle buff.append(""); break; // Zodiacal symbol case 0xE646: // Aries buff.append(""); break; case 0xE647: // Taurus buff.append(""); break; case 0xE648: // Gemini buff.append(""); break; case 0xE649: // Cancer buff.append(""); break; case 0xE64A: // Leo buff.append(""); break; case 0xE64B: // Virgo buff.append(""); break; case 0xE64C: // Libra buff.append(""); break; case 0xE64D: // Scorpio buff.append(""); break; case 0xE64E: // Sagittarius buff.append(""); break; case 0xE64F: // Capricorn buff.append(""); break; case 0xE650: // Aquarius buff.append(""); break; case 0xE651: // Pisces buff.append(""); break; case 0xE652: buff.append(""); break; case 0xE653: buff.append(""); break; case 0xE654: buff.append(""); break; case 0xE655: buff.append(""); break; case 0xE656: buff.append(""); break; case 0xE657: buff.append(""); break; case 0xE658: buff.append(""); break; case 0xE659: buff.append(""); break; case 0xE65A: buff.append(""); break; case 0xE65B: buff.append(""); break; case 0xE65C: buff.append(""); break; case 0xE65D: buff.append(""); break; case 0xE65E: buff.append(""); break; case 0xE65F: buff.append(""); break; case 0xE660: buff.append(""); break; case 0xE661: buff.append(""); break; case 0xE662: buff.append(""); break; case 0xE663: buff.append(""); break; case 0xE664: buff.append(""); break; case 0xE665: buff.append(""); break; case 0xE666: buff.append(""); break; case 0xE667: buff.append(""); break; case 0xE668: buff.append(""); break; case 0xE669: buff.append(""); break; case 0xE66A: buff.append(""); break; case 0xE66B: buff.append(""); break; case 0xE66C: buff.append(""); break; case 0xE66D: buff.append(""); break; case 0xE66E: buff.append(""); break; case 0xE66F: buff.append(""); break; case 0xE670: buff.append(""); break; case 0xE671: buff.append(""); break; case 0xE672: buff.append(""); break; case 0xE673: buff.append(""); break; case 0xE674: buff.append(""); break; case 0xE675: // Hairdresser buff.append(""); break; case 0xE676: buff.append(""); break; case 0xE677: buff.append(""); break; case 0xE678: buff.append(""); break; case 0xE679: buff.append(""); break; case 0xE67A: buff.append(""); break; case 0xE67B: buff.append(""); break; case 0xE67C: buff.append(""); break; case 0xE67D: buff.append(""); break; case 0xE67E: buff.append(""); break; case 0xE67F: buff.append(""); break; case 0xE680: buff.append(""); break; case 0xE681: buff.append(""); break; case 0xE682: buff.append(""); break; case 0xE683: buff.append(""); break; case 0xE684: buff.append(""); break; case 0xE685: buff.append(""); break; case 0xE686: buff.append(""); break; case 0xE687: buff.append(""); break; case 0xE688: buff.append(""); break; case 0xE689: buff.append(""); break; case 0xE68A: buff.append(""); break; case 0xE68B: buff.append(""); break; case 0xE68C: buff.append(""); break; case 0xE68D: buff.append(""); break; case 0xE68E: buff.append(""); break; case 0xE68F: buff.append(""); break; case 0xE690: buff.append(""); break; case 0xE691: // Eyes buff.append(""); break; case 0xE692: // Ear buff.append(""); break; case 0xE693: buff.append(""); break; case 0xE694: buff.append(""); break; case 0xE695: buff.append(""); break; case 0xE696: buff.append(""); break; case 0xE697: buff.append(""); break; case 0xE698: buff.append(""); break; case 0xE699: buff.append(""); break; case 0xE69A: buff.append(""); break; case 0xE69B: buff.append(""); break; case 0xE69C: // New moon buff.append(""); break; case 0xE69D: // Waning moon buff.append(""); break; case 0xE69E: // Half moon buff.append(""); break; case 0xE69F: // Crescent moon buff.append(""); break; case 0xE6A0: // Full moon buff.append(""); break; case 0xE6A1: buff.append(""); break; case 0xE6A2: buff.append(""); break; case 0xE6A3: buff.append(""); break; case 0xE6A4: buff.append(""); break; case 0xE6A5: buff.append(""); break; case 0xE6AC: buff.append(""); break; case 0xE6AD: buff.append(""); break; case 0xE6AE: buff.append(""); break; case 0xE6B1: // Silhouette buff.append(""); break; case 0xE6B2: buff.append(""); break; case 0xE6B3: // Night buff.append(""); break; case 0xE6B7: buff.append(""); break; case 0xE6B8: buff.append(""); break; case 0xE6B9: buff.append(""); break; case 0xE6BA: // Clock buff.append(""); break; case 0xE6CE: buff.append(""); break; case 0xE6CF: buff.append(""); break; case 0xE6D0: buff.append(""); break; case 0xE6D1: buff.append(""); break; case 0xE6D2: buff.append(""); break; case 0xE6D3: buff.append(""); break; case 0xE6D4: buff.append(""); break; case 0xE6D5: buff.append(""); break; case 0xE6D6: buff.append(""); break; case 0xE6D7: buff.append(""); break; case 0xE6D8: buff.append(""); break; case 0xE6D9: buff.append(""); break; case 0xE6DA: buff.append(""); break; case 0xE6DB: buff.append(""); break; case 0xE6DC: buff.append(""); break; case 0xE6DD: buff.append(""); break; case 0xE6DE: buff.append(""); break; case 0xE6DF: buff.append(""); break; case 0xE6E0: buff.append(""); break; case 0xE6E1: buff.append(""); break; case 0xE6E2: buff.append(""); break; case 0xE6E3: buff.append(""); break; case 0xE6E4: buff.append(""); break; case 0xE6E5: buff.append(""); break; case 0xE6E6: buff.append(""); break; case 0xE6E7: buff.append(""); break; case 0xE6E8: buff.append(""); break; case 0xE6E9: buff.append(""); break; case 0xE6EA: buff.append(""); break; case 0xE6EB: buff.append(""); break; case 0xE6EC: // Black heart buff.append(""); break; case 0xE6ED: buff.append(""); break; case 0xE6EE: buff.append(""); break; case 0xE6EF: buff.append(""); break; case 0xE6F0: // Happy face buff.append(""); break; case 0xE6F1: buff.append(""); break; case 0xE6F2: buff.append(""); break; case 0xE6F3: buff.append(""); break; case 0xE6F4: buff.append(""); break; case 0xE6F5: buff.append(""); break; case 0xE6F6: buff.append(""); break; case 0xE6F7: buff.append(""); break; case 0xE6F8: buff.append(""); break; case 0xE6F9: // Kiss buff.append(""); break; case 0xE6FA: buff.append(""); break; case 0xE6FB: buff.append(""); break; case 0xE6FC: buff.append(""); break; case 0xE6FD: buff.append(""); break; case 0xE6FE: buff.append(""); break; case 0xE6FF: buff.append(""); break; case 0xE700: buff.append(""); break; case 0xE701: buff.append(""); break; case 0xE702: buff.append(""); break; case 0xE703: buff.append(""); break; case 0xE704: buff.append(""); break; case 0xE705: buff.append(""); break; case 0xE706: buff.append(""); break; case 0xE707: buff.append(""); break; case 0xE708: buff.append(""); break; case 0xE709: buff.append(""); break; case 0xE70A: buff.append(""); break; case 0xE70B: buff.append(""); break; case 0xE70C: buff.append(""); break; case 0xE70D: buff.append(""); break; case 0xE70E: buff.append(""); break; case 0xE70F: buff.append(""); break; case 0xE710: // Make-up buff.append(""); break; case 0xE711: buff.append(""); break; case 0xE712: buff.append(""); break; case 0xE713: buff.append(""); break; case 0xE714: buff.append(""); break; case 0xE715: buff.append(""); break; case 0xE716: buff.append(""); break; case 0xE717: buff.append(""); break; case 0xE718: buff.append(""); break; case 0xE719: buff.append(""); break; case 0xE71A: buff.append(""); break; case 0xE71B: buff.append(""); break; case 0xE71C: // Sandglass buff.append(""); break; case 0xE71D: buff.append(""); break; case 0xE71E: buff.append(""); break; case 0xE71F: // Wrist watch buff.append(""); break; case 0xE720: buff.append(""); break; case 0xE721: buff.append(""); break; case 0xE722: buff.append(""); break; case 0xE723: buff.append(""); break; case 0xE724: // Pouting face buff.append(""); break; case 0xE725: buff.append(""); break; case 0xE726: buff.append(""); break; case 0xE727: buff.append(""); break; case 0xE728: // Sticking tongue out buff.append(""); break; case 0xE729: buff.append(""); break; case 0xE72A: buff.append(""); break; case 0xE72B: // Enduring face buff.append(""); break; case 0xE72C: buff.append(""); break; case 0xE72D: buff.append(""); break; case 0xE72E: // Tear buff.append(""); break; case 0xE72F: buff.append(""); break; case 0xE730: buff.append(""); break; case 0xE731: buff.append(""); break; case 0xE732: buff.append(""); break; case 0xE733: buff.append(""); break; case 0xE734: buff.append(""); break; case 0xE735: buff.append(""); break; case 0xE736: buff.append(""); break; case 0xE737: buff.append(""); break; case 0xE738: buff.append(""); break; case 0xE739: buff.append(""); break; case 0xE73A: buff.append(""); break; case 0xE73B: buff.append(""); break; case 0xE73C: buff.append(""); break; case 0xE73D: buff.append(""); break; case 0xE73E: buff.append(""); break; case 0xE73F: // Wave buff.append(""); break; case 0xE740: buff.append(""); break; case 0xE741: // 4-leaf clover buff.append(""); break; case 0xE742: // Cherries buff.append(""); break; case 0xE743: // Tulip buff.append(""); break; case 0xE744: // Banana buff.append(""); break; case 0xE745: // Apple buff.append(""); break; case 0xE746: // Seedling buff.append(""); break; case 0xE747: // Maple leaf buff.append(""); break; case 0xE748: // Cherry blossom buff.append(""); break; case 0xE749: buff.append(""); break; case 0xE74A: buff.append(""); break; case 0xE74B: buff.append(""); break; case 0xE74C: buff.append(""); break; case 0xE74D: buff.append(""); break; case 0xE74E: buff.append(""); break; case 0xE74F: buff.append(""); break; case 0xE750: buff.append(""); break; case 0xE751: buff.append(""); break; case 0xE752: buff.append(""); break; case 0xE753: buff.append(""); break; case 0xE754: buff.append(""); break; case 0xE755: buff.append(""); break; case 0xE756: buff.append(""); break; case 0xE757: // Very thin buff.append(""); break; default: buff.append((char)c); }//switch } } catch (IOException e) { //Should never happen Log.e(K9.LOG_TAG, null, e); } return buff.toString(); } @Override public boolean isInTopGroup() { return inTopGroup; } public void setInTopGroup(boolean inTopGroup) { this.inTopGroup = inTopGroup; } } public class LocalTextBody extends TextBody { private String mBodyForDisplay; public LocalTextBody(String body) { super(body); } public LocalTextBody(String body, String bodyForDisplay) throws MessagingException { super(body); this.mBodyForDisplay = bodyForDisplay; } public String getBodyForDisplay() { return mBodyForDisplay; } public void setBodyForDisplay(String mBodyForDisplay) { this.mBodyForDisplay = mBodyForDisplay; } }//LocalTextBody public class LocalMessage extends MimeMessage { private long mId; private int mAttachmentCount; private String mSubject; private String mPreview = ""; private boolean mHeadersLoaded = false; private boolean mMessageDirty = false; public LocalMessage() { } LocalMessage(String uid, Folder folder) throws MessagingException { this.mUid = uid; this.mFolder = folder; } private void populateFromGetMessageCursor(Cursor cursor) throws MessagingException { this.setSubject(cursor.getString(0) == null ? "" : cursor.getString(0)); Address[] from = Address.unpack(cursor.getString(1)); if (from.length > 0) { this.setFrom(from[0]); } this.setInternalSentDate(new Date(cursor.getLong(2))); this.setUid(cursor.getString(3)); String flagList = cursor.getString(4); if (flagList != null && flagList.length() > 0) { String[] flags = flagList.split(","); for (String flag : flags) { try { this.setFlagInternal(Flag.valueOf(flag), true); } catch (Exception e) { if ("X_BAD_FLAG".equals(flag) == false) { Log.w(K9.LOG_TAG, "Unable to parse flag " + flag); } } } } this.mId = cursor.getLong(5); this.setRecipients(RecipientType.TO, Address.unpack(cursor.getString(6))); this.setRecipients(RecipientType.CC, Address.unpack(cursor.getString(7))); this.setRecipients(RecipientType.BCC, Address.unpack(cursor.getString(8))); this.setReplyTo(Address.unpack(cursor.getString(9))); this.mAttachmentCount = cursor.getInt(10); this.setInternalDate(new Date(cursor.getLong(11))); this.setMessageId(cursor.getString(12)); mPreview = (cursor.getString(14) == null ? "" : cursor.getString(14)); if (this.mFolder == null) { LocalFolder f = new LocalFolder(cursor.getInt(13)); f.open(LocalFolder.OpenMode.READ_WRITE); this.mFolder = f; } } /* Custom version of writeTo that updates the MIME message based on localMessage * changes. */ @Override public void writeTo(OutputStream out) throws IOException, MessagingException { if (mMessageDirty) buildMimeRepresentation(); super.writeTo(out); } private void buildMimeRepresentation() throws MessagingException { if (!mMessageDirty) { return; } super.setSubject(mSubject); if (this.mFrom != null && this.mFrom.length > 0) { super.setFrom(this.mFrom[0]); } super.setReplyTo(mReplyTo); super.setSentDate(this.getSentDate()); super.setRecipients(RecipientType.TO, mTo); super.setRecipients(RecipientType.CC, mCc); super.setRecipients(RecipientType.BCC, mBcc); if (mMessageId != null) super.setMessageId(mMessageId); mMessageDirty = false; return; } public String getPreview() { return mPreview; } @Override public String getSubject() throws MessagingException { return mSubject; } @Override public void setSubject(String subject) throws MessagingException { mSubject = subject; mMessageDirty = true; } @Override public void setMessageId(String messageId) { mMessageId = messageId; mMessageDirty = true; } public int getAttachmentCount() { return mAttachmentCount; } @Override public void setFrom(Address from) throws MessagingException { this.mFrom = new Address[] { from }; mMessageDirty = true; } @Override public void setReplyTo(Address[] replyTo) throws MessagingException { if (replyTo == null || replyTo.length == 0) { mReplyTo = null; } else { mReplyTo = replyTo; } mMessageDirty = true; } /* * For performance reasons, we add headers instead of setting them (see super implementation) * which removes (expensive) them before adding them */ @Override public void setRecipients(RecipientType type, Address[] addresses) throws MessagingException { if (type == RecipientType.TO) { if (addresses == null || addresses.length == 0) { this.mTo = null; } else { this.mTo = addresses; } } else if (type == RecipientType.CC) { if (addresses == null || addresses.length == 0) { this.mCc = null; } else { this.mCc = addresses; } } else if (type == RecipientType.BCC) { if (addresses == null || addresses.length == 0) { this.mBcc = null; } else { this.mBcc = addresses; } } else { throw new MessagingException("Unrecognized recipient type."); } mMessageDirty = true; } public void setFlagInternal(Flag flag, boolean set) throws MessagingException { super.setFlag(flag, set); } public long getId() { return mId; } @Override public void setFlag(Flag flag, boolean set) throws MessagingException { if (flag == Flag.DELETED && set) { /* * If a message is being marked as deleted we want to clear out it's content * and attachments as well. Delete will not actually remove the row since we need * to retain the uid for synchronization purposes. */ /* * Delete all of the messages' content to save space. */ ((LocalFolder) mFolder).deleteAttachments(getUid()); mDb.execSQL( "UPDATE messages SET " + "deleted = 1," + "subject = NULL, " + "sender_list = NULL, " + "date = NULL, " + "to_list = NULL, " + "cc_list = NULL, " + "bcc_list = NULL, " + "preview = NULL, " + "html_content = NULL, " + "text_content = NULL, " + "reply_to_list = NULL " + "WHERE id = ?", new Object[] { mId }); /* * Delete all of the messages' attachments to save space. */ mDb.execSQL("DELETE FROM attachments WHERE message_id = ?", new Object[] { mId }); ((LocalFolder)mFolder).deleteHeaders(mId); } else if (flag == Flag.X_DESTROYED && set) { ((LocalFolder) mFolder).deleteAttachments(getUid()); mDb.execSQL("DELETE FROM messages WHERE id = ?", new Object[] { mId }); ((LocalFolder)mFolder).deleteHeaders(mId); } /* * Update the unread count on the folder. */ try { if (flag == Flag.DELETED || flag == Flag.X_DESTROYED || (flag == Flag.SEEN && !isSet(Flag.DELETED))) { LocalFolder folder = (LocalFolder)mFolder; if (set && !isSet(Flag.SEEN)) { folder.setUnreadMessageCount(folder.getUnreadMessageCount() - 1); } else if (!set && isSet(Flag.SEEN)) { folder.setUnreadMessageCount(folder.getUnreadMessageCount() + 1); } } if ((flag == Flag.DELETED || flag == Flag.X_DESTROYED) && isSet(Flag.FLAGGED)) { LocalFolder folder = (LocalFolder)mFolder; if (set) { folder.setFlaggedMessageCount(folder.getFlaggedMessageCount() - 1); } else { folder.setFlaggedMessageCount(folder.getFlaggedMessageCount() + 1); } } if (flag == Flag.FLAGGED && !isSet(Flag.DELETED)) { LocalFolder folder = (LocalFolder)mFolder; if (set) { folder.setFlaggedMessageCount(folder.getFlaggedMessageCount() + 1); } else { folder.setFlaggedMessageCount(folder.getFlaggedMessageCount() - 1); } } } catch (MessagingException me) { Log.e(K9.LOG_TAG, "Unable to update LocalStore unread message count", me); throw new RuntimeException(me); } super.setFlag(flag, set); /* * Set the flags on the message. */ mDb.execSQL("UPDATE messages " + "SET flags = ? " + " WHERE id = ?", new Object[] { Utility.combine(getFlags(), ',').toUpperCase(), mId }); } private void loadHeaders() { ArrayListmessages = new ArrayList (); messages.add(this); mHeadersLoaded = true; // set true before calling populate headers to stop recursion ((LocalFolder) mFolder).populateHeaders(messages); } @Override public void addHeader(String name, String value) { if (!mHeadersLoaded) loadHeaders(); super.addHeader(name, value); } @Override public void setHeader(String name, String value) { if (!mHeadersLoaded) loadHeaders(); super.setHeader(name, value); } @Override public String[] getHeader(String name) { if (!mHeadersLoaded) loadHeaders(); return super.getHeader(name); } @Override public void removeHeader(String name) { if (!mHeadersLoaded) loadHeaders(); super.removeHeader(name); } @Override public Set getHeaderNames() { if (!mHeadersLoaded) loadHeaders(); return super.getHeaderNames(); } } public class LocalAttachmentBodyPart extends MimeBodyPart { private long mAttachmentId = -1; public LocalAttachmentBodyPart(Body body, long attachmentId) throws MessagingException { super(body); mAttachmentId = attachmentId; } /** * Returns the local attachment id of this body, or -1 if it is not stored. * @return */ public long getAttachmentId() { return mAttachmentId; } public void setAttachmentId(long attachmentId) { mAttachmentId = attachmentId; } @Override public String toString() { return "" + mAttachmentId; } } public static class LocalAttachmentBody implements Body { private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; private Application mApplication; private Uri mUri; public LocalAttachmentBody(Uri uri, Application application) { mApplication = application; mUri = uri; } public InputStream getInputStream() throws MessagingException { try { return mApplication.getContentResolver().openInputStream(mUri); } catch (FileNotFoundException fnfe) { /* * Since it's completely normal for us to try to serve up attachments that * have been blown away, we just return an empty stream. */ return new ByteArrayInputStream(EMPTY_BYTE_ARRAY); } } public void writeTo(OutputStream out) throws IOException, MessagingException { InputStream in = getInputStream(); Base64OutputStream base64Out = new Base64OutputStream(out); IOUtils.copy(in, base64Out); base64Out.close(); } public Uri getContentUri() { return mUri; } } }