() {
@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 {
Utility.closeQuietly(messagesCursor);
}
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);
}
// try to remove lines of dashes in the preview
text = text.replaceAll("(?m)^----.*?$", "");
// remove quoted text from the preview
text = text.replaceAll("(?m)^[#>].*$", "");
// Remove a common quote header from the preview
text = text.replaceAll("(?m)^On .*wrote.?$", "");
// Remove a more generic quote header from the preview
text = text.replaceAll("(?m)^.*\\w+:$", "");
// URLs in the preview should just be shown as "..." - They're not
// clickable and they usually overwhelm the preview
text = text.replaceAll("https?://\\S+", "...");
// Don't show newlines in the preview
text = text.replaceAll("(\\r|\\n)+", " ");
// Collapse whitespace in the preview
text = text.replaceAll("\\s+", " ");
if (text.length() <= 512) {
return text;
} else {
return text.substring(0, 512);
}
}
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 {
Utility.closeQuietly(cursor);
}
return null;
}
});
if (K9.DEBUG)
Log.d(K9.LOG_TAG, "Updated last UID for folder " + mName + " to " + lastUid);
mLastUid = lastUid;
}
public Long getOldestMessageDate() throws MessagingException {
return database.execute(false, new DbCallback() {
@Override
public Long doDbWork(final SQLiteDatabase db) {
Cursor cursor = null;
try {
open(OpenMode.READ_ONLY);
cursor = db.rawQuery("SELECT MIN(date) FROM messages WHERE folder_id=?", new String[] { Long.toString(mFolderId) });
if (cursor.getCount() > 0) {
cursor.moveToFirst();
return cursor.getLong(0);
}
} catch (Exception e) {
Log.e(K9.LOG_TAG, "Unable to fetch oldest message date: ", e);
} finally {
Utility.closeQuietly(cursor);
}
return null;
}
});
}
}
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 or null.
* @throws MessagingException
*/
public String getTextForDisplay() throws MessagingException {
String text = null; // 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 && part.getBody() instanceof LocalStore.LocalTextBody) {
text = ((LocalStore.LocalTextBody) part.getBody()).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(Locale.US), 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();
}
@Override
public LocalMessage clone() {
LocalMessage message = new LocalMessage();
super.copy(message);
message.mId = mId;
message.mAttachmentCount = mAttachmentCount;
message.mSubject = mSubject;
message.mPreview = mPreview;
message.mToMeCalculated = mToMeCalculated;
message.mCcMeCalculated = mCcMeCalculated;
message.mToMe = mToMe;
message.mCcMe = mCcMe;
message.mHeadersLoaded = mHeadersLoaded;
message.mMessageDirty = mMessageDirty;
return message;
}
}
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);
try {
IOUtils.copy(in, base64Out);
} finally {
base64Out.close();
}
}
public Uri getContentUri() {
return mUri;
}
}
}