Messagelib

dkimauthenticationstatusinfo.cpp
1/*
2 SPDX-FileCopyrightText: 2018-2025 Laurent Montel <montel@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "dkimauthenticationstatusinfo.h"
8#include "dkimauthenticationstatusinfoutil.h"
9#include "messageviewer_dkimcheckerdebug.h"
10
11#include <QRegularExpressionMatch>
12using namespace MessageViewer;
13// see https://tools.ietf.org/html/rfc7601
14DKIMAuthenticationStatusInfo::DKIMAuthenticationStatusInfo() = default;
15
16bool DKIMAuthenticationStatusInfo::parseAuthenticationStatus(const QString &key, bool relaxingParsing)
17{
18 QString valueKey = key;
19 // kmime remove extra \r\n but we need it for regexp at the end.
20 if (!valueKey.endsWith(QLatin1StringView("\r\n"))) {
21 valueKey += QLatin1StringView("\r\n");
22 }
23 // https://tools.ietf.org/html/rfc7601#section-2.2
24 // authres-header = "Authentication-Results:" [CFWS] authserv-id
25 // [ CFWS authres-version ]
26 // ( no-result / 1*resinfo ) [CFWS] CRLF
27
28 // 1) extract AuthservId and AuthVersion
29 QRegularExpressionMatch match;
30 const QString regStr = DKIMAuthenticationStatusInfoUtil::value_cp() + QLatin1StringView("(?:") + DKIMAuthenticationStatusInfoUtil::cfws_p()
31 + QLatin1StringView("([0-9]+)") + DKIMAuthenticationStatusInfoUtil::cfws_op() + QLatin1StringView(" )?");
32 // qDebug() << " regStr" << regStr;
33 static const QRegularExpression regular1(regStr);
34 int index = valueKey.indexOf(regular1, 0, &match);
35 if (index != -1) {
36 mAuthservId = match.captured(1);
37 const QString authVersionStr = match.captured(2);
38 if (!authVersionStr.isEmpty()) {
39 mAuthVersion = authVersionStr.toInt();
40 } else {
41 mAuthVersion = 1;
42 }
43 valueKey = valueKey.right(valueKey.length() - (index + match.capturedLength(0)));
44 // qDebug() << " match.captured(0)"<<match.captured(0)<<"match.captured(1)" <<match.captured(1) << authVersionStr;
45 // qDebug() << " valueKey" << valueKey;
46 } else {
47 return false;
48 }
49 // check if message authentication was performed
50 const QString authResultStr = DKIMAuthenticationStatusInfoUtil::regexMatchO(DKIMAuthenticationStatusInfoUtil::value_cp() + QLatin1StringView(";")
51 + DKIMAuthenticationStatusInfoUtil::cfws_op() + QLatin1StringView("?none"));
52 // qDebug() << "authResultStr "<<authResultStr;
53 static const QRegularExpression regular2(authResultStr);
54 index = valueKey.indexOf(regular2, 0, &match);
55 if (index != -1) {
56 // no result
57 return false;
58 }
59 while (!valueKey.isEmpty()) {
60 // qDebug() << "valueKey LOOP" << valueKey;
61 const AuthStatusInfo resultInfo = parseAuthResultInfo(valueKey, relaxingParsing);
62 if (resultInfo.isValid()) {
63 mListAuthStatusInfo.append(resultInfo);
64 }
65 }
66 return true;
67}
68
69bool DKIMAuthenticationStatusInfo::checkResultKeyword(const QString &method, const QString &resultKeyword) const
70{
71 QStringList allowedKeywords;
72
73 // DKIM and DomainKeys (RFC 8601 section 2.7.1.)
74 if (method == QStringLiteral("dkim") || method == QStringLiteral("domainkeys")) {
75 allowedKeywords = {QStringLiteral("none"),
76 QStringLiteral("pass"),
77 QStringLiteral("fail"),
78 QStringLiteral("policy"),
79 QStringLiteral("neutral"),
80 QStringLiteral("temperror"),
81 QStringLiteral("permerror")};
82 }
83
84 // SPF and Sender ID (RFC 8601 section 2.7.2.)
85 if (method == QStringLiteral("spf") || method == QStringLiteral("sender-id")) {
86 allowedKeywords = {QStringLiteral("none"),
87 QStringLiteral("pass"),
88 QStringLiteral("fail"),
89 QStringLiteral("softfail"),
90 QStringLiteral("policy"),
91 QStringLiteral("neutral"),
92 QStringLiteral("temperror"),
93 QStringLiteral("permerror")
94 // Deprecated from older ARH RFC 5451.
95 ,
96 QStringLiteral("hardfail")
97 // Older SPF specs (e.g. RFC 4408) used mixed case.
98 ,
99 QStringLiteral("None"),
100 QStringLiteral("Pass"),
101 QStringLiteral("Fail"),
102 QStringLiteral("SoftFail"),
103 QStringLiteral("Neutral"),
104 QStringLiteral("TempError"),
105 QStringLiteral("PermError")};
106 }
107
108 // DMARC (RFC 7489 section 11.2.)
109 if (method == QStringLiteral("dmarc")) {
110 allowedKeywords = {QStringLiteral("none"), QStringLiteral("pass"), QStringLiteral("fail"), QStringLiteral("temperror"), QStringLiteral("permerror")};
111 }
112
113 // BIMI (https://datatracker.ietf.org/doc/draft-brand-indicators-for-message-identification/04/ section 7.7.)
114 if (method == QStringLiteral("bimi")) {
115 allowedKeywords = {QStringLiteral("pass"),
116 QStringLiteral("none"),
117 QStringLiteral("fail"),
118 QStringLiteral("temperror"),
119 QStringLiteral("declined"),
120 QStringLiteral("skipped")};
121 }
122
123 // Note: Both the ARH RFC and the IANA registry contain keywords for more than the above methods.
124 // As we don't really care about them, for simplicity we treat them the same as unknown methods,
125 // And don't restrict the keyword.
126
127 if (!allowedKeywords.contains(resultKeyword)) {
128 qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Result keyword " << resultKeyword << " is not allowed for method " << method;
129 return false;
130 }
131 return true;
132}
133
134DKIMAuthenticationStatusInfo::AuthStatusInfo DKIMAuthenticationStatusInfo::parseAuthResultInfo(QString &valueKey, bool relaxingParsing)
135{
136 // qDebug() << " valueKey *****************" << valueKey;
137 DKIMAuthenticationStatusInfo::AuthStatusInfo authStatusInfo;
138 // 2) extract methodspec
139 const QString methodVersionp =
140 DKIMAuthenticationStatusInfoUtil::cfws_op() + QLatin1Char('/') + DKIMAuthenticationStatusInfoUtil::cfws_op() + QLatin1StringView("([0-9]+)");
141 const QString method_p =
142 QLatin1Char('(') + DKIMAuthenticationStatusInfoUtil::keyword_p() + QLatin1StringView(")(?:") + methodVersionp + QLatin1StringView(")?");
143 QString Keyword_result_p = QStringLiteral("none|pass|fail|softfail|policy|neutral|temperror|permerror");
144 // older SPF specs (e.g. RFC 4408) use mixed case
145 Keyword_result_p += QLatin1StringView("|None|Pass|Fail|SoftFail|Neutral|TempError|PermError");
146 const QString result_p = QLatin1Char('=') + DKIMAuthenticationStatusInfoUtil::cfws_op() + QLatin1Char('(') + Keyword_result_p + QLatin1Char(')');
147 const QString methodspec_p =
148 QLatin1Char(';') + DKIMAuthenticationStatusInfoUtil::cfws_op() + method_p + DKIMAuthenticationStatusInfoUtil::cfws_op() + result_p;
149
150 // qDebug() << "methodspec_p " << methodspec_p;
151 QRegularExpressionMatch match;
152 static const QRegularExpression reg2(methodspec_p);
153 int index = valueKey.indexOf(reg2, 0, &match);
154 if (index == -1) {
155 valueKey = QString(); // remove it !
156 qCDebug(MESSAGEVIEWER_DKIMCHECKER_LOG) << "methodspec not found ";
157 // no result
158 return authStatusInfo;
159 }
160 // qDebug() << " match" << match.captured(0) << match.captured(1) << match.capturedTexts();
161 authStatusInfo.method = match.captured(1);
162 const QString authVersionStr = match.captured(2);
163 if (!authVersionStr.isEmpty()) {
164 authStatusInfo.methodVersion = authVersionStr.toInt();
165 } else {
166 authStatusInfo.methodVersion = 1;
167 }
168 authStatusInfo.result = match.captured(3);
169
170 valueKey = valueKey.right(valueKey.length() - (index + match.capturedLength(0))); // Improve it!
171
172 // 3) extract reasonspec (optional)
173 const QString reasonspec_p =
174 DKIMAuthenticationStatusInfoUtil::regexMatchO(QLatin1StringView("reason") + DKIMAuthenticationStatusInfoUtil::cfws_op() + QLatin1Char('=')
175 + DKIMAuthenticationStatusInfoUtil::cfws_op() + DKIMAuthenticationStatusInfoUtil::value_cp());
176 static const QRegularExpression reg31(reasonspec_p);
177 index = valueKey.indexOf(reg31, 0, &match);
178 if (index != -1) {
179 // qDebug() << " reason " << match.capturedTexts();
180 authStatusInfo.reason = match.captured(2);
181 valueKey = valueKey.right(valueKey.length() - (index + match.capturedLength(0))); // Improve it!
182 }
183 // 4) extract propspec (optional)
184 QString pvalue_p = DKIMAuthenticationStatusInfoUtil::value_p() + QLatin1StringView("|(?:(?:") + DKIMAuthenticationStatusInfoUtil::localPart_p()
185 + QLatin1StringView("?@)?") + DKIMAuthenticationStatusInfoUtil::domainName_p() + QLatin1Char(')');
186 if (relaxingParsing) {
187 // Allow "/" in the header.b (or other) property, even if it is not in a quoted-string
188 pvalue_p += QStringLiteral("|[^ \\x00-\\x1F\\x7F()<>@,;:\\\\\"[\\]?=]+");
189 }
190
191 const QString property_p = QLatin1StringView("mailfrom|rcptto") + QLatin1Char('|') + DKIMAuthenticationStatusInfoUtil::keyword_p();
192 const QString propspec_p = QLatin1Char('(') + DKIMAuthenticationStatusInfoUtil::keyword_p() + QLatin1Char(')') + DKIMAuthenticationStatusInfoUtil::cfws_op()
193 + QLatin1StringView("\\.") + DKIMAuthenticationStatusInfoUtil::cfws_op() + QLatin1Char('(') + property_p + QLatin1Char(')')
194 + DKIMAuthenticationStatusInfoUtil::cfws_op() + QLatin1Char('=') + DKIMAuthenticationStatusInfoUtil::cfws_op() + QLatin1Char('(')
195 + pvalue_p /*+ QLatin1Char(')')*/;
196
197 // qDebug() << "propspec_p " << propspec_p;
198
199 const QString regexp = DKIMAuthenticationStatusInfoUtil::regexMatchO(propspec_p);
200 static const QRegularExpression reg(regexp);
201 if (!reg.isValid()) {
202 qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << " reg error : " << reg.errorString();
203 } else {
204 index = valueKey.indexOf(reg, 0, &match);
205 while (index != -1) {
206 // qDebug() << " propspec " << match.capturedTexts();
207 valueKey = valueKey.right(valueKey.length() - (index + match.capturedLength(0))); // Improve it!
208 // qDebug() << " value KEy " << valueKey;
209 const QString &captured1 = match.captured(1);
210 // qDebug() << " captured1 " << captured1;
211 if (captured1 == QLatin1StringView("header")) {
212 AuthStatusInfo::Property prop;
213 prop.type = match.captured(2);
214 prop.value = match.captured(3);
215 authStatusInfo.header.append(prop);
216 } else if (captured1 == QLatin1StringView("smtp")) {
217 AuthStatusInfo::Property prop;
218 prop.type = match.captured(2);
219 prop.value = match.captured(3);
220 authStatusInfo.smtp.append(prop);
221 } else if (captured1 == QLatin1StringView("body")) {
222 AuthStatusInfo::Property prop;
223 prop.type = match.captured(2);
224 prop.value = match.captured(3);
225 authStatusInfo.body.append(prop);
226 } else if (captured1 == QLatin1StringView("policy")) {
227 AuthStatusInfo::Property prop;
228 prop.type = match.captured(2);
229 prop.value = match.captured(3);
230 authStatusInfo.policy.append(prop);
231 } else {
232 qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Unknown type found " << captured1;
233 }
234 index = valueKey.indexOf(reg, 0, &match);
235 }
236 }
237 return authStatusInfo;
238}
239
240int DKIMAuthenticationStatusInfo::authVersion() const
241{
242 return mAuthVersion;
243}
244
245void DKIMAuthenticationStatusInfo::setAuthVersion(int authVersion)
246{
247 mAuthVersion = authVersion;
248}
249
250QString DKIMAuthenticationStatusInfo::reasonSpec() const
251{
252 return mReasonSpec;
253}
254
255void DKIMAuthenticationStatusInfo::setReasonSpec(const QString &reasonSpec)
256{
257 mReasonSpec = reasonSpec;
258}
259
260bool DKIMAuthenticationStatusInfo::operator==(const DKIMAuthenticationStatusInfo &other) const
261{
262 return mAuthservId == other.authservId() && mAuthVersion == other.authVersion() && mReasonSpec == other.reasonSpec()
263 && mListAuthStatusInfo == other.listAuthStatusInfo();
264}
265
266QList<DKIMAuthenticationStatusInfo::AuthStatusInfo> DKIMAuthenticationStatusInfo::listAuthStatusInfo() const
267{
268 return mListAuthStatusInfo;
269}
270
271void DKIMAuthenticationStatusInfo::setListAuthStatusInfo(const QList<AuthStatusInfo> &listAuthStatusInfo)
272{
273 mListAuthStatusInfo = listAuthStatusInfo;
274}
275
276QString DKIMAuthenticationStatusInfo::authservId() const
277{
278 return mAuthservId;
279}
280
281void DKIMAuthenticationStatusInfo::setAuthservId(const QString &authservId)
282{
283 mAuthservId = authservId;
284}
285
286QDebug operator<<(QDebug d, const DKIMAuthenticationStatusInfo &t)
287{
288 d << "mAuthservId: " << t.authservId();
289 d << "mReasonSpec: " << t.reasonSpec();
290 d << "mAuthVersion: " << t.authVersion() << '\n';
291 const auto listAuthStatusInfo = t.listAuthStatusInfo();
292 for (const DKIMAuthenticationStatusInfo::AuthStatusInfo &info : listAuthStatusInfo) {
293 d << "mListAuthStatusInfo: " << info.method << " : " << info.result << " : " << info.methodVersion << " : " << info.reason << '\n';
294 d << "Property:" << '\n';
295 if (!info.smtp.isEmpty()) {
296 for (const DKIMAuthenticationStatusInfo::AuthStatusInfo::Property &prop : info.smtp) {
297 d << " smtp " << prop.type << " : " << prop.value << '\n';
298 }
299 }
300 if (!info.header.isEmpty()) {
301 for (const DKIMAuthenticationStatusInfo::AuthStatusInfo::Property &prop : info.header) {
302 d << " header " << prop.type << " : " << prop.value << '\n';
303 }
304 }
305 if (!info.body.isEmpty()) {
306 for (const DKIMAuthenticationStatusInfo::AuthStatusInfo::Property &prop : info.body) {
307 d << " body " << prop.type << " : " << prop.value << '\n';
308 }
309 }
310 if (!info.policy.isEmpty()) {
311 for (const DKIMAuthenticationStatusInfo::AuthStatusInfo::Property &prop : info.policy) {
312 d << " policy " << prop.type << " : " << prop.value << '\n';
313 }
314 }
315 }
316 return d;
317}
318
319bool DKIMAuthenticationStatusInfo::AuthStatusInfo::operator==(const DKIMAuthenticationStatusInfo::AuthStatusInfo &other) const
320{
321 return other.method == method && other.result == result && other.methodVersion == methodVersion && other.reason == reason && other.policy == policy
322 && other.smtp == smtp && other.header == header && other.body == body;
323}
324
325bool DKIMAuthenticationStatusInfo::AuthStatusInfo::isValid() const
326{
327 return !method.isEmpty();
328}
KCOREADDONS_EXPORT Result match(QStringView pattern, QStringView str)
KTEXTEDITOR_EXPORT QDebug operator<<(QDebug s, const MovingCursor &cursor)
void append(QList< T > &&value)
bool endsWith(QChar c, Qt::CaseSensitivity cs) const const
qsizetype indexOf(QChar ch, qsizetype from, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
qsizetype length() const const
QString right(qsizetype n) const const
int toInt(bool *ok, int base) const const
bool contains(QLatin1StringView str, Qt::CaseSensitivity cs) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Feb 28 2025 11:50:07 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.