Libksieve

vacationutils.cpp
1 /*
2  SPDX-FileCopyrightText: 2013-2021 Laurent Montel <[email protected]>
3 
4  SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6 
7 #include "vacationutils.h"
8 #include "legacy/vacationutils.h"
9 #include "sieve-vacation.h"
10 #include "vacationscriptextractor.h"
11 #include <KIdentityManagement/Identity>
12 #include <KIdentityManagement/IdentityManager>
13 
14 #include <KLocalizedString>
15 #include <QDate>
16 #include <QLocale>
17 #include <QRegularExpression>
18 
20 using namespace KSieveUi;
21 
22 static inline QString dotstuff(QString s) // krazy:exclude=passbyvalue
23 {
24  if (s.startsWith(QLatin1Char('.'))) {
25  return QLatin1Char('.') + s.replace(QLatin1String("\n."), QStringLiteral("\n.."));
26  } else {
27  return s.replace(QLatin1String("\n."), QStringLiteral("\n.."));
28  }
29 }
30 
31 static inline QString stringReplace(QString s)
32 {
33  s.replace(QRegularExpression(QStringLiteral("[\n\t]+")), QStringLiteral(" "));
34  return s.replace(QLatin1Char('\"'), QStringLiteral("\\\""));
35 }
36 
37 QString VacationUtils::defaultSubject()
38 {
39  return i18n("Out of office till %1", QLocale().toString(QDate::currentDate().addDays(1)));
40 }
41 
42 QString KSieveUi::VacationUtils::mailAction(KSieveUi::VacationUtils::MailAction action)
43 {
44  switch (action) {
45  case Keep:
46  return i18n("Keep");
47  case Discard:
48  return i18n("Discard");
49  case Sendto:
50  return i18n("Redirect to");
51  case CopyTo:
52  return i18n("Copy to");
53  }
54  Q_UNREACHABLE();
55  return {};
56 }
57 
58 KSieveUi::VacationUtils::MailAction KSieveUi::VacationUtils::defaultMailAction()
59 {
60  return KSieveUi::VacationUtils::Keep;
61 }
62 
63 QString KSieveUi::VacationUtils::defaultMessageText()
64 {
65  return i18n(
66  "I am out of office till %1.\n"
67  "\n"
68  "In urgent cases, please contact Mrs. \"vacation replacement\"\n"
69  "\n"
70  "email: \"email address of vacation replacement\"\n"
71  "phone: +49 711 1111 11\n"
72  "fax.: +49 711 1111 12\n"
73  "\n"
74  "Yours sincerely,\n"
75  "-- \"enter your name and email address here\"\n",
76  QLocale().toString(QDate::currentDate().addDays(1)));
77 }
78 
79 int VacationUtils::defaultNotificationInterval()
80 {
81  return 7; // days
82 }
83 
84 KMime::Types::AddrSpecList VacationUtils::defaultMailAliases()
85 {
88  KIdentityManagement::IdentityManager::ConstIterator end(manager->end());
89  for (KIdentityManagement::IdentityManager::ConstIterator it = manager->begin(); it != end; ++it) {
90  if (!(*it).primaryEmailAddress().isEmpty()) {
92  a.fromUnicodeString((*it).primaryEmailAddress());
93  sl.push_back(a.addrSpec());
94  }
95  const QStringList lstEmails = (*it).emailAliases();
96  for (const QString &email : lstEmails) {
98  a.fromUnicodeString(email);
99  sl.push_back(a.addrSpec());
100  }
101  }
102 
103  return sl;
104 }
105 
106 bool VacationUtils::defaultSendForSpam()
107 {
108  return VacationSettings::outOfOfficeReactToSpam();
109 }
110 
111 QString VacationUtils::defaultDomainName()
112 {
113  return VacationSettings::outOfOfficeDomain();
114 }
115 
116 QDate VacationUtils::defaultStartDate()
117 {
118  return QDate::currentDate();
119 }
120 
121 QDate VacationUtils::defaultEndDate()
122 {
123  return defaultStartDate().addDays(7);
124 }
125 
126 VacationUtils::Vacation parseScriptLegacy(const QString &script)
127 {
128  KSieveUi::VacationUtils::Vacation vacation;
129  vacation.active = true;
130  vacation.valid = Legacy::VacationUtils::parseScript(script,
131  vacation.messageText,
132  vacation.subject,
133  vacation.notificationInterval,
134  vacation.aliases,
135  vacation.sendForSpam,
136  vacation.reactOndomainName,
137  vacation.startDate,
138  vacation.endDate);
139  return vacation;
140 }
141 
142 VacationUtils::Vacation VacationUtils::parseScript(const QString &script)
143 {
144  KSieveUi::VacationUtils::Vacation vacation;
145  if (script.trimmed().isEmpty()) {
146  vacation.valid = false;
147  vacation.active = false;
148  vacation.mailAction = VacationUtils::defaultMailAction();
149  vacation.messageText = VacationUtils::defaultMessageText();
150  vacation.subject = VacationUtils::defaultSubject();
151  vacation.notificationInterval = VacationUtils::defaultNotificationInterval();
152  vacation.aliases = VacationUtils::defaultMailAliases();
153  vacation.sendForSpam = VacationUtils::defaultSendForSpam();
154  vacation.reactOndomainName = VacationUtils::defaultDomainName();
155  return vacation;
156  }
157 
158  // The trimmed() call below prevents parsing errors. The
159  // slave somehow omits the last \n, which results in a lone \r at
160  // the end, leading to a parse error.
161  const QByteArray scriptUTF8 = script.trimmed().toUtf8();
162  qCDebug(LIBKSIEVE_LOG) << "scriptUtf8 = \"" + scriptUTF8 + "\"";
163  KSieve::Parser parser(scriptUTF8.begin(), scriptUTF8.begin() + scriptUTF8.length());
164  VacationDataExtractor vdx;
165  SpamDataExtractor sdx;
166  DomainRestrictionDataExtractor drdx;
167  DateExtractor dx;
168  KSieveExt::MultiScriptBuilder tsb(&vdx, &sdx, &drdx, &dx);
169  parser.setScriptBuilder(&tsb);
170  if (!parser.parse() || !vdx.commandFound()) {
171  const auto vac = parseScriptLegacy(script);
172  if (vac.isValid()) {
173  return vac;
174  }
175  vacation.active = false;
176  vacation.valid = false;
177  return vacation;
178  }
179  vacation.valid = true;
180  vacation.active = vdx.active();
181  vacation.mailAction = vdx.mailAction();
182  vacation.mailActionRecipient = vdx.mailActionRecipient();
183  vacation.messageText = vdx.messageText().trimmed();
184  if (!vdx.subject().isEmpty()) {
185  vacation.subject = vdx.subject().trimmed();
186  }
187  vacation.notificationInterval = vdx.notificationInterval();
188  vacation.aliases = KMime::Types::AddrSpecList();
189  const QStringList lstAliases = vdx.aliases();
190  for (const QString &alias : lstAliases) {
192  a.fromUnicodeString(alias);
193  vacation.aliases.append(a.addrSpec());
194  }
195 
196  if (!vacation.active && !vdx.ifComment().isEmpty()) {
197  const QByteArray newScript = QByteArrayLiteral("if ") + vdx.ifComment().toUtf8() + QByteArrayLiteral("{vacation;}");
198  tsb = KSieveExt::MultiScriptBuilder(&sdx, &drdx, &dx);
199  KSieve::Parser activeScriptParser(newScript.begin(), newScript.begin() + newScript.length());
200  activeScriptParser.setScriptBuilder(&tsb);
201  if (!activeScriptParser.parse()) {
202  vacation.valid = false;
203  return vacation;
204  }
205  }
206 
207  vacation.sendForSpam = !sdx.found();
208  vacation.reactOndomainName = drdx.domainName();
209  vacation.startDate = dx.startDate();
210  vacation.startTime = dx.startTime();
211  vacation.endDate = dx.endDate();
212  vacation.endTime = dx.endTime();
213 
214  if (vacation.sendForSpam && vacation.reactOndomainName.isEmpty() && !vacation.startDate.isValid() && !vacation.endDate.isValid()) {
215  const auto vac = parseScriptLegacy(script);
216  if (vac.isValid()) {
217  vacation.sendForSpam = vac.sendForSpam;
218  vacation.reactOndomainName = vac.reactOndomainName;
219  vacation.startDate = vac.startDate;
220  vacation.startTime = vac.startTime;
221  vacation.endDate = vac.endDate;
222  vacation.endTime = vac.endTime;
223  }
224  }
225 
226  return vacation;
227 }
228 
229 QString KSieveUi::VacationUtils::composeScript(const Vacation &vacation)
230 {
231  QStringList condition;
232  QStringList require;
233 
234  require << QStringLiteral("vacation");
235 
236  if (vacation.startDate.isValid() || vacation.endDate.isValid()) {
237  require << QStringLiteral("date");
238  require << QStringLiteral("relational");
239  }
240 
241  if (vacation.startDate.isValid()) {
242  if (vacation.startTime.isValid()) {
243  const QDateTime start(vacation.startDate, vacation.startTime);
244  condition.append(QStringLiteral("currentdate :value \"ge\" \"iso8601\" \"%1\"").arg(start.toString(Qt::ISODate)));
245  } else {
246  condition.append(QStringLiteral("currentdate :value \"ge\" \"date\" \"%1\"").arg(vacation.startDate.toString(Qt::ISODate)));
247  }
248  }
249 
250  if (vacation.endDate.isValid()) {
251  if (vacation.endTime.isValid()) {
252  const QDateTime end(vacation.endDate, vacation.endTime);
253  condition.append(QStringLiteral("currentdate :value \"le\" \"iso8601\" \"%1\"").arg(end.toString(Qt::ISODate)));
254  } else {
255  condition.append(QStringLiteral("currentdate :value \"le\" \"date\" \"%1\"").arg(vacation.endDate.toString(Qt::ISODate)));
256  }
257  }
258 
259  if (!vacation.sendForSpam) {
260  condition.append(QStringLiteral("not header :contains \"X-Spam-Flag\" \"YES\""));
261  }
262 
263  if (!vacation.reactOndomainName.isEmpty()) {
264  condition.append(QStringLiteral("address :domain :contains \"from\" \"%1\"").arg(vacation.reactOndomainName));
265  }
266 
267  QString addressesArgument;
268  QStringList aliases;
269  if (!vacation.aliases.empty()) {
270  addressesArgument += QStringLiteral(":addresses [ ");
271  QStringList sl;
272  sl.reserve(vacation.aliases.count());
273  AddrSpecList::const_iterator end = vacation.aliases.constEnd();
274  for (AddrSpecList::const_iterator it = vacation.aliases.begin(); it != end; ++it) {
275  sl.push_back(QLatin1Char('"')
276  + (*it).asString().replace(QLatin1Char('\\'), QStringLiteral("\\\\")).replace(QLatin1Char('"'), QStringLiteral("\\\""))
277  + QLatin1Char('"'));
278  aliases.push_back((*it).asString());
279  }
280  addressesArgument += sl.join(QLatin1String(", ")) + QStringLiteral(" ] ");
281  }
282 
283  QString sVacation(QStringLiteral("vacation "));
284  sVacation += addressesArgument;
285  if (vacation.notificationInterval > 0) {
286  sVacation += QStringLiteral(":days %1 ").arg(vacation.notificationInterval);
287  }
288 
289  if (!vacation.subject.trimmed().isEmpty()) {
290  sVacation += QStringLiteral(":subject \"%1\" ").arg(stringReplace(vacation.subject).trimmed());
291  }
292 
293  sVacation += QStringLiteral("text:\n");
294  sVacation += dotstuff(vacation.messageText.isEmpty() ? VacationUtils::defaultMessageText() : vacation.messageText);
295  sVacation += QStringLiteral("\n.\n;");
296 
297  switch (vacation.mailAction) {
298  case VacationUtils::Keep:
299  break;
300  case VacationUtils::Discard:
301  sVacation += QStringLiteral("\ndiscard;");
302  break;
303  case VacationUtils::Sendto:
304  sVacation += QLatin1String("\nredirect \"") + vacation.mailActionRecipient + QLatin1String("\";");
305  break;
306  case VacationUtils::CopyTo:
307  require << QStringLiteral("copy");
308  sVacation += QLatin1String("\nredirect :copy \"") + vacation.mailActionRecipient + QLatin1String("\";");
309  break;
310  }
311 
312  QString script = QStringLiteral("require [\"%1\"];\n\n").arg(require.join(QStringLiteral("\", \"")));
313 
314  if (condition.isEmpty()) {
315  if (vacation.active) {
316  script += sVacation;
317  } else {
318  script += QStringLiteral("if false\n{\n\t");
319  script += sVacation;
320  script += QStringLiteral("\n}");
321  }
322  } else {
323  if (vacation.active) {
324  script += QStringLiteral("if allof(%1)\n{\n\t").arg(condition.join(QStringLiteral(", ")));
325  } else {
326  script += QStringLiteral("if false # allof(%1)\n{\n\t").arg(condition.join(QStringLiteral(", ")));
327  }
328  script += sVacation;
329  script += QStringLiteral("\n}");
330  }
331 
332  script += QStringLiteral("\n");
333 
334  return script;
335 }
336 
337 QString KSieveUi::VacationUtils::mergeRequireLine(const QString &script, const QString &scriptUpdate)
338 {
339  const QByteArray scriptUTF8 = script.trimmed().toUtf8();
340  if (scriptUTF8.isEmpty()) {
341  return scriptUpdate;
342  }
343 
344  const QByteArray scriptUpdateUTF8 = scriptUpdate.trimmed().toUtf8();
345  if (scriptUpdateUTF8.isEmpty()) {
346  return script;
347  }
348 
349  KSieve::Parser parser(scriptUTF8.begin(), scriptUTF8.begin() + scriptUTF8.length());
350  KSieve::Parser parserUpdate(scriptUpdateUTF8.begin(), scriptUpdateUTF8.begin() + scriptUpdateUTF8.length());
351  RequireExtractor rx, rxUpdate;
352  parser.setScriptBuilder(&rx);
353  parserUpdate.setScriptBuilder(&rxUpdate);
354 
355  int insert(0);
356  QStringList lines = script.split(QLatin1Char('\n'));
357  QSet<QString> requirements;
358 
359  if (parser.parse() && rx.commandFound()) {
360  insert = rx.lineStart();
361  const int endOld(rx.lineEnd());
362  for (int i = insert; i <= endOld; ++i) {
363  lines.removeAt(insert);
364  }
365  const auto requirementsSet = rx.requirements();
366  requirements = QSet<QString>(requirementsSet.begin(), requirementsSet.end());
367  }
368 
369  if (parserUpdate.parse() && rxUpdate.commandFound()) {
370  const auto requirementsSet = rxUpdate.requirements();
371  requirements += QSet<QString>(requirementsSet.begin(), requirementsSet.end());
372  }
373 
374  const int requirementscount = requirements.count();
375  if (requirementscount > 1) {
376  QStringList req = requirements.values();
377  req.sort();
378  lines.insert(insert, QStringLiteral("require [\"%1\"];").arg(req.join(QStringLiteral("\", \""))));
379  } else if (requirementscount == 1) {
380  lines.insert(insert, QStringLiteral("require \"%1\";").arg(requirements.values().constFirst()));
381  }
382 
383  return lines.join(QLatin1Char('\n'));
384 }
385 
386 QString KSieveUi::VacationUtils::updateVacationBlock(const QString &oldScript, const QString &newScript)
387 {
388  const QByteArray oldScriptUTF8 = oldScript.trimmed().toUtf8();
389  if (oldScriptUTF8.isEmpty()) {
390  return newScript;
391  }
392 
393  const QByteArray newScriptUTF8 = newScript.trimmed().toUtf8();
394  if (newScriptUTF8.isEmpty()) {
395  return oldScript;
396  }
397 
398  KSieve::Parser parserOld(oldScriptUTF8.begin(), oldScriptUTF8.begin() + oldScriptUTF8.length());
399  KSieve::Parser parserNew(newScriptUTF8.begin(), newScriptUTF8.begin() + newScriptUTF8.length());
400  VacationDataExtractor vdxOld, vdxNew;
401  RequireExtractor rx;
402  KSieveExt::MultiScriptBuilder tsb(&vdxOld, &rx);
403  parserOld.setScriptBuilder(&tsb);
404  parserNew.setScriptBuilder(&vdxNew);
405 
406  int startOld(0);
407 
408  QStringList lines = oldScript.split(QLatin1Char('\n'));
409 
410  if (parserOld.parse() && vdxOld.commandFound()) {
411  startOld = vdxOld.lineStart();
412  const int endOld(vdxOld.lineEnd());
413  for (int i = startOld; i <= endOld; ++i) {
414  lines.removeAt(startOld);
415  }
416  } else {
417  if (rx.commandFound()) { // after require
418  startOld = rx.lineEnd() + 1;
419  } else {
420  startOld = 0;
421  }
422  }
423 
424  if (parserNew.parse() && vdxNew.commandFound()) {
425  const int startNew(vdxNew.lineStart());
426  const int endNew(vdxNew.lineEnd());
427  QStringList linesNew = newScript.split(QLatin1Char('\n'));
428  for (int i = endNew; i >= startNew; --i) {
429  lines.insert(startOld, linesNew.at(i));
430  }
431  }
432 
433  return lines.join(QLatin1Char('\n'));
434 }
void push_back(const T &value)
void reserve(int alloc)
const T & constFirst() const const
void removeAt(int i)
bool isEmpty() const const
QString join(const QString &separator) const const
int length() const const
bool insert(Part *part, qint64 *insertId=nullptr)
void append(const T &value)
QList< T > values() const const
void fromUnicodeString(const QString &s)
bool isEmpty() const const
bool isEmpty() const const
QString trimmed() const const
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const const
QStringList split(const QString &sep, QString::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
QByteArray::iterator begin()
int count() const const
QString i18n(const char *text, const TYPE &arg...)
QString & replace(int position, int n, QChar after)
const QList< QKeySequence > & end()
char * toString(const T &value)
void insert(int i, const T &value)
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
QDate currentDate()
void push_back(const T &value)
void sort(Qt::CaseSensitivity cs)
static IdentityManager * self()
QDate addDays(qint64 ndays) const const
Parser for the Sieve grammar.
Definition: ksieve/parser.h:23
QByteArray toUtf8() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2021 The KDE developers.
Generated on Fri Apr 16 2021 23:09:34 by doxygen 1.8.11 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.