Add support for HTML composition with text alternative.

Revamp K9 Identity string.
Quote names in Address only when needed.
Remove quoted text bar and move button to quoted text area.
This commit is contained in:
Andrew Chen 2011-01-12 23:48:28 +00:00
parent ed4aec26f1
commit e56b044bbc
15 changed files with 1239 additions and 322 deletions

View file

@ -194,40 +194,35 @@
<RelativeLayout
android:id="@+id/quoted_text_bar"
android:layout_width="fill_parent"
android:layout_height="45dip"
android:background="@drawable/email_quoted_bar" >
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
<EditText
android:id="@+id/quoted_text"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:text="@string/message_compose_quoted_text_label"
android:layout_weight="1.0"
android:gravity="left|top"
android:minLines="3"
android:autoText="true"
android:capitalize="sentences"
android:textColor="@android:color/primary_text_light"
android:textAppearance="?android:attr/textAppearanceMedium" />
<com.fsck.k9.view.MessageWebView
android:id="@+id/quoted_html"
android:layout_height="wrap_content"
android:layout_width="fill_parent" />
<ImageButton
android:id="@+id/quoted_text_delete"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_alignParentTop="true"
android:layout_alignParentRight="true"
android:background="@drawable/btn_dialog" />
</RelativeLayout>
<EditText
android:id="@+id/quoted_text"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1.0"
android:gravity="left|top"
android:minLines="3"
android:autoText="true"
android:capitalize="sentences"
android:textColor="@android:color/primary_text_light"
android:textAppearance="?android:attr/textAppearanceMedium" />
<EditText
android:id="@+id/lower_signature"
android:layout_width="fill_parent"

View file

@ -663,4 +663,14 @@
<item>HEADER</item>
</string-array>
<string-array name="account_settings_message_format_entries">
<item>@string/account_settings_message_format_text</item>
<item>@string/account_settings_message_format_html</item>
</string-array>
<string-array name="account_settings_message_format_values">
<item>TEXT</item>
<item>HTML</item>
</string-array>
</resources>

View file

@ -561,6 +561,10 @@ Welcome to K-9 Mail setup. K-9 is an open source mail client for Android origin
<string name="account_settings_reply_after_quote_label">Reply after quoted text</string>
<string name="account_settings_reply_after_quote_summary">When replying to messages, the original message will appear above your reply.</string>
<string name="account_settings_message_format_label">Message Format</string>
<string name="account_settings_message_format_text">Plain Text (images and formatting will be removed)</string>
<string name="account_settings_message_format_html">HTML (images and formatting are preserved)</string>
<string name="account_settings_quote_style_label">Reply quoting style</string>
<string name="account_settings_quote_style_prefix">Prefix (like Gmail, Pine)</string>
<string name="account_settings_quote_style_header">Header (like Outlook, Yahoo!, Hotmail)</string>

View file

@ -225,6 +225,13 @@
android:title="@string/account_settings_identities_label"
android:summary="@string/account_settings_identities_summary" />
<ListPreference
android:persistent="false"
android:key="message_format"
android:title="@string/account_settings_message_format_label"
android:entries="@array/account_settings_message_format_entries"
android:entryValues="@array/account_settings_message_format_values" />
<ListPreference
android:persistent="false"
android:key="quote_style"

View file

