diff --git a/images/drawables-pgp/status_lock_closed.svg b/images/drawables-pgp/status_lock_closed.svg
new file mode 100644
index 000000000..286e89297
--- /dev/null
+++ b/images/drawables-pgp/status_lock_closed.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/images/drawables-pgp/status_lock_error.svg b/images/drawables-pgp/status_lock_error.svg
new file mode 100644
index 000000000..d3c4e1d1d
--- /dev/null
+++ b/images/drawables-pgp/status_lock_error.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/images/drawables-pgp/status_lock_open.svg b/images/drawables-pgp/status_lock_open.svg
new file mode 100644
index 000000000..9beb127af
--- /dev/null
+++ b/images/drawables-pgp/status_lock_open.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/images/drawables-pgp/status_signature_expired_cutout.svg b/images/drawables-pgp/status_signature_expired_cutout.svg
new file mode 100644
index 000000000..61ac8fdd0
--- /dev/null
+++ b/images/drawables-pgp/status_signature_expired_cutout.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/images/drawables-pgp/status_signature_invalid_cutout.svg b/images/drawables-pgp/status_signature_invalid_cutout.svg
new file mode 100644
index 000000000..61fd2ace0
--- /dev/null
+++ b/images/drawables-pgp/status_signature_invalid_cutout.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/images/drawables-pgp/status_signature_revoked_cutout.svg b/images/drawables-pgp/status_signature_revoked_cutout.svg
new file mode 100644
index 000000000..0421286fe
--- /dev/null
+++ b/images/drawables-pgp/status_signature_revoked_cutout.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/images/drawables-pgp/status_signature_unknown_cutout.svg b/images/drawables-pgp/status_signature_unknown_cutout.svg
new file mode 100644
index 000000000..402bffcaa
--- /dev/null
+++ b/images/drawables-pgp/status_signature_unknown_cutout.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/images/drawables-pgp/status_signature_unverified_cutout.svg b/images/drawables-pgp/status_signature_unverified_cutout.svg
new file mode 100644
index 000000000..ffa98580a
--- /dev/null
+++ b/images/drawables-pgp/status_signature_unverified_cutout.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/images/drawables-pgp/status_signature_verified_cutout.svg b/images/drawables-pgp/status_signature_verified_cutout.svg
new file mode 100644
index 000000000..04356a977
--- /dev/null
+++ b/images/drawables-pgp/status_signature_verified_cutout.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/images/update-drawables-pgp.sh b/images/update-drawables-pgp.sh
new file mode 100755
index 000000000..45220ebc5
--- /dev/null
+++ b/images/update-drawables-pgp.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+APP_DIR=../k9mail/src/main
+MDPI_DIR=$APP_DIR/res/drawable-mdpi
+HDPI_DIR=$APP_DIR/res/drawable-hdpi
+XDPI_DIR=$APP_DIR/res/drawable-xhdpi
+XXDPI_DIR=$APP_DIR/res/drawable-xxhdpi
+XXXDPI_DIR=$APP_DIR/res/drawable-xxxhdpi
+SRC_DIR=./drawables-pgp/
+
+
+for NAME in "status_lock_closed" "status_lock_error" "status_lock_open" "status_signature_expired_cutout" "status_signature_invalid_cutout" "status_signature_revoked_cutout" "status_signature_unknown_cutout" "status_signature_unverified_cutout" "status_signature_verified_cutout"
+do
+echo $NAME
+inkscape -w 24 -h 24 -e "$MDPI_DIR/$NAME.png" "$SRC_DIR/$NAME.svg"
+inkscape -w 32 -h 32 -e "$HDPI_DIR/$NAME.png" "$SRC_DIR/$NAME.svg"
+inkscape -w 48 -h 48 -e "$XDPI_DIR/$NAME.png" "$SRC_DIR/$NAME.svg"
+inkscape -w 64 -h 64 -e "$XXDPI_DIR/$NAME.png" "$SRC_DIR/$NAME.svg"
+done
diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/BodyPart.java b/k9mail-library/src/main/java/com/fsck/k9/mail/BodyPart.java
index 551866829..ef48dce8f 100644
--- a/k9mail-library/src/main/java/com/fsck/k9/mail/BodyPart.java
+++ b/k9mail-library/src/main/java/com/fsck/k9/mail/BodyPart.java
@@ -2,14 +2,25 @@ package com.fsck.k9.mail;
public abstract class BodyPart implements Part {
- private Multipart mParent;
+ private String serverExtra;
+ private Multipart parent;
+
+ @Override
+ public String getServerExtra() {
+ return serverExtra;
+ }
+
+ @Override
+ public void setServerExtra(String serverExtra) {
+ this.serverExtra = serverExtra;
+ }
public Multipart getParent() {
- return mParent;
+ return parent;
}
public void setParent(Multipart parent) {
- mParent = parent;
+ this.parent = parent;
}
public abstract void setEncoding(String encoding) throws MessagingException;
diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/Message.java b/k9mail-library/src/main/java/com/fsck/k9/mail/Message.java
index a77e0c282..67ba3a244 100644
--- a/k9mail-library/src/main/java/com/fsck/k9/mail/Message.java
+++ b/k9mail-library/src/main/java/com/fsck/k9/mail/Message.java
@@ -120,9 +120,6 @@ public abstract class Message implements Part, CompositeBody {
@Override
public abstract Body getBody();
- @Override
- public abstract String getContentType() throws MessagingException;
-
@Override
public abstract void addHeader(String name, String value) throws MessagingException;
@@ -141,7 +138,7 @@ public abstract class Message implements Part, CompositeBody {
public abstract void removeHeader(String name) throws MessagingException;
@Override
- public abstract void setBody(Body body) throws MessagingException;
+ public abstract void setBody(Body body);
public abstract long getId();
@@ -150,55 +147,6 @@ public abstract class Message implements Part, CompositeBody {
public abstract int getSize();
- /*
- * 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 static 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);
- }
-
- // Remove (correctly delimited by '-- \n') signatures
- text = text.replaceAll("(?ms)^-- [\\r\\n]+.*", "");
- // 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+:$", "");
- // Remove horizontal rules.
- text = text.replaceAll("\\s*([-=_]{30,}+)\\s*", " ");
-
- // 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+", " ");
- // Remove any whitespace at the beginning and end of the string.
- text = text.trim();
-
- return (text.length() <= 512) ? text : text.substring(0, 512);
- }
-
public void delete(String trashFolderName) throws MessagingException {}
/*
diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/Multipart.java b/k9mail-library/src/main/java/com/fsck/k9/mail/Multipart.java
index 5f82e062d..b89b69182 100644
--- a/k9mail-library/src/main/java/com/fsck/k9/mail/Multipart.java
+++ b/k9mail-library/src/main/java/com/fsck/k9/mail/Multipart.java
@@ -28,7 +28,9 @@ public abstract class Multipart implements CompositeBody {
return Collections.unmodifiableList(mParts);
}
- public abstract String getContentType();
+ public abstract String getMimeType();
+
+ public abstract String getBoundary();
public int getCount() {
return mParts.size();
@@ -64,4 +66,7 @@ public abstract class Multipart implements CompositeBody {
((TextBody)body).setCharset(charset);
}
}
+
+ public abstract byte[] getPreamble();
+ public abstract byte[] getEpilogue();
}
diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/Part.java b/k9mail-library/src/main/java/com/fsck/k9/mail/Part.java
index 1d0274f32..d78132f21 100644
--- a/k9mail-library/src/main/java/com/fsck/k9/mail/Part.java
+++ b/k9mail-library/src/main/java/com/fsck/k9/mail/Part.java
@@ -2,6 +2,7 @@
package com.fsck.k9.mail;
import java.io.IOException;
+import java.io.InputStream;
import java.io.OutputStream;
public interface Part {
@@ -15,22 +16,24 @@ public interface Part {
Body getBody();
- String getContentType() throws MessagingException;
+ String getContentType();
String getDisposition() throws MessagingException;
- String getContentId() throws MessagingException;
+ String getContentId();
String[] getHeader(String name) throws MessagingException;
boolean isMimeType(String mimeType) throws MessagingException;
- String getMimeType() throws MessagingException;
+ String getMimeType();
- void setBody(Body body) throws MessagingException;
+ void setBody(Body body);
void writeTo(OutputStream out) throws IOException, MessagingException;
+ void writeHeaderTo(OutputStream out) throws IOException, MessagingException;
+
/**
* Called just prior to transmission, once the type of transport is known to
* be 7bit.
@@ -44,4 +47,8 @@ public interface Part {
*/
//TODO perhaps it would be clearer to use a flag "force7bit" in writeTo
void setUsing7bitTransport() throws MessagingException;
+
+ String getServerExtra();
+
+ void setServerExtra(String serverExtra);
}
diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/BinaryTempFileBody.java b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/BinaryTempFileBody.java
index 17870c140..157b4046c 100644
--- a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/BinaryTempFileBody.java
+++ b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/BinaryTempFileBody.java
@@ -1,6 +1,5 @@
package com.fsck.k9.mail.internet;
-import com.fsck.k9.mail.Body;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.filter.Base64OutputStream;
import org.apache.commons.io.IOUtils;
@@ -15,7 +14,7 @@ import java.io.*;
* and writeTo one time. After writeTo is called, or the InputStream returned from
* getInputStream is closed the file is deleted and the Body should be considered disposed of.
*/
-public class BinaryTempFileBody implements RawDataBody {
+public class BinaryTempFileBody implements RawDataBody, SizeAware {
private static File mTempDirectory;
private File mFile;
@@ -26,6 +25,10 @@ public class BinaryTempFileBody implements RawDataBody {
mTempDirectory = tempDirectory;
}
+ public static File getTempDirectory() {
+ return mTempDirectory;
+ }
+
@Override
public String getEncoding() {
return mEncoding;
@@ -101,6 +104,15 @@ public class BinaryTempFileBody implements RawDataBody {
}
}
+ @Override
+ public long getSize() {
+ return mFile.length();
+ }
+
+ public File getFile() {
+ return mFile;
+ }
+
class BinaryTempFileBodyInputStream extends FilterInputStream {
public BinaryTempFileBodyInputStream(InputStream in) {
super(in);
diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/BinaryTempFileMessageBody.java b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/BinaryTempFileMessageBody.java
index e5fe7c370..a47f2dffd 100644
--- a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/BinaryTempFileMessageBody.java
+++ b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/BinaryTempFileMessageBody.java
@@ -11,10 +11,7 @@ import com.fsck.k9.mail.CompositeBody;
import com.fsck.k9.mail.MessagingException;
/**
- * A {@link BinaryTempFileBody} extension containing a body of type
- * message/rfc822. This relates to a BinaryTempFileBody the same way that a
- * {@link LocalAttachmentMessageBody} relates to a {@link LocalAttachmentBody}.
- *
+ * A {@link BinaryTempFileBody} extension containing a body of type message/rfc822.
*/
public class BinaryTempFileMessageBody extends BinaryTempFileBody implements CompositeBody {
@@ -63,4 +60,4 @@ public class BinaryTempFileMessageBody extends BinaryTempFileBody implements Com
*/
}
-}
\ No newline at end of file
+}
diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MessageExtractor.java b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MessageExtractor.java
index ba0bfa42f..7d1b6e291 100644
--- a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MessageExtractor.java
+++ b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MessageExtractor.java
@@ -48,7 +48,7 @@ public class MessageExtractor {
* determine the charset from HTML message.
*/
if (mimeType.equalsIgnoreCase("text/html") && charset == null) {
- InputStream in = part.getBody().getInputStream();
+ InputStream in = MimeUtility.decodeBody(body);
try {
byte[] buf = new byte[256];
in.read(buf, 0, buf.length);
@@ -64,18 +64,8 @@ public class MessageExtractor {
}
} finally {
try {
- if (in instanceof BinaryTempFileBody.BinaryTempFileBodyInputStream) {
- /*
- * If this is a BinaryTempFileBodyInputStream, calling close()
- * will delete the file. But we can't let that happen because
- * the file needs to be opened again by the code a few lines
- * down.
- */
- ((BinaryTempFileBody.BinaryTempFileBodyInputStream) in).closeWithoutDeleting();
- } else {
- in.close();
- }
- } catch (Exception e) { /* ignore */ }
+ MimeUtility.closeInputStreamWithoutDeletingTemporaryFiles(in);
+ } catch (IOException e) { /* ignore */ }
}
}
charset = fixupCharset(charset, getMessageFromPart(part));
@@ -84,22 +74,12 @@ public class MessageExtractor {
* Now we read the part into a buffer for further processing. Because
* the stream is now wrapped we'll remove any transfer encoding at this point.
*/
- InputStream in = part.getBody().getInputStream();
+ InputStream in = MimeUtility.decodeBody(body);
try {
- String text = CharsetSupport.readToString(in, charset);
-
- // Replace the body with a TextBody that already contains the decoded text
- part.setBody(new TextBody(text));
-
- return text;
+ return CharsetSupport.readToString(in, charset);
} finally {
try {
- /*
- * This time we don't care if it's a BinaryTempFileBodyInputStream. We
- * replaced the body with a TextBody instance and hence don't need the
- * file anymore.
- */
- in.close();
+ MimeUtility.closeInputStreamWithoutDeletingTemporaryFiles(in);
} catch (IOException e) { /* Ignore */ }
}
}
@@ -186,6 +166,8 @@ public class MessageExtractor {
Html html = new Html(part);
viewables.add(html);
}
+ } else if (part.getMimeType().equalsIgnoreCase("application/pgp-signature")) {
+ // ignore this type explicitly
} else {
// Everything else is treated as attachment.
attachments.add(part);
diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeBodyPart.java b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeBodyPart.java
index 32e3e2654..2a099db15 100644
--- a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeBodyPart.java
+++ b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeBodyPart.java
@@ -5,7 +5,6 @@ import com.fsck.k9.mail.Body;
import com.fsck.k9.mail.BodyPart;
import com.fsck.k9.mail.CompositeBody;
import com.fsck.k9.mail.MessagingException;
-import com.fsck.k9.mail.Multipart;
import java.io.BufferedWriter;
import java.io.IOException;
@@ -73,7 +72,7 @@ public class MimeBodyPart extends BodyPart {
}
@Override
- public void setBody(Body body) throws MessagingException {
+ public void setBody(Body body) {
this.mBody = body;
}
@@ -86,7 +85,7 @@ public class MimeBodyPart extends BodyPart {
}
@Override
- public String getContentType() throws MessagingException {
+ public String getContentType() {
String contentType = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE);
return (contentType == null) ? "text/plain" : contentType;
}
@@ -97,7 +96,7 @@ public class MimeBodyPart extends BodyPart {
}
@Override
- public String getContentId() throws MessagingException {
+ public String getContentId() {
String contentId = getFirstHeader(MimeHeader.HEADER_CONTENT_ID);
if (contentId == null) {
return null;
@@ -112,7 +111,7 @@ public class MimeBodyPart extends BodyPart {
}
@Override
- public String getMimeType() throws MessagingException {
+ public String getMimeType() {
return MimeUtility.getHeaderParameter(getContentType(), null);
}
@@ -135,6 +134,11 @@ public class MimeBodyPart extends BodyPart {
}
}
+ @Override
+ public void writeHeaderTo(OutputStream out) throws IOException, MessagingException {
+ mHeader.writeTo(out);
+ }
+
@Override
public void setUsing7bitTransport() throws MessagingException {
String type = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE);
diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeHeader.java b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeHeader.java
index f3835d04a..318ea6da0 100644
--- a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeHeader.java
+++ b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeHeader.java
@@ -11,28 +11,11 @@ import java.util.*;
public class MimeHeader {
private static final String[] EMPTY_STRING_ARRAY = new String[0];
- /**
- * Application specific header that contains Store specific information about an attachment.
- * In IMAP this contains the IMAP BODYSTRUCTURE part id so that the ImapStore can later
- * retrieve the attachment at will from the server.
- * The info is recorded from this header on LocalStore.appendMessages and is put back
- * into the MIME data by LocalStore.fetch.
- */
- public static final String HEADER_ANDROID_ATTACHMENT_STORE_DATA = "X-Android-Attachment-StoreData";
-
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()
- */
- private static final String[] writeOmitFields = {
-// HEADER_ANDROID_ATTACHMENT_DOWNLOADED,
-// HEADER_ANDROID_ATTACHMENT_ID,
- HEADER_ANDROID_ATTACHMENT_STORE_DATA
- };
private List mFields = new ArrayList();
private String mCharset = null;
@@ -101,14 +84,12 @@ public class MimeHeader {
public void writeTo(OutputStream out) throws IOException {
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
for (Field field : mFields) {
- if (!Arrays.asList(writeOmitFields).contains(field.name)) {
- if (field.hasRawData()) {
- writer.write(field.getRaw());
- } else {
- writeNameValueField(writer, field);
- }
- writer.write("\r\n");
+ if (field.hasRawData()) {
+ writer.write(field.getRaw());
+ } else {
+ writeNameValueField(writer, field);
}
+ writer.write("\r\n");
}
writer.flush();
}
diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessage.java b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessage.java
index 632018c29..7b0af70bb 100644
--- a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessage.java
+++ b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessage.java
@@ -57,6 +57,7 @@ public class MimeMessage extends Message {
private Body mBody;
protected int mSize;
+ private String serverExtra;
public MimeMessage() {
}
@@ -162,7 +163,7 @@ public class MimeMessage extends Message {
}
@Override
- public String getContentType() throws MessagingException {
+ public String getContentType() {
String contentType = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE);
return (contentType == null) ? "text/plain" : contentType;
}
@@ -171,12 +172,14 @@ public class MimeMessage extends Message {
public String getDisposition() throws MessagingException {
return getFirstHeader(MimeHeader.HEADER_CONTENT_DISPOSITION);
}
+
@Override
- public String getContentId() throws MessagingException {
+ public String getContentId() {
return null;
}
+
@Override
- public String getMimeType() throws MessagingException {
+ public String getMimeType() {
return MimeUtility.getHeaderParameter(getContentType(), null);
}
@@ -308,13 +311,10 @@ public class MimeMessage extends Message {
if (mMessageId == null) {
mMessageId = getFirstHeader("Message-ID");
}
- if (mMessageId == null) { // even after checking the header
- setMessageId(generateMessageId());
- }
return mMessageId;
}
- private String generateMessageId() {
+ public void generateMessageId() throws MessagingException {
String hostname = null;
if (mFrom != null && mFrom.length >= 1) {
@@ -330,7 +330,9 @@ public class MimeMessage extends Message {
}
/* We use upper case here to match Apple Mail Message-ID format (for privacy) */
- return "<" + UUID.randomUUID().toString().toUpperCase(Locale.US) + "@" + hostname + ">";
+ String messageId = "<" + UUID.randomUUID().toString().toUpperCase(Locale.US) + "@" + hostname + ">";
+
+ setMessageId(messageId);
}
public void setMessageId(String messageId) throws MessagingException {
@@ -394,7 +396,7 @@ public class MimeMessage extends Message {
}
@Override
- public void setBody(Body body) throws MessagingException {
+ public void setBody(Body body) {
this.mBody = body;
}
@@ -444,6 +446,11 @@ public class MimeMessage extends Message {
}
}
+ @Override
+ public void writeHeaderTo(OutputStream out) throws IOException, MessagingException {
+ mHeader.writeTo(out);
+ }
+
@Override
public InputStream getInputStream() throws MessagingException {
return null;
@@ -487,13 +494,11 @@ public class MimeMessage extends Message {
stack.addFirst(MimeMessage.this);
} else {
expect(Part.class);
- try {
- MimeMessage m = new MimeMessage();
- ((Part)stack.peek()).setBody(m);
- stack.addFirst(m);
- } catch (MessagingException me) {
- throw new Error(me);
- }
+ Part part = (Part) stack.peek();
+
+ MimeMessage m = new MimeMessage();
+ part.setBody(m);
+ stack.addFirst(m);
}
}
@@ -519,7 +524,10 @@ public class MimeMessage extends Message {
Part e = (Part)stack.peek();
try {
- MimeMultipart multiPart = new MimeMultipart(e.getContentType());
+ String contentType = e.getContentType();
+ String mimeType = MimeUtility.getHeaderParameter(contentType, null);
+ String boundary = MimeUtility.getHeaderParameter(contentType, "boundary");
+ MimeMultipart multiPart = new MimeMultipart(mimeType, boundary);
e.setBody(multiPart);
stack.addFirst(multiPart);
} catch (MessagingException me) {
@@ -540,7 +548,21 @@ public class MimeMessage extends Message {
@Override
public void endMultipart() {
- stack.removeFirst();
+ expect(Multipart.class);
+ Multipart multipart = (Multipart) stack.removeFirst();
+
+ boolean hasNoBodyParts = multipart.getCount() == 0;
+ boolean hasNoEpilogue = multipart.getEpilogue() == null;
+ if (hasNoBodyParts && hasNoEpilogue) {
+ /*
+ * The parser is calling startMultipart(), preamble(), and endMultipart() when all we have is
+ * headers of a "multipart/*" part. But there's really no point in keeping a Multipart body if all
+ * of the content is missing.
+ */
+ expect(Part.class);
+ Part part = (Part) stack.peek();
+ part.setBody(null);
+ }
}
@Override
@@ -686,4 +708,16 @@ public class MimeMessage extends Message {
setEncoding(MimeUtil.ENC_QUOTED_PRINTABLE);
}
}
+
+ @Override
+ public String getServerExtra() {
+ return serverExtra;
+ }
+
+ @Override
+ public void setServerExtra(String serverExtra) {
+ this.serverExtra = serverExtra;
+ }
+
+
}
diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessageHelper.java b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessageHelper.java
index bc1695607..6cb1bd64f 100644
--- a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessageHelper.java
+++ b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessageHelper.java
@@ -23,9 +23,10 @@ public class MimeMessageHelper {
if (body instanceof Multipart) {
Multipart multipart = ((Multipart) body);
multipart.setParent(part);
- String type = multipart.getContentType();
- part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, type);
- if ("multipart/signed".equalsIgnoreCase(type)) {
+ String mimeType = multipart.getMimeType();
+ String contentType = String.format("%s; boundary=\"%s\"", mimeType, multipart.getBoundary());
+ part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType);
+ if ("multipart/signed".equalsIgnoreCase(mimeType)) {
setEncoding(part, MimeUtil.ENC_7BIT);
} else {
setEncoding(part, MimeUtil.ENC_8BIT);
diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMultipart.java b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMultipart.java
index d6ce4377a..ae4c52016 100644
--- a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMultipart.java
+++ b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMultipart.java
@@ -10,30 +10,26 @@ import java.util.Locale;
import java.util.Random;
public class MimeMultipart extends Multipart {
- private byte[] mPreamble;
- private byte[] mEpilogue;
-
- private String mContentType;
-
- private final String mBoundary;
+ private String mimeType;
+ private byte[] preamble;
+ private byte[] epilogue;
+ private final String boundary;
public MimeMultipart() throws MessagingException {
- mBoundary = generateBoundary();
+ boundary = generateBoundary();
setSubType("mixed");
}
- public MimeMultipart(String contentType) throws MessagingException {
- this.mContentType = contentType;
- try {
- mBoundary = MimeUtility.getHeaderParameter(contentType, "boundary");
- if (mBoundary == null) {
- throw new MessagingException("MultiPart does not contain boundary: " + contentType);
- }
- } catch (Exception e) {
- throw new MessagingException(
- "Invalid MultiPart Content-Type; must contain subtype and boundary. ("
- + contentType + ")", e);
+ public MimeMultipart(String mimeType, String boundary) throws MessagingException {
+ if (mimeType == null) {
+ throw new IllegalArgumentException("mimeType can't be null");
}
+ if (boundary == null) {
+ throw new IllegalArgumentException("boundary can't be null");
+ }
+
+ this.mimeType = mimeType;
+ this.boundary = boundary;
}
public String generateBoundary() {
@@ -46,40 +42,53 @@ public class MimeMultipart extends Multipart {
return sb.toString().toUpperCase(Locale.US);
}
+ @Override
+ public String getBoundary() {
+ return boundary;
+ }
+
+ public byte[] getPreamble() {
+ return preamble;
+ }
+
public void setPreamble(byte[] preamble) {
- this.mPreamble = preamble;
+ this.preamble = preamble;
+ }
+
+ public byte[] getEpilogue() {
+ return epilogue;
}
public void setEpilogue(byte[] epilogue) {
- mEpilogue = epilogue;
+ this.epilogue = epilogue;
}
@Override
- public String getContentType() {
- return mContentType;
+ public String getMimeType() {
+ return mimeType;
}
public void setSubType(String subType) {
- mContentType = String.format("multipart/%s; boundary=\"%s\"", subType, mBoundary);
+ mimeType = "multipart/" + subType;
}
@Override
public void writeTo(OutputStream out) throws IOException, MessagingException {
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
- if (mPreamble != null) {
- out.write(mPreamble);
+ if (preamble != null) {
+ out.write(preamble);
writer.write("\r\n");
}
if (getBodyParts().isEmpty()) {
writer.write("--");
- writer.write(mBoundary);
+ writer.write(boundary);
writer.write("\r\n");
} else {
for (BodyPart bodyPart : getBodyParts()) {
writer.write("--");
- writer.write(mBoundary);
+ writer.write(boundary);
writer.write("\r\n");
writer.flush();
bodyPart.writeTo(out);
@@ -88,11 +97,11 @@ public class MimeMultipart extends Multipart {
}
writer.write("--");
- writer.write(mBoundary);
+ writer.write(boundary);
writer.write("--\r\n");
writer.flush();
- if (mEpilogue != null) {
- out.write(mEpilogue);
+ if (epilogue != null) {
+ out.write(epilogue);
}
}
diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeUtility.java b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeUtility.java
index bfde9e0b4..ee68fe449 100644
--- a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeUtility.java
+++ b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeUtility.java
@@ -16,10 +16,7 @@ import org.apache.james.mime4j.util.MimeUtil;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
-import java.util.ArrayList;
-import java.util.List;
import java.util.Locale;
-import java.util.Set;
import java.util.regex.Pattern;
@@ -1029,7 +1026,7 @@ public class MimeUtility {
@Override
public void close() throws IOException {
super.close();
- rawInputStream.close();
+ closeInputStreamWithoutDeletingTemporaryFiles(rawInputStream);
}
};
} else if (MimeUtil.ENC_QUOTED_PRINTABLE.equalsIgnoreCase(encoding)) {
@@ -1037,7 +1034,7 @@ public class MimeUtility {
@Override
public void close() throws IOException {
super.close();
- rawInputStream.close();
+ closeInputStreamWithoutDeletingTemporaryFiles(rawInputStream);
}
};
} else {
@@ -1050,6 +1047,14 @@ public class MimeUtility {
return inputStream;
}
+ public static void closeInputStreamWithoutDeletingTemporaryFiles(InputStream rawInputStream) throws IOException {
+ if (rawInputStream instanceof BinaryTempFileBody.BinaryTempFileBodyInputStream) {
+ ((BinaryTempFileBody.BinaryTempFileBodyInputStream) rawInputStream).closeWithoutDeleting();
+ } else {
+ rawInputStream.close();
+ }
+ }
+
public static String getMimeTypeByExtension(String filename) {
String returnedType = null;
String extension = null;
diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/SizeAware.java b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/SizeAware.java
new file mode 100644
index 000000000..07d5fdef9
--- /dev/null
+++ b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/SizeAware.java
@@ -0,0 +1,6 @@
+package com.fsck.k9.mail.internet;
+
+
+public interface SizeAware {
+ long getSize();
+}
diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/TextBody.java b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/TextBody.java
index d6355662f..84162bd41 100644
--- a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/TextBody.java
+++ b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/TextBody.java
@@ -10,10 +10,11 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
+import com.fsck.k9.mail.filter.CountingOutputStream;
import org.apache.james.mime4j.codec.QuotedPrintableOutputStream;
import org.apache.james.mime4j.util.MimeUtil;
-public class TextBody implements Body {
+public class TextBody implements Body, SizeAware {
/**
* Immutable empty byte array
@@ -98,4 +99,33 @@ public class TextBody implements Body {
public void setComposedMessageOffset(Integer composedMessageOffset) {
this.mComposedMessageOffset = composedMessageOffset;
}
+
+ @Override
+ public long getSize() {
+ try {
+ byte[] bytes = mBody.getBytes(mCharset);
+
+ if (MimeUtil.ENC_8BIT.equalsIgnoreCase(mEncoding)) {
+ return bytes.length;
+ } else {
+ return getLengthWhenQuotedPrintableEncoded(bytes);
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("Couldn't get body size", e);
+ }
+ }
+
+ private long getLengthWhenQuotedPrintableEncoded(byte[] bytes) throws IOException {
+ CountingOutputStream countingOutputStream = new CountingOutputStream();
+ OutputStream quotedPrintableOutputStream = new QuotedPrintableOutputStream(countingOutputStream, false);
+ try {
+ quotedPrintableOutputStream.write(bytes);
+ } finally {
+ try {
+ quotedPrintableOutputStream.close();
+ } catch (IOException e) { /* ignore */ }
+ }
+
+ return countingOutputStream.getCount();
+ }
}
diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/message/MessageHeaderParser.java b/k9mail-library/src/main/java/com/fsck/k9/mail/message/MessageHeaderParser.java
new file mode 100644
index 000000000..758158ee1
--- /dev/null
+++ b/k9mail-library/src/main/java/com/fsck/k9/mail/message/MessageHeaderParser.java
@@ -0,0 +1,118 @@
+package com.fsck.k9.mail.message;
+
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import com.fsck.k9.mail.MessagingException;
+import com.fsck.k9.mail.Part;
+import org.apache.james.mime4j.MimeException;
+import org.apache.james.mime4j.parser.ContentHandler;
+import org.apache.james.mime4j.parser.MimeStreamParser;
+import org.apache.james.mime4j.stream.BodyDescriptor;
+import org.apache.james.mime4j.stream.Field;
+import org.apache.james.mime4j.stream.MimeConfig;
+
+
+public class MessageHeaderParser {
+
+ public static void parse(final Part part, InputStream headerInputStream) throws MessagingException {
+ MimeStreamParser parser = getMimeStreamParser();
+ parser.setContentHandler(new MessageHeaderParserContentHandler(part));
+
+ try {
+ parser.parse(headerInputStream);
+ } catch (MimeException me) {
+ throw new MessagingException("Error parsing headers", me);
+ } catch (IOException e) {
+ throw new MessagingException("I/O error parsing headers", e);
+ }
+ }
+
+ private static MimeStreamParser getMimeStreamParser() {
+ MimeConfig parserConfig = new MimeConfig();
+ parserConfig.setMaxHeaderLen(-1);
+ parserConfig.setMaxLineLen(-1);
+ parserConfig.setMaxHeaderCount(-1);
+ return new MimeStreamParser(parserConfig);
+ }
+
+ private static class MessageHeaderParserContentHandler implements ContentHandler {
+ private final Part part;
+
+ public MessageHeaderParserContentHandler(Part part) {
+ this.part = part;
+ }
+
+ @Override
+ public void field(Field rawField) throws MimeException {
+ String name = rawField.getName();
+ String raw = rawField.getRaw().toString();
+ try {
+ part.addRawHeader(name, raw);
+ } catch (MessagingException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void startMessage() throws MimeException {
+ /* do nothing */
+ }
+
+ @Override
+ public void endMessage() throws MimeException {
+ /* do nothing */
+ }
+
+ @Override
+ public void startBodyPart() throws MimeException {
+ /* do nothing */
+ }
+
+ @Override
+ public void endBodyPart() throws MimeException {
+ /* do nothing */
+ }
+
+ @Override
+ public void startHeader() throws MimeException {
+ /* do nothing */
+ }
+
+ @Override
+ public void endHeader() throws MimeException {
+ /* do nothing */
+ }
+
+ @Override
+ public void preamble(InputStream is) throws MimeException, IOException {
+ /* do nothing */
+ }
+
+ @Override
+ public void epilogue(InputStream is) throws MimeException, IOException {
+ /* do nothing */
+ }
+
+ @Override
+ public void startMultipart(BodyDescriptor bd) throws MimeException {
+ /* do nothing */
+ }
+
+ @Override
+ public void endMultipart() throws MimeException {
+ /* do nothing */
+ }
+
+ @Override
+ public void body(BodyDescriptor bd, InputStream is) throws MimeException, IOException {
+ /* do nothing */
+ }
+
+ @Override
+ public void raw(InputStream is) throws MimeException, IOException {
+ /* do nothing */
+ }
+ }
+}
diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/store/imap/ImapStore.java b/k9mail-library/src/main/java/com/fsck/k9/mail/store/imap/ImapStore.java
index c42139367..e02d84d84 100644
--- a/k9mail-library/src/main/java/com/fsck/k9/mail/store/imap/ImapStore.java
+++ b/k9mail-library/src/main/java/com/fsck/k9/mail/store/imap/ImapStore.java
@@ -1456,13 +1456,9 @@ public class ImapStore extends RemoteStore {
throws MessagingException {
checkOpen(); //only need READ access
- String[] parts = part.getHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA);
- if (parts == null) {
- return;
- }
+ String partId = part.getServerExtra();
String fetch;
- String partId = parts[0];
if ("TEXT".equalsIgnoreCase(partId)) {
fetch = String.format(Locale.US, "BODY.PEEK[TEXT]<0.%d>",
mStoreConfig.getMaximumAutoDownloadMessageSize());
@@ -1715,7 +1711,7 @@ public class ImapStore extends RemoteStore {
break;
}
}
- part.setBody(mp);
+ MimeMessageHelper.setBody(part, mp);
} else {
/*
* This is a body. We need to add as much information as we can find out about
@@ -1835,7 +1831,7 @@ public class ImapStore extends RemoteStore {
if (part instanceof ImapMessage) {
((ImapMessage) part).setSize(size);
}
- part.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, id);
+ part.setServerExtra(id);
}
}
diff --git a/k9mail/build.gradle b/k9mail/build.gradle
index 8e935d7b7..39d36a55c 100644
--- a/k9mail/build.gradle
+++ b/k9mail/build.gradle
@@ -20,6 +20,7 @@ dependencies {
compile 'com.android.support:support-v13:21.0.2'
compile 'net.sourceforge.htmlcleaner:htmlcleaner:2.10'
compile 'de.cketti.library.changelog:ckchangelog:1.2.1'
+ compile 'com.github.bumptech.glide:glide:3.4.0'
androidTestCompile 'com.android.support.test:testing-support-lib:0.1'
androidTestCompile 'com.android.support.test.espresso:espresso-core:2.0'
diff --git a/k9mail/src/androidTest/java/com/fsck/k9/crypto/MessageDecryptVerifierTest.java b/k9mail/src/androidTest/java/com/fsck/k9/crypto/MessageDecryptVerifierTest.java
new file mode 100644
index 000000000..e06bd7971
--- /dev/null
+++ b/k9mail/src/androidTest/java/com/fsck/k9/crypto/MessageDecryptVerifierTest.java
@@ -0,0 +1,78 @@
+package com.fsck.k9.crypto;
+
+
+import java.util.List;
+
+import android.support.test.runner.AndroidJUnit4;
+
+import com.fsck.k9.mail.Part;
+import com.fsck.k9.mail.internet.MimeBodyPart;
+import com.fsck.k9.mail.internet.MimeMessage;
+import com.fsck.k9.mail.internet.MimeMessageHelper;
+import com.fsck.k9.mail.internet.MimeMultipart;
+import com.fsck.k9.mail.internet.TextBody;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertSame;
+
+
+@RunWith(AndroidJUnit4.class)
+public class MessageDecryptVerifierTest {
+
+ @Test
+ public void findEncryptedPartsShouldReturnEmptyListForEmptyMessage() throws Exception {
+ MimeMessage emptyMessage = new MimeMessage();
+
+ List encryptedParts = MessageDecryptVerifier.findEncryptedParts(emptyMessage);
+ assertEquals(0, encryptedParts.size());
+ }
+
+ @Test
+ public void findEncryptedPartsShouldReturnEmptyListForSimpleMessage() throws Exception {
+ MimeMessage message = new MimeMessage();
+ message.setBody(new TextBody("message text"));
+
+ List encryptedParts = MessageDecryptVerifier.findEncryptedParts(message);
+ assertEquals(0, encryptedParts.size());
+ }
+
+ @Test
+ public void findEncryptedPartsShouldReturnEmptyEncryptedPart() throws Exception {
+ MimeMessage message = new MimeMessage();
+ MimeMultipart mulitpartEncrypted = new MimeMultipart();
+ mulitpartEncrypted.setSubType("encrypted");
+ MimeMessageHelper.setBody(message, mulitpartEncrypted);
+
+ List encryptedParts = MessageDecryptVerifier.findEncryptedParts(message);
+ assertEquals(1, encryptedParts.size());
+ assertSame(message, encryptedParts.get(0));
+ }
+
+ @Test
+ public void findEncryptedPartsShouldReturnMultipleEncryptedParts() throws Exception {
+ MimeMessage message = new MimeMessage();
+ MimeMultipart multipartMixed = new MimeMultipart();
+ multipartMixed.setSubType("mixed");
+ MimeMessageHelper.setBody(message, multipartMixed);
+
+ MimeMultipart mulitpartEncryptedOne = new MimeMultipart();
+ mulitpartEncryptedOne.setSubType("encrypted");
+ MimeBodyPart bodyPartOne = new MimeBodyPart(mulitpartEncryptedOne);
+ multipartMixed.addBodyPart(bodyPartOne);
+
+ MimeBodyPart bodyPartTwo = new MimeBodyPart(null, "text/plain");
+ multipartMixed.addBodyPart(bodyPartTwo);
+
+ MimeMultipart mulitpartEncryptedThree = new MimeMultipart();
+ mulitpartEncryptedThree.setSubType("encrypted");
+ MimeBodyPart bodyPartThree = new MimeBodyPart(mulitpartEncryptedThree);
+ multipartMixed.addBodyPart(bodyPartThree);
+
+ List encryptedParts = MessageDecryptVerifier.findEncryptedParts(message);
+ assertEquals(2, encryptedParts.size());
+ assertSame(bodyPartOne, encryptedParts.get(0));
+ assertSame(bodyPartThree, encryptedParts.get(1));
+ }
+}
diff --git a/k9mail/src/androidTest/java/com/fsck/k9/mailstore/LocalMessageExtractorTest.java b/k9mail/src/androidTest/java/com/fsck/k9/mailstore/LocalMessageExtractorTest.java
index 6086118af..d3e7f40d2 100644
--- a/k9mail/src/androidTest/java/com/fsck/k9/mailstore/LocalMessageExtractorTest.java
+++ b/k9mail/src/androidTest/java/com/fsck/k9/mailstore/LocalMessageExtractorTest.java
@@ -1,6 +1,8 @@
package com.fsck.k9.mailstore;
+import java.util.ArrayList;
import java.util.Date;
+import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
@@ -11,11 +13,14 @@ import com.fsck.k9.activity.K9ActivityCommon;
import com.fsck.k9.mail.Address;
import com.fsck.k9.mail.Message.RecipientType;
import com.fsck.k9.mail.MessagingException;
+import com.fsck.k9.mail.Part;
+import com.fsck.k9.mail.internet.MessageExtractor;
import com.fsck.k9.mail.internet.MimeBodyPart;
import com.fsck.k9.mail.internet.MimeMessage;
import com.fsck.k9.mail.internet.MimeMessageHelper;
import com.fsck.k9.mail.internet.MimeMultipart;
import com.fsck.k9.mail.internet.TextBody;
+import com.fsck.k9.mail.internet.Viewable;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -38,7 +43,10 @@ public class LocalMessageExtractorTest {
MimeMessageHelper.setBody(message, body);
// Extract text
- ViewableContainer container = extractTextAndAttachments(InstrumentationRegistry.getTargetContext(), message);
+ List attachments = new ArrayList();
+ List viewables = MessageExtractor.getViewables(message, attachments);
+ ViewableContainer container = extractTextAndAttachments(InstrumentationRegistry.getTargetContext(),
+ viewables, attachments);
String expectedText = bodyText;
String expectedHtml =
@@ -63,7 +71,10 @@ public class LocalMessageExtractorTest {
MimeMessageHelper.setBody(message, body);
// Extract text
- ViewableContainer container = extractTextAndAttachments(InstrumentationRegistry.getTargetContext(), message);
+ List attachments = new ArrayList();
+ List viewables = MessageExtractor.getViewables(message, attachments);
+ ViewableContainer container = extractTextAndAttachments(InstrumentationRegistry.getTargetContext(),
+ viewables, attachments);
String expectedText = "K-9 Mail rocks :>";
String expectedHtml =
@@ -94,7 +105,10 @@ public class LocalMessageExtractorTest {
MimeMessageHelper.setBody(message, multipart);
// Extract text
- ViewableContainer container = extractTextAndAttachments(InstrumentationRegistry.getTargetContext(), message);
+ List attachments = new ArrayList();
+ List viewables = MessageExtractor.getViewables(message, attachments);
+ ViewableContainer container = extractTextAndAttachments(InstrumentationRegistry.getTargetContext(),
+ viewables, attachments);
String expectedText =
bodyText1 + "\r\n\r\n" +
@@ -151,7 +165,10 @@ public class LocalMessageExtractorTest {
MimeMessageHelper.setBody(message, multipart);
// Extract text
- ViewableContainer container = extractTextAndAttachments(InstrumentationRegistry.getTargetContext(), message);
+ List attachments = new ArrayList();
+ List viewables = MessageExtractor.getViewables(message, attachments);
+ ViewableContainer container = extractTextAndAttachments(InstrumentationRegistry.getTargetContext(),
+ viewables, attachments);
String expectedText =
bodyText +
diff --git a/k9mail/src/androidTest/java/com/fsck/k9/mailstore/LocalMessageTest.java b/k9mail/src/androidTest/java/com/fsck/k9/mailstore/LocalMessageTest.java
deleted file mode 100644
index e37c12da7..000000000
--- a/k9mail/src/androidTest/java/com/fsck/k9/mailstore/LocalMessageTest.java
+++ /dev/null
@@ -1,64 +0,0 @@
-package com.fsck.k9.mailstore;
-
-
-import android.content.Context;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.runner.AndroidJUnit4;
-
-import com.fsck.k9.Account;
-import com.fsck.k9.Preferences;
-import com.fsck.k9.mail.internet.MimeBodyPart;
-import com.fsck.k9.mail.internet.MimeMultipart;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import static junit.framework.Assert.assertEquals;
-
-
-@RunWith(AndroidJUnit4.class)
-public class LocalMessageTest {
- private LocalMessage message;
- private Account account;
- private Preferences preferences;
-
- @Before
- public void setUp() throws Exception {
- Context targetContext = InstrumentationRegistry.getTargetContext();
- preferences = Preferences.getPreferences(targetContext);
- account = preferences.newAccount();
- LocalStore store = LocalStore.getInstance(account, targetContext);
- message = new LocalMessage(store, "uid", new LocalFolder(store, "test"));
- }
-
- @After
- public void tearDown() throws Exception {
- preferences.deleteAccount(account);
- }
-
- @Test
- public void testGetDisplayTextWithPlainTextPart() throws Exception {
- String textBodyText = "text body";
-
- MimeMultipart multipart = new MimeMultipart();
- MimeBodyPart bodyPart1 = new MimeBodyPart(new LocalTextBody(textBodyText, textBodyText), "text/plain");
- multipart.addBodyPart(bodyPart1);
- message.setBody(multipart);
- assertEquals("text body", message.getTextForDisplay());
- }
-
- @Test
- public void testGetDisplayTextWithHtmlPart() throws Exception {
- String htmlBodyText = "html body";
- String textBodyText = "text body";
-
- MimeMultipart multipart = new MimeMultipart();
- MimeBodyPart bodyPart1 = new MimeBodyPart(new LocalTextBody(htmlBodyText, htmlBodyText), "text/html");
- MimeBodyPart bodyPart2 = new MimeBodyPart(new LocalTextBody(textBodyText, textBodyText), "text/plain");
- multipart.addBodyPart(bodyPart1);
- multipart.addBodyPart(bodyPart2);
- message.setBody(multipart);
- assertEquals("html body", message.getTextForDisplay());
- }
-}
diff --git a/k9mail/src/androidTest/java/com/fsck/k9/mailstore/MessageInfoExtractorTest.java b/k9mail/src/androidTest/java/com/fsck/k9/mailstore/MessageInfoExtractorTest.java
new file mode 100644
index 000000000..3a7539240
--- /dev/null
+++ b/k9mail/src/androidTest/java/com/fsck/k9/mailstore/MessageInfoExtractorTest.java
@@ -0,0 +1,141 @@
+package com.fsck.k9.mailstore;
+
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.fsck.k9.mail.MessagingException;
+import com.fsck.k9.mail.internet.MimeBodyPart;
+import com.fsck.k9.mail.internet.MimeMessage;
+import com.fsck.k9.mail.internet.MimeMultipart;
+import com.fsck.k9.mail.internet.TextBody;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertEquals;
+
+
+@RunWith(AndroidJUnit4.class)
+public class MessageInfoExtractorTest {
+
+ @Test
+ public void shouldExtractPreviewFromSinglePlainTextPart() throws MessagingException {
+ MimeMessage message = new MimeMessage();
+ message.addHeader("Content-Type", "text/plain");
+ TextBody body = new TextBody("Message text ");
+ message.setBody(body);
+
+ String preview = new MessageInfoExtractor(getContext(), message).getMessageTextPreview();
+
+ assertEquals("Message text", preview);
+ }
+
+ @Test
+ public void shouldLimitPreviewTo512Characters() throws MessagingException {
+ MimeMessage message = new MimeMessage();
+ message.addHeader("Content-Type", "text/plain");
+ TextBody body = new TextBody("10--------20--------30--------40--------50--------" +
+ "60--------70--------80--------90--------100-------" +
+ "110-------120-------130-------140-------150-------" +
+ "160-------170-------180-------190-------200-------" +
+ "210-------220-------230-------240-------250-------" +
+ "260-------270-------280-------290-------300-------" +
+ "310-------320-------330-------340-------350-------" +
+ "360-------370-------380-------390-------400-------" +
+ "410-------420-------430-------440-------450-------" +
+ "460-------470-------480-------490-------500-------" +
+ "510-------520-------530-------540-------550-------" +
+ "560-------570-------580-------590-------600-------");
+ message.setBody(body);
+
+ String preview = new MessageInfoExtractor(getContext(), message).getMessageTextPreview();
+
+ assertEquals(512, preview.length());
+ assertEquals('…', preview.charAt(511));
+ }
+
+ @Test
+ public void shouldExtractPreviewFromSingleHtmlPart() throws MessagingException {
+ MimeMessage message = new MimeMessage();
+ message.addHeader("Content-Type", "text/html");
+ TextBody body = new TextBody("
Message text
");
+ message.setBody(body);
+
+ String preview = new MessageInfoExtractor(getContext(), message).getMessageTextPreview();
+
+ assertEquals("Message text", preview);
+ }
+
+ @Test
+ public void shouldExtractPreviewFromMultipartAlternative() throws MessagingException {
+ MimeMessage message = new MimeMessage();
+ message.addHeader("Content-Type", "multipart/alternative");
+ MimeMultipart multipart = new MimeMultipart();
+ multipart.setSubType("alternative");
+ message.setBody(multipart);
+
+ TextBody textBody = new TextBody("text");
+ MimeBodyPart textPart = new MimeBodyPart(textBody, "text/plain");
+ multipart.addBodyPart(textPart);
+
+ TextBody htmlBody = new TextBody("html");
+ MimeBodyPart htmlPart = new MimeBodyPart(htmlBody, "text/html");
+ multipart.addBodyPart(htmlPart);
+
+ String preview = new MessageInfoExtractor(getContext(), message).getMessageTextPreview();
+
+ assertEquals("text", preview);
+ }
+
+ @Test
+ public void shouldExtractPreviewFromMultipartMixed() throws MessagingException {
+ MimeMessage message = new MimeMessage();
+ message.addHeader("Content-Type", "multipart/mixed");
+ MimeMultipart multipart = new MimeMultipart();
+ multipart.setSubType("mixed");
+ message.setBody(multipart);
+
+ TextBody textBody = new TextBody("text");
+ MimeBodyPart textPart = new MimeBodyPart(textBody, "text/plain");
+ multipart.addBodyPart(textPart);
+
+ TextBody htmlBody = new TextBody("html");
+ MimeBodyPart htmlPart = new MimeBodyPart(htmlBody, "text/html");
+ multipart.addBodyPart(htmlPart);
+
+ String preview = new MessageInfoExtractor(getContext(), message).getMessageTextPreview();
+
+ assertEquals("text / html", preview);
+ }
+
+ @Test
+ public void shouldExtractPreviewFromMultipartMixedWithInnerMesssage() throws MessagingException {
+ MimeMessage message = new MimeMessage();
+ message.addHeader("Content-Type", "multipart/mixed");
+ MimeMultipart multipart = new MimeMultipart();
+ multipart.setSubType("mixed");
+ message.setBody(multipart);
+
+ TextBody textBody = new TextBody("text");
+ MimeBodyPart textPart = new MimeBodyPart(textBody, "text/plain");
+ multipart.addBodyPart(textPart);
+
+ MimeMessage innerMessage = new MimeMessage();
+ innerMessage.addHeader("Content-Type", "text/html");
+ innerMessage.addHeader("Subject", "inner message");
+ TextBody htmlBody = new TextBody("html");
+ innerMessage.setBody(htmlBody);
+
+ MimeBodyPart messagePart = new MimeBodyPart(innerMessage, "message/rfc822");
+ multipart.addBodyPart(messagePart);
+
+ String preview = new MessageInfoExtractor(getContext(), message).getMessageTextPreview();
+
+ assertEquals("text / Includes message titled \"inner message\" containing: html", preview);
+ }
+
+ private Context getContext() {
+ return InstrumentationRegistry.getTargetContext();
+ }
+}
diff --git a/k9mail/src/androidTest/java/com/fsck/k9/mailstore/ReconstructMessageFromDatabaseTest.java b/k9mail/src/androidTest/java/com/fsck/k9/mailstore/ReconstructMessageFromDatabaseTest.java
new file mode 100644
index 000000000..3b2c272e4
--- /dev/null
+++ b/k9mail/src/androidTest/java/com/fsck/k9/mailstore/ReconstructMessageFromDatabaseTest.java
@@ -0,0 +1,176 @@
+package com.fsck.k9.mailstore;
+
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collections;
+
+import android.content.Context;
+import android.test.ApplicationTestCase;
+import android.test.RenamingDelegatingContext;
+
+import com.fsck.k9.Account;
+import com.fsck.k9.K9;
+import com.fsck.k9.mail.Body;
+import com.fsck.k9.mail.FetchProfile;
+import com.fsck.k9.mail.MessagingException;
+import com.fsck.k9.mail.internet.BinaryTempFileBody;
+import com.fsck.k9.mail.internet.MimeMessage;
+import org.apache.james.mime4j.util.MimeUtil;
+
+
+public class ReconstructMessageFromDatabaseTest extends ApplicationTestCase {
+
+ public static final String MESSAGE_SOURCE = "From: from@example.com\r\n" +
+ "To: to@example.com\r\n" +
+ "Subject: Test Message \r\n" +
+ "Date: Thu, 13 Nov 2014 17:09:38 +0100\r\n" +
+ "Content-Type: multipart/mixed;\r\n" +
+ " boundary=\"----Boundary\"\r\n" +
+ "Content-Transfer-Encoding: 8bit\r\n" +
+ "MIME-Version: 1.0\r\n" +
+ "\r\n" +
+ "This is a multipart MIME message.\r\n" +
+ "------Boundary\r\n" +
+ "Content-Type: text/plain; charset=utf-8\r\n" +
+ "Content-Transfer-Encoding: 8bit\r\n" +
+ "\r\n" +
+ "Testing.\r\n" +
+ "This is a text body with some greek characters.\r\n" +
+ "αβγδεζηθ\r\n" +
+ "End of test.\r\n" +
+ "\r\n" +
+ "------Boundary\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "Content-Transfer-Encoding: base64\r\n" +
+ "\r\n" +
+ "VGhpcyBpcyBhIHRl\r\n" +
+ "c3QgbWVzc2FnZQ==\r\n" +
+ "\r\n" +
+ "------Boundary--\r\n" +
+ "Hi, I'm the epilogue";
+
+ private Account account;
+
+ public ReconstructMessageFromDatabaseTest() {
+ super(K9.class);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ RenamingDelegatingContext context = new RenamingDelegatingContext(getContext(), "db-test-");
+ setContext(context);
+
+ BinaryTempFileBody.setTempDirectory(context.getCacheDir());
+
+ createApplication();
+
+ createDummyAccount(context);
+ }
+
+ private void createDummyAccount(Context context) {
+ account = new DummyAccount(context);
+ }
+
+ public void testThatByteIdenticalCopyOfMessageIsReconstructed() throws IOException, MessagingException {
+
+ LocalFolder folder = createFolderInDatabase();
+
+ MimeMessage message = parseMessage();
+
+ saveMessageToDatabase(folder, message);
+
+ LocalMessage localMessage = readMessageFromDatabase(folder, message);
+
+ String reconstructedMessage = writeMessageToString(localMessage);
+
+ assertEquals(MESSAGE_SOURCE, reconstructedMessage);
+ }
+
+ public void testAddMissingPart() throws MessagingException, IOException {
+ LocalFolder folder = createFolderInDatabase();
+
+ MimeMessage message = new MimeMessage();
+ message.addHeader("To", "to@example.com");
+ message.addHeader("MIME-Version", "1.0");
+ message.addHeader("Content-Type", "text/plain");
+ message.setServerExtra("text");
+
+ saveMessageToDatabase(folder, message);
+
+ LocalMessage localMessage = readMessageFromDatabase(folder, message);
+
+ assertEquals("to@example.com", localMessage.getHeader("To")[0]);
+ assertEquals("text/plain", localMessage.getMimeType());
+ assertEquals("text", localMessage.getServerExtra());
+ assertNull(localMessage.getBody());
+
+ Body body = new BinaryMemoryBody("Test message body".getBytes(), MimeUtil.ENC_7BIT);
+ localMessage.setBody(body);
+ folder.addPartToMessage(localMessage, localMessage);
+
+ LocalMessage completeLocalMessage = readMessageFromDatabase(folder, message);
+ String reconstructedMessage = writeMessageToString(completeLocalMessage);
+
+ assertEquals("To: to@example.com\r\n" +
+ "MIME-Version: 1.0\r\n" +
+ "Content-Type: text/plain\r\n" +
+ "\r\n" +
+ "Test message body",
+ reconstructedMessage);
+ }
+
+ protected MimeMessage parseMessage() throws IOException, MessagingException {
+ InputStream messageInputStream = new ByteArrayInputStream(MESSAGE_SOURCE.getBytes());
+ try {
+ return new MimeMessage(messageInputStream, true);
+ } finally {
+ messageInputStream.close();
+ }
+ }
+
+ protected LocalFolder createFolderInDatabase() throws MessagingException {
+ LocalStore localStore = LocalStore.getInstance(account, getApplication());
+ LocalFolder inbox = localStore.getFolder("INBOX");
+ localStore.createFolders(Collections.singletonList(inbox), 10);
+ return inbox;
+ }
+
+ protected void saveMessageToDatabase(LocalFolder folder, MimeMessage message) throws MessagingException {
+ folder.appendMessages(Collections.singletonList(message));
+ }
+
+ protected LocalMessage readMessageFromDatabase(LocalFolder folder, MimeMessage message) throws MessagingException {
+ LocalMessage localMessage = folder.getMessage(message.getUid());
+
+ FetchProfile fp = new FetchProfile();
+ fp.add(FetchProfile.Item.ENVELOPE);
+ fp.add(FetchProfile.Item.BODY);
+ folder.fetch(Collections.singletonList(localMessage), fp, null);
+ folder.close();
+
+ return localMessage;
+ }
+
+ protected String writeMessageToString(LocalMessage localMessage) throws IOException, MessagingException {
+ ByteArrayOutputStream messageOutputStream = new ByteArrayOutputStream();
+ try {
+ localMessage.writeTo(messageOutputStream);
+ } finally {
+ messageOutputStream.close();
+ }
+
+ return new String(messageOutputStream.toByteArray());
+ }
+
+ static class DummyAccount extends Account {
+
+ protected DummyAccount(Context context) {
+ super(context);
+ }
+ }
+}
diff --git a/k9mail/src/main/AndroidManifest.xml b/k9mail/src/main/AndroidManifest.xml
index 34159eedd..ca890d986 100644
--- a/k9mail/src/main/AndroidManifest.xml
+++ b/k9mail/src/main/AndroidManifest.xml
@@ -415,5 +415,17 @@
android:authorities="${applicationId}.provider.email"
android:exported="false"/>
+
+
+
+
+
+
diff --git a/k9mail/src/main/java/com/fsck/k9/Account.java b/k9mail/src/main/java/com/fsck/k9/Account.java
index 3ea0d1fcd..73f280ec5 100644
--- a/k9mail/src/main/java/com/fsck/k9/Account.java
+++ b/k9mail/src/main/java/com/fsck/k9/Account.java
@@ -461,7 +461,8 @@ public class Account implements BaseAccount, StoreConfig {
mIsSignatureBeforeQuotedText = prefs.getBoolean(mUuid + ".signatureBeforeQuotedText", false);
identities = loadIdentities(prefs);
- mCryptoApp = prefs.getString(mUuid + ".cryptoApp", NO_OPENPGP_PROVIDER);
+ String cryptoApp = prefs.getString(mUuid + ".cryptoApp", NO_OPENPGP_PROVIDER);
+ setCryptoApp(cryptoApp);
mAllowRemoteSearch = prefs.getBoolean(mUuid + ".allowRemoteSearch", false);
mRemoteSearchFullText = prefs.getBoolean(mUuid + ".remoteSearchFullText", false);
mRemoteSearchNumResults = prefs.getInt(mUuid + ".remoteSearchNumResults", DEFAULT_REMOTE_SEARCH_NUM_RESULTS);
@@ -1597,7 +1598,11 @@ public class Account implements BaseAccount, StoreConfig {
}
public void setCryptoApp(String cryptoApp) {
- mCryptoApp = cryptoApp;
+ if (cryptoApp == null || cryptoApp.equals("apg")) {
+ mCryptoApp = NO_OPENPGP_PROVIDER;
+ } else {
+ mCryptoApp = cryptoApp;
+ }
}
public boolean allowRemoteSearch() {
@@ -1641,13 +1646,16 @@ public class Account implements BaseAccount, StoreConfig {
}
public synchronized String getOpenPgpProvider() {
- // return null if set to "APG" or "None"
- if (getCryptoApp().equals("apg") || getCryptoApp().equals("")) {
+ if (!isOpenPgpProviderConfigured()) {
return null;
}
return getCryptoApp();
}
+ public synchronized boolean isOpenPgpProviderConfigured() {
+ return !NO_OPENPGP_PROVIDER.equals(getCryptoApp());
+ }
+
public synchronized NotificationSetting getNotificationSetting() {
return mNotificationSetting;
}
diff --git a/k9mail/src/main/java/com/fsck/k9/activity/Accounts.java b/k9mail/src/main/java/com/fsck/k9/activity/Accounts.java
index 6454ebedf..2fe55a6a1 100644
--- a/k9mail/src/main/java/com/fsck/k9/activity/Accounts.java
+++ b/k9mail/src/main/java/com/fsck/k9/activity/Accounts.java
@@ -1283,7 +1283,8 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
new String[] {"HtmlCleaner", "http://htmlcleaner.sourceforge.net/"},
new String[] {"Android-PullToRefresh", "https://github.com/chrisbanes/Android-PullToRefresh"},
new String[] {"ckChangeLog", "https://github.com/cketti/ckChangeLog"},
- new String[] {"HoloColorPicker", "https://github.com/LarsWerkman/HoloColorPicker"}
+ new String[] {"HoloColorPicker", "https://github.com/LarsWerkman/HoloColorPicker"},
+ new String[] {"Glide", "https://github.com/bumptech/glide"}
};
private void onAbout() {
diff --git a/k9mail/src/main/java/com/fsck/k9/activity/MessageCompose.java b/k9mail/src/main/java/com/fsck/k9/activity/MessageCompose.java
index 0129b82e1..2bc8a0932 100644
--- a/k9mail/src/main/java/com/fsck/k9/activity/MessageCompose.java
+++ b/k9mail/src/main/java/com/fsck/k9/activity/MessageCompose.java
@@ -16,7 +16,6 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
-import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -43,7 +42,6 @@ import android.os.Parcelable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.util.Rfc822Tokenizer;
-import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.ContextThemeWrapper;
@@ -89,7 +87,7 @@ import com.fsck.k9.crypto.PgpData;
import com.fsck.k9.fragment.ProgressDialogFragment;
import com.fsck.k9.helper.ContactItem;
import com.fsck.k9.helper.Contacts;
-import com.fsck.k9.mail.filter.Base64;
+import com.fsck.k9.helper.SimpleTextWatcher;
import com.fsck.k9.helper.HtmlConverter;
import com.fsck.k9.helper.IdentityHelper;
import com.fsck.k9.helper.Utility;
@@ -102,21 +100,19 @@ import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Multipart;
import com.fsck.k9.mail.Part;
import com.fsck.k9.mail.internet.MessageExtractor;
-import com.fsck.k9.mail.internet.MimeBodyPart;
-import com.fsck.k9.mail.internet.MimeHeader;
import com.fsck.k9.mail.internet.MimeMessage;
-import com.fsck.k9.mail.internet.MimeMessageHelper;
-import com.fsck.k9.mail.internet.MimeMultipart;
import com.fsck.k9.mail.internet.MimeUtility;
import com.fsck.k9.mail.internet.TextBody;
-import com.fsck.k9.mailstore.LocalAttachmentBody;
import com.fsck.k9.mailstore.LocalMessage;
-import com.fsck.k9.mailstore.TempFileBody;
-import com.fsck.k9.mailstore.TempFileMessageBody;
+import com.fsck.k9.message.IdentityField;
+import com.fsck.k9.message.IdentityHeaderParser;
+import com.fsck.k9.message.InsertableHtmlContent;
+import com.fsck.k9.message.MessageBuilder;
+import com.fsck.k9.message.QuotedTextMode;
+import com.fsck.k9.message.SimpleMessageFormat;
+import com.fsck.k9.ui.EolConvertingEditText;
import com.fsck.k9.view.MessageWebView;
-import org.apache.james.mime4j.codec.EncoderUtil;
-import org.apache.james.mime4j.util.MimeUtil;
import org.htmlcleaner.CleanerProperties;
import org.htmlcleaner.HtmlCleaner;
import org.htmlcleaner.SimpleHtmlSerializer;
@@ -264,12 +260,6 @@ public class MessageCompose extends K9Activity implements OnClickListener,
*/
private Action mAction;
- private enum QuotedTextMode {
- NONE,
- SHOW,
- HIDE
- }
-
private boolean mReadReceipt = false;
private QuotedTextMode mQuotedTextMode = QuotedTextMode.NONE;
@@ -298,20 +288,14 @@ public class MessageCompose extends K9Activity implements OnClickListener,
private Button mQuotedTextShow;
private View mQuotedTextBar;
private ImageButton mQuotedTextEdit;
- private ImageButton mQuotedTextDelete;
private EolConvertingEditText mQuotedText;
private MessageWebView mQuotedHTML;
private InsertableHtmlContent mQuotedHtmlContent; // Container for HTML reply as it's being built.
- private View mEncryptLayout;
private CheckBox mCryptoSignatureCheckbox;
private CheckBox mEncryptCheckbox;
private TextView mCryptoSignatureUserId;
private TextView mCryptoSignatureUserIdRest;
- private ImageButton mAddToFromContacts;
- private ImageButton mAddCcFromContacts;
- private ImageButton mAddBccFromContacts;
-
private PgpData mPgpData = null;
private String mOpenPgpProvider;
private OpenPgpServiceConnection mOpenPgpServiceConnection;
@@ -322,11 +306,6 @@ public class MessageCompose extends K9Activity implements OnClickListener,
private boolean mSourceProcessed = false;
- enum SimpleMessageFormat {
- TEXT,
- HTML
- }
-
/**
* The currently used message format.
*
@@ -410,8 +389,6 @@ public class MessageCompose extends K9Activity implements OnClickListener,
};
private Listener mListener = new Listener();
- private EmailAddressAdapter mAddressAdapter;
- private Validator mAddressValidator;
private FontSizes mFontSizes = K9.getFontSizes();
private ContextThemeWrapper mThemeContext;
@@ -462,7 +439,6 @@ public class MessageCompose extends K9Activity implements OnClickListener,
* Compose a new message as a reply to the given message. If replyAll is true the function
* is reply all instead of simply reply.
* @param context
- * @param account
* @param message
* @param replyAll
* @param messageBody optional, for decrypted messages, null if it should be grabbed from the given message
@@ -522,11 +498,10 @@ public class MessageCompose extends K9Activity implements OnClickListener,
// theme the whole content according to the theme (except the action bar)
mThemeContext = new ContextThemeWrapper(this,
K9.getK9ThemeResourceId(K9.getK9ComposerTheme()));
- View v = ((LayoutInflater) mThemeContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE)).
- inflate(R.layout.message_compose, null);
+ View v = LayoutInflater.from(mThemeContext).inflate(R.layout.message_compose, null);
TypedValue outValue = new TypedValue();
// background color needs to be forced
- mThemeContext.getTheme().resolveAttribute(R.attr.messageViewHeaderBackgroundColor, outValue, true);
+ mThemeContext.getTheme().resolveAttribute(R.attr.messageViewBackgroundColor, outValue, true);
v.setBackgroundColor(outValue.data);
setContentView(v);
} else {
@@ -566,8 +541,8 @@ public class MessageCompose extends K9Activity implements OnClickListener,
mContacts = Contacts.getInstance(MessageCompose.this);
- mAddressAdapter = new EmailAddressAdapter(mThemeContext);
- mAddressValidator = new EmailAddressValidator();
+ EmailAddressAdapter mAddressAdapter = new EmailAddressAdapter(mThemeContext);
+ Validator mAddressValidator = new EmailAddressValidator();
mChooseIdentityButton = (Button) findViewById(R.id.identity);
mChooseIdentityButton.setOnClickListener(this);
@@ -583,9 +558,10 @@ public class MessageCompose extends K9Activity implements OnClickListener,
mSubjectView = (EditText) findViewById(R.id.subject);
mSubjectView.getInputExtras(true).putBoolean("allowEmoji", true);
- mAddToFromContacts = (ImageButton) findViewById(R.id.add_to);
- mAddCcFromContacts = (ImageButton) findViewById(R.id.add_cc);
- mAddBccFromContacts = (ImageButton) findViewById(R.id.add_bcc);
+ ImageButton mAddToFromContacts = (ImageButton) findViewById(R.id.add_to);
+ ImageButton mAddCcFromContacts = (ImageButton) findViewById(R.id.add_cc);
+ ImageButton mAddBccFromContacts = (ImageButton) findViewById(R.id.add_bcc);
+
mCcWrapper = (LinearLayout) findViewById(R.id.cc_wrapper);
mBccWrapper = (LinearLayout) findViewById(R.id.bcc_wrapper);
@@ -603,7 +579,7 @@ public class MessageCompose extends K9Activity implements OnClickListener,
mQuotedTextShow = (Button)findViewById(R.id.quoted_text_show);
mQuotedTextBar = findViewById(R.id.quoted_text_bar);
mQuotedTextEdit = (ImageButton)findViewById(R.id.quoted_text_edit);
- mQuotedTextDelete = (ImageButton)findViewById(R.id.quoted_text_delete);
+ ImageButton mQuotedTextDelete = (ImageButton) findViewById(R.id.quoted_text_delete);
mQuotedText = (EolConvertingEditText)findViewById(R.id.quoted_text);
mQuotedText.getInputExtras(true).putBoolean("allowEmoji", true);
@@ -618,81 +594,34 @@ public class MessageCompose extends K9Activity implements OnClickListener,
}
});
- TextWatcher watcher = new TextWatcher() {
- @Override
- public void beforeTextChanged(CharSequence s, int start, int before, int after) {
- /* do nothing */
- }
-
+ TextWatcher draftNeedsChangingTextWatcher = new SimpleTextWatcher() {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
mDraftNeedsSaving = true;
}
-
- @Override
- public void afterTextChanged(android.text.Editable s) { /* do nothing */ }
};
- // For watching changes to the To:, Cc:, and Bcc: fields for auto-encryption on a matching
- // address.
- TextWatcher recipientWatcher = new TextWatcher() {
- @Override
- public void beforeTextChanged(CharSequence s, int start, int before, int after) {
- /* do nothing */
- }
-
- @Override
- public void onTextChanged(CharSequence s, int start, int before, int count) {
- mDraftNeedsSaving = true;
- }
-
- @Override
- public void afterTextChanged(android.text.Editable s) {
- /* do nothing */
- }
- };
-
- TextWatcher sigwatcher = new TextWatcher() {
- @Override
- public void beforeTextChanged(CharSequence s, int start, int before, int after) {
- /* do nothing */
- }
-
+ TextWatcher signTextWatcher = new SimpleTextWatcher() {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
mDraftNeedsSaving = true;
mSignatureChanged = true;
}
-
- @Override
- public void afterTextChanged(android.text.Editable s) { /* do nothing */ }
};
- mToView.addTextChangedListener(recipientWatcher);
- mCcView.addTextChangedListener(recipientWatcher);
- mBccView.addTextChangedListener(recipientWatcher);
- mSubjectView.addTextChangedListener(watcher);
+ mToView.addTextChangedListener(draftNeedsChangingTextWatcher);
+ mCcView.addTextChangedListener(draftNeedsChangingTextWatcher);
+ mBccView.addTextChangedListener(draftNeedsChangingTextWatcher);
+ mSubjectView.addTextChangedListener(draftNeedsChangingTextWatcher);
- mMessageContentView.addTextChangedListener(watcher);
- mQuotedText.addTextChangedListener(watcher);
+ mMessageContentView.addTextChangedListener(draftNeedsChangingTextWatcher);
+ mQuotedText.addTextChangedListener(draftNeedsChangingTextWatcher);
- /* Yes, there really are poeple who ship versions of android without a contact picker */
+ /* Yes, there really are people who ship versions of android without a contact picker */
if (mContacts.hasContactPicker()) {
- mAddToFromContacts.setOnClickListener(new OnClickListener() {
- @Override public void onClick(View v) {
- doLaunchContactPicker(CONTACT_PICKER_TO);
- }
- });
- mAddCcFromContacts.setOnClickListener(new OnClickListener() {
- @Override public void onClick(View v) {
- doLaunchContactPicker(CONTACT_PICKER_CC);
- }
- });
- mAddBccFromContacts.setOnClickListener(new OnClickListener() {
- @Override public void onClick(View v) {
- doLaunchContactPicker(CONTACT_PICKER_BCC);
- }
- });
+ mAddToFromContacts.setOnClickListener(new DoLaunchOnClickListener(CONTACT_PICKER_TO));
+ mAddCcFromContacts.setOnClickListener(new DoLaunchOnClickListener(CONTACT_PICKER_CC));
+ mAddBccFromContacts.setOnClickListener(new DoLaunchOnClickListener(CONTACT_PICKER_BCC));
} else {
mAddToFromContacts.setVisibility(View.GONE);
mAddCcFromContacts.setVisibility(View.GONE);
@@ -762,7 +691,7 @@ public class MessageCompose extends K9Activity implements OnClickListener,
mSignatureView = lowerSignature;
upperSignature.setVisibility(View.GONE);
}
- mSignatureView.addTextChangedListener(sigwatcher);
+ mSignatureView.addTextChangedListener(signTextWatcher);
if (!mIdentity.getSignatureUse()) {
mSignatureView.setVisibility(View.GONE);
@@ -814,34 +743,31 @@ public class MessageCompose extends K9Activity implements OnClickListener,
mMessageReference.flag = Flag.FORWARDED;
}
- mEncryptLayout = findViewById(R.id.layout_encrypt);
- mCryptoSignatureCheckbox = (CheckBox)findViewById(R.id.cb_crypto_signature);
- mCryptoSignatureCheckbox.setOnCheckedChangeListener(new OnCheckedChangeListener() {
- @Override
- public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
- updateMessageFormat();
- }
- });
- mCryptoSignatureUserId = (TextView)findViewById(R.id.userId);
- mCryptoSignatureUserIdRest = (TextView)findViewById(R.id.userIdRest);
- mEncryptCheckbox = (CheckBox)findViewById(R.id.cb_encrypt);
- mEncryptCheckbox.setOnCheckedChangeListener(new OnCheckedChangeListener() {
- @Override
- public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
- updateMessageFormat();
- }
- });
-
- if (mSourceMessageBody != null) {
- // mSourceMessageBody is set to something when replying to and forwarding decrypted
- // messages, so the sender probably wants the message to be encrypted.
- mEncryptCheckbox.setChecked(true);
- }
+ final View mEncryptLayout = findViewById(R.id.layout_encrypt);
initializeCrypto();
mOpenPgpProvider = mAccount.getOpenPgpProvider();
- if (mOpenPgpProvider != null) {
+ if (isCryptoProviderEnabled()) {
+ mCryptoSignatureCheckbox = (CheckBox)findViewById(R.id.cb_crypto_signature);
+ final OnCheckedChangeListener updateListener = new OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ updateMessageFormat();
+ }
+ };
+ mCryptoSignatureCheckbox.setOnCheckedChangeListener(updateListener);
+ mCryptoSignatureUserId = (TextView)findViewById(R.id.userId);
+ mCryptoSignatureUserIdRest = (TextView)findViewById(R.id.userIdRest);
+ mEncryptCheckbox = (CheckBox)findViewById(R.id.cb_encrypt);
+ mEncryptCheckbox.setOnCheckedChangeListener(updateListener);
+
+ if (mSourceMessageBody != null) {
+ // mSourceMessageBody is set to something when replying to and forwarding decrypted
+ // messages, so the sender probably wants the message to be encrypted.
+ mEncryptCheckbox.setChecked(true);
+ }
+
// New OpenPGP Provider API
// bind to service
@@ -1037,6 +963,10 @@ public class MessageCompose extends K9Activity implements OnClickListener,
* Fill the encrypt layout with the latest data about signature key and encryption keys.
*/
public void updateEncryptLayout() {
+ if (!isCryptoProviderEnabled()) {
+ return;
+ }
+
if (!mPgpData.hasSignatureKey()) {
mCryptoSignatureCheckbox.setText(R.string.btn_crypto_sign);
mCryptoSignatureCheckbox.setChecked(false);
@@ -1093,16 +1023,10 @@ public class MessageCompose extends K9Activity implements OnClickListener,
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
- ArrayList attachments = new ArrayList();
- for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) {
- View view = mAttachments.getChildAt(i);
- Attachment attachment = (Attachment) view.getTag();
- attachments.add(attachment);
- }
outState.putInt(STATE_KEY_NUM_ATTACHMENTS_LOADING, mNumAttachmentsLoading);
outState.putString(STATE_KEY_WAITING_FOR_ATTACHMENTS, mWaitingForAttachments.name());
- outState.putParcelableArrayList(STATE_KEY_ATTACHMENTS, attachments);
+ outState.putParcelableArrayList(STATE_KEY_ATTACHMENTS, createAttachmentList());
outState.putBoolean(STATE_KEY_CC_SHOWN, mCcWrapper.getVisibility() == View.VISIBLE);
outState.putBoolean(STATE_KEY_BCC_SHOWN, mBccWrapper.getVisibility() == View.VISIBLE);
outState.putSerializable(STATE_KEY_QUOTED_TEXT_MODE, mQuotedTextMode);
@@ -1249,435 +1173,55 @@ public class MessageCompose extends K9Activity implements OnClickListener,
return Address.parseUnencoded(addresses.trim());
}
- /*
- * Build the Body that will contain the text of the message. We'll decide where to
- * include it later. Draft messages are treated somewhat differently in that signatures are not
- * appended and HTML separators between composed text and quoted text are not added.
- * @param isDraft If we should build a message that will be saved as a draft (as opposed to sent).
- */
private TextBody buildText(boolean isDraft) {
- return buildText(isDraft, mMessageFormat);
+ return createMessageBuilder(isDraft).buildText();
}
- /**
- * Build the {@link Body} that will contain the text of the message.
- *
- *
- * Draft messages are treated somewhat differently in that signatures are not appended and HTML
- * separators between composed text and quoted text are not added.
- *
- *
- * @param isDraft
- * If {@code true} we build a message that will be saved as a draft (as opposed to
- * sent).
- * @param messageFormat
- * Specifies what type of message to build ({@code text/plain} vs. {@code text/html}).
- *
- * @return {@link TextBody} instance that contains the entered text and possibly the quoted
- * original message.
- */
- private TextBody buildText(boolean isDraft, SimpleMessageFormat messageFormat) {
- String messageText = mMessageContentView.getCharacters();
-
- TextBodyBuilder textBodyBuilder = new TextBodyBuilder(messageText);
-
- /*
- * Find out if we need to include the original message as quoted text.
- *
- * We include the quoted text in the body if the user didn't choose to
- * hide it. We always include the quoted text when we're saving a draft.
- * That's so the user is able to "un-hide" the quoted text if (s)he
- * opens a saved draft.
- */
- boolean includeQuotedText = (isDraft || mQuotedTextMode == QuotedTextMode.SHOW);
- boolean isReplyAfterQuote = (mQuoteStyle == QuoteStyle.PREFIX && mAccount.isReplyAfterQuote());
-
- textBodyBuilder.setIncludeQuotedText(false);
- if (includeQuotedText) {
- if (messageFormat == SimpleMessageFormat.HTML && mQuotedHtmlContent != null) {
- textBodyBuilder.setIncludeQuotedText(true);
- textBodyBuilder.setQuotedTextHtml(mQuotedHtmlContent);
- textBodyBuilder.setReplyAfterQuote(isReplyAfterQuote);
- }
-
- String quotedText = mQuotedText.getCharacters();
- if (messageFormat == SimpleMessageFormat.TEXT && quotedText.length() > 0) {
- textBodyBuilder.setIncludeQuotedText(true);
- textBodyBuilder.setQuotedText(quotedText);
- textBodyBuilder.setReplyAfterQuote(isReplyAfterQuote);
- }
- }
-
- textBodyBuilder.setInsertSeparator(!isDraft);
-
- boolean useSignature = (!isDraft && mIdentity.getSignatureUse());
- if (useSignature) {
- textBodyBuilder.setAppendSignature(true);
- textBodyBuilder.setSignature(mSignatureView.getCharacters());
- textBodyBuilder.setSignatureBeforeQuotedText(mAccount.isSignatureBeforeQuotedText());
- } else {
- textBodyBuilder.setAppendSignature(false);
- }
-
- TextBody body;
- if (messageFormat == SimpleMessageFormat.HTML) {
- body = textBodyBuilder.buildTextHtml();
- } else {
- body = textBodyBuilder.buildTextPlain();
- }
- return body;
- }
- /**
- * Build the final message to be sent (or saved). If there is another message quoted in this one, it will be baked
- * into the final message here.
- * @param isDraft Indicates if this message is a draft or not. Drafts do not have signatures
- * appended and have some extra metadata baked into their header for use during thawing.
- * @return Message to be sent.
- * @throws MessagingException
- */
- private MimeMessage createMessage(boolean isDraft) throws MessagingException {
- MimeMessage message = new MimeMessage();
- message.addSentDate(new Date(), K9.hideTimeZone());
- Address from = new Address(mIdentity.getEmail(), mIdentity.getName());
- message.setFrom(from);
- message.setRecipients(RecipientType.TO, getAddresses(mToView));
- message.setRecipients(RecipientType.CC, getAddresses(mCcView));
- message.setRecipients(RecipientType.BCC, getAddresses(mBccView));
- message.setSubject(mSubjectView.getText().toString());
- if (mReadReceipt) {
- message.setHeader("Disposition-Notification-To", from.toEncodedString());
- message.setHeader("X-Confirm-Reading-To", from.toEncodedString());
- message.setHeader("Return-Receipt-To", from.toEncodedString());
- }
-
- if (!K9.hideUserAgent()) {
- message.setHeader("User-Agent", getString(R.string.message_header_mua));
- }
-
- final String replyTo = mIdentity.getReplyTo();
- if (replyTo != null) {
- message.setReplyTo(new Address[] { new Address(replyTo) });
- }
-
- if (mInReplyTo != null) {
- message.setInReplyTo(mInReplyTo);
- }
-
- if (mReferences != null) {
- message.setReferences(mReferences);
- }
-
- // Build the body.
- // TODO FIXME - body can be either an HTML or Text part, depending on whether we're in
- // HTML mode or not. Should probably fix this so we don't mix up html and text parts.
- TextBody body = null;
- if (mPgpData.getEncryptedData() != null) {
- String text = mPgpData.getEncryptedData();
- body = new TextBody(text);
- } else {
- body = buildText(isDraft);
- }
-
- // text/plain part when mMessageFormat == MessageFormat.HTML
- TextBody bodyPlain = null;
-
- final boolean hasAttachments = mAttachments.getChildCount() > 0;
-
- if (mMessageFormat == SimpleMessageFormat.HTML) {
- // HTML message (with alternative text part)
-
- // This is the compiled MIME part for an HTML message.
- MimeMultipart composedMimeMessage = new MimeMultipart();
- composedMimeMessage.setSubType("alternative"); // Let the receiver select either the text or the HTML part.
- composedMimeMessage.addBodyPart(new MimeBodyPart(body, "text/html"));
- bodyPlain = buildText(isDraft, SimpleMessageFormat.TEXT);
- composedMimeMessage.addBodyPart(new MimeBodyPart(bodyPlain, "text/plain"));
-
- if (hasAttachments) {
- // If we're HTML and have attachments, we have a MimeMultipart container to hold the
- // whole message (mp here), of which one part is a MimeMultipart container
- // (composedMimeMessage) with the user's composed messages, and subsequent parts for
- // the attachments.
- MimeMultipart mp = new MimeMultipart();
- mp.addBodyPart(new MimeBodyPart(composedMimeMessage));
- addAttachmentsToMessage(mp);
- MimeMessageHelper.setBody(message, mp);
- } else {
- // If no attachments, our multipart/alternative part is the only one we need.
- MimeMessageHelper.setBody(message, composedMimeMessage);
- }
- } else if (mMessageFormat == SimpleMessageFormat.TEXT) {
- // Text-only message.
- if (hasAttachments) {
- MimeMultipart mp = new MimeMultipart();
- mp.addBodyPart(new MimeBodyPart(body, "text/plain"));
- addAttachmentsToMessage(mp);
- MimeMessageHelper.setBody(message, mp);
- } else {
- // No attachments to include, just stick the text body in the message and call it good.
- MimeMessageHelper.setBody(message, body);
- }
- }
-
- // If this is a draft, add metadata for thawing.
- if (isDraft) {
- // Add the identity to the message.
- message.addHeader(K9.IDENTITY_HEADER, buildIdentityHeader(body, bodyPlain));
- }
-
- return message;
+ private MimeMessage createDraftMessage() throws MessagingException {
+ return createMessageBuilder(true).build();
}
- /**
- * Add attachments as parts into a MimeMultipart container.
- * @param mp MimeMultipart container in which to insert parts.
- * @throws MessagingException
- */
- private void addAttachmentsToMessage(final MimeMultipart mp) throws MessagingException {
- Body body;
+ private MimeMessage createMessage() throws MessagingException {
+ return createMessageBuilder(false).build();
+ }
+
+ private MessageBuilder createMessageBuilder(boolean isDraft) {
+ return new MessageBuilder(getApplicationContext())
+ .setSubject(mSubjectView.getText().toString())
+ .setTo(getAddresses(mToView))
+ .setCc(getAddresses(mCcView))
+ .setBcc(getAddresses(mBccView))
+ .setInReplyTo(mInReplyTo)
+ .setReferences(mReferences)
+ .setRequestReadReceipt(mReadReceipt)
+ .setIdentity(mIdentity)
+ .setMessageFormat(mMessageFormat)
+ .setText(mMessageContentView.getCharacters())
+ .setPgpData(mPgpData)
+ .setAttachments(createAttachmentList())
+ .setSignature(mSignatureView.getCharacters())
+ .setQuoteStyle(mQuoteStyle)
+ .setQuotedTextMode(mQuotedTextMode)
+ .setQuotedText(mQuotedText.getCharacters())
+ .setQuotedHtmlContent(mQuotedHtmlContent)
+ .setReplyAfterQuote(mAccount.isReplyAfterQuote())
+ .setSignatureBeforeQuotedText(mAccount.isSignatureBeforeQuotedText())
+ .setIdentityChanged(mIdentityChanged)
+ .setSignatureChanged(mSignatureChanged)
+ .setCursorPosition(mMessageContentView.getSelectionStart())
+ .setMessageReference(mMessageReference)
+ .setDraft(isDraft);
+ }
+
+ private ArrayList createAttachmentList() {
+ ArrayList attachments = new ArrayList();
for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) {
- Attachment attachment = (Attachment) mAttachments.getChildAt(i).getTag();
-
- if (attachment.state != Attachment.LoadingState.COMPLETE) {
- continue;
- }
-
- String contentType = attachment.contentType;
- if (MimeUtil.isMessage(contentType)) {
- body = new TempFileMessageBody(attachment.filename);
- } else {
- body = new TempFileBody(attachment.filename);
- }
- MimeBodyPart bp = new MimeBodyPart(body);
-
- /*
- * Correctly encode the filename here. Otherwise the whole
- * header value (all parameters at once) will be encoded by
- * MimeHeader.writeTo().
- */
- bp.addHeader(MimeHeader.HEADER_CONTENT_TYPE, String.format("%s;\r\n name=\"%s\"",
- contentType,
- EncoderUtil.encodeIfNecessary(attachment.name,
- EncoderUtil.Usage.WORD_ENTITY, 7)));
-
- bp.setEncoding(MimeUtility.getEncodingforType(contentType));
-
- /*
- * TODO: Oh the joys of MIME...
- *
- * From RFC 2183 (The Content-Disposition Header Field):
- * "Parameter values longer than 78 characters, or which
- * contain non-ASCII characters, MUST be encoded as specified
- * in [RFC 2184]."
- *
- * Example:
- *
- * Content-Type: application/x-stuff
- * title*1*=us-ascii'en'This%20is%20even%20more%20
- * title*2*=%2A%2A%2Afun%2A%2A%2A%20
- * title*3="isn't it!"
- */
- bp.addHeader(MimeHeader.HEADER_CONTENT_DISPOSITION, String.format(Locale.US,
- "attachment;\r\n filename=\"%s\";\r\n size=%d",
- attachment.name, attachment.size));
-
- mp.addBodyPart(bp);
- }
- }
-
- // FYI, there's nothing in the code that requires these variables to one letter. They're one
- // letter simply to save space. This name sucks. It's too similar to Account.Identity.
- private enum IdentityField {
- LENGTH("l"),
- OFFSET("o"),
- FOOTER_OFFSET("fo"),
- PLAIN_LENGTH("pl"),
- PLAIN_OFFSET("po"),
- MESSAGE_FORMAT("f"),
- MESSAGE_READ_RECEIPT("r"),
- SIGNATURE("s"),
- NAME("n"),
- EMAIL("e"),
- // TODO - store a reference to the message being replied so we can mark it at the time of send.
- ORIGINAL_MESSAGE("m"),
- CURSOR_POSITION("p"), // Where in the message your cursor was when you saved.
- QUOTED_TEXT_MODE("q"),
- QUOTE_STYLE("qs");
-
- private final String value;
-
- IdentityField(String value) {
- this.value = value;
+ View view = mAttachments.getChildAt(i);
+ Attachment attachment = (Attachment) view.getTag();
+ attachments.add(attachment);
}
- public String value() {
- return value;
- }
-
- /**
- * Get the list of IdentityFields that should be integer values.
- *
- *
- * These values are sanity checked for integer-ness during decoding.
- *
- *
- * @return The list of integer {@link IdentityField}s.
- */
- public static IdentityField[] getIntegerFields() {
- return new IdentityField[] { LENGTH, OFFSET, FOOTER_OFFSET, PLAIN_LENGTH, PLAIN_OFFSET };
- }
- }
-
- // Version identifier for "new style" identity. ! is an impossible value in base64 encoding, so we
- // use that to determine which version we're in.
- private static final String IDENTITY_VERSION_1 = "!";
-
- /**
- * Build the identity header string. This string contains metadata about a draft message to be
- * used upon loading a draft for composition. This should be generated at the time of saving a
- * draft.
- *
- * This is a URL-encoded key/value pair string. The list of possible values are in {@link IdentityField}.
- * @param body {@link TextBody} to analyze for body length and offset.
- * @param bodyPlain {@link TextBody} to analyze for body length and offset. May be null.
- * @return Identity string.
- */
- private String buildIdentityHeader(final TextBody body, final TextBody bodyPlain) {
- Uri.Builder uri = new Uri.Builder();
- if (body.getComposedMessageLength() != null && body.getComposedMessageOffset() != null) {
- // See if the message body length is already in the TextBody.
- uri.appendQueryParameter(IdentityField.LENGTH.value(), body.getComposedMessageLength().toString());
- uri.appendQueryParameter(IdentityField.OFFSET.value(), body.getComposedMessageOffset().toString());
- } else {
- // If not, calculate it now.
- uri.appendQueryParameter(IdentityField.LENGTH.value(), Integer.toString(body.getText().length()));
- uri.appendQueryParameter(IdentityField.OFFSET.value(), Integer.toString(0));
- }
- if (mQuotedHtmlContent != null) {
- uri.appendQueryParameter(IdentityField.FOOTER_OFFSET.value(),
- Integer.toString(mQuotedHtmlContent.getFooterInsertionPoint()));
- }
- if (bodyPlain != null) {
- if (bodyPlain.getComposedMessageLength() != null && bodyPlain.getComposedMessageOffset() != null) {
- // See if the message body length is already in the TextBody.
- uri.appendQueryParameter(IdentityField.PLAIN_LENGTH.value(), bodyPlain.getComposedMessageLength().toString());
- uri.appendQueryParameter(IdentityField.PLAIN_OFFSET.value(), bodyPlain.getComposedMessageOffset().toString());
- } else {
- // If not, calculate it now.
- uri.appendQueryParameter(IdentityField.PLAIN_LENGTH.value(), Integer.toString(body.getText().length()));
- uri.appendQueryParameter(IdentityField.PLAIN_OFFSET.value(), Integer.toString(0));
- }
- }
- // Save the quote style (useful for forwards).
- uri.appendQueryParameter(IdentityField.QUOTE_STYLE.value(), mQuoteStyle.name());
-
- // Save the message format for this offset.
- uri.appendQueryParameter(IdentityField.MESSAGE_FORMAT.value(), mMessageFormat.name());
-
- // If we're not using the standard identity of signature, append it on to the identity blob.
- if (mIdentity.getSignatureUse() && mSignatureChanged) {
- uri.appendQueryParameter(IdentityField.SIGNATURE.value(), mSignatureView.getCharacters());
- }
-
- if (mIdentityChanged) {
- uri.appendQueryParameter(IdentityField.NAME.value(), mIdentity.getName());
- uri.appendQueryParameter(IdentityField.EMAIL.value(), mIdentity.getEmail());
- }
-
- if (mMessageReference != null) {
- uri.appendQueryParameter(IdentityField.ORIGINAL_MESSAGE.value(), mMessageReference.toIdentityString());
- }
-
- uri.appendQueryParameter(IdentityField.CURSOR_POSITION.value(), Integer.toString(mMessageContentView.getSelectionStart()));
-
- uri.appendQueryParameter(IdentityField.QUOTED_TEXT_MODE.value(), mQuotedTextMode.name());
-
- String k9identity = IDENTITY_VERSION_1 + uri.build().getEncodedQuery();
-
- if (K9.DEBUG) {
- Log.d(K9.LOG_TAG, "Generated identity: " + k9identity);
- }
-
- return k9identity;
- }
-
- /**
- * Parse an identity string. Handles both legacy and new (!) style identities.
- *
- * @param identityString
- * The encoded identity string that was saved in a drafts header.
- *
- * @return A map containing the value for each {@link IdentityField} in the identity string.
- */
- private Map parseIdentityHeader(final String identityString) {
- Map identity = new HashMap();
-
- if (K9.DEBUG) {
- Log.d(K9.LOG_TAG, "Decoding identity: " + identityString);
- }
-
- if (identityString == null || identityString.length() < 1) {
- return identity;
- }
-
- // Check to see if this is a "next gen" identity.
- if (identityString.charAt(0) == IDENTITY_VERSION_1.charAt(0) && identityString.length() > 2) {
- Uri.Builder builder = new Uri.Builder();
- builder.encodedQuery(identityString.substring(1)); // Need to cut off the ! at the beginning.
- Uri uri = builder.build();
- for (IdentityField key : IdentityField.values()) {
- String value = uri.getQueryParameter(key.value());
- if (value != null) {
- identity.put(key, value);
- }
- }
-
- if (K9.DEBUG) {
- Log.d(K9.LOG_TAG, "Decoded identity: " + identity.toString());
- }
-
- // Sanity check our Integers so that recipients of this result don't have to.
- for (IdentityField key : IdentityField.getIntegerFields()) {
- if (identity.get(key) != null) {
- try {
- Integer.parseInt(identity.get(key));
- } catch (NumberFormatException e) {
- Log.e(K9.LOG_TAG, "Invalid " + key.name() + " field in identity: " + identity.get(key));
- }
- }
- }
- } else {
- // Legacy identity
-
- if (K9.DEBUG) {
- Log.d(K9.LOG_TAG, "Got a saved legacy identity: " + identityString);
- }
- StringTokenizer tokenizer = new StringTokenizer(identityString, ":", false);
-
- // First item is the body length. We use this to separate the composed reply from the quoted text.
- if (tokenizer.hasMoreTokens()) {
- String bodyLengthS = Base64.decode(tokenizer.nextToken());
- try {
- identity.put(IdentityField.LENGTH, Integer.valueOf(bodyLengthS).toString());
- } catch (Exception e) {
- Log.e(K9.LOG_TAG, "Unable to parse bodyLength '" + bodyLengthS + "'");
- }
- }
- if (tokenizer.hasMoreTokens()) {
- identity.put(IdentityField.SIGNATURE, Base64.decode(tokenizer.nextToken()));
- }
- if (tokenizer.hasMoreTokens()) {
- identity.put(IdentityField.NAME, Base64.decode(tokenizer.nextToken()));
- }
- if (tokenizer.hasMoreTokens()) {
- identity.put(IdentityField.EMAIL, Base64.decode(tokenizer.nextToken()));
- }
- if (tokenizer.hasMoreTokens()) {
- identity.put(IdentityField.QUOTED_TEXT_MODE, Base64.decode(tokenizer.nextToken()));
- }
- }
-
- return identity;
+ return attachments;
}
private void sendMessage() {
@@ -1690,7 +1234,7 @@ public class MessageCompose extends K9Activity implements OnClickListener,
private void saveIfNeeded() {
if (!mDraftNeedsSaving || mPreventDraftSaving || mPgpData.hasEncryptionKeys() ||
- mEncryptCheckbox.isChecked() || !mAccount.hasDraftsFolder()) {
+ shouldEncrypt() || !mAccount.hasDraftsFolder()) {
return;
}
@@ -1734,7 +1278,7 @@ public class MessageCompose extends K9Activity implements OnClickListener,
}
private void performSend() {
- if (mOpenPgpProvider != null) {
+ if (isCryptoProviderEnabled()) {
// OpenPGP Provider API
// If not already encrypted but user wants to encrypt...
@@ -1867,7 +1411,7 @@ public class MessageCompose extends K9Activity implements OnClickListener,
Log.e(K9.LOG_TAG, "OpenPGP Error Message:" + error.getMessage());
Toast.makeText(MessageCompose.this,
- getString(R.string.openpgp_error) + " " + error.getMessage(),
+ getString(R.string.openpgp_error, error.getMessage()),
Toast.LENGTH_LONG).show();
}
});
@@ -1946,7 +1490,7 @@ public class MessageCompose extends K9Activity implements OnClickListener,
*/
@SuppressLint("InlinedApi")
private void onAddAttachment2(final String mime_type) {
- if (mAccount.getOpenPgpProvider() != null) {
+ if (isCryptoProviderEnabled()) {
Toast.makeText(this, R.string.attachment_encryption_unsupported, Toast.LENGTH_LONG).show();
}
Intent i = new Intent(Intent.ACTION_GET_CONTENT);
@@ -2235,11 +1779,6 @@ public class MessageCompose extends K9Activity implements OnClickListener,
}
}
- public void doLaunchContactPicker(int resultId) {
- mIgnoreOnPause = true;
- startActivityForResult(mContacts.contactPickerIntent(), resultId);
- }
-
private void onAccountChosen(Account account, Identity identity) {
if (!mAccount.equals(account)) {
if (K9.DEBUG) {
@@ -2411,7 +1950,7 @@ public class MessageCompose extends K9Activity implements OnClickListener,
onSend();
break;
case R.id.save:
- if (mEncryptCheckbox.isChecked()) {
+ if (shouldEncrypt()) {
showDialog(DIALOG_REFUSE_TO_SAVE_DRAFT_MARKED_ENCRYPTED);
} else {
onSave();
@@ -2461,7 +2000,7 @@ public class MessageCompose extends K9Activity implements OnClickListener,
@Override
public void onBackPressed() {
if (mDraftNeedsSaving) {
- if (mEncryptCheckbox.isChecked()) {
+ if (shouldEncrypt()) {
showDialog(DIALOG_REFUSE_TO_SAVE_DRAFT_MARKED_ENCRYPTED);
} else if (!mAccount.hasDraftsFolder()) {
showDialog(DIALOG_CONFIRM_DISCARD_ON_BACK);
@@ -2622,17 +2161,19 @@ public class MessageCompose extends K9Activity implements OnClickListener,
String name = MimeUtility.getHeaderParameter(contentType, "name");
if (name != null) {
Body body = part.getBody();
- if (body instanceof LocalAttachmentBody) {
- final Uri uri = ((LocalAttachmentBody) body).getContentUri();
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- addAttachment(uri);
- }
- });
- } else {
- return false;
- }
+ //FIXME
+// if (body instanceof LocalAttachmentBody) {
+// final Uri uri = ((LocalAttachmentBody) body).getContentUri();
+// mHandler.post(new Runnable() {
+// @Override
+// public void run() {
+// addAttachment(uri);
+// }
+// });
+// } else {
+// return false;
+// }
+ return false;
}
return true;
}
@@ -2841,7 +2382,7 @@ public class MessageCompose extends K9Activity implements OnClickListener,
// See buildIdentityHeader(TextBody) for a detailed description of the composition of this blob.
Map k9identity = new HashMap();
if (message.getHeader(K9.IDENTITY_HEADER) != null && message.getHeader(K9.IDENTITY_HEADER).length > 0 && message.getHeader(K9.IDENTITY_HEADER)[0] != null) {
- k9identity = parseIdentityHeader(message.getHeader(K9.IDENTITY_HEADER)[0]);
+ k9identity = IdentityHeaderParser.parse(message.getHeader(K9.IDENTITY_HEADER)[0]);
}
Identity newIdentity = new Identity();
@@ -3400,7 +2941,7 @@ public class MessageCompose extends K9Activity implements OnClickListener,
}
@Override
- public void loadMessageForViewFinished(Account account, String folder, String uid, Message message) {
+ public void loadMessageForViewFinished(Account account, String folder, String uid, LocalMessage message) {
if ((mMessageReference == null) || !mMessageReference.uid.equals(uid)) {
return;
}
@@ -3554,7 +3095,7 @@ public class MessageCompose extends K9Activity implements OnClickListener,
*/
MimeMessage message;
try {
- message = createMessage(false); // isDraft = true
+ message = createMessage();
} catch (MessagingException me) {
Log.e(K9.LOG_TAG, "Failed to create new message for send or save.", me);
throw new RuntimeException("Failed to create a new message for send or save.", me);
@@ -3587,7 +3128,7 @@ public class MessageCompose extends K9Activity implements OnClickListener,
*/
MimeMessage message;
try {
- message = createMessage(true); // isDraft = true
+ message = createDraftMessage();
} catch (MessagingException me) {
Log.e(K9.LOG_TAG, "Failed to create new message for send or save.", me);
throw new RuntimeException("Failed to create a new message for send or save.", me);
@@ -3906,7 +3447,7 @@ public class MessageCompose extends K9Activity implements OnClickListener,
// Right now we send a text/plain-only message when the quoted text was edited, no
// matter what the user selected for the message format.
messageFormat = SimpleMessageFormat.TEXT;
- } else if (mEncryptCheckbox.isChecked() || mCryptoSignatureCheckbox.isChecked()) {
+ } else if (shouldEncrypt() || shouldSign()) {
// Right now we only support PGP inline which doesn't play well with HTML. So force
// plain text in those cases.
messageFormat = SimpleMessageFormat.TEXT;
@@ -3952,34 +3493,30 @@ public class MessageCompose extends K9Activity implements OnClickListener,
}
}
- /**
- * An {@link EditText} extension with methods that convert line endings from
- * {@code \r\n} to {@code \n} and back again when setting and getting text.
- *
- */
- public static class EolConvertingEditText extends EditText {
+ private boolean isCryptoProviderEnabled() {
+ return mOpenPgpProvider != null;
+ }
- public EolConvertingEditText(Context context, AttributeSet attrs) {
- super(context, attrs);
+ private boolean shouldEncrypt() {
+ return isCryptoProviderEnabled() && mEncryptCheckbox.isChecked();
+ }
+
+ private boolean shouldSign() {
+ return isCryptoProviderEnabled() && mCryptoSignatureCheckbox.isChecked();
+ }
+
+ class DoLaunchOnClickListener implements OnClickListener {
+
+ private final int resultId;
+
+ DoLaunchOnClickListener(int resultId) {
+ this.resultId = resultId;
}
- /**
- * Return the text the EolConvertingEditText is displaying.
- *
- * @return A string with any line endings converted to {@code \r\n}.
- */
- public String getCharacters() {
- return getText().toString().replace("\n", "\r\n");
- }
-
- /**
- * Sets the string value of the EolConvertingEditText. Any line endings
- * in the string will be converted to {@code \n}.
- *
- * @param text
- */
- public void setCharacters(CharSequence text) {
- setText(text.toString().replace("\r\n", "\n"));
+ @Override
+ public void onClick(View v) {
+ mIgnoreOnPause = true;
+ startActivityForResult(mContacts.contactPickerIntent(), resultId);
}
}
}
diff --git a/k9mail/src/main/java/com/fsck/k9/activity/MessageList.java b/k9mail/src/main/java/com/fsck/k9/activity/MessageList.java
index 357e0ccf6..9d08f2cc7 100644
--- a/k9mail/src/main/java/com/fsck/k9/activity/MessageList.java
+++ b/k9mail/src/main/java/com/fsck/k9/activity/MessageList.java
@@ -40,8 +40,8 @@ import com.fsck.k9.activity.setup.Prefs;
import com.fsck.k9.crypto.PgpData;
import com.fsck.k9.fragment.MessageListFragment;
import com.fsck.k9.fragment.MessageListFragment.MessageListFragmentListener;
-import com.fsck.k9.fragment.MessageViewFragment;
-import com.fsck.k9.fragment.MessageViewFragment.MessageViewFragmentListener;
+import com.fsck.k9.ui.messageview.MessageViewFragment;
+import com.fsck.k9.ui.messageview.MessageViewFragment.MessageViewFragmentListener;
import com.fsck.k9.mailstore.StorageManager;
import com.fsck.k9.mailstore.LocalMessage;
import com.fsck.k9.search.LocalSearch;
@@ -51,7 +51,6 @@ import com.fsck.k9.search.SearchSpecification.Attribute;
import com.fsck.k9.search.SearchSpecification.SearchCondition;
import com.fsck.k9.search.SearchSpecification.SearchField;
import com.fsck.k9.view.MessageHeader;
-import com.fsck.k9.view.MessageOpenPgpView;
import com.fsck.k9.view.MessageTitleView;
import com.fsck.k9.view.ViewSwitcher;
import com.fsck.k9.view.ViewSwitcher.OnSwitchCompleteListener;
@@ -88,6 +87,7 @@ public class MessageList extends K9Activity implements MessageListFragmentListen
private static final int PREVIOUS = 1;
private static final int NEXT = 2;
+
public static void actionDisplaySearch(Context context, SearchSpecification search,
boolean noThreading, boolean newTask) {
actionDisplaySearch(context, search, noThreading, newTask, true);
@@ -1562,13 +1562,8 @@ public class MessageList extends K9Activity implements MessageListFragmentListen
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
- // handle OpenPGP results from PendingIntents in OpenPGP view
- // must be handled in this main activity, because startIntentSenderForResult() does not support Fragments
- MessageOpenPgpView openPgpView = (MessageOpenPgpView) findViewById(R.id.layout_decrypt_openpgp);
- if (openPgpView != null && openPgpView.handleOnActivityResult(requestCode, resultCode, data)) {
- return;
+ if (mMessageViewFragment != null) {
+ mMessageViewFragment.handleCryptoResult(requestCode, resultCode, data);
}
}
-
-
}
diff --git a/k9mail/src/main/java/com/fsck/k9/activity/setup/AccountSettings.java b/k9mail/src/main/java/com/fsck/k9/activity/setup/AccountSettings.java
index 9ebeee4f9..19a718bf0 100644
--- a/k9mail/src/main/java/com/fsck/k9/activity/setup/AccountSettings.java
+++ b/k9mail/src/main/java/com/fsck/k9/activity/setup/AccountSettings.java
@@ -45,7 +45,7 @@ import com.fsck.k9.mailstore.LocalFolder;
import com.fsck.k9.mailstore.StorageManager;
import com.fsck.k9.service.MailService;
-import org.openintents.openpgp.util.OpenPgpListPreference;
+import org.openintents.openpgp.util.OpenPgpAppPreference;
import org.openintents.openpgp.util.OpenPgpUtils;
@@ -174,7 +174,7 @@ public class AccountSettings extends K9PreferenceActivity {
private ListPreference mIdleRefreshPeriod;
private ListPreference mMaxPushFolders;
private boolean mHasCrypto = false;
- private OpenPgpListPreference mCryptoApp;
+ private OpenPgpAppPreference mCryptoApp;
private PreferenceScreen mSearchScreen;
private CheckBoxPreference mCloudSearchEnabled;
@@ -687,7 +687,7 @@ public class AccountSettings extends K9PreferenceActivity {
mHasCrypto = OpenPgpUtils.isAvailable(this);
if (mHasCrypto) {
- mCryptoApp = (OpenPgpListPreference) findPreference(PREFERENCE_CRYPTO_APP);
+ mCryptoApp = (OpenPgpAppPreference) findPreference(PREFERENCE_CRYPTO_APP);
mCryptoApp.setValue(String.valueOf(mAccount.getCryptoApp()));
mCryptoApp.setSummary(mCryptoApp.getEntry());
diff --git a/k9mail/src/main/java/com/fsck/k9/controller/MessagingController.java b/k9mail/src/main/java/com/fsck/k9/controller/MessagingController.java
index 9a5c356f9..ece8a2b5b 100644
--- a/k9mail/src/main/java/com/fsck/k9/controller/MessagingController.java
+++ b/k9mail/src/main/java/com/fsck/k9/controller/MessagingController.java
@@ -3115,64 +3115,39 @@ public class MessagingController implements Runnable {
});
}
- /**
- * Mark the provided message as read if not disabled by the account setting.
- *
- * @param account
- * The account the message belongs to.
- * @param message
- * The message to mark as read. This {@link Message} instance will be modify by calling
- * {@link Message#setFlag(Flag, boolean)} on it.
- *
- * @throws MessagingException
- *
- * @see Account#isMarkMessageAsReadOnView()
- */
- private void markMessageAsReadOnView(Account account, Message message)
+ public LocalMessage loadMessage(Account account, String folderName, String uid) throws MessagingException {
+ LocalStore localStore = account.getLocalStore();
+ LocalFolder localFolder = localStore.getFolder(folderName);
+ localFolder.open(Folder.OPEN_MODE_RW);
+
+ LocalMessage message = localFolder.getMessage(uid);
+ if (message == null || message.getId() == 0) {
+ throw new IllegalArgumentException("Message not found: folder=" + folderName + ", uid=" + uid);
+ }
+
+ FetchProfile fp = new FetchProfile();
+ fp.add(FetchProfile.Item.BODY);
+ localFolder.fetch(Collections.singletonList(message), fp, null);
+ localFolder.close();
+
+ markMessageAsReadOnView(account, message);
+
+ return message;
+ }
+
+ private void markMessageAsReadOnView(Account account, LocalMessage message)
throws MessagingException {
if (account.isMarkMessageAsReadOnView() && !message.isSet(Flag.SEEN)) {
List messageIds = Collections.singletonList(message.getId());
setFlag(account, messageIds, Flag.SEEN, true);
- ((LocalMessage) message).setFlagInternal(Flag.SEEN, true);
+ message.setFlagInternal(Flag.SEEN, true);
}
}
- /**
- * Attempts to load the attachment specified by part from the given account and message.
- * @param account
- * @param message
- * @param part
- * @param listener
- */
- public void loadAttachment(
- final Account account,
- final Message message,
- final Part part,
- final Object tag,
- final MessagingListener listener) {
- /*
- * Check if the attachment has already been downloaded. If it has there's no reason to
- * download it, so we just tell the listener that it's ready to go.
- */
-
- if (part.getBody() != null) {
- for (MessagingListener l : getListeners(listener)) {
- l.loadAttachmentStarted(account, message, part, tag, false);
- }
-
- for (MessagingListener l : getListeners(listener)) {
- l.loadAttachmentFinished(account, message, part, tag);
- }
- return;
- }
-
-
-
- for (MessagingListener l : getListeners(listener)) {
- l.loadAttachmentStarted(account, message, part, tag, true);
- }
+ public void loadAttachment(final Account account, final LocalMessage message, final Part part,
+ final MessagingListener listener) {
put("loadAttachment", listener, new Runnable() {
@Override
@@ -3180,32 +3155,29 @@ public class MessagingController implements Runnable {
Folder remoteFolder = null;
LocalFolder localFolder = null;
try {
- LocalStore localStore = account.getLocalStore();
+ String folderName = message.getFolder().getName();
+
+ LocalStore localStore = account.getLocalStore();
+ localFolder = localStore.getFolder(folderName);
- List attachments = MessageExtractor.collectAttachments(message);
- for (Part attachment : attachments) {
- attachment.setBody(null);
- }
Store remoteStore = account.getRemoteStore();
- localFolder = localStore.getFolder(message.getFolder().getName());
- remoteFolder = remoteStore.getFolder(message.getFolder().getName());
+ remoteFolder = remoteStore.getFolder(folderName);
remoteFolder.open(Folder.OPEN_MODE_RW);
- //FIXME: This is an ugly hack that won't be needed once the Message objects have been united.
Message remoteMessage = remoteFolder.getMessage(message.getUid());
- MimeMessageHelper.setBody(remoteMessage, message.getBody());
remoteFolder.fetchPart(remoteMessage, part, null);
- localFolder.updateMessage((LocalMessage)message);
+ localFolder.addPartToMessage(message, part);
+
for (MessagingListener l : getListeners(listener)) {
- l.loadAttachmentFinished(account, message, part, tag);
+ l.loadAttachmentFinished(account, message, part);
}
} catch (MessagingException me) {
if (K9.DEBUG)
Log.v(K9.LOG_TAG, "Exception loading attachment", me);
for (MessagingListener l : getListeners(listener)) {
- l.loadAttachmentFailed(account, message, part, tag, me.getMessage());
+ l.loadAttachmentFailed(account, message, part, me.getMessage());
}
notifyUserIfCertificateProblem(context, me, account, true);
addErrorMessage(account, null, me);
@@ -4015,7 +3987,7 @@ public class MessagingController implements Runnable {
@Override
public void act(final Account account, final Folder folder,
- final List accountMessages) {
+ final List accountMessages) {
suppressMessages(account, messages);
putBackground("deleteMessages", null, new Runnable() {
diff --git a/k9mail/src/main/java/com/fsck/k9/controller/MessagingListener.java b/k9mail/src/main/java/com/fsck/k9/controller/MessagingListener.java
index 72bd93d18..5b6970d38 100644
--- a/k9mail/src/main/java/com/fsck/k9/controller/MessagingListener.java
+++ b/k9mail/src/main/java/com/fsck/k9/controller/MessagingListener.java
@@ -91,7 +91,7 @@ public class MessagingListener {
Message message) {}
public void loadMessageForViewFinished(Account account, String folder, String uid,
- Message message) {}
+ LocalMessage message) {}
public void loadMessageForViewFailed(Account account, String folder, String uid,
Throwable t) {}
@@ -133,13 +133,9 @@ public class MessagingListener {
public void setPushActive(Account account, String folderName, boolean enabled) {}
- public void loadAttachmentStarted(Account account, Message message, Part part, Object tag,
- boolean requiresDownload) {}
+ public void loadAttachmentFinished(Account account, Message message, Part part) {}
- public void loadAttachmentFinished(Account account, Message message, Part part, Object tag) {}
-
- public void loadAttachmentFailed(Account account, Message message, Part part, Object tag,
- String reason) {}
+ public void loadAttachmentFailed(Account account, Message message, Part part, String reason) {}
diff --git a/k9mail/src/main/java/com/fsck/k9/crypto/DecryptedTempFileBody.java b/k9mail/src/main/java/com/fsck/k9/crypto/DecryptedTempFileBody.java
new file mode 100644
index 000000000..3d7e8845a
--- /dev/null
+++ b/k9mail/src/main/java/com/fsck/k9/crypto/DecryptedTempFileBody.java
@@ -0,0 +1,70 @@
+package com.fsck.k9.crypto;
+
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import com.fsck.k9.mail.MessagingException;
+import com.fsck.k9.mail.internet.RawDataBody;
+import com.fsck.k9.mail.internet.SizeAware;
+import org.apache.commons.io.IOUtils;
+
+
+public class DecryptedTempFileBody implements RawDataBody, SizeAware {
+ private final File tempDirectory;
+ private final String encoding;
+ private File file;
+
+
+ public DecryptedTempFileBody(String encoding, File tempDirectory) {
+ this.encoding = encoding;
+ this.tempDirectory = tempDirectory;
+ }
+
+ @Override
+ public String getEncoding() {
+ return encoding;
+ }
+
+ @Override
+ public void setEncoding(String encoding) throws MessagingException {
+ throw new RuntimeException("Not supported");
+ }
+
+ public OutputStream getOutputStream() throws IOException {
+ file = File.createTempFile("decrypted", null, tempDirectory);
+ return new FileOutputStream(file);
+ }
+
+ @Override
+ public InputStream getInputStream() throws MessagingException {
+ try {
+ return new FileInputStream(file);
+ } catch (IOException ioe) {
+ throw new MessagingException("Unable to open body", ioe);
+ }
+ }
+
+ @Override
+ public void writeTo(OutputStream out) throws IOException, MessagingException {
+ InputStream in = getInputStream();
+ try {
+ IOUtils.copy(in, out);
+ } finally {
+ in.close();
+ }
+ }
+
+ @Override
+ public long getSize() {
+ return file.length();
+ }
+
+ public File getFile() {
+ return file;
+ }
+}
diff --git a/k9mail/src/main/java/com/fsck/k9/crypto/MessageDecryptVerifier.java b/k9mail/src/main/java/com/fsck/k9/crypto/MessageDecryptVerifier.java
new file mode 100644
index 000000000..eb58a11bb
--- /dev/null
+++ b/k9mail/src/main/java/com/fsck/k9/crypto/MessageDecryptVerifier.java
@@ -0,0 +1,134 @@
+package com.fsck.k9.crypto;
+
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Stack;
+
+import com.fsck.k9.mail.Body;
+import com.fsck.k9.mail.BodyPart;
+import com.fsck.k9.mail.MessagingException;
+import com.fsck.k9.mail.Multipart;
+import com.fsck.k9.mail.Part;
+import com.fsck.k9.mail.internet.MessageExtractor;
+import org.openintents.openpgp.util.OpenPgpUtils;
+
+
+public class MessageDecryptVerifier {
+ private static final String MULTIPART_ENCRYPTED = "multipart/encrypted";
+ private static final String MULTIPART_SIGNED = "multipart/signed";
+ private static final String PROTOCOL_PARAMETER = "protocol";
+ private static final String APPLICATION_PGP_ENCRYPTED = "application/pgp-encrypted";
+ private static final String APPLICATION_PGP_SIGNATURE = "application/pgp-signature";
+ private static final String TEXT_PLAIN = "text/plain";
+
+
+ public static List findEncryptedParts(Part startPart) {
+ List encryptedParts = new ArrayList();
+ Stack partsToCheck = new Stack();
+ partsToCheck.push(startPart);
+
+ while (!partsToCheck.isEmpty()) {
+ Part part = partsToCheck.pop();
+ String mimeType = part.getMimeType();
+ Body body = part.getBody();
+
+ if (MULTIPART_ENCRYPTED.equals(mimeType)) {
+ encryptedParts.add(part);
+ } else if (body instanceof Multipart) {
+ Multipart multipart = (Multipart) body;
+ for (int i = multipart.getCount() - 1; i >= 0; i--) {
+ BodyPart bodyPart = multipart.getBodyPart(i);
+ partsToCheck.push(bodyPart);
+ }
+ }
+ }
+
+ return encryptedParts;
+ }
+
+ public static List findSignedParts(Part startPart) {
+ List signedParts = new ArrayList();
+ Stack partsToCheck = new Stack();
+ partsToCheck.push(startPart);
+
+ while (!partsToCheck.isEmpty()) {
+ Part part = partsToCheck.pop();
+ String mimeType = part.getMimeType();
+ Body body = part.getBody();
+
+ if (MULTIPART_SIGNED.equals(mimeType)) {
+ signedParts.add(part);
+ } else if (body instanceof Multipart) {
+ Multipart multipart = (Multipart) body;
+ for (int i = multipart.getCount() - 1; i >= 0; i--) {
+ BodyPart bodyPart = multipart.getBodyPart(i);
+ partsToCheck.push(bodyPart);
+ }
+ }
+ }
+
+ return signedParts;
+ }
+
+ public static List findPgpInlineParts(Part startPart) {
+ List inlineParts = new ArrayList();
+ Stack partsToCheck = new Stack();
+ partsToCheck.push(startPart);
+
+ while (!partsToCheck.isEmpty()) {
+ Part part = partsToCheck.pop();
+ String mimeType = part.getMimeType();
+ Body body = part.getBody();
+
+ if (TEXT_PLAIN.equalsIgnoreCase(mimeType)) {
+ String text = MessageExtractor.getTextFromPart(part);
+ switch (OpenPgpUtils.parseMessage(text)) {
+ case OpenPgpUtils.PARSE_RESULT_MESSAGE:
+ case OpenPgpUtils.PARSE_RESULT_SIGNED_MESSAGE:
+ inlineParts.add(part);
+ }
+ } else if (body instanceof Multipart) {
+ Multipart multipart = (Multipart) body;
+ for (int i = multipart.getCount() - 1; i >= 0; i--) {
+ BodyPart bodyPart = multipart.getBodyPart(i);
+ partsToCheck.push(bodyPart);
+ }
+ }
+ }
+
+ return inlineParts;
+ }
+
+ public static byte[] getSignatureData(Part part) throws IOException, MessagingException {
+
+ if (MULTIPART_SIGNED.equals(part.getMimeType())) {
+ Body body = part.getBody();
+ if (body instanceof Multipart) {
+ Multipart multi = (Multipart) body;
+ BodyPart signatureBody = multi.getBodyPart(1);
+ if (APPLICATION_PGP_SIGNATURE.equals(signatureBody.getMimeType())) {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ signatureBody.getBody().writeTo(bos);
+ return bos.toByteArray();
+ }
+ }
+ }
+
+ return null;
+ }
+
+ public static boolean isPgpMimeSignedPart(Part part) {
+ return MULTIPART_SIGNED.equals(part.getMimeType());
+ }
+
+ public static boolean isPgpMimeEncryptedPart(Part part) {
+ //FIXME: Doesn't work right now because LocalMessage.getContentType() doesn't load headers from database
+// String contentType = part.getContentType();
+// String protocol = MimeUtility.getHeaderParameter(contentType, PROTOCOL_PARAMETER);
+// return APPLICATION_PGP_ENCRYPTED.equals(protocol);
+ return MULTIPART_ENCRYPTED.equals(part.getMimeType());
+ }
+}
diff --git a/k9mail/src/main/java/com/fsck/k9/helper/SimpleTextWatcher.java b/k9mail/src/main/java/com/fsck/k9/helper/SimpleTextWatcher.java
new file mode 100644
index 000000000..94c2cc7b4
--- /dev/null
+++ b/k9mail/src/main/java/com/fsck/k9/helper/SimpleTextWatcher.java
@@ -0,0 +1,22 @@
+package com.fsck.k9.helper;
+
+import android.text.Editable;
+import android.text.TextWatcher;
+
+/**
+ * all methods empty - but this way we can have TextWatchers with less boilder-plate where
+ * we just override the methods we want and not always al 3
+ */
+public class SimpleTextWatcher implements TextWatcher {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ }
+}
diff --git a/k9mail/src/main/java/com/fsck/k9/helper/Utility.java b/k9mail/src/main/java/com/fsck/k9/helper/Utility.java
index 6be32825d..aa93f7d8c 100644
--- a/k9mail/src/main/java/com/fsck/k9/helper/Utility.java
+++ b/k9mail/src/main/java/com/fsck/k9/helper/Utility.java
@@ -393,7 +393,8 @@ public class Utility {
public static boolean hasExternalImages(final String message) {
Matcher imgMatches = IMG_PATTERN.matcher(message);
while (imgMatches.find()) {
- if (!imgMatches.group(1).equals("content")) {
+ String uriScheme = imgMatches.group(1);
+ if (uriScheme.equals("http") || uriScheme.equals("https")) {
if (K9.DEBUG) {
Log.d(K9.LOG_TAG, "External images found");
}
diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/AttachmentViewInfo.java b/k9mail/src/main/java/com/fsck/k9/mailstore/AttachmentViewInfo.java
new file mode 100644
index 000000000..88d1e1981
--- /dev/null
+++ b/k9mail/src/main/java/com/fsck/k9/mailstore/AttachmentViewInfo.java
@@ -0,0 +1,36 @@
+package com.fsck.k9.mailstore;
+
+
+import android.net.Uri;
+
+import com.fsck.k9.mail.Part;
+
+
+public class AttachmentViewInfo {
+ public static final long UNKNOWN_SIZE = -1;
+
+ public final String mimeType;
+ public final String displayName;
+ public final long size;
+
+ /**
+ * A content provider URI that can be used to retrieve the decoded attachment.
+ *
+ * Note: All content providers must support an alternative MIME type appended as last URI segment.
+ *
+ * @see com.fsck.k9.ui.messageview.AttachmentController#getAttachmentUriForMimeType(AttachmentViewInfo, String)
+ */
+ public final Uri uri;
+ public final boolean firstClassAttachment;
+ public final Part part;
+
+ public AttachmentViewInfo(String mimeType, String displayName, long size, Uri uri, boolean firstClassAttachment,
+ Part part) {
+ this.mimeType = mimeType;
+ this.displayName = displayName;
+ this.size = size;
+ this.uri = uri;
+ this.firstClassAttachment = firstClassAttachment;
+ this.part = part;
+ }
+}
diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/BinaryMemoryBody.java b/k9mail/src/main/java/com/fsck/k9/mailstore/BinaryMemoryBody.java
new file mode 100644
index 000000000..b4a289f3a
--- /dev/null
+++ b/k9mail/src/main/java/com/fsck/k9/mailstore/BinaryMemoryBody.java
@@ -0,0 +1,48 @@
+package com.fsck.k9.mailstore;
+
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import com.fsck.k9.mail.Body;
+import com.fsck.k9.mail.MessagingException;
+import com.fsck.k9.mail.internet.RawDataBody;
+import com.fsck.k9.mail.internet.SizeAware;
+
+
+public class BinaryMemoryBody implements Body, RawDataBody, SizeAware {
+ private final byte[] data;
+ private final String encoding;
+
+ public BinaryMemoryBody(byte[] data, String encoding) {
+ this.data = data;
+ this.encoding = encoding;
+ }
+
+ @Override
+ public String getEncoding() {
+ return encoding;
+ }
+
+ @Override
+ public InputStream getInputStream() throws MessagingException {
+ return new ByteArrayInputStream(data);
+ }
+
+ @Override
+ public void setEncoding(String encoding) throws UnavailableStorageException, MessagingException {
+ throw new RuntimeException("nope"); //FIXME
+ }
+
+ @Override
+ public void writeTo(OutputStream out) throws IOException, MessagingException {
+ out.write(data);
+ }
+
+ @Override
+ public long getSize() {
+ return data.length;
+ }
+}
diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/DecryptStreamParser.java b/k9mail/src/main/java/com/fsck/k9/mailstore/DecryptStreamParser.java
new file mode 100644
index 000000000..ff8352b49
--- /dev/null
+++ b/k9mail/src/main/java/com/fsck/k9/mailstore/DecryptStreamParser.java
@@ -0,0 +1,231 @@
+package com.fsck.k9.mailstore;
+
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Stack;
+
+import android.content.Context;
+import android.util.Log;
+
+import com.fsck.k9.K9;
+import com.fsck.k9.crypto.DecryptedTempFileBody;
+import com.fsck.k9.mail.Body;
+import com.fsck.k9.mail.BodyPart;
+import com.fsck.k9.mail.Message;
+import com.fsck.k9.mail.MessagingException;
+import com.fsck.k9.mail.Multipart;
+import com.fsck.k9.mail.Part;
+import com.fsck.k9.mail.internet.MimeBodyPart;
+import com.fsck.k9.mail.internet.MimeMessage;
+import com.fsck.k9.mail.internet.MimeMultipart;
+import com.fsck.k9.mail.internet.MimeUtility;
+import org.apache.commons.io.IOUtils;
+import org.apache.james.mime4j.MimeException;
+import org.apache.james.mime4j.codec.Base64InputStream;
+import org.apache.james.mime4j.codec.QuotedPrintableInputStream;
+import org.apache.james.mime4j.io.EOLConvertingInputStream;
+import org.apache.james.mime4j.parser.ContentHandler;
+import org.apache.james.mime4j.parser.MimeStreamParser;
+import org.apache.james.mime4j.stream.BodyDescriptor;
+import org.apache.james.mime4j.stream.Field;
+import org.apache.james.mime4j.stream.MimeConfig;
+import org.apache.james.mime4j.util.MimeUtil;
+
+// TODO rename this class? this class doesn't really bear any 'decrypted' semantics anymore...
+public class DecryptStreamParser {
+
+ private static final String DECRYPTED_CACHE_DIRECTORY = "decrypted";
+
+ public static MimeBodyPart parse(Context context, InputStream inputStream) throws MessagingException, IOException {
+ File decryptedTempDirectory = getDecryptedTempDirectory(context);
+
+ MimeBodyPart decryptedRootPart = new MimeBodyPart();
+
+ MimeConfig parserConfig = new MimeConfig();
+ parserConfig.setMaxHeaderLen(-1);
+ parserConfig.setMaxLineLen(-1);
+ parserConfig.setMaxHeaderCount(-1);
+
+ MimeStreamParser parser = new MimeStreamParser(parserConfig);
+ parser.setContentHandler(new PartBuilder(decryptedTempDirectory, decryptedRootPart));
+ parser.setRecurse();
+
+ inputStream = new BufferedInputStream(inputStream, 4096);
+
+ try {
+ parser.parse(new EOLConvertingInputStream(inputStream));
+ } catch (MimeException e) {
+ throw new MessagingException("Failed to parse decrypted content", e);
+ }
+
+ return decryptedRootPart;
+ }
+
+ private static Body createBody(InputStream inputStream, String transferEncoding, File decryptedTempDirectory)
+ throws IOException {
+ DecryptedTempFileBody body = new DecryptedTempFileBody(transferEncoding, decryptedTempDirectory);
+ OutputStream outputStream = body.getOutputStream();
+ try {
+ InputStream decodingInputStream;
+ boolean closeStream;
+ if (MimeUtil.ENC_QUOTED_PRINTABLE.equals(transferEncoding)) {
+ decodingInputStream = new QuotedPrintableInputStream(inputStream, false);
+ closeStream = true;
+ } else if (MimeUtil.ENC_BASE64.equals(transferEncoding)) {
+ decodingInputStream = new Base64InputStream(inputStream);
+ closeStream = true;
+ } else {
+ decodingInputStream = inputStream;
+ closeStream = false;
+ }
+
+ try {
+ IOUtils.copy(decodingInputStream, outputStream);
+ } finally {
+ if (closeStream) {
+ decodingInputStream.close();
+ }
+ }
+ } finally {
+ outputStream.close();
+ }
+
+ return body;
+ }
+
+ private static File getDecryptedTempDirectory(Context context) {
+ File directory = new File(context.getCacheDir(), DECRYPTED_CACHE_DIRECTORY);
+ if (!directory.exists()) {
+ if (!directory.mkdir()) {
+ Log.e(K9.LOG_TAG, "Error creating directory: " + directory.getAbsolutePath());
+ }
+ }
+
+ return directory;
+ }
+
+
+ private static class PartBuilder implements ContentHandler {
+ private final File decryptedTempDirectory;
+ private final MimeBodyPart decryptedRootPart;
+ private final Stack