diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml
index ce0ce9cc6..f38314c38 100644
--- a/res/values-cs/strings.xml
+++ b/res/values-cs/strings.xml
@@ -84,6 +84,10 @@
"Zpráva byla smazána."
"Zpráva byla zrušena."
"Zpráva byla uložena jako koncept."
+
+
+
+
"Nastavit e-mail"
"Zadejte e-mailovou adresu účtu:"
"E-mailová adresa"
diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml
index 9fe47dbdc..28bb5d218 100644
--- a/res/values-de/strings.xml
+++ b/res/values-de/strings.xml
@@ -84,6 +84,10 @@
"Nachricht gelöscht"
"Nachricht gelöscht"
"Nachricht als Entwurf gespeichert"
+
+
+
+
"E-Mail einrichten"
"Geben Sie Ihre im Konto gespeicherte E-Mail-Adresse ein:"
"E-Mail-Adresse"
diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml
index 2c2c2faab..71a7fd0d7 100644
--- a/res/values-es/strings.xml
+++ b/res/values-es/strings.xml
@@ -84,6 +84,10 @@
"Mensaje suprimido"
"Mensaje descartado"
"Mensaje guardado como borrador"
+
+
+
+
"Configurar correo electrónico"
"Introduce la dirección de correo electrónico de tu cuenta:"
"Dirección de correo electrónico"
diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml
index 14d79dc8d..f72f185c0 100644
--- a/res/values-fr/strings.xml
+++ b/res/values-fr/strings.xml
@@ -84,6 +84,10 @@
"Message supprimé."
"Message supprimé."
"Message enregistré comme brouillon."
+
+
+
+
"Configurer la messagerie électronique"
"Saisissez l\'adresse e-mail de votre compte :"
"Adresse e-mail"
diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml
index f83274890..112fad49d 100644
--- a/res/values-it/strings.xml
+++ b/res/values-it/strings.xml
@@ -84,6 +84,10 @@
"Messaggio eliminato."
"Messaggio eliminato."
"Messaggio salvato come bozza."
+
+
+
+
"Imposta email"
"Digita l\'indirizzo email del tuo account:"
"Indirizzo email"
diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml
index c0ab1aa72..1c7c95840 100644
--- a/res/values-ja/strings.xml
+++ b/res/values-ja/strings.xml
@@ -84,6 +84,10 @@
"メッセージを削除しました。"
"メッセージを破棄しました。"
"メッセージを下書きとして保存しました。"
+
+
+
+
"メールアカウントの登録"
"メールのアカウント情報を入力:"
"メールアドレス"
diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml
index 9bad079a9..85511805b 100644
--- a/res/values-ko/strings.xml
+++ b/res/values-ko/strings.xml
@@ -84,6 +84,10 @@
"메일이 삭제되었습니다."
"메일이 삭제되었습니다."
"메일을 임시로 저장했습니다."
+
+
+
+
"이메일 설정"
"계정 이메일 주소 입력:"
"이메일 주소"
diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml
index ba3e7bd86..7e6b025c2 100644
--- a/res/values-nb/strings.xml
+++ b/res/values-nb/strings.xml
@@ -84,6 +84,10 @@
"Meldingen ble slettet."
"Meldingen ble forkastet."
"Meldingen ble lagret som utkast."
+
+
+
+
"Sett opp e-post"
"Skriv e-postadressen til kontoen din:"
"E-postadresse"
diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml
index e76d039fe..e71de77a3 100644
--- a/res/values-nl/strings.xml
+++ b/res/values-nl/strings.xml
@@ -84,6 +84,10 @@
"Bericht verwijderd."
"Bericht wordt verwijderd"
"Bericht opgeslagen als concept."
+
+
+
+
"E-mail instellen"
"Typ het e-mailadres van je account:"
"E-mailadres"
diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml
index dfac04fc0..ab4f036cc 100644
--- a/res/values-pl/strings.xml
+++ b/res/values-pl/strings.xml
@@ -84,6 +84,10 @@
"Wiadomość została usunięta."
"Wiadomość została odrzucona."
"Wiadomość została zapisana jako wersja robocza."
+
+
+
+
"Skonfiguruj konto e-mail"
"Podaj adres e-mail swojego konta:"
"Adres e-mail"
diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml
index f92669cbd..805e85d07 100644
--- a/res/values-ru/strings.xml
+++ b/res/values-ru/strings.xml
@@ -84,6 +84,10 @@
"Письмо удалено."
"Письмо не сохранено."
"Письмо сохранено как черновик."
+
+
+
+
"Настройка электронной почты"
"Укажите почтовый адрес своего аккаунта:"
"Адрес электронной почты"
diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml
index 8b413a0c0..2e0614814 100644
--- a/res/values-zh-rCN/strings.xml
+++ b/res/values-zh-rCN/strings.xml
@@ -84,6 +84,10 @@
"邮件已删除。"
"邮件已取消。"
"邮件已另存为草稿。"
+
+
+
+
"设置电子邮件"
"键入您的帐户电子邮件地址:"
"电子邮件地址"
diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml
index 30abecd74..d2345e1d9 100644
--- a/res/values-zh-rTW/strings.xml
+++ b/res/values-zh-rTW/strings.xml
@@ -84,6 +84,10 @@
"已刪除郵件。"
"已捨棄郵件。"
"已儲存郵件草稿。"
+
+
+
+
"設定電子郵件"
"請輸入您帳戶的電子郵件地址:"
"電子郵件地址"
diff --git a/src/com/android/email/activity/MessageView.java b/src/com/android/email/activity/MessageView.java
index 0aab39cb5..22f248784 100644
--- a/src/com/android/email/activity/MessageView.java
+++ b/src/com/android/email/activity/MessageView.java
@@ -75,9 +75,8 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Date;
-import java.util.Map;
-import java.util.Random;
import java.util.regex.Matcher;
+import java.util.regex.Pattern;
public class MessageView extends Activity
implements OnClickListener {
@@ -93,6 +92,9 @@ public class MessageView extends Activity
};
private static final int METHODS_STATUS_COLUMN = 1;
+ // regex that matches start of img tag. '.*<(?i)img\s+.*'.
+ private static final Pattern IMG_TAG_START_REGEX = Pattern.compile(".*<(?i)img\\s+.*");
+
private TextView mSubjectView;
private TextView mFromView;
private TextView mDateView;
@@ -451,6 +453,10 @@ public class MessageView extends Activity
Intent contactIntent = new Intent(Contacts.Intents.SHOW_OR_CREATE_CONTACT);
contactIntent.setData(contactUri);
+ // Pass along full E-mail string for possible create dialog
+ contactIntent.putExtra(Contacts.Intents.EXTRA_CREATE_DESCRIPTION,
+ senderEmail.toString());
+
// Only provide personal name hint if we have one
String senderPersonal = senderEmail.getPersonal();
if (senderPersonal != null) {
@@ -694,6 +700,50 @@ public class MessageView extends Activity
}
}
+ /**
+ * Resolve content-id reference in src attribute of img tag to AttachmentProvider's
+ * content uri. This method calls itself recursively at most the number of
+ * LocalAttachmentPart that mime type is image and has content id.
+ * The attribute src="cid:content_id" is resolved as src="content://...".
+ * This method is package scope for testing purpose.
+ *
+ * @param text html email text
+ * @param part mime part which may contain inline image
+ * @return html text in which src attribute of img tag may be replaced with content uri
+ */
+ /* package */ String resolveInlineImage(String text, Part part, int depth)
+ throws MessagingException {
+ // avoid too deep recursive call.
+ if (depth >= 10) {
+ return text;
+ }
+ String contentType = MimeUtility.unfoldAndDecode(part.getContentType());
+ String contentId = part.getContentId();
+ if (contentType.startsWith("image/") &&
+ contentId != null &&
+ part instanceof LocalAttachmentBodyPart) {
+ LocalAttachmentBodyPart attachment = (LocalAttachmentBodyPart)part;
+ Uri contentUri = AttachmentProvider.getAttachmentUri(
+ mAccount,
+ attachment.getAttachmentId());
+ if (contentUri != null) {
+ // Regexp which matches ' src="cid:contentId"'.
+ String contentIdRe = "\\s+(?i)src=\"cid(?-i):\\Q" + contentId + "\\E\"";
+ // Replace all occurrences of src attribute with ' src="content://contentUri"'.
+ text = text.replaceAll(contentIdRe, " src=\"" + contentUri + "\"");
+ }
+ }
+
+ if (part.getBody() instanceof Multipart) {
+ Multipart mp = (Multipart)part.getBody();
+ for (int i = 0; i < mp.getCount(); i++) {
+ text = resolveInlineImage(text, mp.getBodyPart(i), depth + 1);
+ }
+ }
+
+ return text;
+ }
+
private void renderAttachments(Part part, int depth) throws MessagingException {
String contentType = MimeUtility.unfoldAndDecode(part.getContentType());
String name = MimeUtility.getHeaderParameter(contentType, "name");
@@ -875,7 +925,7 @@ public class MessageView extends Activity
if (part != null) {
String text = MimeUtility.getTextFromPart(part);
if (part.getMimeType().equalsIgnoreCase("text/html")) {
- text = text.replaceAll("cid:", "http://cid/");
+ text = resolveInlineImage(text, mMessage, 0);
} else {
/*
* Linkify the plain text and convert it to HTML by replacing
@@ -898,10 +948,11 @@ public class MessageView extends Activity
}
/*
- * TODO this should be smarter, change to regex for img, but consider how to
- * get backgroung images and a million other things that HTML allows.
+ * TODO consider how to get background images and a million other things
+ * that HTML allows.
*/
- if (text.contains("img")) {
+ // Check if text contains img tag.
+ if (IMG_TAG_START_REGEX.matcher(text).matches()) {
mHandler.showShowPictures(true);
}
diff --git a/src/com/android/email/mail/Address.java b/src/com/android/email/mail/Address.java
index e0851db2d..3ed3a8f80 100644
--- a/src/com/android/email/mail/Address.java
+++ b/src/com/android/email/mail/Address.java
@@ -73,10 +73,10 @@ public class Address {
* @return An array of 0 or more Addresses.
*/
public static Address[] parse(String addressList) {
- ArrayList
addresses = new ArrayList();
- if (addressList == null) {
+ if (addressList == null || addressList.length() == 0) {
return new Address[] {};
}
+ ArrayList addresses = new ArrayList();
try {
MailboxList parsedList = AddressList.parse(addressList).flatten();
for (int i = 0, count = parsedList.size(); i < count; i++) {
@@ -173,7 +173,7 @@ public class Address {
* @return
*/
public static Address[] unpack(String addressList) {
- if (addressList == null) {
+ if (addressList == null || addressList.length() == 0) {
return new Address[] { };
}
ArrayList addresses = new ArrayList();
@@ -205,7 +205,7 @@ public class Address {
/**
* Packs an address list into a String that is very quick to read
* and parse. Packed lists can be unpacked with unpackAddressList()
- * The packed list is a comma seperated list of:
+ * The packed list is a comma separated list of:
* URLENCODE(address)[;URLENCODE(personal)]
* @param list
* @return
@@ -213,6 +213,8 @@ public class Address {
public static String pack(Address[] addresses) {
if (addresses == null) {
return null;
+ } else if (addresses.length == 0) {
+ return "";
}
StringBuffer sb = new StringBuffer();
for (int i = 0, count = addresses.length; i < count; i++) {
diff --git a/src/com/android/email/mail/Part.java b/src/com/android/email/mail/Part.java
index 1e1fcf498..175812591 100644
--- a/src/com/android/email/mail/Part.java
+++ b/src/com/android/email/mail/Part.java
@@ -31,6 +31,8 @@ public interface Part {
public String getContentType() throws MessagingException;
public String getDisposition() throws MessagingException;
+
+ public String getContentId() throws MessagingException;
public String[] getHeader(String name) throws MessagingException;
diff --git a/src/com/android/email/mail/internet/MimeBodyPart.java b/src/com/android/email/mail/internet/MimeBodyPart.java
index f105a3110..49b729f5d 100644
--- a/src/com/android/email/mail/internet/MimeBodyPart.java
+++ b/src/com/android/email/mail/internet/MimeBodyPart.java
@@ -20,6 +20,7 @@ import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
+import java.util.regex.Pattern;
import com.android.email.mail.Body;
import com.android.email.mail.BodyPart;
@@ -34,6 +35,9 @@ public class MimeBodyPart extends BodyPart {
protected Body mBody;
protected int mSize;
+ // regex that matches content id surrounded by "<>" optionally.
+ private static final Pattern REMOVE_OPTIONAL_BRACKETS = Pattern.compile("^([^>]+)>?$");
+
public MimeBodyPart() throws MessagingException {
this(null);
}
@@ -109,6 +113,16 @@ public class MimeBodyPart extends BodyPart {
}
}
+ public String getContentId() throws MessagingException {
+ String contentId = getFirstHeader(MimeHeader.HEADER_CONTENT_ID);
+ if (contentId == null) {
+ return null;
+ } else {
+ // remove optionally surrounding brackets.
+ return REMOVE_OPTIONAL_BRACKETS.matcher(contentId).replaceAll("$1");
+ }
+ }
+
public String getMimeType() throws MessagingException {
return MimeUtility.getHeaderParameter(getContentType(), null);
}
diff --git a/src/com/android/email/mail/internet/MimeHeader.java b/src/com/android/email/mail/internet/MimeHeader.java
index 5132934a2..8a53ece34 100644
--- a/src/com/android/email/mail/internet/MimeHeader.java
+++ b/src/com/android/email/mail/internet/MimeHeader.java
@@ -38,6 +38,7 @@ public class MimeHeader {
public static final String HEADER_CONTENT_TYPE = "Content-Type";
public static final String HEADER_CONTENT_TRANSFER_ENCODING = "Content-Transfer-Encoding";
public static final String HEADER_CONTENT_DISPOSITION = "Content-Disposition";
+ public static final String HEADER_CONTENT_ID = "Content-ID";
/**
* Fields that should be omitted when writing the header using writeTo()
diff --git a/src/com/android/email/mail/internet/MimeMessage.java b/src/com/android/email/mail/internet/MimeMessage.java
index 01ad59a88..27d7aadb4 100644
--- a/src/com/android/email/mail/internet/MimeMessage.java
+++ b/src/com/android/email/mail/internet/MimeMessage.java
@@ -25,6 +25,7 @@ import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Stack;
+import java.util.regex.Pattern;
import org.apache.james.mime4j.BodyDescriptor;
import org.apache.james.mime4j.ContentHandler;
@@ -38,6 +39,7 @@ import com.android.email.mail.Body;
import com.android.email.mail.BodyPart;
import com.android.email.mail.Message;
import com.android.email.mail.MessagingException;
+import com.android.email.mail.Multipart;
import com.android.email.mail.Part;
/**
@@ -46,12 +48,17 @@ import com.android.email.mail.Part;
*/
public class MimeMessage extends Message {
protected MimeHeader mHeader = new MimeHeader();
+
+ // NOTE: The fields here are transcribed out of headers, and values stored here will supercede
+ // the values found in the headers. Use caution to prevent any out-of-phase errors. In
+ // particular, any adds/changes/deletes here must be echoed by changes in the parse() function.
protected Address[] mFrom;
protected Address[] mTo;
protected Address[] mCc;
protected Address[] mBcc;
protected Address[] mReplyTo;
protected Date mSentDate;
+
// In MIME, en_US-like date format should be used. In other words "MMM" should be encoded to
// "Jan", not the other localized format like "Ene" (meaning January in locale es).
// This conversion is used when generating outgoing MIME messages. Incoming MIME date
@@ -62,6 +69,9 @@ public class MimeMessage extends Message {
protected Body mBody;
protected int mSize;
+ // regex that matches content id surrounded by "<>" optionally.
+ private static final Pattern REMOVE_OPTIONAL_BRACKETS = Pattern.compile("^([^>]+)>?$");
+
public MimeMessage() {
/*
* Every new messages gets a Message-ID
@@ -100,12 +110,16 @@ public class MimeMessage extends Message {
}
protected void parse(InputStream in) throws IOException, MessagingException {
+ // Before parsing the input stream, clear all local fields that may be superceded by
+ // the new incoming message.
mHeader.clear();
- mBody = null;
- mBcc = null;
- mTo = null;
mFrom = null;
+ mTo = null;
+ mCc = null;
+ mBcc = null;
+ mReplyTo = null;
mSentDate = null;
+ mBody = null;
MimeStreamParser parser = new MimeStreamParser();
parser.setContentHandler(new MimeMessageBuilder());
@@ -152,6 +166,16 @@ public class MimeMessage extends Message {
}
}
+ public String getContentId() throws MessagingException {
+ String contentId = getFirstHeader(MimeHeader.HEADER_CONTENT_ID);
+ if (contentId == null) {
+ return null;
+ } else {
+ // remove optionally surrounding brackets.
+ return REMOVE_OPTIONAL_BRACKETS.matcher(contentId).replaceAll("$1");
+ }
+ }
+
public String getMimeType() throws MessagingException {
return MimeUtility.getHeaderParameter(getContentType(), null);
}
diff --git a/src/com/android/email/mail/internet/MimeUtility.java b/src/com/android/email/mail/internet/MimeUtility.java
index 100a0abc5..7aa740221 100644
--- a/src/com/android/email/mail/internet/MimeUtility.java
+++ b/src/com/android/email/mail/internet/MimeUtility.java
@@ -126,13 +126,9 @@ public class MimeUtility {
}
}
}
- String[] header = part.getHeader("Content-ID");
- if (header != null) {
- for (String s : header) {
- if (s.equals(contentId)) {
- return part;
- }
- }
+ String cid = part.getContentId();
+ if (contentId.equals(cid)) {
+ return part;
}
return null;
}
diff --git a/src/com/android/email/mail/store/ImapResponseParser.java b/src/com/android/email/mail/store/ImapResponseParser.java
index 4c7028b89..410be6601 100644
--- a/src/com/android/email/mail/store/ImapResponseParser.java
+++ b/src/com/android/email/mail/store/ImapResponseParser.java
@@ -16,6 +16,15 @@
package com.android.email.mail.store;
+import com.android.email.Email;
+import com.android.email.FixedLengthInputStream;
+import com.android.email.PeekableInputStream;
+import com.android.email.mail.MessagingException;
+import com.android.email.mail.transport.LoggingInputStream;
+
+import android.util.Config;
+import android.util.Log;
+
import java.io.IOException;
import java.io.InputStream;
import java.text.ParseException;
@@ -24,14 +33,6 @@ import java.util.ArrayList;
import java.util.Date;
import java.util.Locale;
-import android.util.Config;
-import android.util.Log;
-
-import com.android.email.Email;
-import com.android.email.FixedLengthInputStream;
-import com.android.email.PeekableInputStream;
-import com.android.email.mail.MessagingException;
-
public class ImapResponseParser {
// DEBUG ONLY - Always check in as "false"
private static boolean DEBUG_LOG_RAW_STREAM = false;
@@ -43,11 +44,11 @@ public class ImapResponseParser {
PeekableInputStream mIn;
InputStream mActiveLiteral;
- public ImapResponseParser(PeekableInputStream in) {
+ public ImapResponseParser(InputStream in) {
if (DEBUG_LOG_RAW_STREAM && Config.LOGD && Email.DEBUG) {
in = new LoggingInputStream(in);
}
- this.mIn = in;
+ this.mIn = new PeekableInputStream(in);
}
/**
@@ -383,69 +384,4 @@ public class ImapResponseParser {
}
}
-
- /**
- * Simple class used for debugging only that affords us a view of the raw Imap stream,
- * in addition to the tokenized version.
- */
- private static class LoggingInputStream extends PeekableInputStream {
-
- PeekableInputStream mIn;
- StringBuilder mSb;
-
- public LoggingInputStream(PeekableInputStream in) {
- super(null);
- mIn = in;
- mSb = new StringBuilder();
- }
-
- /**
- * Collect chars as read, and log them when EOL reached.
- */
- @Override
- public int read() throws IOException {
- int oneByte = mIn.read();
- logRaw(oneByte);
- return oneByte;
- }
-
- /**
- * Collect chars as read, and log them when EOL reached.
- */
- @Override
- public int read(byte[] b, int offset, int length) throws IOException {
- int bytesRead = mIn.read(b, offset, length);
- int copyBytes = bytesRead;
- while (copyBytes > 0) {
- logRaw((char)b[offset]);
- copyBytes--;
- offset++;
- }
-
- return bytesRead;
- }
-
- /**
- * Pass-through any peeks
- */
- @Override
- public int peek() throws IOException {
- return mIn.peek();
- }
-
- /**
- * Write and clear the buffer
- */
- private void logRaw(int oneByte) {
- if (oneByte == '\r' || oneByte == '\n') {
- if (mSb.length() > 0) {
- Log.d(Email.LOG_TAG, "RAW " + mSb.toString());
- mSb = new StringBuilder();
- }
- } else {
- mSb.append((char)oneByte);
- }
- }
- }
-
}
diff --git a/src/com/android/email/mail/store/ImapStore.java b/src/com/android/email/mail/store/ImapStore.java
index d723982d7..13b85f12f 100644
--- a/src/com/android/email/mail/store/ImapStore.java
+++ b/src/com/android/email/mail/store/ImapStore.java
@@ -856,6 +856,7 @@ public class ImapStore extends Store {
if (bs.get(2) instanceof ImapList) {
bodyParams = bs.getList(2);
}
+ String cid = bs.getString(3);
String encoding = bs.getString(5);
int size = bs.getNumber(6);
@@ -941,6 +942,12 @@ public class ImapStore extends Store {
* to parse the body.
*/
part.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, encoding);
+ /*
+ * Set the Content-ID header.
+ */
+ if (!"NIL".equalsIgnoreCase(cid)) {
+ part.setHeader(MimeHeader.HEADER_CONTENT_ID, cid);
+ }
if (part instanceof ImapMessage) {
((ImapMessage) part).setSize(size);
@@ -1084,8 +1091,6 @@ public class ImapStore extends Store {
private int mNextCommandTag;
public void open() throws IOException, MessagingException {
- PeekableInputStream mIn;
-
if (mTransport != null && mTransport.isOpen()) {
return;
}
@@ -1101,8 +1106,7 @@ public class ImapStore extends Store {
mTransport.open();
mTransport.setSoTimeout(MailTransport.SOCKET_READ_TIMEOUT);
- mIn = new PeekableInputStream(mTransport.getInputStream());
- mParser = new ImapResponseParser(mIn);
+ mParser = new ImapResponseParser(mTransport.getInputStream());
// BANNER
mParser.readResponse();
@@ -1119,8 +1123,7 @@ public class ImapStore extends Store {
mTransport.reopenTls();
mTransport.setSoTimeout(MailTransport.SOCKET_READ_TIMEOUT);
- mIn = new PeekableInputStream(mTransport.getInputStream());
- mParser = new ImapResponseParser(mIn);
+ mParser = new ImapResponseParser(mTransport.getInputStream());
} else if (mTransport.getSecurity() ==
Transport.CONNECTION_SECURITY_TLS_REQUIRED) {
if (Config.LOGD && Email.DEBUG) {
diff --git a/src/com/android/email/mail/store/LocalStore.java b/src/com/android/email/mail/store/LocalStore.java
index 0021abc42..995b64fef 100644
--- a/src/com/android/email/mail/store/LocalStore.java
+++ b/src/com/android/email/mail/store/LocalStore.java
@@ -76,10 +76,11 @@ public class LocalStore extends Store {
* ---------- ---------- -----
* 18 pre-1.0 Development versions. No upgrade path.
* 18 1.0, 1.1 1.0 Release version.
- * 19 1.5 Added message_id column
+ * 19 - Added message_id column to messages table.
+ * 20 1.5 Added content_id column to attachments table.
*/
- private static final int DB_VERSION = 19;
+ private static final int DB_VERSION = 20;
private static final Flag[] PERMANENT_FLAGS = { Flag.DELETED, Flag.X_DESTROYED, Flag.SEEN };
@@ -110,6 +111,10 @@ public class LocalStore extends Store {
}
mDb = SQLiteDatabase.openOrCreateDatabase(mPath, null);
int oldVersion = mDb.getVersion();
+
+ /*
+ * TODO we should have more sophisticated way to upgrade database.
+ */
if (oldVersion != DB_VERSION) {
if (Config.LOGV) {
Log.v(Email.LOG_TAG, String.format("Upgrading database from %d to %d",
@@ -133,7 +138,7 @@ public class LocalStore extends Store {
mDb.execSQL("DROP TABLE IF EXISTS attachments");
mDb.execSQL("CREATE TABLE attachments (id INTEGER PRIMARY KEY, message_id INTEGER,"
+ "store_data TEXT, content_uri TEXT, size INTEGER, name TEXT,"
- + "mime_type TEXT)");
+ + "mime_type TEXT, content_id TEXT)");
mDb.execSQL("DROP TABLE IF EXISTS pending_commands");
mDb.execSQL("CREATE TABLE pending_commands " +
@@ -146,12 +151,21 @@ public class LocalStore extends Store {
mDb.execSQL("CREATE TRIGGER delete_message BEFORE DELETE ON messages BEGIN DELETE FROM attachments WHERE old.id = message_id; END;");
mDb.setVersion(DB_VERSION);
}
- else if (oldVersion < 19) {
- /**
- * Upgrade 18 to 19: add message_id to messages table
- */
- mDb.execSQL("ALTER TABLE messages ADD COLUMN message_id TEXT;");
- mDb.setVersion(DB_VERSION);
+ else {
+ if (oldVersion < 19) {
+ /**
+ * Upgrade 18 to 19: add message_id to messages table
+ */
+ mDb.execSQL("ALTER TABLE messages ADD COLUMN message_id TEXT;");
+ mDb.setVersion(19);
+ }
+ if (oldVersion < 20) {
+ /**
+ * Upgrade 19 to 20: add content_id to attachments table
+ */
+ mDb.execSQL("ALTER TABLE attachments ADD COLUMN content_id TEXT;");
+ mDb.setVersion(20);
+ }
}
if (mDb.getVersion() != DB_VERSION) {
@@ -523,7 +537,8 @@ public class LocalStore extends Store {
"name",
"mime_type",
"store_data",
- "content_uri" },
+ "content_uri",
+ "content_id" },
"message_id = ?",
new String[] { Long.toString(localMessage.mId) },
null,
@@ -537,6 +552,7 @@ public class LocalStore extends Store {
String type = cursor.getString(3);
String storeData = cursor.getString(4);
String contentUri = cursor.getString(5);
+ String contentId = cursor.getString(6);
Body body = null;
if (contentUri != null) {
body = new LocalAttachmentBody(Uri.parse(contentUri), mContext);
@@ -551,6 +567,7 @@ public class LocalStore extends Store {
String.format("attachment;\n filename=\"%s\";\n size=%d",
name,
size));
+ bp.setHeader(MimeHeader.HEADER_CONTENT_ID, contentId);
/*
* HEADER_ANDROID_ATTACHMENT_STORE_DATA is a custom header we add to that
@@ -924,6 +941,7 @@ public class LocalStore extends Store {
MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA), ',');
String name = MimeUtility.getHeaderParameter(attachment.getContentType(), "name");
+ String contentId = attachment.getContentId();
if (attachmentId == -1) {
ContentValues cv = new ContentValues();
@@ -933,6 +951,7 @@ public class LocalStore extends Store {
cv.put("size", size);
cv.put("name", name);
cv.put("mime_type", attachment.getMimeType());
+ cv.put("content_id", contentId);
attachmentId = mDb.insert("attachments", "message_id", cv);
}
@@ -940,6 +959,7 @@ public class LocalStore extends Store {
ContentValues cv = new ContentValues();
cv.put("content_uri", contentUri != null ? contentUri.toString() : null);
cv.put("size", size);
+ cv.put("content_id", contentId);
mDb.update(
"attachments",
cv,
diff --git a/src/com/android/email/mail/store/Pop3Store.java b/src/com/android/email/mail/store/Pop3Store.java
index 48fe878fb..31468d896 100644
--- a/src/com/android/email/mail/store/Pop3Store.java
+++ b/src/com/android/email/mail/store/Pop3Store.java
@@ -29,6 +29,7 @@ import com.android.email.mail.Store;
import com.android.email.mail.Transport;
import com.android.email.mail.Folder.OpenMode;
import com.android.email.mail.internet.MimeMessage;
+import com.android.email.mail.transport.LoggingInputStream;
import com.android.email.mail.transport.MailTransport;
import android.util.Config;
@@ -46,6 +47,7 @@ public class Pop3Store extends Store {
// All flags defining debug or development code settings must be FALSE
// when code is checked in or released.
private static boolean DEBUG_FORCE_SINGLE_LINE_UIDL = false;
+ private static boolean DEBUG_LOG_RAW_STREAM = false;
private static final Flag[] PERMANENT_FLAGS = { Flag.DELETED };
@@ -718,7 +720,11 @@ public class Pop3Store extends Store {
}
if (response != null) {
try {
- message.parse(new Pop3ResponseInputStream(mTransport.getInputStream()));
+ InputStream in = mTransport.getInputStream();
+ if (DEBUG_LOG_RAW_STREAM && Config.LOGD && Email.DEBUG) {
+ in = new LoggingInputStream(in);
+ }
+ message.parse(new Pop3ResponseInputStream(in));
}
catch (MessagingException me) {
/*
diff --git a/src/com/android/email/mail/transport/LoggingInputStream.java b/src/com/android/email/mail/transport/LoggingInputStream.java
new file mode 100644
index 000000000..233c3a5d5
--- /dev/null
+++ b/src/com/android/email/mail/transport/LoggingInputStream.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.email.mail.transport;
+
+import com.android.email.Email;
+
+import android.util.Log;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Simple class used for debugging only that affords us a view of the raw IMAP or POP3 stream,
+ * in addition to the tokenized version.
+ *
+ * Use of this class *MUST* be restricted to logging-enabled situations only.
+ */
+public class LoggingInputStream extends InputStream {
+
+ InputStream mIn;
+ StringBuilder mSb;
+ boolean mBufferDirty;
+
+ private final String LINE_TAG = "RAW ";
+
+ public LoggingInputStream(InputStream in) {
+ super();
+ mIn = in;
+ mSb = new StringBuilder(LINE_TAG);
+ mBufferDirty = false;
+ }
+
+ /**
+ * Collect chars as read, and log them when EOL reached.
+ */
+ @Override
+ public int read() throws IOException {
+ int oneByte = mIn.read();
+ logRaw(oneByte);
+ return oneByte;
+ }
+
+ /**
+ * Collect chars as read, and log them when EOL reached.
+ */
+ @Override
+ public int read(byte[] b, int offset, int length) throws IOException {
+ int bytesRead = mIn.read(b, offset, length);
+ int copyBytes = bytesRead;
+ while (copyBytes > 0) {
+ logRaw((char)b[offset]);
+ copyBytes--;
+ offset++;
+ }
+
+ return bytesRead;
+ }
+
+ /**
+ * Write and clear the buffer
+ */
+ private void logRaw(int oneByte) {
+ if (oneByte == '\r' || oneByte == '\n') {
+ if (mBufferDirty) {
+ Log.d(Email.LOG_TAG, mSb.toString());
+ mSb = new StringBuilder(LINE_TAG);
+ mBufferDirty = false;
+ }
+ } else {
+ mSb.append((char)oneByte);
+ mBufferDirty = true;
+ }
+ }
+}
diff --git a/tests/src/com/android/email/activity/MessageViewTests.java b/tests/src/com/android/email/activity/MessageViewTests.java
index 64b42d679..6217f2a94 100644
--- a/tests/src/com/android/email/activity/MessageViewTests.java
+++ b/tests/src/com/android/email/activity/MessageViewTests.java
@@ -21,16 +21,25 @@ import com.android.email.Email;
import com.android.email.MessagingController;
import com.android.email.Preferences;
import com.android.email.R;
+import com.android.email.mail.MessageTestUtils;
+import com.android.email.mail.Message;
+import com.android.email.mail.MessagingException;
+import com.android.email.mail.MessageTestUtils.MessageBuilder;
+import com.android.email.mail.MessageTestUtils.MultipartBuilder;
+import com.android.email.mail.MessageTestUtils.TextBuilder;
+import com.android.email.mail.internet.BinaryTempFileBody;
+import com.android.email.mail.store.LocalStore;
import android.content.Context;
import android.content.Intent;
+import android.net.Uri;
import android.test.ActivityInstrumentationTestCase2;
import android.test.suitebuilder.annotation.MediumTest;
import android.test.suitebuilder.annotation.Suppress;
-import android.view.MenuItem;
import android.webkit.WebView;
import android.widget.TextView;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
@@ -58,6 +67,7 @@ public class MessageViewTests
private TextView mToView;
private TextView mSubjectView;
private WebView mMessageContentView;
+ private Context mContext;
public MessageViewTests() {
super("com.android.email", MessageView.class);
@@ -66,13 +76,13 @@ public class MessageViewTests
@Override
protected void setUp() throws Exception {
super.setUp();
- Context context = getInstrumentation().getTargetContext();
- Account[] accounts = Preferences.getPreferences(context).getAccounts();
+ mContext = getInstrumentation().getTargetContext();
+ Account[] accounts = Preferences.getPreferences(mContext).getAccounts();
if (accounts.length > 0)
{
// This depends on getDefaultAccount() to auto-assign the default account, if necessary
- mAccount = Preferences.getPreferences(context).getDefaultAccount();
- Email.setServicesEnabled(context);
+ mAccount = Preferences.getPreferences(mContext).getDefaultAccount();
+ Email.setServicesEnabled(mContext);
}
// configure a mock controller
@@ -93,6 +103,9 @@ public class MessageViewTests
mToView = (TextView) a.findViewById(R.id.to);
mSubjectView = (TextView) a.findViewById(R.id.subject);
mMessageContentView = (WebView) a.findViewById(R.id.message_content);
+
+ // This is needed for mime image bodypart.
+ BinaryTempFileBody.setTempDirectory(getActivity().getCacheDir());
}
/**
@@ -135,6 +148,90 @@ public class MessageViewTests
a.handleMenuItem(R.id.forward);
a.handleMenuItem(R.id.mark_as_unread);
}
+
+ /**
+ * Tests for resolving inline image src cid: reference to content uri.
+ */
+
+ public void testResolveInlineImage() throws MessagingException, IOException {
+ final MessageView a = getActivity();
+ final LocalStore store = new LocalStore(mAccount.getLocalStoreUri(), mContext);
+
+ // Single cid case.
+ final String cid1 = "cid.1@android.com";
+ final long aid1 = 10;
+ final Uri uri1 = MessageTestUtils.contentUri(aid1, mAccount);
+ final String text1 = new TextBuilder("text1 > ").addCidImg(cid1).build(" <.");
+ final String expected1 = new TextBuilder("text1 > ").addUidImg(uri1).build(" <.");
+
+ // message with cid1
+ final Message msg1 = new MessageBuilder()
+ .setBody(new MultipartBuilder("multipart/related")
+ .addBodyPart(MessageTestUtils.textPart("text/html", text1))
+ .addBodyPart(MessageTestUtils.imagePart("image/jpeg", "<"+cid1+">", aid1, store))
+ .build())
+ .build();
+ // Simple case.
+ final String actual1 = a.resolveInlineImage(text1, msg1, 0);
+ assertEquals("one content id reference is not resolved",
+ expected1, actual1);
+
+ // Exceed recursive limit.
+ final String actual0 = a.resolveInlineImage(text1, msg1, 10);
+ assertEquals("recursive call limit may exceeded",
+ text1, actual0);
+
+ // Multiple cids case.
+ final String cid2 = "cid.2@android.com";
+ final long aid2 = 20;
+ final Uri uri2 = MessageTestUtils.contentUri(aid2, mAccount);
+ final String text2 = new TextBuilder("text2 ").addCidImg(cid2).build(".");
+ final String expected2 = new TextBuilder("text2 ").addUidImg(uri2).build(".");
+
+ // message with only cid2
+ final Message msg2 = new MessageBuilder()
+ .setBody(new MultipartBuilder("multipart/related")
+ .addBodyPart(MessageTestUtils.textPart("text/html", text1 + text2))
+ .addBodyPart(MessageTestUtils.imagePart("image/gif", cid2, aid2, store))
+ .build())
+ .build();
+ // cid1 is not replaced
+ final String actual2 = a.resolveInlineImage(text1 + text2, msg2, 0);
+ assertEquals("only one of two content id is resolved",
+ text1 + expected2, actual2);
+
+ // message with cid1 and cid2
+ final Message msg3 = new MessageBuilder()
+ .setBody(new MultipartBuilder("multipart/related")
+ .addBodyPart(MessageTestUtils.textPart("text/html", text2 + text1))
+ .addBodyPart(MessageTestUtils.imagePart("image/jpeg", cid1, aid1, store))
+ .addBodyPart(MessageTestUtils.imagePart("image/gif", cid2, aid2, store))
+ .build())
+ .build();
+ // cid1 and cid2 are replaced
+ final String actual3 = a.resolveInlineImage(text2 + text1, msg3, 0);
+ assertEquals("two content ids are resolved correctly",
+ expected2 + expected1, actual3);
+
+ // message with many cids and normal attachments
+ final Message msg4 = new MessageBuilder()
+ .setBody(new MultipartBuilder("multipart/mixed")
+ .addBodyPart(MessageTestUtils.imagePart("image/jpeg", null, 30, store))
+ .addBodyPart(MessageTestUtils.imagePart("application/pdf", cid1, aid1, store))
+ .addBodyPart(new MultipartBuilder("multipart/related")
+ .addBodyPart(MessageTestUtils.textPart("text/html", text2 + text1))
+ .addBodyPart(MessageTestUtils.imagePart("image/jpg", cid1, aid1, store))
+ .addBodyPart(MessageTestUtils.imagePart("image/gif", cid2, aid2, store))
+ .buildBodyPart())
+ .addBodyPart(MessageTestUtils.imagePart("application/pdf", cid2, aid2, store))
+ .build())
+ .build();
+ // cid1 and cid2 are replaced
+ final String actual4 = a.resolveInlineImage(text2 + text1, msg4, 0);
+ assertEquals("two content ids in deep multipart level are resolved",
+ expected2 + expected1, actual4);
+ }
+
/**
* Mock Messaging controller, so we can drive its callbacks. This probably should be
diff --git a/tests/src/com/android/email/mail/AddressUnitTests.java b/tests/src/com/android/email/mail/AddressUnitTests.java
index 10d151f24..585a1b9d0 100644
--- a/tests/src/com/android/email/mail/AddressUnitTests.java
+++ b/tests/src/com/android/email/mail/AddressUnitTests.java
@@ -43,9 +43,27 @@ public class AddressUnitTests extends AndroidTestCase {
}
/**
- * TODO: test parse()
+ * TODO: more in-depth tests for parse()
*/
+ /**
+ * Simple quick checks of empty-input edge conditions for parse()
+ *
+ * NOTE: This is not a claim that these edge cases are "correct", only to maintain consistent
+ * behavior while I am changing some of the code in the function under test.
+ */
+ public void testEmptyParse() {
+ Address[] result;
+
+ // null input => empty array
+ result = Address.parse(null);
+ assertTrue("parsing null address", result != null && result.length == 0);
+
+ // empty string input => empty array
+ result = Address.parse("");
+ assertTrue("parsing zero-length", result != null && result.length == 0);
+ }
+
/**
* TODO: test toString() (single & list)
*/
@@ -75,6 +93,43 @@ public class AddressUnitTests extends AndroidTestCase {
}
/**
- * TODO: test pack() and unpack()
+ * TODO: more in-depth tests for pack() and unpack()
*/
+
+ /**
+ * Simple quick checks of empty-input edge conditions for pack()
+ *
+ * NOTE: This is not a claim that these edge cases are "correct", only to maintain consistent
+ * behavior while I am changing some of the code in the function under test.
+ */
+ public void testEmptyPack() {
+ String result;
+
+ // null input => null string
+ result = Address.pack(null);
+ assertNull("packing null", result);
+
+ // zero-length input => empty string
+ result = Address.pack(new Address[] { });
+ assertEquals("packing empty array", "", result);
+ }
+
+ /**
+ * Simple quick checks of empty-input edge conditions for unpack()
+ *
+ * NOTE: This is not a claim that these edge cases are "correct", only to maintain consistent
+ * behavior while I am changing some of the code in the function under test.
+ */
+ public void testEmptyUnpack() {
+ Address[] result;
+
+ // null input => empty array
+ result = Address.unpack(null);
+ assertTrue("unpacking null address", result != null && result.length == 0);
+
+ // empty string input => empty array
+ result = Address.unpack("");
+ assertTrue("unpacking zero-length", result != null && result.length == 0);
+ }
+
}
diff --git a/tests/src/com/android/email/mail/MessageTestUtils.java b/tests/src/com/android/email/mail/MessageTestUtils.java
new file mode 100644
index 000000000..506b54974
--- /dev/null
+++ b/tests/src/com/android/email/mail/MessageTestUtils.java
@@ -0,0 +1,302 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.email.mail;
+
+import com.android.email.Account;
+import com.android.email.mail.internet.BinaryTempFileBody;
+import com.android.email.mail.internet.MimeBodyPart;
+import com.android.email.mail.internet.MimeHeader;
+import com.android.email.mail.internet.MimeMessage;
+import com.android.email.mail.internet.MimeMultipart;
+import com.android.email.mail.internet.TextBody;
+import com.android.email.mail.store.LocalStore;
+import com.android.email.provider.AttachmentProvider;
+
+import android.net.Uri;
+
+import java.io.IOException;
+import java.util.ArrayList;
+
+/**
+ * Utility class makes it easier for developer to build mail message objects.
+ *
+ * Typical usage of these helper functions and builder objects are as follows.
+ *
+ *
+ * String text2 = new TextBuilder("").text("")
+ * .text("").cidImg("contetid@domain").text("").build("").text("")
+ * .text("").uriImg(contentUri).text("").build("