using GxRect and GxPoint for some coord manupulation (and clipping rectangle)
[chiroptera.git] / account.d
blobc78d7f77beb07bec54a5271469e5875328e10f42
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, either version 3 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 module account is aliced;
20 import arsd.simpledisplay;
22 import iv.cmdcon;
23 import iv.encoding;
24 import iv.strex;
25 import iv.vfs;
27 import maildb;
28 import folder;
29 import filterengine;
31 import egui;
34 // ////////////////////////////////////////////////////////////////////////// //
35 public class UpdatingAccountEvent { string accName; this (string aaccName) { accName = aaccName; } }
36 public class UpdatingAccountCompleteEvent { string accName; this (string aaccName) { accName = aaccName; } }
39 // ////////////////////////////////////////////////////////////////////////// //
40 abstract class Account {
41 protected import core.time;
43 protected:
44 bool smtpNoAuth;
45 MonoTime lastUpdateTime = MonoTime.zero;
46 bool mUpdateForced;
47 Article[] sendQueue;
48 bool defaultFlag;
50 public:
51 string name;
52 string user; // on server
53 string pass;
54 string server;
55 string sendserver;
56 string realname;
57 bool debugdump;
58 int checkminutes; // <=0: never
59 string mail;
61 string inboxFolder;
62 bool mUpdating;
64 protected:
65 this () {
66 lastUpdateTime = MonoTime.currTime;
69 Folder getInboxFolder () nothrow @nogc {
70 foreach (Folder fld; folders) if (fld.folderPath == inboxFolder) return fld;
71 return null;
74 protected:
75 final @property string sendQueueFileName () {
76 import std.path : buildPath;
77 if (inboxFolder.length == 0) assert(0, "empty inbox folder, wtf?!");
78 return buildPath(mailRootDir, inboxFolder, "sendqueue.eml");
81 bool fixSendingArticleHeaders (Folder fld, Article art) {
82 art.removeHeader("From");
83 if (realname.length) {
84 art.prependHeader("From", encodeq(realname)~" <"~mail~">");
85 } else {
86 art.prependHeader("From", mail);
88 art.genMsgId();
89 art.replaceHeader("Message-ID", art.msgid);
90 string subj = art.subj.xstrip;
91 if (subj.length == 0) subj = "no subject";
92 art.replaceHeader("Subject", encodeq(subj));
93 art.replaceHeader("Mime-Version", "1.0");
94 art.replaceHeader("Content-Type", "text/plain; charset=utf-8; format=flowed; delsp=no");
95 art.replaceHeader("Content-Transfer-Encoding", "8bit");
96 art.replaceHeader("User-Agent", "Chiroptera");
97 //if (art.inreplyto) art.replaceHeader("In-Reply-To", art.inreplyto);
98 if (art.getHeaderValue("Date").length == 0) {
99 import std.datetime;
100 art.replaceHeader("Date", Clock.currTime().SysTimetoRFCString);
103 // build references
104 auto repto = art.getInReplyTo();
105 if (repto.length) {
106 string refs;
107 bool hadReplyTo = false;
109 void addRef (const(char)[] s) {
110 s = s.xstrip;
111 if (s.length == 0) return;
112 if (!hadReplyTo && s == repto) hadReplyTo = true;
113 if (refs.length) refs ~= ' ';
114 refs ~= s;
117 void procRefs (const(char)[] s) {
118 for (;;) {
119 s = s.xstrip;
120 if (s.length == 0) break;
121 usize pos = 0;
122 while (pos < s.length && s[pos] > ' ') ++pos;
123 assert(pos > 0);
124 addRef(s[0..pos]);
125 s = s[pos..$];
129 addRef(art.getHeaderValue("References"));
130 if (!hadReplyTo) addRef(repto);
132 if (refs.length) art.replaceHeader("References", refs);
135 return true;
138 bool doPostArticle (Article art);
140 protected:
141 final void markAsUpdatedNoLock () @nogc { lastUpdateTime = MonoTime.currTime; mUpdateForced = false; }
143 void doSendingNL () {
144 if (sendQueue.length == 0) return;
145 bool sqchanged = false;
146 // send queued articles
147 usize idx = 0;
148 while (idx < sendQueue.length) {
149 //conwriteln(" POSTING #", idx+1, " of ", sendQueue.length, "...");
150 if (doPostArticle(sendQueue[idx])) {
151 // remove from queue
152 foreach (immutable cc; idx+1..sendQueue.length) sendQueue[cc-1] = sendQueue[cc];
153 sendQueue[$-1] = null; // so GC can collect it
154 sendQueue.length -= 1;
155 sendQueue.assumeSafeAppend;
156 sqchanged = true;
157 } else {
158 ++idx;
161 if (sqchanged) saveSendQueue();
162 //conwriteln("============");
165 public:
166 final void forceUpdating () @nogc { synchronized(this) { lastUpdateTime = MonoTime.zero; mUpdateForced = true; } }
167 final void forceUpdated () @nogc { synchronized(this) { lastUpdateTime = MonoTime.currTime; mUpdateForced = false; } }
169 final bool needUpdate () @nogc {
170 synchronized(this) {
171 if (mUpdating) return false;
172 if (checkminutes < 1 && !mUpdateForced) return false;
173 if (mUpdateForced) return true;
174 auto ctt = MonoTime.currTime;
175 if ((ctt-lastUpdateTime).total!"minutes" < checkminutes) return false;
177 return true;
180 final @property bool updating () nothrow @nogc { synchronized(this) return mUpdating; }
182 final void loadSendQueue () {
183 import std.file : exists;
184 string fname = sendQueueFileName;
185 if (!fname.exists) return;
186 try {
187 auto fl = VFile(fname);
188 for (;;) {
189 auto art = loadArticleText(fl);
190 if (art is null || !art.valid) break;
191 // hack: clear "loaded" flags
192 art.markAsStandalone();
193 sendQueue ~= art;
195 } catch (Exception e) {
196 conwriteln("ERROR loading send queue for account '", name, "'");
198 if (sendQueue.length) conwriteln("[", name, "]: ", sendQueue.length, " message", (sendQueue.length > 1 ? "s" : ""), " in send queue");
199 if (sendQueue.length) forceUpdating();
202 final void saveSendQueue () {
203 import std.file : exists;
204 string fname = sendQueueFileName;
205 try {
206 auto fl = VFile(fname, "w");
207 foreach (Article art; sendQueue) {
208 fl.writeArticteText(art);
209 art.markAsStandalone();
211 } catch (Exception e) {
212 conwriteln("ERROR writing send queue for account '", name, "'");
216 public:
217 // should be called ONLY from updater thread!
218 void update ();
220 // this method should insert the article into `fld` (or another folder) if necessary
221 // next `update` should send articles (so the method may mark account for immediate update)
222 // art is "free" and can be modified freely
223 // WARNING! `fld` is NOT locked!
224 bool addToSendQueue (Folder fld, Article art) {
225 if (!art.isStandalone) { conwriteln("ERROR: can't add non-standalone article to send queue!"); return false; }
226 if (!art.contentLoaded) { conwriteln("ERROR: can't add article without content to send queue!"); return false; }
227 synchronized(this) {
228 if (!fixSendingArticleHeaders(fld, art)) return false;
229 art.setupFromHeaders!true();
230 try {
231 art.appendAttachments();
232 } catch (Exception e) {
233 conwriteln("ERROR adding attachments: ", e.msg);
234 return false;
236 sendQueue ~= art;
237 saveSendQueue();
238 forceUpdating(); // so it will be sent
239 postDoCheckBoxesCycle(); // and do it now
240 return true;
244 protected:
245 final bool parseCommonOption (ref ConString[] options) {
246 if (options.length == 0) return false;
247 ConString opt = options[0];
248 if (opt == "debuglog" || opt == "debug_log") { this.debugdump = true; options = options[1..$]; return true; }
249 if (opt == "no_debuglog" || opt == "no_debug_log") { this.debugdump = false; options = options[1..$]; return true; }
250 if (opt == "smtp_no_auth" || opt == "smtp_noauth") { this.smtpNoAuth = true; options = options[1..$]; return true; }
251 if (opt == "default") { this.defaultFlag = true; options = options[1..$]; return true; }
252 ConString arg;
253 switch (opt) {
254 case "user":
255 if (this.user.length) { conwriteln("ERROR: account '", this.name, "': duplicate option '", opt, "'"); throw new Exception("option error"); }
256 if (options.length < 2) { conwriteln("ERROR: account '", this.name, "': no argument for option '", opt, "'"); throw new Exception("option error"); }
257 arg = options[1];
258 if (arg.length == 0) { conwriteln("ERROR: account '", this.name, "': empty option '", opt, "'"); throw new Exception("option error"); }
259 options = options[2..$];
260 this.user = arg.idup;
261 return true;
262 case "pass":
263 case "password":
264 if (this.pass.length) { conwriteln("ERROR: account '", this.name, "': duplicate option '", opt, "'"); throw new Exception("option error"); }
265 if (options.length < 2) { conwriteln("ERROR: account '", this.name, "': no argument for option '", opt, "'"); throw new Exception("option error"); }
266 arg = options[1];
267 if (arg.length == 0) { conwriteln("ERROR: account '", this.name, "': empty option '", opt, "'"); throw new Exception("option error"); }
268 options = options[2..$];
269 this.pass = arg.idup;
270 return true;
271 case "name":
272 case "realname":
273 case "real_name":
274 if (this.realname.length) { conwriteln("ERROR: account '", this.name, "': duplicate option '", opt, "'"); throw new Exception("option error"); }
275 if (options.length < 2) { conwriteln("ERROR: account '", this.name, "': no argument for option '", opt, "'"); throw new Exception("option error"); }
276 arg = options[1];
277 if (arg.length == 0) { conwriteln("ERROR: account '", this.name, "': empty option '", opt, "'"); throw new Exception("option error"); }
278 options = options[2..$];
279 this.realname = arg.idup;
280 return true;
281 case "server":
282 if (this.server.length) { conwriteln("ERROR: account '", this.name, "': duplicate option '", opt, "'"); throw new Exception("option error"); }
283 if (options.length < 2) { conwriteln("ERROR: account '", this.name, "': no argument for option '", opt, "'"); throw new Exception("option error"); }
284 arg = options[1];
285 if (arg.length == 0) { conwriteln("ERROR: account '", this.name, "': empty option '", opt, "'"); throw new Exception("option error"); }
286 options = options[2..$];
287 this.server = arg.idup;
288 return true;
289 case "smtpserver":
290 case "smtp_server":
291 if (this.sendserver.length) { conwriteln("ERROR: account '", this.name, "': duplicate option '", opt, "'"); throw new Exception("option error"); }
292 if (options.length < 2) { conwriteln("ERROR: account '", this.name, "': no argument for option '", opt, "'"); throw new Exception("option error"); }
293 arg = options[1];
294 if (arg.length == 0) { conwriteln("ERROR: account '", this.name, "': empty option '", opt, "'"); throw new Exception("option error"); }
295 options = options[2..$];
296 this.sendserver = arg.idup;
297 return true;
298 case "mail":
299 case "email":
300 case "e-mail":
301 if (this.mail.length) { conwriteln("ERROR: account '", this.name, "': duplicate option '", opt, "'"); throw new Exception("option error"); }
302 if (options.length < 2) { conwriteln("ERROR: account '", this.name, "': no argument for option '", opt, "'"); throw new Exception("option error"); }
303 arg = options[1];
304 if (arg.length == 0) { conwriteln("ERROR: account '", this.name, "': empty option '", opt, "'"); throw new Exception("option error"); }
305 options = options[2..$];
306 this.mail = arg.idup;
307 return true;
308 case "checktime":
309 case "check_time":
310 if (options.length < 2) { conwriteln("ERROR: account '", this.name, "': no argument for option '", opt, "'"); throw new Exception("option error"); }
311 arg = options[1];
312 if (arg.length == 0) { conwriteln("ERROR: account '", this.name, "': empty option '", opt, "'"); throw new Exception("option error"); }
313 options = options[2..$];
314 import std.conv : to;
315 auto ctm = arg.to!int;
316 if (ctm < 0) ctm = 0;
317 this.checkminutes = ctm;
318 return true;
319 default:
320 break;
322 return false;
325 void parseOptions (ConString[] options);
326 void checkParsedOptions ();
328 void accountBeforeAdd () {
329 import std.file : mkdirRecurse;
330 import std.path : buildPath;
331 if (inboxFolder.length == 0 || inboxFolder[0] == '/' || inboxFolder[$-1] == '/') { conwriteln("ERROR: account '", name, "': invalid inbox folder: '", inboxFolder, "'"); throw new Exception("option error"); }
332 mkdirRecurse(buildPath(mailRootDir, inboxFolder));
333 if (sendserver.length == 0) sendserver = server;
334 loadSendQueue();
339 // ////////////////////////////////////////////////////////////////////////// //
340 public final class Pop3Account : Account {
341 private:
342 private this (ConString aname) {
343 inboxFolder = "accounts/";
344 immutable xlen = inboxFolder.length;
345 foreach (char ch; aname) {
346 if (ch == '/' || ch == '\\') {
347 if (inboxFolder.length > 0 && inboxFolder[$-1] != '/') inboxFolder ~= '/';
348 } else {
349 inboxFolder ~= ch;
352 if (inboxFolder.length <= xlen || inboxFolder[$-1] == '/') throw new Exception("invalid inbox folder");
353 name = inboxFolder[xlen..$];
354 inboxFolder ~= "/inbox";
355 super();
358 public:
359 // this method should insert the article into `fld` (or another folder) if necessary
360 // next `update` should send articles (so the method may mark account for immediate update)
361 // art is "free" and can be modified freely
362 // WARNING! `fld` is NOT locked!
363 override bool addToSendQueue (Folder fld, Article art) {
364 if (super.addToSendQueue(fld, art)) {
365 if (fld !is null) {
366 auto newart = art.clone();
367 newart.setupFromHeaders!true();
368 fld.withBaseWriter((abase) {
369 if (abase.insert(newart)) abase.writeUpdates();
371 fld.markForRebuild();
372 fld.buildVisibleList();
373 postScreenRebuild();
375 return true;
377 return true;
380 // should be called ONLY from updater thread!
381 override void update () {
382 scope(exit) {
383 synchronized(this) {
384 mUpdating = false;
385 markAsUpdatedNoLock();
389 synchronized(this) {
390 mUpdating = true;
391 doSendingNL();
394 Folder inbox = getInboxFolder;
395 if (inbox is null) {
396 conwriteln("*** ERROR: [", name, "]: no inbox!");
397 return;
401 conwriteln("*** [", name, "]: connecting...");
402 auto pop3 = new SocketPOP3(server, debugdump);
403 scope(exit) pop3.close();
404 conwriteln("[", name, "]: authenticating...");
405 pop3.auth(user, pass);
406 auto newmsg = pop3.getNewMailCount;
407 if (newmsg == 0) {
408 conwriteln("[", name, "]: no new messages");
409 return;
411 conwriteln("[", name, "]: ", newmsg, " new message", (newmsg > 1 ? "s" : ""));
412 foreach (immutable int popidx; 1..newmsg+1) {
413 import core.time;
414 import std.datetime;
415 auto msg = pop3.getMessage(popidx);
417 conwriteln("===============================");
418 foreach (string s; msg) conwriteln(s, "|");
419 conwriteln("-------------------------------");
421 auto art = parseMessage(msg);
422 pop3.deleteMessage(popidx);
424 if (debugdump) {
425 conwriteln("From: ", art.fromname, " <", art.frommail, ">");
426 conwriteln("Subj: ", art.subj.recodeToKOI8);
427 conwriteln("Date: ", SysTime.fromUnixTime(art.time));
428 conwriteln("MID : ", art.msgid);
429 conwriteln("RID : ", art.getInReplyTo());
431 auto eml = new IncomingEmailMessage(art.lines);
432 conwriteln("-------------------------------");
433 conwriteln("From: ", eml.from);
434 conwriteln("Subj: ", eml.subject.recodeToKOI8);
435 conwriteln(eml.textMessageBody.recodeToKOI8);
439 art.unread = true;
441 FilterData fda;
442 fda.art = art;
443 fda.destfolder = inboxFolder;
445 doFiltering!"pre"(fda);
446 if (fda.destfolder.length == 0) {
447 // deleted
448 conwriteln("*DROP: ", art.fromname.recodeToKOI8, " <", art.frommail, "> (", art.subj.recodeToKOI8, ")");
449 continue;
452 Bogo bogo = fda.bogo;
454 if (bogo == Bogo.Error) bogo = articleBogoCheck(art);
456 final switch (bogo) {
457 case Bogo.Error: break;
458 case Bogo.Ham:
459 art.ham = true;
460 break;
461 case Bogo.Unsure:
462 art.spamUnsure = true;
463 break;
464 case Bogo.Spam:
465 art.spam = true;
466 art.unread = false;
467 break;
469 if (fda.markRead) art.unread = false;
471 if (!art.spam) {
472 doFiltering!"post"(fda);
473 } else {
474 fda.destfolder = "zz_spam";
477 if (fda.destfolder.length == 0) {
478 // deleted
479 conwriteln("*DROP: ", art.fromname.recodeToKOI8, " <", art.frommail, "> (", art.subj.recodeToKOI8, ")");
480 continue;
483 while (fda.destfolder.length && fda.destfolder[0] == '/') fda.destfolder = fda.destfolder[1..$];
484 while (fda.destfolder.length && fda.destfolder[$-1] == '/') fda.destfolder = fda.destfolder[0..$-1];
485 if (fda.destfolder.length == 0) fda.destfolder = inboxFolder;
487 if (fda.destfolder != inboxFolder) {
488 import std.file : mkdirRecurse;
489 import std.path : buildPath;
490 conwriteln("*** MOVE TO: [", fda.destfolder, "]");
491 // find folder to move
492 Folder xfolder;
493 foreach (Folder fld; folders) if (fld.folderPath == fda.destfolder) { xfolder = fld; break; }
494 if (xfolder is null) {
495 conwriteln(" *** ERROR: [", name, "]: folder [", fda.destfolder, "] not found!");
496 } else if (xfolder !is inbox) {
497 try {
498 xfolder.withBaseWriter((abase) {
499 auto aidx = abase.insert(art);
500 if (aidx && fda.softDelete) abase.softDeleted(aidx, true);
501 if (xfolder.isTwittedByArtIndexNL(aidx) !is null) abase.setRead(aidx);
502 //abase.selfCheck();
503 abase.writeUpdates();
504 conwriteln(" ", abase.length-1, " messages in database (", abase.aliveCount, " alive)");
506 art.releaseContent();
507 xfolder.markForRebuild();
508 continue;
509 } catch (Exception e) {
510 conwriteln("MOVE ERROR: ", e.msg);
515 inbox.withBaseWriter((abase) {
516 auto aidx = abase.insert(art);
517 if (aidx && fda.softDelete) abase.softDeleted(aidx, true);
518 if (inbox.isTwittedByArtIndexNL(aidx) !is null) abase.setRead(aidx);
519 abase.writeUpdates();
521 art.releaseContent();
522 inbox.markForRebuild();
525 conwriteln("[", name, "]: ", inbox.length, " messages in database (", inbox.aliveCount, " alive)");
527 inbox.withBaseWriter((abase) {
528 abase.selfCheck();
529 abase.writeUpdates();
534 protected:
535 static const(char)[] extractToMail (Article art) {
536 if (art is null || !art.valid) return null;
537 //conwriteln("x0: ", art.headers);
538 auto dest = art.getHeaderValue("To");
539 //conwriteln("00: [", dest, "]");
540 auto atpos = dest.lastIndexOf('@');
541 if (atpos <= 0 || dest.length-atpos < 3) return null;
542 auto spos = atpos;
543 while (spos > 0 && dest[spos-1] != '<') --spos;
544 if (spos == atpos) return null;
545 auto epos = spos+1;
546 while (epos < dest.length && dest[epos] != '>') ++epos;
547 if (epos < dest.length && dest.length-epos != 1) return null;
548 return dest[spos..epos];
551 // WARNING! `fld` is NOT locked!
552 override bool fixSendingArticleHeaders (Folder fld, Article art) {
553 if (!super.fixSendingArticleHeaders(fld, art)) return false;
554 return (extractToMail(art).length != 0);
557 override bool doPostArticle (Article art) {
558 assert(art !is null);
560 string from = art.frommail;
561 if (from.length == 0) {
562 conwriteln("SMTP ERROR: no FROM!");
563 return false;
566 auto to = extractToMail(art);
567 if (to.length == 0) {
568 conwriteln("SMTP ERROR: no TO!");
569 return false;
572 conwriteln("[", name, "]: trying to send mail from <", from, "> to <", to, "> using ", sendserver);
574 SocketSMTP nsk;
575 try {
576 nsk = new SocketSMTP(sendserver, /*debugdump*/true);
577 } catch (Exception e) {
578 conwriteln("[", name, "]: connection error: ", e.msg);
579 return false;
581 scope(exit) nsk.close();
583 try {
584 if (!smtpNoAuth) nsk.auth(mail, user, pass);
585 nsk.sendMessage(from, to, art.getTextToSend);
586 } catch (Exception e) {
587 conwriteln("[", name, "]: sending error: ", e.msg);
588 return false;
591 return true;
594 protected:
595 override void parseOptions (ConString[] options) {
596 while (options.length) {
597 if (parseCommonOption(options)) continue;
598 conwriteln("ERROR: account '", name, "': unknown option '", options[0], "'");
599 throw new Exception("option error");
603 override void checkParsedOptions () {
604 if (inboxFolder.length == 0 || inboxFolder[0] == '/' || inboxFolder[$-1] == '/') { conwriteln("ERROR: account '", name, "': invalid inbox folder: '", inboxFolder, "'"); throw new Exception("option error"); }
605 if (user.length == 0) { conwriteln("ERROR: account '", name, "': no user"); throw new Exception("option error"); }
606 if (server.length == 0) { conwriteln("ERROR: account '", name, "': no server"); throw new Exception("option error"); }
607 if (mail.length == 0) { conwriteln("ERROR: account '", name, "': no mail"); throw new Exception("option error"); }
610 override void accountBeforeAdd () { super.accountBeforeAdd(); }
614 // ////////////////////////////////////////////////////////////////////////// //
615 public final class NntpAccount : Account {
616 public:
617 string group; // DO NOT MODIFY!
619 private:
620 private this (ConString aname) {
621 if (aname.length == 0) throw new Exception("empty account name");
622 name = aname.idup;
623 super();
626 public:
627 // should be called ONLY from updater thread!
628 override void update () {
629 scope(exit) {
630 synchronized(this) {
631 mUpdating = false;
632 markAsUpdatedNoLock();
636 synchronized(this) {
637 mUpdating = true;
638 doSendingNL();
641 Folder inbox = getInboxFolder;
642 if (inbox is null) {
643 conwriteln("*** ERROR: [", name, ":", group, "]: no inbox!");
644 return;
648 conwriteln("*** [", name, ":", group, "]: connecting...");
649 auto nsk = new SocketNNTP(server, debugdump);
650 scope(exit) nsk.close();
652 nsk.selectGroup(group);
653 if (nsk.emptyGroup) {
654 conwriteln("[", name, ":", group, "]: no new articles");
655 return;
657 if (debugdump) conwriteln("[", name, ":", group, "]: lo=", nsk.lowater, "; hi=", nsk.hiwater, "; lastloaded=", inbox.maxNntpIndex);
659 uint stnum = inbox.maxNntpIndex+1;
660 if (stnum == 0) stnum = (nsk.hiwater > 1023 ? nsk.hiwater-1023 : 0);
661 if (stnum > nsk.hiwater) {
662 conwriteln("[", name, ":", group, "]: no new articles");
663 return;
666 conwriteln("[", name, ":", group, "]: ", nsk.hiwater+1-stnum, " (possible) new articles");
668 // download new articles
669 //bool wantNewLine = false;
670 foreach (immutable uint anum; stnum..nsk.hiwater+1) {
671 import core.time;
672 import std.datetime;
673 auto msg = nsk.getArticle(anum);
675 auto art = parseMessage(msg);
677 if (debugdump || true) {
678 conwriteln("From: ", art.fromname, " <", art.frommail, ">");
679 conwriteln("Subj: ", art.subj.recodeToKOI8);
680 conwriteln("Date: ", SysTime.fromUnixTime(art.time));
681 conwriteln("MID : ", art.msgid);
682 conwriteln("RID : ", art.getInReplyTo());
684 if (art.nntpindex == 0) throw new Exception("NNTP article without number!");
686 art.unread = true;
688 //TODO: separate filtering system
690 inbox.withBaseWriter((abase) {
691 auto aidx = abase.insert(art);
692 auto twt = inbox.isTwittedByArtIndexNL(aidx);
693 conwriteln("DB INDEX: ", aidx, " (", abase.length, ")");
694 if (twt !is null) {
695 import iv.encoding;
696 conwriteln(" ***FILTERED BY TWIT FILTER (", twt.recodeToKOI8, ")");
697 abase.setRead(aidx);
699 abase.writeUpdates();
701 art.releaseContent();
702 inbox.markForRebuild();
705 conwriteln("[", name, ":", group, "]: ", inbox.length, " articles in database (", inbox.aliveCount, " alive)");
707 inbox.withBaseWriter((abase) {
708 abase.selfCheck();
709 abase.writeUpdates();
714 protected:
715 // WARNING! `fld` is NOT locked!
716 override bool fixSendingArticleHeaders (Folder fld, Article art) {
717 if (!super.fixSendingArticleHeaders(fld, art)) return false;
719 art.replaceHeader("Newsgroups", group);
720 art.removeHeader("To");
721 return true;
724 override bool doPostArticle (Article art) {
725 assert(art !is null);
727 SocketNNTP nsk;
728 try {
729 nsk = new SocketNNTP(server, debugdump);
730 } catch (Exception e) {
731 conwriteln("[", name, ":", group, "]: connection error: ", e.msg);
732 return false;
734 scope(exit) nsk.close();
736 try {
737 nsk.selectGroup(group);
738 nsk.doSend("POST");
739 nsk.doSendRaw(art.getTextToSend);
740 auto ln = nsk.readLine;
741 conwriteln(ln); // 340 Ok, recommended message-ID <o7dq4o$mpm$1@digitalmars.com>
742 if (ln.length == 0 || ln[0] != '3') throw new Exception(ln.idup);
743 } catch (Exception e) {
744 conwriteln("[", name, ":", group, "]: sending error: ", e.msg);
745 return false;
748 return true;
751 protected:
752 override void parseOptions (ConString[] options) {
753 while (options.length) {
754 if (parseCommonOption(options)) continue;
755 ConString opt = options[0];
756 ConString arg;
757 switch (opt) {
758 case "group":
759 if (group.length) { conwriteln("ERROR: account '", name, "': duplicate option '", opt, "'"); throw new Exception("option error"); }
760 if (options.length < 2) { conwriteln("ERROR: account '", name, "': no argument for option '", opt, "'"); throw new Exception("option error"); }
761 arg = options[1];
762 if (arg.length == 0) { conwriteln("ERROR: account '", name, "': empty option '", opt, "'"); throw new Exception("option error"); }
763 options = options[2..$];
764 foreach (char ch; arg) {
765 if (ch != '.' && !ch.isalnum) { conwriteln("ERROR: account '", name, "': invalid group name: '", arg, "'"); throw new Exception("option error"); }
767 group = arg.idup;
768 break;
769 case "folder":
770 if (inboxFolder.length) { conwriteln("ERROR: account '", name, "': duplicate option '", opt, "'"); throw new Exception("option error"); }
771 if (options.length < 2) { conwriteln("ERROR: account '", name, "': no argument for option '", opt, "'"); throw new Exception("option error"); }
772 arg = options[1];
773 if (arg.length == 0) { conwriteln("ERROR: account '", name, "': empty option '", opt, "'"); throw new Exception("option error"); }
774 options = options[2..$];
775 if (arg[0] == '/' || arg[$-1] == '/') { conwriteln("ERROR: account '", name, "': invalid folder name: '", arg, "'"); throw new Exception("option error"); }
776 inboxFolder = arg.idup;
777 break;
778 default:
779 conwriteln("ERROR: account '", name, "': unknown option '", options[0], "'");
780 throw new Exception("option error");
785 override void checkParsedOptions () {
786 if (inboxFolder.length == 0 || inboxFolder[0] == '/' || inboxFolder[$-1] == '/') { conwriteln("ERROR: account '", name, "': invalid inbox folder: '", inboxFolder, "'"); throw new Exception("option error"); }
787 if (group.length == 0) { conwriteln("ERROR: account '", name, "': no group"); throw new Exception("option error"); }
788 foreach (char ch; group) {
789 if (ch != '.' && !ch.isalnum) { conwriteln("ERROR: account '", name, "': invalid group name: '", group, "'"); throw new Exception("option error"); }
791 if (server.length == 0) { conwriteln("ERROR: account '", name, "': no server"); throw new Exception("option error"); }
792 if (mail.length == 0) { conwriteln("ERROR: account '", name, "': no mail"); throw new Exception("option error"); }
795 override void accountBeforeAdd () {
796 super.accountBeforeAdd();
797 if (auto inbox = getInboxFolder) inbox.hideOldThreads = true;
802 // ////////////////////////////////////////////////////////////////////////// //
803 __gshared Account[] accounts;
804 __gshared Account defaultAcc;
807 // ////////////////////////////////////////////////////////////////////////// //
808 Account findNntpAccountForFolder (Folder fld) nothrow @nogc {
809 if (fld is null) return null;
810 foreach (Account acc; accounts) {
811 if (auto nac = cast(NntpAccount)acc) {
812 if (fld is nac.getInboxFolder) return acc;
815 return null;
819 // ////////////////////////////////////////////////////////////////////////// //
820 Account accFindByMail (const(char)[] mail) nothrow @nogc {
821 mail = mail.xstrip;
822 if (mail.length == 0) return null;
823 foreach (Account acc; accounts) {
824 if (acc.mail.length == 0) continue;
825 if (acc.mail.strEquCI(mail)) return acc;
827 return null;
831 // ////////////////////////////////////////////////////////////////////////// //
832 Account accFindByFolder (Folder fld) nothrow @nogc {
833 if (fld is null) return null;
834 foreach (Account acc; accounts) {
835 if (acc.inboxFolder.strEquCI(fld.folderPath)) return acc;
837 return null;
841 // ////////////////////////////////////////////////////////////////////////// //
842 Account accFindNntpByFolder (Folder fld) nothrow @nogc {
843 if (fld is null) return null;
844 foreach (Account acc; accounts) {
845 if (auto nna = cast(NntpAccount)acc) {
846 if (nna.inboxFolder.strEquCI(fld.folderPath)) return acc;
849 return null;
853 // ////////////////////////////////////////////////////////////////////////// //
854 shared static this () {
855 //popbox name options...
856 // user username
857 // name realname
858 // mail email
859 // server [tls:]server
860 // smtpserver [tls:]server
861 // password password
862 // debuglog
863 // checktime minutes
864 conRegFunc!((ConString name, ConString[] options) {
865 try {
866 if (name.length == 0) { conwriteln("ERROR: empty account name!"); return; }
867 foreach (char ch; name) {
868 if (!ch.isalnum && ch != '.' && ch != '_' && ch != '-') { conwriteln("ERROR: invalid account name: '", name, "'"); return; }
870 auto acc = new Pop3Account(name);
871 acc.parseOptions(options);
872 acc.checkParsedOptions();
873 synchronized(Account.classinfo) {
874 foreach (ref Account a; accounts) if (a.name == acc.name) { conwriteln("ERROR: duplicate account name: '", name, "'"); return; }
875 acc.accountBeforeAdd();
876 conwriteln("POP3 server '", acc.name, "': ", acc.server, " (", acc.mail, ")");
877 accounts ~= acc;
878 if (acc.defaultFlag) defaultAcc = acc;
880 } catch (Exception e) {
881 conwriteln("ERROR: ", e.msg);
882 //conwriteln(e.toString);
884 })("popbox", "register POP3 mail box");
886 //nntpbox name options...
887 // user username
888 // name realname
889 // mail email
890 // server server
891 // password password
892 // folder inbox_folder (required!)
893 // group nntp_group_name (required!)
894 // debuglog
895 // checktime minutes
896 conRegFunc!((ConString name, ConString[] options) {
897 try {
898 if (name.length == 0) { conwriteln("ERROR: empty account name!"); return; }
899 foreach (char ch; name) {
900 if (!ch.isalnum && ch != '.' && ch != '_' && ch != '-') { conwriteln("ERROR: invalid account name: '", name, "'"); return; }
902 auto acc = new NntpAccount(name);
903 acc.parseOptions(options);
904 acc.checkParsedOptions();
905 synchronized(Account.classinfo) {
906 foreach (ref Account a; accounts) if (a.name == acc.name) { conwriteln("ERROR: duplicate account name: '", name, "'"); return; }
907 acc.accountBeforeAdd();
908 conwriteln("NNTP server: ", acc.server, " (", acc.group, ")");
909 accounts ~= acc;
911 } catch (Exception e) {
912 conwriteln("ERROR: ", e.msg);
913 //conwriteln(e.toString);
915 })("nntpbox", "register NNTP box");