receiver: better filter processing
[chiroptera.git] / chibackend / mbuilder.d
blobbb2aaa44d2d8e57fffc291de02d5a635d9f6a358
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.parse : skipOneLine;
27 // ////////////////////////////////////////////////////////////////////////// //
28 public:
29 mixin(NewExceptionClass!("MessageBuilderException", "Exception"));
32 // ////////////////////////////////////////////////////////////////////////// //
33 struct MessageBuilder {
34 private:
35 static class Attach {
36 DynStr name;
37 DynStr mime;
38 DynStr data;
41 static class Reference {
42 DynStr s;
43 this (const(char)[] as) nothrow @trusted @nogc { s = as; }
46 //WARNING! all those fields are NOT GC-ALLOCATED!
47 DynStr fromName;
48 DynStr fromMail;
49 DynStr toName;
50 DynStr toMail;
51 DynStr toNewsgroup;
52 DynStr subj;
53 DynStr body;
54 Reference[] references = null;
55 DynStr boundary;
56 DynStr prepared;
58 Attach[] attaches = null;
61 void buildHeaders () {
62 boundary.clear();
63 prepared.clear();
64 prepared.reserve(8192);
66 prepared ~= "From: ";
67 if (fromName.length) { prepared.appendQEncoded(fromName); prepared ~= " <"; }
68 prepared ~= fromMail;
69 if (fromName.length) prepared ~= ">";
70 prepared ~= "\r\n";
72 if (toNewsgroup.length) {
73 prepared ~= "Newsgroups: ";
74 prepared ~= toNewsgroup;
75 prepared ~= "\r\n";
76 } else {
77 prepared ~= "To: ";
78 if (toName.length) { prepared.appendQEncoded(toName); prepared ~= " <"; }
79 prepared ~= toMail;
80 if (toName.length) prepared ~= ">";
81 prepared ~= "\r\n";
84 prepared ~= "Subject: ";
85 if (subj.length) {
86 prepared.appendQEncoded(subj);
87 } else {
88 prepared ~= "no subject";
90 prepared ~= "\r\n";
92 // msgid
94 prepared ~= "Message-ID: <";
95 import std.uuid;
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;
109 prepared ~= ">\r\n";
112 if (references.length) {
113 prepared ~= "References:";
114 usize nlen = 12;
115 foreach (Reference reference; references) {
116 if (nlen >= 76) {
117 prepared ~= "\r\n ";
118 nlen = 1;
120 prepared ~= " <";
121 prepared ~= reference.s;
122 prepared ~= ">";
123 nlen += reference.s.length+3;
125 prepared ~= "\r\n";
128 // date
130 import std.datetime;
131 prepared ~= "Date: ";
132 prepared ~= SysTimeToRFCString(Clock.currTime);
133 prepared ~= "\r\n";
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) {
141 // no attachments
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";
146 } else {
147 // generate boundary
148 import std.uuid;
149 UUID id = randomUUID();
150 boundary.clear();
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
163 prepared ~= "\r\n";
164 // useless comment
165 prepared ~= "This is a multi-part message in MIME format.\r\n";
166 prepared ~= boundary;
167 prepared ~= "\r\n";
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";
174 // end of headers
175 prepared ~= "\r\n";
178 void appendBody () {
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..$];
182 while (buf.length) {
183 usize epos = skipOneLine(buf, 0);
184 usize eend = epos;
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;
187 // dot-stuffing
188 if (eend == 1 && buf[0] == '.') {
189 prepared ~= '.';
190 } else if (boundary.length && eend == boundary.length && buf[0..eend] == boundary.getData) {
191 prepared ~= '_';
193 bool canWhole = true;
194 foreach (immutable char ch; buf[0..eend]) {
195 if (ch < 32) {
196 if (ch < 9 || ch > 13 || ch == 11) { canWhole = false; break; }
199 if (canWhole) {
200 prepared ~= buf[0..eend];
201 } else {
202 foreach (char ch; buf[0..eend]) {
203 if (ch < 32) {
204 if (ch < 9 || ch > 13 || ch == 11) ch = ' ';
206 prepared ~= ch;
209 prepared ~= "\r\n";
210 buf = buf[epos..$];
214 void appendAttaches () {
215 static struct ExtMime {
216 string ext;
217 string mime;
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"),
224 // text
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"),
234 // html
235 ExtMime(".htm", "text/html; charset=US-ASCII"),
236 ExtMime(".html", "text/html; charset=US-ASCII"),
238 // archives
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('/');
256 if (stp < 0) break;
257 fname = fname[stp+1..$].xstrip;
260 void putFName () {
261 prepared ~= '"';
262 if (fname.length == 0) {
263 prepared ~= "unnamed";
264 } else {
265 foreach (char ch; fname) {
266 if (ch <= 32 || ch >= 127 || ch == '/' || ch == '\\' || ch == '?' ||
267 ch == '*' || ch == '&' || ch == '|' || ch == '<' || ch == '>' ||
268 ch == '"' || ch == '\'')
270 ch = '_';
272 prepared ~= ch;
275 prepared ~= '"';
278 prepared ~= boundary;
279 prepared ~= "\r\n";
281 prepared ~= "Content-Disposition: attachment; filename=";
282 putFName();
283 prepared ~= "\r\n";
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)) {
290 mime = ee.mime;
291 break;
294 // check if it can be treated as a text
295 if (mime.length == 0) {
296 bool oktext = true;
297 foreach (immutable idx, immutable char ch; attach.data.getData) {
298 if (ch < 32) {
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) {
302 oktext = false;
303 break;
306 if (oktext) {
307 mime = "text/plain; charset=US-ASCII";
308 tenc = "8bit";
309 } else {
310 mime = "application/octet-stream";
314 prepared ~= "Content-Type: ";
315 prepared ~= mime;
316 prepared ~= "; name=";
317 putFName();
318 prepared ~= "\r\n";
319 prepared ~= "Content-Transfer-Encoding: ";
320 prepared ~= tenc;
321 prepared ~= "\r\n";
322 prepared ~= "\r\n"; // end of headers
323 if (tenc == "base64") {
324 prepared.appendB64Encoded(attach.data);
325 } else {
326 assert(tenc == "8bit");
327 prepared ~= attach.data;
332 void finishPrepared () {
333 // final boundary
334 if (attaches.length != 0) {
335 prepared ~= "--";
336 prepared ~= boundary;
337 prepared ~= "\r\n";
339 // final dot
340 prepared ~= ".\r\n";
343 public:
344 //this () pure nothrow @trusted @nogc {}
346 @disable this (this);
348 ~this () {
349 foreach (ref Reference reference; references) delete reference;
350 delete references;
351 foreach (ref Attach attach; attaches) delete attach;
352 delete attaches;
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;
373 att.name = filename;
374 att.mime = mime;
375 att.data = cast(const(char)[])data;
376 attaches ~= att;
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;
385 att.name = filename;
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]);
391 attaches ~= att;
394 // should be called after setting everything
395 // WARNING! returned data will NOT outlive this struct!
396 const(char)[] getPrepared () {
397 buildHeaders();
398 appendBody();
399 appendAttaches();
400 finishPrepared();
401 return prepared.getData;