sqlite: accept "binary" encoding (idiotic lj sends this sometimes)
[chiroptera.git] / account.d
blobd5b93f87059123005ae5d79aaa9d622b2ec4dfcc
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 account /*is aliced*/;
19 import arsd.simpledisplay;
21 import iv.alice;
22 import iv.cmdcon;
23 import iv.encoding;
24 import iv.strex;
25 import iv.vfs;
27 import egui;
28 import maildb;
30 import chievents;
31 import folder;
32 import filterengine;
35 // ////////////////////////////////////////////////////////////////////////// //
36 public class UpdatingAccountEvent { string accName; this (string aaccName) { accName = aaccName; } }
37 public class UpdatingAccountCompleteEvent { string accName; this (string aaccName) { accName = aaccName; } }
40 // ////////////////////////////////////////////////////////////////////////// //
41 abstract class Account {
42 protected import core.time;
44 protected:
45 bool smtpNoAuth;
46 MonoTime lastUpdateTime = MonoTime.zero;
47 bool mUpdateForced;
48 Article[] sendQueue;
49 bool defaultFlag;
51 public:
52 string name;
53 string user; // on server
54 string pass;
55 string server;
56 string sendserver;
57 string realname;
58 bool debugdump;
59 int checkminutes; // <=0: never
60 string mail;
62 string inboxFolder;
63 bool mUpdating;
65 protected:
66 this () {
67 lastUpdateTime = MonoTime.currTime;
70 Folder getInboxFolder () nothrow @nogc {
71 foreach (Folder fld; folders) if (fld.folderPath == inboxFolder) return fld;
72 return null;
75 protected:
76 final @property string sendQueueFileName () {
77 import std.path : buildPath;
78 if (inboxFolder.length == 0) assert(0, "empty inbox folder, wtf?!");
79 return buildPath(mailRootDir, inboxFolder, "sendqueue.eml");
82 bool fixSendingArticleHeaders (Folder fld, Article art) {
83 art.removeHeader("From");
84 if (realname.length) {
85 art.prependHeader("From", encodeq(realname)~" <"~mail~">");
86 } else {
87 art.prependHeader("From", mail);
89 art.genMsgId();
90 art.replaceHeader("Message-ID", art.msgid);
91 string subj = art.subj.xstrip;
92 if (subj.length == 0) subj = "no subject";
93 art.replaceHeader("Subject", encodeq(subj));
94 art.replaceHeader("Mime-Version", "1.0");
95 art.replaceHeader("Content-Type", "text/plain; charset=utf-8; format=flowed; delsp=no");
96 art.replaceHeader("Content-Transfer-Encoding", "8bit");
97 art.replaceHeader("User-Agent", "Chiroptera");
98 //if (art.inreplyto) art.replaceHeader("In-Reply-To", art.inreplyto);
99 if (art.getHeaderValue("Date").length == 0) art.replaceHeader("Date", CurrTimeToRFCString);
101 // build references
102 auto repto = art.getInReplyTo();
103 if (repto.length) {
104 string refs;
105 bool hadReplyTo = false;
107 void addRef (const(char)[] s) {
108 s = s.xstrip;
109 if (s.length == 0) return;
110 if (!hadReplyTo && s == repto) hadReplyTo = true;
111 if (refs.length) refs ~= ' ';
112 refs ~= s;
115 void procRefs (const(char)[] s) {
116 for (;;) {
117 s = s.xstrip;
118 if (s.length == 0) break;
119 usize pos = 0;
120 while (pos < s.length && s[pos] > ' ') ++pos;
121 assert(pos > 0);
122 addRef(s[0..pos]);
123 s = s[pos..$];
127 addRef(art.getHeaderValue("References"));
128 if (!hadReplyTo) addRef(repto);
130 if (refs.length) art.replaceHeader("References", refs);
133 return true;
136 bool doPostArticle (Article art);
138 protected:
139 final void markAsUpdatedNoLock () @nogc { lastUpdateTime = MonoTime.currTime; mUpdateForced = false; }
141 void doSendingNL () {
142 if (sendQueue.length == 0) return;
143 bool sqchanged = false;
144 // send queued articles
145 usize idx = 0;
146 while (idx < sendQueue.length) {
147 //conwriteln(" POSTING #", idx+1, " of ", sendQueue.length, "...");
148 if (doPostArticle(sendQueue[idx])) {
149 // remove from queue
150 foreach (immutable cc; idx+1..sendQueue.length) sendQueue[cc-1] = sendQueue[cc];
151 sendQueue[$-1] = null; // so GC can collect it
152 sendQueue.length -= 1;
153 sendQueue.assumeSafeAppend;
154 sqchanged = true;
155 } else {
156 ++idx;
159 if (sqchanged) saveSendQueue();
160 //conwriteln("============");
163 public:
164 final void forceUpdating () @nogc { synchronized(this) { lastUpdateTime = MonoTime.zero; mUpdateForced = true; } }
165 final void forceUpdated () @nogc { synchronized(this) { lastUpdateTime = MonoTime.currTime; mUpdateForced = false; } }
167 final bool needUpdate () @nogc {
168 synchronized(this) {
169 if (mUpdating) return false;
170 if (checkminutes < 1 && !mUpdateForced) return false;
171 if (mUpdateForced) return true;
172 auto ctt = MonoTime.currTime;
173 if ((ctt-lastUpdateTime).total!"minutes" < checkminutes) return false;
175 return true;
178 final @property bool updating () nothrow @nogc { bool res; try { synchronized(this) res = mUpdating; } catch (Exception e) {} return res; /*fuck vanilla!*/ }
180 final void loadSendQueue () {
181 import std.file : exists;
182 string fname = sendQueueFileName;
183 if (!fname.exists) return;
184 try {
185 auto fl = VFile(fname);
186 for (;;) {
187 auto art = loadArticleText(fl);
188 if (art is null || !art.valid) break;
189 // hack: clear "loaded" flags
190 art.markAsStandalone();
191 sendQueue ~= art;
193 } catch (Exception e) {
194 conwriteln("ERROR loading send queue for account '", name, "'");
196 if (sendQueue.length) conwriteln("[", name, "]: ", sendQueue.length, " message", (sendQueue.length > 1 ? "s" : ""), " in send queue");
197 if (sendQueue.length) forceUpdating();
200 final void saveSendQueue () {
201 import std.file : exists;
202 string fname = sendQueueFileName;
203 try {
204 auto fl = VFile(fname, "w");
205 foreach (Article art; sendQueue) {
206 fl.writeArticteText(art);
207 art.markAsStandalone();
209 } catch (Exception e) {
210 conwriteln("ERROR writing send queue for account '", name, "'");
214 public:
215 // should be called ONLY from updater thread!
216 void update ();
218 // this method should insert the article into `fld` (or another folder) if necessary
219 // next `update` should send articles (so the method may mark account for immediate update)
220 // art is "free" and can be modified freely
221 // WARNING! `fld` is NOT locked!
222 bool addToSendQueue (Folder fld, Article art) {
223 if (!art.isStandalone) { conwriteln("ERROR: can't add non-standalone article to send queue!"); return false; }
224 if (!art.contentLoaded) { conwriteln("ERROR: can't add article without content to send queue!"); return false; }
225 synchronized(this) {
226 if (!fixSendingArticleHeaders(fld, art)) return false;
227 art.setupFromHeaders!true();
228 try {
229 art.appendAttachments();
230 } catch (Exception e) {
231 conwriteln("ERROR adding attachments: ", e.msg);
232 return false;
234 sendQueue ~= art;
235 saveSendQueue();
236 forceUpdating(); // so it will be sent
237 postDoCheckBoxesCycle(); // and do it now
238 return true;
242 protected:
243 final bool parseCommonOption (ref ConString[] options) {
244 if (options.length == 0) return false;
245 ConString opt = options[0];
246 if (opt == "debuglog" || opt == "debug_log") { this.debugdump = true; options = options[1..$]; return true; }
247 if (opt == "no_debuglog" || opt == "no_debug_log") { this.debugdump = false; options = options[1..$]; return true; }
248 if (opt == "smtp_no_auth" || opt == "smtp_noauth") { this.smtpNoAuth = true; options = options[1..$]; return true; }
249 if (opt == "default") { this.defaultFlag = true; options = options[1..$]; return true; }
250 ConString arg;
251 switch (opt) {
252 case "user":
253 if (this.user.length) { conwriteln("ERROR: account '", this.name, "': duplicate option '", opt, "'"); throw new Exception("option error"); }
254 if (options.length < 2) { conwriteln("ERROR: account '", this.name, "': no argument for option '", opt, "'"); throw new Exception("option error"); }
255 arg = options[1];
256 if (arg.length == 0) { conwriteln("ERROR: account '", this.name, "': empty option '", opt, "'"); throw new Exception("option error"); }
257 options = options[2..$];
258 this.user = arg.idup;
259 return true;
260 case "pass":
261 case "password":
262 if (this.pass.length) { conwriteln("ERROR: account '", this.name, "': duplicate option '", opt, "'"); throw new Exception("option error"); }
263 if (options.length < 2) { conwriteln("ERROR: account '", this.name, "': no argument for option '", opt, "'"); throw new Exception("option error"); }
264 arg = options[1];
265 if (arg.length == 0) { conwriteln("ERROR: account '", this.name, "': empty option '", opt, "'"); throw new Exception("option error"); }
266 options = options[2..$];
267 this.pass = arg.idup;
268 return true;
269 case "name":
270 case "realname":
271 case "real_name":
272 if (this.realname.length) { conwriteln("ERROR: account '", this.name, "': duplicate option '", opt, "'"); throw new Exception("option error"); }
273 if (options.length < 2) { conwriteln("ERROR: account '", this.name, "': no argument for option '", opt, "'"); throw new Exception("option error"); }
274 arg = options[1];
275 if (arg.length == 0) { conwriteln("ERROR: account '", this.name, "': empty option '", opt, "'"); throw new Exception("option error"); }
276 options = options[2..$];
277 this.realname = arg.idup;
278 return true;
279 case "server":
280 if (this.server.length) { conwriteln("ERROR: account '", this.name, "': duplicate option '", opt, "'"); throw new Exception("option error"); }
281 if (options.length < 2) { conwriteln("ERROR: account '", this.name, "': no argument for option '", opt, "'"); throw new Exception("option error"); }
282 arg = options[1];
283 if (arg.length == 0) { conwriteln("ERROR: account '", this.name, "': empty option '", opt, "'"); throw new Exception("option error"); }
284 options = options[2..$];
285 this.server = arg.idup;
286 return true;
287 case "smtpserver":
288 case "smtp_server":
289 if (this.sendserver.length) { conwriteln("ERROR: account '", this.name, "': duplicate option '", opt, "'"); throw new Exception("option error"); }
290 if (options.length < 2) { conwriteln("ERROR: account '", this.name, "': no argument for option '", opt, "'"); throw new Exception("option error"); }
291 arg = options[1];
292 if (arg.length == 0) { conwriteln("ERROR: account '", this.name, "': empty option '", opt, "'"); throw new Exception("option error"); }
293 options = options[2..$];
294 this.sendserver = arg.idup;
295 return true;
296 case "mail":
297 case "email":
298 case "e-mail":
299 if (this.mail.length) { conwriteln("ERROR: account '", this.name, "': duplicate option '", opt, "'"); throw new Exception("option error"); }
300 if (options.length < 2) { conwriteln("ERROR: account '", this.name, "': no argument for option '", opt, "'"); throw new Exception("option error"); }
301 arg = options[1];
302 if (arg.length == 0) { conwriteln("ERROR: account '", this.name, "': empty option '", opt, "'"); throw new Exception("option error"); }
303 options = options[2..$];
304 this.mail = arg.idup;
305 return true;
306 case "checktime":
307 case "check_time":
308 if (options.length < 2) { conwriteln("ERROR: account '", this.name, "': no argument for option '", opt, "'"); throw new Exception("option error"); }
309 arg = options[1];
310 if (arg.length == 0) { conwriteln("ERROR: account '", this.name, "': empty option '", opt, "'"); throw new Exception("option error"); }
311 options = options[2..$];
312 import std.conv : to;
313 auto ctm = arg.to!int;
314 if (ctm < 0) ctm = 0;
315 this.checkminutes = ctm;
316 return true;
317 default:
318 break;
320 return false;
323 void parseOptions (ConString[] options);
324 void checkParsedOptions ();
326 void accountBeforeAdd () {
327 import std.file : mkdirRecurse;
328 import std.path : buildPath;
329 if (inboxFolder.length == 0 || inboxFolder[0] == '/' || inboxFolder[$-1] == '/') { conwriteln("ERROR: account '", name, "': invalid inbox folder: '", inboxFolder, "'"); throw new Exception("option error"); }
330 mkdirRecurse(buildPath(mailRootDir, inboxFolder));
331 if (sendserver.length == 0) sendserver = server;
332 loadSendQueue();
337 // ////////////////////////////////////////////////////////////////////////// //
338 public final class Pop3Account : Account {
339 private:
340 private this (ConString aname) {
341 inboxFolder = "accounts/";
342 immutable xlen = inboxFolder.length;
343 foreach (char ch; aname) {
344 if (ch == '/' || ch == '\\') {
345 if (inboxFolder.length > 0 && inboxFolder[$-1] != '/') inboxFolder ~= '/';
346 } else {
347 inboxFolder ~= ch;
350 if (inboxFolder.length <= xlen || inboxFolder[$-1] == '/') throw new Exception("invalid inbox folder");
351 name = inboxFolder[xlen..$];
352 inboxFolder ~= "/inbox";
353 super();
356 public:
357 // this method should insert the article into `fld` (or another folder) if necessary
358 // next `update` should send articles (so the method may mark account for immediate update)
359 // art is "free" and can be modified freely
360 // WARNING! `fld` is NOT locked!
361 override bool addToSendQueue (Folder fld, Article art) {
362 if (super.addToSendQueue(fld, art)) {
363 if (fld !is null) {
364 auto newart = art.clone();
365 newart.setupFromHeaders!true();
366 fld.withBaseWriter((abase) {
367 if (abase.insert(newart)) abase.writeUpdates();
369 fld.markForRebuild();
370 fld.buildVisibleList();
371 postScreenRebuild();
373 return true;
375 return true;
378 // should be called ONLY from updater thread!
379 override void update () {
380 scope(exit) {
381 synchronized(this) {
382 mUpdating = false;
383 markAsUpdatedNoLock();
387 synchronized(this) {
388 mUpdating = true;
389 doSendingNL();
392 Folder inbox = getInboxFolder;
393 if (inbox is null) {
394 conwriteln("*** ERROR: [", name, "]: no inbox!");
395 return;
399 conwriteln("*** [", name, "]: connecting...");
400 auto pop3 = new SocketPOP3(server, debugdump);
401 scope(exit) pop3.close();
402 conwriteln("[", name, "]: authenticating...");
403 pop3.auth(user, pass);
404 auto newmsg = pop3.getNewMailCount;
405 if (newmsg == 0) {
406 conwriteln("[", name, "]: no new messages");
407 return;
409 conwriteln("[", name, "]: ", newmsg, " new message", (newmsg > 1 ? "s" : ""));
410 foreach (immutable int popidx; 1..newmsg+1) {
411 import core.time;
412 import std.datetime;
413 auto msg = pop3.getMessage(popidx);
415 conwriteln("===============================");
416 foreach (string s; msg) conwriteln(s, "|");
417 conwriteln("-------------------------------");
419 auto art = parseMessage(msg);
420 pop3.deleteMessage(popidx);
422 if (debugdump) {
423 conwriteln("From: ", art.fromname, " <", art.frommail, ">");
424 conwriteln("Subj: ", art.subj.recodeToKOI8);
425 conwriteln("Date: ", SysTime.fromUnixTime(art.time));
426 conwriteln("MID : ", art.msgid);
427 conwriteln("RID : ", art.getInReplyTo());
429 auto eml = new IncomingEmailMessage(art.lines);
430 conwriteln("-------------------------------");
431 conwriteln("From: ", eml.from);
432 conwriteln("Subj: ", eml.subject.recodeToKOI8);
433 conwriteln(eml.textMessageBody.recodeToKOI8);
437 art.unread = true;
439 FilterData fda;
440 fda.art = art;
441 fda.destfolder = inboxFolder;
443 doFiltering!"pre"(fda);
444 if (fda.destfolder.length == 0) {
445 // deleted
446 conwriteln("*DROP: ", art.fromname.recodeToKOI8, " <", art.frommail, "> (", art.subj.recodeToKOI8, ")");
447 continue;
450 Bogo bogo = fda.bogo;
452 if (bogo == Bogo.Error) bogo = articleBogoCheck(art);
454 final switch (bogo) {
455 case Bogo.Error: break;
456 case Bogo.Ham:
457 art.ham = true;
458 break;
459 case Bogo.Unsure:
460 art.spamUnsure = true;
461 break;
462 case Bogo.Spam:
463 art.spam = true;
464 art.unread = false;
465 break;
467 if (fda.markRead) art.unread = false;
469 if (!art.spam) {
470 doFiltering!"post"(fda);
471 } else {
472 fda.destfolder = "zz_spam";
475 if (fda.destfolder.length == 0) {
476 // deleted
477 conwriteln("*DROP: ", art.fromname.recodeToKOI8, " <", art.frommail, "> (", art.subj.recodeToKOI8, ")");
478 continue;
481 while (fda.destfolder.length && fda.destfolder[0] == '/') fda.destfolder = fda.destfolder[1..$];
482 while (fda.destfolder.length && fda.destfolder[$-1] == '/') fda.destfolder = fda.destfolder[0..$-1];
483 if (fda.destfolder.length == 0) fda.destfolder = inboxFolder;
485 if (fda.destfolder != inboxFolder) {
486 import std.file : mkdirRecurse;
487 import std.path : buildPath;
488 conwriteln("*** MOVE TO: [", fda.destfolder, "]");
489 // find folder to move
490 Folder xfolder;
491 foreach (Folder fld; folders) if (fld.folderPath == fda.destfolder) { xfolder = fld; break; }
492 if (xfolder is null) {
493 conwriteln(" *** ERROR: [", name, "]: folder [", fda.destfolder, "] not found!");
494 } else if (xfolder !is inbox) {
495 try {
496 xfolder.withBaseWriter((abase) {
497 auto aidx = abase.insert(art);
498 if (aidx && fda.softDelete) abase.softDeleted(aidx, true);
499 if (xfolder.isTwittedByArtIndexNL(aidx) !is null) abase.setRead(aidx);
500 //abase.selfCheck();
501 abase.writeUpdates();
502 conwriteln(" ", abase.length-1, " messages in database (", abase.aliveCount, " alive)");
504 art.releaseContent();
505 xfolder.markForRebuild();
506 continue;
507 } catch (Exception e) {
508 conwriteln("MOVE ERROR: ", e.msg);
513 inbox.withBaseWriter((abase) {
514 auto aidx = abase.insert(art);
515 if (aidx && fda.softDelete) abase.softDeleted(aidx, true);
516 if (inbox.isTwittedByArtIndexNL(aidx) !is null) abase.setRead(aidx);
517 abase.writeUpdates();
519 art.releaseContent();
520 inbox.markForRebuild();
523 conwriteln("[", name, "]: ", inbox.length, " messages in database (", inbox.aliveCount, " alive)");
525 inbox.withBaseWriter((abase) {
526 abase.selfCheck();
527 abase.writeUpdates();
532 protected:
533 static const(char)[] extractToMail (Article art) {
534 if (art is null || !art.valid) return null;
535 //conwriteln("x0: ", art.headers);
536 auto dest = art.getHeaderValue("To");
537 //conwriteln("00: [", dest, "]");
538 auto atpos = dest.lastIndexOf('@');
539 if (atpos <= 0 || dest.length-atpos < 3) return null;
540 auto spos = atpos;
541 while (spos > 0 && dest[spos-1] != '<') --spos;
542 if (spos == atpos) return null;
543 auto epos = spos+1;
544 while (epos < dest.length && dest[epos] != '>') ++epos;
545 if (epos < dest.length && dest.length-epos != 1) return null;
546 return dest[spos..epos];
549 // WARNING! `fld` is NOT locked!
550 override bool fixSendingArticleHeaders (Folder fld, Article art) {
551 if (!super.fixSendingArticleHeaders(fld, art)) return false;
552 return (extractToMail(art).length != 0);
555 override bool doPostArticle (Article art) {
556 assert(art !is null);
558 string from = art.frommail;
559 if (from.length == 0) {
560 conwriteln("SMTP ERROR: no FROM!");
561 return false;
564 auto to = extractToMail(art);
565 if (to.length == 0) {
566 conwriteln("SMTP ERROR: no TO!");
567 return false;
570 conwriteln("[", name, "]: trying to send mail from <", from, "> to <", to, "> using ", sendserver);
572 SocketSMTP nsk;
573 try {
574 nsk = new SocketSMTP(sendserver, /*debugdump*/true);
575 } catch (Exception e) {
576 conwriteln("[", name, "]: connection error: ", e.msg);
577 return false;
579 scope(exit) nsk.close();
581 try {
582 if (!smtpNoAuth) nsk.auth(mail, user, pass);
583 nsk.sendMessage(from, to, art.getTextToSend);
584 } catch (Exception e) {
585 conwriteln("[", name, "]: sending error: ", e.msg);
586 return false;
589 return true;
592 protected:
593 override void parseOptions (ConString[] options) {
594 while (options.length) {
595 if (parseCommonOption(options)) continue;
596 conwriteln("ERROR: account '", name, "': unknown option '", options[0], "'");
597 throw new Exception("option error");
601 override void checkParsedOptions () {
602 if (inboxFolder.length == 0 || inboxFolder[0] == '/' || inboxFolder[$-1] == '/') { conwriteln("ERROR: account '", name, "': invalid inbox folder: '", inboxFolder, "'"); throw new Exception("option error"); }
603 if (user.length == 0) { conwriteln("ERROR: account '", name, "': no user"); throw new Exception("option error"); }
604 if (server.length == 0) { conwriteln("ERROR: account '", name, "': no server"); throw new Exception("option error"); }
605 if (mail.length == 0) { conwriteln("ERROR: account '", name, "': no mail"); throw new Exception("option error"); }
608 override void accountBeforeAdd () { super.accountBeforeAdd(); }
612 // ////////////////////////////////////////////////////////////////////////// //
613 public final class NntpAccount : Account {
614 public:
615 string group; // DO NOT MODIFY!
617 private:
618 private this (ConString aname) {
619 if (aname.length == 0) throw new Exception("empty account name");
620 name = aname.idup;
621 super();
624 public:
625 // should be called ONLY from updater thread!
626 override void update () {
627 scope(exit) {
628 synchronized(this) {
629 mUpdating = false;
630 markAsUpdatedNoLock();
634 synchronized(this) {
635 mUpdating = true;
636 doSendingNL();
639 Folder inbox = getInboxFolder;
640 if (inbox is null) {
641 conwriteln("*** ERROR: [", name, ":", group, "]: no inbox!");
642 return;
646 conwriteln("*** [", name, ":", group, "]: connecting...");
647 auto nsk = new SocketNNTP(server, debugdump);
648 scope(exit) nsk.close();
650 nsk.selectGroup(group);
651 if (nsk.emptyGroup) {
652 conwriteln("[", name, ":", group, "]: no new articles");
653 return;
655 if (debugdump) conwriteln("[", name, ":", group, "]: lo=", nsk.lowater, "; hi=", nsk.hiwater, "; lastloaded=", inbox.maxNntpIndex);
657 uint stnum = inbox.maxNntpIndex+1;
658 if (stnum == 0) stnum = (nsk.hiwater > 1023 ? nsk.hiwater-1023 : 0);
659 if (stnum > nsk.hiwater) {
660 conwriteln("[", name, ":", group, "]: no new articles");
661 return;
664 conwriteln("[", name, ":", group, "]: ", nsk.hiwater+1-stnum, " (possible) new articles");
666 // download new articles
667 //bool wantNewLine = false;
668 foreach (immutable uint anum; stnum..nsk.hiwater+1) {
669 import core.time;
670 import std.datetime;
671 auto msg = nsk.getArticle(anum);
672 Article art;
674 try {
675 art = parseMessage(msg);
676 } catch (Exception e) {
677 conwriteln("=== ERROR: CANNOT PARSE MESSAGE ===\n", e.msg, "\n---\n", msg, "\n---\n", e.toString);
678 continue;
681 if (debugdump || true) {
682 conwriteln("From: ", art.fromname, " <", art.frommail, ">");
683 conwriteln("Subj: ", art.subj.recodeToKOI8);
684 conwriteln("Date: ", SysTime.fromUnixTime(art.time));
685 conwriteln("MID : ", art.msgid);
686 conwriteln("RID : ", art.getInReplyTo());
688 if (art.nntpindex == 0) throw new Exception("NNTP article without number!");
690 art.unread = true;
692 //TODO: separate filtering system
694 inbox.withBaseWriter((abase) {
695 auto aidx = abase.insert(art);
696 auto twt = inbox.isTwittedByArtIndexNL(aidx);
697 conwriteln("DB INDEX: ", aidx, " (", abase.length, ")");
698 if (twt !is null) {
699 import iv.encoding;
700 conwriteln(" ***FILTERED BY TWIT FILTER (", twt.recodeToKOI8, ")");
701 abase.setRead(aidx);
703 abase.writeUpdates();
705 art.releaseContent();
706 inbox.markForRebuild();
709 conwriteln("[", name, ":", group, "]: ", inbox.length, " articles in database (", inbox.aliveCount, " alive)");
711 inbox.withBaseWriter((abase) {
712 abase.selfCheck();
713 abase.writeUpdates();
718 protected:
719 // WARNING! `fld` is NOT locked!
720 override bool fixSendingArticleHeaders (Folder fld, Article art) {
721 if (!super.fixSendingArticleHeaders(fld, art)) return false;
723 art.replaceHeader("Newsgroups", group);
724 art.removeHeader("To");
725 return true;
728 override bool doPostArticle (Article art) {
729 assert(art !is null);
731 SocketNNTP nsk;
732 try {
733 nsk = new SocketNNTP(server, debugdump);
734 } catch (Exception e) {
735 conwriteln("[", name, ":", group, "]: connection error: ", e.msg);
736 return false;
738 scope(exit) nsk.close();
740 try {
741 nsk.selectGroup(group);
742 nsk.doSend("POST");
743 nsk.doSendRaw(art.getTextToSend);
744 auto ln = nsk.readLine;
745 conwriteln(ln); // 340 Ok, recommended message-ID <o7dq4o$mpm$1@digitalmars.com>
746 if (ln.length == 0 || ln[0] != '3') throw new Exception(ln.idup);
747 } catch (Exception e) {
748 conwriteln("[", name, ":", group, "]: sending error: ", e.msg);
749 return false;
752 return true;
755 protected:
756 override void parseOptions (ConString[] options) {
757 while (options.length) {
758 if (parseCommonOption(options)) continue;
759 ConString opt = options[0];
760 ConString arg;
761 switch (opt) {
762 case "group":
763 if (group.length) { conwriteln("ERROR: account '", name, "': duplicate option '", opt, "'"); throw new Exception("option error"); }
764 if (options.length < 2) { conwriteln("ERROR: account '", name, "': no argument for option '", opt, "'"); throw new Exception("option error"); }
765 arg = options[1];
766 if (arg.length == 0) { conwriteln("ERROR: account '", name, "': empty option '", opt, "'"); throw new Exception("option error"); }
767 options = options[2..$];
768 foreach (char ch; arg) {
769 if (ch != '.' && !ch.isalnum) { conwriteln("ERROR: account '", name, "': invalid group name: '", arg, "'"); throw new Exception("option error"); }
771 group = arg.idup;
772 break;
773 case "folder":
774 if (inboxFolder.length) { conwriteln("ERROR: account '", name, "': duplicate option '", opt, "'"); throw new Exception("option error"); }
775 if (options.length < 2) { conwriteln("ERROR: account '", name, "': no argument for option '", opt, "'"); throw new Exception("option error"); }
776 arg = options[1];
777 if (arg.length == 0) { conwriteln("ERROR: account '", name, "': empty option '", opt, "'"); throw new Exception("option error"); }
778 options = options[2..$];
779 if (arg[0] == '/' || arg[$-1] == '/') { conwriteln("ERROR: account '", name, "': invalid folder name: '", arg, "'"); throw new Exception("option error"); }
780 inboxFolder = arg.idup;
781 break;
782 default:
783 conwriteln("ERROR: account '", name, "': unknown option '", options[0], "'");
784 throw new Exception("option error");
789 override void checkParsedOptions () {
790 if (inboxFolder.length == 0 || inboxFolder[0] == '/' || inboxFolder[$-1] == '/') { conwriteln("ERROR: account '", name, "': invalid inbox folder: '", inboxFolder, "'"); throw new Exception("option error"); }
791 if (group.length == 0) { conwriteln("ERROR: account '", name, "': no group"); throw new Exception("option error"); }
792 foreach (char ch; group) {
793 if (ch != '.' && !ch.isalnum) { conwriteln("ERROR: account '", name, "': invalid group name: '", group, "'"); throw new Exception("option error"); }
795 if (server.length == 0) { conwriteln("ERROR: account '", name, "': no server"); throw new Exception("option error"); }
796 if (mail.length == 0) { conwriteln("ERROR: account '", name, "': no mail"); throw new Exception("option error"); }
799 override void accountBeforeAdd () {
800 super.accountBeforeAdd();
801 if (auto inbox = getInboxFolder) inbox.hideOldThreads = true;
806 // ////////////////////////////////////////////////////////////////////////// //
807 __gshared Account[] accounts;
808 __gshared Account defaultAcc;
811 // ////////////////////////////////////////////////////////////////////////// //
812 Account findNntpAccountForFolder (Folder fld) nothrow @nogc {
813 if (fld is null) return null;
814 foreach (Account acc; accounts) {
815 if (auto nac = cast(NntpAccount)acc) {
816 if (fld is nac.getInboxFolder) return acc;
819 return null;
823 // ////////////////////////////////////////////////////////////////////////// //
824 Account accFindByMail (const(char)[] mail) nothrow @nogc {
825 mail = mail.xstrip;
826 if (mail.length == 0) return null;
827 foreach (Account acc; accounts) {
828 if (acc.mail.length == 0) continue;
829 if (acc.mail.strEquCI(mail)) return acc;
831 return null;
835 // ////////////////////////////////////////////////////////////////////////// //
836 Account accFindByFolder (Folder fld) nothrow @nogc {
837 if (fld is null) return null;
838 foreach (Account acc; accounts) {
839 if (acc.inboxFolder.strEquCI(fld.folderPath)) return acc;
841 return null;
845 // ////////////////////////////////////////////////////////////////////////// //
846 Account accFindNntpByFolder (Folder fld) nothrow @nogc {
847 if (fld is null) return null;
848 foreach (Account acc; accounts) {
849 if (auto nna = cast(NntpAccount)acc) {
850 if (nna.inboxFolder.strEquCI(fld.folderPath)) return acc;
853 return null;
857 // ////////////////////////////////////////////////////////////////////////// //
858 shared static this () {
859 //popbox name options...
860 // user username
861 // name realname
862 // mail email
863 // server [tls:]server
864 // smtpserver [tls:]server
865 // password password
866 // debuglog
867 // checktime minutes
868 conRegFunc!((ConString name, ConString[] options) {
869 try {
870 if (name.length == 0) { conwriteln("ERROR: empty account name!"); return; }
871 foreach (char ch; name) {
872 if (!ch.isalnum && ch != '.' && ch != '_' && ch != '-') { conwriteln("ERROR: invalid account name: '", name, "'"); return; }
874 auto acc = new Pop3Account(name);
875 acc.parseOptions(options);
876 acc.checkParsedOptions();
877 synchronized(Account.classinfo) {
878 foreach (ref Account a; accounts) if (a.name == acc.name) { conwriteln("ERROR: duplicate account name: '", name, "'"); return; }
879 acc.accountBeforeAdd();
880 conwriteln("POP3 server '", acc.name, "': ", acc.server, " (", acc.mail, ")");
881 accounts ~= acc;
882 if (acc.defaultFlag) defaultAcc = acc;
884 } catch (Exception e) {
885 conwriteln("ERROR: ", e.msg);
886 //conwriteln(e.toString);
888 })("popbox", "register POP3 mail box");
890 //nntpbox name options...
891 // user username
892 // name realname
893 // mail email
894 // server server
895 // password password
896 // folder inbox_folder (required!)
897 // group nntp_group_name (required!)
898 // debuglog
899 // checktime minutes
900 conRegFunc!((ConString name, ConString[] options) {
901 try {
902 if (name.length == 0) { conwriteln("ERROR: empty account name!"); return; }
903 foreach (char ch; name) {
904 if (!ch.isalnum && ch != '.' && ch != '_' && ch != '-') { conwriteln("ERROR: invalid account name: '", name, "'"); return; }
906 auto acc = new NntpAccount(name);
907 acc.parseOptions(options);
908 acc.checkParsedOptions();
909 synchronized(Account.classinfo) {
910 foreach (ref Account a; accounts) if (a.name == acc.name) { conwriteln("ERROR: duplicate account name: '", name, "'"); return; }
911 acc.accountBeforeAdd();
912 conwriteln("NNTP server: ", acc.server, " (", acc.group, ")");
913 accounts ~= acc;
915 } catch (Exception e) {
916 conwriteln("ERROR: ", e.msg);
917 //conwriteln(e.toString);
919 })("nntpbox", "register NNTP box");