Messagelib

mailinglist.cpp
1 /*
2  * SPDX-License-Identifier: LGPL-2.1-or-later
3  *
4  */
5 
6 #include "mailinglist.h"
7 
8 #include "messagecore_debug.h"
9 #include <KConfig>
10 #include <KConfigGroup>
11 #include <QUrl>
12 
13 #include <QSharedData>
14 #include <QStringList>
15 
16 using namespace MessageCore;
17 
18 using MagicDetectorFunc = QString (*)(const KMime::Message::Ptr &, QByteArray &, QString &);
19 
20 /* Sender: (owner-([^@]+)|([^@+]-owner)@ */
21 static QString check_sender(const KMime::Message::Ptr &message, QByteArray &headerName, QString &headerValue)
22 {
23  QString header = message->sender()->asUnicodeString();
24 
25  if (header.isEmpty()) {
26  return {};
27  }
28 
29  if (header.left(6) == QLatin1String("owner-")) {
30  headerName = "Sender";
31  headerValue = header;
32  header = header.mid(6, header.indexOf(QLatin1Char('@')) - 6);
33  } else {
34  const int index = header.indexOf(QLatin1String("-owner@ "));
35  if (index == -1) {
36  return {};
37  }
38 
39  header.truncate(index);
40  headerName = "Sender";
41  headerValue = header;
42  }
43 
44  return header;
45 }
46 
47 /* X-BeenThere: ([^@]+) */
48 static QString check_x_beenthere(const KMime::Message::Ptr &message, QByteArray &headerName, QString &headerValue)
49 {
50  QString header;
51  if (auto hrd = message->headerByType("X-BeenThere")) {
52  header = hrd->asUnicodeString();
53  }
54  if (header.isNull() || header.indexOf(QLatin1Char('@')) == -1) {
55  return {};
56  }
57 
58  headerName = "X-BeenThere";
59  headerValue = header;
60  header.truncate(header.indexOf(QLatin1Char('@')));
61 
62  return header;
63 }
64 
65 /* Delivered-To:: <([^@]+) */
66 static QString check_delivered_to(const KMime::Message::Ptr &message, QByteArray &headerName, QString &headerValue)
67 {
68  QString header;
69  if (auto hrd = message->headerByType("Delivered-To")) {
70  header = hrd->asUnicodeString();
71  }
72  if (header.isNull() || header.left(13) != QLatin1String("mailing list") || header.indexOf(QLatin1Char('@')) == -1) {
73  return {};
74  }
75 
76  headerName = "Delivered-To";
77  headerValue = header;
78 
79  return header.mid(13, header.indexOf(QLatin1Char('@')) - 13);
80 }
81 
82 /* X-Mailing-List: <?([^@]+) */
83 static QString check_x_mailing_list(const KMime::Message::Ptr &message, QByteArray &headerName, QString &headerValue)
84 {
85  QString header;
86  if (auto hrd = message->headerByType("X-Mailing-List")) {
87  header = hrd->asUnicodeString();
88  }
89  if (header.isEmpty()) {
90  return {};
91  }
92 
93  if (header.indexOf(QLatin1Char('@')) < 1) {
94  return {};
95  }
96 
97  headerName = "X-Mailing-List";
98  headerValue = header;
99  if (header[0] == QLatin1Char('<')) {
100  header = header.mid(1, header.indexOf(QLatin1Char('@')) - 1);
101  } else {
102  header.truncate(header.indexOf(QLatin1Char('@')));
103  }
104 
105  return header;
106 }
107 
108 /* List-Id: [^<]* <([^.]+) */
109 static QString check_list_id(const KMime::Message::Ptr &message, QByteArray &headerName, QString &headerValue)
110 {
111  QString header;
112  if (auto hrd = message->headerByType("List-Id")) {
113  header = hrd->asUnicodeString();
114  }
115  if (header.isEmpty()) {
116  return {};
117  }
118 
119  const int leftAnglePos = header.indexOf(QLatin1Char('<'));
120  if (leftAnglePos < 0) {
121  return {};
122  }
123 
124  const int firstDotPos = header.indexOf(QLatin1Char('.'), leftAnglePos);
125  if (firstDotPos < 0) {
126  return {};
127  }
128 
129  headerName = "List-Id";
130  headerValue = header.mid(leftAnglePos);
131  header = header.mid(leftAnglePos + 1, firstDotPos - leftAnglePos - 1);
132 
133  return header;
134 }
135 
136 /* List-Post: <mailto:[^< ]*>) */
137 static QString check_list_post(const KMime::Message::Ptr &message, QByteArray &headerName, QString &headerValue)
138 {
139  QString header;
140  if (auto hrd = message->headerByType("List-Post")) {
141  header = hrd->asUnicodeString();
142  }
143  if (header.isEmpty()) {
144  return {};
145  }
146 
147  int leftAnglePos = header.indexOf(QLatin1String("<mailto:"));
148  if (leftAnglePos < 0) {
149  return {};
150  }
151 
152  headerName = "List-Post";
153  headerValue = header;
154  header = header.mid(leftAnglePos + 8, header.length());
155  header.truncate(header.indexOf(QLatin1Char('@')));
156 
157  return header;
158 }
159 
160 /* Mailing-List: list ([^@]+) */
161 static QString check_mailing_list(const KMime::Message::Ptr &message, QByteArray &headerName, QString &headerValue)
162 {
163  QString header;
164  if (auto hrd = message->headerByType("Mailing-List")) {
165  header = hrd->asUnicodeString();
166  }
167  if (header.isEmpty()) {
168  return {};
169  }
170 
171  if (header.left(5) != QLatin1String("list ") || header.indexOf(QLatin1Char('@')) < 5) {
172  return {};
173  }
174 
175  headerName = "Mailing-List";
176  headerValue = header;
177  header = header.mid(5, header.indexOf(QLatin1Char('@')) - 5);
178 
179  return header;
180 }
181 
182 /* X-Loop: ([^@]+) */
183 static QString check_x_loop(const KMime::Message::Ptr &message, QByteArray &headerName, QString &headerValue)
184 {
185  QString header;
186  if (auto hrd = message->headerByType("X-Loop")) {
187  header = hrd->asUnicodeString();
188  }
189  if (header.isEmpty()) {
190  return {};
191  }
192 
193  const int indexOfHeader(header.indexOf(QLatin1Char('@')));
194  if (indexOfHeader < 2) {
195  return {};
196  }
197 
198  headerName = "X-Loop";
199  headerValue = header;
200  header.truncate(indexOfHeader);
201 
202  return header;
203 }
204 
205 /* X-ML-Name: (.+) */
206 static QString check_x_ml_name(const KMime::Message::Ptr &message, QByteArray &headerName, QString &headerValue)
207 {
208  QString header;
209  if (auto hrd = message->headerByType("X-ML-Name")) {
210  header = hrd->asUnicodeString();
211  }
212  if (header.isEmpty()) {
213  return {};
214  }
215 
216  headerName = "X-ML-Name";
217  headerValue = header;
218  header.truncate(header.indexOf(QLatin1Char('@')));
219 
220  return header;
221 }
222 
223 static const MagicDetectorFunc magic_detectors[] = {check_list_id,
224  check_list_post,
225  check_sender,
226  check_x_mailing_list,
227  check_mailing_list,
228  check_delivered_to,
229  check_x_beenthere,
230  check_x_loop,
231  check_x_ml_name};
232 
233 static QStringList headerToAddress(const QString &header)
234 {
235  QStringList addresses;
236  if (header.isEmpty()) {
237  return addresses;
238  }
239 
240  int start = 0;
241  while ((start = header.indexOf(QLatin1Char('<'), start)) != -1) {
242  int end = 0;
243  if ((end = header.indexOf(QLatin1Char('>'), ++start)) == -1) {
244  qCWarning(MESSAGECORE_LOG) << "Serious mailing list header parsing error!";
245  return addresses;
246  }
247 
248  addresses.append(header.mid(start, end - start));
249  }
250 
251  return addresses;
252 }
253 
254 class Q_DECL_HIDDEN MessageCore::MailingList::MailingListPrivate : public QSharedData
255 {
256 public:
257  MailingListPrivate()
258  : mFeatures(None)
259  , mHandler(KMail)
260  {
261  }
262 
263  MailingListPrivate(const MailingListPrivate &other)
264  : QSharedData(other)
265  {
266  mFeatures = other.mFeatures;
267  mHandler = other.mHandler;
268  mPostUrls = other.mPostUrls;
269  mSubscribeUrls = other.mSubscribeUrls;
270  mUnsubscribeUrls = other.mUnsubscribeUrls;
271  mHelpUrls = other.mHelpUrls;
272  mArchiveUrls = other.mArchiveUrls;
273  mOwnerUrls = other.mOwnerUrls;
274  mArchivedAtUrls = other.mArchivedAtUrls;
275  mId = other.mId;
276  }
277 
278  Features mFeatures;
279  Handler mHandler;
280  QList<QUrl> mPostUrls;
281  QList<QUrl> mSubscribeUrls;
282  QList<QUrl> mUnsubscribeUrls;
283  QList<QUrl> mHelpUrls;
284  QList<QUrl> mArchiveUrls;
285  QList<QUrl> mOwnerUrls;
286  QList<QUrl> mArchivedAtUrls;
287  QString mId;
288 };
289 
291 {
292  MailingList mailingList;
293 
294  if (auto hrd = message->headerByType("List-Post")) {
295  mailingList.setPostUrls(QUrl::fromStringList(headerToAddress(hrd->asUnicodeString())));
296  }
297 
298  if (auto hrd = message->headerByType("List-Help")) {
299  mailingList.setHelpUrls(QUrl::fromStringList(headerToAddress(hrd->asUnicodeString())));
300  }
301 
302  if (auto hrd = message->headerByType("List-Subscribe")) {
303  mailingList.setSubscribeUrls(QUrl::fromStringList(headerToAddress(hrd->asUnicodeString())));
304  }
305 
306  if (auto hrd = message->headerByType("List-Unsubscribe")) {
307  mailingList.setUnsubscribeUrls(QUrl::fromStringList(headerToAddress(hrd->asUnicodeString())));
308  }
309 
310  if (auto hrd = message->headerByType("List-Archive")) {
311  mailingList.setArchiveUrls(QUrl::fromStringList(headerToAddress(hrd->asUnicodeString())));
312  }
313 
314  if (auto hrd = message->headerByType("List-Owner")) {
315  mailingList.setOwnerUrls(QUrl::fromStringList(headerToAddress(hrd->asUnicodeString())));
316  }
317 
318  if (auto hrd = message->headerByType("Archived-At")) {
319  mailingList.setArchivedAtUrls(QUrl::fromStringList(headerToAddress(hrd->asUnicodeString())));
320  }
321 
322  if (auto hrd = message->headerByType("List-Id")) {
323  mailingList.setId(hrd->asUnicodeString());
324  }
325 
326  return mailingList;
327 }
328 
329 QString MailingList::name(const KMime::Message::Ptr &message, QByteArray &headerName, QString &headerValue)
330 {
331  QString mailingList;
332  headerName = QByteArray();
333  headerValue.clear();
334 
335  if (!message) {
336  return {};
337  }
338 
339  for (const MagicDetectorFunc &detector : magic_detectors) {
340  mailingList = detector(message, headerName, headerValue);
341  if (!mailingList.isNull()) {
342  return mailingList;
343  }
344  }
345 
346  return {};
347 }
348 
350  : d(new MailingListPrivate)
351 {
352 }
353 
355 
356  = default;
357 
359 {
360  if (this != &other) {
361  d = other.d;
362  }
363 
364  return *this;
365 }
366 
367 bool MailingList::operator==(const MailingList &other) const
368 {
369  return other.features() == d->mFeatures && other.handler() == d->mHandler && other.postUrls() == d->mPostUrls && other.subscribeUrls() == d->mSubscribeUrls
370  && other.unsubscribeUrls() == d->mUnsubscribeUrls && other.helpUrls() == d->mHelpUrls && other.archiveUrls() == d->mArchiveUrls
371  && other.ownerUrls() == d->mOwnerUrls && other.archivedAtUrls() == d->mArchivedAtUrls && other.id() == d->mId;
372 }
373 
374 MailingList::~MailingList() = default;
375 
377 {
378  return d->mFeatures;
379 }
380 
382 {
383  d->mHandler = handler;
384 }
385 
387 {
388  return d->mHandler;
389 }
390 
392 {
393  d->mFeatures |= Post;
394 
395  if (urls.empty()) {
396  d->mFeatures ^= Post;
397  }
398 
399  d->mPostUrls = urls;
400 }
401 
403 {
404  return d->mPostUrls;
405 }
406 
408 {
409  d->mFeatures |= Subscribe;
410 
411  if (urls.empty()) {
412  d->mFeatures ^= Subscribe;
413  }
414 
415  d->mSubscribeUrls = urls;
416 }
417 
419 {
420  return d->mSubscribeUrls;
421 }
422 
424 {
425  d->mFeatures |= Unsubscribe;
426 
427  if (urls.empty()) {
428  d->mFeatures ^= Unsubscribe;
429  }
430 
431  d->mUnsubscribeUrls = urls;
432 }
433 
435 {
436  return d->mUnsubscribeUrls;
437 }
438 
440 {
441  d->mFeatures |= Help;
442 
443  if (urls.empty()) {
444  d->mFeatures ^= Help;
445  }
446 
447  d->mHelpUrls = urls;
448 }
449 
451 {
452  return d->mHelpUrls;
453 }
454 
456 {
457  d->mFeatures |= Archive;
458 
459  if (urls.empty()) {
460  d->mFeatures ^= Archive;
461  }
462 
463  d->mArchiveUrls = urls;
464 }
465 
467 {
468  return d->mArchiveUrls;
469 }
470 
472 {
473  d->mFeatures |= Owner;
474 
475  if (urls.empty()) {
476  d->mFeatures ^= Owner;
477  }
478 
479  d->mOwnerUrls = urls;
480 }
481 
483 {
484  return d->mOwnerUrls;
485 }
486 
488 {
489  d->mFeatures |= ArchivedAt;
490 
491  if (urls.isEmpty()) {
492  d->mFeatures ^= ArchivedAt;
493  }
494 
495  d->mArchivedAtUrls = urls;
496 }
497 
499 {
500  return d->mArchivedAtUrls;
501 }
502 
504 {
505  d->mFeatures |= Id;
506 
507  if (id.isEmpty()) {
508  d->mFeatures ^= Id;
509  }
510 
511  d->mId = id;
512 }
513 
515 {
516  return d->mId;
517 }
518 
520 {
521  if (d->mFeatures != Feature::None) {
522  group.writeEntry("MailingListFeatures", static_cast<int>(d->mFeatures));
523  } else {
524  group.deleteEntry("MailingListFeatures");
525  }
526  if (d->mHandler != Handler::KMail) {
527  group.writeEntry("MailingListHandler", static_cast<int>(d->mHandler));
528  } else {
529  group.deleteEntry("MailingListHandler");
530  }
531  if (!d->mId.isEmpty()) {
532  group.writeEntry("MailingListId", d->mId);
533  } else {
534  group.deleteEntry("MailingListId");
535  }
536  QStringList lst = QUrl::toStringList(d->mPostUrls);
537  if (!lst.isEmpty()) {
538  group.writeEntry("MailingListPostingAddress", lst);
539  } else {
540  group.deleteEntry("MailingListPostingAddress");
541  }
542 
543  lst = QUrl::toStringList(d->mSubscribeUrls);
544  if (!lst.isEmpty()) {
545  group.writeEntry("MailingListSubscribeAddress", lst);
546  } else {
547  group.deleteEntry("MailingListSubscribeAddress");
548  }
549 
550  lst = QUrl::toStringList(d->mUnsubscribeUrls);
551  if (!lst.isEmpty()) {
552  group.writeEntry("MailingListUnsubscribeAddress", lst);
553  } else {
554  group.deleteEntry("MailingListUnsubscribeAddress");
555  }
556 
557  lst = QUrl::toStringList(d->mArchiveUrls);
558  if (!lst.isEmpty()) {
559  group.writeEntry("MailingListArchiveAddress", lst);
560  } else {
561  group.deleteEntry("MailingListArchiveAddress");
562  }
563 
564  lst = QUrl::toStringList(d->mOwnerUrls);
565  if (!lst.isEmpty()) {
566  group.writeEntry("MailingListOwnerAddress", lst);
567  } else {
568  group.deleteEntry("MailingListOwnerAddress");
569  }
570 
571  lst = QUrl::toStringList(d->mHelpUrls);
572  if (!lst.isEmpty()) {
573  group.writeEntry("MailingListHelpAddress", lst);
574  } else {
575  group.deleteEntry("MailingListHelpAddress");
576  }
577 
578  /* Note: mArchivedAtUrl deliberately not saved here as it refers to a single
579  * instance of a message rather than an element of a general mailing list.
580  * http://reviewboard.kde.org/r/1768/#review2783
581  */
582 }
583 
585 {
586  d->mFeatures = static_cast<MailingList::Features>(group.readEntry("MailingListFeatures", 0));
587  d->mHandler = static_cast<MailingList::Handler>(group.readEntry("MailingListHandler", static_cast<int>(MailingList::KMail)));
588  d->mId = group.readEntry("MailingListId");
589  d->mPostUrls = QUrl::fromStringList(group.readEntry("MailingListPostingAddress", QStringList()));
590  d->mSubscribeUrls = QUrl::fromStringList(group.readEntry("MailingListSubscribeAddress", QStringList()));
591  d->mUnsubscribeUrls = QUrl::fromStringList(group.readEntry("MailingListUnsubscribeAddress", QStringList()));
592  d->mArchiveUrls = QUrl::fromStringList(group.readEntry("MailingListArchiveAddress", QStringList()));
593  d->mOwnerUrls = QUrl::fromStringList(group.readEntry("MailingListOwnerAddress", QStringList()));
594  d->mHelpUrls = QUrl::fromStringList(group.readEntry("MailingListHelpAddress", QStringList()));
595 }
void setArchivedAtUrls(const QList< QUrl > &url)
Sets the Archived-At url.
void append(const T &value)
QString readEntry(const char *key, const char *aDefault=nullptr) const
void writeEntry(const char *key, const char *value, WriteConfigFlags pFlags=Normal)
bool isNull() const const
void truncate(int position)
QList< QUrl > unsubscribeUrls() const
Returns the list of List-Unsubscribe urls.
void deleteEntry(const char *key, WriteConfigFlags pFlags=Normal)
QStringList toStringList(const QList< QUrl > &urls, QUrl::FormattingOptions options)
Handler
Defines what entity should manage the mailing list.
Definition: mailinglist.h:37
@ Unsubscribe
List-Unsubscribe header exists.
Definition: mailinglist.h:49
QList< QUrl > postUrls() const
Returns the list of List-Post urls.
void clear()
Q_SCRIPTABLE Q_NOREPLY void start()
void setOwnerUrls(const QList< QUrl > &urls)
Sets the list of List-Owner urls.
void setPostUrls(const QList< QUrl > &urls)
Sets the list of List-Post urls.
MailingList()
Creates an empty mailing list.
@ KMail
The list is handled by KMail.
Definition: mailinglist.h:38
@ Id
List-ID header exists.
Definition: mailinglist.h:52
QList< QUrl > fromStringList(const QStringList &urls, QUrl::ParsingMode mode)
bool empty() const const
QList< QUrl > ownerUrls() const
Returns the list of List-Owner urls.
A class to extract information about mailing lists from emails.
Definition: mailinglist.h:31
@ Subscribe
List-Subscribe header exists.
Definition: mailinglist.h:48
@ Help
List-Help header exists.
Definition: mailinglist.h:50
@ ArchivedAt
Archive-At header exists.
Definition: mailinglist.h:54
bool isEmpty() const const
@ Owner
List-Owner header exists.
Definition: mailinglist.h:53
int length() const const
MailingList & operator=(const MailingList &other)
Overwrites this mailing list with an other mailing list.
bool isEmpty() const const
QList< QUrl > archiveUrls() const
Returns the list of List-Archive urls.
int indexOf(QChar ch, int from, Qt::CaseSensitivity cs) const const
void writeConfig(KConfigGroup &group) const
Saves the configuration for the mailing list to the config group.
QList< QUrl > subscribeUrls() const
Returns the list of List-Subscribe urls.
static MailingList detect(const KMime::Message::Ptr &message)
Extracts the information about a mailing list from the given message.
void setUnsubscribeUrls(const QList< QUrl > &urls)
Sets the list of List-Unsubscribe urls.
Features features() const
Returns the features the mailing list supports.
void setId(const QString &id)
Sets the id of the mailing list.
QString id() const
Returns the id of the mailing list.
@ Post
List-Post header exists.
Definition: mailinglist.h:47
QString left(int n) const const
void setSubscribeUrls(const QList< QUrl > &urls)
Sets the list of List-Subscribe urls.
QList< QUrl > helpUrls() const
Returns the list of List-Help urls.
void setHandler(Handler handler)
Sets the handler for the mailing list.
void readConfig(const KConfigGroup &group)
Restores the configuration for the mailing list from the config group.
void setHelpUrls(const QList< QUrl > &urls)
Sets the list of List-Help urls.
QList< QUrl > archivedAtUrls() const
Returns the Archived-At url.
QString mid(int position, int n) const const
void setArchiveUrls(const QList< QUrl > &urls)
Sets the list of List-Archive urls.
QString message
const QList< QKeySequence > & end()
~MailingList()
Destroys the mailing list.
Handler handler() const
Returns the handler for the mailing list.
@ Archive
List-Archive header exists.
Definition: mailinglist.h:51
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Sun Dec 3 2023 03:57:07 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.