Merge pull request #5152 from ByteHamster/message-header-view

Show message headers in fragment
This commit is contained in:
cketti 2021-02-23 14:25:10 +01:00 committed by GitHub
commit 3cd28c5b2c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 447 additions and 322 deletions

View file

@ -11,4 +11,5 @@ val mailStoreModule = module {
single { K9BackendStorageFactory(get(), get(), get(), get()) }
factory { SpecialLocalFoldersCreator(preferences = get(), localStoreProvider = get()) }
single { MessageStoreProvider(messageStoreFactory = get()) }
single { MessageRepository(preferences = get(), localStoreProvider = get()) }
}

View file

@ -37,6 +37,7 @@ import com.fsck.k9.mail.internet.MimeMessage;
import com.fsck.k9.mail.internet.MimeMultipart;
import com.fsck.k9.mail.internet.MimeUtility;
import com.fsck.k9.mail.internet.SizeAware;
import com.fsck.k9.mail.message.MessageHeaderCollector;
import com.fsck.k9.mail.message.MessageHeaderParser;
import com.fsck.k9.mailstore.LockableDatabase.DbCallback;
import com.fsck.k9.mailstore.LockableDatabase.WrappedException;
@ -660,7 +661,7 @@ public class LocalFolder {
}
private void parseHeaderBytes(Part part, byte[] header) throws MessagingException {
MessageHeaderParser.parse(part, new ByteArrayInputStream(header));
MessageHeaderParser.parse(new ByteArrayInputStream(header), part::addRawHeader);
}
public String getMessageUidById(final long id) throws MessagingException {

View file

@ -131,7 +131,7 @@ public class LocalMessage extends MimeMessage {
byte[] header = cursor.getBlob(LocalStore.MSG_INDEX_HEADER_DATA);
if (header != null) {
MessageHeaderParser.parse(this, new ByteArrayInputStream(header));
MessageHeaderParser.parse(new ByteArrayInputStream(header), this::addRawHeader);
} else {
Timber.d("No headers available for this message!");
}

View file

@ -0,0 +1,7 @@
package com.fsck.k9.mailstore
import com.fsck.k9.controller.MessageReference
import java.lang.RuntimeException
class MessageNotFoundException(val messageReference: MessageReference) :
RuntimeException("Message not found: $messageReference")

View file

@ -0,0 +1,40 @@
package com.fsck.k9.mailstore
import com.fsck.k9.Account
import com.fsck.k9.Preferences
import com.fsck.k9.controller.MessageReference
import com.fsck.k9.mail.Header
import com.fsck.k9.mail.internet.MimeHeader
import com.fsck.k9.mail.message.MessageHeaderParser
class MessageRepository(
private val preferences: Preferences,
private val localStoreProvider: LocalStoreProvider
) {
fun getHeaders(messageReference: MessageReference): List<Header> {
val accountUuid = messageReference.accountUuid
val account = preferences.getAccount(accountUuid) ?: error("Account not found: $accountUuid")
return account.database.execute(false) { db ->
db.rawQuery(
"SELECT message_parts.header FROM messages" +
" LEFT JOIN message_parts ON (messages.message_part_id = message_parts.id)" +
" WHERE messages.folder_id = ? AND messages.uid = ?",
arrayOf(messageReference.folderId.toString(), messageReference.uid),
).use { cursor ->
if (!cursor.moveToFirst()) throw MessageNotFoundException(messageReference)
val headerBytes = cursor.getBlob(0)
val header = MimeHeader()
MessageHeaderParser.parse(headerBytes.inputStream()) { name, value ->
header.addRawHeader(name, value)
}
header.headers
}
}
}
private val Account.database: LockableDatabase
get() = localStoreProvider.getInstance(this).database
}

View file

@ -285,6 +285,10 @@
android:name=".ui.settings.account.AccountSettingsActivity"
android:label="@string/account_settings_title_fmt" />
<activity
android:name=".ui.messagesource.MessageSourceActivity"
android:label="@string/show_headers_action" />
<receiver
android:name=".service.StorageReceiver"
android:enabled="true">

View file

@ -9,6 +9,7 @@ dependencies {
api "androidx.appcompat:appcompat:${versions.androidxAppCompat}"
api "androidx.navigation:navigation-fragment-ktx:${versions.androidxNavigation}"
api "androidx.navigation:navigation-ui-ktx:${versions.androidxNavigation}"
api "androidx.lifecycle:lifecycle-livedata-ktx:${versions.androidxLifecycle}"
implementation "androidx.core:core-ktx:${versions.androidxCore}"
implementation "com.jakewharton.timber:timber:${versions.timber}"

View file

@ -0,0 +1,56 @@
package com.fsck.k9.ui.base.loader
import androidx.lifecycle.LiveData
import androidx.lifecycle.liveData
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
const val LOADING_INDICATOR_DELAY = 500L
/**
* Load data in an I/O thread. Updates the returned [LiveData] with the current loading state.
*
* If loading takes longer than [LOADING_INDICATOR_DELAY] the [LoaderState.Loading] state will be emitted so the UI can
* display a loading indicator. We use a delay so fast loads won't flash a loading indicator.
* If an exception is thrown during loading the [LoaderState.Error] state is emitted.
* If the data was loaded successfully [LoaderState.Data] will be emitted containing the data.
*/
fun <T> liveDataLoader(block: CoroutineScope.() -> T): LiveData<LoaderState<T>> = liveData {
coroutineScope {
val job = launch {
delay(LOADING_INDICATOR_DELAY)
// Emit loading state if loading took longer than configured delay. If the data was loaded faster than that,
// this coroutine will have been canceled before the next line is executed.
emit(LoaderState.Loading)
}
val finalState = try {
val data = withContext(Dispatchers.IO) {
block()
}
LoaderState.Data(data)
} catch (e: Exception) {
Timber.e(e, "Error loading data")
LoaderState.Error
}
// Cancel job that emits Loading state
job.cancelAndJoin()
emit(finalState)
}
}
sealed class LoaderState<out T> {
object Loading : LoaderState<Nothing>()
object Error : LoaderState<Nothing>()
class Data<T>(val data: T) : LoaderState<T>()
}

View file

@ -0,0 +1,50 @@
package com.fsck.k9.ui.base.loader
import android.view.View
import androidx.core.view.isVisible
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
/**
* Use to observe the [LiveData] returned by [liveDataLoader].
*
* Works with separate views for the [LoaderState.Loading], [LoaderState.Error], and [LoaderState.Data] states. The
* view associated with the current state is made visible and the others are hidden. For the [LoaderState.Data] state
* the [displayData] function is also called.
*/
fun <T> LiveData<LoaderState<T>>.observeLoading(
owner: LifecycleOwner,
loadingView: View,
errorView: View,
dataView: View,
displayData: (T) -> Unit
) {
observe(owner, LoaderStateObserver(loadingView, errorView, dataView, displayData))
}
private class LoaderStateObserver<T>(
private val loadingView: View,
private val errorView: View,
private val dataView: View,
private val displayData: (T) -> Unit
) : Observer<LoaderState<T>> {
private val allViews = setOf(loadingView, errorView, dataView)
override fun onChanged(state: LoaderState<T>?) {
when (state) {
is LoaderState.Loading -> loadingView.show()
is LoaderState.Error -> errorView.show()
is LoaderState.Data -> {
dataView.show()
displayData(state.data)
}
}
}
private fun View.show() {
for (view in allViews) {
view.isVisible = view === this
}
}
}

View file

@ -12,6 +12,7 @@ import com.fsck.k9.ui.endtoend.endToEndUiModule
import com.fsck.k9.ui.folders.foldersUiModule
import com.fsck.k9.ui.managefolders.manageFoldersUiModule
import com.fsck.k9.ui.messagelist.messageListUiModule
import com.fsck.k9.ui.messagesource.messageSourceModule
import com.fsck.k9.ui.settings.settingsUiModule
import com.fsck.k9.ui.uiModule
import com.fsck.k9.view.viewModule
@ -31,5 +32,6 @@ val uiModules = listOf(
accountModule,
autodiscoveryProvidersXmlModule,
viewModule,
changelogUiModule
changelogUiModule,
messageSourceModule
)

View file

@ -46,6 +46,7 @@ import com.fsck.k9.ui.base.K9Activity
import com.fsck.k9.ui.base.Theme
import com.fsck.k9.ui.managefolders.ManageFoldersActivity
import com.fsck.k9.ui.messagelist.DefaultFolderProvider
import com.fsck.k9.ui.messagesource.MessageSourceActivity
import com.fsck.k9.ui.messageview.MessageViewFragment
import com.fsck.k9.ui.messageview.MessageViewFragment.MessageViewFragmentListener
import com.fsck.k9.ui.messageview.PlaceholderFragment
@ -899,9 +900,8 @@ open class MessageList :
} else if (id == R.id.move_to_drafts) {
messageViewFragment!!.onMoveToDrafts()
return true
} else if (id == R.id.show_headers || id == R.id.hide_headers) {
messageViewFragment!!.onToggleAllHeadersView()
updateMenu()
} else if (id == R.id.show_headers) {
startActivity(MessageSourceActivity.createLaunchIntent(this, messageViewFragment!!.messageReference))
return true
}
@ -969,7 +969,6 @@ open class MessageList :
menu.findItem(R.id.toggle_unread).isVisible = false
menu.findItem(R.id.toggle_message_view_theme).isVisible = false
menu.findItem(R.id.show_headers).isVisible = false
menu.findItem(R.id.hide_headers).isVisible = false
} else {
// hide prev/next buttons in split mode
if (displayMode != DisplayMode.MESSAGE_VIEW) {
@ -1051,12 +1050,6 @@ open class MessageList :
if (messageViewFragment!!.isOutbox) {
menu.findItem(R.id.move_to_drafts).isVisible = true
}
if (messageViewFragment!!.allHeadersVisible()) {
menu.findItem(R.id.show_headers).isVisible = false
} else {
menu.findItem(R.id.hide_headers).isVisible = false
}
}
// Set visibility of menu items related to the message list

View file

@ -22,6 +22,6 @@ inline fun FragmentActivity.fragmentTransactionWithBackStack(
}
}
fun Fragment.withArguments(vararg argumentPairs: Pair<String, Any?>) = apply {
fun <T : Fragment> T.withArguments(vararg argumentPairs: Pair<String, Any?>) = apply {
arguments = bundleOf(*argumentPairs)
}

View file

@ -0,0 +1,8 @@
package com.fsck.k9.ui.messagesource
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
val messageSourceModule = module {
viewModel { MessageHeadersViewModel(messageRepository = get()) }
}

View file

@ -0,0 +1,75 @@
package com.fsck.k9.ui.messagesource
import android.graphics.Typeface
import android.os.Bundle
import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.text.style.StyleSpan
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import com.fsck.k9.controller.MessageReference
import com.fsck.k9.mail.Header
import com.fsck.k9.mail.internet.MimeUtility
import com.fsck.k9.ui.R
import com.fsck.k9.ui.base.loader.observeLoading
import com.fsck.k9.ui.withArguments
import org.koin.androidx.viewmodel.ext.android.viewModel
class MessageHeadersFragment : Fragment() {
private val messageHeadersViewModel: MessageHeadersViewModel by viewModel()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.message_view_headers, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val messageReferenceString = requireArguments().getString(ARG_REFERENCE)
?: error("Missing argument $ARG_REFERENCE")
val messageReference = MessageReference.parse(messageReferenceString)
?: error("Invalid message reference: $messageReferenceString")
val messageHeaderView = view.findViewById<TextView>(R.id.message_source)
messageHeadersViewModel.loadHeaders(messageReference).observeLoading(
owner = this,
loadingView = view.findViewById(R.id.message_headers_loading),
errorView = view.findViewById(R.id.message_headers_error),
dataView = view.findViewById(R.id.message_headers_data)
) { headers ->
populateHeadersList(messageHeaderView, headers)
}
}
private fun populateHeadersList(messageHeaderView: TextView, headers: List<Header>) {
val sb = SpannableStringBuilder()
var first = true
for ((name, value) in headers) {
if (!first) {
sb.append("\n")
} else {
first = false
}
val boldSpan = StyleSpan(Typeface.BOLD)
val label = SpannableString("$name: ")
label.setSpan(boldSpan, 0, label.length, 0)
sb.append(label)
sb.append(MimeUtility.unfoldAndDecode(value))
}
messageHeaderView.text = sb
}
companion object {
private const val ARG_REFERENCE = "reference"
fun newInstance(reference: MessageReference): MessageHeadersFragment {
return MessageHeadersFragment().withArguments(
ARG_REFERENCE to reference.toIdentityString()
)
}
}
}

View file

@ -0,0 +1,25 @@
package com.fsck.k9.ui.messagesource
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import com.fsck.k9.controller.MessageReference
import com.fsck.k9.mail.Header
import com.fsck.k9.mailstore.MessageRepository
import com.fsck.k9.ui.base.loader.LoaderState
import com.fsck.k9.ui.base.loader.liveDataLoader
private typealias MessageHeaderState = LoaderState<List<Header>>
class MessageHeadersViewModel(private val messageRepository: MessageRepository) : ViewModel() {
private var messageHeaderLiveData: LiveData<MessageHeaderState>? = null
fun loadHeaders(messageReference: MessageReference): LiveData<MessageHeaderState> {
return messageHeaderLiveData ?: loadMessageHeader(messageReference).also { messageHeaderLiveData = it }
}
private fun loadMessageHeader(messageReference: MessageReference): LiveData<MessageHeaderState> {
return liveDataLoader {
messageRepository.getHeaders(messageReference)
}
}
}

View file

@ -0,0 +1,55 @@
package com.fsck.k9.ui.messagesource
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.MenuItem
import androidx.fragment.app.commit
import com.fsck.k9.controller.MessageReference
import com.fsck.k9.ui.R
import com.fsck.k9.ui.base.K9Activity
/**
* Temporary Activity used until the fragment can be displayed in the main activity.
*/
class MessageSourceActivity : K9Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setLayout(R.layout.message_view_headers_activity)
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
if (savedInstanceState == null) {
addMessageHeadersFragment()
}
}
private fun addMessageHeadersFragment() {
val messageReferenceString = intent.getStringExtra(ARG_REFERENCE) ?: error("Missing argument $ARG_REFERENCE")
val messageReference = MessageReference.parse(messageReferenceString)
?: error("Invalid message reference: $messageReferenceString")
val fragment = MessageHeadersFragment.newInstance(messageReference)
supportFragmentManager.commit {
add(R.id.message_headers_fragment, fragment)
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return if (item.itemId == android.R.id.home) {
finish()
true
} else {
super.onOptionsItemSelected(item)
}
}
companion object {
private const val ARG_REFERENCE = "reference"
fun createLaunchIntent(context: Context, messageReference: MessageReference): Intent {
return Intent(context, MessageSourceActivity::class.java).apply {
putExtra(ARG_REFERENCE, messageReference.toIdentityString())
}
}
}
}

