()
{
@Override
public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException
{
Cursor messagesCursor = null;
try
{
messagesCursor = db.query("messages", new String[]
{ "id" }, "folder_id = ? AND uid = ?", new String[]
{ Long.toString(mFolderId), uid }, null, null, null);
while (messagesCursor.moveToNext())
{
long messageId = messagesCursor.getLong(0);
deleteAttachments(messageId);
}
}
catch (MessagingException e)
{
throw new WrappedException(e);
}
finally
{
if (messagesCursor != null)
{
messagesCursor.close();
}
}
return null;
}
});
}
catch (WrappedException e)
{
throw (MessagingException) e.getCause();
}
}
/*
* calculateContentPreview
* Takes a plain text message body as a string.
* Returns a message summary as a string suitable for showing in a message list
*
* A message summary should be about the first 160 characters
* of unique text written by the message sender
* Quoted text, "On $date" and so on will be stripped out.
* All newlines and whitespace will be compressed.
*
*/
public String calculateContentPreview(String text)
{
if (text == null)
{
return null;
}
// Only look at the first 8k of a message when calculating
// the preview. This should avoid unnecessary
// memory usage on large messages
if (text.length() > 8192)
{
text = text.substring(0,8192);
}
text = text.replaceAll("(?m)^----.*?$","");
text = text.replaceAll("(?m)^[#>].*$","");
text = text.replaceAll("(?m)^On .*wrote.?$","");
text = text.replaceAll("(?m)^.*\\w+:$","");
text = text.replaceAll("https?://\\S+","...");
text = text.replaceAll("(\\r|\\n)+"," ");
text = text.replaceAll("\\s+"," ");
if (text.length() <= 512)
{
return text;
}
else
{
text = text.substring(0,512);
return text;
}
}
public String markupContent(String text, String html)
{
if (text.length() > 0 && html.length() == 0)
{
html = HtmlConverter.textToHtml(text);
}
html = HtmlConverter.convertEmoji2Img(html);
return html;
}
@Override
public boolean isInTopGroup()
{
return mInTopGroup;
}
public void setInTopGroup(boolean inTopGroup) throws MessagingException
{
mInTopGroup = inTopGroup;
updateFolderColumn( "top_group", mInTopGroup ? 1 : 0 );
}
public Integer getLastUid()
{
return mLastUid;
}
/**
* Fetches the most recent numeric UID value in this folder. This is used by
* {@link com.fsck.k9.controller.MessagingController#shouldNotifyForMessage} to see if messages being
* fetched are new and unread. Messages are "new" if they have a UID higher than the most recent UID prior
* to synchronization.
*
* This only works for protocols with numeric UIDs (like IMAP). For protocols with
* alphanumeric UIDs (like POP), this method quietly fails and shouldNotifyForMessage() will
* always notify for unread messages.
*
* Once Issue 1072 has been fixed, this method and shouldNotifyForMessage() should be
* updated to use internal dates rather than UIDs to determine new-ness. While this doesn't
* solve things for POP (which doesn't have internal dates), we can likely use this as a
* framework to examine send date in lieu of internal date.
* @throws MessagingException
*/
public void updateLastUid() throws MessagingException
{
Integer lastUid = database.execute(false, new DbCallback()
{
@Override
public Integer doDbWork(final SQLiteDatabase db)
{
Cursor cursor = null;
try
{
open(OpenMode.READ_ONLY);
cursor = db.rawQuery("SELECT MAX(uid) FROM messages WHERE folder_id=?", new String[] { Long.toString(mFolderId) });
if (cursor.getCount() > 0)
{
cursor.moveToFirst();
return cursor.getInt(0);
}
}
catch (Exception e)
{
Log.e(K9.LOG_TAG, "Unable to updateLastUid: ", e);
}
finally
{
if (cursor != null)
{
cursor.close();
}
}
return null;
}
});
if(K9.DEBUG)
Log.d(K9.LOG_TAG, "Updated last UID for folder " + mName + " to " + lastUid);
mLastUid = lastUid;
}
}
public static class LocalTextBody extends TextBody
{
/**
* This is an HTML-ified version of the message for display purposes.
*/
private String mBodyForDisplay;
public LocalTextBody(String body)
{
super(body);
}
public LocalTextBody(String body, String bodyForDisplay)
{
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 mToMeCalculated = false;
private boolean mCcMeCalculated = false;
private boolean mToMe = false;
private boolean mCcMe = false;
private boolean mHeadersLoaded = false;
private boolean mMessageDirty = false;
public LocalMessage()
{
}
LocalMessage(String uid, Folder folder)
{
this.mUid = uid;
this.mFolder = folder;
}
private void populateFromGetMessageCursor(Cursor cursor)
throws MessagingException
{
final String subject = cursor.getString(0);
this.setSubject(subject == null ? "" : subject);
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))
{
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));
final String preview = cursor.getString(14);
mPreview = (preview == null ? "" : preview);
if (this.mFolder == null)
{
LocalFolder f = new LocalFolder(cursor.getInt(13));
f.open(LocalFolder.OpenMode.READ_WRITE);
this.mFolder = f;
}
}
/**
* Fetch the message text for display. This always returns an HTML-ified version of the
* message, even if it was originally a text-only message.
* @return HTML version of message for display purposes.
* @throws MessagingException
*/
public String getTextForDisplay() throws MessagingException
{
String text; // First try and fetch an HTML part.
Part part = MimeUtility.findFirstPartByMimeType(this, "text/html");
if (part == null)
{
// If that fails, try and get a text part.
part = MimeUtility.findFirstPartByMimeType(this, "text/plain");
if (part == null)
{
text = null;
}
else
{
LocalStore.LocalTextBody body = (LocalStore.LocalTextBody) part.getBody();
if (body == null)
{
text = null;
}
else
{
text = body.getBodyForDisplay();
}
}
}
else
{
// We successfully found an HTML part; do the necessary character set decoding.
text = MimeUtility.getTextFromPart(part);
}
return text;
}
/* 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;
}
public String getPreview()
{
return mPreview;
}
@Override
public String getSubject()
{
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 boolean hasAttachments()
{
if (mAttachmentCount > 0)
{
return true;
}
else
{
return false;
}
}
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 boolean toMe()
{
try
{
if (!mToMeCalculated)
{
for (Address address : getRecipients(RecipientType.TO))
{
if (mAccount.isAnIdentity(address))
{
mToMe = true;
mToMeCalculated = true;
}
}
}
}
catch (MessagingException e)
{
// do something better than ignore this
// getRecipients can throw a messagingexception
}
return mToMe;
}
public boolean ccMe()
{
try
{
if (!mCcMeCalculated)
{
for(Address address : getRecipients(RecipientType.CC))
{
if (mAccount.isAnIdentity(address))
{
mCcMe = true;
mCcMeCalculated = true;
}
}
}
}
catch (MessagingException e)
{
// do something better than ignore this
// getRecipients can throw a messagingexception
}
return mCcMe;
}
public void setFlagInternal(Flag flag, boolean set) throws MessagingException
{
super.setFlag(flag, set);
}
public long getId()
{
return mId;
}
@Override
public void setFlag(final Flag flag, final boolean set) throws MessagingException
{
try
{
database.execute(true, new DbCallback()
{
@Override
public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException
{
try
{
if (flag == Flag.DELETED && set)
{
delete();
}
updateFolderCountsOnFlag(flag, set);
LocalMessage.super.setFlag(flag, set);
}
catch (MessagingException e)
{
throw new WrappedException(e);
}
/*
* Set the flags on the message.
*/
db.execSQL("UPDATE messages " + "SET flags = ? " + " WHERE id = ?", new Object[]
{ Utility.combine(getFlags(), ',').toUpperCase(), mId });
return null;
}
});
}
catch (WrappedException e)
{
throw (MessagingException) e.getCause();
}
}
/*
* 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.
*/
private void delete() throws MessagingException
{
/*
* Delete all of the message's content to save space.
*/
try
{
database.execute(true, new DbCallback()
{
@Override
public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException
{
db.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 message's attachments to save space.
* We do this explicit deletion here because we're not deleting the record
* in messages, which means our ON DELETE trigger for messages won't cascade
*/
try
{
((LocalFolder) mFolder).deleteAttachments(mId);
}
catch (MessagingException e)
{
throw new WrappedException(e);
}
db.execSQL("DELETE FROM attachments WHERE message_id = ?", new Object[]
{ mId });
return null;
}
});
}
catch (WrappedException e)
{
throw (MessagingException) e.getCause();
}
((LocalFolder)mFolder).deleteHeaders(mId);
}
/*
* Completely remove a message from the local database
*/
@Override
public void destroy() throws MessagingException
{
try
{
database.execute(true, new DbCallback()
{
@Override
public Void doDbWork(final SQLiteDatabase db) throws WrappedException,
UnavailableStorageException
{
try
{
updateFolderCountsOnFlag(Flag.X_DESTROYED, true);
((LocalFolder) mFolder).deleteAttachments(mId);
db.execSQL("DELETE FROM messages WHERE id = ?", new Object[] { mId });
}
catch (MessagingException e)
{
throw new WrappedException(e);
}
return null;
}
});
}
catch (WrappedException e)
{
throw (MessagingException) e.getCause();
}
}
private void updateFolderCountsOnFlag(Flag flag, boolean set)
{
/*
* Update the unread count on the folder.
*/
try
{
LocalFolder folder = (LocalFolder)mFolder;
if (flag == Flag.DELETED || flag == Flag.X_DESTROYED)
{
if (!isSet(Flag.SEEN))
{
folder.setUnreadMessageCount(folder.getUnreadMessageCount() + ( set ? -1:1) );
}
if (isSet(Flag.FLAGGED))
{
folder.setFlaggedMessageCount(folder.getFlaggedMessageCount() + (set ? -1 : 1));
}
}
if ( !isSet(Flag.DELETED) )
{
if ( flag == Flag.SEEN )
{
if (set != isSet(Flag.SEEN))
{
folder.setUnreadMessageCount(folder.getUnreadMessageCount() + ( set ? -1: 1) );
}
}
if ( flag == Flag.FLAGGED )
{
folder.setFlaggedMessageCount(folder.getFlaggedMessageCount() + (set ? 1 : -1));
}
}
}
catch (MessagingException me)
{
Log.e(K9.LOG_TAG, "Unable to update LocalStore unread message count",
me);
throw new RuntimeException(me);
}
}
private void loadHeaders() throws UnavailableStorageException
{
ArrayList messages = 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) throws UnavailableStorageException
{
if (!mHeadersLoaded)
loadHeaders();
super.addHeader(name, value);
}
@Override
public void setHeader(String name, String value) throws UnavailableStorageException
{
if (!mHeadersLoaded)
loadHeaders();
super.setHeader(name, value);
}
@Override
public String[] getHeader(String name) throws UnavailableStorageException
{
if (!mHeadersLoaded)
loadHeaders();
return super.getHeader(name);
}
@Override
public void removeHeader(String name) throws UnavailableStorageException
{
if (!mHeadersLoaded)
loadHeaders();
super.removeHeader(name);
}
@Override
public Set getHeaderNames() throws UnavailableStorageException
{
if (!mHeadersLoaded)
loadHeaders();
return super.getHeaderNames();
}
}
public static 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;
}
}
}