more memory cosmetix
[chiroptera.git] / chibackend / mbuilder.d
blob723dd5faeb5a55157be241a802c3560be77bb98d
1 /* E-Mail Client
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;
18 private:
20 import iv.strex;
21 import iv.vfs;
23 import chibackend : DynStr, SysTimeToRFCString;
24 import chibackend.decode : strEncodeQ;
25 import chibackend.parse : skipOneLine;
28 // ////////////////////////////////////////////////////////////////////////// //
29 public:
30 mixin(NewExceptionClass!("MessageBuilderException", "Exception"));
33 // ////////////////////////////////////////////////////////////////////////// //
34 struct MessageBuilder {
35 private:
36 static class Attach {
37 DynStr name;
38 DynStr mime;
39 DynStr data;
42 static class Reference {
43 DynStr s;
44 this (const(char)[] as) nothrow @trusted @nogc { s = as; }
47 //WARNING! all those fields are NOT GC-ALLOCATED!
48 DynStr fromName;
49 DynStr fromMail;
50 DynStr toName;
51 DynStr toMail;
52 DynStr toNewsgroup;
53 DynStr subj;
54 DynStr body;
55 Reference[] references = null;
56 DynStr boundary;
57 DynStr prepared;
59 Attach[] attaches = null;
62 void buildHeaders () {
63 boundary.clear();
64 prepared.clear();
65 prepared.reserve(8192);
67 prepared ~= "From: ";
68 if (fromName.length) { prepared.appendQEncoded(fromName); prepared ~= " <"; }
69 prepared ~= fromMail;
70 if (fromName.length) prepared ~= ">";
71 prepared ~= "\r\n";
73 if (toNewsgroup.length) {
74 prepared ~= "Newsgroups: ";
75 prepared ~= toNewsgroup;
76 prepared ~= "\r\n";
77 } else {
78 prepared ~= "To: ";
79 if (toName.length) { prepared.appendQEncoded(toName); prepared ~= " <"; }
80 prepared ~= toMail;
81 if (toName.length) prepared ~= ">";
82 prepared ~= "\r\n";
85 prepared ~= "Subject: ";
86 if (subj.length) {
87 prepared.appendQEncoded(subj);
88 } else {
89 prepared ~= "no subject";
91 prepared ~= "\r\n";
93 // msgid
95 prepared ~= "Message-ID: <";
96 import std.uuid;
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;
110 prepared ~= ">\r\n";
113 if (references.length) {
114 prepared ~= "References:";
115 usize nlen = 12;
116 foreach (Reference reference; references) {
117 if (nlen >= 76) {
118 prepared ~= "\r\n ";
119 nlen = 1;
121 prepared ~= " <";
122 prepared ~= reference.s;
123 prepared ~= ">";
124 nlen += reference.s.length+3;
126 prepared ~= "\r\n";
129 // date
131 import std.datetime;
132 prepared ~= "Data: ";
133 prepared ~= SysTimeToRFCString(Clock.currTime);
134 prepared ~= "\r\n";
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) {
142 // no attachments
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";
147 } else {
148 // generate boundary
149 import std.uuid;
150 UUID id = randomUUID();
151 boundary.clear();
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
164 prepared ~= "\r\n";
165 // useless comment
166 prepared ~= "This is a multi-part message in MIME format.\r\n";
167 prepared ~= boundary;
168 prepared ~= "\r\n";
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";
175 // end of headers
176 prepared ~= "\r\n";
179 void appendBody () {
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..$];
183 while (buf.length) {
184 usize epos = skipOneLine(buf, 0);
185 usize eend = epos;
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;
188 // dot-stuffing
189 if (eend == 1 && buf[0] == '.') {
190 prepared ~= '.';
191 } else if (boundary.length && eend == boundary.length && buf[0..eend] == boundary.getData) {
192 prepared ~= '_';
194 bool canWhole = true;
195 foreach (immutable char ch; buf[0..eend]) {
196 if (ch < 32) {
197 if (ch < 9 || ch > 13 || ch == 11) { canWhole = false; break; }
200 if (canWhole) {
201 prepared ~= buf[0..eend];
202 } else {
203 foreach (char ch; buf[0..eend]) {
204 if (ch < 32) {
205 if (ch < 9 || ch > 13 || ch == 11) ch = ' ';
207 prepared ~= ch;
210 prepared ~= "\r\n";
211 buf = buf[epos..$];
215 void appendAttaches () {
216 static struct ExtMime {
217 string ext;
218 string mime;
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"),
225 // text
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"),
235 // html
236 ExtMime(".htm", "text/html; charset=US-ASCII"),
237 ExtMime(".html", "text/html; charset=US-ASCII"),
239 // archives
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('/');
257 if (stp < 0) break;
258 fname = fname[stp+1..$].xstrip;
261 void putFName () {
262 prepared ~= '"';
263 if (fname.length == 0) {
264 prepared ~= "unnamed";
265 } else {
266 foreach (char ch; fname) {
267 if (ch <= 32 || ch >= 127 || ch == '/' || ch == '\\' || ch == '?' ||
268 ch == '*' || ch == '&' || ch == '|' || ch == '<' || ch == '>' ||
269 ch == '"' || ch == '\'')
271 ch = '_';
273 prepared ~= ch;
276 prepared ~= '"';
279 prepared ~= boundary;
280 prepared ~= "\r\n";
282 prepared ~= "Content-Disposition: attachment; filename=";
283 putFName();
284 prepared ~= "\r\n";
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)) {
291 mime = ee.mime;
292 break;
295 // check if it can be treated as a text
296 if (mime.length == 0) {
297 bool oktext = true;
298 foreach (immutable idx, immutable char ch; attach.data.getData) {
299 if (ch < 32) {
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) {
303 oktext = false;
304 break;
307 if (oktext) {
308 mime = "text/plain; charset=US-ASCII";
309 tenc = "8bit";
310 } else {
311 mime = "application/octet-stream";
315 prepared ~= "Content-Type: ";
316 prepared ~= mime;
317 prepared ~= "; name=";
318 putFName();
319 prepared ~= "\r\n";
320 prepared ~= "Content-Transfer-Encoding: ";
321 prepared ~= tenc;
322 prepared ~= "\r\n";
323 prepared ~= "\r\n"; // end of headers
324 if (tenc == "base64") {
325 prepared.appendB64Encoded(attach.data);
326 } else {
327 assert(tenc == "8bit");
328 prepared ~= attach.data;
333 void finishPrepared () {
334 // final boundary
335 if (attaches.length != 0) {
336 prepared ~= "--";
337 prepared ~= boundary;
338 prepared ~= "\r\n";
340 // final dot
341 prepared ~= ".\r\n";
344 public:
345 //this () pure nothrow @trusted @nogc {}
347 @disable this (this);
349 ~this () {
350 foreach (ref Reference reference; references) delete reference;
351 delete references;
352 foreach (ref Attach attach; attaches) delete attach;
353 delete attaches;
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;
374 att.name = filename;
375 att.mime = mime;
376 att.data = cast(const(char)[])data;
377 attaches ~= att;
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;
386 att.name = filename;
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]);
392 attaches ~= att;
395 // should be called after setting everything
396 // WARNING! returned data will NOT outlive this struct!
397 const(char)[] getPrepared () {
398 buildHeaders();
399 appendBody();
400 appendAttaches();
401 finishPrepared();
402 return prepared.getData;