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
.mbuilder
is aliced
;
23 import chibackend
: DynStr
, SysTimeToRFCString
;
24 import chibackend
.parse
: skipOneLine
;
27 // ////////////////////////////////////////////////////////////////////////// //
29 mixin(NewExceptionClass
!("MessageBuilderException", "Exception"));
32 // ////////////////////////////////////////////////////////////////////////// //
33 struct MessageBuilder
{
41 static class Reference
{
43 this (const(char)[] as
) nothrow @trusted @nogc { s
= as
; }
46 //WARNING! all those fields are NOT GC-ALLOCATED!
54 Reference
[] references
= null;
58 Attach
[] attaches
= null;
61 void buildHeaders () {
64 prepared
.reserve(8192);
67 if (fromName
.length
) { prepared
.appendQEncoded(fromName
); prepared
~= " <"; }
69 if (fromName
.length
) prepared
~= ">";
72 if (toNewsgroup
.length
) {
73 prepared
~= "Newsgroups: ";
74 prepared
~= toNewsgroup
;
78 if (toName
.length
) { prepared
.appendQEncoded(toName
); prepared
~= " <"; }
80 if (toName
.length
) prepared
~= ">";
84 prepared
~= "Subject: ";
86 prepared
.appendQEncoded(subj
);
88 prepared
~= "no subject";
94 prepared
~= "Message-ID: <";
96 UUID id
= randomUUID();
97 foreach (immutable ubyte b
; id
.data
[]) {
98 prepared
~= "0123456789abcdef"[b
>>4];
99 prepared
~= "0123456789abcdef"[b
&0x0f];
101 prepared
~= "@chiroptera>\r\n";
104 prepared
~= "User-Agent: Chiroptera\r\n";
106 if (references
.length
) {
107 prepared
~= "In-Reply-To: <";
108 prepared
~= references
[0].s
;
112 if (references
.length
) {
113 prepared
~= "References:";
115 foreach (Reference reference
; references
) {
121 prepared
~= reference
.s
;
123 nlen
+= reference
.s
.length
+3;
131 prepared
~= "Date: ";
132 prepared
~= SysTimeToRFCString(Clock
.currTime
);
136 string textEncoding
= "US-ASCII";
137 foreach (immutable char ch
; body.getData
) if (ch
>= 128) { textEncoding
= "UTF-8"; break; }
139 prepared
~= "Mime-Version: 1.0\r\n";
140 if (attaches
.length
== 0) {
142 prepared
~= "Content-Type: text/plain; charset=";
143 prepared
~= textEncoding
;
144 prepared
~= "; format=flowed; delsp=no\r\n";
145 prepared
~= "Content-Transfer-Encoding: 8bit\r\n";
149 UUID id
= randomUUID();
151 boundary
.reserve
!false(2+16*2+11+8+64);
152 boundary
~= "------==--";
153 foreach (immutable ubyte b
; id
.data
[]) {
154 boundary
~= "0123456789abcdef"[b
>>4];
155 boundary
~= "0123456789abcdef"[b
&0x0f];
157 boundary
~= ".chiroptera--";
159 prepared
~= "Content-Type: multipart/mixed; boundary=\"";
160 prepared
~= boundary
;
161 prepared
~= "\"\r\n";
162 // end of main headers
165 prepared
~= "This is a multi-part message in MIME format.\r\n";
166 prepared
~= boundary
;
168 prepared
~= "Content-Type: text/plain; charset=";
169 prepared
~= textEncoding
;
170 prepared
~= "; format=flowed; delsp=no\r\n";
171 prepared
~= "Content-Transfer-Encoding: 8bit\r\n";
179 // put body; correctly dot-stuffed, with CRLF
180 const(char)[] buf
= body.getData
.xstripright
;
181 while (buf
.length
&& (buf
[0] == '\r' || buf
[0] == '\n')) buf
= buf
[1..$];
183 usize epos
= skipOneLine(buf
, 0);
185 if (eend
>= 2 && buf
[eend
-2] == '\r' && buf
[eend
-1] == '\n') eend
-= 2;
186 else if (eend
>= 1 && buf
[eend
-1] == '\n') eend
-= 1;
188 if (eend
== 1 && buf
[0] == '.') {
190 } else if (boundary
.length
&& eend
== boundary
.length
&& buf
[0..eend
] == boundary
.getData
) {
193 bool canWhole
= true;
194 foreach (immutable char ch
; buf
[0..eend
]) {
196 if (ch
< 9 || ch
> 13 || ch
== 11) { canWhole
= false; break; }
200 prepared
~= buf
[0..eend
];
202 foreach (char ch
; buf
[0..eend
]) {
204 if (ch
< 9 || ch
> 13 || ch
== 11) ch
= ' ';
214 void appendAttaches () {
215 static struct ExtMime
{
219 static immutable ExtMime
[24] knownMimes
= [
220 ExtMime(".png", "image/png"),
221 ExtMime(".jpg", "image/jpeg"),
222 ExtMime(".jpeg", "image/jpeg"),
223 ExtMime(".gif", "image/gif"),
226 ExtMime(".txt", "text/plain; charset=US-ASCII"),
227 ExtMime(".patch", "text/plain; charset=US-ASCII"),
228 ExtMime(".diff", "text/plain; charset=US-ASCII"),
229 ExtMime(".d", "text/plain; charset=US-ASCII"),
230 ExtMime(".c", "text/plain; charset=US-ASCII"),
231 ExtMime(".cc", "text/plain; charset=US-ASCII"),
232 ExtMime(".h", "text/plain; charset=US-ASCII"),
233 ExtMime(".hpp", "text/plain; charset=US-ASCII"),
235 ExtMime(".htm", "text/html; charset=US-ASCII"),
236 ExtMime(".html", "text/html; charset=US-ASCII"),
239 ExtMime(".zip", "application/x-compressed"),
240 ExtMime(".7z", "application/x-compressed"),
241 ExtMime(".pk3", "application/x-compressed"),
242 ExtMime(".gz", "application/x-compressed"),
243 ExtMime(".lz", "application/x-compressed"),
244 ExtMime(".xz", "application/x-compressed"),
245 ExtMime(".tgz", "application/x-compressed"),
246 ExtMime(".tlz", "application/x-compressed"),
247 ExtMime(".txz", "application/x-compressed"),
248 ExtMime(".tar", "application/x-compressed"),
251 foreach (const ref Attach attach
; attaches
) {
252 if (attach
.data
.length
== 0) continue;
253 const(char)[] fname
= attach
.name
.getData
.xstrip
;
254 while (fname
.length
) {
255 auto stp
= fname
.lastIndexOf('/');
257 fname
= fname
[stp
+1..$].xstrip
;
262 if (fname
.length
== 0) {
263 prepared
~= "unnamed";
265 foreach (char ch
; fname
) {
266 if (ch
<= 32 || ch
>= 127 || ch
== '/' || ch
== '\\' || ch
== '?' ||
267 ch
== '*' || ch
== '&' || ch
== '|' || ch
== '<' || ch
== '>' ||
268 ch
== '"' || ch
== '\'')
278 prepared
~= boundary
;
281 prepared
~= "Content-Disposition: attachment; filename=";
285 string tenc
= "base64";
286 const(char)[] mime
= attach
.mime
.getData
;
287 if (mime
.length
== 0) {
288 foreach (const ref ExtMime ee
; knownMimes
) {
289 if (fname
.endsWithCI(ee
.ext
)) {
294 // check if it can be treated as a text
295 if (mime
.length
== 0) {
297 foreach (immutable idx
, immutable char ch
; attach
.data
.getData
) {
299 if (ch
< 9 || ch
> 13 || ch
== 11) { oktext
= false; break; }
300 if (ch
== 27 && idx
== attach
.data
.length
-1) break;
301 } else if (ch
>= 127) {
307 mime
= "text/plain; charset=US-ASCII";
310 mime
= "application/octet-stream";
314 prepared
~= "Content-Type: ";
316 prepared
~= "; name=";
319 prepared
~= "Content-Transfer-Encoding: ";
322 prepared
~= "\r\n"; // end of headers
323 if (tenc
== "base64") {
324 prepared
.appendB64Encoded(attach
.data
);
326 assert(tenc
== "8bit");
327 prepared
~= attach
.data
;
332 void finishPrepared () {
334 if (attaches
.length
!= 0) {
336 prepared
~= boundary
;
344 //this () pure nothrow @trusted @nogc {}
346 @disable this (this);
349 foreach (ref Reference reference
; references
) delete reference
;
351 foreach (ref Attach attach
; attaches
) delete attach
;
355 void setFromName (const(char)[] value
) nothrow @trusted @nogc { fromName
= value
; }
356 void setFromMail (const(char)[] value
) nothrow @trusted @nogc { fromMail
= value
; }
357 void setToName (const(char)[] value
) nothrow @trusted @nogc { toName
= value
; }
358 void setToMail (const(char)[] value
) nothrow @trusted @nogc { toMail
= value
; }
359 void setNewsgroup (const(char)[] value
) nothrow @trusted @nogc { toNewsgroup
= value
; }
360 void setSubj (const(char)[] value
) nothrow @trusted @nogc { subj
= value
; }
361 void setBody (const(char)[] value
) nothrow @trusted @nogc { body = value
; }
363 // first appended reference will be "In-Reply-To"
364 void appendReference (const(char)[] msgid
) {
365 msgid
= msgid
.xstrip
;
366 if (msgid
.length
== 0) return;
367 references
~= new Reference(msgid
);
370 void appendAttach (const(char)[] filename
, const(void)[] data
, const(char)[] mime
=null) {
371 if (data
.length
== 0) return;
372 Attach att
= new Attach
;
375 att
.data
= cast(const(char)[])data
;
379 void attachFile (const(char)[] filename
) {
380 auto fl
= VFile(filename
);
381 if (fl
.size
> 0x00ff_ffff
) throw new MessageBuilderException("file too big");
382 if (fl
.size
== 0) return;
383 Attach att
= new Attach
;
384 scope(failure
) delete att
;
386 //att.mime = mime; // let the engine determine it
387 //att.data.reserve!false(cast(uint)fl.size);
388 att
.data
.length
= cast(uint)fl
.size
;
389 assert(!att
.data
.isShared
&& !att
.data
.isSlice
&& att
.data
.length
== cast(uint)fl
.size
);
390 fl
.rawReadExact(att
.data
.makeUniquePointer
[0..cast(uint)fl
.size
]);
394 // should be called after setting everything
395 // WARNING! returned data will NOT outlive this struct!
396 const(char)[] getPrepared () {
401 return prepared
.getData
;