@ -50,6 +50,7 @@ public class Account implements BaseAccount
public static final String TYPE_OTHER = "OTHER";
private static final String[] networkTypes = { TYPE_WIFI, TYPE_MOBILE, TYPE_OTHER };
private static final MessageFormat DEFAULT_MESSAGE_FORMAT = MessageFormat.HTML;
private static final QuoteStyle DEFAULT_QUOTE_STYLE = QuoteStyle.PREFIX;
private static final String DEFAULT_QUOTE_PREFIX = ">";
private static final boolean DEFAULT_REPLY_AFTER_QUOTE = false;
@ -114,6 +115,7 @@ public class Account implements BaseAccount
// Tracks if we have sent a notification for this account for
// current set of fetched messages
private boolean mRingNotified;
private MessageFormat mMessageFormat;
private QuoteStyle mQuoteStyle;
private String mQuotePrefix;
private boolean mReplyAfterQuote;
@ -160,6 +162,11 @@ public class Account implements BaseAccount
PREFIX, HEADER
}
public enum MessageFormat
{
TEXT, HTML
}
protected Account(Context context)
{
mUuid = UUID.randomUUID().toString();
@ -191,6 +198,7 @@ public class Account implements BaseAccount
subscribedFoldersOnly = false;
maximumPolledMessageAge = -1;
maximumAutoDownloadMessageSize = 32768;
mMessageFormat = DEFAULT_MESSAGE_FORMAT;
mQuoteStyle = DEFAULT_QUOTE_STYLE;
mQuotePrefix = DEFAULT_QUOTE_PREFIX;
mReplyAfterQuote = DEFAULT_REPLY_AFTER_QUOTE;
@ -282,9 +290,10 @@ public class Account implements BaseAccount
subscribedFoldersOnly = prefs.getBoolean(mUuid + ".subscribedFoldersOnly",
false);
maximumPolledMessageAge = prefs.getInt(mUuid
+ ".maximumPolledMessageAge", -1);
+ ".maximumPolledMessageAge", -1);
maximumAutoDownloadMessageSize = prefs.getInt(mUuid
+ ".maximumAutoDownloadMessageSize", 32768);
mMessageFormat = MessageFormat.valueOf(prefs.getString(mUuid + ".messageFormat", DEFAULT_MESSAGE_FORMAT.name()));
mQuoteStyle = QuoteStyle.valueOf(prefs.getString(mUuid + ".quoteStyle", DEFAULT_QUOTE_STYLE.name()));
mQuotePrefix = prefs.getString(mUuid + ".quotePrefix", DEFAULT_QUOTE_PREFIX);
mReplyAfterQuote = prefs.getBoolean(mUuid + ".replyAfterQuote", DEFAULT_REPLY_AFTER_QUOTE);
@ -570,6 +579,7 @@ public class Account implements BaseAccount
editor.putBoolean(mUuid + ".subscribedFoldersOnly", subscribedFoldersOnly);
editor.putInt(mUuid + ".maximumPolledMessageAge", maximumPolledMessageAge);
editor.putInt(mUuid + ".maximumAutoDownloadMessageSize", maximumAutoDownloadMessageSize);
editor.putString(mUuid + ".messageFormat", mMessageFormat.name());
editor.putString(mUuid + ".quoteStyle", mQuoteStyle.name());
editor.putString(mUuid + ".quotePrefix", mQuotePrefix);
editor.putBoolean(mUuid + ".replyAfterQuote", mReplyAfterQuote);
@ -1490,6 +1500,16 @@ public class Account implements BaseAccount
}
}
public MessageFormat getMessageFormat()
{
return mMessageFormat;
}
public void setMessageFormat(MessageFormat messageFormat)
{
this.mMessageFormat = messageFormat;
}
public QuoteStyle getQuoteStyle()
{
return mQuoteStyle;

View file

@ -225,7 +225,7 @@ public class K9 extends Application
public static final String REMOTE_UID_PREFIX = "K9REMOTE:";
public static final String K9MAIL_IDENTITY = "X-K9mail-Identity";
public static final String IDENTITY_HEADER = "X-K9mail-Identity";
/**
* Specifies how many messages will be shown in a folder by default. This number is set

View file

@ -0,0 +1,124 @@
package com.fsck.k9.activity;
import java.io.Serializable;
/**
* <p>Represents an HTML document with an insertion point for placing a reply. The quoted
* document may have been modified to make it suitable for insertion. The modified quoted
* document should be used in place of the original document.</p>
*
* <p>Changes to the user-generated inserted content should be done with {@link
* #setUserContent(String)}.</p>
*
* TODO: This container should also have a text part, along with its insertion point. Or maybe a generic InsertableContent and maintain one each for Html and Text?
*/
class InsertableHtmlContent implements Serializable
{
private static final long serialVersionUID = 2397327034L;
// Default to a headerInsertionPoint at the beginning of the message.
private int headerInsertionPoint = 0;
private int footerInsertionPoint = 0;
// Quoted message, if any. headerInsertionPoint refers to a position in this string.
private StringBuilder quotedContent = new StringBuilder();
// User content (typically their reply or comments on a forward)
private StringBuilder userContent = new StringBuilder();
public int getHeaderInsertionPoint()
{
return headerInsertionPoint;
}
public void setHeaderInsertionPoint(int headerInsertionPoint)
{
this.headerInsertionPoint = headerInsertionPoint;
}
public void setFooterInsertionPoint(int footerInsertionPoint)
{
this.footerInsertionPoint = footerInsertionPoint;
}
public String getQuotedContent()
{
return quotedContent.toString();
}
/**
* Set the quoted content. The insertion point should be set against this content.
* @param content
*/
public void setQuotedContent(StringBuilder content)
{
this.quotedContent = content;
}
/**
* Insert something into the quoted content header. This is typically used for inserting
* reply/forward headers into the quoted content rather than inserting the user-generated reply
* content.
* @param content
*/
public void insertIntoQuotedHeader(final String content)
{
quotedContent.insert(headerInsertionPoint, content);
// Update the location of the footer insertion point.
footerInsertionPoint += content.length();
}
/**
* Insert something into the quoted content footer. This is typically used for inserting closing
* tags of reply/forward headers rather than inserting the user-generated reply content.
* @param content
*/
public void insertIntoQuotedFooter(final String content)
{
quotedContent.insert(footerInsertionPoint, content);
}
/**
* Remove all quoted content.
*/
public void clearQuotedContent() {
quotedContent.setLength(0);
footerInsertionPoint = 0;
headerInsertionPoint = 0;
}
/**
* Set the inserted content to the specified content. Replaces anything currently in the
* inserted content buffer.
* @param content
*/
public void setUserContent(final String content) {
userContent = new StringBuilder(content);
}
/**
* Build the composed string with the inserted and original content.
* @return Composed string.
*/
@Override
public String toString()
{
// Inserting and deleting was twice as fast as instantiating a new StringBuilder and
// using substring() to build the new pieces.
String result = quotedContent.insert(headerInsertionPoint, userContent.toString()).toString();
quotedContent.delete(headerInsertionPoint, headerInsertionPoint + userContent.length());
return result;
}
/**
* Return debugging information for this container.
* @return Debug string.
*/
public String toDebugString()
{
return "InsertableHtmlContent{" +
"headerInsertionPoint=" + headerInsertionPoint +
", footerInsertionPoint=" + footerInsertionPoint +
", quotedContent=" + quotedContent +
", userContent=" + userContent +
", compiledResult=" + toString() +
'}';
}
}

File diff suppressed because it is too large Load diff

View file

@ -86,6 +86,7 @@ public class AccountSettings extends K9PreferenceActivity
private static final String PREFERENCE_MESSAGE_AGE = "account_message_age";
private static final String PREFERENCE_MESSAGE_SIZE = "account_autodownload_size";
private static final String PREFERENCE_SAVE_ALL_HEADERS = "account_save_all_headers";
private static final String PREFERENCE_MESSAGE_FORMAT = "message_format";
private static final String PREFERENCE_QUOTE_PREFIX = "account_quote_prefix";
private static final String PREFERENCE_QUOTE_STYLE = "quote_style";
private static final String PREFERENCE_REPLY_AFTER_QUOTE = "reply_after_quote";
@ -142,6 +143,7 @@ public class AccountSettings extends K9PreferenceActivity
private boolean mIncomingChanged = false;
private CheckBoxPreference mNotificationOpensUnread;
private CheckBoxPreference mNotificationUnreadCount;
private ListPreference mMessageFormat;
private ListPreference mQuoteStyle;
private EditTextPreference mAccountQuotePrefix;
private CheckBoxPreference mReplyAfterQuote;
@ -206,6 +208,21 @@ public class AccountSettings extends K9PreferenceActivity
}
});
mMessageFormat = (ListPreference) findPreference(PREFERENCE_MESSAGE_FORMAT);
mMessageFormat.setValue(mAccount.getMessageFormat().name());
mMessageFormat.setSummary(mMessageFormat.getEntry());
mMessageFormat.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener()
{
public boolean onPreferenceChange(Preference preference, Object newValue)
{
final String summary = newValue.toString();
int index = mMessageFormat.findIndexOfValue(summary);
mMessageFormat.setSummary(mMessageFormat.getEntries()[index]);
mMessageFormat.setValue(summary);
return false;
}
});
mAccountQuotePrefix = (EditTextPreference) findPreference(PREFERENCE_QUOTE_PREFIX);
mAccountQuotePrefix.setSummary(mAccount.getQuotePrefix());
mAccountQuotePrefix.setText(mAccount.getQuotePrefix());
@ -756,6 +773,7 @@ public class AccountSettings extends K9PreferenceActivity
mAccount.setSyncRemoteDeletions(mSyncRemoteDeletions.isChecked());
mAccount.setSaveAllHeaders(mSaveAllHeaders.isChecked());
mAccount.setSearchableFolders(Account.Searchable.valueOf(mSearchableFolders.getValue()));
mAccount.setMessageFormat(Account.MessageFormat.valueOf(mMessageFormat.getValue()));
mAccount.setQuoteStyle(QuoteStyle.valueOf(mQuoteStyle.getValue()));
mAccount.setQuotePrefix(mAccountQuotePrefix.getText());
mAccount.setReplyAfterQuote(mReplyAfterQuote.isChecked());

