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
.parse
is aliced
;
19 import chibackend
.decode
;
22 private import iv
.vfs
;
23 private import iv
.vfs
.util
;
24 private import iv
.vfs
.io
: byLine
;
25 private import chibackend
: chiroCLIMailPath
, DynStr
;
28 // ////////////////////////////////////////////////////////////////////////// //
29 public string
[][] loadRCFile (const(char)[] fname
) {
32 if (fname
.length
&& fname
[0] == '/') {
33 ff
= cast(string
)fname
; // it is safe to cast here
34 } else if (fname
.length
&& fname
[0] == '~') {
35 char[] dpath
= new char[fname
.length
+128];
36 dpath
= expandTilde(dpath
, fname
);
37 ff
= cast(string
)dpath
; // it is safe to cast here
40 dpath
.reserve(chiroCLIMailPath
.length
+fname
.length
+65);
41 dpath
~= chiroCLIMailPath
;
43 ff
= cast(string
)dpath
; // it is safe to cast here
45 foreach (auto line
; VFile(ff
).byLine
) {
47 if (line
.length
== 0 || line
[0] == '#') continue;
50 if (line
[0] <= 32) { line
= line
[1..$]; continue; }
59 if (ch
== '\\') { ch
= line
[0]; line
= line
[1..$]; }
63 while (line
.length
&& line
[0] > 32) {
68 argv
~= cast(string
)word
; // it is safe to cast here
70 if (argv
.length
) res
~= argv
;
76 // ////////////////////////////////////////////////////////////////////////// //
77 // returned position is always [0..buf.length]
78 public usize
skipOneLine (const(char)[] buf
, usize pos
) pure nothrow @trusted @nogc {
79 import core
.stdc
.string
: memchr
;
80 if (pos
>= buf
.length || buf
.length
== 0) return buf
.length
;
81 const(char)* ep
= cast(const(char) *)memchr(buf
.ptr
+pos
, '\n', buf
.length
-pos
);
82 if (ep
is null) return buf
.length
;
84 return cast(usize
)(ep
-buf
.ptr
);
88 // ////////////////////////////////////////////////////////////////////////// //
89 // return `false` from dg to stop
90 public void forEachHeaderLine (const(char)[] buf
, bool delegate (const(char)[] line
) dg
) {
92 if (dg
is null) return;
93 if (buf
.length
== 0) return;
95 while (lpos
< buf
.length
) {
96 if (isEmptyLine(buf
, lpos
)) return;
97 usize nlpos
= skipOneLine(buf
, lpos
);
98 // collect continuations
99 while (nlpos
< buf
.length
&& buf
.ptr
[nlpos
] <= ' ') nlpos
= skipOneLine(buf
, nlpos
);
100 if (!dg(buf
[lpos
..nlpos
])) return;
106 // ////////////////////////////////////////////////////////////////////////// //
107 private bool isDotLine (const(char)[] buf
, usize pos
) pure nothrow @trusted @nogc {
108 if (pos
>= buf
.length || buf
.ptr
[pos
] != '.') return false;
110 if (pos
< buf
.length
&& buf
.ptr
[pos
] == '\r') ++pos
;
111 return (pos
>= buf
.length || buf
.ptr
[pos
] == '\n');
115 // ////////////////////////////////////////////////////////////////////////// //
116 private bool isEmptyLine (const(char)[] buf
, usize pos
) pure nothrow @trusted @nogc {
117 if (pos
>= buf
.length
) return true;
118 if (buf
.ptr
[pos
] == '\r') { if (++pos
>= buf
.length
) return false; }
119 return (pos
>= buf
.length || buf
.ptr
[pos
] == '\n');
123 // ////////////////////////////////////////////////////////////////////////// //
124 // returns `buf.length` if no proper end was found
125 // otherwise returns position BEFORE the final dot and newline
126 public usize
findMessageEnd(bool withDot
=false) (const(char)[] buf
) pure nothrow @trusted @nogc {
127 if (buf
.length
== 0) return 0;
129 while (lpos
< buf
.length
) {
130 if (isDotLine(buf
, lpos
)) {
131 static if (withDot
) {
132 return skipOneLine(buf
, lpos
);
137 lpos
= skipOneLine(buf
, lpos
);
143 // ////////////////////////////////////////////////////////////////////////// //
144 // returns `buf.length` if no proper end was found
145 // otherwise returns position at the beginnig of the empty line
146 public usize
findHeadersEnd (const(char)[] buf
) pure nothrow @trusted @nogc {
147 if (buf
.length
== 0) return 0;
149 while (lpos
< buf
.length
) {
150 if (isEmptyLine(buf
, lpos
)) return lpos
;
151 lpos
= skipOneLine(buf
, lpos
);
157 // ////////////////////////////////////////////////////////////////////////// //
158 public T
cutTopMessage(T
:const(char)[]) (T buf
) pure nothrow @trusted @nogc {
159 static if (!is(T
== typeof(null))) {
160 if (buf
.length
== 0) return null;
162 while (lpos
< buf
.length
) {
163 immutable usize nlpos
= skipOneLine(buf
, lpos
);
164 if (isDotLine(buf
, lpos
)) return (nlpos
< buf
.length ? buf
[nlpos
..$] : null);
172 // ////////////////////////////////////////////////////////////////////////// //
173 // this takes the first field
174 // returns field data, or `null` (never returns empty values)
175 // field name should not contain ':'
176 public T
findHeaderField(T
:const(char)[]) (T buf
, const(char)[] fldname
, uint fidx
=0) pure nothrow @trusted @nogc {
177 static if (!is(T
== typeof(null))) {
178 if (buf
.length
== 0) return null;
179 fldname
= fldname
.xstrip
;
180 while (fldname
.length
&& (fldname
[$-1] == ':' || fldname
[$-1] <= ' ')) fldname
= fldname
[0..$-1];
181 if (fldname
.length
== 0) return null;
183 while (lpos
< buf
.length
) {
184 if (isEmptyLine(buf
, lpos
)) return null;
185 usize nlpos
= skipOneLine(buf
, lpos
);
186 auto hl
= buf
[lpos
..nlpos
];
187 if (!hl
.startsWithCI(fldname
)) { lpos
= nlpos
; continue; }
188 //{ import std.stdio; writeln("hl=<", hl.xstripright, "> : <", fldname, ">"); }
189 hl
= hl
[fldname
.length
..$].xstrip
;
190 if (hl
.length
== 0 || hl
.ptr
[0] != ':') { lpos
= nlpos
; continue; }
192 if (fidx
) { --fidx
; lpos
= nlpos
; continue; }
193 // collect continuations
194 while (nlpos
< buf
.length
&& buf
.ptr
[nlpos
] <= ' ') nlpos
= skipOneLine(buf
, nlpos
);
195 hl
= buf
[lpos
..nlpos
];
197 while (hl
.length
&& hl
.ptr
[0] != ':') hl
= hl
[1..$];
198 if (hl
.length
) hl
= hl
[1..$]; // skip ':'
200 if (hl
.length
== 0) { lpos
= nlpos
; continue; } // skip empty fields (because why not)
208 // ////////////////////////////////////////////////////////////////////////// //
210 // returned position is always valid for slicing
211 private usize
skipWord (T
:const(char)[]) (T buf
, usize pos
, char termch
) pure nothrow @trusted @nogc {
212 static if (is(T
== typeof(null))) {
215 if (pos
>= buf
.length
) return buf
.length
;
217 while (pos
< buf
.length
) {
218 immutable char ch
= buf
.ptr
[pos
++];
220 if (ch
== '"') inq
= false;
222 if (ch
== '"') inq
= true;
223 else if (ch
== termch
) return pos
-1;
231 // ////////////////////////////////////////////////////////////////////////// //
232 private T
strUnquote (T
:const(char)[]) (T buf
) pure nothrow @trusted @nogc {
233 static if (is(T
== typeof(null))) {
237 if (buf
.length
>= 2) {
238 if (buf
.ptr
[0] == '"' && buf
[$-1] == '"') buf
= buf
[1..$-1];
239 else if (buf
.ptr
[0] == '<' && buf
[$-1] == '>') buf
= buf
[1..$-1];
246 // ////////////////////////////////////////////////////////////////////////// //
247 // removes double quotes, or "<>" quotes
248 public T
getFieldValue (T
:const(char)[]) (T buf
) pure nothrow @trusted @nogc {
249 static if (is(T
== typeof(null))) {
252 return strUnquote(buf
);
257 // ////////////////////////////////////////////////////////////////////////// //
258 // removes double quotes, or "<>" quotes
259 public T
getNextFieldValue (T
:const(char)[]) (ref T buf
) pure nothrow @trusted @nogc {
260 static if (is(T
== typeof(null))) {
264 if (buf
.length
== 0) return null;
265 if (buf
.ptr
[0] == '<') {
267 while (pos
< buf
.length
&& buf
.ptr
[pos
] != '>') ++pos
;
269 if (pos
< buf
.length
&& buf
.ptr
[pos
] == '>') ++pos
;
270 buf
= buf
[pos
..$].xstrip
;
272 } else if (buf
.ptr
[0] == '"') {
274 while (pos
< buf
.length
&& buf
.ptr
[pos
] != '"') ++pos
;
276 if (pos
< buf
.length
&& buf
.ptr
[pos
] == '"') ++pos
;
277 buf
= buf
[pos
..$].xstrip
;
281 while (pos
< buf
.length
&& buf
.ptr
[pos
] > 32) ++pos
;
283 buf
= buf
[pos
..$].xstrip
;
290 // ////////////////////////////////////////////////////////////////////////// //
291 // get next word until ";"
293 // skips empty ";" (this is not standard, because it can skip the first empty token)
294 // returns empty slice when there are no more words
295 public T
getFieldParams (T
:const(char)[]) (ref T buf
) pure nothrow @trusted @nogc {
296 static if (is(T
== typeof(null))) {
299 while (buf
.length
&& (buf
.ptr
[0] <= 32 || buf
.ptr
[0] == ';')) buf
= buf
[1..$];
300 if (buf
.length
== 0) return null;
301 immutable usize end
= skipWord(buf
, 0, ';');
302 // it is guaranteed that we have at least one non-space char here
303 T res
= buf
[0..end
].xstripright
;
305 while (buf
.length
&& (buf
.ptr
[0] <= 32 || buf
.ptr
[0] == ';')) buf
= buf
[1..$];
311 // ////////////////////////////////////////////////////////////////////////// //
312 // returns name part of `getFieldParams()` result
313 // removes double quotes, or "<>" quotes
314 public T
getParamName (T
:const(char)[]) (T buf
) pure nothrow @trusted @nogc {
315 static if (is(T
== typeof(null))) {
318 while (buf
.length
&& buf
.ptr
[0] <= 32) buf
= buf
[1..$];
319 if (buf
.length
== 0) return null;
320 immutable usize end
= skipWord(buf
, 0, '=');
321 return buf
[0..end
].strUnquote
;
326 // ////////////////////////////////////////////////////////////////////////// //
327 // returns value part of `getFieldParams()` result
328 // removes double quotes, or "<>" quotes
329 public T
getParamValue (T
:const(char)[]) (T buf
) pure nothrow @trusted @nogc {
330 static if (is(T
== typeof(null))) {
333 while (buf
.length
&& buf
.ptr
[0] <= 32) buf
= buf
[1..$];
334 if (buf
.length
== 0) return null;
335 usize start
= skipWord(buf
, 0, '=');
336 if (start
>= buf
.length
) return null;
338 return buf
[start
..$].xstrip
.strUnquote
;
343 // ////////////////////////////////////////////////////////////////////////// //
344 // returns starting position of the found boundary
345 // if no boundary was found, returns `buf.length`,
346 public usize
findBoundary (T
:const(char)[]) (T buf
, usize stpos
, const(char)[] boundary
, out bool last
) pure nothrow @trusted @nogc {
347 static if (is(T
== typeof(null))) {
351 if (boundary
.length
== 0 || stpos
>= buf
.length
) { last
= true; return buf
.length
; }
354 // just in case, find line beginning
355 while (pos
> 0 && buf
.ptr
[pos
-1] != '\n') --pos
;
356 while (pos
< buf
.length
) {
357 immutable usize bpos
= pos
;
358 pos
= skipOneLine(buf
, pos
);
359 if (pos
-bpos
< boundary
.length
+2) continue;
360 if (buf
.ptr
[bpos
] != '-' || buf
.ptr
[bpos
+1] != '-') continue;
361 if (buf
[bpos
+2..bpos
+2+boundary
.length
] != boundary
) continue;
362 usize epos
= bpos
+2+boundary
.length
;
363 if (epos
>= buf
.length
) return bpos
;
364 if (buf
.ptr
[epos
] == '\n') return bpos
;
365 if (buf
.ptr
[epos
] == '\r' && (epos
+1 >= buf
.length || buf
.ptr
[epos
+1] == '\n')) return bpos
;
366 if (buf
.ptr
[epos
] == '-' && epos
+1 < buf
.length
&& buf
.ptr
[epos
+1] == '-') {
369 if (epos
>= buf
.length
) return bpos
;
370 if (buf
.ptr
[epos
] == '\n') return bpos
;
371 if (buf
.ptr
[epos
] == '\r' && (epos
+1 >= buf
.length || buf
.ptr
[epos
+1] == '\n')) return bpos
;
381 // ////////////////////////////////////////////////////////////////////////// //
382 public struct Content
{
383 DynStr mime
; // always lowercased
384 DynStr name
; // for attachments; `null` for normal parts
386 DynStr data
; // properly decoded
390 // ////////////////////////////////////////////////////////////////////////// //
391 public void parseContent (ref Content
[] content
, const(char)[] hdrs
, const(char)[] body, bool noattaches
=false) {
392 const(char)[] enc
= findHeaderField(hdrs
, "Content-Transfer-Encoding").getFieldValue
;
393 if (enc
.length
== 0) enc
= "8bit";
395 // parse content type
400 DynStr mime
= "text/plain";
401 auto ctype
= findHeaderField(hdrs
, "Content-Type");
404 mime
= getFieldParams(ctype
).getFieldValue
;
406 if (mime
.length
== 0) mime
= "text/plain";
407 else if (mime
== "text" || mime
== "text/") mime
= "text/plain";
409 while (ctype
.length
) {
410 auto kv
= getFieldParams(ctype
);
411 if (kv
.length
== 0) continue;
412 auto n
= getParamName(kv
);
413 auto v
= getParamValue(kv
);
415 if (n
.strEquCI("charset")) {
416 if (charset
.length
!= 0) continue;
420 charset
.lowerInPlace();
425 if (n
.strEquCI("format")) {
426 if (format
.length
!= 0) continue;
430 format
.lowerInPlace();
435 if (n
.strEquCI("name")) {
436 if (name
.length
!= 0) continue;
437 v
= v
.sanitizeFileNameStr
;
438 if (v
.length
!= 0) name
= v
;
442 if (n
.strEquCI("boundary")) {
443 if (boundary
.length
!= 0) continue;
444 if (v
.length
!= 0) boundary
= v
;
448 if (mime
== "text/richtext" || mime
== "text/enriched") {
451 } else if (mime
.startsWith("text/html") || mime
.startsWith("text/xhtml")) {
454 } else if (mime
.startsWith("text/")) {
457 format
~= mime
[5..$];
464 if (charset
.length
== 0) charset
= "us-ascii";
467 auto disp
= findHeaderField(hdrs
, "Content-Disposition");
468 while (disp
.length
) {
469 auto kv
= getFieldParams(disp
);
470 if (kv
.length
== 0) continue;
471 auto n
= getParamName(kv
);
472 auto v
= getParamValue(kv
);
473 if (n
.strEquCI("attachment")) {
478 if (n
.strEquCI("filename")) {
479 v
= v
.sanitizeFileNameStr
;
480 if (v
.length
!= 0) name
= v
;
486 writeln("--------------------------------");
487 writeln("encoding: <", enc, ">");
488 writeln("name : <", name, ">");
489 writeln("boundary: <", boundary, ">");
490 writeln("format : <", format, ">");
491 writeln("charset : <", charset, ">");
492 writeln("inline : ", inline);
495 if (boundary
.length
== 0 ||
(mime
!= "multipart" && !mime
.startsWith("multipart/"))) {
496 immutable bool istext
= mime
.startsWith("text/");
498 if (noattaches
&& (!istext ||
!inline
)) return; // not a text, or not an inline text, do not want
501 cc
.name
= (inline
&& istext ?
null : name
.idup
);
502 cc
.format
= (format
.length ? format
.idup
: "");
504 cc
.data
= decodeContent(body, enc
).xstripright
.recodeToUtf8(charset
); // it is safe to cast here
506 cc
.data
= decodeContent(body, enc
); // it is safe to cast here
511 import std.string : format;
512 auto fo = VFile("z__%04u.bin".format(cnt++), "w");
513 fo.rawWriteExact(cc.data[]);
520 // multipart, process it recursively
523 usize bpos
= findBoundary(body, 0, boundary
, out last
);
526 bpos
= skipOneLine(body, bpos
);
527 // find next boundary
528 immutable usize epos
= findBoundary(body, bpos
, boundary
, out last
);
530 const(char)[] bpart
= body[bpos
..epos
].xstripright
;
531 if (bpart
.length
!= 0) {
532 //{ writeln("===[", enc, "]===[", boundary, "]=== (bpos=", bpos, "; epos=", epos, ")"); writeln(bpart); writeln("------------------"); }
533 bpart
= decodeContent(bpart
, enc
);
535 usize hdrend
= findHeadersEnd(bpart
);
536 hdrs
= bpart
[0..hdrend
];
537 hdrend
= skipOneLine(bpart
, hdrend
);
538 bpart
= bpart
[hdrend
..$];
539 parseContent(ref content
, hdrs
, bpart
, noattaches
);
541 body = body[epos
..$];