diff --git a/res/layout/account_setup_incoming.xml b/res/layout/account_setup_incoming.xml index 30a1fbca6..a362694e3 100644 --- a/res/layout/account_setup_incoming.xml +++ b/res/layout/account_setup_incoming.xml @@ -212,6 +212,17 @@ android:text="@string/account_setup_incoming_other_label" /> + + - - + + + - + Add star Remove star Copy - + Show full header + Hide full header + Mark as unread Move to Folders @@ -260,7 +262,12 @@ Welcome to K-9 Mail setup. K-9 is an open source mail client for Android origin Show pictures Fetching attachment. Unable to find viewer for %s. - + + + Not all headers have been downloaded or saved. Select \"Save all headers locally\" in the account\'s incoming server settings to enable this for the future. + All headers have been downloaded, but there are no additional headers to show. + The retrieval of additional headers from the database or mail server failed. + Folders New folder @@ -341,6 +348,9 @@ Welcome to K-9 Mail setup. K-9 is an open source mail client for Android origin Wi-Fi Other + Downloading of mail headers + Save all headers locally + Expunge messages Immediately after delete or move During each poll @@ -764,6 +774,7 @@ Welcome to K-9 Mail setup. K-9 is an open source mail client for Android origin Message sender Message receiver (To) Message receiver (CC) + Additional headers Message subject Message time Message date diff --git a/res/xml/font_preferences.xml b/res/xml/font_preferences.xml index 1a9385745..b8c7624e1 100644 --- a/res/xml/font_preferences.xml +++ b/res/xml/font_preferences.xml @@ -86,6 +86,13 @@ android:entryValues="@array/font_values" android:dialogTitle="@string/font_size_message_view_cc" /> + + additionalHeaders = getAdditionalHeaders(mMessage); + + if (!additionalHeaders.isEmpty()) + { + // Show the additional headers that we have got. + setupAdditionalHeadersView(additionalHeaders); + mAdditionalHeadersView.setVisibility(View.VISIBLE); + } + + if (!allHeadersDownloaded) + { + /* + * Tell the user about the "save all headers" setting + * + * NOTE: This is only a temporary solution... in fact, + * the system should download headers on-demand when they + * have not been saved in their entirety initially. + */ + messageToShow = R.string.message_additional_headers_not_downloaded; + } + else if (additionalHeaders.isEmpty()) + { + // All headers have been downloaded, but there are no additional headers. + messageToShow = R.string.message_no_additional_headers_available; + } + } + catch(MessagingException e) + { + messageToShow = R.string.message_additional_headers_retrieval_failed; + } + + // Show a message to the user, if any + if (messageToShow != null) + { + Toast toast = Toast.makeText(MessageView.this, messageToShow, Toast.LENGTH_LONG); + toast.setGravity(Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL, 0, 0); + toast.show(); + } + } + }); + } + + /** + * Set up the additional headers text view with the supplied header data. + * + * @param additionalHeaders + * List of header entries. Each entry consists of a header + * name and a header value. Header names may appear multiple + * times. + * + * This method is always called from within the UI thread by + * {@link #showAdditionalHeaders()}. + */ + private void setupAdditionalHeadersView(final List additionalHeaders) + { + SpannableStringBuilder sb = new SpannableStringBuilder(); + boolean first = true; + for (HeaderEntry additionalHeader : additionalHeaders) + { + if (!first) + { + sb.append("\n"); + } + else + { + first = false; + } + + StyleSpan boldSpan = new StyleSpan(Typeface.BOLD); + SpannableString label = new SpannableString(additionalHeader.label + ": "); + label.setSpan(boldSpan, 0, label.length(), 0); + + sb.append(label); + sb.append(additionalHeader.value.replaceAll("\\r\\n", "")); + } + + mAdditionalHeadersView.setText(sb); + } } class Attachment @@ -460,6 +593,8 @@ public class MessageView extends K9Activity implements OnClickListener mSubjectView = (TextView)findViewById(R.id.subject); defaultSubjectColor = mSubjectView.getCurrentTextColor(); + mAdditionalHeadersView = (TextView)findViewById(R.id.additional_headers_view); + chip = findViewById(R.id.chip); mDateView = (TextView)findViewById(R.id.date); @@ -503,7 +638,8 @@ public class MessageView extends K9Activity implements OnClickListener mSubjectView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, mFontSizes.getMessageViewSubject()); mTimeView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, mFontSizes.getMessageViewTime()); mDateView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, mFontSizes.getMessageViewDate()); - + mAdditionalHeadersView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, mFontSizes.getMessageViewAdditionalHeaders()); + mAdditionalHeadersView.setVisibility(View.GONE); mAttachments.setVisibility(View.GONE); mAttachmentIcon.setVisibility(View.GONE); @@ -515,6 +651,9 @@ public class MessageView extends K9Activity implements OnClickListener setOnClickListener(R.id.next); setOnClickListener(R.id.previous); + // To show full header + setOnClickListener(R.id.header_container); + setOnClickListener(R.id.reply_scrolling); // setOnClickListener(R.id.reply_all_scrolling); setOnClickListener(R.id.delete_scrolling); @@ -830,7 +969,7 @@ public class MessageView extends K9Activity implements OnClickListener { mMessage.setFlag(Flag.FLAGGED, !mMessage.isSet(Flag.FLAGGED)); setHeaders(mAccount, mMessage.getFolder().getName(), mMessage.getUid(), mMessage); - setMenuFlag(); + prepareMenuItems(); } catch (MessagingException me) { @@ -877,6 +1016,43 @@ public class MessageView extends K9Activity implements OnClickListener startActivityForResult(intent, ACTIVITY_CHOOSE_FOLDER_COPY); } + private void onShowAdditionalHeaders() + { + int currentVisibility = mAdditionalHeadersView.getVisibility(); + + if (currentVisibility == View.VISIBLE) + { + mHandler.hideAdditionalHeaders(); + } + else + { + mHandler.showAdditionalHeaders(); + } + } + + private List getAdditionalHeaders(final Message message) + throws MessagingException + { + List additionalHeaders = new LinkedList(); + + // Do not include the following headers, since they are always visible anyway + Set headerNames = new HashSet(message.getHeaderNames()); + headerNames.remove("To"); + headerNames.remove("From"); + headerNames.remove("Cc"); + headerNames.remove("Subject"); + + for (String headerName : headerNames) + { + String[] headerValues = message.getHeader(headerName); + for (String headerValue : headerValues) + { + additionalHeaders.add(new HeaderEntry(headerName, headerValue)); + } + } + return additionalHeaders; + } + @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { @@ -1100,6 +1276,9 @@ public class MessageView extends K9Activity implements OnClickListener case R.id.show_pictures: onShowPictures(); break; + case R.id.header_container: + onShowAdditionalHeaders(); + break; } } @@ -1135,6 +1314,9 @@ public class MessageView extends K9Activity implements OnClickListener case R.id.copy: onCopy(); break; + case R.id.show_full_header: + onShowAdditionalHeaders(); + break; default: return super.onOptionsItemSelected(item); } @@ -1147,7 +1329,7 @@ public class MessageView extends K9Activity implements OnClickListener super.onCreateOptionsMenu(menu); getMenuInflater().inflate(R.menu.message_view_option, menu); optionsMenu = menu; - setMenuFlag(); + prepareMenuItems(); if (MessagingController.getInstance(getApplication()).isCopyCapable(mAccount) == false) { menu.findItem(R.id.copy).setVisible(false); @@ -1162,11 +1344,11 @@ public class MessageView extends K9Activity implements OnClickListener @Override public boolean onPrepareOptionsMenu(Menu menu) { - setMenuFlag(); + prepareMenuItems(); return super.onPrepareOptionsMenu(menu); } - private void setMenuFlag() + private void prepareMenuItems() { Menu menu = optionsMenu; if (menu != null) @@ -1176,6 +1358,13 @@ public class MessageView extends K9Activity implements OnClickListener { flagItem.setTitle((mMessage.isSet(Flag.FLAGGED) ? R.string.unflag_action : R.string.flag_action)); } + + MenuItem additionalHeadersItem = menu.findItem(R.id.show_full_header); + if (additionalHeadersItem != null) + { + additionalHeadersItem.setTitle((mAdditionalHeadersView.getVisibility() == View.VISIBLE) ? + R.string.hide_full_header_action : R.string.show_full_header_action); + } } } @@ -1328,10 +1517,12 @@ public class MessageView extends K9Activity implements OnClickListener String timeText = getTimeFormat().format(message.getSentDate()); String toText = Address.toFriendly(message.getRecipients(RecipientType.TO)); String ccText = Address.toFriendly(message.getRecipients(RecipientType.CC)); + int color = mAccount.getChipColor(); boolean hasAttachments = ((LocalMessage) message).getAttachmentCount() > 0; boolean isDownloading = !message.isSet(Flag.X_DOWNLOADED_FULL); boolean unread = !message.isSet(Flag.SEEN); + mHandler.setHeaders(subjectText, fromText, dateText, @@ -1344,6 +1535,12 @@ public class MessageView extends K9Activity implements OnClickListener isDownloading, message.isSet(Flag.FLAGGED), message.isSet(Flag.ANSWERED)); + + // Update additional headers display, if visible + if (mAdditionalHeadersView.getVisibility() == View.VISIBLE) + { + mHandler.showAdditionalHeaders(); + } } class Listener extends MessagingListener diff --git a/src/com/fsck/k9/activity/setup/AccountSetupIncoming.java b/src/com/fsck/k9/activity/setup/AccountSetupIncoming.java index 4c8afbd8f..072cc36ac 100644 --- a/src/com/fsck/k9/activity/setup/AccountSetupIncoming.java +++ b/src/com/fsck/k9/activity/setup/AccountSetupIncoming.java @@ -85,6 +85,7 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener private CheckBox compressionMobile; private CheckBox compressionWifi; private CheckBox compressionOther; + private CheckBox saveAllHeaders; private CheckBox pushPollOnConnect; private Spinner idleRefreshPeriod; private Spinner folderPushLimit; @@ -130,6 +131,7 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener compressionMobile = (CheckBox)findViewById(R.id.compression_mobile); compressionWifi = (CheckBox)findViewById(R.id.compression_wifi); compressionOther = (CheckBox)findViewById(R.id.compression_other); + saveAllHeaders = (CheckBox)findViewById(R.id.save_all_headers); pushPollOnConnect = (CheckBox)findViewById(R.id.push_poll_on_connect); idleRefreshPeriod = (Spinner)findViewById(R.id.idle_refresh_period); @@ -389,6 +391,7 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener compressionMobile.setChecked(mAccount.useCompression(Account.TYPE_MOBILE)); compressionWifi.setChecked(mAccount.useCompression(Account.TYPE_WIFI)); compressionOther.setChecked(mAccount.useCompression(Account.TYPE_OTHER)); + if (uri.getHost() != null) { mServerView.setText(uri.getHost()); @@ -402,6 +405,8 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener { updatePortFromSecurityType(); } + + saveAllHeaders.setChecked(mAccount.isSaveAllHeaders()); pushPollOnConnect.setChecked(mAccount.isPushPollOnConnect()); SpinnerHelper.initSpinner(this, idleRefreshPeriod, R.array.idle_refresh_period_entries, R.array.idle_refresh_period_values, String.valueOf(mAccount.getIdleRefreshMinutes())); @@ -557,6 +562,7 @@ public class AccountSetupIncoming extends K9Activity implements OnClickListener mAccount.setCompression(Account.TYPE_MOBILE, compressionMobile.isChecked()); mAccount.setCompression(Account.TYPE_WIFI, compressionWifi.isChecked()); mAccount.setCompression(Account.TYPE_OTHER, compressionOther.isChecked()); + mAccount.setSaveAllHeaders(saveAllHeaders.isChecked()); mAccount.setPushPollOnConnect(pushPollOnConnect.isChecked()); String idleRefreshPeriodValue = SpinnerHelper.getSpinnerValue(idleRefreshPeriod); try diff --git a/src/com/fsck/k9/activity/setup/FontSizeSettings.java b/src/com/fsck/k9/activity/setup/FontSizeSettings.java index 12b9060da..b962be05e 100644 --- a/src/com/fsck/k9/activity/setup/FontSizeSettings.java +++ b/src/com/fsck/k9/activity/setup/FontSizeSettings.java @@ -32,6 +32,7 @@ public class FontSizeSettings extends K9PreferenceActivity private static final String PREFERENCE_MESSAGE_VIEW_SENDER_FONT = "message_view_sender_font"; private static final String PREFERENCE_MESSAGE_VIEW_TO_FONT = "message_view_to_font"; private static final String PREFERENCE_MESSAGE_VIEW_CC_FONT = "message_view_cc_font"; + private static final String PREFERENCE_MESSAGE_VIEW_ADDITIONAL_HEADERS_FONT = "message_view_additional_headers_font"; private static final String PREFERENCE_MESSAGE_VIEW_SUBJECT_FONT = "message_view_subject_font"; private static final String PREFERENCE_MESSAGE_VIEW_TIME_FONT = "message_view_time_font"; private static final String PREFERENCE_MESSAGE_VIEW_DATE_FONT = "message_view_date_font"; @@ -47,6 +48,7 @@ public class FontSizeSettings extends K9PreferenceActivity private ListPreference mMessageViewSender; private ListPreference mMessageViewTo; private ListPreference mMessageViewCC; + private ListPreference mMessageViewAdditionalHeaders; private ListPreference mMessageViewSubject; private ListPreference mMessageViewTime; private ListPreference mMessageViewDate; @@ -85,6 +87,7 @@ public class FontSizeSettings extends K9PreferenceActivity mMessageViewSender = initializeListPreference(PREFERENCE_MESSAGE_VIEW_SENDER_FONT, fontSizes.getMessageViewSender()); mMessageViewTo = initializeListPreference(PREFERENCE_MESSAGE_VIEW_TO_FONT, fontSizes.getMessageViewTo()); mMessageViewCC = initializeListPreference(PREFERENCE_MESSAGE_VIEW_CC_FONT, fontSizes.getMessageViewCC()); + mMessageViewAdditionalHeaders = initializeListPreference(PREFERENCE_MESSAGE_VIEW_ADDITIONAL_HEADERS_FONT, fontSizes.getMessageViewAdditionalHeaders()); mMessageViewSubject = initializeListPreference(PREFERENCE_MESSAGE_VIEW_SUBJECT_FONT, fontSizes.getMessageViewSubject()); mMessageViewTime = initializeListPreference(PREFERENCE_MESSAGE_VIEW_TIME_FONT, fontSizes.getMessageViewTime()); mMessageViewDate = initializeListPreference(PREFERENCE_MESSAGE_VIEW_DATE_FONT, fontSizes.getMessageViewDate()); @@ -112,6 +115,7 @@ public class FontSizeSettings extends K9PreferenceActivity fontSizes.setMessageViewSender(Integer.parseInt(mMessageViewSender.getValue())); fontSizes.setMessageViewTo(Integer.parseInt(mMessageViewTo.getValue())); fontSizes.setMessageViewCC(Integer.parseInt(mMessageViewCC.getValue())); + fontSizes.setMessageViewAdditionalHeaders(Integer.parseInt(mMessageViewAdditionalHeaders.getValue())); fontSizes.setMessageViewSubject(Integer.parseInt(mMessageViewSubject.getValue())); fontSizes.setMessageViewTime(Integer.parseInt(mMessageViewTime.getValue())); fontSizes.setMessageViewDate(Integer.parseInt(mMessageViewDate.getValue())); diff --git a/src/com/fsck/k9/mail/Flag.java b/src/com/fsck/k9/mail/Flag.java index 706abf5d2..9291fcecf 100644 --- a/src/com/fsck/k9/mail/Flag.java +++ b/src/com/fsck/k9/mail/Flag.java @@ -15,9 +15,6 @@ public enum Flag /* * The following flags are for internal library use only. - * TODO Eventually we should creates a Flags class that extends ArrayList that allows - * these flags and Strings to represent user defined flags. At that point the below - * flags should become user defined flags. */ /** * Delete and remove from the LocalStore immediately. @@ -51,4 +48,11 @@ public enum Flag * Indicates that the copy of a message to the Sent folder has started. */ X_REMOTE_COPY_STARTED, + + /** + * Indicates that all headers of the message have been stored in the + * database. If this is false, additional headers might be retrieved from + * the server (if the message is still there). + */ + X_GOT_ALL_HEADERS, } diff --git a/src/com/fsck/k9/mail/Message.java b/src/com/fsck/k9/mail/Message.java index 06bc8fc18..fa9ea2981 100644 --- a/src/com/fsck/k9/mail/Message.java +++ b/src/com/fsck/k9/mail/Message.java @@ -3,6 +3,7 @@ package com.fsck.k9.mail; import java.util.Date; import java.util.HashSet; +import java.util.Set; import com.fsck.k9.activity.MessageReference; @@ -123,6 +124,8 @@ public abstract class Message implements Part, Body public abstract String[] getHeader(String name) throws MessagingException; + public abstract Set getHeaderNames(); + public abstract void removeHeader(String name) throws MessagingException; public abstract void setBody(Body body) throws MessagingException; diff --git a/src/com/fsck/k9/mail/internet/MimeHeader.java b/src/com/fsck/k9/mail/internet/MimeHeader.java index 0fdbff0f1..4c66f642f 100644 --- a/src/com/fsck/k9/mail/internet/MimeHeader.java +++ b/src/com/fsck/k9/mail/internet/MimeHeader.java @@ -9,8 +9,7 @@ import java.io.BufferedWriter; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; -import java.util.ArrayList; -import java.util.List; +import java.util.*; public class MimeHeader { @@ -69,9 +68,9 @@ public class MimeHeader addHeader(name, value); } - public List getHeaderNames() + public Set getHeaderNames() { - ArrayList names = new ArrayList(); + Set names = new HashSet(); for (Field field : mFields) { names.add(field.name); diff --git a/src/com/fsck/k9/mail/internet/MimeMessage.java b/src/com/fsck/k9/mail/internet/MimeMessage.java index 4b32a6b2d..74062980a 100644 --- a/src/com/fsck/k9/mail/internet/MimeMessage.java +++ b/src/com/fsck/k9/mail/internet/MimeMessage.java @@ -459,7 +459,8 @@ public class MimeMessage extends Message mHeader.removeHeader(name); } - public List getHeaderNames() + @Override + public Set getHeaderNames() { return mHeader.getHeaderNames(); } diff --git a/src/com/fsck/k9/mail/store/LocalStore.java b/src/com/fsck/k9/mail/store/LocalStore.java index 65430dccd..5b9b96ffe 100644 --- a/src/com/fsck/k9/mail/store/LocalStore.java +++ b/src/com/fsck/k9/mail/store/LocalStore.java @@ -1441,6 +1441,13 @@ public class LocalStore extends Store implements Serializable "LocalStore.getMessages(int, int, MessageRetrievalListener) not yet implemented"); } + /** + * Populate the header fields of the given list of messages by reading + * the saved header data from the database. + * + * @param messages + * The messages whose headers should be loaded. + */ private void populateHeaders(List messages) { Cursor cursor = null; @@ -1635,6 +1642,11 @@ public class LocalStore extends Store implements Serializable * assigned and it matches the uid of an existing message then this message will replace the * old message. It is implemented as a delete/insert. This functionality is used in saving * of drafts and re-synchronization of updated server messages. + * + * NOTE that although this method is located in the LocalStore class, it is not guaranteed + * that the messages supplied as parameters are actually {@link LocalMessage} instances (in + * fact, in most cases, they are not). Therefore, if you want to make local changes only to a + * message, retrieve the appropriate local message instance first (if it already exists). */ @Override public void appendMessages(Message[] messages) throws MessagingException @@ -1647,6 +1659,11 @@ public class LocalStore extends Store implements Serializable * assigned and it matches the uid of an existing message then this message will replace the * old message. It is implemented as a delete/insert. This functionality is used in saving * of drafts and re-synchronization of updated server messages. + * + * NOTE that although this method is located in the LocalStore class, it is not guaranteed + * that the messages supplied as parameters are actually {@link LocalMessage} instances (in + * fact, in most cases, they are not). Therefore, if you want to make local changes only to a + * message, retrieve the appropriate local message instance first (if it already exists). */ private void appendMessages(Message[] messages, boolean copy) throws MessagingException { @@ -1863,12 +1880,19 @@ public class LocalStore extends Store implements Serializable } } - private void saveHeaders(long id, MimeMessage message) + /** + * Save the headers of the given message. Note that the message is not + * necessarily a {@link LocalMessage} instance. + */ + private void saveHeaders(long id, MimeMessage message) throws MessagingException { + boolean saveAllHeaders = mAccount.isSaveAllHeaders(); + boolean gotAdditionalHeaders = false; + deleteHeaders(id); for (String name : message.getHeaderNames()) { - if (HEADERS_TO_SAVE.contains(name)) + if (saveAllHeaders || HEADERS_TO_SAVE.contains(name)) { String[] values = message.getHeader(name); for (String value : values) @@ -1880,6 +1904,22 @@ public class LocalStore extends Store implements Serializable mDb.insert("headers", "name", cv); } } + else + { + gotAdditionalHeaders = true; + } + } + + if (!gotAdditionalHeaders) + { + // Remember that all headers for this message have been saved, so it is + // not necessary to download them again in case the user wants to see all headers. + List appendedFlags = new ArrayList(); + appendedFlags.addAll(Arrays.asList(message.getFlags())); + appendedFlags.add(Flag.X_GOT_ALL_HEADERS); + + mDb.execSQL("UPDATE messages " + "SET flags = ? " + " WHERE id = ?", new Object[] + { Utility.combine(appendedFlags.toArray(), ',').toUpperCase(), id } ); } } @@ -2698,9 +2738,7 @@ public class LocalStore extends Store implements Serializable public void addHeader(String name, String value) { if (!mHeadersLoaded) - { loadHeaders(); - } super.addHeader(name, value); } @@ -2717,7 +2755,6 @@ public class LocalStore extends Store implements Serializable { if (!mHeadersLoaded) loadHeaders(); - return super.getHeader(name); } @@ -2729,8 +2766,12 @@ public class LocalStore extends Store implements Serializable super.removeHeader(name); } - - + @Override + public Set getHeaderNames() { + if (!mHeadersLoaded) + loadHeaders(); + return super.getHeaderNames(); + } } public class LocalAttachmentBodyPart extends MimeBodyPart