View file

@ -1,9 +1,6 @@
package com.fsck.k9.helper;
import android.text.Annotation;
import android.text.Editable;
import android.text.Html;
import android.text.Spannable;
import android.text.*;
import android.util.Log;
import com.fsck.k9.K9;
import org.xml.sax.XMLReader;
@ -142,7 +139,8 @@ public class HtmlConverter
/**
* Convert a text string into an HTML document. Attempts to do smart replacement for large
* documents to prevent OOM errors.
* documents to prevent OOM errors. This method adds headers and footers to create a proper HTML
* document. To convert to a fragment, use {@link #textToHtmlFragment(String)}.
* @param text Plain text string.
* @return HTML string.
*/
@ -251,4 +249,18 @@ public class HtmlConverter
}
}
/**
* Convert a plain text string into an HTML fragment.
* @param text Plain text.
* @return HTML fragment.
*/
public static String textToHtmlFragment(final String text)
{
// Escape the entities and add newlines.
// TODO - Perhaps use LocalStore.htmlifyString?
String result = TextUtils.htmlEncode(text).replace("\n", "<br>\n");
// For some reason, TextUtils.htmlEncode escapes ' into &apos;, which is technically part of the XHTML 1.0
// standard, but Gmail doesn't recognize it as an HTML entity. We unescape that here.
return result.replace("&apos;", "&#39;");
}
}

