diff --git a/app/src/main/java/eu/faircode/email/AdapterMessage.java b/app/src/main/java/eu/faircode/email/AdapterMessage.java index e055fcd75d..982bf9d125 100644 --- a/app/src/main/java/eu/faircode/email/AdapterMessage.java +++ b/app/src/main/java/eu/faircode/email/AdapterMessage.java @@ -5858,10 +5858,20 @@ public class AdapterMessage extends RecyclerView.Adapter 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 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(); diff --git a/app/src/main/java/eu/faircode/email/FragmentDialogUnsubscribe.java b/app/src/main/java/eu/faircode/email/FragmentDialogUnsubscribe.java new file mode 100644 index 0000000000..d249fcbc81 --- /dev/null +++ b/app/src/main/java/eu/faircode/email/FragmentDialogUnsubscribe.java @@ -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 . + + 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() { + @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(); + } +} diff --git a/app/src/main/java/eu/faircode/email/MessageHelper.java b/app/src/main/java/eu/faircode/email/MessageHelper.java index ec34d5aaaf..8b2c0fbf18 100644 --- a/app/src/main/java/eu/faircode/email/MessageHelper.java +++ b/app/src/main/java/eu/faircode/email/MessageHelper.java @@ -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 UNZIP_FORMATS = Collections.unmodifiableList(Arrays.asList( "zip", "gz", "tar.gz" @@ -2761,7 +2762,7 @@ public class MessageHelper { } } - Pair 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)); diff --git a/app/src/main/res/layout/dialog_unsubscribe.xml b/app/src/main/res/layout/dialog_unsubscribe.xml new file mode 100644 index 0000000000..32186e4d3b --- /dev/null +++ b/app/src/main/res/layout/dialog_unsubscribe.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + \ No newline at end of file