Merge pull request #959 from k9mail/display_encrypted_in_message_preview
Display "*Encrypted*" in message preview
This commit is contained in:
commit
a5ad84481d
25 changed files with 1169 additions and 224 deletions
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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) });
|
||||
|
|
Loading…
Reference in a new issue