View file

@ -141,6 +141,27 @@ public class Utility
return false;
}
private static final Pattern ATOM = Pattern.compile("^(?:[a-zA-Z0-9!#$%&'*+\\-/=?^_`{|}~]|\\s)+$");
/**
* Quote a string, if necessary, based upon the definition of an "atom," as defined by RFC2822
* (http://tools.ietf.org/html/rfc2822#section-3.2.4). Strings that consist purely of atoms are
* left unquoted; anything else is returned as a quoted string.
* @param text String to quote.
* @return Possibly quoted string.
*/
public static String quoteAtoms(final String text)
{
if (ATOM.matcher(text).matches())
{
return text;
}
else
{
return quoteString(text);
}
}
/**
* Ensures that the given string starts and ends with the double quote character. The string is not modified in any way except to add the
* double quote character to start and end if it's not already there.

View file

@ -187,7 +187,7 @@ public class Address
{
if (mPersonal != null)
{
return Utility.quoteString(mPersonal) + " <" + mAddress + ">";
return Utility.quoteAtoms(mPersonal) + " <" + mAddress + ">";
}
else
{

View file

@ -19,6 +19,11 @@ public class TextBody implements Body
private String mBody;
private String mEncoding;
private String mCharset = "UTF-8";
// Length of the message composed (as opposed to quoted). I don't like the name of this variable and am open to
// suggestions as to what it should otherwise be. -achen 20101207
private Integer mComposedMessageLength;
// Offset from position 0 where the composed message begins.
private Integer mComposedMessageOffset;
public TextBody(String body)
{
@ -85,4 +90,24 @@ public class TextBody implements Body
{
mCharset = charset;
}
public Integer getComposedMessageLength()
{
return mComposedMessageLength;
}
public void setComposedMessageLength(Integer composedMessageLength)
{
this.mComposedMessageLength = composedMessageLength;
}
public Integer getComposedMessageOffset()
{
return mComposedMessageOffset;
}
public void setComposedMessageOffset(Integer composedMessageOffset)
{
this.mComposedMessageOffset = composedMessageOffset;
}
}

View file

@ -1343,7 +1343,7 @@ public class ImapStore extends Store
fetchFields.add("INTERNALDATE");
fetchFields.add("RFC822.SIZE");
fetchFields.add("BODY.PEEK[HEADER.FIELDS (date subject from content-type to cc reply-to "
+ K9.K9MAIL_IDENTITY + ")]");
+ K9.IDENTITY_HEADER + ")]");
}
if (fp.contains(FetchProfile.Item.STRUCTURE))
{

View file

@ -76,7 +76,7 @@ public class LocalStore extends Store implements Serializable
private static Set<String> HEADERS_TO_SAVE = new HashSet<String>();
static
{
HEADERS_TO_SAVE.add(K9.K9MAIL_IDENTITY);
HEADERS_TO_SAVE.add(K9.IDENTITY_HEADER);
HEADERS_TO_SAVE.add("To");
HEADERS_TO_SAVE.add("Cc");
HEADERS_TO_SAVE.add("From");
@ -94,7 +94,7 @@ public class LocalStore extends Store implements Serializable
"subject, sender_list, date, uid, flags, id, to_list, cc_list, "
+ "bcc_list, reply_to_list, attachment_count, internal_date, message_id, folder_id, preview ";
protected static final int DB_VERSION = 39;
protected static final int DB_VERSION = 40;
protected String uUid = null;
@ -158,7 +158,8 @@ public class LocalStore extends Store implements Serializable
db.execSQL("DROP TABLE IF EXISTS messages");
db.execSQL("CREATE TABLE messages (id INTEGER PRIMARY KEY, deleted INTEGER default 0, folder_id INTEGER, uid TEXT, subject TEXT, "
+ "date INTEGER, flags TEXT, sender_list TEXT, to_list TEXT, cc_list TEXT, bcc_list TEXT, reply_to_list TEXT, "
+ "html_content TEXT, text_content TEXT, attachment_count INTEGER, internal_date INTEGER, message_id TEXT, preview TEXT)");
+ "html_content TEXT, text_content TEXT, attachment_count INTEGER, internal_date INTEGER, message_id TEXT, preview TEXT, "
+ "mime_type TEXT)");
db.execSQL("DROP TABLE IF EXISTS headers");
db.execSQL("CREATE TABLE headers (id INTEGER PRIMARY KEY, message_id INTEGER, name TEXT, value TEXT)");
@ -275,7 +276,6 @@ public class LocalStore extends Store implements Serializable
}
}
// Database version 38 is solely to prune cached attachments now that we clear them better
if (db.getVersion() < 39)
{
@ -289,6 +289,18 @@ public class LocalStore extends Store implements Serializable
}
}
// V40: Store the MIME type for a message.
if (db.getVersion() < 40)
{
try
{
db.execSQL("ALTER TABLE messages ADD mime_type TEXT");
}
catch (SQLiteException e)
{
Log.e(K9.LOG_TAG, "Unable to add mime_type column to messages");
}
}
}
@ -1804,25 +1816,90 @@ public class LocalStore extends Store implements Serializable
mp.setSubType("mixed");
try
{
cursor = db.rawQuery("SELECT html_content, text_content FROM messages "
cursor = db.rawQuery("SELECT html_content, text_content, mime_type FROM messages "
+ "WHERE id = ?",
new String[] { Long.toString(localMessage.mId) });
cursor.moveToNext();
String htmlContent = cursor.getString(0);
String textContent = cursor.getString(1);
if (textContent != null)
String mimeType = cursor.getString(2);
if (mimeType != null && mimeType.toLowerCase().startsWith("multipart/"))
{
LocalTextBody body = new LocalTextBody(textContent, htmlContent);
MimeBodyPart bp = new MimeBodyPart(body, "text/plain");
mp.addBodyPart(bp);
// If this is a multipart message, preserve both text
// and html parts, as well as the subtype.
mp.setSubType(mimeType.toLowerCase().replaceFirst("^multipart/", ""));
if (textContent != null)
{
LocalTextBody body = new LocalTextBody(textContent, htmlContent);
MimeBodyPart bp = new MimeBodyPart(body, "text/plain");
mp.addBodyPart(bp);
}
if (htmlContent != null)
{
TextBody body = new TextBody(htmlContent);
MimeBodyPart bp = new MimeBodyPart(body, "text/html");
mp.addBodyPart(bp);
}
// If we have both text and html content and our MIME type
// isn't multipart/alternative, then corral them into a new
// multipart/alternative part and put that into the parent.
// If it turns out that this is the only part in the parent
// MimeMultipart, it'll get fixed below before we attach to
// the message.
if (textContent != null && htmlContent != null && !mimeType.equalsIgnoreCase("multipart/alternative"))
{
MimeMultipart alternativeParts = mp;
alternativeParts.setSubType("alternative");
mp = new MimeMultipart();
mp.addBodyPart(new MimeBodyPart(alternativeParts));
}
}
else if (mimeType != null && mimeType.equalsIgnoreCase("text/plain"))
{
// If it's text, add only the plain part. The MIME
// container will drop away below.
if (textContent != null)
{
LocalTextBody body = new LocalTextBody(textContent, htmlContent);
MimeBodyPart bp = new MimeBodyPart(body, "text/plain");
mp.addBodyPart(bp);
}
}
else if (mimeType != null && mimeType.equalsIgnoreCase("text/html"))
{
// If it's html, add only the html part. The MIME
// container will drop away below.
if (htmlContent != null)
{
TextBody body = new TextBody(htmlContent);
MimeBodyPart bp = new MimeBodyPart(body, "text/html");
mp.addBodyPart(bp);
}
}
else
{
TextBody body = new TextBody(htmlContent);
MimeBodyPart bp = new MimeBodyPart(body, "text/html");
mp.addBodyPart(bp);
// MIME type not set. Grab whatever part we can get,
// with Text taking precedence. This preserves pre-HTML
// composition behaviour.
if (textContent != null)
{
LocalTextBody body = new LocalTextBody(textContent, htmlContent);
MimeBodyPart bp = new MimeBodyPart(body, "text/plain");
mp.addBodyPart(bp);
}
else if (htmlContent != null)
{
TextBody body = new TextBody(htmlContent);
MimeBodyPart bp = new MimeBodyPart(body, "text/html");
mp.addBodyPart(bp);
}
}
}
catch (Exception e)
{
Log.e(K9.LOG_TAG, "Exception fetching message:", e);
}
finally
{
@ -1889,7 +1966,7 @@ public class LocalStore extends Store implements Serializable
bp.setHeader(MimeHeader.HEADER_CONTENT_ID, contentId);
/*
* HEADER_ANDROID_ATTACHMENT_STORE_DATA is a custom header we add to that
* we can later pull the attachment from the remote store if neccesary.
* we can later pull the attachment from the remote store if necessary.
*/
bp.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, storeData);
@ -1904,15 +1981,25 @@ public class LocalStore extends Store implements Serializable
}
}
if (mp.getCount() == 1)
if (mp.getCount() == 0)
{
// If we have no body, remove the container and create a
// dummy plain text body. This check helps prevents us from
// triggering T_MIME_NO_TEXT and T_TVD_MIME_NO_HEADERS
// SpamAssassin rules.
localMessage.setHeader(MimeHeader.HEADER_CONTENT_TYPE, "text/plain");
localMessage.setBody(new TextBody(""));
}
else if (mp.getCount() == 1)
{
// If we have only one part, drop the MimeMultipart container.
BodyPart part = mp.getBodyPart(0);
localMessage.setHeader(MimeHeader.HEADER_CONTENT_TYPE, part.getContentType());
localMessage.setBody(part.getBody());
}
else
{
localMessage.setHeader(MimeHeader.HEADER_CONTENT_TYPE, "multipart/mixed");
// Otherwise, attach the MimeMultipart to the message.
localMessage.setBody(mp);
}
}
@ -2381,6 +2468,7 @@ public class LocalStore extends Store implements Serializable
cv.put("attachment_count", attachments.size());
cv.put("internal_date", message.getInternalDate() == null
? System.currentTimeMillis() : message.getInternalDate().getTime());
cv.put("mime_type", message.getMimeType());
String messageId = message.getMessageId();
if (messageId != null)