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