Merge pull request #959 from k9mail/display_encrypted_in_message_preview

Display "*Encrypted*" in message preview
This commit is contained in:
cketti 2015-12-19 18:25:32 +01:00
commit a5ad84481d
25 changed files with 1169 additions and 224 deletions

View file

@ -142,7 +142,6 @@ public abstract class Message implements Part, CompositeBody {
public abstract long getId();
public abstract String getPreview();
public abstract boolean hasAttachments();
public abstract int getSize();

View file

@ -656,11 +656,6 @@ public class MimeMessage extends Message {
return Long.parseLong(mUid); //or maybe .mMessageId?
}
@Override
public String getPreview() {
return "";
}
@Override
public boolean hasAttachments() {
return false;

View file

@ -88,6 +88,7 @@ import com.fsck.k9.mail.Flag;
import com.fsck.k9.mail.Folder;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mailstore.DatabasePreviewType;
import com.fsck.k9.mailstore.LocalFolder;
import com.fsck.k9.mailstore.LocalMessage;
import com.fsck.k9.mailstore.LocalStore;
@ -125,6 +126,7 @@ public class MessageListFragment extends Fragment implements OnItemClickListener
MessageColumns.FORWARDED,
MessageColumns.ATTACHMENT_COUNT,
MessageColumns.FOLDER_ID,
MessageColumns.PREVIEW_TYPE,
MessageColumns.PREVIEW,
ThreadColumns.ROOT,
SpecialColumns.ACCOUNT_UUID,
@ -147,11 +149,12 @@ public class MessageListFragment extends Fragment implements OnItemClickListener
private static final int FORWARDED_COLUMN = 11;
private static final int ATTACHMENT_COUNT_COLUMN = 12;
private static final int FOLDER_ID_COLUMN = 13;
private static final int PREVIEW_COLUMN = 14;
private static final int THREAD_ROOT_COLUMN = 15;
private static final int ACCOUNT_UUID_COLUMN = 16;
private static final int FOLDER_NAME_COLUMN = 17;
private static final int THREAD_COUNT_COLUMN = 18;
private static final int PREVIEW_TYPE_COLUMN = 14;
private static final int PREVIEW_COLUMN = 15;
private static final int THREAD_ROOT_COLUMN = 16;
private static final int ACCOUNT_UUID_COLUMN = 17;
private static final int FOLDER_NAME_COLUMN = 18;
private static final int THREAD_COUNT_COLUMN = 19;
private static final String[] PROJECTION = Arrays.copyOf(THREADED_PROJECTION,
THREAD_COUNT_COLUMN);
@ -2029,10 +2032,8 @@ public class MessageListFragment extends Fragment implements OnItemClickListener
.append(beforePreviewText);
if (mPreviewLines > 0) {
String preview = cursor.getString(PREVIEW_COLUMN);
if (preview != null) {
messageStringBuilder.append(" ").append(preview);
}
String preview = getPreview(cursor);
messageStringBuilder.append(" ").append(preview);
}
holder.preview.setText(messageStringBuilder, TextView.BufferType.SPANNABLE);
@ -2096,6 +2097,25 @@ public class MessageListFragment extends Fragment implements OnItemClickListener
holder.date.setText(displayDate);
}
private String getPreview(Cursor cursor) {
String previewTypeString = cursor.getString(PREVIEW_TYPE_COLUMN);
DatabasePreviewType previewType = DatabasePreviewType.fromDatabaseValue(previewTypeString);
switch (previewType) {
case NONE: {
return "";
}
case ENCRYPTED: {
return getString(R.string.preview_encrypted);
}
case TEXT: {
return cursor.getString(PREVIEW_COLUMN);
}
}
throw new AssertionError("Unknown preview type: " + previewType);
}
}
class MessageViewHolder implements View.OnClickListener {

View file

@ -0,0 +1,20 @@
package com.fsck.k9.mailstore;
import java.util.ArrayList;
import java.util.List;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Part;
import com.fsck.k9.mail.internet.MessageExtractor;
class AttachmentCounter {
public int getAttachmentCount(Message message) throws MessagingException {
List<Part> attachments = new ArrayList<Part>();
MessageExtractor.getViewables(message, attachments);
return attachments.size();
}
}

View file

@ -0,0 +1,49 @@
package com.fsck.k9.mailstore;
import com.fsck.k9.message.preview.PreviewResult.PreviewType;
public enum DatabasePreviewType {
NONE("none", PreviewType.NONE),
TEXT("text", PreviewType.TEXT),
ENCRYPTED("encrypted", PreviewType.ENCRYPTED);
private final String databaseValue;
private final PreviewType previewType;
DatabasePreviewType(String databaseValue, PreviewType previewType) {
this.databaseValue = databaseValue;
this.previewType = previewType;
}
public static DatabasePreviewType fromDatabaseValue(String databaseValue) {
for (DatabasePreviewType databasePreviewType : values()) {
if (databasePreviewType.getDatabaseValue().equals(databaseValue)) {
return databasePreviewType;
}
}
throw new AssertionError("Unknown database value: " + databaseValue);
}
public static DatabasePreviewType fromPreviewType(PreviewType previewType) {
for (DatabasePreviewType databasePreviewType : values()) {
if (databasePreviewType.previewType == previewType) {
return databasePreviewType;
}
}
throw new AssertionError("Unknown preview type: " + previewType);
}
public String getDatabaseValue() {
return databaseValue;
}
public PreviewType getPreviewType() {
return previewType;
}
}

View file

@ -54,6 +54,9 @@ import com.fsck.k9.mail.internet.SizeAware;
import com.fsck.k9.mail.message.MessageHeaderParser;
import com.fsck.k9.mailstore.LockableDatabase.DbCallback;
import com.fsck.k9.mailstore.LockableDatabase.WrappedException;
import com.fsck.k9.message.preview.MessagePreviewCreator;
import com.fsck.k9.message.preview.PreviewResult;
import com.fsck.k9.message.preview.PreviewResult.PreviewType;
import org.apache.commons.io.IOUtils;
import org.apache.james.mime4j.util.MimeUtil;
@ -1233,9 +1236,13 @@ public class LocalFolder extends Folder<LocalMessage> implements Serializable {
}
try {
MessageInfoExtractor messageExtractor = new MessageInfoExtractor(localStore.context, message);
String preview = messageExtractor.getMessageTextPreview();
int attachmentCount = messageExtractor.getAttachmentCount();
MessagePreviewCreator previewCreator = localStore.getMessagePreviewCreator();
PreviewResult previewResult = previewCreator.createPreview(message);
PreviewType previewType = previewResult.getPreviewType();
DatabasePreviewType databasePreviewType = DatabasePreviewType.fromPreviewType(previewType);
AttachmentCounter attachmentCounter = localStore.getAttachmentCounter();
int attachmentCount = attachmentCounter.getAttachmentCount(message);
long rootMessagePartId = saveMessageParts(db, message);
@ -1256,7 +1263,6 @@ public class LocalFolder extends Folder<LocalMessage> implements Serializable {
cv.put("to_list", Address.pack(message.getRecipients(RecipientType.TO)));
cv.put("cc_list", Address.pack(message.getRecipients(RecipientType.CC)));
cv.put("bcc_list", Address.pack(message.getRecipients(RecipientType.BCC)));
cv.put("preview", preview);
cv.put("reply_to_list", Address.pack(message.getReplyTo()));
cv.put("attachment_count", attachmentCount);
cv.put("internal_date", message.getInternalDate() == null
@ -1264,6 +1270,13 @@ public class LocalFolder extends Folder<LocalMessage> implements Serializable {
cv.put("mime_type", message.getMimeType());
cv.put("empty", 0);
cv.put("preview_type", databasePreviewType.getDatabaseValue());
if (previewResult.isPreviewTextAvailable()) {
cv.put("preview", previewResult.getPreviewText());
} else {
cv.putNull("preview");
}
String messageId = message.getMessageId();
if (messageId != null) {
cv.put("message_id", messageId);

View file

@ -20,6 +20,8 @@ import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.internet.MimeMessage;
import com.fsck.k9.mailstore.LockableDatabase.DbCallback;
import com.fsck.k9.mailstore.LockableDatabase.WrappedException;
import com.fsck.k9.message.preview.PreviewResult.PreviewType;
public class LocalMessage extends MimeMessage {
protected MessageReference mReference;
@ -37,6 +39,7 @@ public class LocalMessage extends MimeMessage {
private long mRootId;
private long messagePartId;
private String mimeType;
private PreviewType previewType;
private LocalMessage(LocalStore localStore) {
this.localStore = localStore;
@ -84,8 +87,14 @@ public class LocalMessage extends MimeMessage {
this.setInternalDate(new Date(cursor.getLong(11)));
this.setMessageId(cursor.getString(12));
final String preview = cursor.getString(14);
mPreview = (preview == null ? "" : preview);
String previewTypeString = cursor.getString(24);
DatabasePreviewType databasePreviewType = DatabasePreviewType.fromDatabaseValue(previewTypeString);
previewType = databasePreviewType.getPreviewType();
if (previewType == PreviewType.TEXT) {
mPreview = cursor.getString(14);
} else {
mPreview = "";
}
if (this.mFolder == null) {
LocalFolder f = new LocalFolder(this.localStore, cursor.getInt(13));
@ -134,7 +143,10 @@ public class LocalMessage extends MimeMessage {
super.writeTo(out);
}
@Override
public PreviewType getPreviewType() {
return previewType;
}
public String getPreview() {
return mPreview;
}

View file

@ -26,6 +26,7 @@ import com.fsck.k9.mailstore.LocalFolder.MoreMessages;
import com.fsck.k9.mailstore.StorageManager.StorageProvider;
import com.fsck.k9.mailstore.LockableDatabase.DbCallback;
import com.fsck.k9.mailstore.LockableDatabase.WrappedException;
import com.fsck.k9.message.preview.MessagePreviewCreator;
import com.fsck.k9.provider.EmailProvider;
import com.fsck.k9.provider.EmailProvider.MessageColumns;
import com.fsck.k9.search.LocalSearch;
@ -85,7 +86,7 @@ public class LocalStore extends Store implements Serializable {
"subject, sender_list, date, uid, flags, messages.id, to_list, cc_list, " +
"bcc_list, reply_to_list, attachment_count, internal_date, messages.message_id, " +
"folder_id, preview, threads.id, threads.root, deleted, read, flagged, answered, " +
"forwarded, message_part_id, mime_type ";
"forwarded, message_part_id, mime_type, preview_type ";
static final String GET_FOLDER_COLS =
"folders.id, name, visible_limit, last_updated, status, push_state, last_pushed, " +
@ -129,7 +130,7 @@ public class LocalStore extends Store implements Serializable {
*/
private static final int THREAD_FLAG_UPDATE_BATCH_SIZE = 500;
public static final int DB_VERSION = 53;
public static final int DB_VERSION = 54;
public static String getColumnNameForFlag(Flag flag) {
@ -161,6 +162,8 @@ public class LocalStore extends Store implements Serializable {
private ContentResolver mContentResolver;
private final Account mAccount;
private final MessagePreviewCreator messagePreviewCreator;
private final AttachmentCounter attachmentCounter;
/**
* local://localhost/path/to/database/uuid.db
@ -178,6 +181,9 @@ public class LocalStore extends Store implements Serializable {
database.setStorageProviderId(account.getLocalStorageProviderId());
uUid = account.getUuid();
messagePreviewCreator = MessagePreviewCreator.newInstance();
attachmentCounter = new AttachmentCounter();
database.open();
}
@ -831,6 +837,14 @@ public class LocalStore extends Store implements Serializable {
return database;
}
public MessagePreviewCreator getMessagePreviewCreator() {
return messagePreviewCreator;
}
public AttachmentCounter getAttachmentCounter() {
return attachmentCounter;
}
void notifyChange() {
Uri uri = Uri.withAppendedPath(EmailProvider.CONTENT_URI, "account/" + uUid + "/messages");
mContentResolver.notifyChange(uri, null);

View file

@ -1,43 +0,0 @@
package com.fsck.k9.mailstore;
import java.util.ArrayList;
import java.util.List;
import android.content.Context;
import com.fsck.k9.mail.Message;
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.Viewable;
class MessageInfoExtractor {
private final Context context;
private final Message message;
private List<Viewable> viewables;
private List<Part> attachments;
public MessageInfoExtractor(Context context, Message message) {
this.context = context;
this.message = message;
}
public String getMessageTextPreview() throws MessagingException {
getViewablesIfNecessary();
return MessagePreviewExtractor.extractPreview(context, viewables);
}
public int getAttachmentCount() throws MessagingException {
getViewablesIfNecessary();
return attachments.size();
}
private void getViewablesIfNecessary() throws MessagingException {
if (viewables == null) {
attachments = new ArrayList<Part>();
viewables = MessageExtractor.getViewables(message, attachments);
}
}
}

View file

@ -1,154 +0,0 @@
package com.fsck.k9.mailstore;
import java.util.List;
import android.content.Context;
import android.text.TextUtils;
import com.fsck.k9.R;
import com.fsck.k9.helper.HtmlConverter;
import com.fsck.k9.mail.Part;
import com.fsck.k9.mail.internet.MessageExtractor;
import com.fsck.k9.mail.internet.Viewable;
import com.fsck.k9.mail.internet.Viewable.Alternative;
import com.fsck.k9.mail.internet.Viewable.Html;
import com.fsck.k9.mail.internet.Viewable.MessageHeader;
import com.fsck.k9.mail.internet.Viewable.Textual;
class MessagePreviewExtractor {
private static final int MAX_PREVIEW_LENGTH = 512;
private static final int MAX_CHARACTERS_CHECKED_FOR_PREVIEW = 8192;
public static String extractPreview(Context context, List<Viewable> viewables) {
StringBuilder text = new StringBuilder();
boolean divider = false;
for (Viewable viewable : viewables) {
if (viewable instanceof Textual) {
appendText(text, viewable, divider);
divider = true;
} else if (viewable instanceof MessageHeader) {
appendMessagePreview(context, text, (MessageHeader) viewable, divider);
divider = false;
} else if (viewable instanceof Alternative) {
appendAlternative(text, (Alternative) viewable, divider);
divider = true;
}
if (hasMaxPreviewLengthBeenReached(text)) {
break;
}
}
if (hasMaxPreviewLengthBeenReached(text)) {
text.setLength(MAX_PREVIEW_LENGTH - 1);
text.append('…');
}
return text.toString();
}
private static void appendText(StringBuilder text, Viewable viewable, boolean prependDivider) {
if (viewable instanceof Textual) {
appendTextual(text, (Textual) viewable, prependDivider);
} else if (viewable instanceof Alternative) {
appendAlternative(text, (Alternative) viewable, prependDivider);
} else {
throw new IllegalArgumentException("Unknown Viewable");
}
}
private static void appendTextual(StringBuilder text, Textual textual, boolean prependDivider) {
Part part = textual.getPart();
if (prependDivider) {
appendDivider(text);
}
String textFromPart = MessageExtractor.getTextFromPart(part);
if (textFromPart == null) {
textFromPart = "";
} else if (textual instanceof Html) {
textFromPart = HtmlConverter.htmlToText(textFromPart);
}
text.append(stripTextForPreview(textFromPart));
}
private static void appendAlternative(StringBuilder text, Alternative alternative, boolean prependDivider) {
List<Viewable> textAlternative = alternative.getText().isEmpty() ?
alternative.getHtml() : alternative.getText();
boolean divider = prependDivider;
for (Viewable textViewable : textAlternative) {
appendText(text, textViewable, divider);
divider = true;
if (hasMaxPreviewLengthBeenReached(text)) {
break;
}
}
}
private static void appendMessagePreview(Context context, StringBuilder text, MessageHeader messageHeader,
boolean divider) {
if (divider) {
appendDivider(text);
}
String subject = messageHeader.getMessage().getSubject();
if (TextUtils.isEmpty(subject)) {
text.append(context.getString(R.string.preview_untitled_inner_message));
} else {
text.append(context.getString(R.string.preview_inner_message, subject));
}
}
private static void appendDivider(StringBuilder text) {
text.append(" / ");
}
private static String stripTextForPreview(String text) {
if (text == null) {
return "";
}
// 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() > MAX_CHARACTERS_CHECKED_FOR_PREVIEW) {
text = text.substring(0, MAX_CHARACTERS_CHECKED_FOR_PREVIEW);
}
// 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() <= MAX_PREVIEW_LENGTH) ? text : text.substring(0, MAX_PREVIEW_LENGTH);
}
private static boolean hasMaxPreviewLengthBeenReached(StringBuilder text) {
return text.length() >= MAX_PREVIEW_LENGTH;
}
}

View file

@ -90,6 +90,7 @@ class StoreSchemaDefinition implements LockableDatabase.SchemaDefinition {
"attachment_count INTEGER, " +
"internal_date INTEGER, " +
"message_id TEXT, " +
"preview_type TEXT default \"none\", " +
"preview TEXT, " +
"mime_type TEXT, "+
"normalized_subject_hash INTEGER, " +
@ -574,6 +575,9 @@ class StoreSchemaDefinition implements LockableDatabase.SchemaDefinition {
if (db.getVersion() < 53) {
removeNullValuesFromEmptyColumnInMessagesTable(db);
}
if (db.getVersion() < 54) {
addPreviewTypeColumn(db);
}
}
db.setVersion(LocalStore.DB_VERSION);
@ -636,4 +640,9 @@ class StoreSchemaDefinition implements LockableDatabase.SchemaDefinition {
private void removeNullValuesFromEmptyColumnInMessagesTable(SQLiteDatabase db) {
db.execSQL("UPDATE messages SET empty = 0 WHERE empty IS NULL");
}
private void addPreviewTypeColumn(SQLiteDatabase db) {
db.execSQL("ALTER TABLE messages ADD preview_type TEXT default \"none\"");
db.execSQL("UPDATE messages SET preview_type = 'text' WHERE preview IS NOT NULL");
}
}

View file

@ -0,0 +1,84 @@
package com.fsck.k9.message.preview;
import java.util.regex.Pattern;
import android.support.annotation.NonNull;
import com.fsck.k9.mail.Body;
import com.fsck.k9.mail.BodyPart;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.Multipart;
import com.fsck.k9.mail.Part;
import com.fsck.k9.mail.internet.MessageExtractor;
import static com.fsck.k9.mail.internet.MimeUtility.isSameMimeType;
class EncryptionDetector {
private static final Pattern PGP_MESSAGE_PATTERN = Pattern.compile(
"(^|\\r\\n)-----BEGIN PGP MESSAGE-----\\r\\n.*?\\r\\n-----END PGP MESSAGE-----(\\r\\n|$)", Pattern.DOTALL);
private final TextPartFinder textPartFinder;
EncryptionDetector(TextPartFinder textPartFinder) {
this.textPartFinder = textPartFinder;
}
public boolean isEncrypted(@NonNull Message message) {
return isPgpMimeOrSMimeEncrypted(message) || containsInlinePgpEncryptedText(message);
}
private boolean isPgpMimeOrSMimeEncrypted(Message message) {
return containsPartWithMimeType(message, "multipart/encrypted", "application/pkcs7-mime");
}
private boolean containsInlinePgpEncryptedText(Message message) {
Part textPart = textPartFinder.findFirstTextPart(message);
if (!isUsableTextPart(textPart)) {
return false;
}
String text = MessageExtractor.getTextFromPart(textPart);
if (text == null) {
return false;
}
return PGP_MESSAGE_PATTERN.matcher(text).find();
}
private boolean isUsableTextPart(Part textPart) {
return textPart != null && textPart.getBody() != null;
}
private boolean containsPartWithMimeType(Part part, String... wantedMimeTypes) {
String mimeType = part.getMimeType();
if (isMimeTypeAnyOf(mimeType, wantedMimeTypes)) {
return true;
}
Body body = part.getBody();
if (body instanceof Multipart) {
Multipart multipart = (Multipart) body;
for (BodyPart bodyPart : multipart.getBodyParts()) {
if (containsPartWithMimeType(bodyPart, wantedMimeTypes)) {
return true;
}
}
}
return false;
}
private boolean isMimeTypeAnyOf(String mimeType, String... wantedMimeTypes) {
for (String wantedMimeType : wantedMimeTypes) {
if (isSameMimeType(mimeType, wantedMimeType)) {
return true;
}
}
return false;
}
}

View file

@ -0,0 +1,51 @@
package com.fsck.k9.message.preview;
import android.support.annotation.NonNull;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.Part;
public class MessagePreviewCreator {
private final TextPartFinder textPartFinder;
private final PreviewTextExtractor previewTextExtractor;
private final EncryptionDetector encryptionDetector;
MessagePreviewCreator(TextPartFinder textPartFinder, PreviewTextExtractor previewTextExtractor,
EncryptionDetector encryptionDetector) {
this.textPartFinder = textPartFinder;
this.previewTextExtractor = previewTextExtractor;
this.encryptionDetector = encryptionDetector;
}
public static MessagePreviewCreator newInstance() {
TextPartFinder textPartFinder = new TextPartFinder();
PreviewTextExtractor previewTextExtractor = new PreviewTextExtractor();
EncryptionDetector encryptionDetector = new EncryptionDetector(textPartFinder);
return new MessagePreviewCreator(textPartFinder, previewTextExtractor, encryptionDetector);
}
public PreviewResult createPreview(@NonNull Message message) {
if (encryptionDetector.isEncrypted(message)) {
return PreviewResult.encrypted();
}
return extractText(message);
}
private PreviewResult extractText(Message message) {
Part textPart = textPartFinder.findFirstTextPart(message);
if (textPart == null || hasEmptyBody(textPart)) {
return PreviewResult.none();
}
String previewText = previewTextExtractor.extractPreview(textPart);
return PreviewResult.text(previewText);
}
private boolean hasEmptyBody(Part textPart) {
return textPart.getBody() == null;
}
}

View file

@ -0,0 +1,51 @@
package com.fsck.k9.message.preview;
import android.support.annotation.NonNull;
public class PreviewResult {
private final PreviewType previewType;
private final String previewText;
private PreviewResult(PreviewType previewType, String previewText) {
this.previewType = previewType;
this.previewText = previewText;
}
public static PreviewResult text(@NonNull String previewText) {
return new PreviewResult(PreviewType.TEXT, previewText);
}
public static PreviewResult encrypted() {
return new PreviewResult(PreviewType.ENCRYPTED, null);
}
public static PreviewResult none() {
return new PreviewResult(PreviewType.NONE, null);
}
public PreviewType getPreviewType() {
return previewType;
}
public boolean isPreviewTextAvailable() {
return previewType == PreviewType.TEXT;
}
public String getPreviewText() {
if (!isPreviewTextAvailable()) {
throw new IllegalStateException("Preview is not available");
}
return previewText;
}
public enum PreviewType {
NONE,
TEXT,
ENCRYPTED
}
}

View file

@ -0,0 +1,71 @@
package com.fsck.k9.message.preview;
import android.support.annotation.NonNull;
import com.fsck.k9.helper.HtmlConverter;
import com.fsck.k9.mail.Part;
import com.fsck.k9.mail.internet.MessageExtractor;
import static com.fsck.k9.mail.internet.MimeUtility.isSameMimeType;
class PreviewTextExtractor {
private static final int MAX_PREVIEW_LENGTH = 512;
private static final int MAX_CHARACTERS_CHECKED_FOR_PREVIEW = 8192;
public String extractPreview(@NonNull Part textPart) {
String text = MessageExtractor.getTextFromPart(textPart);
String plainText = convertFromHtmlIfNecessary(textPart, text);
return stripTextForPreview(plainText);
}
private String convertFromHtmlIfNecessary(Part textPart, String text) {
String mimeType = textPart.getMimeType();
if (!isSameMimeType(mimeType, "text/html")) {
return text;
}
return HtmlConverter.htmlToText(text);
}
private String stripTextForPreview(String text) {
if (text == null) {
return "";
}
// 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() > MAX_CHARACTERS_CHECKED_FOR_PREVIEW) {
text = text.substring(0, MAX_CHARACTERS_CHECKED_FOR_PREVIEW);
}
// 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() > MAX_PREVIEW_LENGTH) ? text.substring(0, MAX_PREVIEW_LENGTH - 1) + "" : text;
}
}

View file

@ -0,0 +1,82 @@
package com.fsck.k9.message.preview;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.fsck.k9.mail.Body;
import com.fsck.k9.mail.BodyPart;
import com.fsck.k9.mail.Multipart;
import com.fsck.k9.mail.Part;
import static com.fsck.k9.mail.internet.MimeUtility.isSameMimeType;
class TextPartFinder {
@Nullable
public Part findFirstTextPart(@NonNull Part part) {
String mimeType = part.getMimeType();
Body body = part.getBody();
if (body instanceof Multipart) {
Multipart multipart = (Multipart) body;
if (isSameMimeType(mimeType, "multipart/alternative")) {
return findTextPartInMultipartAlternative(multipart);
} else {
return findTextPartInMultipart(multipart);
}
} else if (isSameMimeType(mimeType, "text/plain") || isSameMimeType(mimeType, "text/html")) {
return part;
}
return null;
}
private Part findTextPartInMultipartAlternative(Multipart multipart) {
Part htmlPart = null;
for (BodyPart bodyPart : multipart.getBodyParts()) {
String mimeType = bodyPart.getMimeType();
Body body = bodyPart.getBody();
if (body instanceof Multipart) {
Part candidatePart = findFirstTextPart(bodyPart);
if (candidatePart != null) {
if (isSameMimeType(candidatePart.getMimeType(), "text/html")) {
htmlPart = candidatePart;
} else {
return candidatePart;
}
}
} else if (isSameMimeType(mimeType, "text/plain")) {
return bodyPart;
} else if (isSameMimeType(mimeType, "text/html") && htmlPart == null) {
htmlPart = bodyPart;
}
}
if (htmlPart != null) {
return htmlPart;
}
return null;
}
private Part findTextPartInMultipart(Multipart multipart) {
for (BodyPart bodyPart : multipart.getBodyParts()) {
String mimeType = bodyPart.getMimeType();
Body body = bodyPart.getBody();
if (body instanceof Multipart) {
Part candidatePart = findFirstTextPart(bodyPart);
if (candidatePart != null) {
return candidatePart;
}
} else if (isSameMimeType(mimeType, "text/plain") || isSameMimeType(mimeType, "text/html")) {
return bodyPart;
}
}
return null;
}
}

View file

@ -18,6 +18,7 @@ import com.fsck.k9.mail.Flag;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mailstore.LocalMessage;
import com.fsck.k9.message.preview.PreviewResult.PreviewType;
class NotificationContentCreator {
@ -41,12 +42,12 @@ class NotificationContentCreator {
return new NotificationContent(messageReference, displaySender, subject, preview, summary, starred);
}
private CharSequence getMessagePreview(Message message) {
private CharSequence getMessagePreview(LocalMessage message) {
String subject = message.getSubject();
String snippet = message.getPreview();
String snippet = getPreview(message);
boolean isSubjectEmpty = TextUtils.isEmpty(subject);
boolean isSnippetPresent = !TextUtils.isEmpty(snippet);
boolean isSnippetPresent = message.getPreviewType() != PreviewType.NONE;
if (isSubjectEmpty && isSnippetPresent) {
return snippet;
}
@ -65,6 +66,20 @@ class NotificationContentCreator {
return preview;
}
private String getPreview(LocalMessage message) {
PreviewType previewType = message.getPreviewType();
switch (previewType) {
case NONE:
return null;
case TEXT:
return message.getPreview();
case ENCRYPTED:
return context.getString(R.string.preview_encrypted);
}
throw new AssertionError("Unknown preview type: " + previewType);
}
private CharSequence buildMessageSummary(String sender, String subject) {
if (sender == null) {
return subject;

View file

@ -143,6 +143,7 @@ public class EmailProvider extends ContentProvider {
public static final String FLAGS = "flags";
public static final String ATTACHMENT_COUNT = "attachment_count";
public static final String FOLDER_ID = "folder_id";
public static final String PREVIEW_TYPE = "preview_type";
public static final String PREVIEW = "preview";
public static final String READ = "read";
public static final String FLAGGED = "flagged";

View file

@ -1158,5 +1158,6 @@ Please submit bug reports, contribute new features and ask questions at
<string name="crypto_incomplete_message">Incomplete message</string>
<!-- Note: This references message_view_download_remainder -->
<string name="crypto_download_complete_message_to_decrypt">Click \'Download complete message\' to allow decryption.</string>
<string name="preview_encrypted">*Encrypted*</string>
</resources>

View file

@ -0,0 +1,69 @@
package com.fsck.k9.message;
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.internet.MimeBodyPart;
import com.fsck.k9.mail.internet.MimeHeader;
import com.fsck.k9.mail.internet.MimeMessage;
import com.fsck.k9.mail.internet.MimeMultipart;
import com.fsck.k9.mail.internet.TextBody;
import com.fsck.k9.mailstore.BinaryMemoryBody;
public class MessageCreationHelper {
public static BodyPart createTextPart(String mimeType) throws MessagingException {
return createTextPart(mimeType, "");
}
public static BodyPart createTextPart(String mimeType, String text) throws MessagingException {
TextBody body = new TextBody(text);
return new MimeBodyPart(body, mimeType);
}
public static BodyPart createEmptyPart(String mimeType) throws MessagingException {
return new MimeBodyPart(null, mimeType);
}
public static BodyPart createPart(String mimeType) throws MessagingException {
BinaryMemoryBody body = new BinaryMemoryBody(new byte[0], "utf-8");
return new MimeBodyPart(body, mimeType);
}
public static BodyPart createMultipart(String mimeType, BodyPart... parts) throws MessagingException {
MimeMultipart multipart = createMultipartBody(mimeType, parts);
return new MimeBodyPart(multipart, mimeType);
}
public static Message createTextMessage(String mimeType, String text) throws MessagingException {
TextBody body = new TextBody(text);
return createMessage(mimeType, body);
}
public static Message createMultipartMessage(String mimeType, BodyPart... parts) throws MessagingException {
MimeMultipart body = createMultipartBody(mimeType, parts);
return createMessage(mimeType, body);
}
public static Message createMessage(String mimeType) throws MessagingException {
return createMessage(mimeType, null);
}
private static Message createMessage(String mimeType, Body body) throws MessagingException {
MimeMessage message = new MimeMessage();
message.setBody(body);
message.setHeader(MimeHeader.HEADER_CONTENT_TYPE, mimeType);
return message;
}
private static MimeMultipart createMultipartBody(String mimeType, BodyPart[] parts) throws MessagingException {
MimeMultipart multipart = new MimeMultipart(mimeType, "boundary");
for (BodyPart part : parts) {
multipart.addBodyPart(part);
}
return multipart;
}
}

View file

@ -0,0 +1,116 @@
package com.fsck.k9.message.preview;
import com.fsck.k9.mail.Message;
import org.junit.Before;
import org.junit.Test;
import static com.fsck.k9.message.MessageCreationHelper.createMessage;
import static com.fsck.k9.message.MessageCreationHelper.createMultipartMessage;
import static com.fsck.k9.message.MessageCreationHelper.createPart;
import static com.fsck.k9.message.MessageCreationHelper.createTextMessage;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class EncryptionDetectorTest {
private static final String CRLF = "\r\n";
private TextPartFinder textPartFinder;
private EncryptionDetector encryptionDetector;
@Before
public void setUp() throws Exception {
textPartFinder = mock(TextPartFinder.class);
encryptionDetector = new EncryptionDetector(textPartFinder);
}
@Test
public void isEncrypted_withTextPlain_shouldReturnFalse() throws Exception {
Message message = createTextMessage("text/plain", "plain text");
boolean encrypted = encryptionDetector.isEncrypted(message);
assertFalse(encrypted);
}
@Test
public void isEncrypted_withMultipartEncrypted_shouldReturnTrue() throws Exception {
Message message = createMultipartMessage("multipart/encrypted",
createPart("application/octet-stream"), createPart("application/octet-stream"));
boolean encrypted = encryptionDetector.isEncrypted(message);
assertTrue(encrypted);
}
@Test
public void isEncrypted_withSMimePart_shouldReturnTrue() throws Exception {
Message message = createMessage("application/pkcs7-mime");
boolean encrypted = encryptionDetector.isEncrypted(message);
assertTrue(encrypted);
}
@Test
public void isEncrypted_withMultipartMixedContainingSMimePart_shouldReturnTrue() throws Exception {
Message message = createMultipartMessage("multipart/mixed",
createPart("application/pkcs7-mime"), createPart("text/plain"));
boolean encrypted = encryptionDetector.isEncrypted(message);
assertTrue(encrypted);
}
@Test
public void isEncrypted_withInlinePgp_shouldReturnTrue() throws Exception {
Message message = createTextMessage("text/plain", "" +
"-----BEGIN PGP MESSAGE-----" + CRLF +
"some encrypted stuff here" + CRLF +
"-----END PGP MESSAGE-----");
when(textPartFinder.findFirstTextPart(message)).thenReturn(message);
boolean encrypted = encryptionDetector.isEncrypted(message);
assertTrue(encrypted);
}
@Test
public void isEncrypted_withPlainTextAndInlinePgp_shouldReturnTrue() throws Exception {
Message message = createTextMessage("text/plain", "" +
"preamble" + CRLF +
"-----BEGIN PGP MESSAGE-----" + CRLF +
"some encrypted stuff here" + CRLF +
"-----END PGP MESSAGE-----" + CRLF +
"epilogue");
when(textPartFinder.findFirstTextPart(message)).thenReturn(message);
boolean encrypted = encryptionDetector.isEncrypted(message);
assertTrue(encrypted);
}
@Test
public void isEncrypted_withQuotedInlinePgp_shouldReturnFalse() throws Exception {
Message message = createTextMessage("text/plain", "" +
"good talk!" + CRLF +
CRLF +
"> -----BEGIN PGP MESSAGE-----" + CRLF +
"> some encrypted stuff here" + CRLF +
"> -----END PGP MESSAGE-----" + CRLF +
CRLF +
"-- " + CRLF +
"my signature");
when(textPartFinder.findFirstTextPart(message)).thenReturn(message);
boolean encrypted = encryptionDetector.isEncrypted(message);
assertFalse(encrypted);
}
}

View file

@ -0,0 +1,94 @@
package com.fsck.k9.message.preview;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.Part;
import com.fsck.k9.mail.internet.MimeMessage;
import com.fsck.k9.message.preview.PreviewResult.PreviewType;
import org.junit.Before;
import org.junit.Test;
import static com.fsck.k9.message.MessageCreationHelper.createEmptyPart;
import static com.fsck.k9.message.MessageCreationHelper.createTextPart;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
public class MessagePreviewCreatorTest {
private TextPartFinder textPartFinder;
private PreviewTextExtractor previewTextExtractor;
private EncryptionDetector encryptionDetector;
private MessagePreviewCreator previewCreator;
@Before
public void setUp() throws Exception {
textPartFinder = mock(TextPartFinder.class);
previewTextExtractor = mock(PreviewTextExtractor.class);
encryptionDetector = mock(EncryptionDetector.class);
previewCreator = new MessagePreviewCreator(textPartFinder, previewTextExtractor, encryptionDetector);
}
@Test
public void createPreview_withEncryptedMessage() throws Exception {
Message message = createDummyMessage();
when(encryptionDetector.isEncrypted(message)).thenReturn(true);
PreviewResult result = previewCreator.createPreview(message);
assertFalse(result.isPreviewTextAvailable());
assertEquals(PreviewType.ENCRYPTED, result.getPreviewType());
verifyNoMoreInteractions(textPartFinder);
verifyNoMoreInteractions(previewTextExtractor);
}
@Test
public void createPreview_withoutTextPart() throws Exception {
Message message = createDummyMessage();
when(encryptionDetector.isEncrypted(message)).thenReturn(false);
when(textPartFinder.findFirstTextPart(message)).thenReturn(null);
PreviewResult result = previewCreator.createPreview(message);
assertFalse(result.isPreviewTextAvailable());
assertEquals(PreviewType.NONE, result.getPreviewType());
verifyNoMoreInteractions(previewTextExtractor);
}
@Test
public void createPreview_withEmptyTextPart() throws Exception {
Message message = createDummyMessage();
Part textPart = createEmptyPart("text/plain");
when(encryptionDetector.isEncrypted(message)).thenReturn(false);
when(textPartFinder.findFirstTextPart(message)).thenReturn(textPart);
PreviewResult result = previewCreator.createPreview(message);
assertFalse(result.isPreviewTextAvailable());
assertEquals(PreviewType.NONE, result.getPreviewType());
verifyNoMoreInteractions(previewTextExtractor);
}
@Test
public void createPreview_withTextPart() throws Exception {
Message message = createDummyMessage();
Part textPart = createTextPart("text/plain");
when(encryptionDetector.isEncrypted(message)).thenReturn(false);
when(textPartFinder.findFirstTextPart(message)).thenReturn(textPart);
when(previewTextExtractor.extractPreview(textPart)).thenReturn("expected");
PreviewResult result = previewCreator.createPreview(message);
assertTrue(result.isPreviewTextAvailable());
assertEquals(PreviewType.TEXT, result.getPreviewType());
assertEquals("expected", result.getPreviewText());
}
private Message createDummyMessage() {
return new MimeMessage();
}
}

View file

@ -0,0 +1,162 @@
package com.fsck.k9.message.preview;
import com.fsck.k9.mail.Part;
import com.fsck.k9.mail.internet.MimeBodyPart;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import static com.fsck.k9.message.MessageCreationHelper.createTextPart;
import static org.junit.Assert.assertEquals;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class PreviewTextExtractorTest {
private PreviewTextExtractor previewTextExtractor;
@Before
public void setUp() throws Exception {
previewTextExtractor = new PreviewTextExtractor();
}
@Test
public void extractPreview_withEmptyBody() throws Exception {
Part part = new MimeBodyPart(null, "text/plain");
//TODO: throw exception
String preview = previewTextExtractor.extractPreview(part);
assertEquals("", preview);
}
@Test
public void extractPreview_withSimpleTextPlain() throws Exception {
String text = "The quick brown fox jumps over the lazy dog";
Part part = createTextPart("text/plain", text);
String preview = previewTextExtractor.extractPreview(part);
assertEquals(text, preview);
}
@Test
public void extractPreview_withSimpleTextHtml() throws Exception {
String text = "<b>The quick brown fox jumps over the lazy dog</b>";
Part part = createTextPart("text/html", text);
String preview = previewTextExtractor.extractPreview(part);
assertEquals("The quick brown fox jumps over the lazy dog", preview);
}
@Test
public void extractPreview_withLongTextPlain() throws Exception {
String text = "" +
"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-------";
Part part = createTextPart("text/plain", text);
String preview = previewTextExtractor.extractPreview(part);
assertEquals(text.substring(0, 511) + "", preview);
}
@Test
public void extractPreview_shouldStripSignature() throws Exception {
String text = "" +
"Some text\r\n" +
"-- \r\n" +
"Signature";
Part part = createTextPart("text/plain", text);
String preview = previewTextExtractor.extractPreview(part);
assertEquals("Some text", preview);
}
@Test
public void extractPreview_shouldStripHorizontalLine() throws Exception {
String text = "" +
"line 1\r\n" +
"----\r\n" +
"line 2";
Part part = createTextPart("text/plain", text);
String preview = previewTextExtractor.extractPreview(part);
assertEquals("line 1 line 2", preview);
}
@Test
public void extractPreview_shouldStripQuoteHeaderAndQuotedText() throws Exception {
String text = "" +
"some text\r\n" +
"On 01/02/03 someone wrote\r\n" +
"> some quoted text\r\n" +
"# some other quoted text\r\n";
Part part = createTextPart("text/plain", text);
String preview = previewTextExtractor.extractPreview(part);
assertEquals("some text", preview);
}
@Test
public void extractPreview_shouldStripGenericQuoteHeader() throws Exception {
String text = "" +
"Am 13.12.2015 um 23:42 schrieb Hans:\r\n" +
"> hallo\r\n" +
"hi there\r\n";
Part part = createTextPart("text/plain", text);
String preview = previewTextExtractor.extractPreview(part);
assertEquals("hi there", preview);
}
@Test
public void extractPreview_shouldStripHorizontalRules() throws Exception {
String text = "line 1" +
"------------------------------\r\n" +
"line 2";
Part part = createTextPart("text/plain", text);
String preview = previewTextExtractor.extractPreview(part);
assertEquals("line 1 line 2", preview);
}
@Test
public void extractPreview_shouldReplaceUrl() throws Exception {
String text = "some url: https://k9mail.org/";
Part part = createTextPart("text/plain", text);
String preview = previewTextExtractor.extractPreview(part);
assertEquals("some url: ...", preview);
}
@Test
public void extractPreview_shouldCollapseAndTrimWhitespace() throws Exception {
String text = " whitespace is\t\tfun ";
Part part = createTextPart("text/plain", text);
String preview = previewTextExtractor.extractPreview(part);
assertEquals("whitespace is fun", preview);
}
}

View file

@ -0,0 +1,198 @@
package com.fsck.k9.message.preview;
import com.fsck.k9.mail.BodyPart;
import com.fsck.k9.mail.Part;
import org.junit.Before;
import org.junit.Test;
import static com.fsck.k9.message.MessageCreationHelper.createEmptyPart;
import static com.fsck.k9.message.MessageCreationHelper.createMultipart;
import static com.fsck.k9.message.MessageCreationHelper.createPart;
import static com.fsck.k9.message.MessageCreationHelper.createTextPart;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
public class TextPartFinderTest {
private TextPartFinder textPartFinder;
@Before
public void setUp() throws Exception {
textPartFinder = new TextPartFinder();
}
@Test
public void findFirstTextPart_withTextPlainPart() throws Exception {
Part part = createTextPart("text/plain");
Part result = textPartFinder.findFirstTextPart(part);
assertEquals(part, result);
}
@Test
public void findFirstTextPart_withTextHtmlPart() throws Exception {
Part part = createTextPart("text/html");
Part result = textPartFinder.findFirstTextPart(part);
assertEquals(part, result);
}
@Test
public void findFirstTextPart_withoutTextPart() throws Exception {
Part part = createPart("image/jpeg");
Part result = textPartFinder.findFirstTextPart(part);
assertNull(result);
}
@Test
public void findFirstTextPart_withMultipartAlternative() throws Exception {
BodyPart expected = createTextPart("text/plain");
Part part = createMultipart("multipart/alternative", expected, createTextPart("text/html"));
Part result = textPartFinder.findFirstTextPart(part);
assertEquals(expected, result);
}
@Test
public void findFirstTextPart_withMultipartAlternativeHtmlPartFirst() throws Exception {
BodyPart expected = createTextPart("text/plain");
Part part = createMultipart("multipart/alternative", createTextPart("text/html"), expected);
Part result = textPartFinder.findFirstTextPart(part);
assertEquals(expected, result);
}
@Test
public void findFirstTextPart_withMultipartAlternativeContainingOnlyTextHtmlPart() throws Exception {
BodyPart expected = createTextPart("text/html");
Part part = createMultipart("multipart/alternative",
createPart("image/gif"),
expected,
createTextPart("text/html"));
Part result = textPartFinder.findFirstTextPart(part);
assertEquals(expected, result);
}
@Test
public void findFirstTextPart_withMultipartAlternativeNotContainingTextPart() throws Exception {
Part part = createMultipart("multipart/alternative",
createPart("image/gif"),
createPart("application/pdf"));
Part result = textPartFinder.findFirstTextPart(part);
assertNull(result);
}
@Test
public void findFirstTextPart_withMultipartAlternativeContainingMultipartRelatedContainingTextPlain()
throws Exception {
BodyPart expected = createTextPart("text/plain");
Part part = createMultipart("multipart/alternative",
createMultipart("multipart/related", expected, createPart("image/jpeg")),
createTextPart("text/html"));
Part result = textPartFinder.findFirstTextPart(part);
assertEquals(expected, result);
}
@Test
public void findFirstTextPart_withMultipartAlternativeContainingMultipartRelatedContainingTextHtmlFirst()
throws Exception {
BodyPart expected = createTextPart("text/plain");
Part part = createMultipart("multipart/alternative",
createMultipart("multipart/related", createTextPart("text/html"), createPart("image/jpeg")),
expected);
Part result = textPartFinder.findFirstTextPart(part);
assertEquals(expected, result);
}
@Test
public void findFirstTextPart_withMultipartMixedContainingTextPlain() throws Exception {
BodyPart expected = createTextPart("text/plain");
Part part = createMultipart("multipart/mixed", createPart("image/jpeg"), expected);
Part result = textPartFinder.findFirstTextPart(part);
assertEquals(expected, result);
}
@Test
public void findFirstTextPart_withMultipartMixedContainingTextHtmlFirst() throws Exception {
BodyPart expected = createTextPart("text/html");
Part part = createMultipart("multipart/mixed", expected, createTextPart("text/plain"));
Part result = textPartFinder.findFirstTextPart(part);
assertEquals(expected, result);
}
@Test
public void findFirstTextPart_withMultipartMixedNotContainingTextPart() throws Exception {
Part part = createMultipart("multipart/mixed", createPart("image/jpeg"), createPart("image/gif"));
Part result = textPartFinder.findFirstTextPart(part);
assertNull(result);
}
@Test
public void findFirstTextPart_withMultipartMixedContainingMultipartAlternative() throws Exception {
BodyPart expected = createTextPart("text/plain");
Part part = createMultipart("multipart/mixed",
createPart("image/jpeg"),
createMultipart("multipart/alternative", expected, createTextPart("text/html")),
createTextPart("text/plain"));
Part result = textPartFinder.findFirstTextPart(part);
assertEquals(expected, result);
}
@Test
public void findFirstTextPart_withMultipartMixedContainingMultipartAlternativeWithTextPlainPartLast()
throws Exception {
BodyPart expected = createTextPart("text/plain");
Part part = createMultipart("multipart/mixed",
createMultipart("multipart/alternative", createTextPart("text/html"), expected));
Part result = textPartFinder.findFirstTextPart(part);
assertEquals(expected, result);
}
@Test
public void findFirstTextPart_withMultipartAlternativeContainingEmptyTextPlainPart()
throws Exception {
BodyPart expected = createEmptyPart("text/plain");
Part part = createMultipart("multipart/alternative", expected, createTextPart("text/html"));
Part result = textPartFinder.findFirstTextPart(part);
assertEquals(expected, result);
}
@Test
public void findFirstTextPart_withMultipartMixedContainingEmptyTextHtmlPart()
throws Exception {
BodyPart expected = createEmptyPart("text/html");
Part part = createMultipart("multipart/mixed", expected, createTextPart("text/plain"));
Part result = textPartFinder.findFirstTextPart(part);
assertEquals(expected, result);
}
}

View file

@ -9,6 +9,7 @@ import com.fsck.k9.mail.Address;
import com.fsck.k9.mail.Flag;
import com.fsck.k9.mail.Message.RecipientType;
import com.fsck.k9.mailstore.LocalMessage;
import com.fsck.k9.message.preview.PreviewResult.PreviewType;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -76,6 +77,7 @@ public class NotificationContentCreatorTest {
@Test
public void createFromMessage_withoutPreview() throws Exception {
when(message.getPreviewType()).thenReturn(PreviewType.NONE);
when(message.getPreview()).thenReturn(null);
NotificationContent content = contentCreator.createFromMessage(account, message);
@ -84,6 +86,18 @@ public class NotificationContentCreatorTest {
assertEquals(SUBJECT, content.preview.toString());
}
@Test
public void createFromMessage_withEncryptedMessage() throws Exception {
when(message.getPreviewType()).thenReturn(PreviewType.ENCRYPTED);
when(message.getPreview()).thenReturn(null);
NotificationContent content = contentCreator.createFromMessage(account, message);
String encrypted = "*Encrypted*";
assertEquals(SUBJECT, content.subject);
assertEquals(SUBJECT + "\n" + encrypted, content.preview.toString());
}
@Test
public void createFromMessage_withoutSender() throws Exception {
when(message.getFrom()).thenReturn(null);
@ -118,6 +132,7 @@ public class NotificationContentCreatorTest {
public void createFromMessage_withoutEmptyMessage() throws Exception {
when(message.getFrom()).thenReturn(null);
when(message.getSubject()).thenReturn(null);
when(message.getPreviewType()).thenReturn(PreviewType.NONE);
when(message.getPreview()).thenReturn(null);
NotificationContent content = contentCreator.createFromMessage(account, message);
@ -145,6 +160,7 @@ public class NotificationContentCreatorTest {
LocalMessage message = mock(LocalMessage.class);
when(message.makeMessageReference()).thenReturn(messageReference);
when(message.getPreviewType()).thenReturn(PreviewType.TEXT);
when(message.getPreview()).thenReturn(PREVIEW);
when(message.getSubject()).thenReturn(SUBJECT);
when(message.getFrom()).thenReturn(new Address[] { new Address(SENDER_ADDRESS, SENDER_NAME) });