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
;
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
;
45 MonoTime lastUpdateTime
= MonoTime
.zero
;
52 string user
; // on server
58 int checkminutes
; // <=0: never
66 lastUpdateTime
= MonoTime
.currTime
;
69 Folder
getInboxFolder () nothrow @nogc {
70 foreach (Folder
fld; folders
) if (fld.folderPath
== inboxFolder
) return fld;
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
~">");
86 art
.prependHeader("From", mail
);
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) {
100 art
.replaceHeader("Date", Clock
.currTime().SysTimetoRFCString
);
104 auto repto
= art
.getInReplyTo();
107 bool hadReplyTo
= false;
109 void addRef (const(char)[] s
) {
111 if (s
.length
== 0) return;
112 if (!hadReplyTo
&& s
== repto
) hadReplyTo
= true;
113 if (refs
.length
) refs
~= ' ';
117 void procRefs (const(char)[] s
) {
120 if (s
.length
== 0) break;
122 while (pos
< s
.length
&& s
[pos
] > ' ') ++pos
;
129 addRef(art
.getHeaderValue("References"));
130 if (!hadReplyTo
) addRef(repto
);
132 if (refs
.length
) art
.replaceHeader("References", refs
);
138 bool doPostArticle (Article art
);
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
148 while (idx
< sendQueue
.length
) {
149 //conwriteln(" POSTING #", idx+1, " of ", sendQueue.length, "...");
150 if (doPostArticle(sendQueue
[idx
])) {
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
;
161 if (sqchanged
) saveSendQueue();
162 //conwriteln("============");
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 {
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;
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;
187 auto fl
= VFile(fname
);
189 auto art
= loadArticleText(fl
);
190 if (art
is null ||
!art
.valid
) break;
191 // hack: clear "loaded" flags
192 art
.markAsStandalone();
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
;
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
, "'");
217 // should be called ONLY from updater thread!
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; }
228 if (!fixSendingArticleHeaders(fld, art
)) return false;
229 art
.setupFromHeaders
!true();
231 art
.appendAttachments();
232 } catch (Exception e
) {
233 conwriteln("ERROR adding attachments: ", e
.msg
);
238 forceUpdating(); // so it will be sent
239 postDoCheckBoxesCycle(); // and do it now
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; }
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"); }
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
;
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"); }
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
;
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"); }
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
;
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"); }
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
;
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"); }
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
;
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"); }
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
;
310 if (options
.length
< 2) { conwriteln("ERROR: account '", this.name
, "': no argument for option '", opt
, "'"); throw new Exception("option error"); }
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
;
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
;
339 // ////////////////////////////////////////////////////////////////////////// //
340 public final class Pop3Account
: Account
{
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
~= '/';
352 if (inboxFolder
.length
<= xlen || inboxFolder
[$-1] == '/') throw new Exception("invalid inbox folder");
353 name
= inboxFolder
[xlen
..$];
354 inboxFolder
~= "/inbox";
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
)) {
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();
380 // should be called ONLY from updater thread!
381 override void update () {
385 markAsUpdatedNoLock();
394 Folder inbox
= getInboxFolder
;
396 conwriteln("*** ERROR: [", name
, "]: no inbox!");
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
;
408 conwriteln("[", name
, "]: no new messages");
411 conwriteln("[", name
, "]: ", newmsg
, " new message", (newmsg
> 1 ?
"s" : ""));
412 foreach (immutable int popidx
; 1..newmsg
+1) {
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
);
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);
443 fda
.destfolder
= inboxFolder
;
445 doFiltering
!"pre"(fda
);
446 if (fda
.destfolder
.length
== 0) {
448 conwriteln("*DROP: ", art
.fromname
.recodeToKOI8
, " <", art
.frommail
, "> (", art
.subj
.recodeToKOI8
, ")");
452 Bogo bogo
= fda
.bogo
;
454 if (bogo
== Bogo
.Error
) bogo
= articleBogoCheck(art
);
456 final switch (bogo
) {
457 case Bogo
.Error
: break;
462 art
.spamUnsure
= true;
469 if (fda
.markRead
) art
.unread
= false;
472 doFiltering
!"post"(fda
);
474 fda
.destfolder
= "zz_spam";
477 if (fda
.destfolder
.length
== 0) {
479 conwriteln("*DROP: ", art
.fromname
.recodeToKOI8
, " <", art
.frommail
, "> (", art
.subj
.recodeToKOI8
, ")");
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
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
) {
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
);
503 abase
.writeUpdates();
504 conwriteln(" ", abase
.length
-1, " messages in database (", abase
.aliveCount
, " alive)");
506 art
.releaseContent();
507 xfolder
.markForRebuild();
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) {
529 abase.writeUpdates();
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;
543 while (spos
> 0 && dest
[spos
-1] != '<') --spos
;
544 if (spos
== atpos
) return null;
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!");
566 auto to
= extractToMail(art
);
567 if (to
.length
== 0) {
568 conwriteln("SMTP ERROR: no TO!");
572 conwriteln("[", name
, "]: trying to send mail from <", from
, "> to <", to
, "> using ", sendserver
);
576 nsk
= new SocketSMTP(sendserver
, /*debugdump*/true);
577 } catch (Exception e
) {
578 conwriteln("[", name
, "]: connection error: ", e
.msg
);
581 scope(exit
) nsk
.close();
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
);
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
{
617 string group
; // DO NOT MODIFY!
620 private this (ConString aname
) {
621 if (aname
.length
== 0) throw new Exception("empty account name");
627 // should be called ONLY from updater thread!
628 override void update () {
632 markAsUpdatedNoLock();
641 Folder inbox
= getInboxFolder
;
643 conwriteln("*** ERROR: [", name
, ":", group
, "]: no inbox!");
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");
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");
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) {
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!");
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
, ")");
696 conwriteln(" ***FILTERED BY TWIT FILTER (", twt
.recodeToKOI8
, ")");
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) {
709 abase.writeUpdates();
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");
724 override bool doPostArticle (Article art
) {
725 assert(art
!is null);
729 nsk
= new SocketNNTP(server
, debugdump
);
730 } catch (Exception e
) {
731 conwriteln("[", name
, ":", group
, "]: connection error: ", e
.msg
);
734 scope(exit
) nsk
.close();
737 nsk
.selectGroup(group
);
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
);
752 override void parseOptions (ConString
[] options
) {
753 while (options
.length
) {
754 if (parseCommonOption(options
)) continue;
755 ConString opt
= options
[0];
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"); }
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"); }
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"); }
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
;
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
;
819 // ////////////////////////////////////////////////////////////////////////// //
820 Account
accFindByMail (const(char)[] mail
) nothrow @nogc {
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
;
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
;
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
;
853 // ////////////////////////////////////////////////////////////////////////// //
854 shared static this () {
855 //popbox name options...
859 // server [tls:]server
860 // smtpserver [tls:]server
864 conRegFunc
!((ConString name
, ConString
[] options
) {
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
, ")");
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...
892 // folder inbox_folder (required!)
893 // group nntp_group_name (required!)
896 conRegFunc
!((ConString name
, ConString
[] options
) {
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
, ")");
911 } catch (Exception e
) {
912 conwriteln("ERROR: ", e
.msg
);
913 //conwriteln(e.toString);
915 })("nntpbox", "register NNTP box");