Initial commit
This commit is contained in:
commit
0765a9801b
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
.vscode/
|
||||||
|
build
|
71
CMakeLists.txt
Normal file
71
CMakeLists.txt
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
cmake_minimum_required(VERSION 3.16 FATAL_ERROR)
|
||||||
|
|
||||||
|
project(kmail-unsubscribe VERSION "1.0.0")
|
||||||
|
|
||||||
|
set(KF_MIN_VERSION "6.0.0")
|
||||||
|
set(QT_REQUIRED_VERSION "6.6.0")
|
||||||
|
|
||||||
|
# Extra CMake Modules
|
||||||
|
find_package(ECM ${KF_MIN_VERSION} CONFIG REQUIRED)
|
||||||
|
set(CMAKE_MODULE_PATH ${ECM_MODULE_DIR} ${ECM_KDE_MODULE_DIR})
|
||||||
|
include(ECMQtDeclareLoggingCategory)
|
||||||
|
include(KDEInstallDirs)
|
||||||
|
include(KDECMakeSettings)
|
||||||
|
|
||||||
|
find_package(Qt6 ${QT_REQUIRED_VERSION} CONFIG REQUIRED Network Widgets)
|
||||||
|
|
||||||
|
find_package(KF6Config ${KF_MIN_VERSION} CONFIG REQUIRED)
|
||||||
|
find_package(KF6GuiAddons ${KF_MIN_VERSION} CONFIG REQUIRED)
|
||||||
|
find_package(KF6XmlGui ${KF_MIN_VERSION} CONFIG REQUIRED)
|
||||||
|
find_package(KF6Parts ${KF_MIN_VERSION} CONFIG REQUIRED)
|
||||||
|
find_package(KF6KIO ${KF_MIN_VERSION} CONFIG REQUIRED)
|
||||||
|
|
||||||
|
|
||||||
|
find_package(KPim6MailCommon ${KF_MIN_VERSION} CONFIG REQUIRED)
|
||||||
|
find_package(KPim6MessageCore ${KF_MIN_VERSION} CONFIG REQUIRED)
|
||||||
|
find_package(KPim6MessageViewer ${KF_MIN_VERSION} CONFIG REQUIRED)
|
||||||
|
find_package(KPim6Libkdepim ${KF_MIN_VERSION} CONFIG REQUIRED)
|
||||||
|
find_package(KPim6PimCommonAkonadi ${KF_MIN_VERSION} CONFIG REQUIRED)
|
||||||
|
find_package(KF6I18n ${KF_MIN_VERSION} NO_MODULE)
|
||||||
|
ki18n_install(po)
|
||||||
|
|
||||||
|
set(kmail_unsubscribe_SRCS
|
||||||
|
unsubscribemanager.cpp
|
||||||
|
unsubscribemanager.h
|
||||||
|
unsubscribeplugin.cpp
|
||||||
|
unsubscribeplugin.h
|
||||||
|
unsubscribeplugininterface.cpp
|
||||||
|
unsubscribeplugininterface.h
|
||||||
|
oneclickunsubscribejob.cpp
|
||||||
|
oneclickunsubscribejob.h
|
||||||
|
)
|
||||||
|
|
||||||
|
ecm_qt_declare_logging_category(
|
||||||
|
kmail_unsubscribe_SRCS
|
||||||
|
HEADER "unsubscribe_debug.h"
|
||||||
|
IDENTIFIER "UnsubscribePlugin"
|
||||||
|
CATEGORY_NAME "xyz.datagirl.kpim.unsubscribe"
|
||||||
|
DESCRIPTION "Unsubscribe Plugin"
|
||||||
|
DEFAULT_SEVERITY Info
|
||||||
|
EXPORT
|
||||||
|
)
|
||||||
|
|
||||||
|
set(BUILD_SHARED_LIBS ON)
|
||||||
|
|
||||||
|
kcoreaddons_add_plugin(kmail_unsubscribe
|
||||||
|
SOURCES ${kmail_unsubscribe_SRCS}
|
||||||
|
INSTALL_NAMESPACE pim6/messageviewer/viewerplugin)
|
||||||
|
target_link_libraries(kmail_unsubscribe
|
||||||
|
KPim6::PimCommon
|
||||||
|
KPim6::Libkdepim
|
||||||
|
KPim6::PimCommonAkonadi
|
||||||
|
KPim6::MessageViewer
|
||||||
|
KF6::XmlGui
|
||||||
|
KF6::KIOCore
|
||||||
|
KF6::KIOGui
|
||||||
|
KF6::KIOWidgets
|
||||||
|
KF6::I18n)
|
||||||
|
set_target_properties(kmail_unsubscribe PROPERTIES
|
||||||
|
CXX_STANDARD 17
|
||||||
|
CXX_STANDARD_REQUIRED ON
|
||||||
|
)
|
39
build-msgs.sh
Executable file
39
build-msgs.sh
Executable file
|
@ -0,0 +1,39 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
APPNAME="kmail_unsubscribe"
|
||||||
|
|
||||||
|
DEFAULT_XGETTEXT="$(command -v xgettext)"
|
||||||
|
XGETTEXT_PROGRAM="${XGETTEXT:-${DEFAULT_XGETTEXT}}"
|
||||||
|
if [ -z "$XGETTEXT_PROGRAM" ] ; then
|
||||||
|
echo "error: Couldn't find xgettext. Set \$XGETTEXT to its path, or make sure you have gettext installed." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Pulled from https://invent.kde.org/sysadmin/l10n-scripty/-/blob/master/extract-messages.sh
|
||||||
|
do_xgettext() {
|
||||||
|
$XGETTEXT_PROGRAM --copyright-holder="snow flurry" \
|
||||||
|
--package-name=$APPNAME \
|
||||||
|
--msgid-bugs-address=https://git.2ki.xyz/snow/kmail_unsubscribe \
|
||||||
|
--from-code=UTF-8 \
|
||||||
|
-C --kde \
|
||||||
|
-ci18n \
|
||||||
|
-ki18n:1 -ki18nc:1c,2 -ki18np:1,2 -ki18ncp:1c,2,3 \
|
||||||
|
-ki18nd:2 -ki18ndc:2c,3 -ki18ndp:2,3 -ki18ndcp:2c,3,4 \
|
||||||
|
-kki18n:1 -kki18nc:1c,2 -kki18np:1,2 -kki18ncp:1c,2,3 \
|
||||||
|
-kki18nd:2 -kki18ndc:2c,3 -kki18ndp:2,3 -kki18ndcp:2c,3,4 \
|
||||||
|
-kxi18n:1 -kxi18nc:1c,2 -kxi18np:1,2 -kxi18ncp:1c,2,3 \
|
||||||
|
-kxi18nd:2 -kxi18ndc:2c,3 -kxi18ndp:2,3 -kxi18ndcp:2c,3,4 \
|
||||||
|
-kkxi18n:1 -kkxi18nc:1c,2 -kkxi18np:1,2 -kkxi18ncp:1c,2,3 \
|
||||||
|
-kkxi18nd:2 -kkxi18ndc:2c,3 -kkxi18ndp:2,3 -kkxi18ndcp:2c,3,4 \
|
||||||
|
-kkli18n:1 -kkli18nc:1c,2 -kkli18np:1,2 -kkli18ncp:1c,2,3 \
|
||||||
|
-kklxi18n:1 -kklxi18nc:1c,2 -kklxi18np:1,2 -kklxi18ncp:1c,2,3 \
|
||||||
|
-kI18N_NOOP:1 -kI18NC_NOOP:1c,2 \
|
||||||
|
-kI18N_NOOP2:1c,2 -kI18N_NOOP2_NOSTRIP:1c,2 \
|
||||||
|
-ktr2i18n:1 -ktr2xi18n:1 \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
SRC_ROOT="$(dirname "${BASH_SOURCE[0]}")"
|
||||||
|
|
||||||
|
do_xgettext `find "${SRC_ROOT}" \( -name \*.cpp -o -name \*.h -o -name \*.qml \)` -o "${SRC_ROOT}/po/${APPNAME}.pot"
|
8
kmail_unsubscribeplugin.json
Normal file
8
kmail_unsubscribeplugin.json
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"KPlugin": {
|
||||||
|
"Description": "Adds RFC 8058 One-Click Unsubscribe to KMail.",
|
||||||
|
"EnabledByDefault": true,
|
||||||
|
"Name": "Unsubscribe",
|
||||||
|
"Version": "2.0"
|
||||||
|
}
|
||||||
|
}
|
68
oneclickunsubscribejob.cpp
Normal file
68
oneclickunsubscribejob.cpp
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
#include "oneclickunsubscribejob.h"
|
||||||
|
|
||||||
|
#include <QNetworkReply>
|
||||||
|
#include <QHttpMultiPart>
|
||||||
|
#include "unsubscribe_debug.h"
|
||||||
|
|
||||||
|
using namespace MessageViewer;
|
||||||
|
|
||||||
|
OneClickUnsubscribeJob::OneClickUnsubscribeJob(QUrl &oneClickUrl, UnsubscribeManager *parent)
|
||||||
|
: QObject(parent),
|
||||||
|
mNetworkAccessManager(new QNetworkAccessManager(this))
|
||||||
|
{
|
||||||
|
mNetworkAccessManager->setStrictTransportSecurityEnabled(true);
|
||||||
|
mNetworkAccessManager->enableStrictTransportSecurityStore(true);
|
||||||
|
|
||||||
|
connect(mNetworkAccessManager, &QNetworkAccessManager::finished, this, &OneClickUnsubscribeJob::slotFinished);
|
||||||
|
connect(mNetworkAccessManager, &QNetworkAccessManager::sslErrors, this, &OneClickUnsubscribeJob::slotSslErrors);
|
||||||
|
|
||||||
|
mUrl = oneClickUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OneClickUnsubscribeJob::start()
|
||||||
|
{
|
||||||
|
QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType, this);
|
||||||
|
QHttpPart mainPart;
|
||||||
|
// Per RFC 8058, only "List-Unsubscribe=One-Click" is allowed. To avoid
|
||||||
|
// parsing issues, we simply won't parse anything
|
||||||
|
mainPart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"List-Unsubscribe\""));
|
||||||
|
mainPart.setBody("One-Click");
|
||||||
|
multiPart->append(mainPart);
|
||||||
|
QNetworkRequest request(mUrl);
|
||||||
|
|
||||||
|
qCDebug(UnsubscribePlugin) << "Sending one-click unsubscribe request to" << mUrl;
|
||||||
|
|
||||||
|
mReply = mNetworkAccessManager->post(request, multiPart);
|
||||||
|
connect(mReply, &QNetworkReply::errorOccurred, this, &OneClickUnsubscribeJob::slotError);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OneClickUnsubscribeJob::slotSslErrors(QNetworkReply *reply, const QList<QSslError> &error)
|
||||||
|
{
|
||||||
|
// TODO: allow override somehow
|
||||||
|
qCDebug(UnsubscribePlugin) << "Got" << error.count() << "SSL error(s)";
|
||||||
|
UnsubscribeManager::Result sslErrResult = {
|
||||||
|
.Type = UnsubscribeManager::Result::SslError,
|
||||||
|
.ErrorString = error.first().errorString(),
|
||||||
|
};
|
||||||
|
Q_EMIT result(sslErrResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OneClickUnsubscribeJob::slotFinished(QNetworkReply *reply)
|
||||||
|
{
|
||||||
|
qCDebug(UnsubscribePlugin) << "Successful response from" << mUrl << "with result" << reply->errorString();
|
||||||
|
UnsubscribeManager::Result successResult = {
|
||||||
|
.Type = UnsubscribeManager::Result::None,
|
||||||
|
};
|
||||||
|
Q_EMIT result(successResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OneClickUnsubscribeJob::slotError(QNetworkReply::NetworkError error)
|
||||||
|
{
|
||||||
|
UnsubscribeManager::Result errorResult = {
|
||||||
|
.Type = UnsubscribeManager::Result::NetworkError,
|
||||||
|
.ErrorString = mReply->errorString(),
|
||||||
|
};
|
||||||
|
Q_EMIT result(errorResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
#include "moc_oneclickunsubscribejob.cpp"
|
49
oneclickunsubscribejob.h
Normal file
49
oneclickunsubscribejob.h
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
#ifndef _ONECLICKUNSUBSCRIBEJOB_H_
|
||||||
|
#define _ONECLICKUNSUBSCRIBEJOB_H_
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QNetworkAccessManager>
|
||||||
|
#include <QNetworkReply>
|
||||||
|
|
||||||
|
#include "unsubscribemanager.h"
|
||||||
|
|
||||||
|
namespace MessageViewer
|
||||||
|
{
|
||||||
|
class OneClickUnsubscribeJob : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit OneClickUnsubscribeJob(QUrl &oneClickUrl, UnsubscribeManager *parent);
|
||||||
|
~OneClickUnsubscribeJob() = default;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Starts the unsubscribe job.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
void start();
|
||||||
|
|
||||||
|
// Slots are used for connection to mNetworkAccessManager
|
||||||
|
public slots:
|
||||||
|
void slotFinished(QNetworkReply *reply);
|
||||||
|
void slotSslErrors(QNetworkReply *reply, const QList<QSslError> &errors);
|
||||||
|
void slotError(QNetworkReply::NetworkError error);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
/**
|
||||||
|
* @brief Triggered on job completion/failure.
|
||||||
|
*
|
||||||
|
* @param success Whether the job was successful. If true, sslError and error should be ignored.
|
||||||
|
* @param sslError If unsuccessful, whether one or more SSL errors occurred.
|
||||||
|
* @param error If unsuccessful, the error code returned by the QNetworkRequest.
|
||||||
|
*/
|
||||||
|
void result(const UnsubscribeManager::Result &data);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QNetworkAccessManager *const mNetworkAccessManager;
|
||||||
|
QUrl mUrl;
|
||||||
|
bool sentResult = false;
|
||||||
|
QNetworkReply *mReply = nullptr;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* !_ONECLICKUNSUBSCRIBEJOB_H_ */
|
100
po/kmail_unsubscribe.pot
Normal file
100
po/kmail_unsubscribe.pot
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
# SOME DESCRIPTIVE TITLE.
|
||||||
|
# Copyright (C) YEAR snow flurry
|
||||||
|
# This file is distributed under the same license as the kmail_unsubscribe package.
|
||||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||||
|
#
|
||||||
|
#, fuzzy
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: kmail_unsubscribe\n"
|
||||||
|
"Report-Msgid-Bugs-To: https://git.2ki.xyz/snow/kmail_unsubscribe\n"
|
||||||
|
"POT-Creation-Date: 2024-06-13 00:32-0700\n"
|
||||||
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
"Language: \n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=CHARSET\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
|
||||||
|
#: unsubscribemanager.cpp:178
|
||||||
|
#, kde-format
|
||||||
|
msgid "Unable to send unsubscribe request: %1"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: unsubscribemanager.cpp:181
|
||||||
|
#, kde-format
|
||||||
|
msgid "Got one or more SSL errors: %1"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: unsubscribemanager.cpp:184
|
||||||
|
#, kde-format
|
||||||
|
msgid "Plugin hit an unreachable point"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: unsubscribeplugininterface.cpp:59
|
||||||
|
#, kde-format
|
||||||
|
msgid "Network not available"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: unsubscribeplugininterface.cpp:60
|
||||||
|
#, kde-format
|
||||||
|
msgid "Please go back online to unsubscribe from this list."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: unsubscribeplugininterface.cpp:72
|
||||||
|
#, kde-format
|
||||||
|
msgid "Can't Unsubscribe"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: unsubscribeplugininterface.cpp:73
|
||||||
|
#, kde-format
|
||||||
|
msgid "This email doesn't advertise a way to unsubscribe."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: unsubscribeplugininterface.cpp:93
|
||||||
|
#, kde-format
|
||||||
|
msgid "The digital signature of this email couldn't be validated."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: unsubscribeplugininterface.cpp:94
|
||||||
|
#, kde-format
|
||||||
|
msgid "Do you still want to unsubscribe?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: unsubscribeplugininterface.cpp:104
|
||||||
|
#, kde-format
|
||||||
|
msgid "This mailing list supports One-Click Unsubscribe."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: unsubscribeplugininterface.cpp:105
|
||||||
|
#, kde-format
|
||||||
|
msgid "Do you want to unsubscribe?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: unsubscribeplugininterface.cpp:145
|
||||||
|
#, kde-format
|
||||||
|
msgid "Unsubscribe"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: unsubscribeplugininterface.cpp:146
|
||||||
|
#, kde-format
|
||||||
|
msgid ""
|
||||||
|
"Allows you to unsubscribe from a mailing list, if the sender supports One-"
|
||||||
|
"Click Unsubscribe"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: unsubscribeplugininterface.cpp:158
|
||||||
|
#, kde-format
|
||||||
|
msgid "Request Complete"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: unsubscribeplugininterface.cpp:159
|
||||||
|
#, kde-format
|
||||||
|
msgid "The unsubscribe request was successfully sent."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: unsubscribeplugininterface.cpp:164
|
||||||
|
#, kde-format
|
||||||
|
msgid "Unsubscribe Error"
|
||||||
|
msgstr ""
|
190
unsubscribemanager.cpp
Normal file
190
unsubscribemanager.cpp
Normal file
|
@ -0,0 +1,190 @@
|
||||||
|
#include <KLocalizedString>
|
||||||
|
|
||||||
|
#include "unsubscribe_debug.h"
|
||||||
|
|
||||||
|
#include "unsubscribemanager.h"
|
||||||
|
#include "oneclickunsubscribejob.h"
|
||||||
|
|
||||||
|
using namespace MessageViewer;
|
||||||
|
|
||||||
|
UnsubscribeManager::UnsubscribeManager(QObject *parent)
|
||||||
|
: QObject(parent),
|
||||||
|
mDkimMgr(DKIMManager(this))
|
||||||
|
{
|
||||||
|
connect(&mDkimMgr, &DKIMManager::result, this, &UnsubscribeManager::getDkimResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
UnsubscribeManager::~UnsubscribeManager() = default;
|
||||||
|
|
||||||
|
void UnsubscribeManager::reset()
|
||||||
|
{
|
||||||
|
mDKIMValid = false;
|
||||||
|
mItemId = -1;
|
||||||
|
mPostUrl = QUrl();
|
||||||
|
mMessage = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UnsubscribeManager::setMessageItem(const Akonadi::Item &item)
|
||||||
|
{
|
||||||
|
if (item.id() == mItemId)
|
||||||
|
{
|
||||||
|
// We already have this item, do nothing
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (mItemId != -1)
|
||||||
|
{
|
||||||
|
// reset wasn't called first, so call it for them
|
||||||
|
this->reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
// First, we have to have a KMime::Message::Ptr
|
||||||
|
if (item.hasPayload<KMime::Message::Ptr>())
|
||||||
|
{
|
||||||
|
mMessage = item.payload<KMime::Message::Ptr>();
|
||||||
|
if (mMessage == nullptr)
|
||||||
|
{
|
||||||
|
// Sometimes we get nullptr even though item.hasPayload() was true...
|
||||||
|
qCInfo(UnsubscribePlugin) << "Can't get current email";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mItemId = item.id();
|
||||||
|
// Check if we even have Unsubscribe info
|
||||||
|
mList = MessageCore::MailingList::detect(mMessage);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qWarning(UnsubscribePlugin) << "Received email doesn't seem to be an email";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't bother checking DKIM if we don't have an unsubscribe URL, or if we
|
||||||
|
// don't have DKIM enabled
|
||||||
|
if (!(getUrl().isEmpty()) && MessageViewerSettings::self()->enabledDkim())
|
||||||
|
{
|
||||||
|
mDkimMgr.checkDKim(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UnsubscribeManager::hasMessage()
|
||||||
|
{
|
||||||
|
return !(mMessage == nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UnsubscribeManager::getDkimResult(const MessageViewer::DKIMCheckSignatureJob::CheckSignatureResult &checkResult, Akonadi::Item::Id id)
|
||||||
|
{
|
||||||
|
if (id == mItemId)
|
||||||
|
{
|
||||||
|
mDKIMValid = checkResult.isValid();
|
||||||
|
qCDebug(UnsubscribePlugin) << "Got DKIM result! Valid:" << mDKIMValid;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qCDebug(UnsubscribePlugin) << "Got DKIM result for wrong ID! Wanted" << mItemId << "but got" << id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UnsubscribeManager::Status
|
||||||
|
UnsubscribeManager::unsubscribeStatus()
|
||||||
|
{
|
||||||
|
if (mMessage != nullptr && mList.features().testFlag(MessageCore::MailingList::Unsubscribe))
|
||||||
|
{
|
||||||
|
if (!oneClickUrl().isEmpty())
|
||||||
|
{
|
||||||
|
// One-Click requires List-Unsubscribe-Post
|
||||||
|
if (mMessage->hasHeader(LIST_UNSUBSCRIBE_POST_HDR))
|
||||||
|
{
|
||||||
|
if (MessageViewerSettings::self()->enabledDkim() &&
|
||||||
|
!mDKIMValid)
|
||||||
|
{
|
||||||
|
return UnsubscribeManager::InvalidOneClick;
|
||||||
|
}
|
||||||
|
return UnsubscribeManager::ValidOneClick;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return UnsubscribeManager::NoOneClick;
|
||||||
|
}
|
||||||
|
|
||||||
|
return UnsubscribeManager::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UnsubscribeManager::doOneClick()
|
||||||
|
{
|
||||||
|
auto status = unsubscribeStatus();
|
||||||
|
if (status == UnsubscribeManager::InvalidOneClick || status == UnsubscribeManager::ValidOneClick)
|
||||||
|
{
|
||||||
|
auto job = new OneClickUnsubscribeJob(mPostUrl, this);
|
||||||
|
connect(job, &OneClickUnsubscribeJob::result, this, &UnsubscribeManager::checkResult);
|
||||||
|
job->start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QUrl UnsubscribeManager::oneClickUrl()
|
||||||
|
{
|
||||||
|
if (mPostUrl.isEmpty() && mList.features().testFlag(MessageCore::MailingList::Unsubscribe))
|
||||||
|
{
|
||||||
|
foreach (QUrl url, mList.unsubscribeUrls())
|
||||||
|
{
|
||||||
|
// Per the RFC, there should only be one HTTPS URL. So grab the
|
||||||
|
// first one we find
|
||||||
|
if (url.scheme() == QStringLiteral("https"))
|
||||||
|
{
|
||||||
|
mPostUrl = url;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mPostUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
QUrl UnsubscribeManager::getUrl()
|
||||||
|
{
|
||||||
|
// TODO: Maybe this should be customizable.
|
||||||
|
// The theory is that HTTPS is most secure, mailto is more likely to be
|
||||||
|
// secure (MTAs often use TLS these days), and http as last resort. I really
|
||||||
|
// hope nobody's requesting unsubscribe over IRC...
|
||||||
|
const QStringList protocols = {"https", "mailto", "http"};
|
||||||
|
if (!mPostUrl.isEmpty())
|
||||||
|
{
|
||||||
|
return mPostUrl;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (QString scheme, protocols)
|
||||||
|
{
|
||||||
|
foreach (QUrl url, mList.unsubscribeUrls())
|
||||||
|
{
|
||||||
|
if (url.scheme() == scheme)
|
||||||
|
{
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return QUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UnsubscribeManager::checkResult(const Result &result)
|
||||||
|
{
|
||||||
|
bool isSuccess = false;
|
||||||
|
QString resultStr;
|
||||||
|
switch (result.Type)
|
||||||
|
{
|
||||||
|
case Result::None:
|
||||||
|
isSuccess = true;
|
||||||
|
break;
|
||||||
|
case Result::NetworkError:
|
||||||
|
resultStr = i18n("Unable to send unsubscribe request: %1", result.ErrorString);
|
||||||
|
break;
|
||||||
|
case Result::SslError:
|
||||||
|
resultStr = i18n("Got one or more SSL errors: %1", result.ErrorString);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
resultStr = i18n("Plugin hit an unreachable point");
|
||||||
|
}
|
||||||
|
|
||||||
|
Q_EMIT oneClickResult(isSuccess, resultStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
#include "moc_unsubscribemanager.cpp"
|
127
unsubscribemanager.h
Normal file
127
unsubscribemanager.h
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
#ifndef _UNSUBSCRIBEMANAGER_H_
|
||||||
|
#define _UNSUBSCRIBEMANAGER_H_
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <Akonadi/Item>
|
||||||
|
#include <KMime/KMimeMessage>
|
||||||
|
#include <MessageCore/MailingList>
|
||||||
|
#include <MessageViewer/DKIMManager>
|
||||||
|
|
||||||
|
#define LIST_UNSUBSCRIBE_POST_HDR "List-Unsubscribe-Post"
|
||||||
|
#define LIST_UNSUBSCRIBE_POST_VALUE "List-Unsubscribe=One-Click"
|
||||||
|
|
||||||
|
namespace MessageViewer
|
||||||
|
{
|
||||||
|
class UnsubscribeManager : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit UnsubscribeManager(QObject *parent = nullptr);
|
||||||
|
~UnsubscribeManager() override;
|
||||||
|
|
||||||
|
struct Result
|
||||||
|
{
|
||||||
|
enum _errorType
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
NetworkError,
|
||||||
|
SslError,
|
||||||
|
} Type;
|
||||||
|
QString ErrorString;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum Status
|
||||||
|
{
|
||||||
|
/// @brief No unsubscribe method is available.
|
||||||
|
None,
|
||||||
|
/// @brief Unsubscribe is available, but not as one-click unsubscribe.
|
||||||
|
NoOneClick,
|
||||||
|
/**
|
||||||
|
* @brief One-Click Unsubscribe is available, but DKIM didn't verify.
|
||||||
|
*
|
||||||
|
* Note: This state is considered non-compliant with RFC 8058.
|
||||||
|
*/
|
||||||
|
InvalidOneClick,
|
||||||
|
/// @brief One-Click Unsubscribe is available.
|
||||||
|
ValidOneClick,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Sets the current message item.
|
||||||
|
*
|
||||||
|
* @param item The current message item.
|
||||||
|
*/
|
||||||
|
void setMessageItem(const Akonadi::Item &item);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Tests whether the current message item has been set.
|
||||||
|
*/
|
||||||
|
bool hasMessage();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Tests if the message has information to programmatically unsubscribe
|
||||||
|
*
|
||||||
|
* @return true if the message can be unsubscribed from.
|
||||||
|
* @return false if the message cannot be unsubscribed from.
|
||||||
|
*/
|
||||||
|
Status unsubscribeStatus();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Performs a One-Click unsubscribe.
|
||||||
|
*/
|
||||||
|
void doOneClick();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the URL for One-Click Unsubscribe.
|
||||||
|
*
|
||||||
|
* @return QUrl The URL. If none was found, the URL will be empty.
|
||||||
|
*/
|
||||||
|
QUrl oneClickUrl();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the best Unsubscribe URL.
|
||||||
|
* Unlike oneClickUrl, this will check for any usable Unsubscribe URL.
|
||||||
|
* The search order is currently HTTPS, MAILTO, and HTTP URLs.
|
||||||
|
*
|
||||||
|
* @return QUrl The best available Unsubscribe URL, per the above heuristic
|
||||||
|
*/
|
||||||
|
QUrl getUrl();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Resets the object's state.
|
||||||
|
* @remark This does not cancel any running One-Click Unsubscribe jobs.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
void reset();
|
||||||
|
public slots:
|
||||||
|
/**
|
||||||
|
* @brief This is used as a slot for DKIMManager::result.
|
||||||
|
* This signal can only be signalled once-- further signals will be
|
||||||
|
* ignored.
|
||||||
|
*
|
||||||
|
* @param checkResult
|
||||||
|
* @param id
|
||||||
|
*/
|
||||||
|
void getDkimResult(const MessageViewer::DKIMCheckSignatureJob::CheckSignatureResult &checkResult, Akonadi::Item::Id id);
|
||||||
|
|
||||||
|
void checkResult(const Result &result);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void oneClickResult(bool isSuccess, const QString &resultString);
|
||||||
|
|
||||||
|
private:
|
||||||
|
// message info
|
||||||
|
KMime::Message::Ptr mMessage = nullptr;
|
||||||
|
MessageCore::MailingList mList;
|
||||||
|
Akonadi::Item::Id mItemId = -1;
|
||||||
|
|
||||||
|
// Cached by oneClickUrl()
|
||||||
|
QUrl mPostUrl;
|
||||||
|
|
||||||
|
// Used to check DKIM, for RFC 8058 compliance
|
||||||
|
DKIMManager mDkimMgr;
|
||||||
|
bool mDKIMValid = false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* !_UNSUBSCRIBEMANAGER_H_ */
|
21
unsubscribeplugin.cpp
Normal file
21
unsubscribeplugin.cpp
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
#include "unsubscribeplugin.h"
|
||||||
|
#include "unsubscribeplugininterface.h"
|
||||||
|
#include <KActionCollection>
|
||||||
|
#include <KPluginFactory>
|
||||||
|
|
||||||
|
using namespace MessageViewer;
|
||||||
|
K_PLUGIN_CLASS_WITH_JSON(UnsubscribePlugin, "kmail_unsubscribeplugin.json")
|
||||||
|
|
||||||
|
UnsubscribePlugin::UnsubscribePlugin(QObject *parent, const QList<QVariant> &)
|
||||||
|
: MessageViewer::ViewerPlugin(parent)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
ViewerPluginInterface *MessageViewer::UnsubscribePlugin::createView(QWidget *parent, KActionCollection *ac)
|
||||||
|
{
|
||||||
|
MessageViewer::ViewerPluginInterface *view = new MessageViewer::UnsubscribePluginInterface(parent, ac);
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
#include "unsubscribeplugin.moc"
|
||||||
|
#include "moc_unsubscribeplugin.cpp"
|
23
unsubscribeplugin.h
Normal file
23
unsubscribeplugin.h
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
#ifndef _UNSUBSCRIBEPLUGIN_H
|
||||||
|
#define _UNSUBSCRIBEPLUGIN_H
|
||||||
|
|
||||||
|
#include <MessageViewer/ViewerPlugin>
|
||||||
|
#include <QVariant>
|
||||||
|
|
||||||
|
namespace MessageViewer
|
||||||
|
{
|
||||||
|
class UnsubscribePlugin : public MessageViewer::ViewerPlugin
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit UnsubscribePlugin(QObject *parent = nullptr, const QList<QVariant> & = QList<QVariant>());
|
||||||
|
|
||||||
|
[[nodiscard]] ViewerPluginInterface *createView(QWidget *parent, KActionCollection *ac) override;
|
||||||
|
[[nodiscard]] QString viewerPluginName() const override
|
||||||
|
{
|
||||||
|
return QStringLiteral("oneclick-unsubscribe");
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
162
unsubscribeplugininterface.cpp
Normal file
162
unsubscribeplugininterface.cpp
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
#include "unsubscribe_debug.h"
|
||||||
|
#include "unsubscribeplugininterface.h"
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include <KActionCollection>
|
||||||
|
#include <KLocalizedString>
|
||||||
|
#include <MessageCore/MailingList>
|
||||||
|
#include <PimCommon/NetworkManager>
|
||||||
|
#include <KIO/OpenUrlJob>
|
||||||
|
#include <KIO/JobUiDelegate>
|
||||||
|
#include <KIO/JobUiDelegateFactory>
|
||||||
|
|
||||||
|
using namespace MessageViewer;
|
||||||
|
|
||||||
|
// General yes/no confirm dialog
|
||||||
|
static bool
|
||||||
|
confirmDialog(const QString &text, const QString &question, bool safe)
|
||||||
|
{
|
||||||
|
QMessageBox msgBox;
|
||||||
|
msgBox.setText(text);
|
||||||
|
msgBox.setInformativeText(question);
|
||||||
|
msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
|
||||||
|
msgBox.setIcon(safe ? QMessageBox::Question : QMessageBox::Warning);
|
||||||
|
msgBox.setDefaultButton(safe ? QMessageBox::Yes : QMessageBox::No);
|
||||||
|
|
||||||
|
return msgBox.exec() == QMessageBox::Yes;
|
||||||
|
}
|
||||||
|
|
||||||
|
UnsubscribePluginInterface::UnsubscribePluginInterface(QWidget *parent, KActionCollection *ac)
|
||||||
|
: ViewerPluginInterface(parent),
|
||||||
|
mParent(parent)
|
||||||
|
{
|
||||||
|
if (ac)
|
||||||
|
{
|
||||||
|
// Create the action...
|
||||||
|
auto action = new QAction(this);
|
||||||
|
action->setIcon(QIcon::fromTheme(QStringLiteral("news-unsubscribe")));
|
||||||
|
action->setIconText(i18n("Unsubscribe"));
|
||||||
|
action->setWhatsThis(i18n("Allows you to unsubscribe from a mailing list, if the sender supports One-Click Unsubscribe"));
|
||||||
|
|
||||||
|
// ... and add it to the application's collection
|
||||||
|
ac->addAction(QStringLiteral("oneclick_unsubscribe"), action);
|
||||||
|
|
||||||
|
connect(action, &QAction::triggered, this, &UnsubscribePluginInterface::slotActivatePlugin);
|
||||||
|
// also add it to our list, for actions()
|
||||||
|
mActions.append(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(&mUnsub, &UnsubscribeManager::oneClickResult, this, &UnsubscribePluginInterface::getOneClickResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
UnsubscribePluginInterface::~UnsubscribePluginInterface() = default;
|
||||||
|
|
||||||
|
QList<QAction *> UnsubscribePluginInterface::actions() const
|
||||||
|
{
|
||||||
|
return mActions;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UnsubscribePluginInterface::closePlugin()
|
||||||
|
{
|
||||||
|
// Reset the UnsubscribeManager's state.
|
||||||
|
// XXX: Currently, this doesn't cancel any running One-Click Unsubscribe
|
||||||
|
// calls!
|
||||||
|
mUnsub.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @brief Called when the action is clicked or otherwise activated
|
||||||
|
void UnsubscribePluginInterface::execute()
|
||||||
|
{
|
||||||
|
qCDebug(UnsubscribePlugin) << "-click!-";
|
||||||
|
// short-circuit if we're offline
|
||||||
|
if (!PimCommon::NetworkManager::self()->isOnline())
|
||||||
|
{
|
||||||
|
QMessageBox::critical(mParent,
|
||||||
|
i18n("Network not available"),
|
||||||
|
i18n("Please go back online to unsubscribe from this list."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mUnsub.hasMessage())
|
||||||
|
{
|
||||||
|
switch (mUnsub.unsubscribeStatus())
|
||||||
|
{
|
||||||
|
case UnsubscribeManager::None:
|
||||||
|
{
|
||||||
|
// Should not happen
|
||||||
|
QMessageBox::warning(mParent,
|
||||||
|
i18n("Can't Unsubscribe"),
|
||||||
|
i18n("This email doesn't advertise a way to unsubscribe."));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case UnsubscribeManager::NoOneClick:
|
||||||
|
{
|
||||||
|
// Load the unsubscribe URL normally
|
||||||
|
QUrl url = mUnsub.getUrl();
|
||||||
|
if (!url.isEmpty())
|
||||||
|
{
|
||||||
|
auto job = new KIO::OpenUrlJob(url);
|
||||||
|
job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, mParent));
|
||||||
|
job->start();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case UnsubscribeManager::InvalidOneClick:
|
||||||
|
{
|
||||||
|
if (confirmDialog(
|
||||||
|
i18n("The digital signature of this email couldn't be validated."),
|
||||||
|
i18n("Do you still want to unsubscribe?"),
|
||||||
|
false))
|
||||||
|
{
|
||||||
|
mUnsub.doOneClick();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case UnsubscribeManager::ValidOneClick:
|
||||||
|
{
|
||||||
|
if (confirmDialog(
|
||||||
|
i18n("This mailing list supports One-Click Unsubscribe."),
|
||||||
|
i18n("Do you want to unsubscribe?"),
|
||||||
|
true))
|
||||||
|
{
|
||||||
|
mUnsub.doOneClick();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UnsubscribePluginInterface::setMessageItem(const Akonadi::Item &item)
|
||||||
|
{
|
||||||
|
mUnsub.setMessageItem(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @brief Triggered when the selected item changes.
|
||||||
|
/// @param item The new item.
|
||||||
|
void UnsubscribePluginInterface::updateAction(const Akonadi::Item &item)
|
||||||
|
{
|
||||||
|
qCDebug(UnsubscribePlugin) << "updateAction!?" << mActions.count() << "actions";
|
||||||
|
mUnsub.setMessageItem(item);
|
||||||
|
|
||||||
|
mActions.first()->setDisabled(mUnsub.unsubscribeStatus() == UnsubscribeManager::None);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UnsubscribePluginInterface::getOneClickResult(bool isSuccess, const QString &resultString)
|
||||||
|
{
|
||||||
|
if (isSuccess)
|
||||||
|
{
|
||||||
|
QMessageBox::information(mParent,
|
||||||
|
i18n("Request Complete"),
|
||||||
|
i18n("The unsubscribe request was successfully sent."));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
QMessageBox::critical(mParent,
|
||||||
|
i18n("Unsubscribe Error"),
|
||||||
|
resultString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#include "moc_unsubscribeplugininterface.cpp"
|
39
unsubscribeplugininterface.h
Normal file
39
unsubscribeplugininterface.h
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
#ifndef _UNSUBSCRIBEPLUGININTERFACE_H_
|
||||||
|
#define _UNSUBSCRIBEPLUGININTERFACE_H_
|
||||||
|
|
||||||
|
#include <MessageViewer/ViewerPluginInterface>
|
||||||
|
#include "unsubscribemanager.h"
|
||||||
|
|
||||||
|
namespace MessageViewer
|
||||||
|
{
|
||||||
|
class UnsubscribePluginInterface : public MessageViewer::ViewerPluginInterface
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit UnsubscribePluginInterface(QWidget *parent, KActionCollection *ac = nullptr);
|
||||||
|
~UnsubscribePluginInterface() override;
|
||||||
|
|
||||||
|
[[nodiscard]] QList<QAction *> actions() const override;
|
||||||
|
void closePlugin() override;
|
||||||
|
void execute() override;
|
||||||
|
void setMessageItem(const Akonadi::Item &item) override;
|
||||||
|
void updateAction(const Akonadi::Item &item) override;
|
||||||
|
[[nodiscard]] ViewerPluginInterface::SpecificFeatureTypes featureTypes() const override
|
||||||
|
{
|
||||||
|
return ViewerPluginInterface::NeedMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void getOneClickResult(bool isSuccess, const QString &resultString);
|
||||||
|
|
||||||
|
private:
|
||||||
|
UnsubscribeManager mUnsub;
|
||||||
|
// Whether we're in the middle of a one-click unsubscribe operation
|
||||||
|
bool mUnsubscribing = true;
|
||||||
|
|
||||||
|
QList<QAction *> mActions;
|
||||||
|
QWidget *mParent;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
Loading…
Reference in a new issue