View file

@ -224,14 +224,6 @@ public class MessageTopView extends LinearLayout {
mHeaderContainer.setOnMenuItemClickListener(listener);
}
public void showAllHeaders() {
mHeaderContainer.onShowAdditionalHeaders();
}
public boolean additionalHeadersVisible() {
return mHeaderContainer.additionalHeadersVisible();
}
private void hideHeaderView() {
mHeaderContainer.setVisibility(View.GONE);
}

View file

@ -304,14 +304,6 @@ public class MessageViewFragment extends Fragment implements ConfirmationDialogF
}
}
public void onToggleAllHeadersView() {
mMessageView.getMessageHeaderView().onShowAdditionalHeaders();
}
public boolean allHeadersVisible() {
return mMessageView.getMessageHeaderView().additionalHeadersVisible();
}
private void delete() {
if (mMessage != null) {
// Disable the delete button after it's tapped (to try to prevent

View file

@ -68,7 +68,6 @@ public class MessageHeader extends LinearLayout implements OnClickListener, OnLo
private View mChip;
private CheckBox mFlagged;
private int defaultSubjectColor;
private TextView mAdditionalHeadersView;
private View singleMessageOptionIcon;
private View mAnsweredIcon;
private View mForwardedIcon;
@ -76,7 +75,6 @@ public class MessageHeader extends LinearLayout implements OnClickListener, OnLo
private Account mAccount;
private FontSizes mFontSizes = K9.getFontSizes();
private Contacts mContacts;
private SavedState mSavedState;
private MessageHelper mMessageHelper;
private ContactPictureLoader mContactsPictureLoader;
@ -112,7 +110,6 @@ public class MessageHeader extends LinearLayout implements OnClickListener, OnLo
singleMessageOptionIcon = findViewById(R.id.icon_single_message_options);
mSubjectView = findViewById(R.id.subject);
mAdditionalHeadersView = findViewById(R.id.additional_headers_view);
mChip = findViewById(R.id.chip);
mDateView = findViewById(R.id.date);
mFlagged = findViewById(R.id.flagged);
@ -120,7 +117,6 @@ public class MessageHeader extends LinearLayout implements OnClickListener, OnLo
defaultSubjectColor = mSubjectView.getCurrentTextColor();
mFontSizes.setViewTextSize(mSubjectView, mFontSizes.getMessageViewSubject());
mFontSizes.setViewTextSize(mDateView, mFontSizes.getMessageViewDate());
mFontSizes.setViewTextSize(mAdditionalHeadersView, mFontSizes.getMessageViewAdditionalHeaders());
mFontSizes.setViewTextSize(mFromView, mFontSizes.getMessageViewSender());
mFontSizes.setViewTextSize(mToView, mFontSizes.getMessageViewTo());
@ -147,8 +143,6 @@ public class MessageHeader extends LinearLayout implements OnClickListener, OnLo
mCryptoStatusIcon.setOnClickListener(this);
mMessageHelper = MessageHelper.getInstance(mContext);
hideAdditionalHeaders();
}
@Override
@ -222,51 +216,6 @@ public class MessageHeader extends LinearLayout implements OnClickListener, OnLo
mFlagged.setOnClickListener(listener);
}
public boolean additionalHeadersVisible() {
return (mAdditionalHeadersView != null &&
mAdditionalHeadersView.getVisibility() == View.VISIBLE);
}
/**
* Clear the text field for the additional headers display if they are
* not shown, to save UI resources.
*/
private void hideAdditionalHeaders() {
mAdditionalHeadersView.setVisibility(View.GONE);
mAdditionalHeadersView.setText("");
}
/**
* Set up and then show the additional headers view. Called by
* {@link #onShowAdditionalHeaders()}
* (when switching between messages).
*/
private void showAdditionalHeaders() {
Integer messageToShow = null;
try {
// Retrieve additional headers
List<Header> additionalHeaders = mMessage.getHeaders();
if (!additionalHeaders.isEmpty()) {
// Show the additional headers that we have got.
populateAdditionalHeadersView(additionalHeaders);
mAdditionalHeadersView.setVisibility(View.VISIBLE);
} else {
// All headers have been downloaded, but there are no additional headers.
messageToShow = R.string.message_no_additional_headers_available;
}
} catch (Exception e) {
messageToShow = R.string.message_additional_headers_retrieval_failed;
}
// Show a message to the user, if any
if (messageToShow != null) {
Toast toast = Toast.makeText(mContext, messageToShow, Toast.LENGTH_LONG);
toast.setGravity(Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL, 0, 0);
toast.show();
}
}
public void populate(final Message message, final Account account, boolean showStar) {
Address fromAddress = null;
Address[] fromAddresses = message.getFrom();
@ -334,15 +283,6 @@ public class MessageHeader extends LinearLayout implements OnClickListener, OnLo
mChip.setBackgroundColor(mAccount.getChipColor());
setVisibility(View.VISIBLE);
if (mSavedState != null) {
if (mSavedState.additionalHeadersVisible) {
showAdditionalHeaders();
}
mSavedState = null;
} else {
hideAdditionalHeaders();
}
}
public void setSubject(@NonNull String subject) {
@ -381,20 +321,6 @@ public class MessageHeader extends LinearLayout implements OnClickListener, OnLo
mCryptoStatusIcon.setColorFilter(color);
}
public void onShowAdditionalHeaders() {
int currentVisibility = mAdditionalHeadersView.getVisibility();
if (currentVisibility == View.VISIBLE) {
hideAdditionalHeaders();
expand(mToView, false);
expand(mCcView, false);
} else {
showAdditionalHeaders();
expand(mToView, true);
expand(mCcView, true);
}
}
private void updateAddressField(TextView v, CharSequence text, View label) {
boolean hasText = !TextUtils.isEmpty(text);
@ -416,91 +342,6 @@ public class MessageHeader extends LinearLayout implements OnClickListener, OnLo
}
}
/**
* 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.
* <p/>
* This method is always called from within the UI thread by
* {@link #showAdditionalHeaders()}.
*/
private void populateAdditionalHeadersView(final List<Header> additionalHeaders) {
SpannableStringBuilder sb = new SpannableStringBuilder();
boolean first = true;
for (Header additionalHeader : additionalHeaders) {
if (!first) {
sb.append("\n");
} else {
first = false;
}
StyleSpan boldSpan = new StyleSpan(Typeface.BOLD);
SpannableString label = new SpannableString(additionalHeader.getName() + ": ");
label.setSpan(boldSpan, 0, label.length(), 0);
sb.append(label);
sb.append(MimeUtility.unfoldAndDecode(additionalHeader.getValue()));
}
mAdditionalHeadersView.setText(sb);
}
@Override
public Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
SavedState savedState = new SavedState(superState);
savedState.additionalHeadersVisible = additionalHeadersVisible();
return savedState;
}
@Override
public void onRestoreInstanceState(Parcelable state) {
if(!(state instanceof SavedState)) {
super.onRestoreInstanceState(state);
return;
}
SavedState savedState = (SavedState)state;
super.onRestoreInstanceState(savedState.getSuperState());
mSavedState = savedState;
}
static class SavedState extends BaseSavedState {
boolean additionalHeadersVisible;
public static final Parcelable.Creator<SavedState> CREATOR =
new Parcelable.Creator<SavedState>() {
@Override
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
@Override
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
SavedState(Parcelable superState) {
super(superState);
}
private SavedState(Parcel in) {
super(in);
this.additionalHeadersVisible = (in.readInt() != 0);
}
@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeInt((this.additionalHeadersVisible) ? 1 : 0);
}
}
public void setOnCryptoClickListener(OnCryptoClickListener onCryptoClickListener) {
this.onCryptoClickListener = onCryptoClickListener;
}

