KMime

kmime_util.cpp
1 /*
2  kmime_util.cpp
3 
4  KMime, the KDE Internet mail/usenet news message library.
5  Copyright (c) 2001 the KMime authors.
6  See file AUTHORS for details
7 
8  This library is free software; you can redistribute it and/or
9  modify it under the terms of the GNU Library General Public
10  License as published by the Free Software Foundation; either
11  version 2 of the License, or (at your option) any later version.
12 
13  This library is distributed in the hope that it will be useful,
14  but WITHOUT ANY WARRANTY; without even the implied warranty of
15  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16  Library General Public License for more details.
17 
18  You should have received a copy of the GNU Library General Public License
19  along with this library; see the file COPYING.LIB. If not, write to
20  the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
21  Boston, MA 02110-1301, USA.
22 */
23 
24 #include "kmime_util.h"
25 #include "kmime_util_p.h"
26 
27 #include "kmime_charfreq.h"
28 #include "kmime_debug.h"
29 #include "kmime_header_parsing.h"
30 #include "kmime_message.h"
31 #include "kmime_warning.h"
32 
33 #include <config-kmime.h>
34 // #include <kdefakes.h> // for strcasestr
35 
36 #include <KLocalizedString>
37 #include <KCharsets>
38 #include <QCoreApplication>
39 
40 #include <QVector>
41 #include <QString>
42 #include <QTextCodec>
43 
44 #include <ctype.h>
45 #include <time.h>
46 #include <stdlib.h>
47 
48 using namespace KMime;
49 
50 namespace KMime
51 {
52 
53 QVector<QByteArray> c_harsetCache;
54 bool u_seOutlookEncoding = false;
55 
56 QByteArray cachedCharset(const QByteArray &name)
57 {
58  for (const QByteArray &charset : qAsConst(c_harsetCache)) {
59  if (qstricmp(name.data(), charset.data()) == 0) {
60  return charset;
61  }
62  }
63 
64  c_harsetCache.append(name.toUpper());
65  //qCDebug(KMIME_LOG) << "KNMimeBase::cachedCharset() number of cs" << c_harsetCache.count();
66  return c_harsetCache.last();
67 }
68 
69 bool isUsAscii(const QString &s)
70 {
71  uint sLength = s.length();
72  for (uint i = 0; i < sLength; i++) {
73  if (s.at(i).toLatin1() <= 0) { // c==0: non-latin1, c<0: non-us-ascii
74  return false;
75  }
76  }
77  return true;
78 }
79 
80 QString nameForEncoding(Headers::contentEncoding enc)
81 {
82  switch (enc) {
83  case Headers::CE7Bit: return QStringLiteral("7bit");
84  case Headers::CE8Bit: return QStringLiteral("8bit");
85  case Headers::CEquPr: return QStringLiteral("quoted-printable");
86  case Headers::CEbase64: return QStringLiteral("base64");
87  case Headers::CEuuenc: return QStringLiteral("uuencode");
88  case Headers::CEbinary: return QStringLiteral("binary");
89  default: return QStringLiteral("unknown");
90  }
91 }
92 
93 QVector<Headers::contentEncoding> encodingsForData(const QByteArray &data)
94 {
96  CharFreq cf(data);
97 
98  switch (cf.type()) {
100  allowed << Headers::CE7Bit;
101  Q_FALLTHROUGH();
103  allowed << Headers::CE8Bit;
104  Q_FALLTHROUGH();
106  if (cf.printableRatio() > 5.0 / 6.0) {
107  // let n the length of data and p the number of printable chars.
108  // Then base64 \approx 4n/3; qp \approx p + 3(n-p)
109  // => qp < base64 iff p > 5n/6.
110  allowed << Headers::CEquPr;
111  allowed << Headers::CEbase64;
112  } else {
113  allowed << Headers::CEbase64;
114  allowed << Headers::CEquPr;
115  }
116  break;
118  allowed << Headers::CEbase64;
119  break;
120  case CharFreq::None:
121  default:
122  Q_ASSERT(false);
123  }
124 
125  return allowed;
126 }
127 
128 // all except specials, CTLs, SPACE.
129 const uchar aTextMap[16] = {
130  0x00, 0x00, 0x00, 0x00,
131  0x5F, 0x35, 0xFF, 0xC5,
132  0x7F, 0xFF, 0xFF, 0xE3,
133  0xFF, 0xFF, 0xFF, 0xFE
134 };
135 
136 // all except tspecials, CTLs, SPACE.
137 const uchar tTextMap[16] = {
138  0x00, 0x00, 0x00, 0x00,
139  0x5F, 0x36, 0xFF, 0xC0,
140  0x7F, 0xFF, 0xFF, 0xE3,
141  0xFF, 0xFF, 0xFF, 0xFE
142 };
143 
144 void setUseOutlookAttachmentEncoding(bool violateStandard)
145 {
146  u_seOutlookEncoding = violateStandard;
147 }
148 
149 bool useOutlookAttachmentEncoding()
150 {
151  return u_seOutlookEncoding;
152 }
153 
154 QByteArray uniqueString()
155 {
156  static const char chars[] = "0123456789abcdefghijklmnopqrstuvxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
157  time_t now;
158  char p[11];
159  int ran;
160  unsigned int timeval;
161 
162  p[10] = '\0';
163  now = time(nullptr);
164  ran = 1 + (int)(1000.0 * rand() / (RAND_MAX + 1.0));
165  timeval = (now / ran) + QCoreApplication::applicationPid();
166 
167  for (int i = 0; i < 10; i++) {
168  int pos = (int)(61.0 * rand() / (RAND_MAX + 1.0));
169  //qCDebug(KMIME_LOG) << pos;
170  p[i] = chars[pos];
171  }
172 
173  QByteArray ret;
174  ret.setNum(timeval);
175  ret += '.';
176  ret += p;
177 
178  return ret;
179 }
180 
181 QByteArray multiPartBoundary()
182 {
183  return "nextPart" + uniqueString();
184 }
185 
186 QByteArray unfoldHeader(const char *header, size_t headerSize)
187 {
188  QByteArray result;
189  if (headerSize == 0) {
190  return result;
191  }
192 
193  // unfolding skips characters so result will be at worst headerSize long
194  result.reserve(headerSize);
195 
196  const char *end = header + headerSize;
197  const char *pos = header, *foldBegin = nullptr, *foldMid = nullptr, *foldEnd = nullptr;
198  while ((foldMid = strchr(pos, '\n')) && foldMid < end) {
199  foldBegin = foldEnd = foldMid;
200  // find the first space before the line-break
201  while (foldBegin) {
202  if (!QChar::isSpace(*(foldBegin - 1))) {
203  break;
204  }
205  --foldBegin;
206  }
207  // find the first non-space after the line-break
208  while (foldEnd <= end - 1) {
209  if (QChar::isSpace(*foldEnd)) {
210  ++foldEnd;
211  } else if (foldEnd && *(foldEnd - 1) == '\n' &&
212  *foldEnd == '=' && foldEnd + 2 < (header + headerSize - 1) &&
213  ((*(foldEnd + 1) == '0' &&
214  *(foldEnd + 2) == '9') ||
215  (*(foldEnd + 1) == '2' &&
216  *(foldEnd + 2) == '0'))) {
217  // bug #86302: malformed header continuation starting with =09/=20
218  foldEnd += 3;
219  } else {
220  break;
221  }
222  }
223 
224  result.append(pos, foldBegin - pos);
225  if (foldEnd < end - 1) {
226  result += ' ';
227  }
228  pos = foldEnd;
229  }
230  if (end > pos) {
231  result.append(pos, end - pos);
232  }
233  return result;
234 }
235 
236 QByteArray unfoldHeader(const QByteArray &header)
237 {
238  return unfoldHeader(header.constData(), header.size());
239 }
240 
241 int findHeaderLineEnd(const QByteArray &src, int &dataBegin, bool *folded)
242 {
243  int end = dataBegin;
244  int len = src.length() - 1;
245 
246  if (folded) {
247  *folded = false;
248  }
249 
250  if (dataBegin < 0) {
251  // Not found
252  return -1;
253  }
254 
255  if (dataBegin > len) {
256  // No data available
257  return len + 1;
258  }
259 
260  // If the first line contains nothing, but the next line starts with a space
261  // or a tab, that means a stupid mail client has made the first header field line
262  // entirely empty, and has folded the rest to the next line(s).
263  if (src.at(end) == '\n' && end + 1 < len &&
264  (src[end + 1] == ' ' || src[end + 1] == '\t')) {
265 
266  // Skip \n and first whitespace
267  dataBegin += 2;
268  end += 2;
269  }
270 
271  if (src.at(end) != '\n') { // check if the header is not empty
272  while (true) {
273  end = src.indexOf('\n', end + 1);
274  if (end == -1 || end == len) {
275  // end of string
276  break;
277  } else if (src[end + 1] == ' ' || src[end + 1] == '\t' ||
278  (src[end + 1] == '=' && end + 3 <= len &&
279  ((src[end + 2] == '0' && src[end + 3] == '9') ||
280  (src[end + 2] == '2' && src[end + 3] == '0')))) {
281  // next line is header continuation or starts with =09/=20 (bug #86302)
282  if (folded) {
283  *folded = true;
284  }
285  } else {
286  // end of header (no header continuation)
287  break;
288  }
289  }
290  }
291 
292  if (end < 0) {
293  end = len + 1; //take the rest of the string
294  }
295  return end;
296 }
297 
298 #ifndef HAVE_STRCASESTR
299 #ifdef WIN32
300 #define strncasecmp _strnicmp
301 #endif
302 static const char *strcasestr(const char *haystack, const char *needle)
303 {
304  /* Copied from libreplace as part of qtwebengine 5.5.1 */
305  const char *s;
306  size_t nlen = strlen(needle);
307  for (s = haystack; *s; s++) {
308  if (toupper(*needle) == toupper(*s) && strncasecmp(s, needle, nlen) == 0) {
309  return (char *)((uintptr_t)s);
310  }
311  }
312  return NULL;
313 }
314 #endif
315 
316 int indexOfHeader(const QByteArray &src, const QByteArray &name, int &end, int &dataBegin, bool *folded)
317 {
318  QByteArray n = name;
319  n.append(':');
320  int begin = -1;
321 
322  if (qstrnicmp(n.constData(), src.constData(), n.length()) == 0) {
323  begin = 0;
324  } else {
325  n.prepend('\n');
326  const char *p = strcasestr(src.constData(), n.constData());
327  if (!p) {
328  begin = -1;
329  } else {
330  begin = p - src.constData();
331  ++begin;
332  }
333  }
334 
335  if (begin > -1) { //there is a header with the given name
336  dataBegin = begin + name.length() + 1; //skip the name
337  // skip the usual space after the colon
338  if (dataBegin < src.length() && src.at(dataBegin) == ' ') {
339  ++dataBegin;
340  }
341  end = findHeaderLineEnd(src, dataBegin, folded);
342  return begin;
343 
344  } else {
345  end = -1;
346  dataBegin = -1;
347  return -1; //header not found
348  }
349 }
350 
351 QByteArray extractHeader(const QByteArray &src, const QByteArray &name)
352 {
353  int begin, end;
354  bool folded;
355  QByteArray result;
356 
357  if (src.isEmpty() || indexOfHeader(src, name, end, begin, &folded) < 0) {
358  return result;
359  }
360 
361  if (begin >= 0) {
362  if (!folded) {
363  result = src.mid(begin, end - begin);
364  } else {
365  if (end > begin) {
366  result = unfoldHeader(src.constData() + begin, end - begin);
367  }
368  }
369  }
370  return result;
371 }
372 
373 QByteArray CRLFtoLF(const QByteArray &s)
374 {
375  if (!s.contains("\r\n")) {
376  return s;
377  }
378 
379  QByteArray ret = s;
380  ret.replace("\r\n", "\n");
381  return ret;
382 }
383 
384 QByteArray CRLFtoLF(const char *s)
385 {
386  QByteArray ret = s;
387  return CRLFtoLF(ret);
388 }
389 
390 QByteArray LFtoCRLF(const QByteArray &s)
391 {
392  const int firstNewline = s.indexOf('\n');
393  if (firstNewline == -1) {
394  return s;
395  }
396  if (firstNewline > 0 && s.at(firstNewline - 1) == '\r') {
397  // We found \r\n already, don't change anything
398  // This check assumes that input is consistent in terms of newlines,
399  // but so did if (s.contains("\r\n")), too.
400  return s;
401  }
402 
403  QByteArray ret = s;
404  ret.replace('\n', "\r\n");
405  return ret;
406 }
407 
408 QByteArray LFtoCRLF(const char *s)
409 {
410  QByteArray ret = s;
411  return LFtoCRLF(ret);
412 }
413 
414 QByteArray CRtoLF(const QByteArray &s)
415 {
416  const int firstNewline = s.indexOf('\r');
417  if (firstNewline == -1) {
418  return s;
419  }
420  if (firstNewline > 0 && (s.length() > firstNewline + 1) && s.at(firstNewline + 1) == '\n') {
421  // We found \r\n already, don't change anything
422  // This check assumes that input is consistent in terms of newlines,
423  // but so did if (s.contains("\r\n")), too.
424  return s;
425  }
426 
427  QByteArray ret = s;
428  ret.replace('\r', '\n');
429  return ret;
430 }
431 
432 QByteArray CRtoLF(const char *s)
433 {
434  const QByteArray ret = s;
435  return CRtoLF(ret);
436 }
437 
438 namespace
439 {
440 template < typename StringType, typename CharType > void removeQuotesGeneric(StringType &str)
441 {
442  bool inQuote = false;
443  for (int i = 0; i < str.length(); ++i) {
444  if (str[i] == CharType('"')) {
445  str.remove(i, 1);
446  i--;
447  inQuote = !inQuote;
448  } else {
449  if (inQuote && (str[i] == CharType('\\'))) {
450  str.remove(i, 1);
451  }
452  }
453  }
454 }
455 }
456 
457 void removeQuotes(QByteArray &str)
458 {
459  removeQuotesGeneric<QByteArray, char>(str);
460 }
461 
462 void removeQuotes(QString &str)
463 {
464  removeQuotesGeneric<QString, QLatin1Char>(str);
465 }
466 
467 template<class StringType, class CharType, class CharConverterType, class StringConverterType, class ToString>
468 void addQuotes_impl(StringType &str, bool forceQuotes)
469 {
470  bool needsQuotes = false;
471  for (int i = 0; i < str.length(); i++) {
472  const CharType cur = str.at(i);
473  if (QString(ToString(str)).contains(QRegExp(QStringLiteral("\"|\\\\|=|\\]|\\[|:|;|,|\\.|,|@|<|>|\\)|\\(")))) {
474  needsQuotes = true;
475  }
476  if (cur == CharConverterType('\\') || cur == CharConverterType('\"')) {
477  str.insert(i, CharConverterType('\\'));
478  i++;
479  }
480  }
481 
482  if (needsQuotes || forceQuotes) {
483  str.insert(0, CharConverterType('\"'));
484  str.append(StringConverterType("\""));
485  }
486 }
487 
488 void addQuotes(QByteArray &str, bool forceQuotes)
489 {
490  addQuotes_impl<QByteArray, char, char, char *, QLatin1String>(str, forceQuotes);
491 }
492 
493 void addQuotes(QString &str, bool forceQuotes)
494 {
495  addQuotes_impl<QString, QChar, QLatin1Char, QLatin1String, QString>(str, forceQuotes);
496 }
497 
498 KMIME_EXPORT QString balanceBidiState(const QString &input)
499 {
500  const int LRO = 0x202D;
501  const int RLO = 0x202E;
502  const int LRE = 0x202A;
503  const int RLE = 0x202B;
504  const int PDF = 0x202C;
505 
506  QString result = input;
507 
508  int openDirChangers = 0;
509  int numPDFsRemoved = 0;
510  for (int i = 0; i < input.length(); i++) {
511  const ushort &code = input.at(i).unicode();
512  if (code == LRO || code == RLO || code == LRE || code == RLE) {
513  openDirChangers++;
514  } else if (code == PDF) {
515  if (openDirChangers > 0) {
516  openDirChangers--;
517  } else {
518  // One PDF too much, remove it
519  qCWarning(KMIME_LOG) << "Possible Unicode spoofing (unexpected PDF) detected in" << input;
520  result.remove(i - numPDFsRemoved, 1);
521  numPDFsRemoved++;
522  }
523  }
524  }
525 
526  if (openDirChangers > 0) {
527  qCWarning(KMIME_LOG) << "Possible Unicode spoofing detected in" << input;
528 
529  // At PDF chars to the end until the correct state is restored.
530  // As a special exception, when encountering quoted strings, place the PDF before
531  // the last quote.
532  for (int i = openDirChangers; i > 0; i--) {
533  if (result.endsWith(QLatin1Char('"'))) {
534  result.insert(result.length() - 1, QChar(PDF));
535  } else {
536  result += QChar(PDF);
537  }
538  }
539  }
540 
541  return result;
542 }
543 
544 QString removeBidiControlChars(const QString &input)
545 {
546  const int LRO = 0x202D;
547  const int RLO = 0x202E;
548  const int LRE = 0x202A;
549  const int RLE = 0x202B;
550  QString result = input;
551  result.remove(LRO);
552  result.remove(RLO);
553  result.remove(LRE);
554  result.remove(RLE);
555  return result;
556 }
557 
558 bool isCryptoPart(Content *content)
559 {
560  auto ct = content->contentType(false);
561  if (!ct || !ct->isMediatype("application")) {
562  return false;
563  }
564 
565  const QByteArray lowerSubType = ct->subType().toLower();
566  if (lowerSubType == "pgp-encrypted" ||
567  lowerSubType == "pgp-signature" ||
568  lowerSubType == "pkcs7-mime" ||
569  lowerSubType == "x-pkcs7-mime" ||
570  lowerSubType == "pkcs7-signature" ||
571  lowerSubType == "x-pkcs7-signature") {
572  return true;
573  }
574 
575  if (lowerSubType == "octet-stream") {
576  auto cd = content->contentDisposition(false);
577  if (!cd)
578  return false;
579  const auto fileName = cd->filename().toLower();
580  return fileName == QLatin1String("msg.asc") || fileName == QLatin1String("encrypted.asc");
581  }
582 
583  return false;
584 }
585 
586 bool isAttachment(Content* content)
587 {
588  if (!content) {
589  return false;
590  }
591 
592  const auto contentType = content->contentType(false);
593  // multipart/* is never an attachment itself, message/rfc822 always is
594  if (contentType) {
595  if (contentType->isMultipart())
596  return false;
597  if (contentType->isMimeType("message/rfc822"))
598  return true;
599  }
600 
601  // the main body part is not an attachment
602  if (content->parent()) {
603  const auto top = content->topLevel();
604  if (content == top->textContent())
605  return false;
606  }
607 
608  // ignore crypto parts
609  if (isCryptoPart(content))
610  return false;
611 
612 
613  // content type or content disposition having a file name set looks like an attachment
614  const auto contentDisposition = content->contentDisposition(false);
615  if (contentDisposition && !contentDisposition->filename().isEmpty()) {
616  return true;
617  }
618 
619  if (contentType && !contentType->name().isEmpty()) {
620  return true;
621  }
622 
623  // "attachment" content disposition is otherwise a good indicator though
624  if (contentDisposition && contentDisposition->disposition() == Headers::CDattachment)
625  return true;
626 
627  return false;
628 }
629 
630 bool hasAttachment(Content *content)
631 {
632  if (!content)
633  return false;
634 
635  if (isAttachment(content))
636  return true;
637 
638  // Ok, content itself is not an attachment. now we deal with multiparts
639  auto ct = content->contentType(false);
640  if (ct && ct->isMultipart() && !ct->isSubtype("related")) {// && !ct->isSubtype("alternative")) {
641  const auto contents = content->contents();
642  for (Content *child : contents) {
643  if (hasAttachment(child)) {
644  return true;
645  }
646  }
647  }
648  return false;
649 }
650 
651 bool hasInvitation(Content *content)
652 {
653  if (!content) {
654  return false;
655  }
656 
657  if (isInvitation(content)) {
658  return true;
659  }
660 
661  // Ok, content itself is not an invitation. now we deal with multiparts
662  if (content->contentType()->isMultipart()) {
663  const auto contents = content->contents();
664  for (Content *child : contents) {
665  if (hasInvitation(child)) {
666  return true;
667  }
668  }
669  }
670  return false;
671 }
672 
673 bool isSigned(Message *message)
674 {
675  if (!message) {
676  return false;
677  }
678 
679  const KMime::Headers::ContentType *const contentType = message->contentType();
680  if (contentType->isSubtype("signed") ||
681  contentType->isSubtype("pgp-signature") ||
682  contentType->isSubtype("pkcs7-signature") ||
683  contentType->isSubtype("x-pkcs7-signature") ||
684  message->mainBodyPart("multipart/signed") ||
685  message->mainBodyPart("application/pgp-signature") ||
686  message->mainBodyPart("application/pkcs7-signature") ||
687  message->mainBodyPart("application/x-pkcs7-signature")) {
688  return true;
689  }
690  return false;
691 }
692 
693 bool isEncrypted(Message *message)
694 {
695  if (!message) {
696  return false;
697  }
698 
699  const KMime::Headers::ContentType *const contentType = message->contentType();
700  if (contentType->isSubtype("encrypted") ||
701  contentType->isSubtype("pgp-encrypted") ||
702  contentType->isSubtype("pkcs7-mime") ||
703  contentType->isSubtype("x-pkcs7-mime") ||
704  message->mainBodyPart("multipart/encrypted") ||
705  message->mainBodyPart("application/pgp-encrypted") ||
706  message->mainBodyPart("application/pkcs7-mime") ||
707  message->mainBodyPart("application/x-pkcs7-mime")) {
708  return true;
709  }
710 
711  return false;
712 }
713 
714 bool isInvitation(Content *content)
715 {
716  if (!content) {
717  return false;
718  }
719 
720  const KMime::Headers::ContentType *const contentType = content->contentType(false);
721 
722  if (contentType && contentType->isMediatype("text") && contentType->isSubtype("calendar")) {
723  return true;
724  }
725 
726  return false;
727 }
728 
729 } // namespace KMime
bool isMultipart() const
Returns true if the associated MIME entity is a mulitpart container.
QByteArray toLower() const const
void append(const T &value)
void reserve(int size)
char at(int i) const const
QByteArray & setNum(short n, int base)
T & last()
QByteArray toUpper() const const
bool isEmpty() const const
Content * mainBodyPart(const QByteArray &type=QByteArray())
Returns the first main body part of a given type, taking multipart/mixed and multipart/alternative no...
Headers::ContentDisposition * contentDisposition(bool create=true)
Returns the Content-Disposition header.
int length() const const
QString & remove(int position, int n)
bool isSubtype(const char *subtype) const
Tests if the mime sub-type equals subtype.
const QList< QKeySequence > & begin()
int indexOf(char ch, int from) const const
contentDisposition
Various possible values for the "Content-Disposition" header.
Definition: kmime_headers.h:86
This file is part of the API for handling MIME data and defines the CharFreq class.
QVector< Content * > contents() const
For multipart contents, this will return a list of all multipart child contents.
bool isSpace() const const
QString & insert(int position, QChar ch)
QByteArray & prepend(char ch)
const char * constData() const const
QByteArray & replace(int pos, int len, const char *after)
bool endsWith(const QString &s, Qt::CaseSensitivity cs) const const
qint64 applicationPid()
QByteArray mid(int pos, int len) const const
ushort unicode() const const
Headers::ContentType * contentType(bool create=true)
Returns the Content-Type header.
QByteArray & append(char ch)
QString toLower() const const
char toLatin1() const const
Content * topLevel() const
Returns the toplevel content object, 0 if there is no such object.
const QList< QKeySequence > & end()
Represents a (email) message.
Definition: kmime_message.h:80
Content * textContent()
Returns the first Content with mimetype text/.
const QChar at(int position) const const
bool isMediatype(const char *mediatype) const
Tests if the media type equals mediatype.
bool contains(char ch) const const
int length() const const
A class that encapsulates MIME encoded Content.
char * data()
Content * parent() const
Returns the parent content object, or 0 if the content doesn&#39;t have a parent.
QString filename() const
Returns the suggested filename for the associated MIME part.
A class for performing basic data typing using frequency count heuristics.
int size() const const
Represents a "Content-Type" header.
contentEncoding
Various possible values for the "Content-Transfer-Encoding" header.
Definition: kmime_headers.h:74
This file is part of the KDE documentation.
Documentation copyright © 1996-2020 The KDE developers.
Generated on Sat Jul 11 2020 23:11:42 by doxygen 1.8.11 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.