diff --git a/src/vmime/address.cpp b/src/vmime/address.cpp
index 5703a389..619d5315 100644
--- a/src/vmime/address.cpp
+++ b/src/vmime/address.cpp
@@ -112,27 +112,39 @@ shared_ptr
address::parseNext(
case '"':
- quoted = !quoted;
+ if (commentLevel == 0) {
+ quoted = !quoted;
+ }
+
break;
case '<':
- inRouteAddr = true;
+ if (!quoted && commentLevel == 0) {
+ inRouteAddr = true;
+ }
+
break;
case '>':
- inRouteAddr = false;
+ if (!quoted && commentLevel == 0) {
+ inRouteAddr = false;
+ }
+
break;
case '(':
- ++commentLevel;
+ if (!quoted) {
+ ++commentLevel;
+ }
+
break;
case ')':
- if (commentLevel > 0) {
+ if (!quoted && commentLevel > 0) {
--commentLevel;
}
diff --git a/src/vmime/addressList.cpp b/src/vmime/addressList.cpp
index 75f62245..6f7645d8 100644
--- a/src/vmime/addressList.cpp
+++ b/src/vmime/addressList.cpp
@@ -26,6 +26,7 @@
#include "vmime/exception.hpp"
#include "vmime/mailboxList.hpp"
#include "vmime/mailboxGroup.hpp"
+#include "vmime/utility/outputStreamStringAdapter.hpp"
namespace vmime {
@@ -87,21 +88,43 @@ void addressList::generateImpl(
size_t pos = curLinePos;
- generationContext tmpCtx(ctx);
- tmpCtx.setMaxLineLength(tmpCtx.getMaxLineLength() - 2);
+ // Leave room for the comma after all addresses but the last one
+ generationContext commaCtx(ctx);
- if (!m_list.empty()) {
+ if (ctx.getMaxLineLength() != lineLengthLimits::infinite &&
+ ctx.getMaxLineLength() > NEW_LINE_SEQUENCE_LENGTH + 1) {
- for (std::vector >::const_iterator i = m_list.begin() ; ; ) {
+ commaCtx.setMaxLineLength(ctx.getMaxLineLength() - 1);
+ }
- (*i)->generate(ctx, os, pos, &pos);
+ for (std::vector >::const_iterator i = m_list.begin() ;
+ i != m_list.end() ; ++i) {
- if (++i == m_list.end()) {
- break;
- }
+ const generationContext& addrCtx = (i + 1 == m_list.end()) ? ctx : commaCtx;
+
+ if (i == m_list.begin()) {
+ (*i)->generate(addrCtx, os, pos, &pos);
+ continue;
+ }
+
+ string tmp;
+ utility::outputStreamStringAdapter tmpStream(tmp);
+ size_t tmpPos = 0;
+
+ (*i)->generate(addrCtx, tmpStream, pos + 2, &tmpPos);
+
+ // Unless the address fits entirely after ", ", it starts a new line,
+ // with the fold after the comma
+ if (pos > NEW_LINE_SEQUENCE_LENGTH &&
+ (tmp.find(CRLF) != string::npos || tmpPos > addrCtx.getMaxLineLength())) {
+
+ os << "," << NEW_LINE_SEQUENCE;
+ (*i)->generate(addrCtx, os, NEW_LINE_SEQUENCE_LENGTH, &pos);
+
+ } else {
- os << ", ";
- pos += 2;
+ os << ", " << tmp;
+ pos = tmpPos;
}
}
diff --git a/src/vmime/emailAddress.cpp b/src/vmime/emailAddress.cpp
index e6873803..7003e125 100644
--- a/src/vmime/emailAddress.cpp
+++ b/src/vmime/emailAddress.cpp
@@ -22,6 +22,7 @@
//
#include "vmime/emailAddress.hpp"
+#include "vmime/wordEncoder.hpp"
#include "vmime/platform.hpp"
@@ -587,9 +588,38 @@ void emailAddress::generateImpl(
} else {
- // Local part
+ // Local part. It may not be folded, nor contain an encoded-word
+ // (RFC 2047 §5), unless it cannot be written otherwise.
vmime::utility::outputStreamStringAdapter os(localPart);
- m_localName.generate(ctx, os, 0, NULL, text::QUOTE_IF_NEEDED, NULL);
+ const string& buffer = m_localName.getBuffer();
+
+ // Not is7bit(), which is also false for "=?" and a trailing '='.
+ // ESC (ISO-2022-JP), CR and LF are not printable.
+ bool printable = true;
+
+ for (size_t i = 0 ; printable && i < buffer.length() ; ++i) {
+ printable = parserHelpers::isPrint(buffer[i]);
+ }
+
+ if (printable) {
+
+ generationContext localCtx(ctx);
+ localCtx.setMaxLineLength(lineLengthLimits::infinite);
+
+ m_localName.generate(localCtx, os, 0, NULL,
+ text::FORCE_NO_ENCODING | text::QUOTE_IF_NEEDED, NULL);
+
+ } else {
+
+ wordEncoder enc(buffer, m_localName.getCharset());
+ const string lang = m_localName.getLanguage();
+
+ os << "=?" << m_localName.getCharset().getName()
+ << (lang.empty() ? "" : "*" + lang)
+ << (enc.getEncoding() == wordEncoder::ENCODING_B64 ? "?B?" : "?Q?")
+ << enc.getNextChunk(lineLengthLimits::infinite)
+ << "?=";
+ }
// Domain part as IDNA
domainPart = domainNameToIDNA(m_domainName.getConvertedText(vmime::charsets::UTF_8));
diff --git a/src/vmime/mailbox.cpp b/src/vmime/mailbox.cpp
index 0ede48fc..52def32a 100644
--- a/src/vmime/mailbox.cpp
+++ b/src/vmime/mailbox.cpp
@@ -335,11 +335,14 @@ void mailbox::generateImpl(
size_t pos = curLinePos;
- // No display name is specified, only email address.
- if (curLinePos + generatedEmail.length() > ctx.getMaxLineLength()) {
+ // No display name is specified, only email address. Fold before
+ // it only if it then fits, as it cannot be folded itself.
+ if (curLinePos + generatedEmail.length() > ctx.getMaxLineLength() &&
+ curLinePos > NEW_LINE_SEQUENCE_LENGTH &&
+ NEW_LINE_SEQUENCE_LENGTH + generatedEmail.length() <= ctx.getMaxLineLength()) {
os << NEW_LINE_SEQUENCE;
- pos = NEW_LINE_SEQUENCE.length();
+ pos = NEW_LINE_SEQUENCE_LENGTH;
}
os << generatedEmail;
@@ -378,14 +381,20 @@ void mailbox::generateImpl(
text::QUOTE_IF_POSSIBLE | (forceEncode ? text::FORCE_ENCODING : 0)
);
+ // The fold, or a space, separates the name from the address
if (pos + generatedEmail.length() + 3 > ctx.getMaxLineLength()) {
os << NEW_LINE_SEQUENCE;
- pos = NEW_LINE_SEQUENCE.length();
+ pos = NEW_LINE_SEQUENCE_LENGTH;
+
+ } else {
+
+ os << " ";
+ ++pos;
}
- os << " <" << generatedEmail << ">";
- pos += 2 + generatedEmail.length() + 1;
+ os << "<" << generatedEmail << ">";
+ pos += 1 + generatedEmail.length() + 1;
if (newLinePos) {
*newLinePos = pos;
diff --git a/src/vmime/mailboxGroup.cpp b/src/vmime/mailboxGroup.cpp
index ff9c74ff..8bf94095 100644
--- a/src/vmime/mailboxGroup.cpp
+++ b/src/vmime/mailboxGroup.cpp
@@ -24,6 +24,7 @@
#include "vmime/mailboxGroup.hpp"
#include "vmime/parserHelpers.hpp"
#include "vmime/exception.hpp"
+#include "vmime/utility/outputStreamStringAdapter.hpp"
namespace vmime {
@@ -161,9 +162,6 @@ void mailboxGroup::generateImpl(
size_t pos = curLinePos;
- generationContext tmpCtx(ctx);
- tmpCtx.setMaxLineLength(ctx.getMaxLineLength() - 2);
-
m_name.encodeAndFold(
ctx, os, pos, &pos,
forceEncode ? text::FORCE_ENCODING : 0
@@ -172,21 +170,42 @@ void mailboxGroup::generateImpl(
os << ":";
++pos;
+ // Leave room for the ',' or ';' after each mailbox
+ generationContext mboxCtx(ctx);
+
+ if (ctx.getMaxLineLength() != lineLengthLimits::infinite &&
+ ctx.getMaxLineLength() > NEW_LINE_SEQUENCE_LENGTH + 1) {
+
+ mboxCtx.setMaxLineLength(ctx.getMaxLineLength() - 1);
+ }
+
for (std::vector >::const_iterator it = m_list.begin() ;
it != m_list.end() ; ++it) {
if (it != m_list.begin()) {
+ os << ",";
+ ++pos;
+ }
+
+ string tmp;
+ utility::outputStreamStringAdapter tmpStream(tmp);
+ size_t tmpPos = 0;
+
+ (*it)->generate(mboxCtx, tmpStream, pos + 1, &tmpPos);
- os << ", ";
- pos += 2;
+ // Unless the mailbox fits entirely after a space, it starts a new
+ // line, with the fold in place of the space
+ if (pos > NEW_LINE_SEQUENCE_LENGTH &&
+ (tmp.find(CRLF) != string::npos || tmpPos > mboxCtx.getMaxLineLength())) {
+
+ os << NEW_LINE_SEQUENCE;
+ (*it)->generate(mboxCtx, os, NEW_LINE_SEQUENCE_LENGTH, &pos);
} else {
- os << " ";
- ++pos;
+ os << " " << tmp;
+ pos = tmpPos;
}
-
- (*it)->generate(tmpCtx, os, pos, &pos);
}
os << ";";
diff --git a/src/vmime/messageIdSequence.cpp b/src/vmime/messageIdSequence.cpp
index e1d324a1..a816bb39 100644
--- a/src/vmime/messageIdSequence.cpp
+++ b/src/vmime/messageIdSequence.cpp
@@ -23,6 +23,7 @@
#include "vmime/messageIdSequence.hpp"
#include "vmime/exception.hpp"
+#include "vmime/utility/outputStreamStringAdapter.hpp"
#include
@@ -123,19 +124,33 @@ void messageIdSequence::generateImpl(
if (!m_list.empty()) {
- generationContext tmpCtx(ctx);
- tmpCtx.setMaxLineLength(ctx.getMaxLineLength() - 2);
+ m_list.front()->generate(ctx, os, pos, &pos);
- for (std::vector >::const_iterator it = m_list.begin() ; ; ) {
+ // The following ids are folded here, in place of the separator
+ generationContext idCtx(ctx);
+ idCtx.setWrapMessageId(false);
- (*it)->generate(ctx, os, pos, &pos);
+ for (std::vector >::const_iterator it = m_list.begin() + 1 ;
+ it != m_list.end() ; ++it) {
- if (++it == m_list.end()) {
- break;
+ string id;
+ utility::outputStreamStringAdapter idStream(id);
+
+ (*it)->generate(idCtx, idStream, 0, NULL);
+
+ if (ctx.getWrapMessageId() && pos + 1 + id.length() > ctx.getMaxLineLength()) {
+
+ os << NEW_LINE_SEQUENCE;
+ pos = NEW_LINE_SEQUENCE_LENGTH;
+
+ } else {
+
+ os << " ";
+ ++pos;
}
- os << " ";
- pos++;
+ os << id;
+ pos += id.length();
}
}
diff --git a/src/vmime/word.cpp b/src/vmime/word.cpp
index 9d7ea735..7cfa732b 100644
--- a/src/vmime/word.cpp
+++ b/src/vmime/word.cpp
@@ -442,6 +442,166 @@ void word::generateImpl(
}
+enum QuoteResult {
+ QUOTE_NOT_DONE, // nothing written, fold the text unquoted
+ QUOTE_DONE,
+ QUOTE_NEEDS_ENCODING // nothing written, a run does not fit on a line
+};
+
+
+static bool isWSP(const char c) {
+
+ return c == ' ' || c == '\t';
+}
+
+
+// Whether "buffer" can only be written as a quoted-string: it contains
+// specials (RFC 5322 §3.2.3), or white-space which would be read
+// as a single space outside quotes (§3.2.2)
+static bool phraseNeedsQuoting(const string& buffer) {
+
+ if (utility::stringUtils::needQuoting(buffer, "()<>[]:;@\\,.\"\t")) {
+ return true;
+ }
+
+ return !buffer.empty() &&
+ (buffer[0] == ' ' || buffer[buffer.length() - 1] == ' ' ||
+ buffer.find(" ") != string::npos);
+}
+
+
+enum FoldPoint {
+ FOLD_NONE,
+ FOLD_SPACE, // before a single space
+ FOLD_RUN_END, // before the last space of a white-space run
+ FOLD_TAB, // before a tab, which a lenient parser reads as a space
+ FOLD_BACKSLASH // after '\', which a lenient parser takes as continuation
+};
+
+
+// Where a fold may be put before buffer[pos], "prev" being the character
+// before buffer[0]. Only at the end of a run, which lenient parsers
+// collapse, and the kind tells how much a fold there is preferred.
+static FoldPoint getFoldPoint(const string& buffer, const size_t pos, const char prev) {
+
+ const char before = (pos == 0 ? prev : buffer[pos - 1]);
+
+ if (!isWSP(buffer[pos]) ||
+ (pos + 1 < buffer.length() && isWSP(buffer[pos + 1])) ||
+ (pos == 0 && isWSP(before))) {
+
+ return FOLD_NONE;
+ }
+
+ if (before == '\\') {
+ return FOLD_BACKSLASH;
+ } else if (buffer[pos] == '\t') {
+ return FOLD_TAB;
+ }
+
+ return isWSP(before) ? FOLD_RUN_END : FOLD_SPACE;
+}
+
+
+// Position after "pos" where a quoted-string may be folded, or the end
+static size_t nextFoldPoint(const string& quoted, size_t pos) {
+
+ for (++pos ; pos < quoted.length() ; ++pos) {
+
+ if (getFoldPoint(quoted, pos, 'x') == FOLD_SPACE) {
+ break;
+ }
+ }
+
+ return std::min(pos, quoted.length());
+}
+
+
+// Write "buffer" as a quoted-string (RFC 5322 §3.2.4). If it does
+// not fit on the current line, fold before it, or inside it before
+// white-space, where unfolding restores the text exactly.
+static QuoteResult generateQuotedPhrase(
+ const string& buffer,
+ const generationContext& ctx,
+ utility::outputStream& os,
+ size_t& curLineLength,
+ const int flags,
+ word::generatorState* state
+) {
+
+ const size_t maxLineLength = ctx.getMaxLineLength();
+ const string quoted = utility::stringUtils::quote(buffer, "\\\"", "\\");
+
+ if (curLineLength + 2 /* 2 x " */ + quoted.length() > maxLineLength) {
+
+ if (!phraseNeedsQuoting(buffer)) {
+ return QUOTE_NOT_DONE;
+ }
+
+ // Lengths of the first and the longest run, with the quotes
+ size_t firstRun = 0, maxRun = 0;
+
+ for (size_t i = 0, next ; i < quoted.length() ; i = next) {
+
+ next = nextFoldPoint(quoted, i);
+
+ const size_t len = next - i + (i == 0 ? 1 : 0) + (next == quoted.length() ? 1 : 0);
+
+ if (i == 0) {
+ firstRun = len;
+ }
+
+ maxRun = std::max(maxRun, len);
+ }
+
+ // Same rule as for unquoted text
+ if ((flags & text::FORCE_NO_ENCODING) == 0 && maxRun >= maxLineLength - 3) {
+ return QUOTE_NEEDS_ENCODING;
+ }
+
+ // Start a new line if not even the first run fits on this one, or if
+ // the quoted string then fits entirely, but leave a fold before the
+ // first word to the enclosing component, and prefer not to fold
+ // after a '\'
+ if (curLineLength > NEW_LINE_SEQUENCE_LENGTH &&
+ (curLineLength + firstRun > maxLineLength ||
+ (!state->isFirstWord && !state->lastCharIsBackslash &&
+ NEW_LINE_SEQUENCE_LENGTH + 2 + quoted.length() <= maxLineLength))) {
+
+ os << NEW_LINE_SEQUENCE;
+ curLineLength = NEW_LINE_SEQUENCE_LENGTH;
+ }
+ }
+
+ os << '"';
+ ++curLineLength;
+
+ for (size_t i = 0, next ; i < quoted.length() ; i = next) {
+
+ next = nextFoldPoint(quoted, i);
+
+ const size_t len = next - i + (next == quoted.length() ? 1 : 0);
+
+ if (i != 0 && curLineLength + len > maxLineLength) {
+ os << CRLF;
+ curLineLength = 0;
+ }
+
+ os << string(quoted, i, next - i);
+ curLineLength += next - i;
+ }
+
+ os << '"';
+ ++curLineLength;
+
+ state->prevWordIsEncoded = false;
+ state->lastCharIsSpace = false;
+ state->lastCharIsBackslash = false;
+
+ return QUOTE_DONE;
+}
+
+
void word::generate(
const generationContext& ctx,
utility::outputStream& os,
@@ -459,6 +619,16 @@ void word::generate(
state = &defaultGeneratorState;
}
+ // Nothing to write, keep the state of the previous word
+ if (m_buffer.empty() && (flags & (text::QUOTE_IF_POSSIBLE | text::QUOTE_IF_NEEDED)) == 0) {
+
+ if (newLinePos) {
+ *newLinePos = curLinePos;
+ }
+
+ return;
+ }
+
// Find out if encoding is forced or required by contents + charset
bool encodingNeeded = false;
@@ -470,6 +640,8 @@ void word::generate(
encodingNeeded = wordEncoder::isEncodingNeeded(ctx, m_buffer, m_charset, m_lang);
}
+ QuoteResult quoteResult = QUOTE_NOT_DONE;
+
// If text does not need to be encoded, quote the buffer (no folding is performed).
if (!encodingNeeded &&
(flags & text::QUOTE_IF_NEEDED) &&
@@ -480,18 +652,22 @@ void word::generate(
os << '"' << quoted << '"';
curLineLength += 1 + quoted.length() + 1;
- // If possible and requested (with flag), quote the buffer (no folding is performed).
+ state->prevWordIsEncoded = false;
+ state->lastCharIsBackslash = false;
+
+ // If possible and requested (with flag), quote the buffer.
// Quoting is possible if and only if:
// - the buffer does not need to be encoded
- // - the buffer does not contain quoting character (")
- // - there is enough remaining space on the current line to hold the whole buffer
+ // - it fits on the current line, or it cannot be written unquoted
} else if (!encodingNeeded &&
(flags & text::QUOTE_IF_POSSIBLE) &&
- m_buffer.find('"') == string::npos &&
- (curLineLength + 2 /* 2 x " */ + m_buffer.length()) < ctx.getMaxLineLength()) {
+ (quoteResult = generateQuotedPhrase(m_buffer, ctx, os, curLineLength,
+ flags, state)) != QUOTE_NOT_DONE) {
- os << '"' << m_buffer << '"';
- curLineLength += 2 + m_buffer.length();
+ if (quoteResult == QUOTE_NEEDS_ENCODING) {
+ generate(ctx, os, curLinePos, newLinePos, flags | text::FORCE_ENCODING, state);
+ return;
+ }
// We will fold lines without encoding them.
} else if (!encodingNeeded) {
@@ -514,15 +690,18 @@ void word::generate(
// * a maximum line length of N bytes
// * a buffer containing N+1 bytes, with no whitespace
//
- // Look in the buffer for any run (ie. whitespace-separated sequence) which
+ // Look in the buffer for any run (ie. sequence between two fold points) which
// is longer than the maximum line length. If there is one, then force encoding,
// so that no generated line is longer than the maximum line length.
+ const char prevChar = state->lastCharIsBackslash ? '\\'
+ : (state->lastCharIsSpace ? ' ' : 'x');
+
size_t maxRunLength = 0;
size_t curRunLength = 0;
- for (string::const_iterator p = buffer.begin(), end = buffer.end() ; p != end ; ++p) {
+ for (size_t i = 0 ; i < buffer.length() ; ++i) {
- if (parserHelpers::isSpace(*p)) {
+ if (getFoldPoint(buffer, i, prevChar) != FOLD_NONE) {
maxRunLength = std::max(maxRunLength, curRunLength);
curRunLength = 0;
@@ -542,8 +721,21 @@ void word::generate(
return;
}
+ // Separate from previous word, unless the text starts with white-space
+ bool sepPending =
+ !state->isFirstWord &&
+ (state->prevWordIsEncoded || ctx.getInternationalizedEmailSupport()) &&
+ !state->lastCharIsSpace &&
+ !buffer.empty() && !parserHelpers::isSpace(buffer[0]);
+
+ if (sepPending) {
+ ++curLineLength;
+ }
+
// Output runs, and fold line when a whitespace is encountered
string::const_iterator lastWSpos = buffer.end(); // last white-space position
+ string::const_iterator lastRunEnd = buffer.end(), lastTab = buffer.end();
+ string::const_iterator lastBackslash = buffer.end();
string::const_iterator curLineStart = buffer.begin(); // current line start
string::const_iterator p = buffer.begin();
@@ -557,14 +749,38 @@ void word::generate(
for ( ; p != end ; ++p, ++curLineLength) {
// Exceeded maximum line length, but we have found a white-space
- // where we can cut the line...
- if (curLineLength >= ctx.getMaxLineLength() && lastWSpos != end) {
- break;
+ // where we can cut the line: a single space, else the end of a
+ // run, else a tab, else after a '\'
+ if (curLineLength >= ctx.getMaxLineLength()) {
+
+ if (lastWSpos == end) {
+ lastWSpos = lastRunEnd;
+ }
+
+ if (lastWSpos == end) {
+ lastWSpos = lastTab;
+ }
+
+ if (lastWSpos == end) {
+ lastWSpos = lastBackslash;
+ }
+
+ if (lastWSpos != end) {
+ break;
+ }
}
- if (*p == ' ' || *p == '\t') {
- // Remember the position of this white-space character
- lastWSpos = p;
+ if (newLine && p == curLineStart) {
+ continue; // a fold was just put here
+ }
+
+ switch (getFoldPoint(buffer, p - buffer.begin(), prevChar)) {
+
+ case FOLD_SPACE: lastWSpos = p; break;
+ case FOLD_RUN_END: lastRunEnd = p; break;
+ case FOLD_TAB: lastTab = p; break;
+ case FOLD_BACKSLASH: lastBackslash = p; break;
+ case FOLD_NONE: break;
}
}
@@ -600,17 +816,15 @@ void word::generate(
}
p = curLineStart;
- lastWSpos = end;
+ lastWSpos = lastRunEnd = lastTab = lastBackslash = end;
newLine = true;
+ sepPending = false;
} else {
- if (!state->isFirstWord &&
- (state->prevWordIsEncoded || ctx.getInternationalizedEmailSupport()) &&
- !state->lastCharIsSpace &&
- !parserHelpers::isSpace(*curLineStart)) {
-
+ if (sepPending) {
os << " "; // Separate from previous word
+ sepPending = false;
}
os << string(curLineStart, p);
@@ -639,7 +853,7 @@ void word::generate(
}
curLineStart = p;
- lastWSpos = end;
+ lastWSpos = lastRunEnd = lastTab = lastBackslash = end;
newLine = true;
}
}
@@ -650,11 +864,9 @@ void word::generate(
// characters _after_ the last white-space; so we cut the line at this
// last white-space.
- if (curLineLength != NEW_LINE_SEQUENCE_LENGTH &&
- !state->isFirstWord &&
- state->prevWordIsEncoded) {
-
+ if (sepPending) {
os << " "; // Separate from previous word
+ sepPending = false;
}
os << string(curLineStart, lastWSpos);
@@ -665,6 +877,8 @@ void word::generate(
state->lastCharIsSpace = false;
}
+ curLineStart = lastWSpos + 1;
+
if (flags & text::NO_NEW_LINE_SEQUENCE) {
os << CRLF;
@@ -672,6 +886,14 @@ void word::generate(
state->lastCharIsSpace = true;
+ } else if (*lastWSpos == '\t') {
+
+ os << CRLF; // the tab starts the next line
+ curLineLength = 0;
+ curLineStart = lastWSpos;
+
+ state->lastCharIsSpace = true;
+
} else {
os << NEW_LINE_SEQUENCE;
@@ -680,14 +902,15 @@ void word::generate(
state->lastCharIsSpace = true;
}
- curLineStart = lastWSpos + 1;
-
- p = lastWSpos + 1;
- lastWSpos = end;
+ p = curLineStart;
+ lastWSpos = lastRunEnd = lastTab = lastBackslash = end;
newLine = true;
}
}
+ state->prevWordIsEncoded = false;
+ state->lastCharIsBackslash = (!buffer.empty() && buffer[buffer.length() - 1] == '\\');
+
/*
RFC #2047:
4. Encodings
@@ -732,6 +955,9 @@ void word::generate(
const size_t maxLineLength2 = (maxLineLength3 < minWordLength + 1)
? maxLineLength3 + minWordLength + 1 : maxLineLength3;
+ // RFC 2047 §2: an encoded-word may not be more than 75 characters long
+ const size_t maxEncodedWordLength = 75;
+
// Checks whether remaining space on this line is usable. If too few
// characters can be encoded, start a new line.
bool startNewLine = true;
@@ -753,31 +979,47 @@ void word::generate(
}
}
+ // White-space after an unencoded word is displayed (RFC 2047 section
+ // 6.2), so the fold or space added here stands for a leading space
+ // or tab, which is then written as is
+ const bool leadingWSP =
+ !state->isFirstWord && !state->prevWordIsEncoded && isWSP(m_buffer[0]);
+ const char sep = leadingWSP ? m_buffer[0] : ' ';
+
+ bool sepAdded = false;
+
if (startNewLine) {
- os << NEW_LINE_SEQUENCE;
+ os << CRLF << sep;
curLineLength = NEW_LINE_SEQUENCE_LENGTH;
state->lastCharIsSpace = true;
+ sepAdded = true;
}
// Encode and fold input buffer
if (!startNewLine && !state->isFirstWord && !state->lastCharIsSpace) {
- os << " "; // Separate from previous word
- if (!state->prevWordIsEncoded && m_buffer[0] == ' ')
- wordEnc.getNextChunk(1);
-
+ os << sep; // Separate from previous word
++curLineLength;
state->lastCharIsSpace = true;
+ sepAdded = true;
+ }
+
+ if (sepAdded && leadingWSP) {
+ wordEnc.getNextChunk(1);
}
+ const bool infiniteLength = (ctx.getMaxLineLength() == lineLengthLimits::infinite);
+
for (unsigned int i = 0 ; ; ++i) {
// Compute the number of encoded chars that will fit on this line
- const size_t fit = maxLineLength2 - minWordLength
- - (i == 0 ? curLineLength : NEW_LINE_SEQUENCE_LENGTH);
+ const size_t used = (i == 0 ? curLineLength : NEW_LINE_SEQUENCE_LENGTH);
+ const size_t lineRoom = (maxLineLength2 > used) ? maxLineLength2 - used : 0;
+ const size_t wordRoom = std::min(lineRoom, maxEncodedWordLength);
+ const size_t fit = (wordRoom > minWordLength) ? wordRoom - minWordLength : 1;
// Get the next encoded chunk
const string chunk = wordEnc.getNextChunk(fit);
@@ -786,8 +1028,13 @@ void word::generate(
break;
}
- // Start a new encoded word
- if (i != 0) {
+ // Start a new encoded word, on the same line if there is no limit
+ if (i != 0 && infiniteLength) {
+
+ os << " ";
+ ++curLineLength;
+
+ } else if (i != 0) {
os << NEW_LINE_SEQUENCE;
curLineLength = NEW_LINE_SEQUENCE_LENGTH;
@@ -804,6 +1051,7 @@ void word::generate(
state->prevWordIsEncoded = true;
state->lastCharIsSpace = false;
+ state->lastCharIsBackslash = false;
}
}
diff --git a/src/vmime/word.hpp b/src/vmime/word.hpp
index e20fa908..57a5f6bc 100644
--- a/src/vmime/word.hpp
+++ b/src/vmime/word.hpp
@@ -169,13 +169,15 @@ class VMIME_EXPORT word : public headerFieldValue {
generatorState()
: isFirstWord(true),
prevWordIsEncoded(false),
- lastCharIsSpace(false) {
+ lastCharIsSpace(false),
+ lastCharIsBackslash(false) {
}
bool isFirstWord;
bool prevWordIsEncoded;
bool lastCharIsSpace;
+ bool lastCharIsBackslash;
};
class parserState {
diff --git a/src/vmime/wordEncoder.cpp b/src/vmime/wordEncoder.cpp
index 404a57b4..92b42ed1 100644
--- a/src/vmime/wordEncoder.cpp
+++ b/src/vmime/wordEncoder.cpp
@@ -147,7 +147,7 @@ const string wordEncoder::getNextChunk(const size_t maxLength) {
// bytes to encode knowing the maximum number of encoded chars. In
// Base64 encoding, 3 bytes of input provide 4 bytes of output.
const size_t inputCount =
- std::min(remaining, (maxLength > 1) ? ((maxLength - 1) * 3) / 4 : 1);
+ std::min(remaining, std::max(static_cast (1), (maxLength / 4) * 3));
// Encode chunk
utility::inputStreamStringAdapter in(m_buffer, m_pos, m_pos + inputCount);
@@ -163,12 +163,17 @@ const string wordEncoder::getNextChunk(const size_t maxLength) {
size_t inputCount = 0;
size_t outputCount = 0;
- while ((inputCount == 0 || outputCount < maxLength) && (inputCount < remaining)) {
+ while (inputCount < remaining) {
const unsigned char c = m_buffer[m_pos + inputCount];
+ const size_t len = utility::encoder::qpEncoder::RFC2047_getEncodedLength(c);
+
+ if (inputCount != 0 && outputCount + len > maxLength) {
+ break;
+ }
inputCount++;
- outputCount += utility::encoder::qpEncoder::RFC2047_getEncodedLength(c);
+ outputCount += len;
}
// Encode chunk
@@ -187,7 +192,7 @@ const string wordEncoder::getNextChunk(const size_t maxLength) {
size_t outputCount = 0;
string encodeBuffer;
- while ((inputCount == 0 || outputCount < maxLength) && (inputCount < remaining)) {
+ while (inputCount < remaining) {
// Get the next UTF8 character
const size_t inputCharLength =
@@ -202,25 +207,29 @@ const string wordEncoder::getNextChunk(const size_t maxLength) {
string encodeBytes;
conv->convert(inputChar, encodeBytes);
- encodeBuffer += encodeBytes;
-
// Compute number of output bytes
+ size_t newOutputCount = outputCount;
+
if (m_encoding == ENCODING_B64) {
- outputCount = std::max(
- static_cast (4),
- (encodeBuffer.length() * 4) / 3
- );
+ newOutputCount = ((encodeBuffer.length() + encodeBytes.length() + 2) / 3) * 4;
} else { // ENCODING_QP
for (size_t i = 0, n = encodeBytes.length() ; i < n ; ++i) {
const unsigned char c = encodeBytes[i];
- outputCount += utility::encoder::qpEncoder::RFC2047_getEncodedLength(c);
+ newOutputCount += utility::encoder::qpEncoder::RFC2047_getEncodedLength(c);
}
}
+ // Always take at least one character, so that we make progress
+ if (inputCount != 0 && newOutputCount > maxLength) {
+ break;
+ }
+
+ encodeBuffer += encodeBytes;
+ outputCount = newOutputCount;
inputCount += inputCharLength;
}
diff --git a/tests/parser/emailAddressTest.cpp b/tests/parser/emailAddressTest.cpp
index c755150f..f695a226 100644
--- a/tests/parser/emailAddressTest.cpp
+++ b/tests/parser/emailAddressTest.cpp
@@ -39,6 +39,8 @@ VMIME_TEST_SUITE_BEGIN(emailAddressTest)
VMIME_TEST(testParseCommentInDomainPart)
VMIME_TEST(testParseRFC2047EncodedLocalPart)
VMIME_TEST(testGenerateSpecialChars)
+ VMIME_TEST(testGenerateLongLocalPart)
+ VMIME_TEST(testGenerateASCIILocalPart)
VMIME_TEST_LIST_END
@@ -257,4 +259,56 @@ VMIME_TEST_SUITE_BEGIN(emailAddressTest)
);
}
+ void testGenerateLongLocalPart() {
+
+ // Neither encoded nor folded
+ const std::string local(80, 'a');
+ vmime::emailAddress e1(local, "example.com");
+
+ VASSERT_EQ("1", local + "@example.com", e1.generate(78, 4));
+ VASSERT_EQ("2", local + "@example.com", e1.generate());
+
+ vmime::emailAddress back;
+ back.parse(e1.generate(78, 4));
+ VASSERT_EQ("3", local, back.getLocalName().getBuffer());
+
+ // Non-ASCII without SMTPUTF8: one encoded-word, not folded
+ std::string local2;
+
+ for (int i = 0 ; i < 40 ; ++i) {
+ local2 += "\xc3\xa4";
+ }
+
+ const std::string out = vmime::emailAddress(local2, "example.com").generate(78, 4);
+
+ VASSERT_EQ("4", std::string::npos, out.find_first_of(" \r\n"));
+
+ back.parse(out);
+ VASSERT_EQ("5", local2, back.getLocalName().getConvertedText(vmime::charsets::UTF_8));
+ }
+
+ void testGenerateASCIILocalPart() {
+
+ // Plain ASCII is not encoded, even with '=' (eg. VERP) or "=?"
+ const char* locals[] = { "bounce-dGVzdA==", "abc=", "a=?b" };
+
+ for (const char* local : locals) {
+
+ vmime::emailAddress e(local, "example.com");
+ VASSERT_EQ(local, std::string(local) + "@example.com", e.generate());
+ }
+
+ vmime::emailAddress back;
+ back.parse(vmime::emailAddress("bounce-dGVzdA==", "example.com").generate());
+ VASSERT_EQ("parse", "bounce-dGVzdA==", back.getLocalName().getBuffer());
+
+ // 7-bit, but with escape sequences: still encoded
+ const vmime::word jp("\x1b$B$3$s$K$A$O\x1b(B", vmime::charset("iso-2022-jp"));
+ const std::string out =
+ vmime::emailAddress(jp, vmime::word("example.com", vmime::charsets::US_ASCII)).generate();
+
+ VASSERT("iso-2022-jp: " + out, out.compare(0, 16, "=?iso-2022-jp?B?") == 0);
+ VASSERT_EQ("no ESC", std::string::npos, out.find('\x1b'));
+ }
+
VMIME_TEST_SUITE_END
diff --git a/tests/parser/mailboxGroupTest.cpp b/tests/parser/mailboxGroupTest.cpp
index 00a7baad..5a8c781f 100644
--- a/tests/parser/mailboxGroupTest.cpp
+++ b/tests/parser/mailboxGroupTest.cpp
@@ -34,6 +34,7 @@ VMIME_TEST_SUITE_BEGIN(mailboxGroupTest)
VMIME_TEST(testEncodedEmptyGroup)
VMIME_TEST(testGroupInGroup)
VMIME_TEST(testBrokenGroup)
+ VMIME_TEST(testGenerateFolding)
VMIME_TEST_LIST_END
@@ -132,4 +133,37 @@ VMIME_TEST_SUITE_BEGIN(mailboxGroupTest)
VASSERT_EQ("count", 0, mgrp.getMailboxCount());
}
+ void testGenerateFolding() {
+
+ using namespace vmime;
+
+ mailboxGroup grp(text("Team", charsets::UTF_8));
+ grp.appendMailbox(make_shared (text("Alice Allison", charsets::UTF_8), emailAddress("alice@example.com")));
+ grp.appendMailbox(make_shared (text("Bob Builder", charsets::UTF_8), emailAddress("bob@example.com")));
+ grp.appendMailbox(make_shared (text("Doe, John", charsets::UTF_8), emailAddress("john@example.com")));
+ grp.appendMailbox(make_shared (emailAddress("first.person@example.com")));
+
+ // Whole mailboxes move to the next line, the fold replaces the
+ // space after the comma
+ VASSERT_EQ(
+ "1",
+ "Team: \"Alice Allison\" ,\r\n"
+ " \"Bob Builder\" , \"Doe, John\" ,\r\n"
+ " first.person@example.com;",
+ grp.generate(78, 4)
+ );
+
+ mailboxGroup back;
+ back.parse(grp.generate(78, 4));
+ VASSERT_EQ("2", 4, back.getMailboxCount());
+ VASSERT_EQ("3", "Doe, John", back.getMailboxAt(2)->getName().getWholeBuffer());
+
+ VASSERT_EQ(
+ "4",
+ "Team: \"Alice Allison\" , \"Bob Builder\" , "
+ "\"Doe, John\" , first.person@example.com;",
+ grp.generate()
+ );
+ }
+
VMIME_TEST_SUITE_END
diff --git a/tests/parser/mailboxListTest.cpp b/tests/parser/mailboxListTest.cpp
index 2036863d..407ee89d 100644
--- a/tests/parser/mailboxListTest.cpp
+++ b/tests/parser/mailboxListTest.cpp
@@ -29,6 +29,8 @@ VMIME_TEST_SUITE_BEGIN(mailboxListTest)
VMIME_TEST_LIST_BEGIN
VMIME_TEST(testParseGroup)
VMIME_TEST(testBrokenGroup)
+ VMIME_TEST(testGenerateFolding)
+ VMIME_TEST(testParseQuotedSpecials)
VMIME_TEST_LIST_END
@@ -59,4 +61,63 @@ VMIME_TEST_SUITE_BEGIN(mailboxListTest)
VASSERT_EQ("count", 0, mboxList.getMailboxCount());
}
+ void testGenerateFolding() {
+
+ using namespace vmime;
+
+ mailboxList ml;
+ ml.appendMailbox(make_shared (text("Alice Allison", charsets::UTF_8), emailAddress("alice@example.com")));
+ ml.appendMailbox(make_shared (text("Bob Builder", charsets::UTF_8), emailAddress("bob@example.com")));
+ ml.appendMailbox(make_shared (text("Doe, John", charsets::UTF_8), emailAddress("john@example.com")));
+ ml.appendMailbox(make_shared (text("O'Brien; Pat", charsets::UTF_8), emailAddress("pat@example.com")));
+ ml.appendMailbox(make_shared (text("Carol Smith", charsets::UTF_8), emailAddress("carol@example.com")));
+ ml.appendMailbox(make_shared (text("J\xc3\xbcrgen M\xc3\xbcller", charsets::UTF_8), emailAddress("juergen@example.com")));
+ ml.appendMailbox(make_shared (emailAddress("first.person@example.com")));
+
+ // Whole mailboxes move to the next line, the fold replaces the
+ // space after the comma
+ VASSERT_EQ(
+ "1",
+ "\"Alice Allison\" , \"Bob Builder\" ,\r\n"
+ " \"Doe, John\" , \"O'Brien; Pat\" ,\r\n"
+ " \"Carol Smith\" ,\r\n"
+ " =?utf-8?Q?J=C3=BCrgen_M=C3=BCller?= ,\r\n"
+ " first.person@example.com",
+ ml.generate(78, 4)
+ );
+
+ mailboxList back;
+ back.parse(ml.generate(78, 4));
+ VASSERT_EQ("2", 7, back.getMailboxCount());
+ VASSERT_EQ("3", "Doe, John", back.getMailboxAt(2)->getName().getWholeBuffer());
+
+ VASSERT_EQ(
+ "4",
+ "\"Alice Allison\" , \"Bob Builder\" , "
+ "\"Doe, John\" , \"O'Brien; Pat\" , "
+ "\"Carol Smith\" , "
+ "=?utf-8?Q?J=C3=BCrgen_M=C3=BCller?= , "
+ "first.person@example.com",
+ ml.generate()
+ );
+ }
+
+ void testParseQuotedSpecials() {
+
+ // '(' inside a quoted-string does not start a comment
+ vmime::mailboxList ml1;
+ ml1.parse("\"F\" , \"a(b\" , \"Z\" ");
+
+ VASSERT_EQ("1", 3, ml1.getMailboxCount());
+ VASSERT_EQ("2", "a(b", ml1.getMailboxAt(1)->getName().getWholeBuffer());
+ VASSERT_EQ("3", "r@x.com", ml1.getMailboxAt(1)->getEmail().generate());
+
+ // '"' inside a comment does not start a quoted-string
+ vmime::mailboxList ml2;
+ ml2.parse("(a\"b) , ");
+
+ VASSERT_EQ("4", 2, ml2.getMailboxCount());
+ VASSERT_EQ("5", "s@x.com", ml2.getMailboxAt(1)->getEmail().generate());
+ }
+
VMIME_TEST_SUITE_END
diff --git a/tests/parser/mailboxTest.cpp b/tests/parser/mailboxTest.cpp
index a186a5a7..a4154a5b 100644
--- a/tests/parser/mailboxTest.cpp
+++ b/tests/parser/mailboxTest.cpp
@@ -34,6 +34,10 @@ VMIME_TEST_SUITE_BEGIN(mailboxTest)
VMIME_TEST(testExcessiveQuoting)
VMIME_TEST(testSpacing)
VMIME_TEST(testInternationalizedEmail)
+ VMIME_TEST(testQuotedNameSpecials)
+ VMIME_TEST(testQuotedNameFolding)
+ VMIME_TEST(testFoldPoints)
+ VMIME_TEST(testFoldBeforeAddress)
VMIME_TEST_LIST_END
@@ -216,6 +220,127 @@ VMIME_TEST_SUITE_BEGIN(mailboxTest)
VASSERT_EQ("3", "\"J\xc3\xa4ger\" ", oss.str());
}
+ void testQuotedNameSpecials() {
+
+ using namespace vmime;
+
+ // '"' and '\' are escaped inside the quoted-string
+ mailbox m1(text("Meier, Hans \"Hansi\"", charsets::UTF_8), emailAddress("x@example.com"));
+ VASSERT_EQ("1", "\"Meier, Hans \\\"Hansi\\\"\" ", m1.generate());
+
+ mailbox m2(text("IT \\ Support", charsets::UTF_8), emailAddress("x@example.com"));
+ VASSERT_EQ("2", "\"IT \\\\ Support\" ", m2.generate());
+
+ mailbox back;
+ back.parse(m1.generate());
+ VASSERT_EQ("3", "Meier, Hans \"Hansi\"", back.getName().getWholeBuffer());
+ back.parse(m2.generate());
+ VASSERT_EQ("4", "IT \\ Support", back.getName().getWholeBuffer());
+ }
+
+ void testQuotedNameFolding() {
+
+ using namespace vmime;
+
+ generationContext ctx;
+ ctx.setMaxLineLength(78);
+
+ // Fits exactly
+ std::string out;
+ utility::outputStreamStringAdapter os(out);
+ word("Doe, John", charsets::US_ASCII).generate(ctx, os, 67, NULL, text::QUOTE_IF_POSSIBLE, NULL);
+ VASSERT_EQ("1", "\"Doe, John\"", out);
+
+ // Does not fit: the quotes are kept, the fold goes inside
+ mailbox m1(text("Doe, John", charsets::UTF_8), emailAddress("john@example.com"));
+ VASSERT_EQ("2", "\"Doe,\r\n John\" ", m1.generate(78, 68));
+
+ mailbox back;
+ back.parse(m1.generate(78, 68));
+ VASSERT_EQ("3", "Doe, John", back.getName().getWholeBuffer());
+
+ // Longer than a line
+ const std::string name =
+ "Very Long Department Name, With Commas, And More Commas, Until It Is Longer Than A Line";
+ mailbox m2(text(name, charsets::UTF_8), emailAddress("lc@example.com"));
+ VASSERT_EQ(
+ "4",
+ "\"Very Long Department Name, With Commas, And More Commas, Until It Is\r\n"
+ " Longer Than A Line\" ",
+ m2.generate(78, 4)
+ );
+
+ back.parse(m2.generate(78, 4));
+ VASSERT_EQ("5", name, back.getName().getWholeBuffer());
+
+ // A double space is only kept inside quotes
+ mailbox m3(text("John Doe Smith", charsets::UTF_8), emailAddress("js@example.com"));
+ VASSERT_EQ("6", "\"John Doe\r\n Smith\" ", m3.generate(78, 66));
+
+ // Atoms may still be folded without quotes
+ mailbox m4(text("Bob Builder", charsets::UTF_8), emailAddress("bob@example.com"));
+ VASSERT_EQ("7", "Bob\r\n Builder ", m4.generate(78, 72));
+ }
+
+ void testFoldPoints() {
+
+ using namespace vmime;
+
+ // No fold after '\' or inside a white-space run, which a lenient
+ // parser would not unfold to the same text
+ const char* names[] = {
+ "IT \\ Support Department of the Central Administration Office",
+ "Doe, John Central Office Team North and South and East and West",
+ };
+
+ generationContext ctx;
+ ctx.setMaxLineLength(78);
+
+ for (const char* name : names) {
+
+ for (size_t col = 4 ; col < 78 ; ++col) {
+
+ std::string out;
+ utility::outputStreamStringAdapter os(out);
+ text(name, charsets::UTF_8).encodeAndFold(ctx, os, col, NULL, text::QUOTE_IF_POSSIBLE);
+
+ VASSERT(out, out.find("\\\r\n") == std::string::npos);
+ VASSERT(out, out.find(" \r\n") == std::string::npos);
+ VASSERT(out, out.find("\r\n ") == std::string::npos);
+
+ mailbox back;
+ back.parse(out + " ");
+ VASSERT_EQ(out, name, back.getName().getWholeBuffer());
+ }
+ }
+ }
+
+ void testFoldBeforeAddress() {
+
+ using namespace vmime;
+
+ // The fold replaces the space before "<"
+ mailbox m1(text("Carol Smith-Jones", charsets::UTF_8), emailAddress("carol.smith-jones@example.com"));
+ VASSERT_EQ("1", "\"Carol Smith-Jones\"\r\n ", m1.generate(78, 30));
+
+ // A bare address is folded only if it then fits
+ generationContext ctx;
+ ctx.setMaxLineLength(78);
+
+ std::string out;
+ utility::outputStreamStringAdapter os(out);
+ size_t pos = 0;
+
+ mailbox m2(emailAddress("first.person@example.com"));
+ m2.generate(ctx, os, 60, &pos);
+ VASSERT_EQ("2", "\r\n first.person@example.com", out);
+ VASSERT_EQ("3", 25, pos);
+
+ const std::string addr = std::string(70, 'x') + "@example.com";
+ mailbox m3((emailAddress(addr)));
+ VASSERT_EQ("4", addr, m3.generate(78, 4));
+ }
+
void testAPI() {
auto take_string_directly = [](vmime::string &&) {};
diff --git a/tests/parser/messageIdSequenceTest.cpp b/tests/parser/messageIdSequenceTest.cpp
index 0237108d..ded6e275 100644
--- a/tests/parser/messageIdSequenceTest.cpp
+++ b/tests/parser/messageIdSequenceTest.cpp
@@ -29,6 +29,7 @@ VMIME_TEST_SUITE_BEGIN(messageIdSequenceTest)
VMIME_TEST_LIST_BEGIN
VMIME_TEST(testParse)
VMIME_TEST(testGenerate)
+ VMIME_TEST(testGenerateFolding)
VMIME_TEST_LIST_END
@@ -75,4 +76,35 @@ VMIME_TEST_SUITE_BEGIN(messageIdSequenceTest)
VASSERT_EQ("2", " ", s2.generate());
}
+ void testGenerateFolding() {
+
+ vmime::messageIdSequence s;
+ s.appendMessageId(vmime::make_shared ("first.id.1234567890", "example.com"));
+ s.appendMessageId(vmime::make_shared ("second.id.1234567890", "example.com"));
+ s.appendMessageId(vmime::make_shared ("third.id.1234567890", "example.com"));
+
+ // No white-space before the fold
+ VASSERT_EQ(
+ "1",
+ " \r\n"
+ " ",
+ s.generate(78, 5)
+ );
+
+ vmime::generationContext ctx;
+ ctx.setMaxLineLength(78);
+ ctx.setWrapMessageId(false);
+
+ std::string out;
+ vmime::utility::outputStreamStringAdapter os(out);
+ s.generate(ctx, os, 5);
+
+ VASSERT_EQ(
+ "2",
+ " "
+ " ",
+ out
+ );
+ }
+
VMIME_TEST_SUITE_END
diff --git a/tests/parser/parameterTest.cpp b/tests/parser/parameterTest.cpp
index 68a987ad..08640dd2 100644
--- a/tests/parser/parameterTest.cpp
+++ b/tests/parser/parameterTest.cpp
@@ -405,20 +405,20 @@ VMIME_TEST_SUITE_BEGIN(parameterTest)
VASSERT_EQ(
"3.7bit-only",
"F: X; \r\n "
- "param1=\"=?utf-8?B?zrTPg8+Dz4PPg8+Dz4PPg8+Dz4PPg8+Dz4PPg8+Dz4PPg8+Dz4PPg8+DzrTPg860?=\r\n "
- "=?utf-8?B?zrEgzrTPg86xzrTPg860z4POsc60z4POsc60zrHPg860zrHPgyDOtM+DzrHOtM6x?=\r\n "
- "=?utf-8?B?z4POtM+DzrEgzrTPg86xzrTOsc+DzrTPg86xIM60zrHPg860zrHPg860zrHPgyDOtA==?=\r\n "
- "=?utf-8?B?zrHPg86xz4fPhs60zrTPg86xIDIwMDguZG9j?=\"",
+ "param1=\"=?utf-8?B?zrTPg8+Dz4PPg8+Dz4PPg8+Dz4PPg8+Dz4PPg8+Dz4PPg8+Dz4PPg8+DzrQ=?=\r\n "
+ "=?utf-8?B?z4POtM6xIM60z4POsc60z4POtM+DzrHOtM+DzrHOtM6xz4POtM6xz4MgzrQ=?=\r\n "
+ "=?utf-8?B?z4POsc60zrHPg860z4POsSDOtM+DzrHOtM6xz4POtM+DzrEgzrTOsc+DzrQ=?=\r\n "
+ "=?utf-8?B?zrHPg860zrHPgyDOtM6xz4POsc+Hz4bOtM60z4POsSAyMDA4LmRvYw==?=\"",
p3.generate(vmime::generationContext::PARAMETER_VALUE_RFC2047_ONLY, 80) // max line length = 80
);
VASSERT_EQ(
"3.both",
"F: X; \r\n "
- "param1=\"=?utf-8?B?zrTPg8+Dz4PPg8+Dz4PPg8+Dz4PPg8+Dz4PPg8+Dz4PPg8+Dz4PPg8+DzrTPg860?=\r\n "
- "=?utf-8?B?zrEgzrTPg86xzrTPg860z4POsc60z4POsc60zrHPg860zrHPgyDOtM+DzrHOtM6x?=\r\n "
- "=?utf-8?B?z4POtM+DzrEgzrTPg86xzrTOsc+DzrTPg86xIM60zrHPg860zrHPg860zrHPgyDOtA==?=\r\n "
- "=?utf-8?B?zrHPg86xz4fPhs60zrTPg86xIDIwMDguZG9j?=\";\r\n "
+ "param1=\"=?utf-8?B?zrTPg8+Dz4PPg8+Dz4PPg8+Dz4PPg8+Dz4PPg8+Dz4PPg8+Dz4PPg8+DzrQ=?=\r\n "
+ "=?utf-8?B?z4POtM6xIM60z4POsc60z4POtM+DzrHOtM+DzrHOtM6xz4POtM6xz4MgzrQ=?=\r\n "
+ "=?utf-8?B?z4POsc60zrHPg860z4POsSDOtM+DzrHOtM6xz4POtM+DzrEgzrTOsc+DzrQ=?=\r\n "
+ "=?utf-8?B?zrHPg860zrHPgyDOtM6xz4POsc+Hz4bOtM60z4POsSAyMDA4LmRvYw==?=\";\r\n "
"param1*0*=utf-8''%CE%B4%CF%83%CF%83%CF%83%CF%83%CF%83%CF%83%CF%83%CF%83%CF%83;\r\n "
"param1*1*=%CF%83%CF%83%CF%83%CF%83%CF%83%CF%83%CF%83%CF%83%CF%83%CF%83%CF%83;\r\n "
"param1*2*=%CE%B4%CF%83%CE%B4%CE%B1%20%CE%B4%CF%83%CE%B1%CE%B4%CF%83%CE%B4%CF;\r\n "
diff --git a/tests/parser/textTest.cpp b/tests/parser/textTest.cpp
index 13e498e1..5284d554 100644
--- a/tests/parser/textTest.cpp
+++ b/tests/parser/textTest.cpp
@@ -41,14 +41,19 @@ VMIME_TEST_SUITE_BEGIN(textTest)
VMIME_TEST(testWordGenerateSpace)
VMIME_TEST(testWordGenerateSpace2)
VMIME_TEST(testWordGenerateMultiBytes)
+ VMIME_TEST(testEncodedWordLength)
VMIME_TEST(testWordGenerateQuote)
VMIME_TEST(testWordGenerateSpecialCharsets)
VMIME_TEST(testWordGenerateSpecials)
VMIME_TEST(testWhitespace)
VMIME_TEST(testWhitespaceMBox)
+ VMIME_TEST(testWhitespaceAfterPlainWord)
+ VMIME_TEST(testWhitespaceAroundFold)
VMIME_TEST(testFoldingAscii)
+ VMIME_TEST(testFoldPoints)
+ VMIME_TEST(testFoldWhitespaceRuns)
VMIME_TEST(testForcedNonEncoding)
VMIME_TEST(testBugFix20110511)
@@ -84,6 +89,36 @@ VMIME_TEST_SUITE_BEGIN(textTest)
return res;
}
+ // RFC 2047: encoded-words of at most 75 chars, lines of at most 76
+ static void checkEncodedWordLength(
+ const std::string& id,
+ const std::string& str,
+ const size_t maxLineLength
+ ) {
+
+ for (size_t start = 0 ; ; ) {
+
+ const size_t end = str.find("\r\n", start);
+ const std::string line = str.substr(start, end == std::string::npos ? end : end - start);
+
+ VASSERT(id + ": line: " + line, line.length() <= maxLineLength);
+
+ for (size_t p = 0 ; (p = line.find("=?", p)) != std::string::npos ; ) {
+
+ const size_t e = line.find("?=", line.find('?', line.find('?', p + 2) + 1) + 1);
+
+ VASSERT(id + ": word: " + line, e != std::string::npos && e + 2 - p <= 75);
+ p = e + 2;
+ }
+
+ if (end == std::string::npos) {
+ break;
+ }
+
+ start = end + 2;
+ }
+ }
+
void testConstructors() {
@@ -417,15 +452,60 @@ VMIME_TEST_SUITE_BEGIN(textTest)
)
);
+ // "aaa=C3" fits, but not the whole character
VASSERT_EQ(
"2",
+ "=?utf-8?Q?aaa?==?utf-8?Q?=C3=A9?==?utf-8?Q?zzz?=",
+ cleanGeneratedWords(
+ vmime::word("aaa\xc3\xa9zzz", vmime::charset("utf-8")).generate(19)
+ )
+ );
+
+ VASSERT_EQ(
+ "3",
"=?utf-8?Q?aaa=C3=A9?==?utf-8?Q?zzz?=",
cleanGeneratedWords(
- vmime::word("aaa\xc3\xa9zzz", vmime::charset("utf-8")).generate(17)
+ vmime::word("aaa\xc3\xa9zzz", vmime::charset("utf-8")).generate(22)
)
);
}
+ void testEncodedWordLength() {
+
+ std::string in;
+
+ for (int i = 0 ; i < 60 ; ++i) {
+ in += "\xc3\xbc\xf0\x9f\x98\x80" "ab";
+ }
+
+ const vmime::word w(in, vmime::charset("utf-8"));
+ const size_t lengths[] = { 50, 76, 78, 100, vmime::lineLengthLimits::infinite };
+
+ for (const size_t maxLen : lengths) {
+
+ for (size_t col = 0 ; col < 10 ; ++col) {
+
+ const std::string id = std::to_string(maxLen) + "/" + std::to_string(col);
+ const std::string out = w.generate(maxLen, col);
+
+ if (maxLen == vmime::lineLengthLimits::infinite) {
+
+ checkEncodedWordLength(id, out, maxLen);
+ VASSERT_EQ(id + ": no fold", std::string::npos, out.find('\r'));
+
+ } else {
+
+ checkEncodedWordLength(id, std::string(col, 'x') + out, std::min(maxLen, size_t(76)));
+ }
+
+ vmime::text back;
+ back.parse(out);
+
+ VASSERT_EQ(id, in, back.getWholeBuffer());
+ }
+ }
+ }
+
void testWordGenerateQuote() {
std::string str;
@@ -536,6 +616,83 @@ VMIME_TEST_SUITE_BEGIN(textTest)
VASSERT_EQ("parse.email", "me@vmime.org", mbox.getEmail());
}
+ void testWhitespaceAfterPlainWord() {
+
+ // The space before an encoded word which follows an unencoded
+ // word is not encoded a second time
+ VASSERT_EQ(
+ "1",
+ "=?utf-8?B?R3LDvMOfZQ==?= aus =?utf-8?Q?K=C3=B6ln?=",
+ vmime::text("Gr\xc3\xbc\xc3\x9f" "e aus K\xc3\xb6ln", vmime::charsets::UTF_8).generate()
+ );
+
+ const std::string subject =
+ "Re: AW: Angebot, Lieferung und Montage im Werk S\xc3\xbc" "d, "
+ "Termin n\xc3\xa4" "chste Woche bitte best\xc3\xa4tigen";
+
+ vmime::text back;
+ back.parse(vmime::text(subject, vmime::charsets::UTF_8).generate());
+
+ VASSERT_EQ("2", subject, back.getWholeBuffer());
+
+ // Neither is a leading tab, nor is a space added
+ vmime::text t;
+ t.appendWord(vmime::make_shared ("foo@", vmime::charset("us-ascii")));
+ t.appendWord(vmime::make_shared ("\t\xc3\xa4", vmime::charset("utf-8")));
+
+ VASSERT_EQ("3", "foo@\t=?utf-8?B?w6Q=?=", t.generate());
+
+ back.parse(t.generate());
+ VASSERT_EQ("4", "foo@\t\xc3\xa4", back.getConvertedText(vmime::charsets::UTF_8));
+ }
+
+ void testWhitespaceAroundFold() {
+
+ // The fold before an encoded word stands for its leading space
+ vmime::text t1;
+ t1.appendWord(vmime::make_shared ("Termin", vmime::charset("us-ascii")));
+ t1.appendWord(vmime::make_shared (" n\xc3\xa4" "chste", vmime::charset("utf-8")));
+
+ VASSERT_EQ("1", "Termin\r\n =?utf-8?Q?n=C3=A4chste?=", t1.generate(78, 52));
+
+ // An empty word writes nothing, the space stays encoded
+ vmime::text e1, e2;
+ e1.parse("=?utf-8?Q?=C3=A4?= =?us-ascii?Q?" "?= =?utf-8?Q?_=C3=B6?=");
+ e2.parse(e1.generate());
+
+ VASSERT_EQ("empty", "\xc3\xa4 \xc3\xb6", e2.getConvertedText(vmime::charsets::UTF_8));
+
+ const std::string subject =
+ "Re: AW: Angebot, Lieferung und Montage der neuen Anlage im Werk Nord und S\xc3\xbc" "d, "
+ "Termin n\xc3\xa4" "chste Woche bitte best\xc3\xa4tigen";
+
+ vmime::text back;
+ back.parse(vmime::text(subject, vmime::charsets::UTF_8).generate(78, 9));
+
+ VASSERT_EQ("2", subject, back.getWholeBuffer());
+
+ // No separator in addition to the leading space of an unencoded word
+ const vmime::text t2(
+ "\xc3\x9c" "berschrift one two three four five six seven eight nine ten"
+ " eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen",
+ vmime::charsets::UTF_8
+ );
+
+ VASSERT_EQ(
+ "3",
+ "=?utf-8?Q?=C3=9Cberschrift?= one two three four five six seven eight\r\n"
+ " nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen",
+ t2.generate(78, 9)
+ );
+
+ // The separator after an encoded word counts for the line length
+ vmime::text t3;
+ t3.appendWord(vmime::make_shared ("\xc3\xa4", vmime::charset("utf-8")));
+ t3.appendWord(vmime::make_shared (std::string(60, 'b') + " c", vmime::charset("us-ascii")));
+
+ VASSERT_EQ("4", "=?utf-8?B?w6Q=?= " + std::string(60, 'b') + "\r\n c", t3.generate(78, 0));
+ }
+
void testFoldingAscii() {
// In this test, no encoding is needed, but line should be folded anyway
@@ -550,6 +707,148 @@ VMIME_TEST_SUITE_BEGIN(textTest)
);
}
+ void testFoldPoints() {
+
+ // No fold after '\' if there is another fold point, nor inside a
+ // white-space run
+ const std::string in =
+ "Copy C:\\ Temp and D:\\ Data to the backup share on the file server,"
+ " then check the logs";
+
+ for (size_t col = 0 ; col < 78 ; ++col) {
+
+ const std::string out = vmime::text(in, vmime::charsets::UTF_8).generate(78, col);
+
+ VASSERT(out, out.find("\\\r\n") == std::string::npos);
+ VASSERT(out, out.find(" \r\n") == std::string::npos);
+ VASSERT(out, out.find("\r\n ") == std::string::npos);
+
+ vmime::text back;
+ back.parse(out);
+ VASSERT_EQ(out, in, back.getWholeBuffer());
+ }
+
+ // Neither before the next word, unless "Copy C:\ Temp" does not fit
+ vmime::text t;
+ t.appendWord(vmime::make_shared ("Copy C:\\", vmime::charset("us-ascii")));
+ t.appendWord(vmime::make_shared (" Temp to the backup share", vmime::charset("us-ascii")));
+
+ for (size_t col = 0 ; col < 78 ; ++col) {
+
+ const std::string out = t.generate(78, col);
+
+ if (col + 13 < 78) {
+ VASSERT(out, out.find("\\\r\n") == std::string::npos);
+ }
+
+ // Only a first line without a fold point may be longer
+ for (size_t start = 0, offset = col ; ; offset = 0) {
+
+ const size_t eol = out.find("\r\n", start);
+ const std::string line = out.substr(start, eol == std::string::npos ? eol : eol - start);
+
+ VASSERT(out, offset + line.length() <= 78 || (start == 0 && line.find(' ') == std::string::npos));
+
+ if (eol == std::string::npos) {
+ break;
+ }
+
+ start = eol + 2;
+ }
+ }
+
+ // Before an encoded-word, rather than going past 76 chars (RFC 2047)
+ vmime::text t2;
+ t2.appendWord(vmime::make_shared (
+ std::string(50, 'a') + " C:\\", vmime::charset("us-ascii")));
+ t2.appendWord(vmime::make_shared (
+ " \xc3\xa4nderungen \xc3\xa4nderungen", vmime::charsets::UTF_8));
+
+ VASSERT_EQ(
+ "encoded",
+ std::string(50, 'a') + " C:\\\r\n =?utf-8?Q?=C3=A4nderungen_=C3=A4nderungen?=",
+ t2.generate(78, 20)
+ );
+ }
+
+ void testFoldWhitespaceRuns() {
+
+ // Without a single space, fold before the last space of a run
+ VASSERT_EQ(
+ "1",
+ "Invoice 12345 from ACME Corp due 2026-09-30 please pay \r\n"
+ " promptly thanks a lot",
+ vmime::text(
+ "Invoice 12345 from ACME Corp due 2026-09-30 please pay"
+ " promptly thanks a lot", vmime::charset("us-ascii")
+ ).generate(78, 9)
+ );
+
+ // Without a space, fold before a tab, which then starts the next line
+ VASSERT_EQ(
+ "2",
+ "Col1\tCol2\tCol3\tCol4\tCol5\tCol6\tCol7\tCol8\tCol9\tCol10\tCol11\tCol12\tCol13\r\n"
+ "\tCol14",
+ vmime::text(
+ "Col1\tCol2\tCol3\tCol4\tCol5\tCol6\tCol7\tCol8\tCol9\tCol10"
+ "\tCol11\tCol12\tCol13\tCol14", vmime::charset("us-ascii")
+ ).generate(78, 9)
+ );
+
+ // A first word which does not fit on the line is not encoded
+ const std::string url =
+ "https://www.example.com/a/rather/long/path/to/the/page.html?id=123456789";
+
+ VASSERT("does not fit", 9 + url.length() > 78 && url.length() < 75);
+ VASSERT_EQ("3", url, vmime::text(url, vmime::charset("us-ascii")).generate(78, 9));
+
+ // Unencoded, fold before the last space of a run, or before a tab
+ std::string spaces = "tok", tabs = "tok";
+
+ for (int i = 1 ; spaces.length() < 1200 ; ++i) {
+
+ spaces += " tok" + std::to_string(i);
+ tabs += "\ttok" + std::to_string(i);
+ }
+
+ const std::string ins[] = { spaces, tabs };
+
+ for (int i = 0 ; i < 2 ; ++i) {
+
+ vmime::generationContext ctx;
+ ctx.setMaxLineLength(78);
+
+ std::string out;
+ vmime::utility::outputStreamStringAdapter os(out);
+
+ vmime::text(ins[i], vmime::charset("us-ascii"))
+ .encodeAndFold(ctx, os, 10, NULL, vmime::text::FORCE_NO_ENCODING);
+
+ std::string unfolded;
+ size_t lineLength = 10;
+
+ for (size_t j = 0 ; j < out.length() ; ++j) {
+
+ if (out[j] == '\r' && out[j + 1] == '\n') {
+
+ VASSERT(out, lineLength <= 78);
+ VASSERT(out, out.compare(j + 2, 2, " ") != 0);
+
+ lineLength = 0;
+ ++j;
+
+ } else {
+
+ unfolded += out[j];
+ ++lineLength;
+ }
+ }
+
+ VASSERT(out, lineLength <= 78);
+ VASSERT_EQ("unfolded", ins[i], unfolded);
+ }
+ }
+
void testForcedNonEncoding() {
// Testing long unbreakable and unencodable header
diff --git a/tests/parser/wordEncoderTest.cpp b/tests/parser/wordEncoderTest.cpp
index 0c827bff..cde0996f 100644
--- a/tests/parser/wordEncoderTest.cpp
+++ b/tests/parser/wordEncoderTest.cpp
@@ -31,6 +31,8 @@ VMIME_TEST_SUITE_BEGIN(wordEncoderTest)
VMIME_TEST_LIST_BEGIN
VMIME_TEST(testGetNextChunk)
VMIME_TEST(testGetNextChunk_integral)
+ VMIME_TEST(testGetNextChunk_maxLength)
+ VMIME_TEST(testGetNextChunk_simple)
VMIME_TEST(testIsEncodingNeeded_ascii)
VMIME_TEST(testIsEncodingNeeded_withLanguage)
VMIME_TEST(testIsEncodingNeeded_specialChars)
@@ -64,8 +66,42 @@ VMIME_TEST_SUITE_BEGIN(wordEncoderTest)
vmime::wordEncoder::ENCODING_AUTO
);
- VASSERT_EQ("1", "buffer=C3=A0", we.getNextChunk(7));
- VASSERT_EQ("2", "plop", we.getNextChunk(10));
+ VASSERT_EQ("1", "buffer", we.getNextChunk(7));
+ VASSERT_EQ("2", "=C3=A0", we.getNextChunk(1));
+ VASSERT_EQ("3", "plop", we.getNextChunk(10));
+ }
+
+ void testGetNextChunk_maxLength() {
+
+ vmime::wordEncoder we(
+ "\xc3\xa4\xc3\xa4\xc3\xa4\xc3\xa4\xc3\xa4",
+ vmime::charset("utf-8"),
+ vmime::wordEncoder::ENCODING_B64
+ );
+
+ VASSERT_EQ("1", "w6TDpMOk", we.getNextChunk(10));
+ VASSERT_EQ("2", "w6TDpA==", we.getNextChunk(10));
+ }
+
+ void testGetNextChunk_simple() {
+
+ // Unknown charset: bytes are encoded as they are
+ vmime::wordEncoder we1(
+ "abcdefghij",
+ vmime::charset("x-unknown-charset"),
+ vmime::wordEncoder::ENCODING_B64
+ );
+
+ VASSERT_EQ("1", "YWJj", we1.getNextChunk(7));
+
+ vmime::wordEncoder we2(
+ "abcdef\xffghi",
+ vmime::charset("x-unknown-charset"),
+ vmime::wordEncoder::ENCODING_QP
+ );
+
+ VASSERT_EQ("2", "abcdef", we2.getNextChunk(7));
+ VASSERT_EQ("3", "=FFghi", we2.getNextChunk(7));
}
void testIsEncodingNeeded_ascii() {