View file

@ -277,18 +277,6 @@
</LinearLayout>
<TextView
android:id="@+id/additional_headers_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:layout_marginRight="6dip"
android:singleLine="false"
android:ellipsize="none"
android:textColor="?android:attr/textColorSecondary"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textIsSelectable="true" />
</LinearLayout>
</LinearLayout>

View file

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:id="@+id/message_headers_data"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
tools:visibility="visible">
<TextView
android:id="@+id/message_source"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="none"
android:padding="16dp"
android:singleLine="false"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondary"
android:textIsSelectable="true"
tools:text="From: alice@domain.example\nTo: bob@domain.example\nSubject: Hi Bob" />
</ScrollView>
<ProgressBar
android:id="@+id/message_headers_loading"
style="@style/Widget.AppCompat.ProgressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"
android:visibility="gone"
tools:visibility="gone" />
<TextView
android:id="@+id/message_headers_error"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/generic_loading_error"
android:visibility="gone"
tools:visibility="gone" />
</FrameLayout>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<include layout="@layout/toolbar"/>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/message_headers_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

View file

@ -150,10 +150,6 @@
app:showAsAction="never"
android:title="@string/show_headers_action"/>
<item android:id="@+id/hide_headers"
app:showAsAction="never"
android:title="@string/hide_headers_action"/>
<!-- always -->
<item
android:id="@+id/compose"

