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
.decode
: strEncodeQ
;
25 import chibackend
.parse
: skipOneLine
;
28 // ////////////////////////////////////////////////////////////////////////// //
30 mixin(NewExceptionClass
!("MessageBuilderException", "Exception"));
33 // ////////////////////////////////////////////////////////////////////////// //
34 struct MessageBuilder
{
42 static class Reference
{
44 this (const(char)[] as
) nothrow @trusted @nogc { s
= as
; }
47 //WARNING! all those fields are NOT GC-ALLOCATED!
55 Reference
[] references
= null;
59 Attach
[] attaches
= null;
62 void buildHeaders () {
65 prepared
.reserve(8192);
68 if (fromName
.length
) { prepared
.appendQEncoded(fromName
); prepared
~= " <"; }
70 if (fromName
.length
) prepared
~= ">";
73 if (toNewsgroup
.length
) {
74 prepared
~= "Newsgroups: ";
75 prepared
~= toNewsgroup
;
79 if (toName
.length
) { prepared
.appendQEncoded(toName
); prepared
~= " <"; }
81 if (toName
.length
) prepared
~= ">";
85 prepared
~= "Subject: ";
87 prepared
.appendQEncoded(subj
);
89 prepared
~= "no subject";
95 prepared
~= "Message-ID: <";
97 UUID id
= randomUUID();
98 foreach (immutable ubyte b
; id
.data
[]) {
99 prepared
~= "0123456789abcdef"[b
>>4];
100 prepared
~= "0123456789abcdef"[b
&0x0f];
102 prepared
~= "@chiroptera>\r\n";
105 prepared
~= "User-Agent: Chiroptera\r\n";
107 if (references
.length
) {
108 prepared
~= "In-Reply-To: <";
109 prepared
~= references
[0].s
;
113 if (references
.length
) {
114 prepared
~= "References:";
116 foreach (Reference reference
; references
) {
122 prepared
~= reference
.s
;
124 nlen
+= reference
.s
.length
+3;
132 prepared
~= "Data: ";
133 prepared
~= SysTimeToRFCString(Clock
.currTime
);
137 string textEncoding
= "US-ASCII";
138 foreach (immutable char ch
; body.getData
) if (ch
>= 128) { textEncoding
= "UTF-8"; break; }
140 prepared
~= "Mime-Version: 1.0\r\n";
141 if (attaches
.length
== 0) {
143 prepared
~= "Content-Type: text/plain; charset=";
144 prepared
~= textEncoding
;
145 prepared
~= "; format=flowed; delsp=no\r\n";
146 prepared
~= "Content-Transfer-Encoding: 8bit\r\n";
150 UUID id
= randomUUID();
152 boundary
.reserve
!false(2+16*2+11+8+64);
153 boundary
~= "------==--";
154 foreach (immutable ubyte b
; id
.data
[]) {
155 boundary
~= "0123456789abcdef"[b
>>4];
156 boundary
~= "0123456789abcdef"[b
&0x0f];
158 boundary
~= ".chiroptera--";
160 prepared
~= "Content-Type: multipart/mixed; boundary=\"";
161 prepared
~= boundary
;
162 prepared
~= "\"\r\n";
163 // end of main headers
166 prepared
~= "This is a multi-part message in MIME format.\r\n";
167 prepared
~= boundary
;
169 prepared
~= "Content-Type: text/plain; charset=";
170 prepared
~= textEncoding
;
171 prepared
~= "; format=flowed; delsp=no\r\n";
172 prepared
~= "Content-Transfer-Encoding: 8bit\r\n";
180 // put body; correctly dot-stuffed, with CRLF
181 const(char)[] buf
= body.getData
.xstripright
;
182 while (buf
.length
&& (buf
[0] == '\r' || buf
[0] == '\n')) buf
= buf
[1..$];
184 usize epos
= skipOneLine(buf
, 0);
186 if (eend
>= 2 && buf
[eend
-2] == '\r' && buf
[eend
-1] == '\n') eend
-= 2;
187 else if (eend
>= 1 && buf
[eend
-1] == '\n') eend
-= 1;
189 if (eend
== 1 && buf
[0] == '.') {
191 } else if (boundary
.length
&& eend
== boundary
.length
&& buf
[0..eend
] == boundary
.getData
) {
194 bool canWhole
= true;
195 foreach (immutable char ch
; buf
[0..eend
]) {
197 if (ch
< 9 || ch
> 13 || ch
== 11) { canWhole
= false; break; }
201 prepared
~= buf
[0..eend
];
203 foreach (char ch
; buf
[0..eend
]) {
205 if (ch
< 9 || ch
> 13 || ch
== 11) ch
= ' ';
215 void appendAttaches () {
216 static struct ExtMime
{
220 static immutable ExtMime
[24] knownMimes
= [
221 ExtMime(".png", "image/png"),
222 ExtMime(".jpg", "image/jpeg"),
223 ExtMime(".jpeg", "image/jpeg"),
224 ExtMime(".gif", "image/gif"),
227 ExtMime(".txt", "text/plain; charset=US-ASCII"),
228 ExtMime(".patch", "text/plain; charset=US-ASCII"),
229 ExtMime(".diff", "text/plain; charset=US-ASCII"),
230 ExtMime(".d", "text/plain; charset=US-ASCII"),
231 ExtMime(".c", "text/plain; charset=US-ASCII"),
232 ExtMime(".cc", "text/plain; charset=US-ASCII"),
233 ExtMime(".h", "text/plain; charset=US-ASCII"),
234 ExtMime(".hpp", "text/plain; charset=US-ASCII"),
236 ExtMime(".htm", "text/html; charset=US-ASCII"),
237 ExtMime(".html", "text/html; charset=US-ASCII"),
240 ExtMime(".zip", "application/x-compressed"),
241 ExtMime(".7z", "application/x-compressed"),
242 ExtMime(".pk3", "application/x-compressed"),
243 ExtMime(".gz", "application/x-compressed"),
244 ExtMime(".lz", "application/x-compressed"),
245 ExtMime(".xz", "application/x-compressed"),
246 ExtMime(".tgz", "application/x-compressed"),
247 ExtMime(".tlz", "application/x-compressed"),
248 ExtMime(".txz", "application/x-compressed"),
249 ExtMime(".tar", "application/x-compressed"),
252 foreach (const ref Attach attach
; attaches
) {
253 if (attach
.data
.length
== 0) continue;
254 const(char)[] fname
= attach
.name
.getData
.xstrip
;
255 while (fname
.length
) {
256 auto stp
= fname
.lastIndexOf('/');
258 fname
= fname
[stp
+1..$].xstrip
;
263 if (fname
.length
== 0) {
264 prepared
~= "unnamed";
266 foreach (char ch
; fname
) {
267 if (ch
<= 32 || ch
>= 127 || ch
== '/' || ch
== '\\' || ch
== '?' ||
268 ch
== '*' || ch
== '&' || ch
== '|' || ch
== '<' || ch
== '>' ||
269 ch
== '"' || ch
== '\'')
279 prepared
~= boundary
;
282 prepared
~= "Content-Disposition: attachment; filename=";
286 string tenc
= "base64";
287 const(char)[] mime
= attach
.mime
.getData
;
288 if (mime
.length
== 0) {
289 foreach (const ref ExtMime ee
; knownMimes
) {
290 if (fname
.endsWithCI(ee
.ext
)) {
295 // check if it can be treated as a text
296 if (mime
.length
== 0) {
298 foreach (immutable idx
, immutable char ch
; attach
.data
.getData
) {
300 if (ch
< 9 || ch
> 13 || ch
== 11) { oktext
= false; break; }
301 if (ch
== 27 && idx
== attach
.data
.length
-1) break;
302 } else if (ch
>= 127) {
308 mime
= "text/plain; charset=US-ASCII";
311 mime
= "application/octet-stream";
315 prepared
~= "Content-Type: ";
317 prepared
~= "; name=";
320 prepared
~= "Content-Transfer-Encoding: ";
323 prepared
~= "\r\n"; // end of headers
324 if (tenc
== "base64") {
325 prepared
.appendB64Encoded(attach
.data
);
327 assert(tenc
== "8bit");
328 prepared
~= attach
.data
;
333 void finishPrepared () {
335 if (attaches
.length
!= 0) {
337 prepared
~= boundary
;
345 //this () pure nothrow @trusted @nogc {}
347 @disable this (this);
350 foreach (ref Reference reference
; references
) delete reference
;
352 foreach (ref Attach attach
; attaches
) delete attach
;
356 void setFromName (const(char)[] value
) nothrow @trusted @nogc { fromName
= value
; }
357 void setFromMail (const(char)[] value
) nothrow @trusted @nogc { fromMail
= value
; }
358 void setToName (const(char)[] value
) nothrow @trusted @nogc { toName
= value
; }
359 void setToMail (const(char)[] value
) nothrow @trusted @nogc { toMail
= value
; }
360 void setNewsgroup (const(char)[] value
) nothrow @trusted @nogc { toNewsgroup
= value
; }
361 void setSubj (const(char)[] value
) nothrow @trusted @nogc { subj
= value
; }
362 void setBody (const(char)[] value
) nothrow @trusted @nogc { body = value
; }
364 // first appended reference will be "In-Reply-To"
365 void appendReference (const(char)[] msgid
) {
366 msgid
= msgid
.xstrip
;
367 if (msgid
.length
== 0) return;
368 references
~= new Reference(msgid
);
371 void appendAttach (const(char)[] filename
, const(void)[] data
, const(char)[] mime
=null) {
372 if (data
.length
== 0) return;
373 Attach att
= new Attach
;
376 att
.data
= cast(const(char)[])data
;
380 void attachFile (const(char)[] filename
) {
381 auto fl
= VFile(filename
);
382 if (fl
.size
> 0x00ff_ffff
) throw new MessageBuilderException("file too big");
383 if (fl
.size
== 0) return;
384 Attach att
= new Attach
;
385 scope(failure
) delete att
;
387 //att.mime = mime; // let the engine determine it
388 //att.data.reserve!false(cast(uint)fl.size);
389 att
.data
.length
= cast(uint)fl
.size
;
390 assert(!att
.data
.isShared
&& !att
.data
.isSlice
&& att
.data
.length
== cast(uint)fl
.size
);
391 fl
.rawReadExact(att
.data
.makeUniquePointer
[0..cast(uint)fl
.size
]);
395 // should be called after setting everything
396 // WARNING! returned data will NOT outlive this struct!
397 const(char)[] getPrepared () {
402 return prepared
.getData
;