Prepare one click unsubscribe

pull/214/head
M66B 5 months ago
parent 9b9db2f1dd
commit 93cbde951c

@ -5858,10 +5858,20 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
}
private void onActionUnsubscribe(TupleMessageEx message) {
Uri uri = Uri.parse(message.unsubscribe);
onOpenLink(uri,
context.getString(R.string.title_legend_show_unsubscribe),
EntityFolder.JUNK.equals(message.folderType));
if (message.unsubscribe.startsWith(MessageHelper.ONE_CLICK_UNSUBSCRIBE)) {
Bundle args = new Bundle();
args.putString("uri", message.unsubscribe.substring(MessageHelper.ONE_CLICK_UNSUBSCRIBE.length()));
args.putString("from", MessageHelper.formatAddresses(message.from));
FragmentDialogUnsubscribe fragment = new FragmentDialogUnsubscribe();
fragment.setArguments(args);
fragment.show(parentFragment.getParentFragmentManager(), "unsubscribe");
} else {
Uri uri = Uri.parse(message.unsubscribe);
onOpenLink(uri,
context.getString(R.string.title_legend_show_unsubscribe),
EntityFolder.JUNK.equals(message.folderType));
}
}
private void onActionVerifyDecrypt(TupleMessageEx message, boolean auto) {

@ -3429,8 +3429,6 @@ class Core {
String[] authentication = helper.getAuthentication();
MessageHelper.MessageParts parts = helper.getMessageParts();
Pair<String, Boolean> unsubscribe = helper.getListUnsubscribe();
EntityMessage message = new EntityMessage();
message.account = folder.account;
message.folder = folder.id;
@ -3464,7 +3462,7 @@ class Core {
message.bcc = helper.getBcc();
message.reply = helper.getReply();
message.list_post = helper.getListPost();
message.unsubscribe = (unsubscribe == null ? null : unsubscribe.first);
message.unsubscribe = helper.getListUnsubscribe();
message.headers = helper.getHeaders();
message.infrastructure = helper.getInfrastructure();
message.subject = helper.getSubject();
@ -4567,8 +4565,6 @@ class Core {
String[] authentication = helper.getAuthentication();
MessageHelper.MessageParts parts = helper.getMessageParts();
Pair<String, Boolean> unsubscribe = helper.getListUnsubscribe();
message = new EntityMessage();
message.account = folder.account;
message.folder = folder.id;
@ -4615,7 +4611,7 @@ class Core {
message.bcc = helper.getBcc();
message.reply = helper.getReply();
message.list_post = helper.getListPost();
message.unsubscribe = (unsubscribe == null ? null : unsubscribe.first);
message.unsubscribe = helper.getListUnsubscribe();
message.autocrypt = helper.getAutocrypt();
if (download_headers)
message.headers = helper.getHeaders();

@ -0,0 +1,118 @@
package eu.faircode.email;
/*
This file is part of FairEmail.
FairEmail is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
FairEmail is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with FairEmail. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018-2024 by Marcel Bokhorst (M66B)
*/
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import java.io.IOException;
import java.net.URL;
import javax.net.ssl.HttpsURLConnection;
public class FragmentDialogUnsubscribe extends FragmentDialogBase {
private static final int UNSUBSCRIBE_TIMEOUT = 20 * 1000;
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
final Bundle args = getArguments();
final String uri = args.getString("uri");
final String from = args.getString("from");
final Context context = getContext();
View view = LayoutInflater.from(context).inflate(R.layout.dialog_unsubscribe, null);
final TextView tvSender = view.findViewById(R.id.tvSender);
final TextView tvUri = view.findViewById(R.id.tvUri);
tvSender.setText(from);
tvUri.setVisibility(BuildConfig.DEBUG ? View.VISIBLE : View.GONE);
tvUri.setText(uri);
AlertDialog.Builder builder = new AlertDialog.Builder(context)
.setView(view)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
new SimpleTask<String>() {
@Override
protected String onExecute(Context context, Bundle args) throws Throwable {
final String uri = args.getString("uri");
final String request = "List-Unsubscribe=One-Click";
// https://datatracker.ietf.org/doc/html/rfc8058
URL url = new URL(uri);
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
connection.setRequestMethod("POST");
connection.setDoInput(true);
connection.setDoOutput(true);
connection.setReadTimeout(UNSUBSCRIBE_TIMEOUT);
connection.setConnectTimeout(UNSUBSCRIBE_TIMEOUT);
ConnectionHelper.setUserAgent(context, connection);
connection.setRequestProperty("Content-Length", Integer.toString(request.length()));
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
connection.connect();
try {
connection.getOutputStream().write(request.getBytes());
int status = connection.getResponseCode();
if (status != HttpsURLConnection.HTTP_OK) {
String error = "Error " + status + ": " + connection.getResponseMessage();
String detail = Helper.readStream(connection.getErrorStream());
throw new IOException(error + " " + detail);
}
return Helper.readStream(connection.getInputStream());
} finally {
connection.disconnect();
}
}
@Override
protected void onExecuted(Bundle args, String output) {
ToastEx.makeText(context, R.string.title_completed, Toast.LENGTH_LONG).show();
}
@Override
protected void onException(Bundle args, Throwable ex) {
Log.unexpectedError(getParentFragment(), ex);
}
}.execute(FragmentDialogUnsubscribe.this, args, "unsubscribe");
}
})
.setNegativeButton(android.R.string.cancel, null);
return builder.create();
}
}

@ -182,6 +182,7 @@ public class MessageHelper {
static final int DEFAULT_THREAD_RANGE = 7; // 2^7 = 128 days
static final int MAX_UNZIP_COUNT = 20;
static final long MAX_UNZIP_SIZE = 10 * 1024 * 1024L;
static final String ONE_CLICK_UNSUBSCRIBE = "oneclick:";
static final List<String> UNZIP_FORMATS = Collections.unmodifiableList(Arrays.asList(
"zip", "gz", "tar.gz"
@ -2761,7 +2762,7 @@ public class MessageHelper {
}
}
Pair<String, Boolean> getListUnsubscribe() throws MessagingException {
String getListUnsubscribe() throws MessagingException {
ensureHeaders();
try {
@ -2777,12 +2778,12 @@ public class MessageHelper {
return null;
// https://datatracker.ietf.org/doc/html/rfc8058
boolean onclick = false;
boolean oneclick = false;
String post = imessage.getHeader("List-Unsubscribe-Post", null);
if (post != null) {
post = MimeUtility.unfold(post);
post = decodeMime(post);
onclick = "List-Unsubscribe=One-Click".equalsIgnoreCase(post.trim());
oneclick = "List-Unsubscribe=One-Click".equalsIgnoreCase(post.trim());
}
String link = null;
@ -2828,10 +2829,13 @@ public class MessageHelper {
e = list.indexOf('>', s + 1);
}
if (true || link != null && !link.startsWith("https://"))
oneclick = false;
if (link != null)
return new Pair<>(link, onclick);
return (oneclick ? ONE_CLICK_UNSUBSCRIBE : "") + link;
if (mailto != null)
return new Pair<>(mailto, onclick);
return mailto;
if (!BuildConfig.PLAY_STORE_RELEASE)
Log.i(new IllegalArgumentException("List-Unsubscribe: " + list));

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="24dp"
android:scrollbarStyle="outsideOverlay">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tvUnsubscribe"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableStart="@drawable/twotone_unsubscribe_24"
android:drawablePadding="6dp"
android:labelFor="@+id/etNotes"
android:text="@string/title_unsubscribe"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvSender"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="Sender"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvUnsubscribe" />
<TextView
android:id="@+id/tvUri"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="Uri"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvSender" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
Loading…
Cancel
Save