2 * coded by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
3 * Understanding is not required. Only obedience.
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, version 3 of the License ONLY.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 module chibackend
.decode
is aliced
;
24 import iv
.utfutil
: utf8CodeLen
, utf8Valid
;
28 // ////////////////////////////////////////////////////////////////////////// //
31 public byte utf8CodeLen (char ch) pure nothrow @trusted @nogc {
32 //pragma(inline, true);
33 if (ch < 0x80) return 1;
34 if ((ch&0b1111_1110) == 0b1111_1100) return 6;
35 if ((ch&0b1111_1100) == 0b1111_1000) return 5;
36 if ((ch&0b1111_1000) == 0b1111_0000) return 4;
37 if ((ch&0b1111_0000) == 0b1110_0000) return 3;
38 if ((ch&0b1110_0000) == 0b1100_0000) return 2;
43 // ////////////////////////////////////////////////////////////////////////// //
44 public bool utf8Valid (const(void)[] buf) pure nothrow @trusted @nogc {
45 const(ubyte)* bp = cast(const(ubyte)*)buf.ptr;
46 auto left = buf.length;
48 auto len = utf8CodeLen(*bp++)-1;
49 if (len < 0 || len > left) return false;
51 while (len-- > 0) if (((*bp++)&0b1100_0000) != 0b1000_0000) return false;
58 // ////////////////////////////////////////////////////////////////////////// //
59 public bool isValidNickUniChar (immutable dchar ch
) pure nothrow @safe @nogc {
62 (ch
>= '0' && ch
<= '9') ||
63 (ch
>= 'A' && ch
<= 'Z') ||
64 (ch
>= 'a' && ch
<= 'z') ||
65 ch
== '-' || ch
== '_' || ch
== '.' ||
66 isValidCyrillicUni(ch
);
70 public bool isValidUTFNick (const(char)[] s
) nothrow @safe @nogc {
71 if (s
.length
== 0) return false;
73 foreach (immutable char ch
; s
) {
74 dc
.decode(cast(ubyte)ch
);
75 if (dc
.invalid
) return false;
76 if (dc
.complete
&& !isValidNickUniChar(dc
.codepoint
)) return false;
82 // ////////////////////////////////////////////////////////////////////////// //
83 public bool isGoodCtlChar (immutable char ch
) pure nothrow @safe @nogc {
85 return (ch
== '\t' || ch
== '\n');
89 // ////////////////////////////////////////////////////////////////////////// //
90 public bool isGoodText (const(char)[] buf
) pure nothrow @trusted @nogc {
91 foreach (immutable char ch
; buf
) {
92 if (ch
== 127 ||
(ch
< 32 && !isGoodCtlChar(ch
))) return false;
98 // ////////////////////////////////////////////////////////////////////////// //
99 private bool isGoodFileNameChar (immutable char ch
) pure nothrow @safe @nogc {
100 if (ch
<= 32 || ch
== 127) return false;
101 if (ch
>= 128) return true;
102 if (ch
== '/' || ch
== '\\') return false;
107 // ////////////////////////////////////////////////////////////////////////// //
108 // this also sanitizes it
109 public T
toLowerStr (T
:const(char)[]) (T s
) nothrow @trusted {
110 static if (is(T
== typeof(null))) {
113 bool needwork
= false;
114 foreach (immutable char ch
; s
) {
115 if (ch
== 127 ||
(ch
< 32 && !isGoodCtlChar(ch
)) ||
(ch
>= 'A' && ch
<= 'Z')) {
124 res
.reserve(s
.length
);
125 foreach (immutable idx
, char ch
; s
) {
126 if (ch
== 13) { if (idx
+1 >= s
.length || s
.ptr
[idx
+1] != 10) res
~= '\n'; }
127 else if (ch
< 32 && !isGoodCtlChar(ch
)) res
~= ' ';
128 else if (ch
== 127) res
~= '~';
129 else if (ch
>= 'A' && ch
<= 'Z') res
~= ch
.tolower
;
132 return cast(T
)res
; // it is safe to cast here
138 // ////////////////////////////////////////////////////////////////////////// //
139 // this also sanitizes it
140 public T
sanitizeFileNameStr (T
:const(char)[]) (T s
) nothrow @trusted {
141 static if (is(T
== typeof(null))) {
144 bool needwork
= false;
145 foreach (immutable char ch
; s
) if (!isGoodFileNameChar(ch
)) { needwork
= true; break; }
149 char[] res
= new char[s
.length
];
151 foreach (ref char ch
; res
) {
152 if (!isGoodFileNameChar(ch
)) ch
= '_';
154 return cast(T
)res
; // it is safe to cast here
160 // ////////////////////////////////////////////////////////////////////////// //
161 public T
sanitizeStr (T
:const(char)[]) (T s
) nothrow @trusted {
162 static if (is(T
== typeof(null))) {
169 res
.reserve(s
.length
);
170 foreach (immutable idx
, char ch
; s
) {
171 if (ch
== 13) { if (idx
+1 >= s
.length || s
.ptr
[idx
+1] != 10) res
~= '\n'; }
172 else if (ch
< 32 && !isGoodCtlChar(ch
)) res
~= ' ';
173 else if (ch
== 127) res
~= '~';
176 return cast(T
)res
; // it is safe to cast here
182 // ////////////////////////////////////////////////////////////////////////// //
183 public T
sanitizeStrLine (T
:const(char)[]) (T s
) nothrow @trusted {
184 static if (is(T
== typeof(null))) {
188 foreach (immutable idx
, char ch
; s
) {
189 if (ch
< 32 || ch
== 127) { found
= true; break; }
190 if (ch
== 32 && (idx
== 0 || s
.ptr
[idx
-1] <= 32)) { found
= true; break; }
196 res
.reserve(s
.length
);
197 foreach (char ch
; s
) {
198 if (ch
< 32 || ch
== 127) ch
= ' ';
199 if (ch
<= 32 && (res
.length
== 0 || res
[$-1] <= 32)) continue;
202 while (res
.length
&& res
[$-1] <= 32) res
= res
[0..$-1];
203 return cast(T
)res
; // it is safe to cast here
209 // ////////////////////////////////////////////////////////////////////////// //
210 // for decoded subject parts
211 public T
sanitizeStrSubjPart (T
:const(char)[]) (T s
) nothrow @trusted {
212 static if (is(T
== typeof(null))) {
216 foreach (immutable idx
, immutable char ch
; s
) {
217 if (ch
< 32 || ch
== 127 || ch
== '_') { found
= true; break; }
222 char[] res
= new char[s
.length
];
224 foreach (ref char ch
; res
) if (ch
< 32 || ch
== 127 || ch
== '_') ch
= ' ';
225 return cast(T
)res
; // it is safe to cast here
231 // ////////////////////////////////////////////////////////////////////////// //
232 // this also sanitizes it
233 public T
binaryToUtf8 (T
:const(char)[]) (T s
) nothrow @trusted {
234 static if (is(T
== typeof(null))) {
238 foreach (immutable char ch
; s
) {
239 if (ch
>= 127 ||
(ch
< 32 && !isGoodCtlChar(ch
))) { found
= true; break; }
244 import iv
.utfutil
: utf8Valid
;
245 if (utf8Valid(s
)) return sanitizeStr(s
);
250 foreach (immutable char ch
; s
) {
252 immutable int len
= utf8Encode(uc
[], cast(dchar)ch
);
258 foreach (immutable idx
, char ch
; s
) {
260 if (ch
== 13) { if (idx
+1 >= s
.length || s
.ptr
[idx
+1] != 10) res
~= '\n'; }
261 else if (ch
< 32 && !isGoodCtlChar(ch
)) res
~= ' ';
262 else if (ch
== 127) res
~= '~';
265 immutable int len
= utf8Encode(uc
[], cast(dchar)ch
);
270 return cast(T
)res
; // it is safe to cast here
276 // ////////////////////////////////////////////////////////////////////////// //
277 // this also sanitizes it
278 public T
utf8ToUtf8 (T
:const(char)[]) (T s
) nothrow @trusted {
279 static if (is(T
== typeof(null))) {
283 foreach (immutable char ch
; s
) {
284 if (ch
>= 127 ||
(ch
< 32 && !isGoodCtlChar(ch
))) { found
= true; break; }
289 import iv
.utfutil
: utf8Valid
;
290 if (utf8Valid(s
)) return sanitizeStr(s
);
293 res
.reserve(s
.length
);
295 foreach (immutable idx
, char ch
; s
) {
296 if (utfleft
) { --utfleft
; res
~= ch
; continue; }
298 if (ch
== 13) { if (idx
+1 >= s
.length || s
.ptr
[idx
+1] != 10) res
~= '\n'; }
299 else if (ch
< 32 && !isGoodCtlChar(ch
)) res
~= ' ';
300 else if (ch
== 127) res
~= '~';
303 immutable byte ulen
= utf8CodeLen(ch
);
304 if (ulen
< 1) { res
~= '?'; continue; }
305 if (s
.length
-idx
< ulen
) { res
~= '?'; break; }
306 if (!utf8Valid(s
[idx
..idx
+ulen
])) { res
~= '?'; continue; }
311 return cast(T
)res
; // it is safe to cast here
317 // ////////////////////////////////////////////////////////////////////////// //
318 public T
subjRemoveRe(T
:const(char)[]) (T s
) nothrow @trusted {
319 static if (is(T
== typeof(null))) {
324 if (s
.length
< 3) break;
325 if (s
.ptr
[0] != 'r' && s
.ptr
[0] != 'R') break;
326 if (s
.ptr
[1] != 'e' && s
.ptr
[1] != 'E') break;
328 while (pp
< s
.length
&& s
.ptr
[pp
] <= 32) ++pp
;
329 if (pp
>= s
.length || s
.ptr
[pp
] != ':') break;
337 // ////////////////////////////////////////////////////////////////////////// //
338 private static immutable string b64alphabet
= "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
340 private static immutable ubyte[256] b64dc
= () {
341 ubyte[256] res
= 0xff; // invalid
342 foreach (immutable idx
, immutable char ch
; b64alphabet
) {
343 res
[cast(ubyte)ch
] = cast(ubyte)idx
;
345 res
['='] = 0xfe; // padding
348 res
[127] = 0xf0; // just in case
352 public char[] decodeBase64(bool ignoreUnderscore
=false) (const(void)[] datavoid
, out bool error
) nothrow @trusted {
353 const(ubyte)[] data
= cast(const(ubyte)[])datavoid
;
355 bool inPadding
= false;
360 dcx
.reserve((data
.length
+3U)/4U*3U+8U);
363 bool decodeChunk () nothrow @trusted {
364 if (btspos
== 0) return true;
365 if (btspos
== 1) return false; //throw new Base64Exception("incomplete data in base64 decoder");
366 dcx
~= cast(char)((bts.ptr
[0]<<2)|
((bts.ptr
[1]&0x30)>>4)); // 2 and more
367 if (btspos
> 2) dcx
~= cast(char)(((bts.ptr
[1]&0x0f)<<4)|
((bts.ptr
[2]&0x3c)>>2)); // 3 and more
368 if (btspos
> 3) dcx
~= cast(char)(((bts.ptr
[2]&0x03)<<6)|
bts.ptr
[3]);
372 while (data
.length
) {
373 immutable ubyte cb
= b64dc
.ptr
[data
.ptr
[0]];
374 if (cb
== 0xff) { error
= true; delete dcx
; return "<invalid base64 data>".dup
; }
376 if (cb
== 0xf0) continue; // empty
377 static if (ignoreUnderscore
) {
378 if (cb
== '_') continue;
383 if (!decodeChunk()) { error
= true; delete dcx
; return "<invalid base64 data>".dup
; }
386 if (++btspos
== 4) { inPadding
= false; btspos
= 0; }
390 if (btspos
!= 0) { error
= true; delete dcx
; return "<invalid base64 data>".dup
; }
393 bts.ptr
[btspos
++] = cb
;
395 if (!decodeChunk()) { error
= true; delete dcx
; return "<invalid base64 data>".dup
; }
400 if (btspos
!= 0 && !inPadding
) {
401 // assume that it is not padded
402 if (!decodeChunk()) { error
= true; delete dcx
; return "<invalid base64 data>".dup
; }
409 // ////////////////////////////////////////////////////////////////////////// //
410 public char[] decodeQuotedPrintable(bool multiline
) (const(void)[] datavoid
) nothrow @trusted {
411 const(char)[] data
= cast(const(char)[])datavoid
;
412 //{ import core.stdc.stdio; fprintf(stderr, "***<%.*s>\n", cast(uint)data.length, data.ptr); }
414 dcx
.reserve(data
.length
);
415 while (data
.length
) {
416 if (data
.ptr
[0] == '=') {
417 if (data
.length
== 1) break;
418 if (data
.length
>= 3 && digitInBase(data
.ptr
[1], 16) >= 0 && digitInBase(data
.ptr
[2], 16) >= 0) {
419 dcx
~= cast(char)(digitInBase(data
.ptr
[1], 16)*16+digitInBase(data
.ptr
[2], 16));
423 // check if it is followed by blanks up to the newline
424 // if it is so, then this is "line continuation" -- remove both '=' and blanks
425 static if (multiline
) {
427 usize epos
= 1; // skip '='
428 while (epos
< data
.length
) {
429 char ch
= data
.ptr
[epos
++];
430 if (ch
== 9 || ch
== 32) continue;
432 if (epos
>= data
.length
) { ateol
= true; break; }
433 if (data
.ptr
[epos
] == 10) continue;
434 ch
= 10; // trigger next check
437 // check for most fuckin' idiots: new line started with a dot has two dots
438 if (epos
< data
.length
&& data
.ptr
[epos
] == '.' &&
439 epos
+1 < data
.length
&& data
.ptr
[epos
+1] == '.')
441 ++epos
; // skip first dot
449 if (epos
> data
.length
) epos
= data
.length
; // just in case
450 if (ateol || epos
>= data
.length
) {
451 data
= data
[epos
..$];
456 // check for most fuckin' idiots: new line started with a dot has two dots
457 static if (multiline
) {
458 if (data
.length
>= 3 &&
459 (data
.ptr
[0] == '\n' || data
.ptr
[0] == '\r') &&
460 data
.ptr
[1] == '.' && data
.ptr
[2] == '.')
474 // ////////////////////////////////////////////////////////////////////////// //
475 public T
ensureProper7Bit(T
:const(char)[]) (T s
) nothrow @trusted {
476 static if (is(T
== typeof(null))) {
479 bool needwork
= false;
480 foreach (immutable char ch
; s
) if (ch
>= 128) { needwork
= true; break; }
481 if (!needwork
) return s
;
482 char[] dcx
= new char[s
.length
];
484 foreach (ref char ch
; dcx
) ch
&= 0x7f;
485 return cast(T
)dcx
; // it is safe to cast here
490 // ////////////////////////////////////////////////////////////////////////// //
491 // decode things like "=?UTF-8?B?Tm9yZGzDtnc=?="
492 public T
decodeSubj(T
:const(char)[]) (T s
) nothrow @trusted {
493 static if (is(T
== typeof(null))) {
496 if (s
.indexOf("=?") < 0) return s
.sanitizeStrLine
.utf8ToUtf8
;
498 // have to do some work
501 res
.reserve(s
.length
); // at least
503 while (s
.length
> 2) {
504 auto stqpos
= s
.indexOf("=?");
505 if (stqpos
< 0) break;
506 if (stqpos
> 0) res
~= s
[0..stqpos
].utf8ToUtf8
;
509 auto eepos
= s
.indexOf('?');
510 if (eepos
< 0) break;
511 auto enc
= s
[0..eepos
];
513 //conwriteln("ENCODING: '", enc, "'");
515 if (enc
.length
== 0) enc
= "utf-8";
516 if (s
.length
< 2 || s
.ptr
[1] != '?') return origs
.sanitizeStrLine
.utf8ToUtf8
;
520 eepos
= s
.indexOf("?=");
521 if (eepos
< 0) return origs
.sanitizeStrLine
.utf8ToUtf8
;
523 auto part
= s
[0..eepos
];
526 // several encoded parts may be separated with spaces; those spaces should be ignored
528 while (stqpos
< s
.length
&& s
.ptr
[stqpos
] <= ' ') ++stqpos
;
529 if (s
.length
-stqpos
>= 2 && s
.ptr
[stqpos
] == '=' && s
.ptr
[stqpos
+1] == '?') s
= s
[stqpos
..$];
532 if (ect
== 'Q' || ect
== 'q') {
534 part
= cast(T
)decodeQuotedPrintable
!false(part
); // it is safe to cast here
535 } else if (ect
== 'B' || ect
== 'b') {
539 part
= cast(T
)decodeBase64
!true(part
, out error
); // it is safe to cast here
541 //conwriteln("CANNOT DECODE B64: ", xpart);
543 return origs
.sanitizeStrLine
.utf8ToUtf8
;
547 // reencode part if necessary
548 if (!enc
.strEquCI("utf-8") && !enc
.strEquCI("utf8") && !enc
.strEquCI("US-ASCII")) {
550 //conwriteln("RECODING: ", enc);
551 part
= recode(part
, "utf-8", enc
);
552 } catch (Exception e
) {
553 //conwriteln("RECODE ERROR: ", e.msg);
554 return origs
.sanitizeStrLine
.utf8ToUtf8
;
558 part
= part
.sanitizeStrSubjPart
.utf8ToUtf8
;
559 if (part
.length
) res
~= part
;
562 if (s
.length
) res
~= s
.utf8ToUtf8
;
563 return cast(T
)res
.sanitizeStrLine
; // it should be valid utf8 here; also, it is safe to cast here
568 // ////////////////////////////////////////////////////////////////////////// //
569 // decode content with the given encoding type
570 public T
decodeContent(T
:const(char)[]) (T data
, const(char)[] encoding
) nothrow @trusted {
571 static if (is(T
== typeof(null))) {
574 if (data
.length
== 0 || encoding
.length
== 0 || encoding
.strEquCI("8bit") || encoding
.strEquCI("binary")) {
578 if (encoding
.strEquCI("7bit")) {
579 return cast(T
)ensureProper7Bit(data
); // it is safe to cast here
582 if (encoding
.strEquCI("base64")) {
584 return cast(T
)decodeBase64(data
, out error
); // it is safe to cast here
587 if (encoding
.strEquCI("quoted-printable")) {
588 return cast(T
)decodeQuotedPrintable
!true(data
); // it is safe to cast here
591 if (encoding
.length
!= 0) {
592 char[] res
= "<invalid encoding:".dup
;
595 return cast(T
)res
; // it is safe to cast here
603 // ////////////////////////////////////////////////////////////////////////// //
604 public T
recodeToUtf8(T
:const(char)[]) (T data
, const(char)[] charset
) nothrow @trusted {
605 static if (is(T
== typeof(null))) {
608 if (data
.length
== 0) return data
;
610 foreach (immutable char ch
; data
) if (ch
>= 128) { found
= true; break; }
611 if (!found
) return sanitizeStr(data
);
612 if (charset
.length
== 0 || charset
.strEquCI("utf-8") || charset
.strEquCI("utf8") || charset
.strEquCI("US-ASCII")) {
613 return utf8ToUtf8(data
);
616 data
= recode(data
, "utf-8", charset
);
617 if (data
.length
== 0) return data
;
618 return data
.sanitizeStr
;
619 } catch (Exception e
) {}
620 char[] res
= "<cannot decode '".dup
;
621 foreach (char ch
; charset
) {
622 if (ch
<= 32 || ch
>= 127) continue;
626 return cast(T
)res
; // it is safe to cast here
631 // ////////////////////////////////////////////////////////////////////////// //
632 private T
mailNameUnquote (T
:const(char)[]) (T buf
) pure nothrow @trusted @nogc {
633 static if (is(T
== typeof(null))) {
637 if (buf
.length
>= 2) {
638 if ((buf
.ptr
[0] == '"' && buf
[$-1] == '"') ||
639 (buf
.ptr
[0] == '<' && buf
[$-1] == '>') ||
640 (buf
.ptr
[0] == '`' && buf
[$-1] == '\'') ||
641 (buf
.ptr
[0] == '\'' && buf
[$-1] == '\''))
643 buf
= buf
[1..$-1].xstrip
;
651 // ////////////////////////////////////////////////////////////////////////// //
652 // extract email from decoded "From" and "To" fields
653 public T
extractMail(bool doSanitize
=true, T
:const(char)[]) (T data
) nothrow @trusted {
654 static if (is(T
== typeof(null))) {
657 if (data
.length
== 0) return data
;
658 if (data
[$-1] == '>') {
659 usize pos
= data
.length
;
660 while (pos
> 0 && data
.ptr
[pos
-1] != '<') --pos
;
661 data
= data
[pos
..$-1].xstrip
;
665 static if (doSanitize
) {
666 // hack for idiotic LJ (those morons are breaking all possible standards)
667 auto sppos
= data
.indexOf(' ');
668 if (sppos
> 0) data
= data
[0..sppos
];
670 return data
.toLowerStr
;
675 // ////////////////////////////////////////////////////////////////////////// //
676 // strip email from decoded "From" and "To" fields
677 public T
stripMail(T
:const(char)[]) (T data
) nothrow @trusted {
678 static if (is(T
== typeof(null))) {
681 if (data
.length
== 0) return data
;
682 if (data
[$-1] == '>') {
683 usize pos
= data
.length
;
684 while (pos
> 0 && data
.ptr
[pos
-1] != '<') --pos
;
685 if (pos
== 0) return data
[0..0];
686 return data
[0..pos
-1].xstrip
;
693 // ////////////////////////////////////////////////////////////////////////// //
694 // extract name from decoded "From" and "To" fields
695 // can construct name if there is none
696 // special hack for idiotic LJ
697 public T
extractName(T
:const(char)[]) (T data
) nothrow @trusted {
698 static if (is(T
== typeof(null))) {
701 if (data
.length
== 0) return data
;
702 auto origData
= data
;
703 T mail
= extractMail(data
);
704 data
= stripMail(data
).decodeSubj
.xstrip
;
705 // hack for idiotic LJ (those morons are breaking all possible standards)
706 if (mail
.startsWith("lj_dontreply@lj.rossia.org")) {
707 auto dd = extractMail
!false(origData
);
708 auto spos
= dd.indexOf(" (");
710 dd = dd[spos
+2..$-(dd[$-1] == ')' ?
1 : 0)].xstrip
;
711 if (dd == "LJR Comment") {
713 } else if (dd.endsWith(" - LJR Comment")) {
714 auto dpos
= dd.lastIndexOf('-');
715 dd = dd[0..dpos
].xstrip
;
716 if (dd.length
== 0) dd = "anonymous";
718 dd = dd.mailNameUnquote
;
719 if (dd.length
) return dd;
722 data
= data
.mailNameUnquote
;
724 if (mail
.startsWith("lj-notify@livejournal.com")) {
725 if (data
== "LJ Comment") {
727 } else if (data
.endsWith(" - LJ Comment")) {
728 auto dpos
= data
.lastIndexOf('-');
729 data
= data
[0..dpos
].xstrip
;
730 if (data
.length
== 0) data
= "anonymous";
735 // construct name from the mail
736 auto npos
= mail
.indexOf('@');
737 if (npos
<= 0) return mail
;
738 data
= mail
[0..npos
].xstrip
;
739 if (data
.length
== 0) return mail
;
741 res
.reserve(data
.length
);
742 foreach (char ch
; data
) {
743 if (ch
<= 32 || ch
== '.' || ch
== '-' || ch
== '_') ch
= 32;
745 if (res
.length
&& res
[$-1] != 32) res
~= ch
;
747 if (res
.length
== 0 || res
[$-1] == 32) ch
= ch
.toupper
; else ch
= ch
.tolower
;
752 if (res
.length
== 0) return mail
;
753 return cast(T
)res
; // it is safe to cast here
758 // ////////////////////////////////////////////////////////////////////////// //
759 // encode string if it contains some non-ascii
760 // always returns new string, which is safe to `delete`
761 // passed string must be in UTF-8
762 // can return `null` for empty string
763 public dynstring
strEncodeQ (const(char)[] s
) nothrow @trusted {
764 static bool isSpecial (immutable char ch
) pure nothrow @safe @nogc {
775 if (s
.length
== 0) return res
;
776 static immutable string hexd
= "0123456789abcdef";
777 bool needWork
= (s
[0] == '=' || s
[0] == '?');
778 if (!needWork
) foreach (char ch
; s
) if (isSpecial(ch
)) { needWork
= true; break; }
782 res
.reserve(s
.length
*3+32);
783 res
~= "=?UTF-8?Q?"; // quoted printable
784 foreach (char ch
; s
) {
785 if (ch
<= ' ') ch
= '_';
786 if (!isSpecial(ch
) && ch
!= '=' && ch
!= '?') {
790 res
~= hexd
[(cast(ubyte)ch
)>>4];
791 res
~= hexd
[(cast(ubyte)ch
)&0x0f];