diff --git a/autotests/headertest.cpp b/autotests/headertest.cpp index a795def..cacfabd 100644 --- a/autotests/headertest.cpp +++ b/autotests/headertest.cpp @@ -1,1055 +1,1068 @@ /* Copyright (c) 2006 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "headertest.h" #include #include #include using namespace KMime; using namespace KMime::Headers; using namespace KMime::Headers::Generics; // the following test cases are taken from KDE mailinglists, bug reports, RFC 2045, // RFC 2183 and RFC 2822, Appendix A QTEST_MAIN(HeaderTest) void HeaderTest::testIdentHeader() { // empty header Headers::Generics::Ident *h = new Headers::Generics::Ident(); QVERIFY(h->isEmpty()); // parse single identifier h->from7BitString(QByteArray("<1162746587.784559.5038.nullmailer@svn.kde.org>")); QCOMPARE(h->identifiers().count(), 1); QCOMPARE(h->identifiers().first(), QByteArray("1162746587.784559.5038.nullmailer@svn.kde.org")); QCOMPARE(h->asUnicodeString(), QString::fromLatin1("<1162746587.784559.5038.nullmailer@svn.kde.org>")); QVERIFY(!h->isEmpty()); // clearing a header h->clear(); QVERIFY(h->isEmpty()); QVERIFY(h->identifiers().isEmpty()); delete h; // parse multiple identifiers h = new Headers::Generics::Ident(); h->from7BitString(QByteArray("<1234@local.machine.example> <3456@example.net>")); QCOMPARE(h->identifiers().count(), 2); auto ids = h->identifiers(); QCOMPARE(ids.takeFirst(), QByteArray("1234@local.machine.example")); QCOMPARE(ids.first(), QByteArray("3456@example.net")); delete h; // parse multiple identifiers with folded headers h = new Headers::Generics::Ident(); h->from7BitString(QByteArray("<1234@local.machine.example>\n <3456@example.net>")); QCOMPARE(h->identifiers().count(), 2); ids = h->identifiers(); QCOMPARE(ids.takeFirst(), QByteArray("1234@local.machine.example")); QCOMPARE(ids.first(), QByteArray("3456@example.net")); // appending of new identifiers (with and without angle-brackets) h->appendIdentifier(""); h->appendIdentifier("78910@example.net"); QCOMPARE(h->identifiers().count(), 4); // assemble the final header QCOMPARE(h->as7BitString(false), QByteArray("<1234@local.machine.example> <3456@example.net> <78910@example.net>")); delete h; // parsing of ident with literal domain h = new Headers::Generics::Ident(); const QByteArray ident = QByteArray(""); h->appendIdentifier(ident); QEXPECT_FAIL("", "Parsing strips square brackets.", Continue); QCOMPARE(h->as7BitString(false), QByteArray(ident)); delete h; } void HeaderTest::testAddressListHeader() { // empty header Headers::Generics::AddressList *h = new Headers::Generics::AddressList(); QVERIFY(h->isEmpty()); // parse single simple address h->from7BitString("joe@where.test"); QVERIFY(!h->isEmpty()); QCOMPARE(h->addresses().count(), 1); QCOMPARE(h->addresses().first(), QByteArray("joe@where.test")); QCOMPARE(h->displayNames().count(), 1); QCOMPARE(h->displayNames().first(), QLatin1String("joe@where.test")); QCOMPARE(h->displayString(), QLatin1String("joe@where.test")); QCOMPARE(h->asUnicodeString(), QLatin1String("joe@where.test")); // clearing a header h->clear(); QVERIFY(h->isEmpty()); delete h; // parsing and re-assembling a single address with display name h = new Headers::Generics::AddressList(); h->from7BitString("Pete "); QCOMPARE(h->addresses().count(), 1); QCOMPARE(h->addresses().first(), QByteArray("pete@silly.example")); QCOMPARE(h->displayNames().first(), QLatin1String("Pete")); QCOMPARE(h->displayString(), QLatin1String("Pete")); QCOMPARE(h->asUnicodeString(), QLatin1String("Pete ")); QCOMPARE(h->as7BitString(false), QByteArray("Pete ")); delete h; // parsing a single address with legacy comment style display name h = new Headers::Generics::AddressList(); h->from7BitString("jdoe@machine.example (John Doe)"); QCOMPARE(h->addresses().count(), 1); QCOMPARE(h->addresses().first(), QByteArray("jdoe@machine.example")); QCOMPARE(h->displayNames().first(), QLatin1String("John Doe")); QCOMPARE(h->asUnicodeString(), QLatin1String("John Doe ")); delete h; // parsing and re-assembling list of diffrent addresses h = new Headers::Generics::AddressList(); h->from7BitString("Mary Smith , jdoe@example.org, Who? "); QCOMPARE(h->addresses().count(), 3); QStringList names = h->displayNames(); QCOMPARE(names.takeFirst(), QLatin1String("Mary Smith")); QCOMPARE(names.takeFirst(), QLatin1String("jdoe@example.org")); QCOMPARE(names.takeFirst(), QLatin1String("Who?")); QCOMPARE(h->displayString(), QLatin1String("Mary Smith, jdoe@example.org, Who?")); QCOMPARE(h->as7BitString(false), QByteArray("Mary Smith , jdoe@example.org, Who? ")); delete h; // same again with some interessting quoting h = new Headers::Generics::AddressList(); h->from7BitString("\"Joe Q. Public\" , , \"Giant; \\\"Big\\\" Box\" "); QCOMPARE(h->addresses().count(), 3); names = h->displayNames(); QCOMPARE(names.takeFirst(), QLatin1String("Joe Q. Public")); QCOMPARE(names.takeFirst(), QLatin1String("boss@nil.test")); QCOMPARE(names.takeFirst(), QLatin1String("Giant; \"Big\" Box")); QCOMPARE(h->as7BitString(false), QByteArray("\"Joe Q. Public\" , boss@nil.test, \"Giant; \\\"Big\\\" Box\" ")); delete h; // a display name with non-latin1 content h = new Headers::Generics::AddressList(); h->from7BitString("Ingo =?iso-8859-15?q?Kl=F6cker?= "); QCOMPARE(h->addresses().count(), 1); QCOMPARE(h->addresses().first(), QByteArray("kloecker@kde.org")); QCOMPARE(h->displayNames().first(), QString::fromUtf8("Ingo Klöcker")); QCOMPARE(h->asUnicodeString(), QString::fromUtf8("Ingo Klöcker ")); QCOMPARE(h->as7BitString(false), QByteArray("Ingo =?ISO-8859-1?Q?Kl=F6cker?= ")); delete h; // a display name with non-latin1 content in both name components h = new Headers::Generics::AddressList(); const QString testAddress = QString::fromUtf8("Ingö Klöcker "); h->fromUnicodeString(testAddress, "utf-8"); QCOMPARE(h->asUnicodeString(), testAddress); delete h; { // a display name with non-latin1 content in both name components h = new Headers::Generics::AddressList(); const QString testAddress = QString::fromUtf8("\"Rüedi-Huser, Thomas\" "); h->fromUnicodeString(testAddress, "utf-8"); QEXPECT_FAIL("", "AddressList::prettyAddresses() does not quote the mailbox correctly", Continue); QCOMPARE(h->asUnicodeString(), testAddress); delete h; } // again, this time legacy style h = new Headers::Generics::AddressList(); h->from7BitString("kloecker@kde.org (Ingo =?iso-8859-15?q?Kl=F6cker?=)"); QCOMPARE(h->addresses().count(), 1); QCOMPARE(h->addresses().first(), QByteArray("kloecker@kde.org")); QCOMPARE(h->displayNames().first(), QString::fromUtf8("Ingo Klöcker")); delete h; // parsing a empty group h = new Headers::Generics::AddressList(); h->from7BitString("Undisclosed recipients:;"); QCOMPARE(h->addresses().count(), 0); delete h; // parsing and re-assembling a address list with a group h = new Headers::Generics::AddressList(); h->from7BitString("A Group:Chris Jones ,joe@where.test,John ;"); QCOMPARE(h->addresses().count(), 3); names = h->displayNames(); QCOMPARE(names.takeFirst(), QLatin1String("Chris Jones")); QCOMPARE(names.takeFirst(), QLatin1String("joe@where.test")); QCOMPARE(names.takeFirst(), QLatin1String("John")); QCOMPARE(h->as7BitString(false), QByteArray("Chris Jones , joe@where.test, John ")); delete h; // modifying a header h = new Headers::Generics::AddressList(); h->from7BitString("John "); h->addAddress("", QString::fromUtf8("Ingo Klöcker")); h->addAddress("c@a.test"); QCOMPARE(h->addresses().count(), 3); QCOMPARE(h->asUnicodeString(), QString::fromUtf8("John , Ingo Klöcker , c@a.test")); QCOMPARE(h->as7BitString(false), QByteArray("John , Ingo =?ISO-8859-1?Q?Kl=F6cker?= , c@a.test")); delete h; // parsing from utf-8 h = new Headers::Generics::AddressList(); h->fromUnicodeString(QString::fromUtf8("Ingo Klöcker "), "utf-8"); QCOMPARE(h->addresses().count(), 1); QCOMPARE(h->addresses().first(), QByteArray("kloecker@kde.org")); QCOMPARE(h->displayNames().first(), QString::fromUtf8("Ingo Klöcker")); delete h; // based on bug #137033, a header broken in various ways: ';' as list separator, // unquoted '.' in display name h = new Headers::Generics::AddressList(); h->from7BitString("Vice@censored.serverkompetenz.net,\n President@mail2.censored.net;\"Int\\\\\\\\\\\\\\\\\\\\'l\" Lotto Commission. "); QCOMPARE(h->addresses().count(), 3); names = h->displayNames(); QCOMPARE(names.takeFirst(), QLatin1String("Vice@censored.serverkompetenz.net")); QCOMPARE(names.takeFirst(), QLatin1String("President@mail2.censored.net")); // there is an wrong ' ' after the name, but since the header is completely // broken we can be happy it parses at all... QCOMPARE(names.takeFirst(), QLatin1String("Int\\\\\\\\\\'l Lotto Commission. ")); auto addrs = h->addresses(); QCOMPARE(addrs.takeFirst(), QByteArray("Vice@censored.serverkompetenz.net")); QCOMPARE(addrs.takeFirst(), QByteArray("President@mail2.censored.net")); QCOMPARE(addrs.takeFirst(), QByteArray("censored@yahoo.fr")); delete h; // based on bug #102010, a display name containing '<' h = new Headers::Generics::AddressList(); h->from7BitString("\"|"); QCOMPARE(h->addresses().count(), 1); QCOMPARE(h->addresses().first(), QByteArray("censored@censored.dy")); QCOMPARE(h->displayNames().first(), QLatin1String("|as7BitString(false), QByteArray("\"|")); delete h; // based on bug #93790 (legacy display name with nested comments) h = new Headers::Generics::AddressList(); h->from7BitString("first.name@domain.tld (first name (nickname))"); QCOMPARE(h->displayNames().count(), 1); QCOMPARE(h->displayNames().first(), QLatin1String("first name (nickname)")); QCOMPARE(h->as7BitString(false), QByteArray("\"first name (nickname)\" ")); delete h; // rfc 2047 encoding in quoted name (it is not allowed there as per the RFC, but it happens) // some software == current KMail (v1.12.90) ... h = new Headers::Generics::AddressList(); h->from7BitString(QByteArray("\"Ingo =?iso-8859-15?q?Kl=F6cker?=\" ")); QCOMPARE(h->mailboxes().count(), 1); QCOMPARE(h->asUnicodeString(), QString::fromUtf8("Ingo Klöcker ")); delete h; // corner case of almost-rfc2047 encoded string in quoted string but not h = new Headers::Generics::AddressList(); h->from7BitString("\"Some =Use ?r\" "); QCOMPARE(h->mailboxes().count(), 1); QCOMPARE(h->as7BitString(false), QByteArray("\"Some =Use ?r\" ")); delete h; // corner case of almost-rfc2047 encoded string in quoted string but not h = new Headers::Generics::AddressList(); h->from7BitString("\"Some ?=U=?se =?r\" "); QCOMPARE(h->mailboxes().count(), 1); QCOMPARE(h->as7BitString(false), QByteArray("\"Some ?=U=?se =?r\" ")); delete h; // based on bug #139477, trailing '.' in domain name (RFC 3696, section 2 - http://tools.ietf.org/html/rfc3696#page-4) h = new Headers::Generics::AddressList(); h->from7BitString("joe@where.test."); QVERIFY(!h->isEmpty()); QCOMPARE(h->addresses().count(), 1); QCOMPARE(h->addresses().first(), QByteArray("joe@where.test.")); QCOMPARE(h->displayNames().count(), 1); QCOMPARE(h->displayNames().first(), QLatin1String("joe@where.test.")); QCOMPARE(h->asUnicodeString(), QLatin1String("joe@where.test.")); delete h; h = new Headers::Generics::AddressList(); h->from7BitString("Mary Smith , jdoe@example.org., Who? "); QCOMPARE(h->addresses().count(), 3); names = h->displayNames(); QCOMPARE(names.takeFirst(), QLatin1String("Mary Smith")); QCOMPARE(names.takeFirst(), QLatin1String("jdoe@example.org.")); QCOMPARE(names.takeFirst(), QLatin1String("Who?")); QCOMPARE(h->as7BitString(false), QByteArray("Mary Smith , jdoe@example.org., Who? ")); delete h; } void HeaderTest::testMailboxListHeader() { // empty header Headers::Generics::MailboxList *h = new Headers::Generics::MailboxList(); QVERIFY(h->isEmpty()); // parse single simple address h->from7BitString("joe_smith@where.test"); QVERIFY(!h->isEmpty()); QCOMPARE(h->mailboxes().count(), 1); QCOMPARE(h->addresses().count(), 1); QCOMPARE(h->addresses().first(), QByteArray("joe_smith@where.test")); QCOMPARE(h->displayNames().count(), 1); QCOMPARE(h->displayNames().first(), QLatin1String("joe_smith@where.test")); QCOMPARE(h->displayString(), QLatin1String("joe_smith@where.test")); QCOMPARE(h->asUnicodeString(), QLatin1String("joe_smith@where.test")); // https://bugzilla.novell.com/show_bug.cgi?id=421057 (but apparently this was not the cause of the bug) h->from7BitString("fr...@ce.sco (Francesco)"); QVERIFY(!h->isEmpty()); QCOMPARE(h->mailboxes().count(), 1); QCOMPARE(h->displayString(), QLatin1String("Francesco")); QCOMPARE(h->asUnicodeString(), QLatin1String("Francesco ")); delete h; } void HeaderTest::testSingleMailboxHeader() { // empty header Headers::Generics::SingleMailbox *h = new Headers::Generics::SingleMailbox(); QVERIFY(h->isEmpty()); // parse single simple address h->from7BitString("joe_smith@where.test"); QVERIFY(!h->isEmpty()); QCOMPARE(h->addresses().count(), 1); QCOMPARE(h->addresses().first(), QByteArray("joe_smith@where.test")); QCOMPARE(h->displayNames().count(), 1); QCOMPARE(h->displayNames().first(), QLatin1String("joe_smith@where.test")); QCOMPARE(h->asUnicodeString(), QLatin1String("joe_smith@where.test")); // parse single simple address with display name h->from7BitString("John Smith "); QVERIFY(!h->isEmpty()); QCOMPARE(h->addresses().count(), 1); QCOMPARE(h->addresses().first(), QByteArray("joe_smith@where.test")); QCOMPARE(h->displayNames().count(), 1); QCOMPARE(h->displayNames().first(), QLatin1String("John Smith")); QCOMPARE(h->asUnicodeString(), QLatin1String("John Smith ")); QCOMPARE(h->mailboxes().first().prettyAddress(Types::Mailbox::QuoteAlways), QLatin1String("\"John Smith\" ")); // parse quoted display name with \ in it h->from7BitString("\"Lastname\\, Firstname\" "); QVERIFY(!h->isEmpty()); QCOMPARE(h->addresses().count(), 1); QCOMPARE(h->addresses().first(), QByteArray("firstname.lastname@example.com")); QCOMPARE(h->displayNames().count(), 1); QCOMPARE(h->displayNames().first(), QLatin1String("Lastname, Firstname")); QCOMPARE(h->asUnicodeString().toLatin1().data(), "Lastname, Firstname "); QCOMPARE(h->mailboxes().first().prettyAddress().toLatin1().data(), "Lastname, Firstname "); QCOMPARE(h->mailboxes().first().prettyAddress(Types::Mailbox::QuoteWhenNecessary).toLatin1().data(), "\"Lastname, Firstname\" "); // parse quoted display name with " in it h->from7BitString("\"John \\\"the guru\\\" Smith\" "); QVERIFY(!h->isEmpty()); QCOMPARE(h->addresses().count(), 1); QCOMPARE(h->addresses().first().data(), "john.smith@mail.domain"); QCOMPARE(h->displayNames().first().toLatin1().data(), "John \"the guru\" Smith"); QCOMPARE(h->mailboxes().first().prettyAddress(Types::Mailbox::QuoteWhenNecessary).toLatin1().data(), "\"John \\\"the guru\\\" Smith\" "); QCOMPARE(h->as7BitString(false).data(), "\"John \\\"the guru\\\" Smith\" "); // The following tests are for broken clients that by accident add quotes inside of encoded words that enclose the // display name. We strip away those quotes, which is not strictly correct, but much nicer. h->from7BitString("=?iso-8859-1?Q?=22Andre_Woebbeking=22?= "); QVERIFY(!h->isEmpty()); QCOMPARE(h->addresses().count(), 1); QCOMPARE(h->mailboxes().first().name().toLatin1().data(), "Andre Woebbeking"); h->from7BitString("=?iso-8859-1?Q?=22Andre_=22Mr._Tall=22_Woebbeking=22?= "); QVERIFY(!h->isEmpty()); QCOMPARE(h->addresses().count(), 1); QCOMPARE(h->mailboxes().first().name().toLatin1().data(), "Andre \"Mr. Tall\" Woebbeking"); h->from7BitString("=?iso-8859-1?Q?=22Andre_=22?= =?iso-8859-1?Q?Mr._Tall?= =?iso-8859-1?Q?=22_Woebbeking=22?= "); QVERIFY(!h->isEmpty()); QCOMPARE(h->addresses().count(), 1); QCOMPARE(h->mailboxes().first().name().toLatin1().data(), "Andre \"Mr. Tall\" Woebbeking"); delete h; } void HeaderTest::testMailCopiesToHeader() { Headers::MailCopiesTo *h; // empty header h = new Headers::MailCopiesTo(); QVERIFY(h->isEmpty()); QVERIFY(!h->alwaysCopy()); QVERIFY(!h->neverCopy()); // set to always copy to poster h->setAlwaysCopy(); QVERIFY(!h->isEmpty()); QVERIFY(h->alwaysCopy()); QVERIFY(!h->neverCopy()); QCOMPARE(h->as7BitString(), QByteArray("Mail-Copies-To: poster")); // set to never copy h->setNeverCopy(); QVERIFY(!h->isEmpty()); QVERIFY(!h->alwaysCopy()); QVERIFY(h->neverCopy()); QCOMPARE(h->as7BitString(), QByteArray("Mail-Copies-To: nobody")); // clear header h->clear(); QVERIFY(h->isEmpty()); delete h; // parse copy to poster h = new MailCopiesTo; h->from7BitString("always"); QVERIFY(h->addresses().isEmpty()); QVERIFY(!h->isEmpty()); QVERIFY(h->alwaysCopy()); delete h; h = new MailCopiesTo; h->from7BitString("poster"); QVERIFY(h->addresses().isEmpty()); QVERIFY(!h->isEmpty()); QVERIFY(h->alwaysCopy()); delete h; // parse never copy h = new MailCopiesTo; h->from7BitString("never"); QVERIFY(h->addresses().isEmpty()); QVERIFY(!h->isEmpty()); QVERIFY(h->neverCopy()); delete h; h = new MailCopiesTo; h->from7BitString("nobody"); QVERIFY(h->addresses().isEmpty()); QVERIFY(!h->isEmpty()); QVERIFY(h->neverCopy()); delete h; // parsing is case-insensitive h = new MailCopiesTo; h->from7BitString("AlWays"); QVERIFY(h->alwaysCopy()); delete h; // parse address h = new MailCopiesTo; h->from7BitString("vkrause@kde.org"); QVERIFY(!h->addresses().isEmpty()); QVERIFY(h->alwaysCopy()); QVERIFY(!h->neverCopy()); QCOMPARE(h->as7BitString(), QByteArray("Mail-Copies-To: vkrause@kde.org")); delete h; } void HeaderTest::testParametrizedHeader() { Parametrized *h; // empty header h = new Parametrized(); QVERIFY(h->isEmpty()); QVERIFY(!h->hasParameter(QLatin1String("foo"))); // add a parameter h->setParameter(QLatin1String("filename"), QLatin1String("bla.jpg")); QVERIFY(!h->isEmpty()); QVERIFY(h->hasParameter(QLatin1String("filename"))); QVERIFY(h->hasParameter(QLatin1String("FiLeNaMe"))); QVERIFY(!h->hasParameter(QLatin1String("bla.jpg"))); QCOMPARE(h->parameter(QLatin1String("filename")), QLatin1String("bla.jpg")); QCOMPARE(h->as7BitString(false), QByteArray("filename=\"bla.jpg\"")); // clear again h->clear(); QVERIFY(h->isEmpty()); delete h; // parse a parameter list h = new Parametrized; h->from7BitString("filename=genome.jpeg;\n modification-date=\"Wed, 12 Feb 1997 16:29:51 -0500\""); QCOMPARE(h->parameter(QLatin1String("filename")), QLatin1String("genome.jpeg")); QCOMPARE(h->parameter(QLatin1String("modification-date")), QLatin1String("Wed, 12 Feb 1997 16:29:51 -0500")); QCOMPARE(h->as7BitString(false), QByteArray("filename=\"genome.jpeg\"; modification-date=\"Wed, 12 Feb 1997 16:29:51 -0500\"")); delete h; // quoting of whitespaces in parameter value h = new Parametrized(); h->setParameter(QLatin1String("boundary"), QLatin1String("simple boundary")); QCOMPARE(h->as7BitString(false), QByteArray("boundary=\"simple boundary\"")); delete h; // TODO: test RFC 2047 encoded values // TODO: test case-insensitive key-names } void HeaderTest::testContentDispositionHeader() { ContentDisposition *h; // empty header h = new ContentDisposition(); QVERIFY(h->isEmpty()); // set some values h->setFilename(QLatin1String("test.jpg")); QVERIFY(h->isEmpty()); QVERIFY(h->as7BitString(false).isEmpty()); h->setDisposition(CDattachment); QVERIFY(!h->isEmpty()); QCOMPARE(h->as7BitString(false), QByteArray("attachment; filename=\"test.jpg\"")); delete h; // parse parameter-less header h = new ContentDisposition; h->from7BitString("inline"); QCOMPARE(h->disposition(), CDinline); QVERIFY(h->filename().isEmpty()); QCOMPARE(h->as7BitString(true), QByteArray("Content-Disposition: inline")); delete h; // parse header with parameter h = new ContentDisposition; h->from7BitString("attachment; filename=genome.jpeg;\n modification-date=\"Wed, 12 Feb 1997 16:29:51 -0500\";"); QCOMPARE(h->disposition(), CDattachment); QCOMPARE(h->filename(), QLatin1String("genome.jpeg")); delete h; // TODO: test for case-insensitive disposition value } void HeaderTest::testContentTypeHeader() { ContentType *h; // empty header h = new ContentType(); QVERIFY(h->isEmpty()); // Empty content-type means text/plain (RFC 2045 §5.2) QVERIFY(h->isPlainText()); QVERIFY(h->isText()); // set a mimetype h->setMimeType("text/plain"); QVERIFY(!h->isEmpty()); QCOMPARE(h->mimeType(), QByteArray("text/plain")); QCOMPARE(h->mediaType(), QByteArray("text")); QCOMPARE(h->subType(), QByteArray("plain")); QVERIFY(h->isText()); QVERIFY(h->isPlainText()); QVERIFY(!h->isMultipart()); QVERIFY(!h->isPartial()); QVERIFY(h->isMediatype("text")); QVERIFY(h->isSubtype("plain")); QCOMPARE(h->as7BitString(true), QByteArray("Content-Type: text/plain")); // add some parameters h->setId("bla"); h->setCharset("us-ascii"); QCOMPARE(h->as7BitString(false), QByteArray("text/plain; charset=\"us-ascii\"; id=\"bla\"")); // clear header h->clear(); QVERIFY(h->isEmpty()); delete h; // parse a complete header h = new ContentType; h->from7BitString("text/plain; charset=us-ascii (Plain text)"); QVERIFY(h->isPlainText()); QCOMPARE(h->charset(), QByteArray("us-ascii")); delete h; // bug #136631 (name with rfc 2231 style parameter wrapping) h = new ContentType; h->from7BitString("text/plain;\n name*0=\"PIN_Brief_box1@xx.xxx.censored_Konfigkarte.confi\";\n name*1=\"guration.txt\""); QVERIFY(h->isPlainText()); QCOMPARE(h->name(), QLatin1String("PIN_Brief_box1@xx.xxx.censored_Konfigkarte.configuration.txt")); delete h; // bug #197958 (name of Content-Type sent by Mozilla Thunderbird are not parsed -- test case generated with v2.0.0.22) h = new ContentType; h->from7BitString("text/plain;\n name=\"=?ISO-8859-1?Q?lor=E9m_ipsum=2Etxt?=\""); QCOMPARE(h->name(), QString::fromUtf8("lorém ipsum.txt")); delete h; // bug #197958 (name of Content-Type sent by Mozilla Thunderbird are not parsed -- test case generated with v2.0.0.22) // But with unquoted string QEXPECT_FAIL("", "Unqouted rfc2047 strings are not supported as of now", Continue); h = new ContentType; h->from7BitString("text/plain;\n name==?ISO-8859-1?Q?lor=E9m_ipsum=2Etxt?="); QCOMPARE(h->name(), QString::fromUtf8("lorém ipsum.txt")); delete h; // make ervin's unit test happy h = new ContentType; h->setMimeType("MULTIPART/MIXED"); QVERIFY(h->isMultipart()); QVERIFY(h->isMediatype("multipart")); QVERIFY(h->isMediatype("Multipart")); QVERIFY(h->isMediatype("MULTIPART")); QVERIFY(h->isSubtype("mixed")); QVERIFY(h->isSubtype("Mixed")); QVERIFY(h->isSubtype("MIXED")); QCOMPARE(h->mimeType(), QByteArray("MULTIPART/MIXED")); QCOMPARE(h->mediaType(), QByteArray("MULTIPART")); QCOMPARE(h->subType(), QByteArray("MIXED")); delete h; } void HeaderTest::testTokenHeader() { Token *h; // empty header h = new Token(); QVERIFY(h->isEmpty()); // set a token h->setToken("bla"); QVERIFY(!h->isEmpty()); QCOMPARE(h->as7BitString(false), QByteArray("bla")); // clear it again h->clear(); QVERIFY(h->isEmpty()); delete h; // parse a header h = new Token; h->from7BitString("value (comment)"); QCOMPARE(h->token(), QByteArray("value")); QCOMPARE(h->as7BitString(false), QByteArray("value")); delete h; } void HeaderTest::testContentTransferEncoding() { ContentTransferEncoding *h; // empty header h = new ContentTransferEncoding(); QVERIFY(h->isEmpty()); // set an encoding h->setEncoding(CEbinary); QVERIFY(!h->isEmpty()); QCOMPARE(h->as7BitString(true), QByteArray("Content-Transfer-Encoding: binary")); // clear again h->clear(); QVERIFY(h->isEmpty()); delete h; // parse a header h = new ContentTransferEncoding; h->from7BitString("(comment) base64"); QCOMPARE(h->encoding(), CEbase64); QCOMPARE(h->as7BitString(false), QByteArray("base64")); delete h; } void HeaderTest::testPhraseListHeader() { PhraseList *h; // empty header h = new PhraseList(); QVERIFY(h->isEmpty()); delete h; // parse a simple phrase list h = new PhraseList; h->from7BitString("foo,\n bar"); QVERIFY(!h->isEmpty()); QCOMPARE(h->phrases().count(), 2); QStringList phrases = h->phrases(); QCOMPARE(phrases.takeFirst(), QLatin1String("foo")); QCOMPARE(phrases.takeFirst(), QLatin1String("bar")); QCOMPARE(h->as7BitString(false), QByteArray("foo, bar")); // clear header h->clear(); QVERIFY(h->isEmpty()); delete h; // TODO: encoded/quoted phrases } void HeaderTest::testDotAtomHeader() { DotAtom *h; // empty header h = new DotAtom; QVERIFY(h->isEmpty()); // parse a simple dot atom h->from7BitString("1.0 (mime version)"); QVERIFY(!h->isEmpty()); QCOMPARE(h->asUnicodeString(), QLatin1String("1.0")); // clear again h->clear(); QVERIFY(h->isEmpty()); delete h; // TODO: more complex atoms } void HeaderTest::testDateHeader() { Date *h; // empty header h = new Date(); QVERIFY(h->isEmpty()); // parse a simple date h->from7BitString("Fri, 21 Nov 1997 09:55:06 -0600"); QVERIFY(!h->isEmpty()); QCOMPARE(h->dateTime().date(), QDate(1997, 11, 21)); QCOMPARE(h->dateTime().time(), QTime(9, 55, 6)); QCOMPARE(h->dateTime().utcOffset(), -6 * 3600); QCOMPARE(h->as7BitString(), QByteArray("Date: Fri, 21 Nov 1997 09:55:06 -0600")); // clear it again h->clear(); QVERIFY(h->isEmpty()); delete h; // white spaces and comment (from RFC 2822, Appendix A.5) h = new Date; h->from7BitString("Thu,\n 13\n Feb\n 1969\n 23:32\n -0330 (Newfoundland Time)"); QVERIFY(!h->isEmpty()); QCOMPARE(h->dateTime().date(), QDate(1969, 2, 13)); QCOMPARE(h->dateTime().time(), QTime(23, 32)); QCOMPARE(h->dateTime().utcOffset(), -12600); QCOMPARE(h->as7BitString(false), QByteArray("Thu, 13 Feb 1969 23:32:00 -0330")); delete h; // obsolete date format (from RFC 2822, Appendix A.6.2) h = new Date; h->from7BitString("21 Nov 97 09:55:06 GMT"); QVERIFY(!h->isEmpty()); QCOMPARE(h->dateTime().date(), QDate(1997, 11, 21)); QCOMPARE(h->dateTime().time(), QTime(9, 55, 6)); QCOMPARE(h->dateTime().utcOffset(), 0); delete h; // obsolete whitespaces and commnets (from RFC 2822, Appendix A.6.3) h = new Date; h->from7BitString("Fri, 21 Nov 1997 09(comment): 55 : 06 -0600"); QVERIFY(!h->isEmpty()); QCOMPARE(h->dateTime().date(), QDate(1997, 11, 21)); QCOMPARE(h->dateTime().time(), QTime(9, 55, 6)); QCOMPARE(h->dateTime().utcOffset(), -6 * 3600); delete h; // Make sure uppercase OCT is parsed correctly - bug 150620 h = new Date; h->from7BitString("08 OCT 08 16:54:05 +0000"); QVERIFY(!h->isEmpty()); QCOMPARE(h->dateTime().date(), QDate(2008, 10, 8)); QCOMPARE(h->dateTime().time(), QTime(16, 54, 05)); QCOMPARE(h->dateTime().utcOffset(), 0); delete h; // Test for bug 111633, year < 1970 h = new Date; h->from7BitString("Mon, 27 Aug 1956 21:31:46 +0200"); QVERIFY(!h->isEmpty()); QCOMPARE(h->dateTime().date(), QDate(1956, 8, 27)); QCOMPARE(h->dateTime().time(), QTime(21, 31, 46)); QCOMPARE(h->dateTime().utcOffset(), +2 * 3600); delete h; // Test for bug 207766 h = new Date; h->from7BitString("Fri, 18 Sep 2009 04:44:55 -0400"); QVERIFY(!h->isEmpty()); QCOMPARE(h->dateTime().date(), QDate(2009, 9, 18)); QCOMPARE(h->dateTime().time(), QTime(4, 44, 55)); QCOMPARE(h->dateTime().utcOffset(), -4 * 3600); delete h; // Test for bug 260761 h = new Date; h->from7BitString("Sat, 18 Dec 2010 14:01:21 \"GMT\""); QVERIFY(!h->isEmpty()); QCOMPARE(h->dateTime().date(), QDate(2010, 12, 18)); QCOMPARE(h->dateTime().time(), QTime(14, 1, 21)); QCOMPARE(h->dateTime().utcOffset(), 0); delete h; // old asctime()-like formatted date; regression to KDE3; see bug 117848 h = new Date; h->from7BitString("Thu Mar 30 18:36:28 CEST 2006"); QVERIFY(!h->isEmpty()); QCOMPARE(h->dateTime().date(), QDate(2006, 3, 30)); QCOMPARE(h->dateTime().time(), QTime(18, 36, 28)); QCOMPARE(h->dateTime().utcOffset(), 2 * 3600); delete h; h = new Date; h->from7BitString("Thu Mar 30 18:36:28 2006"); QVERIFY(!h->isEmpty()); QCOMPARE(h->dateTime().date(), QDate(2006, 3, 30)); QCOMPARE(h->dateTime().time(), QTime(18, 36, 28)); QCOMPARE(h->dateTime().utcOffset(), 0); delete h; // regression to KDE3; see bug 54098 h = new Date; h->from7BitString("Tue, Feb 04, 2003 00:01:20 +0000"); QVERIFY(!h->isEmpty()); QCOMPARE(h->dateTime().date(), QDate(2003, 2, 4)); QCOMPARE(h->dateTime().time(), QTime(0, 1, 20)); QCOMPARE(h->dateTime().utcOffset(), 0); delete h; } void HeaderTest::testLinesHeader() { Lines *h; // empty header h = new Lines(); QVERIFY(h->isEmpty()); QVERIFY(h->as7BitString().isEmpty()); // set some content h->setNumberOfLines(5); QVERIFY(!h->isEmpty()); QCOMPARE(h->as7BitString(), QByteArray("Lines: 5")); // clear again h->clear(); QVERIFY(h->isEmpty()); delete h; // parse header with comment h = new Lines; h->from7BitString("(this is a comment) 10 (and yet another comment)"); QVERIFY(!h->isEmpty()); QCOMPARE(h->numberOfLines(), 10); delete h; } void HeaderTest::testNewsgroupsHeader() { Newsgroups *h; // empty header h = new Newsgroups(); QVERIFY(h->isEmpty()); QVERIFY(h->as7BitString().isEmpty()); // set newsgroups QVector groups; groups << "gmane.comp.kde.devel.core" << "gmane.comp.kde.devel.buildsystem"; h->setGroups(groups); QVERIFY(!h->isEmpty()); QCOMPARE(h->as7BitString(), QByteArray("Newsgroups: gmane.comp.kde.devel.core,gmane.comp.kde.devel.buildsystem")); // and clear again h->clear(); QVERIFY(h->isEmpty()); delete h; // parse a header h = new Newsgroups; h->from7BitString("gmane.comp.kde.devel.core,gmane.comp.kde.devel.buildsystem"); groups = h->groups(); QCOMPARE(groups.count(), 2); QCOMPARE(groups.takeFirst(), QByteArray("gmane.comp.kde.devel.core")); QCOMPARE(groups.takeFirst(), QByteArray("gmane.comp.kde.devel.buildsystem")); delete h; // same again, this time with whitespaces and comments h = new Newsgroups(); h->from7BitString("(comment) gmane.comp.kde.devel.core (second comment),\n gmane.comp.kde.devel.buildsystem (that all)"); groups = h->groups(); QCOMPARE(groups.count(), 2); QCOMPARE(groups.takeFirst(), QByteArray("gmane.comp.kde.devel.core")); QCOMPARE(groups.takeFirst(), QByteArray("gmane.comp.kde.devel.buildsystem")); delete h; } void HeaderTest::testControlHeader() { Control *h; // empty header h = new Control(); QVERIFY(h->isEmpty()); QVERIFY(h->as7BitString().isEmpty()); // set some content h->setCancel(""); QVERIFY(!h->isEmpty()); QVERIFY(h->isCancel()); QCOMPARE(h->as7BitString(), QByteArray("Control: cancel ")); // clear again h->clear(); QVERIFY(h->isEmpty()); delete h; // parse a control header h = new Control; h->from7BitString("cancel "); QVERIFY(!h->isEmpty()); QCOMPARE(h->parameter(), QByteArray("")); QVERIFY(h->isCancel()); QCOMPARE(h->controlType(), QByteArray("cancel")); delete h; } void HeaderTest::testReturnPath() { ReturnPath *h; h = new ReturnPath(); QVERIFY(h->isEmpty()); QVERIFY(h->as7BitString().isEmpty()); h->from7BitString(""); QVERIFY(!h->isEmpty()); QCOMPARE(h->as7BitString(true), QByteArray("Return-Path: ")); delete h; } void HeaderTest::noAbstractHeaders() { From *h2 = new From(); delete h2; Sender *h3 = new Sender(); delete h3; To *h4 = new To(); delete h4; Cc *h5 = new Cc(); delete h5; Bcc *h6 = new Bcc(); delete h6; ReplyTo *h7 = new ReplyTo(); delete h7; Keywords *h8 = new Keywords(); delete h8; MIMEVersion *h9 = new MIMEVersion(); delete h9; MessageID *h10 = new MessageID(); delete h10; ContentID *h11 = new ContentID(); delete h11; Supersedes *h12 = new Supersedes(); delete h12; InReplyTo *h13 = new InReplyTo(); delete h13; References *h14 = new References(); delete h14; Generic *h15 = new Generic(); delete h15; Subject *h16 = new Subject(); delete h16; Organization *h17 = new Organization(); delete h17; ContentDescription *h18 = new ContentDescription(); delete h18; FollowUpTo *h22 = new FollowUpTo(); delete h22; UserAgent *h24 = new UserAgent(); delete h24; } void HeaderTest::testInvalidButOkQEncoding() { // A stray '?' should not confuse the parser Subject subject; subject.from7BitString("=?us-ascii?q?Why?_Why_do_some_clients_violate_the_RFC?" "?="); QCOMPARE(subject.as7BitString(false), QByteArray("Why? Why do some clients violate the RFC?")); } void HeaderTest::testInvalidQEncoding_data() { QTest::addColumn("encodedWord"); // All examples below should not be treated as invalid encoded strings, since the '?=' is missing QTest::newRow("") << QString::fromLatin1("=?us-ascii?q?Why?_Why_do_some_clients_violate_the_RFC??"); QTest::newRow("") << QString::fromLatin1("=?us-ascii?q?Why?_Why_do_some_clients_violate_the_RFC?"); QTest::newRow("") << QString::fromLatin1("=?us-ascii?q?Why?_Why_do_some_clients_violate_the_RFC"); } void HeaderTest::testInvalidQEncoding() { using namespace HeaderParsing; QFETCH(QString, encodedWord); QByteArray tmp = encodedWord.toLatin1(); const char *data = tmp.data(); const char *start = data + 1; const char *end = data + strlen(data); QString result; QByteArray language; QByteArray usedCS; QVERIFY(!parseEncodedWord(start, end, result, language, usedCS)); } +void HeaderTest::testMissingQuotes() +{ + QByteArray str = "multipart/signed; boundary=nextPart22807781.u8zn2zYrSU; micalg=pgp-sha1; protocol=application/pgp-signature"; + + Headers::ContentType ct; + ct.from7BitString(str); + QCOMPARE(ct.mimeType(), QByteArray{ "multipart/signed" }); + QCOMPARE(ct.boundary(), QByteArray{ "nextPart22807781.u8zn2zYrSU" }); + QCOMPARE(ct.parameter(QStringLiteral("micalg")), QStringLiteral("pgp-sha1")); + QCOMPARE(ct.parameter(QStringLiteral("protocol")), QStringLiteral("application/pgp-signature")); + +} + void HeaderTest::testBug271192() { QFETCH(QString, displayName); QFETCH(bool, quote); const QString addrSpec = QLatin1String("example@example.com"); const QString mailbox = (quote ? QLatin1String("\"") : QString()) + displayName + (quote ? QLatin1String("\"") : QString()) + QLatin1String(" <") + addrSpec + QLatin1String(">"); Headers::Generics::SingleMailbox *h = new Headers::Generics::SingleMailbox(); h->fromUnicodeString(mailbox, "utf-8"); QCOMPARE(h->displayNames().size(), 1); QCOMPARE(h->displayNames().first().toUtf8(), displayName.remove(QLatin1String("\\")).toUtf8()); delete h; h = nullptr; Headers::Generics::MailboxList *h2 = new Headers::Generics::MailboxList(); h2->fromUnicodeString(mailbox + QLatin1String(",") + mailbox, "utf-8"); QCOMPARE(h2->displayNames().size(), 2); QCOMPARE(h2->displayNames()[0].toUtf8(), displayName.remove(QLatin1String("\\")).toUtf8()); QCOMPARE(h2->displayNames()[1].toUtf8(), displayName.remove(QLatin1String("\\")).toUtf8()); delete h2; h2 = nullptr; } void HeaderTest::testBug271192_data() { QTest::addColumn("displayName"); QTest::addColumn("quote"); QTest::newRow("Plain") << QString::fromUtf8("John Doe") << false; QTest::newRow("Firstname_1") << QString::fromUtf8("Marc-André Lastname") << false; QTest::newRow("Firstname_2") << QString::fromUtf8("Интернет-компания Lastname") << false; QTest::newRow("Lastname") << QString::fromUtf8("Tobias König") << false; QTest::newRow("Firstname_Lastname") << QString::fromUtf8("Интернет-компания König") << false; QTest::newRow("Quotemarks") << QString::fromUtf8("John \\\"Rocky\\\" Doe") << true; QTest::newRow("Quotemarks_nonascii") << QString::fromUtf8("Jöhn \\\"Röcky\\\" Döe") << true; QTest::newRow("quote_Plain") << QString::fromUtf8("John Doe") << true; QTest::newRow("quote_Firstname_1") << QString::fromUtf8("Marc-André Lastname") << true; QTest::newRow("quote_Firstname_2") << QString::fromUtf8("Интернет-компания Lastname") << true; QTest::newRow("quote_Lastname") << QString::fromUtf8("Tobias König") << true; QTest::newRow("quote_Firstname_Lastname") << QString::fromUtf8("Интернет-компания König") << true; QTest::newRow("quote_LastName_comma_Firstname") << QString::fromUtf8("König, Интернет-компания") << true; } diff --git a/autotests/headertest.h b/autotests/headertest.h index c601726..4ec4968 100644 --- a/autotests/headertest.h +++ b/autotests/headertest.h @@ -1,57 +1,58 @@ /* Copyright (c) 2006 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef HEADERTEST_H #define HEADERTEST_H #include class HeaderTest : public QObject { Q_OBJECT private Q_SLOTS: void testIdentHeader(); void testAddressListHeader(); void testMailboxListHeader(); void testSingleMailboxHeader(); void testMailCopiesToHeader(); void testParametrizedHeader(); void testContentDispositionHeader(); void testContentTypeHeader(); void testTokenHeader(); void testContentTransferEncoding(); void testPhraseListHeader(); void testDotAtomHeader(); void testDateHeader(); void testLinesHeader(); void testNewsgroupsHeader(); void testControlHeader(); void testReturnPath(); void testInvalidButOkQEncoding(); void testInvalidQEncoding(); void testInvalidQEncoding_data(); void testBug271192(); void testBug271192_data(); + void testMissingQuotes(); // makes sure we don't accidently have an abstract header class that's not // meant to be abstract void noAbstractHeaders(); }; #endif diff --git a/src/kmime_header_parsing.cpp b/src/kmime_header_parsing.cpp index 8f78687..0829308 100644 --- a/src/kmime_header_parsing.cpp +++ b/src/kmime_header_parsing.cpp @@ -1,2105 +1,2107 @@ /* -*- c++ -*- kmime_header_parsing.cpp KMime, the KDE Internet mail/usenet news message library. Copyright (c) 2001-2002 Marc Mutz This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kmime_header_parsing.h" #include "kmime_headerfactory_p.h" #include "kmime_headers.h" #include "kmime_util.h" #include "kmime_util_p.h" #include "kmime_codecs.h" #include "kmime_dateformatter.h" #include "kmime_warning.h" #include #include #include #include #include #include #include // for isdigit #include using namespace KMime; using namespace KMime::Types; namespace KMime { namespace HeaderParsing { // parse the encoded-word (scursor points to after the initial '=') bool parseEncodedWord(const char *&scursor, const char *const send, QString &result, QByteArray &language, QByteArray &usedCS, const QByteArray &defaultCS, bool forceCS) { // make sure the caller already did a bit of the work. assert(*(scursor - 1) == '='); // // STEP 1: // scan for the charset/language portion of the encoded-word // char ch = *scursor++; if (ch != '?') { // qDebug() << "first"; //KMIME_WARN_PREMATURE_END_OF( EncodedWord ); return false; } // remember start of charset (ie. just after the initial "=?") and // language (just after the first '*') fields: const char *charsetStart = scursor; const char *languageStart = nullptr; // find delimiting '?' (and the '*' separating charset and language // tags, if any): for (; scursor != send ; scursor++) { if (*scursor == '?') { break; } else if (*scursor == '*' && languageStart == nullptr) { languageStart = scursor + 1; } } // not found? can't be an encoded-word! if (scursor == send || *scursor != '?') { // qDebug() << "second"; KMIME_WARN_PREMATURE_END_OF(EncodedWord); return false; } // extract the language information, if any (if languageStart is 0, // language will be null, too): QByteArray maybeLanguage(languageStart, scursor - languageStart); // extract charset information (keep in mind: the size given to the // ctor is one off due to the \0 terminator): QByteArray maybeCharset(charsetStart, (languageStart ? languageStart - 1 : scursor) - charsetStart); // // STEP 2: // scan for the encoding portion of the encoded-word // // remember start of encoding (just _after_ the second '?'): scursor++; const char *encodingStart = scursor; // find next '?' (ending the encoding tag): for (; scursor != send ; scursor++) { if (*scursor == '?') { break; } } // not found? Can't be an encoded-word! if (scursor == send || *scursor != '?') { // qDebug() << "third"; KMIME_WARN_PREMATURE_END_OF(EncodedWord); return false; } // extract the encoding information: QByteArray maybeEncoding(encodingStart, scursor - encodingStart); // qDebug() << "parseEncodedWord: found charset == \"" << maybeCharset // << "\"; language == \"" << maybeLanguage // << "\"; encoding == \"" << maybeEncoding << "\""; // // STEP 3: // scan for encoded-text portion of encoded-word // // remember start of encoded-text (just after the third '?'): scursor++; const char *encodedTextStart = scursor; // find the '?=' sequence (ending the encoded-text): for (; scursor != send ; scursor++) { if (*scursor == '?') { if (scursor + 1 != send) { if (*(scursor + 1) != '=') { // We expect a '=' after the '?', but we got something else; ignore KMIME_WARN << "Stray '?' in q-encoded word, ignoring this."; continue; } else { // yep, found a '?=' sequence scursor += 2; break; } } else { // The '?' is the last char, but we need a '=' after it! KMIME_WARN_PREMATURE_END_OF(EncodedWord); return false; } } } if (*(scursor - 2) != '?' || *(scursor - 1) != '=' || scursor < encodedTextStart + 2) { KMIME_WARN_PREMATURE_END_OF(EncodedWord); return false; } // set end sentinel for encoded-text: const char *const encodedTextEnd = scursor - 2; // // STEP 4: // setup decoders for the transfer encoding and the charset // // try if there's a codec for the encoding found: KCodecs::Codec *codec = KCodecs::Codec::codecForName(maybeEncoding); if (!codec) { KMIME_WARN_UNKNOWN(Encoding, maybeEncoding); return false; } // get an instance of a corresponding decoder: KCodecs::Decoder *dec = codec->makeDecoder(); assert(dec); // try if there's a (text)codec for the charset found: bool matchOK = false; QTextCodec *textCodec = nullptr; if (forceCS || maybeCharset.isEmpty()) { textCodec = KCharsets::charsets()->codecForName(QLatin1String(defaultCS), matchOK); usedCS = cachedCharset(defaultCS); } else { textCodec = KCharsets::charsets()->codecForName(QLatin1String(maybeCharset), matchOK); if (!matchOK) { //no suitable codec found => use default charset textCodec = KCharsets::charsets()->codecForName(QLatin1String(defaultCS), matchOK); usedCS = cachedCharset(defaultCS); } else { usedCS = cachedCharset(maybeCharset); } } if (!matchOK || !textCodec) { KMIME_WARN_UNKNOWN(Charset, maybeCharset); delete dec; return false; }; // qDebug() << "mimeName(): \"" << textCodec->name() << "\""; // allocate a temporary buffer to store the 8bit text: int encodedTextLength = encodedTextEnd - encodedTextStart; QByteArray buffer; buffer.resize(codec->maxDecodedSizeFor(encodedTextLength)); char *bbegin = buffer.data(); char *bend = bbegin + buffer.length(); // // STEP 5: // do the actual decoding // if (!dec->decode(encodedTextStart, encodedTextEnd, bbegin, bend)) { KMIME_WARN << codec->name() << "codec lies about its maxDecodedSizeFor(" << encodedTextLength << ")\nresult may be truncated"; } result = textCodec->toUnicode(buffer.data(), bbegin - buffer.data()); // qDebug() << "result now: \"" << result << "\""; // cleanup: delete dec; language = maybeLanguage; return true; } static inline void eatWhiteSpace(const char *&scursor, const char *const send) { while (scursor != send && (*scursor == ' ' || *scursor == '\n' || *scursor == '\t' || *scursor == '\r')) { scursor++; } } bool parseAtom(const char*&scursor, const char *const send, QString &result, bool allow8Bit) { QPair maybeResult; if (parseAtom(scursor, send, maybeResult, allow8Bit)) { result = QString::fromLatin1(maybeResult.first, maybeResult.second); return true; } return false; } bool parseAtom(const char*&scursor, const char *const send, QPair &result, bool allow8Bit) { bool success = false; const char *start = scursor; while (scursor != send) { signed char ch = *scursor++; if (ch > 0 && isAText(ch)) { // AText: OK success = true; } else if (allow8Bit && ch < 0) { // 8bit char: not OK, but be tolerant. KMIME_WARN_8BIT(ch); success = true; } else { // CTL or special - marking the end of the atom: // re-set sursor to point to the offending // char and return: scursor--; break; } } result.first = start; result.second = scursor - start; return success; } // FIXME: Remove this and the other parseToken() method. add a new one where "result" is a // QByteArray. bool parseToken(const char*&scursor, const char *const send, - QString &result, bool allow8Bit) + QString &result, ParseTokenFlags flags) { QPair maybeResult; - if (parseToken(scursor, send, maybeResult, allow8Bit)) { + if (parseToken(scursor, send, maybeResult, flags)) { result = QString::fromLatin1(maybeResult.first, maybeResult.second); return true; } return false; } bool parseToken(const char*&scursor, const char *const send, - QPair &result, bool allow8Bit) + QPair &result, ParseTokenFlags flags) { bool success = false; const char *start = scursor; while (scursor != send) { signed char ch = *scursor++; if (ch > 0 && isTText(ch)) { // TText: OK success = true; - } else if (allow8Bit && ch < 0) { + } else if ((flags & ParseTokenAllow8Bit) && ch < 0) { // 8bit char: not OK, but be tolerant. KMIME_WARN_8BIT(ch); success = true; + } else if ((flags & ParseTokenRelaxedTText) && ch == '/') { + success = true; } else { // CTL or tspecial - marking the end of the atom: // re-set sursor to point to the offending // char and return: scursor--; break; } } result.first = start; result.second = scursor - start; return success; } #define READ_ch_OR_FAIL if ( scursor == send ) { \ KMIME_WARN_PREMATURE_END_OF( GenericQuotedString ); \ return false; \ } else { \ ch = *scursor++; \ } // known issues: // // - doesn't handle quoted CRLF // FIXME: Why is result a QString? This should be a QByteArray, since at this level, we don't // know about encodings yet! bool parseGenericQuotedString(const char *&scursor, const char *const send, QString &result, bool isCRLF, const char openChar, const char closeChar) { char ch; // We are in a quoted-string or domain-literal or comment and the // cursor points to the first char after the openChar. // We will apply unfolding and quoted-pair removal. // We return when we either encounter the end or unescaped openChar // or closeChar. assert(*(scursor - 1) == openChar || *(scursor - 1) == closeChar); while (scursor != send) { ch = *scursor++; if (ch == closeChar || ch == openChar) { // end of quoted-string or another opening char: // let caller decide what to do. return true; } switch (ch) { case '\\': // quoted-pair // misses "\" CRLF LWSP-char handling, see rfc822, 3.4.5 READ_ch_OR_FAIL; KMIME_WARN_IF_8BIT(ch); result += QLatin1Char(ch); break; case '\r': // ### // The case of lonely '\r' is easy to solve, as they're // not part of Unix Line-ending conventions. // But I see a problem if we are given Unix-native // line-ending-mails, where we cannot determine anymore // whether a given '\n' was part of a CRLF or was occurring // on it's own. READ_ch_OR_FAIL; if (ch != '\n') { // CR on it's own... KMIME_WARN_LONE(CR); result += QLatin1Char('\r'); scursor--; // points to after the '\r' again } else { // CRLF encountered. // lookahead: check for folding READ_ch_OR_FAIL; if (ch == ' ' || ch == '\t') { // correct folding; // position cursor behind the CRLF WSP (unfolding) // and add the WSP to the result result += QLatin1Char(ch); } else { // this is the "shouldn't happen"-case. There is a CRLF // inside a quoted-string without it being part of FWS. // We take it verbatim. KMIME_WARN_NON_FOLDING(CRLF); result += QLatin1String("\r\n"); // the cursor is decremented again, so's we need not // duplicate the whole switch here. "ch" could've been // everything (incl. openChar or closeChar). scursor--; } } break; case '\n': // Note: CRLF has been handled above already! // ### LF needs special treatment, depending on whether isCRLF // is true (we can be sure a lonely '\n' was meant this way) or // false ('\n' alone could have meant LF or CRLF in the original // message. This parser assumes CRLF iff the LF is followed by // either WSP (folding) or NULL (premature end of quoted-string; // Should be fixed, since NULL is allowed as per rfc822). READ_ch_OR_FAIL; if (!isCRLF && (ch == ' ' || ch == '\t')) { // folding // correct folding result += QLatin1Char(ch); } else { // non-folding KMIME_WARN_LONE(LF); result += QLatin1Char('\n'); // pos is decremented, so's we need not duplicate the whole // switch here. ch could've been everything (incl. <">, "\"). scursor--; } break; case '=': { // ### Work around broken clients that send encoded words in quoted-strings // For example, older KMail versions. if (scursor == send) { break; } const char *oldscursor = scursor; QString tmp; QByteArray lang, charset; if (*scursor++ == '?') { --scursor; if (parseEncodedWord(scursor, send, tmp, lang, charset)) { result += tmp; break; } else { scursor = oldscursor; } } else { scursor = oldscursor; } // fall through Q_FALLTHROUGH(); } default: KMIME_WARN_IF_8BIT(ch); result += QLatin1Char(ch); } } return false; } // known issues: // // - doesn't handle encoded-word inside comments. bool parseComment(const char *&scursor, const char *const send, QString &result, bool isCRLF, bool reallySave) { int commentNestingDepth = 1; const char *afterLastClosingParenPos = nullptr; QString maybeCmnt; const char *oldscursor = scursor; assert(*(scursor - 1) == '('); while (commentNestingDepth) { QString cmntPart; if (parseGenericQuotedString(scursor, send, cmntPart, isCRLF, '(', ')')) { assert(*(scursor - 1) == ')' || *(scursor - 1) == '('); // see the kdoc for above function for the possible conditions // we have to check: switch (*(scursor - 1)) { case ')': if (reallySave) { // add the chunk that's now surely inside the comment. result += maybeCmnt; result += cmntPart; if (commentNestingDepth > 1) { // don't add the outermost ')'... result += QLatin1Char(')'); } maybeCmnt.clear(); } afterLastClosingParenPos = scursor; --commentNestingDepth; break; case '(': if (reallySave) { // don't add to "result" yet, because we might find that we // are already outside the (broken) comment... maybeCmnt += cmntPart; maybeCmnt += QLatin1Char('('); } ++commentNestingDepth; break; default: assert(0); } // switch } else { // !parseGenericQuotedString, ie. premature end if (afterLastClosingParenPos) { scursor = afterLastClosingParenPos; } else { scursor = oldscursor; } return false; } } // while return true; } // known issues: none. bool parsePhrase(const char *&scursor, const char *const send, QString &result, bool isCRLF) { enum { None, Phrase, Atom, EncodedWord, QuotedString } found = None; QString tmp; QByteArray lang, charset; const char *successfullyParsed = nullptr; // only used by the encoded-word branch const char *oldscursor; // used to suppress whitespace between adjacent encoded-words // (rfc2047, 6.2): bool lastWasEncodedWord = false; while (scursor != send) { char ch = *scursor++; switch (ch) { case '.': // broken, but allow for intorop's sake if (found == None) { --scursor; return false; } else { if (scursor != send && (*scursor == ' ' || *scursor == '\t')) { result += QLatin1String(". "); } else { result += QLatin1Char('.'); } successfullyParsed = scursor; } break; case '"': // quoted-string tmp.clear(); if (parseGenericQuotedString(scursor, send, tmp, isCRLF, '"', '"')) { successfullyParsed = scursor; assert(*(scursor - 1) == '"'); switch (found) { case None: found = QuotedString; break; case Phrase: case Atom: case EncodedWord: case QuotedString: found = Phrase; result += QLatin1Char(' '); // rfc822, 3.4.4 break; default: assert(0); } lastWasEncodedWord = false; result += tmp; } else { // premature end of quoted string. // What to do? Return leading '"' as special? Return as quoted-string? // We do the latter if we already found something, else signal failure. if (found == None) { return false; } else { result += QLatin1Char(' '); // rfc822, 3.4.4 result += tmp; return true; } } break; case '(': // comment // parse it, but ignore content: tmp.clear(); if (parseComment(scursor, send, tmp, isCRLF, false /*don't bother with the content*/)) { successfullyParsed = scursor; lastWasEncodedWord = false; // strictly interpreting rfc2047, 6.2 } else { if (found == None) { return false; } else { scursor = successfullyParsed; return true; } } break; case '=': // encoded-word tmp.clear(); oldscursor = scursor; lang.clear(); charset.clear(); if (parseEncodedWord(scursor, send, tmp, lang, charset)) { successfullyParsed = scursor; switch (found) { case None: found = EncodedWord; break; case Phrase: case EncodedWord: case Atom: case QuotedString: if (!lastWasEncodedWord) { result += QLatin1Char(' '); // rfc822, 3.4.4 } found = Phrase; break; default: assert(0); } lastWasEncodedWord = true; result += tmp; break; } else { // parse as atom: scursor = oldscursor; } Q_FALLTHROUGH(); // fall though... default: //atom tmp.clear(); scursor--; if (parseAtom(scursor, send, tmp, true /* allow 8bit */)) { successfullyParsed = scursor; switch (found) { case None: found = Atom; break; case Phrase: case Atom: case EncodedWord: case QuotedString: found = Phrase; result += QLatin1Char(' '); // rfc822, 3.4.4 break; default: assert(0); } lastWasEncodedWord = false; result += tmp; } else { if (found == None) { return false; } else { scursor = successfullyParsed; return true; } } } eatWhiteSpace(scursor, send); } return found != None; } // FIXME: This should probably by QByteArray &result instead? bool parseDotAtom(const char *&scursor, const char *const send, QString &result, bool isCRLF) { eatCFWS(scursor, send, isCRLF); // always points to just after the last atom parsed: const char *successfullyParsed; QString tmp; if (!parseAtom(scursor, send, tmp, false /* no 8bit */)) { return false; } result += tmp; successfullyParsed = scursor; while (scursor != send) { // end of header or no '.' -> return if (scursor == send || *scursor != '.') { return true; } scursor++; // eat '.' if (scursor == send || !isAText(*scursor)) { // end of header or no AText, but this time following a '.'!: // reset cursor to just after last successfully parsed char and // return: scursor = successfullyParsed; return true; } // try to parse the next atom: QString maybeAtom; if (!parseAtom(scursor, send, maybeAtom, false /*no 8bit*/)) { scursor = successfullyParsed; return true; } result += QLatin1Char('.'); result += maybeAtom; successfullyParsed = scursor; } scursor = successfullyParsed; return true; } void eatCFWS(const char *&scursor, const char *const send, bool isCRLF) { QString dummy; while (scursor != send) { const char *oldscursor = scursor; char ch = *scursor++; switch (ch) { case ' ': case '\t': // whitespace case '\r': case '\n': // folding continue; case '(': // comment if (parseComment(scursor, send, dummy, isCRLF, false /*don't save*/)) { continue; } scursor = oldscursor; return; default: scursor = oldscursor; return; } } } bool parseDomain(const char *&scursor, const char *const send, QString &result, bool isCRLF) { eatCFWS(scursor, send, isCRLF); if (scursor == send) { return false; } // domain := dot-atom / domain-literal / atom *("." atom) // // equivalent to: // domain = dot-atom / domain-literal, // since parseDotAtom does allow CFWS between atoms and dots if (*scursor == '[') { // domain-literal: QString maybeDomainLiteral; // eat '[': scursor++; while (parseGenericQuotedString(scursor, send, maybeDomainLiteral, isCRLF, '[', ']')) { if (scursor == send) { // end of header: check for closing ']': if (*(scursor - 1) == ']') { // OK, last char was ']': result = maybeDomainLiteral; return true; } else { // not OK, domain-literal wasn't closed: return false; } } // we hit openChar in parseGenericQuotedString. // include it in maybeDomainLiteral and keep on parsing: if (*(scursor - 1) == '[') { maybeDomainLiteral += QLatin1Char('['); continue; } // OK, real end of domain-literal: result = maybeDomainLiteral; return true; } } else { // dot-atom: QString maybeDotAtom; if (parseDotAtom(scursor, send, maybeDotAtom, isCRLF)) { result = maybeDotAtom; // Domain may end with '.', if so preserve it' if (scursor != send && *scursor == '.') { result += QLatin1Char('.'); scursor++; } return true; } } return false; } bool parseObsRoute(const char *&scursor, const char *const send, QStringList &result, bool isCRLF, bool save) { while (scursor != send) { eatCFWS(scursor, send, isCRLF); if (scursor == send) { return false; } // empty entry: if (*scursor == ',') { scursor++; if (save) { result.append(QString()); } continue; } // empty entry ending the list: if (*scursor == ':') { scursor++; if (save) { result.append(QString()); } return true; } // each non-empty entry must begin with '@': if (*scursor != '@') { return false; } else { scursor++; } QString maybeDomain; if (!parseDomain(scursor, send, maybeDomain, isCRLF)) { return false; } if (save) { result.append(maybeDomain); } // eat the following (optional) comma: eatCFWS(scursor, send, isCRLF); if (scursor == send) { return false; } if (*scursor == ':') { scursor++; return true; } if (*scursor == ',') { scursor++; } } return false; } bool parseAddrSpec(const char *&scursor, const char *const send, AddrSpec &result, bool isCRLF) { // // STEP 1: // local-part := dot-atom / quoted-string / word *("." word) // // this is equivalent to: // local-part := word *("." word) QString maybeLocalPart; QString tmp; while (scursor != send) { // first, eat any whitespace eatCFWS(scursor, send, isCRLF); char ch = *scursor++; switch (ch) { case '.': // dot maybeLocalPart += QLatin1Char('.'); break; case '@': goto SAW_AT_SIGN; break; case '"': // quoted-string tmp.clear(); if (parseGenericQuotedString(scursor, send, tmp, isCRLF, '"', '"')) { maybeLocalPart += tmp; } else { return false; } break; default: // atom scursor--; // re-set scursor to point to ch again tmp.clear(); if (parseAtom(scursor, send, tmp, false /* no 8bit */)) { maybeLocalPart += tmp; } else { return false; // parseAtom can only fail if the first char is non-atext. } break; } } return false; // // STEP 2: // domain // SAW_AT_SIGN: assert(*(scursor - 1) == '@'); QString maybeDomain; if (!parseDomain(scursor, send, maybeDomain, isCRLF)) { return false; } result.localPart = maybeLocalPart; result.domain = maybeDomain; return true; } bool parseAngleAddr(const char *&scursor, const char *const send, AddrSpec &result, bool isCRLF) { // first, we need an opening angle bracket: eatCFWS(scursor, send, isCRLF); if (scursor == send || *scursor != '<') { return false; } scursor++; // eat '<' eatCFWS(scursor, send, isCRLF); if (scursor == send) { return false; } if (*scursor == '@' || *scursor == ',') { // obs-route: parse, but ignore: KMIME_WARN << "obsolete source route found! ignoring."; QStringList dummy; if (!parseObsRoute(scursor, send, dummy, isCRLF, false /* don't save */)) { return false; } // angle-addr isn't complete until after the '>': if (scursor == send) { return false; } } // parse addr-spec: AddrSpec maybeAddrSpec; if (!parseAddrSpec(scursor, send, maybeAddrSpec, isCRLF)) { return false; } eatCFWS(scursor, send, isCRLF); if (scursor == send || *scursor != '>') { return false; } scursor++; result = maybeAddrSpec; return true; } static QString stripQuotes(const QString &input) { const QLatin1Char quotes('"'); if (input.startsWith(quotes) && input.endsWith(quotes)) { QString stripped(input.mid(1, input.size() - 2)); return stripped; } else { return input; } } bool parseMailbox(const char *&scursor, const char *const send, Mailbox &result, bool isCRLF) { eatCFWS(scursor, send, isCRLF); if (scursor == send) { return false; } AddrSpec maybeAddrSpec; QString maybeDisplayName; // first, try if it's a vanilla addr-spec: const char *oldscursor = scursor; if (parseAddrSpec(scursor, send, maybeAddrSpec, isCRLF)) { result.setAddress(maybeAddrSpec); // check for the obsolete form of display-name (as comment): eatWhiteSpace(scursor, send); if (scursor != send && *scursor == '(') { scursor++; if (!parseComment(scursor, send, maybeDisplayName, isCRLF, true /*keep*/)) { return false; } } result.setName(stripQuotes(maybeDisplayName)); return true; } scursor = oldscursor; // second, see if there's a display-name: if (!parsePhrase(scursor, send, maybeDisplayName, isCRLF)) { // failed: reset cursor, note absent display-name maybeDisplayName.clear(); scursor = oldscursor; } else { // succeeded: eat CFWS eatCFWS(scursor, send, isCRLF); if (scursor == send) { return false; } } // third, parse the angle-addr: if (!parseAngleAddr(scursor, send, maybeAddrSpec, isCRLF)) { return false; } if (maybeDisplayName.isNull()) { // check for the obsolete form of display-name (as comment): eatWhiteSpace(scursor, send); if (scursor != send && *scursor == '(') { scursor++; if (!parseComment(scursor, send, maybeDisplayName, isCRLF, true /*keep*/)) { return false; } } } result.setName(stripQuotes(maybeDisplayName)); result.setAddress(maybeAddrSpec); return true; } bool parseGroup(const char *&scursor, const char *const send, Address &result, bool isCRLF) { // group := display-name ":" [ mailbox-list / CFWS ] ";" [CFWS] // // equivalent to: // group := display-name ":" [ obs-mbox-list ] ";" eatCFWS(scursor, send, isCRLF); if (scursor == send) { return false; } // get display-name: QString maybeDisplayName; if (!parsePhrase(scursor, send, maybeDisplayName, isCRLF)) { return false; } // get ":": eatCFWS(scursor, send, isCRLF); if (scursor == send || *scursor != ':') { return false; } // KDE5 TODO: Don't expose displayName as public, but rather add setter for it that // automatically calls removeBidiControlChars result.displayName = removeBidiControlChars(maybeDisplayName); // get obs-mbox-list (may contain empty entries): scursor++; while (scursor != send) { eatCFWS(scursor, send, isCRLF); if (scursor == send) { return false; } // empty entry: if (*scursor == ',') { scursor++; continue; } // empty entry ending the list: if (*scursor == ';') { scursor++; return true; } Mailbox maybeMailbox; if (!parseMailbox(scursor, send, maybeMailbox, isCRLF)) { return false; } result.mailboxList.append(maybeMailbox); eatCFWS(scursor, send, isCRLF); // premature end: if (scursor == send) { return false; } // regular end of the list: if (*scursor == ';') { scursor++; return true; } // eat regular list entry separator: if (*scursor == ',') { scursor++; } } return false; } bool parseAddress(const char *&scursor, const char *const send, Address &result, bool isCRLF) { // address := mailbox / group eatCFWS(scursor, send, isCRLF); if (scursor == send) { return false; } // first try if it's a single mailbox: Mailbox maybeMailbox; const char *oldscursor = scursor; if (parseMailbox(scursor, send, maybeMailbox, isCRLF)) { // yes, it is: result.displayName.clear(); result.mailboxList.append(maybeMailbox); return true; } scursor = oldscursor; Address maybeAddress; // no, it's not a single mailbox. Try if it's a group: if (!parseGroup(scursor, send, maybeAddress, isCRLF)) { return false; } result = maybeAddress; return true; } bool parseAddressList(const char *&scursor, const char *const send, AddressList &result, bool isCRLF) { while (scursor != send) { eatCFWS(scursor, send, isCRLF); // end of header: this is OK. if (scursor == send) { return true; } // empty entry: ignore: if (*scursor == ',') { scursor++; continue; } // broken clients might use ';' as list delimiter, accept that as well if (*scursor == ';') { scursor++; continue; } // parse one entry Address maybeAddress; if (!parseAddress(scursor, send, maybeAddress, isCRLF)) { return false; } result.append(maybeAddress); eatCFWS(scursor, send, isCRLF); // end of header: this is OK. if (scursor == send) { return true; } // comma separating entries: eat it. if (*scursor == ',') { scursor++; } } return true; } // FIXME: Get rid of the very ugly "QStringOrQPair" thing. At this level, we are supposed to work // on byte arrays, not strings! The result parameter should be a simple // QPair, which is the attribute name and the value. bool parseParameter(const char *&scursor, const char *const send, QPair &result, bool isCRLF) { // parameter = regular-parameter / extended-parameter // regular-parameter = regular-parameter-name "=" value // extended-parameter = // value = token / quoted-string // // note that rfc2231 handling is out of the scope of this function. // Therefore we return the attribute as QString and the value as // (start,length) tupel if we see that the value is encoded // (trailing asterisk), for parseParameterList to decode... eatCFWS(scursor, send, isCRLF); if (scursor == send) { return false; } // // parse the parameter name: // // FIXME: maybeAttribute should be a QByteArray QString maybeAttribute; - if (!parseToken(scursor, send, maybeAttribute, false /* no 8bit */)) { + if (!parseToken(scursor, send, maybeAttribute, ParseTokenNoFlag)) { return false; } eatCFWS(scursor, send, isCRLF); // premature end: not OK (haven't seen '=' yet). if (scursor == send || *scursor != '=') { return false; } scursor++; // eat '=' eatCFWS(scursor, send, isCRLF); if (scursor == send) { // don't choke on attribute=, meaning the value was omitted: if (maybeAttribute.endsWith(QLatin1Char('*'))) { KMIME_WARN << "attribute ends with \"*\", but value is empty!" "Chopping away \"*\"."; maybeAttribute.truncate(maybeAttribute.length() - 1); } result = qMakePair(maybeAttribute.toLower(), QStringOrQPair()); return true; } const char *oldscursor = scursor; // // parse the parameter value: // QStringOrQPair maybeValue; if (*scursor == '"') { // value is a quoted-string: scursor++; if (maybeAttribute.endsWith(QLatin1Char('*'))) { // attributes ending with "*" designate extended-parameters, // which cannot have quoted-strings as values. So we remove the // trailing "*" to not confuse upper layers. KMIME_WARN << "attribute ends with \"*\", but value is a quoted-string!" "Chopping away \"*\"."; maybeAttribute.truncate(maybeAttribute.length() - 1); } if (!parseGenericQuotedString(scursor, send, maybeValue.qstring, isCRLF)) { scursor = oldscursor; result = qMakePair(maybeAttribute.toLower(), QStringOrQPair()); return false; // this case needs further processing by upper layers!! } } else { // value is a token: - if (!parseToken(scursor, send, maybeValue.qpair, false /* no 8bit */)) { + if (!parseToken(scursor, send, maybeValue.qpair, ParseTokenRelaxedTText)) { scursor = oldscursor; result = qMakePair(maybeAttribute.toLower(), QStringOrQPair()); return false; // this case needs further processing by upper layers!! } } result = qMakePair(maybeAttribute.toLower(), maybeValue); return true; } // FIXME: Get rid of QStringOrQPair: Use a simply QMap for "result" // instead! bool parseRawParameterList(const char *&scursor, const char *const send, QMap &result, bool isCRLF) { // we use parseParameter() consecutively to obtain a map of raw // attributes to raw values. "Raw" here means that we don't do // rfc2231 decoding and concatenation. This is left to // parseParameterList(), which will call this function. // // The main reason for making this chunk of code a separate // (private) method is that we can deal with broken parameters // _here_ and leave the rfc2231 handling solely to // parseParameterList(), which will still be enough work. while (scursor != send) { eatCFWS(scursor, send, isCRLF); // empty entry ending the list: OK. if (scursor == send) { return true; } // empty list entry: ignore. if (*scursor == ';') { scursor++; continue; } QPair maybeParameter; if (!parseParameter(scursor, send, maybeParameter, isCRLF)) { // we need to do a bit of work if the attribute is not // NULL. These are the cases marked with "needs further // processing" in parseParameter(). Specifically, parsing of the // token or the quoted-string, which should represent the value, // failed. We take the easy way out and simply search for the // next ';' to start parsing again. (Another option would be to // take the text between '=' and ';' as value) if (maybeParameter.first.isNull()) { return false; } while (scursor != send) { if (*scursor++ == ';') { goto IS_SEMICOLON; } } // scursor == send case: end of list. return true; IS_SEMICOLON: // *scursor == ';' case: parse next entry. continue; } // successful parsing brings us here: result.insert(maybeParameter.first, maybeParameter.second); eatCFWS(scursor, send, isCRLF); // end of header: ends list. if (scursor == send) { return true; } // regular separator: eat it. if (*scursor == ';') { scursor++; } } return true; } static void decodeRFC2231Value(KCodecs::Codec *&rfc2231Codec, QTextCodec *&textcodec, bool isContinuation, QString &value, QPair &source, QByteArray &charset) { // // parse the raw value into (charset,language,text): // const char *decBegin = source.first; const char *decCursor = decBegin; const char *decEnd = decCursor + source.second; if (!isContinuation) { // find the first single quote while (decCursor != decEnd) { if (*decCursor == '\'') { break; } else { decCursor++; } } if (decCursor == decEnd) { // there wasn't a single single quote at all! // take the whole value to be in latin-1: KMIME_WARN << "No charset in extended-initial-value." "Assuming \"iso-8859-1\"."; value += QString::fromLatin1(decBegin, source.second); return; } charset = QByteArray(decBegin, decCursor - decBegin); const char *oldDecCursor = ++decCursor; // find the second single quote (we ignore the language tag): while (decCursor != decEnd) { if (*decCursor == '\'') { break; } else { decCursor++; } } if (decCursor == decEnd) { KMIME_WARN << "No language in extended-initial-value." "Trying to recover."; decCursor = oldDecCursor; } else { decCursor++; } // decCursor now points to the start of the // "extended-other-values": // // get the decoders: // bool matchOK = false; textcodec = KCharsets::charsets()->codecForName(QLatin1String(charset), matchOK); if (!matchOK) { textcodec = nullptr; KMIME_WARN_UNKNOWN(Charset, charset); } } if (!rfc2231Codec) { rfc2231Codec = KCodecs::Codec::codecForName("x-kmime-rfc2231"); assert(rfc2231Codec); } if (!textcodec) { value += QString::fromLatin1(decCursor, decEnd - decCursor); return; } KCodecs::Decoder *dec = rfc2231Codec->makeDecoder(); assert(dec); // // do the decoding: // QByteArray buffer; buffer.resize(rfc2231Codec->maxDecodedSizeFor(decEnd - decCursor)); QByteArray::Iterator bit = buffer.begin(); QByteArray::ConstIterator bend = buffer.end(); if (!dec->decode(decCursor, decEnd, bit, bend)) { KMIME_WARN << rfc2231Codec->name() << "codec lies about its maxDecodedSizeFor()" << endl << "result may be truncated"; } value += textcodec->toUnicode(buffer.begin(), bit - buffer.begin()); // qDebug() << "value now: \"" << value << "\""; // cleanup: delete dec; } // known issues: // - permutes rfc2231 continuations when the total number of parts // exceeds 10 (other-sections then becomes *xy, ie. two digits) bool parseParameterListWithCharset(const char *&scursor, const char *const send, QMap &result, QByteArray &charset, bool isCRLF) { // parse the list into raw attribute-value pairs: QMap rawParameterList; if (!parseRawParameterList(scursor, send, rawParameterList, isCRLF)) { return false; } if (rawParameterList.isEmpty()) { return true; } // decode rfc 2231 continuations and alternate charset encoding: // NOTE: this code assumes that what QMapIterator delivers is sorted // by the key! KCodecs::Codec *rfc2231Codec = nullptr; QTextCodec *textcodec = nullptr; QString attribute; QString value; enum Mode { NoMode = 0x0, Continued = 0x1, Encoded = 0x2 }; enum EncodingMode { NoEncoding, RFC2047, RFC2231 }; QMap::Iterator it, end = rawParameterList.end(); for (it = rawParameterList.begin() ; it != end ; ++it) { if (attribute.isNull() || !it.key().startsWith(attribute)) { // // new attribute: // // store the last attribute/value pair in the result map now: if (!attribute.isNull()) { result.insert(attribute, value); } // and extract the information from the new raw attribute: value.clear(); attribute = it.key(); int mode = NoMode; EncodingMode encodingMode = NoEncoding; // is the value rfc2331-encoded? if (attribute.endsWith(QLatin1Char('*'))) { attribute.truncate(attribute.length() - 1); mode |= Encoded; encodingMode = RFC2231; } // is the value rfc2047-encoded? if (!(*it).qstring.isNull() && (*it).qstring.contains(QLatin1String("=?"))) { mode |= Encoded; encodingMode = RFC2047; } // is the value continued? if (attribute.endsWith(QLatin1String("*0"))) { attribute.truncate(attribute.length() - 2); mode |= Continued; } // // decode if necessary: // if (mode & Encoded) { if (encodingMode == RFC2231) { decodeRFC2231Value(rfc2231Codec, textcodec, false, /* isn't continuation */ value, (*it).qpair, charset); } else if (encodingMode == RFC2047) { value += KCodecs::decodeRFC2047String((*it).qstring.toLatin1(), &charset); } } else { // not encoded. if ((*it).qpair.first) { value += QString::fromLatin1((*it).qpair.first, (*it).qpair.second); } else { value += (*it).qstring; } } // // shortcut-processing when the value isn't encoded: // if (!(mode & Continued)) { // save result already: result.insert(attribute, value); // force begin of a new attribute: attribute.clear(); } } else { // it.key().startsWith( attribute ) // // continuation // // ignore the section and trust QMap to have sorted the keys: if (it.key().endsWith(QLatin1Char('*'))) { // encoded decodeRFC2231Value(rfc2231Codec, textcodec, true, /* is continuation */ value, (*it).qpair, charset); } else { // not encoded if ((*it).qpair.first) { value += QString::fromLatin1((*it).qpair.first, (*it).qpair.second); } else { value += (*it).qstring; } } } } // write last attr/value pair: if (!attribute.isNull()) { result.insert(attribute, value); } return true; } bool parseParameterList(const char *&scursor, const char *const send, QMap &result, bool isCRLF) { QByteArray charset; return parseParameterListWithCharset(scursor, send, result, charset, isCRLF); } static const char stdDayNames[][4] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }; static const int stdDayNamesLen = sizeof stdDayNames / sizeof *stdDayNames; static bool parseDayName(const char *&scursor, const char *const send) { // check bounds: if (send - scursor < 3) { return false; } for (int i = 0 ; i < stdDayNamesLen ; ++i) { if (qstrnicmp(scursor, stdDayNames[i], 3) == 0) { scursor += 3; // qDebug() << "found" << stdDayNames[i]; return true; } } return false; } static const char stdMonthNames[][4] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; static const int stdMonthNamesLen = sizeof stdMonthNames / sizeof *stdMonthNames; static bool parseMonthName(const char *&scursor, const char *const send, int &result) { // check bounds: if (send - scursor < 3) { return false; } for (result = 0 ; result < stdMonthNamesLen ; ++result) { if (qstrnicmp(scursor, stdMonthNames[result], 3) == 0) { scursor += 3; return true; } } // not found: return false; } static const struct { const char tzName[5]; long int secsEastOfGMT; } timeZones[] = { // rfc 822 timezones: { "GMT", 0 }, { "UT", 0 }, { "EDT", -4 * 3600 }, { "EST", -5 * 3600 }, { "MST", -5 * 3600 }, { "CST", -6 * 3600 }, { "MDT", -6 * 3600 }, { "MST", -7 * 3600 }, { "PDT", -7 * 3600 }, { "PST", -8 * 3600 }, // common, non-rfc-822 zones: { "CET", 1 * 3600 }, { "MET", 1 * 3600 }, { "UTC", 0 }, { "CEST", 2 * 3600 }, { "BST", 1 * 3600 }, // rfc 822 military timezones: { "Z", 0 }, { "A", -1 * 3600 }, { "B", -2 * 3600 }, { "C", -3 * 3600 }, { "D", -4 * 3600 }, { "E", -5 * 3600 }, { "F", -6 * 3600 }, { "G", -7 * 3600 }, { "H", -8 * 3600 }, { "I", -9 * 3600 }, // J is not used! { "K", -10 * 3600 }, { "L", -11 * 3600 }, { "M", -12 * 3600 }, { "N", 1 * 3600 }, { "O", 2 * 3600 }, { "P", 3 * 3600 }, { "Q", 4 * 3600 }, { "R", 5 * 3600 }, { "S", 6 * 3600 }, { "T", 7 * 3600 }, { "U", 8 * 3600 }, { "V", 9 * 3600 }, { "W", 10 * 3600 }, { "X", 11 * 3600 }, { "Y", 12 * 3600 }, }; static const int timeZonesLen = sizeof timeZones / sizeof *timeZones; static bool parseAlphaNumericTimeZone(const char *&scursor, const char *const send, long int &secsEastOfGMT, bool &timeZoneKnown) { // allow the timezone to be wrapped in quotes; bug 260761 if (scursor < send && *scursor == '"') { scursor++; if (scursor == send) { return false; } } QPair maybeTimeZone(nullptr, 0); - if (!parseToken(scursor, send, maybeTimeZone, false /*no 8bit*/)) { + if (!parseToken(scursor, send, maybeTimeZone, ParseTokenNoFlag)) { return false; } for (int i = 0 ; i < timeZonesLen ; ++i) { if (qstrnicmp(timeZones[i].tzName, maybeTimeZone.first, maybeTimeZone.second) == 0) { scursor += maybeTimeZone.second; secsEastOfGMT = timeZones[i].secsEastOfGMT; timeZoneKnown = true; if (scursor < send && *scursor == '"') { scursor++; } return true; } } // don't choke just because we don't happen to know the time zone KMIME_WARN_UNKNOWN(time zone, QByteArray(maybeTimeZone.first, maybeTimeZone.second)); secsEastOfGMT = 0; timeZoneKnown = false; return true; } // parse a number and return the number of digits parsed: int parseDigits(const char *&scursor, const char *const send, int &result) { result = 0; int digits = 0; for (; scursor != send && isdigit(*scursor) ; scursor++, digits++) { result *= 10; result += int(*scursor - '0'); } return digits; } static bool parseTimeOfDay(const char *&scursor, const char *const send, int &hour, int &min, int &sec, bool isCRLF = false) { // time-of-day := 2DIGIT [CFWS] ":" [CFWS] 2DIGIT [ [CFWS] ":" 2DIGIT ] // // 2DIGIT representing "hour": // if (!parseDigits(scursor, send, hour)) { return false; } eatCFWS(scursor, send, isCRLF); if (scursor == send || *scursor != ':') { return false; } scursor++; // eat ':' eatCFWS(scursor, send, isCRLF); if (scursor == send) { return false; } // // 2DIGIT representing "minute": // if (!parseDigits(scursor, send, min)) { return false; } eatCFWS(scursor, send, isCRLF); if (scursor == send) { return true; // seconds are optional } // // let's see if we have a 2DIGIT representing "second": // if (*scursor == ':') { // yepp, there are seconds: scursor++; // eat ':' eatCFWS(scursor, send, isCRLF); if (scursor == send) { return false; } if (!parseDigits(scursor, send, sec)) { return false; } } else { sec = 0; } return true; } bool parseTime(const char *&scursor, const char *send, int &hour, int &min, int &sec, long int &secsEastOfGMT, bool &timeZoneKnown, bool isCRLF) { // time := time-of-day CFWS ( zone / obs-zone ) // // obs-zone := "UT" / "GMT" / // "EST" / "EDT" / ; -0500 / -0400 // "CST" / "CDT" / ; -0600 / -0500 // "MST" / "MDT" / ; -0700 / -0600 // "PST" / "PDT" / ; -0800 / -0700 // "A"-"I" / "a"-"i" / // "K"-"Z" / "k"-"z" eatCFWS(scursor, send, isCRLF); if (scursor == send) { return false; } if (!parseTimeOfDay(scursor, send, hour, min, sec, isCRLF)) { return false; } eatCFWS(scursor, send, isCRLF); // there might be no timezone but a year following if ((scursor == send) || isdigit(*scursor)) { timeZoneKnown = false; secsEastOfGMT = 0; return true; // allow missing timezone } timeZoneKnown = true; if (*scursor == '+' || *scursor == '-') { // remember and eat '-'/'+': const char sign = *scursor++; // numerical timezone: int maybeTimeZone; const int tzDigits = parseDigits(scursor, send, maybeTimeZone); if (tzDigits != 4) { // Allow timezones in 02:00 format if (tzDigits == 2 && scursor != send && *scursor == ':') { scursor++; int maybeTimeZone2; if (parseDigits(scursor, send, maybeTimeZone2) != 2) { return false; } maybeTimeZone = maybeTimeZone * 100 + maybeTimeZone2; } else { return false; } } secsEastOfGMT = 60 * (maybeTimeZone / 100 * 60 + maybeTimeZone % 100); if (sign == '-') { secsEastOfGMT *= -1; if (secsEastOfGMT == 0) { timeZoneKnown = false; // -0000 means indetermined tz } } } else { // maybe alphanumeric timezone: if (!parseAlphaNumericTimeZone(scursor, send, secsEastOfGMT, timeZoneKnown)) { return false; } } return true; } bool parseDateTime(const char *&scursor, const char *const send, QDateTime &result, bool isCRLF) { // Parsing date-time; strict mode: // // date-time := [ [CFWS] day-name [CFWS] "," ] ; wday // (expanded) [CFWS] 1*2DIGIT CFWS month-name CFWS 2*DIGIT [CFWS] ; date // time // // day-name := "Mon" / "Tue" / "Wed" / "Thu" / "Fri" / "Sat" / "Sun" // month-name := "Jan" / "Feb" / "Mar" / "Apr" / "May" / "Jun" / // "Jul" / "Aug" / "Sep" / "Oct" / "Nov" / "Dec" result = QDateTime(); QDateTime maybeDateTime; eatCFWS(scursor, send, isCRLF); if (scursor == send) { return false; } // // let's see if there's a day-of-week: // if (parseDayName(scursor, send)) { eatCFWS(scursor, send, isCRLF); if (scursor == send) { return false; } // day-name should be followed by ',' but we treat it as optional: if (*scursor == ',') { scursor++; // eat ',' eatCFWS(scursor, send, isCRLF); } } int maybeMonth = -1; bool asctimeFormat = false; // ANSI-C asctime() format is: Wed Jun 30 21:49:08 1993 if (!isdigit(*scursor) && parseMonthName(scursor, send, maybeMonth)) { asctimeFormat = true; eatCFWS(scursor, send, isCRLF); } // // 1*2DIGIT representing "day" (of month): // int maybeDay; if (!parseDigits(scursor, send, maybeDay)) { return false; } eatCFWS(scursor, send, isCRLF); if (scursor == send) { return false; } // ignore ","; bug 54098 if (*scursor == ',') { scursor++; } // // month-name: // if (!asctimeFormat && !parseMonthName(scursor, send, maybeMonth)) { return false; } if (scursor == send) { return false; } assert(maybeMonth >= 0); assert(maybeMonth <= 11); ++maybeMonth; // 0-11 -> 1-12 eatCFWS(scursor, send, isCRLF); if (scursor == send) { return false; } // check for "year HH:MM:SS" or only "HH:MM:SS" (or "H:MM:SS") bool timeAfterYear = true; if ((send - scursor > 3) && ((scursor[1] == ':') || (scursor[2] == ':'))) { timeAfterYear = false; // first read time, then year } // // 2*DIGIT representing "year": // int maybeYear = 0; if (timeAfterYear && !parseDigits(scursor, send, maybeYear)) { return false; } eatCFWS(scursor, send, isCRLF); if (scursor == send) { return false; } // // time // int maybeHour, maybeMinute, maybeSecond; long int secsEastOfGMT; bool timeZoneKnown = true; if (!parseTime(scursor, send, maybeHour, maybeMinute, maybeSecond, secsEastOfGMT, timeZoneKnown, isCRLF)) { return false; } // in asctime() the year follows the time if (!timeAfterYear) { eatCFWS(scursor, send, isCRLF); if (scursor == send) { return false; } if (!parseDigits(scursor, send, maybeYear)) { return false; } } // RFC 2822 4.3 processing: if (maybeYear < 50) { maybeYear += 2000; } else if (maybeYear < 1000) { maybeYear += 1900; } // else keep as is if (maybeYear < 1900) { return false; // rfc2822, 3.3 } maybeDateTime.setDate(QDate(maybeYear, maybeMonth, maybeDay)); maybeDateTime.setTime(QTime(maybeHour, maybeMinute, maybeSecond)); if (!maybeDateTime.isValid()) { return false; } result = QDateTime(maybeDateTime.date(), maybeDateTime.time(), Qt::OffsetFromUTC, secsEastOfGMT); //result = QDateTime( maybeDateTime, QDateTime::Spec( QDateTime::OffsetFromUTC, secsEastOfGMT ) ); if (!result.isValid()) { return false; } return true; } Headers::Base *extractFirstHeader(QByteArray &head) { int endOfFieldBody = 0; bool folded = false; Headers::Base *header = nullptr; int startOfFieldBody = head.indexOf(':'); if (startOfFieldBody > -1) { //there is another header // Split the original data head[startOfFieldBody] = '\0'; // rawType references the actual data from 'head' QByteArray rawType = QByteArray::fromRawData(head.constData(), startOfFieldBody); startOfFieldBody++; //skip the ':' if (head[startOfFieldBody] == ' ') { // skip the space after the ':', if there startOfFieldBody++; } endOfFieldBody = findHeaderLineEnd(head, startOfFieldBody, &folded); // rawFieldBody references actual data from 'heaed' QByteArray rawFieldBody = QByteArray::fromRawData(head.constData() + startOfFieldBody, endOfFieldBody - startOfFieldBody); if (folded) { rawFieldBody = unfoldHeader(rawFieldBody); } // We might get an invalid mail without a field name, don't crash on that. if (!rawType.isEmpty()) { header = HeaderFactory::createHeader(rawType); } if (!header) { //qWarning() << "Returning Generic header of type" << rawType; header = new Headers::Generic(rawType.constData()); } header->from7BitString(rawFieldBody); head.remove(0, endOfFieldBody + 1); } else { head.clear(); } return header; } void extractHeaderAndBody(const QByteArray &content, QByteArray &header, QByteArray &body) { header.clear(); body.clear(); // empty header if (content.startsWith('\n')) { body = content.right(content.length() - 1); return; } int pos = content.indexOf("\n\n", 0); if (pos > -1) { header = content.left(++pos); //header *must* end with "\n" !! body = content.mid(pos + 1); if (body.startsWith("\n")) { body = "\n" + body; } } else { header = content; } } QVector parseHeaders(const QByteArray &head) { QVector ret; Headers::Base *h; QByteArray copy = head; while ((h = extractFirstHeader(copy))) { ret << h; } return ret; } } // namespace HeaderParsing } // namespace KMime diff --git a/src/kmime_header_parsing.h b/src/kmime_header_parsing.h index 2c52454..de63c60 100644 --- a/src/kmime_header_parsing.h +++ b/src/kmime_header_parsing.h @@ -1,279 +1,289 @@ /* -*- c++ -*- kmime_header_parsing.h KMime, the KDE Internet mail/usenet news message library. Copyright (c) 2001-2002 Marc Mutz This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __KMIME_HEADER_PARSING_H__ #define __KMIME_HEADER_PARSING_H__ #include "kmime_export.h" #include "kmime_types.h" #include #include #include +#include #include template class QMap; class QStringList; namespace KMime { namespace Headers { class Base; } namespace Types { // for when we can't make up our mind what to use... // FIXME: Remove this thing, we should _always_ know whether we are handling a // byte array or a string. // In most places where this is used, it should simply be replaced by QByteArray struct KMIME_EXPORT QStringOrQPair { QStringOrQPair() : qstring(), qpair(0, 0) {} QString qstring; QPair qpair; }; } // namespace KMime::Types namespace HeaderParsing { /** Parses the encoded word. @param scursor pointer to the first character beyond the initial '=' of the input string. @param send pointer to end of input buffer. @param result the decoded string the encoded work represented. @param language The language parameter according to RFC 2231, section 5. @param usedCS the used charset is returned here @param defaultCS the charset to use in case the detected one isn't known to us. @param forceCS force the use of the default charset. @return true if the input string was successfully decode; false otherwise. */ KMIME_EXPORT bool parseEncodedWord(const char *&scursor, const char *const send, QString &result, QByteArray &language, QByteArray &usedCS, const QByteArray &defaultCS = QByteArray(), bool forceCS = false); // // The parsing squad: // /** You may or may not have already started parsing into the atom. This function will go on where you left off. */ KMIME_EXPORT bool parseAtom(const char *&scursor, const char *const send, QString &result, bool allow8Bit = false); KMIME_EXPORT bool parseAtom(const char *&scursor, const char *const send, QPair &result, bool allow8Bit = false); /** You may or may not have already started parsing into the token. This function will go on where you left off. */ +enum ParseTokenFlag { + ParseTokenNoFlag = 0, + ParseTokenAllow8Bit = 1, + ParseTokenRelaxedTText = 2 +}; +Q_DECLARE_FLAGS(ParseTokenFlags, ParseTokenFlag) + KMIME_EXPORT bool parseToken(const char *&scursor, const char *const send, - QString &result, bool allow8Bit = false); + QString &result, ParseTokenFlags flags = ParseTokenNoFlag); KMIME_EXPORT bool parseToken(const char *&scursor, const char *const send, QPair &result, - bool allow8Bit = false); + ParseTokenFlags flags = ParseTokenNoFlag); /** @p scursor must be positioned after the opening openChar. */ KMIME_EXPORT bool parseGenericQuotedString(const char *&scursor, const char *const send, QString &result, bool isCRLF, const char openChar = '"', const char closeChar = '"'); /** @p scursor must be positioned right after the opening '(' */ KMIME_EXPORT bool parseComment(const char *&scursor, const char *const send, QString &result, bool isCRLF = false, bool reallySave = true); /** Parses a phrase. You may or may not have already started parsing into the phrase, but only if it starts with atext. If you setup this function to parse a phrase starting with an encoded-word or quoted-string, @p scursor has to point to the char introducing the encoded-word or quoted-string, resp. @param scursor pointer to the first character beyond the initial '=' of the input string. @param send pointer to end of input buffer. @param result the parsed string. @return true if the input phrase was successfully parsed; false otherwise. */ KMIME_EXPORT bool parsePhrase(const char *&scursor, const char *const send, QString &result, bool isCRLF = false); /** Parses into the initial atom. You may or may not have already started parsing into the initial atom, but not up to it's end. @param scursor pointer to the first character beyond the initial '=' of the input string. @param send pointer to end of input buffer. @param result the parsed string. @return true if the input phrase was successfully parsed; false otherwise. */ KMIME_EXPORT bool parseDotAtom(const char *&scursor, const char *const send, QString &result, bool isCRLF = false); /** Eats comment-folding-white-space, skips whitespace, folding and comments (even nested ones) and stops at the next non-CFWS character. After calling this function, you should check whether @p scursor == @p send (end of header reached). If a comment with unbalanced parantheses is encountered, @p scursor is being positioned on the opening '(' of the outmost comment. @param scursor pointer to the first character beyond the initial '=' of the input string. @param send pointer to end of input buffer. @param isCRLF true if input string is terminated with a CRLF. */ KMIME_EXPORT void eatCFWS(const char *&scursor, const char *const send, bool isCRLF); KMIME_EXPORT bool parseDomain(const char *&scursor, const char *const send, QString &result, bool isCRLF = false); KMIME_EXPORT bool parseObsRoute(const char *&scursor, const char *const send, QStringList &result, bool isCRLF = false, bool save = false); KMIME_EXPORT bool parseAddrSpec(const char *&scursor, const char *const send, Types::AddrSpec &result, bool isCRLF = false); KMIME_EXPORT bool parseAngleAddr(const char *&scursor, const char *const send, Types::AddrSpec &result, bool isCRLF = false); /** Parses a single mailbox. RFC 2822, section 3.4 defines a mailbox as follows:
mailbox := addr-spec / ([ display-name ] angle-addr)
KMime also accepts the legacy format of specifying display names:
mailbox := (addr-spec [ "(" display-name ")" ])
   / ([ display-name ] angle-addr)
   / (angle-addr "(" display-name ")")
@param scursor pointer to the first character of the input string @param send pointer to end of input buffer @param result the parsing result @param isCRLF true if input string is terminated with a CRLF. */ KMIME_EXPORT bool parseMailbox(const char *&scursor, const char *const send, Types::Mailbox &result, bool isCRLF = false); KMIME_EXPORT bool parseGroup(const char *&scursor, const char *const send, Types::Address &result, bool isCRLF = false); KMIME_EXPORT bool parseAddress(const char *&scursor, const char *const send, Types::Address &result, bool isCRLF = false); KMIME_EXPORT bool parseAddressList(const char *&scursor, const char *const send, Types::AddressList &result, bool isCRLF = false); KMIME_EXPORT bool parseParameter(const char *&scursor, const char *const send, QPair &result, bool isCRLF = false); KMIME_EXPORT bool parseParameterList(const char *&scursor, const char *const send, QMap &result, bool isCRLF = false); KMIME_EXPORT bool parseRawParameterList(const char *&scursor, const char *const send, QMap &result, bool isCRLF = false); /** * Extract the charset embedded in the parameter list if there is one. * * @since 4.5 */ KMIME_EXPORT bool parseParameterListWithCharset(const char *&scursor, const char *const send, QMap &result, QByteArray &charset, bool isCRLF = false); /** Parses an integer number. @param scursor pointer to the first character of the input string @param send pointer to end of input buffer @param result the parsing result @returns The number of parsed digits (don't confuse with @p result!) */ KMIME_EXPORT int parseDigits(const char *&scursor, const char *const send, int &result); KMIME_EXPORT bool parseTime(const char *&scursor, const char *const send, int &hour, int &min, int &sec, long int &secsEastOfGMT, bool &timeZoneKnown, bool isCRLF = false); KMIME_EXPORT bool parseDateTime(const char *&scursor, const char *const send, QDateTime &result, bool isCRLF = false); /** * Extracts and returns the first header that is contained in the given byte array. * The header will also be removed from the passed-in byte array head. * * @since 4.4 */ KMIME_EXPORT KMime::Headers::Base *extractFirstHeader(QByteArray &head); /** * Extract the header header and the body from a complete content. * Internally, it will simply look for the first newline and use that as a * separator between the header and the body. * * @param content the complete mail * @param header return value for the extracted header * @param body return value for the extracted body * @since 4.6 */ KMIME_EXPORT void extractHeaderAndBody(const QByteArray &content, QByteArray &header, QByteArray &body); } // namespace HeaderParsing } // namespace KMime +Q_DECLARE_OPERATORS_FOR_FLAGS(KMime::HeaderParsing::ParseTokenFlags) + #endif // __KMIME_HEADER_PARSING_H__ diff --git a/src/kmime_headers.cpp b/src/kmime_headers.cpp index 547d46b..aef8778 100644 --- a/src/kmime_headers.cpp +++ b/src/kmime_headers.cpp @@ -1,2142 +1,2141 @@ /* -*- c++ -*- kmime_headers.cpp KMime, the KDE Internet mail/usenet news message library. Copyright (c) 2001-2002 the KMime authors. See file AUTHORS for details Copyright (c) 2006 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ /** @file This file is part of the API for handling @ref MIME data and defines the various header classes: - header's base class defining the common interface - generic base classes for different types of fields - incompatible, Structured-based field classes - compatible, Unstructured-based field classes @brief Defines the various headers classes. @authors the KMime authors (see AUTHORS file), Volker Krause \ */ #include "kmime_headers.h" #include "kmime_headers_p.h" #include "kmime_util.h" #include "kmime_util_p.h" #include "kmime_codecs.h" #include "kmime_content.h" #include "kmime_header_parsing.h" #include "kmime_headerfactory_p.h" #include "kmime_warning.h" #include #include #include #include #include #include #include // macro to generate a default constructor implementation #define kmime_mk_trivial_ctor( subclass, baseclass ) \ subclass::subclass() : baseclass() \ { \ } \ \ subclass::~subclass() {} // end kmime_mk_trivial_ctor #define kmime_mk_trivial_ctor_with_dptr( subclass, baseclass ) \ subclass::subclass() : baseclass( new subclass##Private ) \ { \ } \ \ subclass::~subclass() { \ Q_D(subclass); \ delete d; \ d_ptr = 0; \ } // end kmime_mk_trivial_ctor_with_dptr #define kmime_mk_trivial_ctor_with_name( subclass, baseclass, name ) \ kmime_mk_trivial_ctor( subclass, baseclass ) \ \ const char *subclass::type() const \ { \ return staticType(); \ } \ const char *subclass::staticType() { return #name; } #define kmime_mk_trivial_ctor_with_name_and_dptr( subclass, baseclass, name ) \ kmime_mk_trivial_ctor_with_dptr( subclass, baseclass ) \ const char *subclass::type() const { return staticType(); } \ const char *subclass::staticType() { return #name; } #define kmime_mk_dptr_ctor( subclass, baseclass ) \ subclass::subclass( subclass##Private *d ) : baseclass( d ) {} using namespace KMime; using namespace KMime::Headers; using namespace KMime::Types; using namespace KMime::HeaderParsing; namespace KMime { namespace Headers { //--------------------------------------- Base::Base() : d_ptr(new BasePrivate) { } Base::Base(BasePrivate *dd) : d_ptr(dd) { } Base::~Base() { delete d_ptr; d_ptr = nullptr; } QByteArray Base::rfc2047Charset() const { if (d_ptr->encCS.isEmpty()) { return Content::defaultCharset(); } else { return d_ptr->encCS; } } void Base::setRFC2047Charset(const QByteArray &cs) { d_ptr->encCS = cachedCharset(cs); } const char *Base::type() const { return ""; } bool Base::is(const char *t) const { return qstricmp(t, type()) == 0; } bool Base::isMimeHeader() const { return qstrnicmp(type(), "Content-", 8) == 0; } QByteArray Base::typeIntro() const { return QByteArray(type()) + ": "; } //-------------------------------------- namespace Generics { //------------------------------ //@cond PRIVATE kmime_mk_dptr_ctor(Unstructured, Base) //@endcond Unstructured::Unstructured() : Base(new UnstructuredPrivate) { } Unstructured::~Unstructured() { Q_D(Unstructured); delete d; d_ptr = nullptr; } void Unstructured::from7BitString(const QByteArray &s) { Q_D(Unstructured); d->decoded = KCodecs::decodeRFC2047String(s, &d->encCS, Content::defaultCharset()); } QByteArray Unstructured::as7BitString(bool withHeaderType) const { const Q_D(Unstructured); QByteArray result; if (withHeaderType) { result = typeIntro(); } result += encodeRFC2047String(d->decoded, d->encCS) ; return result; } void Unstructured::fromUnicodeString(const QString &s, const QByteArray &b) { Q_D(Unstructured); d->decoded = s; d->encCS = cachedCharset(b); } QString Unstructured::asUnicodeString() const { return d_func()->decoded; } void Unstructured::clear() { Q_D(Unstructured); d->decoded.truncate(0); } bool Unstructured::isEmpty() const { return d_func()->decoded.isEmpty(); } //------------------------------ //------------------------------ Structured::Structured() : Base(new StructuredPrivate) { } kmime_mk_dptr_ctor(Structured, Base) Structured::~Structured() { Q_D(Structured); delete d; d_ptr = nullptr; } void Structured::from7BitString(const QByteArray &s) { Q_D(Structured); if (d->encCS.isEmpty()) { d->encCS = Content::defaultCharset(); } const char *cursor = s.constData(); parse(cursor, cursor + s.length()); } QString Structured::asUnicodeString() const { return QString::fromLatin1(as7BitString(false)); } void Structured::fromUnicodeString(const QString &s, const QByteArray &b) { Q_D(Structured); d->encCS = cachedCharset(b); from7BitString(s.toLatin1()); } //------------------------------ //-----
------------------------- Address::Address() : Structured(new AddressPrivate) { } kmime_mk_dptr_ctor(Address, Structured) Address:: ~Address() { } // helper method used in AddressList and MailboxList static bool stringToMailbox(const QByteArray &address, const QString &displayName, Types::Mailbox &mbox) { Types::AddrSpec addrSpec; mbox.setName(displayName); const char *cursor = address.constData(); if (!parseAngleAddr(cursor, cursor + address.length(), addrSpec)) { if (!parseAddrSpec(cursor, cursor + address.length(), addrSpec)) { qWarning() << "stringToMailbox: Invalid address"; return false; } } mbox.setAddress(addrSpec); return true; } //-----
------------------------- //------------------------------ kmime_mk_trivial_ctor_with_dptr(MailboxList, Address) kmime_mk_dptr_ctor(MailboxList, Address) QByteArray MailboxList::as7BitString(bool withHeaderType) const { const Q_D(MailboxList); if (isEmpty()) { return QByteArray(); } QByteArray rv; if (withHeaderType) { rv = typeIntro(); } foreach (const Types::Mailbox &mbox, d->mailboxList) { rv += mbox.as7BitString(d->encCS); rv += ", "; } rv.resize(rv.length() - 2); return rv; } void MailboxList::fromUnicodeString(const QString &s, const QByteArray &b) { Q_D(MailboxList); d->encCS = cachedCharset(b); from7BitString(encodeRFC2047Sentence(s, b)); } QString MailboxList::asUnicodeString() const { Q_D(const MailboxList); return Mailbox::listToUnicodeString(d->mailboxList); } void MailboxList::clear() { Q_D(MailboxList); d->mailboxList.clear(); } bool MailboxList::isEmpty() const { return d_func()->mailboxList.isEmpty(); } void MailboxList::addAddress(const Types::Mailbox &mbox) { Q_D(MailboxList); d->mailboxList.append(mbox); } void MailboxList::addAddress(const QByteArray &address, const QString &displayName) { Q_D(MailboxList); Types::Mailbox mbox; if (stringToMailbox(address, displayName, mbox)) { d->mailboxList.append(mbox); } } QVector MailboxList::addresses() const { QVector rv; rv.reserve(d_func()->mailboxList.count()); foreach (const Types::Mailbox &mbox, d_func()->mailboxList) { rv.append(mbox.address()); } return rv; } QStringList MailboxList::displayNames() const { Q_D(const MailboxList); QStringList rv; rv.reserve(d->mailboxList.count()); foreach (const Types::Mailbox &mbox, d->mailboxList) { if (mbox.hasName()) rv.append(mbox.name()); else rv.append(QString::fromLatin1(mbox.address())); } return rv; } QString MailboxList::displayString() const { Q_D(const MailboxList); if (d->mailboxList.size() == 1) { // fast-path to avoid temporary QStringList in the common case of just one From address const auto& mbox = d->mailboxList.at(0); if (mbox.hasName()) return mbox.name(); else return QString::fromLatin1(mbox.address()); } return displayNames().join(QStringLiteral(", ")); } Types::Mailbox::List MailboxList::mailboxes() const { return d_func()->mailboxList; } bool MailboxList::parse(const char *&scursor, const char *const send, bool isCRLF) { Q_D(MailboxList); // examples: // from := "From:" mailbox-list CRLF // sender := "Sender:" mailbox CRLF // parse an address-list: QVector maybeAddressList; if (!parseAddressList(scursor, send, maybeAddressList, isCRLF)) { return false; } d->mailboxList.clear(); d->mailboxList.reserve(maybeAddressList.count()); // extract the mailboxes and complain if there are groups: foreach (const auto &it, maybeAddressList) { if (!(it).displayName.isEmpty()) { KMIME_WARN << "mailbox groups in header disallowing them! Name: \"" << (it).displayName << "\"" << endl; } d->mailboxList += (it).mailboxList; } return true; } //------------------------------ //------------------------------ //@cond PRIVATE kmime_mk_trivial_ctor_with_dptr(SingleMailbox, MailboxList) //@endcond bool SingleMailbox::parse(const char *&scursor, const char *const send, bool isCRLF) { Q_D(MailboxList); if (!MailboxList::parse(scursor, send, isCRLF)) { return false; } if (d->mailboxList.count() > 1) { KMIME_WARN << "multiple mailboxes in header allowing only a single one!" << endl; } return true; } //------------------------------ //------------------------------ //@cond PRIVATE kmime_mk_trivial_ctor_with_dptr(AddressList, Address) kmime_mk_dptr_ctor(AddressList, Address) //@endcond QByteArray AddressList::as7BitString(bool withHeaderType) const { const Q_D(AddressList); if (d->addressList.isEmpty()) { return QByteArray(); } QByteArray rv; if (withHeaderType) { rv = typeIntro(); } foreach (const Types::Address &addr, d->addressList) { foreach (const Types::Mailbox &mbox, addr.mailboxList) { rv += mbox.as7BitString(d->encCS); rv += ", "; } } rv.resize(rv.length() - 2); return rv; } void AddressList::fromUnicodeString(const QString &s, const QByteArray &b) { Q_D(AddressList); d->encCS = cachedCharset(b); from7BitString(encodeRFC2047Sentence(s, b)); } QString AddressList::asUnicodeString() const { Q_D(const AddressList); QStringList rv; foreach (const Types::Address &addr, d->addressList) { rv.reserve(rv.size() + addr.mailboxList.size()); foreach (const Types::Mailbox &mbox, addr.mailboxList) { rv.append(mbox.prettyAddress()); } } return rv.join(QStringLiteral(", ")); } void AddressList::clear() { Q_D(AddressList); d->addressList.clear(); } bool AddressList::isEmpty() const { return d_func()->addressList.isEmpty(); } void AddressList::addAddress(const Types::Mailbox &mbox) { Q_D(AddressList); Types::Address addr; addr.mailboxList.append(mbox); d->addressList.append(addr); } void AddressList::addAddress(const QByteArray &address, const QString &displayName) { Q_D(AddressList); Types::Address addr; Types::Mailbox mbox; if (stringToMailbox(address, displayName, mbox)) { addr.mailboxList.append(mbox); d->addressList.append(addr); } } QVector AddressList::addresses() const { QVector rv; foreach (const Types::Address &addr, d_func()->addressList) { foreach (const Types::Mailbox &mbox, addr.mailboxList) { rv.append(mbox.address()); } } return rv; } QStringList AddressList::displayNames() const { Q_D(const AddressList); QStringList rv; foreach (const Types::Address &addr, d->addressList) { foreach (const Types::Mailbox &mbox, addr.mailboxList) { if (mbox.hasName()) rv.append(mbox.name()); else rv.append(QString::fromLatin1(mbox.address())); } } return rv; } QString AddressList::displayString() const { // optimize for single entry and avoid creation of the QStringList in that case? return displayNames().join(QStringLiteral(", ")); } Types::Mailbox::List AddressList::mailboxes() const { Types::Mailbox::List rv; foreach (const Types::Address &addr, d_func()->addressList) { foreach (const Types::Mailbox &mbox, addr.mailboxList) { rv.append(mbox); } } return rv; } bool AddressList::parse(const char *&scursor, const char *const send, bool isCRLF) { Q_D(AddressList); QVector maybeAddressList; if (!parseAddressList(scursor, send, maybeAddressList, isCRLF)) { return false; } d->addressList = maybeAddressList; return true; } //------------------------------ //------------------------------ //@cond PRIVATE kmime_mk_trivial_ctor_with_dptr(Token, Structured) kmime_mk_dptr_ctor(Token, Structured) //@endcond QByteArray Token::as7BitString(bool withHeaderType) const { if (isEmpty()) { return QByteArray(); } if (withHeaderType) { return typeIntro() + d_func()->token; } return d_func()->token; } void Token::clear() { Q_D(Token); d->token.clear(); } bool Token::isEmpty() const { return d_func()->token.isEmpty(); } QByteArray Token::token() const { return d_func()->token; } void Token::setToken(const QByteArray &t) { Q_D(Token); d->token = t; } bool Token::parse(const char *&scursor, const char *const send, bool isCRLF) { Q_D(Token); clear(); eatCFWS(scursor, send, isCRLF); // must not be empty: if (scursor == send) { return false; } QPair maybeToken; - if (!parseToken(scursor, send, maybeToken, false /* no 8bit chars */)) { + if (!parseToken(scursor, send, maybeToken, ParseTokenNoFlag)) { return false; } d->token = QByteArray(maybeToken.first, maybeToken.second); // complain if trailing garbage is found: eatCFWS(scursor, send, isCRLF); if (scursor != send) { KMIME_WARN << "trailing garbage after token in header allowing " "only a single token!" << endl; } return true; } //------------------------------ //------------------------------ //@cond PRIVATE kmime_mk_trivial_ctor_with_dptr(PhraseList, Structured) //@endcond QByteArray PhraseList::as7BitString(bool withHeaderType) const { const Q_D(PhraseList); if (isEmpty()) { return QByteArray(); } QByteArray rv; if (withHeaderType) { rv = typeIntro(); } for (int i = 0; i < d->phraseList.count(); ++i) { // FIXME: only encode when needed, quote when needed, etc. rv += encodeRFC2047String(d->phraseList[i], d->encCS, false, false); if (i != d->phraseList.count() - 1) { rv += ", "; } } return rv; } QString PhraseList::asUnicodeString() const { return d_func()->phraseList.join(QStringLiteral(", ")); } void PhraseList::clear() { Q_D(PhraseList); d->phraseList.clear(); } bool PhraseList::isEmpty() const { return d_func()->phraseList.isEmpty(); } QStringList PhraseList::phrases() const { return d_func()->phraseList; } bool PhraseList::parse(const char *&scursor, const char *const send, bool isCRLF) { Q_D(PhraseList); d->phraseList.clear(); while (scursor != send) { eatCFWS(scursor, send, isCRLF); // empty entry ending the list: OK. if (scursor == send) { return true; } // empty entry: ignore. if (*scursor == ',') { scursor++; continue; } QString maybePhrase; if (!parsePhrase(scursor, send, maybePhrase, isCRLF)) { return false; } d->phraseList.append(maybePhrase); eatCFWS(scursor, send, isCRLF); // non-empty entry ending the list: OK. if (scursor == send) { return true; } // comma separating the phrases: eat. if (*scursor == ',') { scursor++; } } return true; } //------------------------------ //------------------------------ //@cond PRIVATE kmime_mk_trivial_ctor_with_dptr(DotAtom, Structured) //@endcond QByteArray DotAtom::as7BitString(bool withHeaderType) const { if (isEmpty()) { return QByteArray(); } QByteArray rv; if (withHeaderType) { rv += typeIntro(); } rv += d_func()->dotAtom.toLatin1(); // FIXME: encoding? return rv; } QString DotAtom::asUnicodeString() const { return d_func()->dotAtom; } void DotAtom::clear() { Q_D(DotAtom); d->dotAtom.clear(); } bool DotAtom::isEmpty() const { return d_func()->dotAtom.isEmpty(); } bool DotAtom::parse(const char *&scursor, const char *const send, bool isCRLF) { Q_D(DotAtom); QString maybeDotAtom; if (!parseDotAtom(scursor, send, maybeDotAtom, isCRLF)) { return false; } d->dotAtom = maybeDotAtom; eatCFWS(scursor, send, isCRLF); if (scursor != send) { KMIME_WARN << "trailing garbage after dot-atom in header allowing " "only a single dot-atom!" << endl; } return true; } //------------------------------ //------------------------------ //@cond PRIVATE kmime_mk_trivial_ctor_with_dptr(Parametrized, Structured) kmime_mk_dptr_ctor(Parametrized, Structured) //@endcond QByteArray Parametrized::as7BitString(bool withHeaderType) const { const Q_D(Parametrized); if (isEmpty()) { return QByteArray(); } QByteArray rv; if (withHeaderType) { rv += typeIntro(); } bool first = true; for (QMap::ConstIterator it = d->parameterHash.constBegin(); it != d->parameterHash.constEnd(); ++it) { if (!first) { rv += "; "; } else { first = false; } if (isUsAscii(it.value())) { rv += it.key().toLatin1() + '='; QByteArray tmp = it.value().toLatin1(); addQuotes(tmp, true); // force quoting, eg. for whitespaces in parameter value rv += tmp; } else { if (useOutlookAttachmentEncoding()) { rv += it.key().toLatin1() + '='; qDebug() << "doing:" << it.value() << QLatin1String(d->encCS); rv += "\"" + encodeRFC2047String(it.value(), d->encCS) + "\""; } else { rv += it.key().toLatin1() + "*="; rv += encodeRFC2231String(it.value(), d->encCS); } } } return rv; } QString Parametrized::parameter(const QString &key) const { return d_func()->parameterHash.value(key.toLower()); } bool Parametrized::hasParameter(const QString &key) const { return d_func()->parameterHash.contains(key.toLower()); } void Parametrized::setParameter(const QString &key, const QString &value) { Q_D(Parametrized); d->parameterHash.insert(key.toLower(), value); } bool Parametrized::isEmpty() const { return d_func()->parameterHash.isEmpty(); } void Parametrized::clear() { Q_D(Parametrized); d->parameterHash.clear(); } bool Parametrized::parse(const char *&scursor, const char *const send, bool isCRLF) { Q_D(Parametrized); d->parameterHash.clear(); QByteArray charset; if (!parseParameterListWithCharset(scursor, send, d->parameterHash, charset, isCRLF)) { return false; } d->encCS = charset; return true; } //------------------------------ //------------------------------ //@cond PRIVATE kmime_mk_trivial_ctor_with_dptr(Ident, Address) kmime_mk_dptr_ctor(Ident, Address) //@endcond QByteArray Ident::as7BitString(bool withHeaderType) const { const Q_D(Ident); if (d->msgIdList.isEmpty()) { return QByteArray(); } QByteArray rv; if (withHeaderType) { rv = typeIntro(); } foreach (const Types::AddrSpec &addr, d->msgIdList) { if (!addr.isEmpty()) { const QString asString = addr.asString(); rv += '<'; if (!asString.isEmpty()) { rv += asString.toLatin1(); // FIXME: change parsing to use QByteArrays } rv += "> "; } } if (!rv.isEmpty()) { rv.resize(rv.length() - 1); } return rv; } void Ident::clear() { Q_D(Ident); d->msgIdList.clear(); d->cachedIdentifier.clear(); } bool Ident::isEmpty() const { return d_func()->msgIdList.isEmpty(); } bool Ident::parse(const char *&scursor, const char *const send, bool isCRLF) { Q_D(Ident); // msg-id := "<" id-left "@" id-right ">" // id-left := dot-atom-text / no-fold-quote / local-part // id-right := dot-atom-text / no-fold-literal / domain // // equivalent to: // msg-id := angle-addr d->msgIdList.clear(); d->cachedIdentifier.clear(); while (scursor != send) { eatCFWS(scursor, send, isCRLF); // empty entry ending the list: OK. if (scursor == send) { return true; } // empty entry: ignore. if (*scursor == ',') { scursor++; continue; } AddrSpec maybeMsgId; if (!parseAngleAddr(scursor, send, maybeMsgId, isCRLF)) { return false; } d->msgIdList.append(maybeMsgId); eatCFWS(scursor, send, isCRLF); // header end ending the list: OK. if (scursor == send) { return true; } // regular item separator: eat it. if (*scursor == ',') { scursor++; } } return true; } QVector Ident::identifiers() const { QVector rv; foreach (const Types::AddrSpec &addr, d_func()->msgIdList) { if (!addr.isEmpty()) { const QString asString = addr.asString(); if (!asString.isEmpty()) { rv.append(asString.toLatin1()); // FIXME: change parsing to use QByteArrays } } } return rv; } void Ident::appendIdentifier(const QByteArray &id) { Q_D(Ident); QByteArray tmp = id; if (!tmp.startsWith('<')) { tmp.prepend('<'); } if (!tmp.endsWith('>')) { tmp.append('>'); } AddrSpec msgId; const char *cursor = tmp.constData(); if (parseAngleAddr(cursor, cursor + tmp.length(), msgId)) { d->msgIdList.append(msgId); } else { qWarning() << "Unable to parse address spec!"; } } //------------------------------ //------------------------------ //@cond PRIVATE kmime_mk_trivial_ctor_with_dptr(SingleIdent, Ident) kmime_mk_dptr_ctor(SingleIdent, Ident) //@endcond QByteArray SingleIdent::identifier() const { if (d_func()->msgIdList.isEmpty()) { return QByteArray(); } if (d_func()->cachedIdentifier.isEmpty()) { const Types::AddrSpec &addr = d_func()->msgIdList.first(); if (!addr.isEmpty()) { const QString asString = addr.asString(); if (!asString.isEmpty()) { d_func()->cachedIdentifier = asString.toLatin1();// FIXME: change parsing to use QByteArrays } } } return d_func()->cachedIdentifier; } void SingleIdent::setIdentifier(const QByteArray &id) { Q_D(SingleIdent); d->msgIdList.clear(); d->cachedIdentifier.clear(); appendIdentifier(id); } bool SingleIdent::parse(const char *&scursor, const char *const send, bool isCRLF) { Q_D(SingleIdent); if (!Ident::parse(scursor, send, isCRLF)) { return false; } if (d->msgIdList.count() > 1) { KMIME_WARN << "more than one msg-id in header " << "allowing only a single one!" << endl; } return true; } //------------------------------ } // namespace Generics //------------------------------ //@cond PRIVATE kmime_mk_trivial_ctor_with_name_and_dptr(ReturnPath, Generics::Address, Return-Path) //@endcond QByteArray ReturnPath::as7BitString(bool withHeaderType) const { if (isEmpty()) { return QByteArray(); } QByteArray rv; if (withHeaderType) { rv += typeIntro(); } rv += '<' + d_func()->mailbox.as7BitString(d_func()->encCS) + '>'; return rv; } void ReturnPath::clear() { Q_D(ReturnPath); d->mailbox.setAddress(Types::AddrSpec()); d->mailbox.setName(QString()); } bool ReturnPath::isEmpty() const { const Q_D(ReturnPath); return !d->mailbox.hasAddress() && !d->mailbox.hasName(); } bool ReturnPath::parse(const char *&scursor, const char *const send, bool isCRLF) { Q_D(ReturnPath); eatCFWS(scursor, send, isCRLF); if (scursor == send) { return false; } const char *oldscursor = scursor; Mailbox maybeMailbox; if (!parseMailbox(scursor, send, maybeMailbox, isCRLF)) { // mailbox parsing failed, but check for empty brackets: scursor = oldscursor; if (*scursor != '<') { return false; } scursor++; eatCFWS(scursor, send, isCRLF); if (scursor == send || *scursor != '>') { return false; } scursor++; // prepare a Null mailbox: AddrSpec emptyAddrSpec; maybeMailbox.setName(QString()); maybeMailbox.setAddress(emptyAddrSpec); } else { // check that there was no display-name: if (maybeMailbox.hasName()) { KMIME_WARN << "display-name \"" << maybeMailbox.name() << "\" in Return-Path!" << endl; } } d->mailbox = maybeMailbox; // see if that was all: eatCFWS(scursor, send, isCRLF); // and warn if it wasn't: if (scursor != send) { KMIME_WARN << "trailing garbage after angle-addr in Return-Path!" << endl; } return true; } //------------------------------ //------------------------------------ // NOTE: Do *not* register Generic with HeaderFactory, since its type() is changeable. Generic::Generic() : Generics::Unstructured(new GenericPrivate) { } Generic::Generic(const char *t) : Generics::Unstructured(new GenericPrivate) { setType(t); } Generic::~Generic() { Q_D(Generic); delete d; d_ptr = nullptr; } void Generic::clear() { Q_D(Generic); delete[] d->type; d->type = nullptr; Unstructured::clear(); } bool Generic::isEmpty() const { return d_func()->type == nullptr || Unstructured::isEmpty(); } const char *Generic::type() const { return d_func()->type; } void Generic::setType(const char *type) { Q_D(Generic); if (d->type) { delete[] d->type; } if (type) { d->type = new char[strlen(type) + 1]; strcpy(d->type, type); } else { d->type = nullptr; } } //------------------------------------ //---------------------------------- //@cond PRIVATE kmime_mk_trivial_ctor_with_name(MessageID, Generics::SingleIdent, Message-ID) //@endcond void MessageID::generate(const QByteArray &fqdn) { setIdentifier('<' + uniqueString() + '@' + fqdn + '>'); } //--------------------------------- //------------------------------------ //@cond PRIVATE kmime_mk_trivial_ctor_with_name_and_dptr(Control, Generics::Structured, Control) //@endcond QByteArray Control::as7BitString(bool withHeaderType) const { const Q_D(Control); if (isEmpty()) { return QByteArray(); } QByteArray rv; if (withHeaderType) { rv += typeIntro(); } rv += d->name; if (!d->parameter.isEmpty()) { rv += ' ' + d->parameter; } return rv; } void Control::clear() { Q_D(Control); d->name.clear(); d->parameter.clear(); } bool Control::isEmpty() const { return d_func()->name.isEmpty(); } QByteArray Control::controlType() const { return d_func()->name; } QByteArray Control::parameter() const { return d_func()->parameter; } bool Control::isCancel() const { return d_func()->name.toLower() == "cancel"; } void Control::setCancel(const QByteArray &msgid) { Q_D(Control); d->name = "cancel"; d->parameter = msgid; } bool Control::parse(const char *&scursor, const char *const send, bool isCRLF) { Q_D(Control); clear(); eatCFWS(scursor, send, isCRLF); if (scursor == send) { return false; } const char *start = scursor; while (scursor != send && !isspace(*scursor)) { ++scursor; } d->name = QByteArray(start, scursor - start); eatCFWS(scursor, send, isCRLF); d->parameter = QByteArray(scursor, send - scursor); return true; } //----------------------------------- //------------------------------- //@cond PRIVATE kmime_mk_trivial_ctor_with_name_and_dptr(MailCopiesTo, Generics::AddressList, Mail-Copies-To) //@endcond QByteArray MailCopiesTo::as7BitString(bool withHeaderType) const { QByteArray rv; if (withHeaderType) { rv += typeIntro(); } if (!AddressList::isEmpty()) { rv += AddressList::as7BitString(false); } else { if (d_func()->alwaysCopy) { rv += "poster"; } else if (d_func()->neverCopy) { rv += "nobody"; } } return rv; } QString MailCopiesTo::asUnicodeString() const { if (!AddressList::isEmpty()) { return AddressList::asUnicodeString(); } if (d_func()->alwaysCopy) { return QStringLiteral("poster"); } if (d_func()->neverCopy) { return QStringLiteral("nobody"); } return QString(); } void MailCopiesTo::clear() { Q_D(MailCopiesTo); AddressList::clear(); d->alwaysCopy = false; d->neverCopy = false; } bool MailCopiesTo::isEmpty() const { return AddressList::isEmpty() && !(d_func()->alwaysCopy || d_func()->neverCopy); } bool MailCopiesTo::alwaysCopy() const { return !AddressList::isEmpty() || d_func()->alwaysCopy; } void MailCopiesTo::setAlwaysCopy() { Q_D(MailCopiesTo); clear(); d->alwaysCopy = true; } bool MailCopiesTo::neverCopy() const { return d_func()->neverCopy; } void MailCopiesTo::setNeverCopy() { Q_D(MailCopiesTo); clear(); d->neverCopy = true; } bool MailCopiesTo::parse(const char *&scursor, const char *const send, bool isCRLF) { Q_D(MailCopiesTo); clear(); if (send - scursor == 5) { if (qstrnicmp("never", scursor, 5) == 0) { d->neverCopy = true; return true; } } if (send - scursor == 6) { if (qstrnicmp("always", scursor, 6) == 0 || qstrnicmp("poster", scursor, 6) == 0) { d->alwaysCopy = true; return true; } if (qstrnicmp("nobody", scursor, 6) == 0) { d->neverCopy = true; return true; } } return AddressList::parse(scursor, send, isCRLF); } //------------------------------ //--------------------------------------- //@cond PRIVATE kmime_mk_trivial_ctor_with_name_and_dptr(Date, Generics::Structured, Date) //@endcond QByteArray Date::as7BitString(bool withHeaderType) const { if (isEmpty()) { return QByteArray(); } QByteArray rv; if (withHeaderType) { rv += typeIntro(); } //QT5 fix port to QDateTime Qt::RFC2822Date is not enough we need to fix it. We need to use QLocale("C") + add "ddd, "; //rv += d_func()->dateTime.toString( Qt::RFC2822Date ).toLatin1(); rv += QLocale::c().toString(d_func()->dateTime, QStringLiteral("ddd, ")).toLatin1(); rv += d_func()->dateTime.toString(Qt::RFC2822Date).toLatin1(); return rv; } void Date::clear() { Q_D(Date); d->dateTime = QDateTime(); } bool Date::isEmpty() const { return d_func()->dateTime.isNull() || !d_func()->dateTime.isValid(); } QDateTime Date::dateTime() const { return d_func()->dateTime; } void Date::setDateTime(const QDateTime & dt) { Q_D(Date); d->dateTime = dt; } int Date::ageInDays() const { QDate today = QDate::currentDate(); return dateTime().date().daysTo(today); } bool Date::parse(const char *&scursor, const char *const send, bool isCRLF) { Q_D(Date); return parseDateTime(scursor, send, d->dateTime, isCRLF); } //-------------------------------------- //--------------------------------- //@cond PRIVATE kmime_mk_trivial_ctor_with_name_and_dptr(Newsgroups, Generics::Structured, Newsgroups) kmime_mk_trivial_ctor_with_name(FollowUpTo, Newsgroups, Followup-To) //@endcond QByteArray Newsgroups::as7BitString(bool withHeaderType) const { const Q_D(Newsgroups); if (isEmpty()) { return QByteArray(); } QByteArray rv; if (withHeaderType) { rv += typeIntro(); } for (int i = 0; i < d->groups.count(); ++i) { rv += d->groups[ i ]; if (i != d->groups.count() - 1) { rv += ','; } } return rv; } void Newsgroups::fromUnicodeString(const QString & s, const QByteArray & b) { Q_UNUSED(b); Q_D(Newsgroups); from7BitString(s.toUtf8()); d->encCS = cachedCharset("UTF-8"); } QString Newsgroups::asUnicodeString() const { return QString::fromUtf8(as7BitString(false)); } void Newsgroups::clear() { Q_D(Newsgroups); d->groups.clear(); } bool Newsgroups::isEmpty() const { return d_func()->groups.isEmpty(); } QVector Newsgroups::groups() const { return d_func()->groups; } void Newsgroups::setGroups(const QVector &groups) { Q_D(Newsgroups); d->groups = groups; } bool Newsgroups::isCrossposted() const { return d_func()->groups.count() >= 2; } bool Newsgroups::parse(const char *&scursor, const char *const send, bool isCRLF) { Q_D(Newsgroups); clear(); forever { eatCFWS(scursor, send, isCRLF); if (scursor != send && *scursor == ',') { ++scursor; } eatCFWS(scursor, send, isCRLF); if (scursor == send) { return true; } const char *start = scursor; while (scursor != send && !isspace(*scursor) && *scursor != ',') { ++scursor; } QByteArray group(start, scursor - start); d->groups.append(group); } return true; } //-------------------------------- //-------------------------------------- //@cond PRIVATE kmime_mk_trivial_ctor_with_name_and_dptr(Lines, Generics::Structured, Lines) //@endcond QByteArray Lines::as7BitString(bool withHeaderType) const { if (isEmpty()) { return QByteArray(); } QByteArray num; num.setNum(d_func()->lines); if (withHeaderType) { return typeIntro() + num; } return num; } QString Lines::asUnicodeString() const { if (isEmpty()) { return QString(); } return QString::number(d_func()->lines); } void Lines::clear() { Q_D(Lines); d->lines = -1; } bool Lines::isEmpty() const { return d_func()->lines == -1; } int Lines::numberOfLines() const { return d_func()->lines; } void Lines::setNumberOfLines(int lines) { Q_D(Lines); d->lines = lines; } bool Lines::parse(const char *&scursor, const char *const send, bool isCRLF) { Q_D(Lines); eatCFWS(scursor, send, isCRLF); if (parseDigits(scursor, send, d->lines) == 0) { clear(); return false; } return true; } //------------------------------------- //------------------------------- //@cond PRIVATE kmime_mk_trivial_ctor_with_name_and_dptr(ContentType, Generics::Parametrized, Content-Type) //@endcond bool ContentType::isEmpty() const { return d_func()->mimeType.isEmpty(); } void ContentType::clear() { Q_D(ContentType); d->category = CCsingle; d->mimeType.clear(); Parametrized::clear(); } QByteArray ContentType::as7BitString(bool withHeaderType) const { if (isEmpty()) { return QByteArray(); } QByteArray rv; if (withHeaderType) { rv += typeIntro(); } rv += mimeType(); if (!Parametrized::isEmpty()) { rv += "; " + Parametrized::as7BitString(false); } return rv; } QByteArray ContentType::mimeType() const { Q_D(const ContentType); return d->mimeType; } QByteArray ContentType::mediaType() const { Q_D(const ContentType); const int pos = d->mimeType.indexOf('/'); if (pos < 0) { return d->mimeType; } else { return d->mimeType.left(pos); } } QByteArray ContentType::subType() const { Q_D(const ContentType); const int pos = d->mimeType.indexOf('/'); if (pos < 0) { return QByteArray(); } else { return d->mimeType.mid(pos + 1); } } void ContentType::setMimeType(const QByteArray & mimeType) { Q_D(ContentType); d->mimeType = mimeType; Parametrized::clear(); if (isMultipart()) { d->category = CCcontainer; } else { d->category = CCsingle; } } bool ContentType::isMediatype(const char *mediatype) const { Q_D(const ContentType); const int len = strlen(mediatype); return qstrnicmp(d->mimeType.constData(), mediatype, len) == 0 && (d->mimeType.at(len) == '/' || d->mimeType.size() == len); } bool ContentType::isSubtype(const char *subtype) const { Q_D(const ContentType); const int pos = d->mimeType.indexOf('/'); if (pos < 0) { return false; } const int len = strlen(subtype); return qstrnicmp(d->mimeType.constData() + pos + 1, subtype, len) == 0 && d->mimeType.size() == pos + len + 1; } bool ContentType::isMimeType(const char* mimeType) const { Q_D(const ContentType); return qstricmp(d->mimeType.constData(), mimeType) == 0; } bool ContentType::isText() const { return (isMediatype("text") || isEmpty()); } bool ContentType::isPlainText() const { return (qstricmp(d_func()->mimeType.constData(), "text/plain") == 0 || isEmpty()); } bool ContentType::isHTMLText() const { return qstricmp(d_func()->mimeType.constData(), "text/html") == 0; } bool ContentType::isImage() const { return isMediatype("image"); } bool ContentType::isMultipart() const { return isMediatype("multipart"); } bool ContentType::isPartial() const { return qstricmp(d_func()->mimeType.constData(), "message/partial") == 0; } QByteArray ContentType::charset() const { QByteArray ret = parameter(QStringLiteral("charset")).toLatin1(); if (ret.isEmpty()) { //return the default-charset if necessary ret = Content::defaultCharset(); } return ret; } void ContentType::setCharset(const QByteArray & s) { setParameter(QStringLiteral("charset"), QString::fromLatin1(s)); } QByteArray ContentType::boundary() const { return parameter(QStringLiteral("boundary")).toLatin1(); } void ContentType::setBoundary(const QByteArray & s) { setParameter(QStringLiteral("boundary"), QString::fromLatin1(s)); } QString ContentType::name() const { return parameter(QStringLiteral("name")); } void ContentType::setName(const QString & s, const QByteArray & cs) { Q_D(ContentType); d->encCS = cs; setParameter(QStringLiteral("name"), s); } QByteArray ContentType::id() const { return parameter(QStringLiteral("id")).toLatin1(); } void ContentType::setId(const QByteArray & s) { setParameter(QStringLiteral("id"), QString::fromLatin1(s)); } int ContentType::partialNumber() const { QByteArray p = parameter(QStringLiteral("number")).toLatin1(); if (!p.isEmpty()) { return p.toInt(); } else { return -1; } } int ContentType::partialCount() const { QByteArray p = parameter(QStringLiteral("total")).toLatin1(); if (!p.isEmpty()) { return p.toInt(); } else { return -1; } } contentCategory ContentType::category() const { return d_func()->category; } void ContentType::setCategory(contentCategory c) { Q_D(ContentType); d->category = c; } void ContentType::setPartialParams(int total, int number) { setParameter(QStringLiteral("number"), QString::number(number)); setParameter(QStringLiteral("total"), QString::number(total)); } bool ContentType::parse(const char *&scursor, const char *const send, bool isCRLF) { Q_D(ContentType); // content-type: type "/" subtype *(";" parameter) - clear(); eatCFWS(scursor, send, isCRLF); if (scursor == send) { return false; // empty header } // type QPair maybeMimeType; - if (!parseToken(scursor, send, maybeMimeType, false /* no 8Bit */)) { + if (!parseToken(scursor, send, maybeMimeType, ParseTokenNoFlag)) { return false; } // subtype eatCFWS(scursor, send, isCRLF); if (scursor == send || *scursor != '/') { return false; } scursor++; eatCFWS(scursor, send, isCRLF); if (scursor == send) { return false; } QPair maybeSubType; - if (!parseToken(scursor, send, maybeSubType, false /* no 8bit */)) { + if (!parseToken(scursor, send, maybeSubType, ParseTokenNoFlag)) { return false; } d->mimeType.reserve(maybeMimeType.second + maybeSubType.second + 1); d->mimeType = QByteArray(maybeMimeType.first, maybeMimeType.second).toLower() + '/' + QByteArray(maybeSubType.first, maybeSubType.second).toLower(); // parameter list eatCFWS(scursor, send, isCRLF); if (scursor == send) { goto success; // no parameters } if (*scursor != ';') { return false; } scursor++; if (!Parametrized::parse(scursor, send, isCRLF)) { return false; } // adjust category success: if (isMultipart()) { d->category = CCcontainer; } else { d->category = CCsingle; } return true; } //------------------------------ //--------------------------- kmime_mk_trivial_ctor_with_name_and_dptr(ContentID, SingleIdent, Content-ID) kmime_mk_dptr_ctor(ContentID, SingleIdent) bool ContentID::parse(const char *&scursor, const char *const send, bool isCRLF) { Q_D(ContentID); // Content-id := "<" contentid ">" // contentid := now whitespaces const char *origscursor = scursor; if (!SingleIdent::parse(scursor, send, isCRLF)) { scursor = origscursor; d->msgIdList.clear(); d->cachedIdentifier.clear(); while (scursor != send) { eatCFWS(scursor, send, isCRLF); // empty entry ending the list: OK. if (scursor == send) { return true; } // empty entry: ignore. if (*scursor == ',') { scursor++; continue; } AddrSpec maybeContentId; // Almost parseAngleAddr if (scursor == send || *scursor != '<') { return false; } scursor++; // eat '<' eatCFWS(scursor, send, isCRLF); if (scursor == send) { return false; } // Save chars untill '>'' QString result; if (!parseDotAtom(scursor, send, result, false)) { return false; } eatCFWS(scursor, send, isCRLF); if (scursor == send || *scursor != '>') { return false; } scursor++; // /Almost parseAngleAddr maybeContentId.localPart = result; d->msgIdList.append(maybeContentId); eatCFWS(scursor, send, isCRLF); // header end ending the list: OK. if (scursor == send) { return true; } // regular item separator: eat it. if (*scursor == ',') { scursor++; } } return true; } else { return true; } } //--------------------------- //--------------------------------- //@cond PRIVATE kmime_mk_trivial_ctor_with_name_and_dptr(ContentTransferEncoding, Generics::Token, Content-Transfer-Encoding) //@endcond typedef struct { const char *s; int e; } encTableType; static const encTableType encTable[] = { { "7Bit", CE7Bit }, { "8Bit", CE8Bit }, { "quoted-printable", CEquPr }, { "base64", CEbase64 }, { "x-uuencode", CEuuenc }, { "binary", CEbinary }, { nullptr, 0} }; void ContentTransferEncoding::clear() { Q_D(ContentTransferEncoding); d->decoded = true; d->cte = CE7Bit; Token::clear(); } contentEncoding ContentTransferEncoding::encoding() const { return d_func()->cte; } void ContentTransferEncoding::setEncoding(contentEncoding e) { Q_D(ContentTransferEncoding); d->cte = e; for (int i = 0; encTable[i].s != nullptr; ++i) { if (d->cte == encTable[i].e) { setToken(encTable[i].s); break; } } } bool ContentTransferEncoding::isDecoded() const { return d_func()->decoded; } void ContentTransferEncoding::setDecoded(bool decoded) { Q_D(ContentTransferEncoding); d->decoded = decoded; } bool ContentTransferEncoding::needToEncode() const { const Q_D(ContentTransferEncoding); return d->decoded && (d->cte == CEquPr || d->cte == CEbase64); } bool ContentTransferEncoding::parse(const char *&scursor, const char *const send, bool isCRLF) { Q_D(ContentTransferEncoding); clear(); if (!Token::parse(scursor, send, isCRLF)) { return false; } // TODO: error handling in case of an unknown encoding? for (int i = 0; encTable[i].s != nullptr; ++i) { if (qstricmp(token().constData(), encTable[i].s) == 0) { d->cte = (contentEncoding)encTable[i].e; break; } } d->decoded = (d->cte == CE7Bit || d->cte == CE8Bit); return true; } //-------------------------------- //------------------------------- //@cond PRIVATE kmime_mk_trivial_ctor_with_name_and_dptr(ContentDisposition, Generics::Parametrized, Content-Disposition) //@endcond QByteArray ContentDisposition::as7BitString(bool withHeaderType) const { if (isEmpty()) { return QByteArray(); } QByteArray rv; if (withHeaderType) { rv += typeIntro(); } if (d_func()->disposition == CDattachment) { rv += "attachment"; } else if (d_func()->disposition == CDinline) { rv += "inline"; } else { return QByteArray(); } if (!Parametrized::isEmpty()) { rv += "; " + Parametrized::as7BitString(false); } return rv; } bool ContentDisposition::isEmpty() const { return d_func()->disposition == CDInvalid; } void ContentDisposition::clear() { Q_D(ContentDisposition); d->disposition = CDInvalid; Parametrized::clear(); } contentDisposition ContentDisposition::disposition() const { return d_func()->disposition; } void ContentDisposition::setDisposition(contentDisposition disp) { Q_D(ContentDisposition); d->disposition = disp; } QString KMime::Headers::ContentDisposition::filename() const { return parameter(QStringLiteral("filename")); } void ContentDisposition::setFilename(const QString & filename) { setParameter(QStringLiteral("filename"), filename); } bool ContentDisposition::parse(const char *&scursor, const char *const send, bool isCRLF) { Q_D(ContentDisposition); clear(); // token QByteArray token; eatCFWS(scursor, send, isCRLF); if (scursor == send) { return false; } QPair maybeToken; - if (!parseToken(scursor, send, maybeToken, false /* no 8Bit */)) { + if (!parseToken(scursor, send, maybeToken, ParseTokenNoFlag)) { return false; } token = QByteArray(maybeToken.first, maybeToken.second).toLower(); if (token == "inline") { d->disposition = CDinline; } else if (token == "attachment") { d->disposition = CDattachment; } else { return false; } // parameter list eatCFWS(scursor, send, isCRLF); if (scursor == send) { return true; // no parameters } if (*scursor != ';') { return false; } scursor++; return Parametrized::parse(scursor, send, isCRLF); } //------------------------------ //@cond PRIVATE kmime_mk_trivial_ctor_with_name(Subject, Generics::Unstructured, Subject) //@endcond Base *createHeader(const QByteArray & type) { return HeaderFactory::createHeader(type); } //@cond PRIVATE kmime_mk_trivial_ctor_with_name(ContentDescription, Generics::Unstructured, Content-Description) kmime_mk_trivial_ctor_with_name(ContentLocation, Generics::Unstructured, Content-Location) kmime_mk_trivial_ctor_with_name(From, Generics::MailboxList, From) kmime_mk_trivial_ctor_with_name(Sender, Generics::SingleMailbox, Sender) kmime_mk_trivial_ctor_with_name(To, Generics::AddressList, To) kmime_mk_trivial_ctor_with_name(Cc, Generics::AddressList, Cc) kmime_mk_trivial_ctor_with_name(Bcc, Generics::AddressList, Bcc) kmime_mk_trivial_ctor_with_name(ReplyTo, Generics::AddressList, Reply-To) kmime_mk_trivial_ctor_with_name(Keywords, Generics::PhraseList, Keywords) kmime_mk_trivial_ctor_with_name(MIMEVersion, Generics::DotAtom, MIME-Version) kmime_mk_trivial_ctor_with_name(Supersedes, Generics::SingleIdent, Supersedes) kmime_mk_trivial_ctor_with_name(InReplyTo, Generics::Ident, In-Reply-To) kmime_mk_trivial_ctor_with_name(References, Generics::Ident, References) kmime_mk_trivial_ctor_with_name(Organization, Generics::Unstructured, Organization) kmime_mk_trivial_ctor_with_name(UserAgent, Generics::Unstructured, User-Agent) //@endcond } // namespace Headers } // namespace KMime diff --git a/tests/test_kmime_header_parsing.cpp b/tests/test_kmime_header_parsing.cpp index f4480bf..3d4ae1f 100644 --- a/tests/test_kmime_header_parsing.cpp +++ b/tests/test_kmime_header_parsing.cpp @@ -1,474 +1,474 @@ #include "kmime_headers.h" #include "kmime_header_parsing.h" #include #include #include //#include //#include #include #include #include #include using namespace KMime::HeaderParsing; using namespace std; static const char *tokenTypes[] = { "encoded-word", "atom", "token", "quoted-string", "domain-literal", "comment", "phrase", "dot-atom", "domain", "obs-route", "addr-spec", "angle-addr", "mailbox", "group", "address", "address-list", "parameter", "raw-parameter-list", "parameter-list", "time", "date-time" }; static const int tokenTypesLen = sizeof tokenTypes / sizeof * tokenTypes; void usage(const char *msg = nullptr) { if (msg && *msg) { cerr << msg << endl; } cerr << "usage: test_kmime_header_parsing " "(--token |--headerfield |--header)\n" "\n" " --token interpret input as and output\n" " (-t) in parsed form. Currently defined values of\n" " are:" << endl; for (int i = 0 ; i < tokenTypesLen ; ++i) { cerr << " " << tokenTypes[i] << endl; } cerr << "\n" " --headerfield interpret input as header field \n" " (-f) and output in parsed form.\n" "\n" " --header parse an RFC2822 header. Iterates over all\n" " (-h) header fields and outputs them in parsed form." << endl; exit(1); } ostream &operator<<(ostream &stream, const QString &str) { return stream << str.toUtf8().data(); } int main(int argc, char *argv[]) { if (argc == 1 || argc > 3) { usage(); } // // process options: // enum { None, Token, HeaderField, Header } action = None; const char *argument = nullptr; bool withCRLF = false; while (true) { int option_index = 0; static const struct option long_options[] = { // actions: { "token", 1, nullptr, 't' }, { "headerfield", 1, nullptr, 'f' }, { "header", 0, nullptr, 'h' }, { "crlf", 0, nullptr, 'c' }, { nullptr, 0, nullptr, 0 } }; int c = getopt_long(argc, argv, "cf:ht:", long_options, &option_index); if (c == -1) { break; } switch (c) { case 'c': // --crlf withCRLF = true; break; case 't': // --token action = Token; argument = optarg; break; case 'f': // --headerfield usage("--headerfield is not yet implemented!"); break; case 'h': // --header usage("--header is not yet implemented!"); break; default: usage("unknown option encountered!"); } } if (optind < argc) { usage("non-option argument encountered!"); } assert(action == Token); int index; for (index = 0 ; index < tokenTypesLen ; ++index) { if (!qstricmp(tokenTypes[index], argument)) { break; } } if (index >= tokenTypesLen) { usage("unknown token type"); } //QT5 KComponentData componentData( "test_kmime_header_parsing" ); QFile stdIn; stdIn.open(stdin, QIODevice::ReadOnly); const QByteArray indata = stdIn.readAll(); stdIn.close(); QByteArray::ConstIterator iit = indata.begin(); const QByteArray::ConstIterator iend = indata.end(); switch (index) { case 0: { // encoded-word QString result; QByteArray language, charset; // must have checked for initial '=' already: bool ok = indata.size() >= 1 && *iit++ == '=' && parseEncodedWord(iit, iend, result, language, charset); cout << (ok ? "OK" : "BAD") << endl << "result:\n" << result << endl << "language:\n" << language.data() << endl; } break; case 1: { // atom QString result = QStringLiteral("with 8bit: "); bool ok = parseAtom(iit, iend, result, true); cout << (ok ? "OK" : "BAD") << endl << "result:\n" << result << endl; result = QStringLiteral("without 8bit: "); #ifdef COMPILE_FAIL ok = parseAtom(indata.begin(), iend, result, false); #else iit = indata.begin(); ok = parseAtom(iit, iend, result, false); #endif cout << (ok ? "OK" : "BAD") << endl << "result:\n" << result << endl; } break; case 2: { // token QString result = QStringLiteral("with 8bit: "); - bool ok = parseToken(iit, iend, result, true); + bool ok = parseToken(iit, iend, result, ParseTokenAllow8Bit); cout << (ok ? "OK" : "BAD") << endl << "result:\n" << result << endl; result = QStringLiteral("without 8bit: "); #ifdef COMPILE_FAIL - ok = parseToken(indata.begin(), iend, result, false); + ok = parseToken(indata.begin(), iend, result, ParseTokenNoFlag); #else iit = indata.begin(); - ok = parseToken(iit, iend, result, false); + ok = parseToken(iit, iend, result, ParseTokenNoFlag); #endif cout << (ok ? "OK" : "BAD") << endl << "result:\n" << result << endl; } break; case 3: { // quoted-string QString result; // must have checked for initial '"' already: bool ok = *iit++ == '"' && parseGenericQuotedString(iit, iend, result, withCRLF, '"', '"'); cout << (ok ? "OK" : "BAD") << endl << "result:\n" << result << endl; } break; case 4: { // domain-literal QString result; // must have checked for initial '[' already: bool ok = *iit++ == '[' && parseGenericQuotedString(iit, iend, result, withCRLF, '[', ']'); cout << (ok ? "OK" : "BAD") << endl << "result:\n" << result << endl; } break; case 5: { // comment QString result; // must have checked for initial '(' already: bool ok = *iit++ == '(' && parseComment(iit, iend, result, withCRLF, true); cout << (ok ? "OK" : "BAD") << endl << "result:\n" << result << endl; } break; case 6: { // phrase QString result; bool ok = parsePhrase(iit, iend, result, withCRLF); cout << (ok ? "OK" : "BAD") << endl << "result:\n" << result << endl; } break; case 7: { // dot-atom QString result; bool ok = parseDotAtom(iit, iend, result, withCRLF); cout << (ok ? "OK" : "BAD") << endl << "result:\n" << result << endl; } break; case 8: { // domain QString result; bool ok = parseDomain(iit, iend, result, withCRLF); cout << (ok ? "OK" : "BAD") << endl << "result:\n" << result << endl; } break; case 9: { // obs-route QStringList result; bool ok = parseObsRoute(iit, iend, result, withCRLF, true /*save*/); cout << (ok ? "OK" : "BAD") << endl << "result: " << result.count() << " domains:" << endl; for (QStringList::ConstIterator it = result.constBegin() ; it != result.constEnd() ; ++it) { cout << (*it) << endl; } } break; case 10: { // addr-spec KMime::Types::AddrSpec result; bool ok = parseAddrSpec(iit, iend, result, withCRLF); cout << (ok ? "OK" : "BAD") << endl << "result.localPart:\n" << result.localPart << endl << "result.domain:\n" << result.domain << endl; } break; case 11: { // angle-addr KMime::Types::AddrSpec result; bool ok = parseAngleAddr(iit, iend, result, withCRLF); cout << (ok ? "OK" : "BAD") << endl << "result.localPart:\n" << result.localPart << endl << "result.domain:\n" << result.domain << endl; } break; case 12: { // mailbox KMime::Types::Mailbox result; bool ok = parseMailbox(iit, iend, result, withCRLF); cout << (ok ? "OK" : "BAD") << endl << "result.displayName:\n" << result.name() << endl << "result.addrSpec.localPart:\n" << result.addrSpec().localPart << endl << "result.addrSpec.domain:\n" << result.addrSpec().domain << endl; } break; case 13: { // group KMime::Types::Address result; bool ok = parseGroup(iit, iend, result, withCRLF); cout << (ok ? "OK" : "BAD") << endl << "result.displayName:\n" << result.displayName << endl; int i = 0; foreach (const auto &it, result.mailboxList) { cout << "result.mailboxList[" << i << "].displayName:\n" << (it).name() << endl << "result.mailboxList[" << i << "].addrSpec.localPart:\n" << (it).addrSpec().localPart << endl << "result.mailboxList[" << i << "].addrSpec.domain:\n" << (it).addrSpec().domain << endl; ++i; } } break; case 14: { // address KMime::Types::Address result; bool ok = parseAddress(iit, iend, result, withCRLF); cout << (ok ? "OK" : "BAD") << endl << "result.displayName:\n" << endl; int i = 0; foreach (const auto &it, result.mailboxList) { cout << "result.mailboxList[" << i << "].displayName:\n" << (it).name() << endl << "result.mailboxList[" << i << "].addrSpec.localPart:\n" << (it).addrSpec().localPart << endl << "result.mailboxList[" << i << "].addrSpec.domain:\n" << (it).addrSpec().domain << endl; ++i; } } break; case 15: { // address-list KMime::Types::AddressList result; bool ok = parseAddressList(iit, iend, result, withCRLF); cout << (ok ? "OK" : "BAD") << endl; int j = 0; foreach (auto jt, result) { cout << "result[" << j << "].displayName:\n" << (jt).displayName << endl; int i = 0; foreach (const auto &it, (jt).mailboxList) { cout << "result[" << j << "].mailboxList[" << i << "].displayName:\n" << (it).name() << endl << "result[" << j << "].mailboxList[" << i << "].addrSpec.localPart:\n" << (it).addrSpec().localPart << endl << "result[" << j << "].mailboxList[" << i << "].addrSpec.domain:\n" << (it).addrSpec().domain << endl; ++i; } ++j; } } break; case 16: { // parameter QPair result; bool ok = parseParameter(iit, iend, result, withCRLF); cout << (ok ? "OK" : "BAD") << endl << "result.first (attribute):\n" << result.first << endl << "result.second.qstring (value):\n" << result.second.qstring << endl << "result.second.qpair (value):\n" << QByteArray(result.second.qpair.first, result.second.qpair.second + 1).data() << endl; } break; case 17: { // raw-parameter-list QMap result; bool ok = parseRawParameterList(iit, iend, result, withCRLF); cout << (ok ? "OK" : "BAD") << endl << "result: " << result.count() << " raw parameters:" << endl; int i = 0; for (QMap::ConstIterator it = result.constBegin(); it != result.constEnd() ; ++it, ++i) { cout << "result[" << i << "].key() (attribute):\n" << it.key() << endl << "result[" << i << "].data().qstring (value):\n" << it.value().qstring << endl << "result[" << i << "].data().qpair (value):\n" << QByteArray(it.value().qpair.first, it.value().qpair.second + 1).data() << endl; } } break; case 18: { // parameter-list QMap result; bool ok = parseParameterList(iit, iend, result, withCRLF); cout << (ok ? "OK" : "BAD") << endl << "result: " << result.count() << " parameters:" << endl; int i = 0; for (QMap::Iterator it = result.begin() ; it != result.end() ; ++it, ++i) { cout << "result[" << i << "].key() (attribute):\n" << it.key() << endl << "result[" << i << "].data() (value):\n" << it.value() << endl; } } break; case 19: { // time int hour, mins, secs; long int secsEastOfGMT; bool timeZoneKnown = true; bool ok = parseTime(iit, iend, hour, mins, secs, secsEastOfGMT, timeZoneKnown, withCRLF); cout << (ok ? "OK" : "BAD") << endl << "result.hour: " << hour << endl << "result.mins: " << mins << endl << "result.secs: " << secs << endl << "result.secsEastOfGMT: " << secsEastOfGMT << endl << "result.timeZoneKnown: " << timeZoneKnown << endl; } break; case 20: { // date-time QDateTime result; bool ok = parseDateTime(iit, iend, result, withCRLF); time_t timet = result.toTime_t(); cout << (ok ? "OK" : "BAD") << endl << "result.time (in local timezone): " << ctime(&timet) << "result.secsEastOfGMT: " << result.utcOffset() << " (" << result.utcOffset() / 60 << "mins)" << endl; } break; default: assert(0); } }