View file

@ -150,7 +150,6 @@ Please submit bug reports, contribute new features and ask questions at
<string name="unflag_action">Remove star</string>
<string name="copy_action">Copy</string>
<string name="show_headers_action">Show headers</string>
<string name="hide_headers_action">Hide headers</string>
<plurals name="copy_address_to_clipboard">
<item quantity="one">Address copied to clipboard</item>
<item quantity="other">Addresses copied to clipboard</item>
@ -1261,4 +1260,6 @@ You can keep this message and use it as a backup for your secret key. If you wan
<!-- permissions -->
<string name="permission_contacts_rationale_title">Allow access to contacts</string>
<string name="permission_contacts_rationale_message">To be able to provide contact suggestions and to display contact names and photos, the app needs access to your contacts.</string>
<string name="generic_loading_error">An error occurred while loading the data</string>
</resources>

View file

@ -1,116 +0,0 @@
package com.fsck.k9.mail.message;
import java.io.IOException;
import java.io.InputStream;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Part;
import org.apache.james.mime4j.MimeException;
import org.apache.james.mime4j.parser.ContentHandler;
import org.apache.james.mime4j.parser.MimeStreamParser;
import org.apache.james.mime4j.stream.BodyDescriptor;
import org.apache.james.mime4j.stream.Field;
import org.apache.james.mime4j.stream.MimeConfig;
public class MessageHeaderParser {
public static void parse(final Part part, InputStream headerInputStream) throws MessagingException {
MimeStreamParser parser = getMimeStreamParser();
parser.setContentHandler(new MessageHeaderParserContentHandler(part));
try {
parser.parse(headerInputStream);
} catch (MimeException me) {
throw new MessagingException("Error parsing headers", me);
} catch (IOException e) {
throw new MessagingException("I/O error parsing headers", e);
}
}
private static MimeStreamParser getMimeStreamParser() {
MimeConfig parserConfig = new MimeConfig.Builder()
.setMaxHeaderLen(-1)
.setMaxLineLen(-1)
.setMaxHeaderCount(-1)
.build();
return new MimeStreamParser(parserConfig);
}
private static class MessageHeaderParserContentHandler implements ContentHandler {
private final Part part;
public MessageHeaderParserContentHandler(Part part) {
this.part = part;
}
@Override
public void field(Field rawField) throws MimeException {
String name = rawField.getName();
String raw = rawField.getRaw().toString();
part.addRawHeader(name, raw);
}
@Override
public void startMessage() throws MimeException {
/* do nothing */
}
@Override
public void endMessage() throws MimeException {
/* do nothing */
}
@Override
public void startBodyPart() throws MimeException {
/* do nothing */
}
@Override
public void endBodyPart() throws MimeException {
/* do nothing */
}
@Override
public void startHeader() throws MimeException {
/* do nothing */
}
@Override
public void endHeader() throws MimeException {
/* do nothing */
}
@Override
public void preamble(InputStream is) throws MimeException, IOException {
/* do nothing */
}
@Override
public void epilogue(InputStream is) throws MimeException, IOException {
/* do nothing */
}
@Override
public void startMultipart(BodyDescriptor bd) throws MimeException {
/* do nothing */
}
@Override
public void endMultipart() throws MimeException {
/* do nothing */
}
@Override
public void body(BodyDescriptor bd, InputStream is) throws MimeException, IOException {
/* do nothing */
}
@Override
public void raw(InputStream is) throws MimeException, IOException {
/* do nothing */
}
}
}

View file

@ -0,0 +1,52 @@
package com.fsck.k9.mail.message
import com.fsck.k9.mail.MessagingException
import java.io.IOException
import java.io.InputStream
import org.apache.james.mime4j.MimeException
import org.apache.james.mime4j.parser.AbstractContentHandler
import org.apache.james.mime4j.parser.MimeStreamParser
import org.apache.james.mime4j.stream.Field
import org.apache.james.mime4j.stream.MimeConfig
object MessageHeaderParser {
@Throws(MessagingException::class)
@JvmStatic
fun parse(headerInputStream: InputStream, collector: MessageHeaderCollector) {
val parser = createMimeStreamParser().apply {
setContentHandler(MessageHeaderParserContentHandler(collector))
}
try {
parser.parse(headerInputStream)
} catch (me: MimeException) {
throw MessagingException("Error parsing headers", me)
} catch (e: IOException) {
throw MessagingException("I/O error parsing headers", e)
}
}
private fun createMimeStreamParser(): MimeStreamParser {
val parserConfig = MimeConfig.Builder()
.setMaxHeaderLen(-1)
.setMaxLineLen(-1)
.setMaxHeaderCount(-1)
.build()
return MimeStreamParser(parserConfig)
}
private class MessageHeaderParserContentHandler(
private val collector: MessageHeaderCollector
) : AbstractContentHandler() {
override fun field(rawField: Field) {
val name = rawField.name
val raw = rawField.raw.toString()
collector.addRawHeader(name, raw)
}
}
}
fun interface MessageHeaderCollector {
fun addRawHeader(name: String, raw: String)
}