From 5cf491fb4a3376ef34e1f7805bf11a7948217bd2 Mon Sep 17 00:00:00 2001 From: ketmar Date: Thu, 2 Dec 2021 12:15:08 +0000 Subject: [PATCH] it is now possible to compose new messages and replies, and send them FossilOrigin-Name: dd0ade5af3afcd3c74bef39c81f8dfbc216c1ca94205f5ea69c14cda51350b5e --- chibackend/mbuilder.d | 2 +- chibackend/net/pop3.d | 2 +- chibackend/sqbase.d | 13 ++- chiroptera.d | 95 +++++++++++++-------- dialogs.d | 93 +++++++++++++++++--- receiver.d | 229 ++++++++++++++++++++++++++++++++++++-------------- 6 files changed, 316 insertions(+), 118 deletions(-) diff --git a/chibackend/mbuilder.d b/chibackend/mbuilder.d index 941b6c8..bb2aaa4 100644 --- a/chibackend/mbuilder.d +++ b/chibackend/mbuilder.d @@ -128,7 +128,7 @@ private: // date { import std.datetime; - prepared ~= "Data: "; + prepared ~= "Date: "; prepared ~= SysTimeToRFCString(Clock.currTime); prepared ~= "\r\n"; } diff --git a/chibackend/net/pop3.d b/chibackend/net/pop3.d index 9782e65..8cb08b5 100644 --- a/chibackend/net/pop3.d +++ b/chibackend/net/pop3.d @@ -70,7 +70,7 @@ sending scope(exit) nsk.close(); try { - if (!smtpNoAuth) nsk.auth(mail, user, pass); + if (!smtpNoAuth) nsk.auth(email, user, pass); nsk.sendMessage(from, to, art.getTextToSend); } catch (Exception e) { conwriteln("[", name, "]: sending error: ", e.msg); diff --git a/chibackend/sqbase.d b/chibackend/sqbase.d index af3d43f..fd52062 100644 --- a/chibackend/sqbase.d +++ b/chibackend/sqbase.d @@ -522,15 +522,17 @@ enum schemaSupportTable = ` -- this table holds all unsent messages -- they are put in the storage and properly inserted, -- but also put in this table, for the receiver to send them - -- also note that NNTP messages will be put in the storage without any tags (but with the contents) + -- also note that NNTP messages will be put in the storage without any tags, but with a content -- (this is because we will receive them back from NNTP server later) - -- succesfully sent messages will be simply DELETEd + -- succesfully sent messages will be marked with non-zero sendtime CREATE TABLE IF NOT EXISTS unsent ( uid INTEGER PRIMARY KEY /* the same as in the storage, not automatic */ , accid INTEGER /* account from which this message should be sent */ , from_pop3 TEXT /* "from" for POP3 */ , to_pop3 TEXT /* "to" for POP3 */ , data TEXT /* PACKED data to send */ + , sendtime INTEGER DEFAULT 0 /* 0: not yet; unixtime */ + , lastsendtime INTEGER DEFAULT 0 /* when we last tried to send it? 0 means "not yet" */ ); `; @@ -3346,13 +3348,14 @@ public struct AccountInfo { DynStr realname; DynStr email; DynStr nntpgroup; + DynStr inbox; @property bool isValid () const pure nothrow @safe @nogc { return (accid != 0); } } public bool chiroGetAccountInfo (uint accid, out AccountInfo nfo) { static auto stat = LazyStatement!"Conf"(` - SELECT name AS name, realname AS realname, email AS email, nntpgroup AS nntpgroup + SELECT name AS name, realname AS realname, email AS email, nntpgroup AS nntpgroup, inbox AS inbox FROM accounts WHERE accid=:accid LIMIT 1 ;`); @@ -3363,6 +3366,7 @@ public bool chiroGetAccountInfo (uint accid, out AccountInfo nfo) { nfo.realname = row.realname!SQ3Text; nfo.email = row.email!SQ3Text; nfo.nntpgroup = row.nntpgroup!SQ3Text; + nfo.inbox = row.inbox!SQ3Text; return true; } return false; @@ -3370,7 +3374,7 @@ public bool chiroGetAccountInfo (uint accid, out AccountInfo nfo) { public bool chiroGetAccountInfo (const(char)[] accname, out AccountInfo nfo) { static auto stat = LazyStatement!"Conf"(` - SELECT accid AS accid, name AS name, realname AS realname, email AS email, nntpgroup AS nntpgroup + SELECT accid AS accid, name AS name, realname AS realname, email AS email, nntpgroup AS nntpgroup, inbox AS inbox FROM accounts WHERE name=:name LIMIT 1 ;`); @@ -3381,6 +3385,7 @@ public bool chiroGetAccountInfo (const(char)[] accname, out AccountInfo nfo) { nfo.realname = row.realname!SQ3Text; nfo.email = row.email!SQ3Text; nfo.nntpgroup = row.nntpgroup!SQ3Text; + nfo.inbox = row.inbox!SQ3Text; return true; } return false; diff --git a/chiroptera.d b/chiroptera.d index 0df35f6..257ee48 100644 --- a/chiroptera.d +++ b/chiroptera.d @@ -1059,43 +1059,27 @@ void initConsole () { // //////////////////////////////////////////////////////////////////// // conRegFunc!(() { auto pw = new PostWindow(); - pw.setFrom("goo boo!"); + //pw.setFrom("goo boo!"); pw.caption = "WRITE NEW MESSAGE"; - /*FIXME - if (auto fld = getActiveFolder) { - auto postDg = delegate (Account acc) { - conwriteln("post with account '", acc.name, "' (", acc.mail, ")"); - auto pw = new PostWindow(); - pw.from.str = acc.realname~" <"~acc.mail~">"; - pw.from.readonly = true; - if (auto nna = cast(NntpAccount)acc) { - pw.title = "Reply to NNTP "~nna.server~":"~nna.group; - pw.to.str = nna.group; - pw.to.readonly = true; - pw.activeWidget = pw.subj; - } else { - pw.title = "Reply from '"~acc.name~"' ("~acc.mail~")"; - pw.to.str = ""; - pw.activeWidget = pw.to; + uint tid = mainPane.lastDecodedTid; + if (tid) { + dynstring tn = chiroGetTagName(tid); + if (tn.length) { + foreach (auto row; dbConf.statement(` + SELECT name AS name, realname AS realname, email AS email, nntpgroup AS nntpgroup + FROM accounts + WHERE inbox=:inbox + LIMIT 1 + ;`).bindConstText(":inbox", tn.getData).range) + { + //pw.desttag = tn; // no need to + pw.accname = row.name!SQ3Text; + pw.setFrom(row.realname!SQ3Text, row.email!SQ3Text); + pw.to.focus(); } - pw.subj.str = ""; - pw.acc = acc; - pw.fld = fld; - }; - - auto acc = fld.findAccountToPost(); - if (acc is null) { - auto wacc = new SelectPopBoxWindow(defaultAcc); - wacc.onSelected = postDg; - } else { - postDg(acc); - acc = defaultAcc; } - } else { - conwriteln("post: no active folder"); } - */ })("new_post", "post a new article or message"); @@ -1133,6 +1117,8 @@ void initConsole () { conwriteln("account: ", accname); } + dynstring fldvalue; + foreach (auto row; dbStore.statement(` SELECT ChiroHdr_Field(ChiroUnpack(data), :fldname) AS fldvalue FROM messages @@ -1141,10 +1127,29 @@ void initConsole () { LIMIT 1 `).bind(":uid", mainPane.msglistCurrUId).bindConstText(":fldname", repfld).range) { - pw.to.str = row.fldvalue!SQ3Text.decodeSubj; - pw.caption = dynstring("Reply to: ")~pw.to.str; + fldvalue = row.fldvalue!SQ3Text.decodeSubj; } + if (fldvalue.length == 0 && !repfld.strEquCI("From")) { + foreach (auto row; dbStore.statement(` + SELECT ChiroHdr_Field(ChiroUnpack(data), :fldname) AS fldvalue + FROM messages + WHERE uid=:uid + ORDER BY uid + LIMIT 1 + `).bind(":uid", mainPane.msglistCurrUId).bindConstText(":fldname", "From").range) + { + fldvalue = row.fldvalue!SQ3Text.decodeSubj; + } + } + + pw.to.str = fldvalue; + dynstring cpt = dynstring("Reply to: ")~fldvalue; + cpt ~= " (field: "; + cpt ~= repfld; + cpt ~= ")"; + pw.caption = cpt; + dynstring fromname; foreach (auto row; dbView.statement(` SELECT subj AS subj, from_name AS fromname @@ -1218,6 +1223,24 @@ void initConsole () { } } + writeln("getting msgid..."); + foreach (auto row; dbView.statement(` + SELECT msgid AS msgid + FROM msgids + WHERE uid=:uid + LIMIT 1 + `).bind(":uid", mainPane.msglistCurrUId).range) + { + conwriteln("MSGID: |", row.msgid!SQ3Text); + if (pw.references.length) pw.references ~= " "; + // two times, one for references field + if (pw.references.length) pw.references ~= " "; + pw.references ~= row.msgid!SQ3Text; + if (pw.references.length) pw.references ~= " "; + pw.references ~= row.msgid!SQ3Text; + } + + writeln("select references..."); foreach (auto row; dbView.statement(` SELECT msgid AS msgid FROM refids @@ -1225,10 +1248,12 @@ void initConsole () { ORDER BY idx `).bind(":uid", mainPane.msglistCurrUId).range) { - //conwriteln("REF: |", row.msgid!SQ3Text); + conwriteln("REF: |", row.msgid!SQ3Text); if (pw.references.length) pw.references ~= " "; pw.references ~= row.msgid!SQ3Text; } + + pw.desttag = chiroGetTagName(mainPane.lastDecodedTid); } static void doReply (string repfld) { diff --git a/dialogs.d b/dialogs.d index 370110c..0d7c9ec 100644 --- a/dialogs.d +++ b/dialogs.d @@ -31,6 +31,7 @@ import iv.utfutil; import iv.vfs; import chibackend /*: DynStr*/; +import receiver /*: DynStr*/; // ////////////////////////////////////////////////////////////////////////// // @@ -400,6 +401,7 @@ public class PostWindow : SubWindow { //dynstring replyto; dynstring references; // of replyto article dynstring accname; + dynstring desttag; bool allowAccountChange = true; bool nntp = false; @@ -409,6 +411,19 @@ public class PostWindow : SubWindow { from.readonly = true; } + void setFrom (const(char)[] name, const(char)[] email) { + dynstring s; + if (name.length) { + s = name; + s ~= " <"; + s ~= email; + s ~= ">"; + } else { + s = email; + } + setFrom(s.getData); + } + bool trySend () { static bool checkString (const(char)[] s) nothrow @trusted @nogc { if (s.length == 0) return false; @@ -501,31 +516,83 @@ public class PostWindow : SubWindow { ed.editor.clearAndDisableUndo(); ed.editor.clear(); + bool skipFilters = true; + // build tags + dynstring tags; + // account + if (!nntp) { + tags ~= "account:"; + tags ~= acc.name; + if (desttag.length) { + tags ~= "|"; + tags ~= desttag; + } else if (acc.inbox.length) { + // put to inbox + tags ~= "|"; + tags ~= acc.inbox; + } else { + skipFilters = false; + } + } + + //conwriteln("TAGS: ", tags); + //assert(0); + + dynstring mdata = msg.getPrepared; + + // for NNTP, we will do the trick: insert deleted message into the storage + // this is because next NNTP check will receive it /+ - -- this table holds all unsent messages - -- they are put in the storage and properly inserted, - -- but also put in this table, for the receiver to send them - -- also note that NNTP messages will be put in the storage without any tags (but with the contents) - -- (this is because we will receive them back from NNTP server later) - -- succesfully sent messages will be simply DELETEd - CREATE TABLE IF NOT EXISTS unsent ( uid INTEGER PRIMARY KEY /* the same as in the storage, not automatic */ , accid INTEGER /* account from which this message should be sent */ , from_pop3 TEXT /* "from" for POP3 */ , to_pop3 TEXT /* "to" for POP3 */ , data TEXT /* PACKED data to send */ - ); + , sendtime INTEGER DEFAULT 0 /* 0: not yet; unixtime */ + , lastsendtime INTEGER DEFAULT 0 /* when we last tried to send it? 0 means "not yet" */ +/ - /+ - static auto stat = LazyStatement!"Store"(` - INSERT INTO messages - ;`); - +/ + // insert into main store + uint uid; + transacted!"Store"{ + foreach (auto row; dbStore.statement(` + INSERT INTO messages(tags, data) VALUES(:tags, ChiroPack(:data)) RETURNING uid;`) + .bindConstText(":tags", tags.getData) + .bindConstBlob(":data", mdata.getData) + .range) + { + uid = row.uid!uint; + } + }; + + if (nntp) tomail.clear(); + // insert into unsent queue + transacted!"View"{ + dbView.statement(` + INSERT INTO unsent + ( uid, accid, from_pop3, to_pop3, data) + VALUES(:uid,:accid,:from_pop3,:to_pop3,ChiroPack(:data)) + ;`) + .bind(":uid", uid) + .bind(":accid", acc.accid) + .bind(":accid", acc.accid) + .bindConstText(":from_pop3", frommail.getData) + .bindConstText(":to_pop3", tomail.getData) + .bindConstBlob(":data", mdata.getData) + .doAll(); + }; + + conwriteln("message inserted"); + if (!nntp) { + updateViewDB(skipFilters:skipFilters); + } + + /* conwriteln("======================="); conwrite(msg.getPrepared); conwriteln("-----------------------"); + */ return true; } diff --git a/receiver.d b/receiver.d index ffc2b49..1f2510b 100644 --- a/receiver.d +++ b/receiver.d @@ -24,6 +24,7 @@ private: import std.concurrency; import iv.cmdcon; +import iv.dynstring; import iv.strex; import iv.sq3; import iv.timer : DurTimer = Timer; @@ -298,6 +299,7 @@ static stmtAccInfo = LazyStatement!"Conf"(` , pass AS pass , inbox AS inbox , nntpgroup AS nntpgroup + , email AS email FROM accounts WHERE accid=:accid LIMIT 1 @@ -688,7 +690,7 @@ public void twitMessage (uint uid) { // check for new messages, and update view database // //========================================================================== -void updateViewDB () { +public void updateViewDB (bool skipFilters=false) { uint maxViewUid = 0; uint maxStoreUid = 0; @@ -705,7 +707,6 @@ void updateViewDB () { scope(exit) delete relinkTids; foreach (uint uid; maxViewUid+1..maxStoreUid+1) { - conwriteln("============ message #", uid, " ============"); DynStr msg, tags; foreach (auto row; dbStore.statement(` SELECT tags AS tags, ChiroUnpack(data) AS data FROM messages WHERE uid=:uid LIMIT 1 @@ -716,71 +717,78 @@ void updateViewDB () { } if (msg.length == 0 || tags.length == 0) continue; // not interesting + conwriteln("============ message #", uid, " ============"); + DynStr acc = tags.extractAccount(); DynStr origTags = tags; - DynStr deftag = tags.extractFirstFolder(); - tags = tags.removeFirstFolder(); - - auto hlp = new RealFilterHelper; + RealFilterHelper hlp; scope(exit) delete hlp; - hlp.account = acc; - hlp.tag = deftag; - if (hlp.tag.length == 0) hlp.tag = "#hobo"; - hlp.message = msg; - // filter - foreach (auto row; dbConf.statement(`SELECT filterid AS filterid, name AS name, body AS body FROM filters ORDER BY idx;`).range) { - //conwrite(" filter '", row.name!SQ3Text, "' (", row.filterid!uint, "): "); - bool goOn = false; - hlp.matched = false; - try { - //version(debug_filter_helper) writeln("::: <", row.body!SQ3Text, ">"); - goOn = executeMailFilter(row.body!SQ3Text, hlp); - } catch (Exception e) { - conwriteln("ERROR IN FILTER '", row.name!SQ3Text, "': ", e.msg); - } - if (hlp.matched) { - conwriteln("...filter '", row.name!SQ3Text, " matched!"); - } - //hlp.writeResult(); writeln; - //version(debug_filter_helper) writeln("::: <", row.body!SQ3Text, ">: goon=", goOn, "; isstop=", hlp.isStop); - //assert(hlp.isStop == !goOn); - if (hlp.isStop) break; - } - //write(" FINAL RESULT:"); hlp.writeResult(); writeln; - // done filtering - bool markSpamHam = false; //!!! - if (!hlp.isSpam && !hlp.isHam) { - auto bogo = messageBogoCheck(uid); - if (bogo == Bogo.Spam) { - bool exists; - conwriteln("BOGO: SPAM message #", uid, "; from={", hlp.getFromName.getData, "}:<", hlp.getFromMail.getData, ">; to={", - hlp.getToName.getData, "}:<", hlp.getToMail.getData, ">; subj=", hlp.getSubj(out exists).getData); - hlp.performAction(hlp.Action.Spam); - markSpamHam = false; + + if (!skipFilters) { + DynStr deftag = tags.extractFirstFolder(); + tags = tags.removeFirstFolder(); + + hlp = new RealFilterHelper; + hlp.account = acc; + hlp.tag = deftag; + if (hlp.tag.length == 0) hlp.tag = "#hobo"; + hlp.message = msg; + // filter + foreach (auto row; dbConf.statement(`SELECT filterid AS filterid, name AS name, body AS body FROM filters ORDER BY idx;`).range) { + //conwrite(" filter '", row.name!SQ3Text, "' (", row.filterid!uint, "): "); + bool goOn = false; + hlp.matched = false; + try { + //version(debug_filter_helper) writeln("::: <", row.body!SQ3Text, ">"); + goOn = executeMailFilter(row.body!SQ3Text, hlp); + } catch (Exception e) { + conwriteln("ERROR IN FILTER '", row.name!SQ3Text, "': ", e.msg); + } + if (hlp.matched) { + conwriteln("...filter '", row.name!SQ3Text, " matched!"); + } + //hlp.writeResult(); writeln; + //version(debug_filter_helper) writeln("::: <", row.body!SQ3Text, ">: goon=", goOn, "; isstop=", hlp.isStop); + //assert(hlp.isStop == !goOn); + if (hlp.isStop) break; + } + //write(" FINAL RESULT:"); hlp.writeResult(); writeln; + // done filtering + + markSpamHam = false; //!!! + if (!hlp.isSpam && !hlp.isHam) { + auto bogo = messageBogoCheck(uid); + if (bogo == Bogo.Spam) { + bool exists; + conwriteln("BOGO: SPAM message #", uid, "; from={", hlp.getFromName.getData, "}:<", hlp.getFromMail.getData, ">; to={", + hlp.getToName.getData, "}:<", hlp.getToMail.getData, ">; subj=", hlp.getSubj(out exists).getData); + hlp.performAction(hlp.Action.Spam); + markSpamHam = false; + } } - } - if (hlp.isSpam) hlp.tag = "#spam"; // always + if (hlp.isSpam) hlp.tag = "#spam"; // always - if (hlp.tag.length == 0) hlp.tag = deftag; // just in case - bool hasTag = false; - forEachTag(tags, (xtag) { - if (xtag == hlp.tag) { - hasTag = true; - return false; // stop - } - return true; // go on - }); - - // `tags` should contain our new tags - if (!hasTag) { - DynStr tt = hlp.tag; - if (tags.length) { - tt ~= "|"; - tt ~= tags; + if (hlp.tag.length == 0) hlp.tag = deftag; // just in case + bool hasTag = false; + forEachTag(tags, (xtag) { + if (xtag == hlp.tag) { + hasTag = true; + return false; // stop + } + return true; // go on + }); + + // `tags` should contain our new tags + if (!hasTag) { + DynStr tt = hlp.tag; + if (tags.length) { + tt ~= "|"; + tt ~= tags; + } + tags = tt; } - tags = tt; } // update tags info in the storage @@ -794,11 +802,11 @@ void updateViewDB () { } // insert the message into the view db - int appearance = Appearance.Unread; - if (hlp.isDelete) appearance = Appearance.SoftDeleteFilter; - else if (hlp.isPurge) appearance = Appearance.SoftDeletePurge; - if (appearance == Appearance.Unread && (hlp.isRead || hlp.isSpam)) appearance = Appearance.Read; - if (markSpamHam) { + int appearance = (skipFilters ? Appearance.Read : Appearance.Unread); + if (hlp !is null && hlp.isDelete) appearance = Appearance.SoftDeleteFilter; + else if (hlp !is null && hlp.isPurge) appearance = Appearance.SoftDeletePurge; + if (hlp !is null && appearance == Appearance.Unread && (hlp.isRead || hlp.isSpam)) appearance = Appearance.Read; + if (hlp !is null && markSpamHam) { if (hlp.isSpam) messageBogoMarkSpam(uid); if (hlp.isHam) messageBogoMarkHam(uid); } @@ -968,6 +976,7 @@ void checkerThread (Tid ownerTid) { DynStr pass; DynStr inbox; DynStr nntpgroup; + DynStr xemail; foreach (auto arow; stmtAccInfo.st.bind(":accid", accid).range) { // i found her! @@ -985,6 +994,7 @@ void checkerThread (Tid ownerTid) { pass = arow.pass!SQ3Text; inbox = arow.inbox!SQ3Text; nntpgroup = arow.nntpgroup!SQ3Text; + xemail = arow.email!SQ3Text; } if (!found) { @@ -992,6 +1002,97 @@ void checkerThread (Tid ownerTid) { return; } + struct ToSend { + uint uid; + dynstring from; + dynstring to; + dynstring data; + bool sent; + } + ToSend[] sendQueue; + scope(exit) { + foreach (ref ToSend ss; sendQueue) { ss.from.clear; ss.to.clear; ss.data.clear; } + sendQueue.length = 0; + } + + //FIXME: nntp sends! + if (sendserver.length && (nntpgroup.length != 0 || xemail.length != 0)) { + // check if we have something to send + foreach (auto srow; dbView.statement(` + SELECT uid AS uid, from_pop3 AS from_pop3, to_pop3 AS to_pop3, ChiroUnpack(data) AS data + FROM unsent + WHERE accid=:accid AND sendtime=0 + ;`).bind(":accid", accid).range) + { + ToSend ss; + ss.uid = srow.uid!uint; + ss.from = srow.from_pop3!SQ3Text; + ss.to = srow.to_pop3!SQ3Text; + ss.data = srow.data!SQ3Text; + sendQueue ~= ss; + } + } + + //FIXME: batch send! + if (sendQueue.length) { + conwriteln("sending ", sendQueue.length, " message", (sendQueue.length == 1 ? "" : "s")); + foreach (ref ToSend ss; sendQueue) { + try { + if (nntpgroup.length == 0) { + conwriteln("*** [", name, "]: connecting... (smtp)"); + SocketSMTP nsk = new SocketSMTP(sendserver.idup); + scope(exit) { nsk.close(); delete nsk; } + if (!nosendauth) { + conwriteln("[", name, "]: authenticating..."); + nsk.auth(xemail.getData, user.getData, pass.getData); + } + conwriteln("[", name, "]: sending (uid=", ss.uid, ")..."); + nsk.sendMessage(ss.from.getData, ss.to.getData, ss.data.getData); + nsk.close(); + conwriteln("[", name, "]: closing..."); + } else { + conwriteln("*** [", name, "]: connecting... (nntp)"); + SocketNNTP nsk = new SocketNNTP(recvserver.idup); + scope(exit) { nsk.close(); delete nsk; } + conwriteln("[", name, "]: selecting group (", nntpgroup, ")"); + nsk.selectGroup(nntpgroup.getData); + conwriteln("[", name, "]: sending (uid=", ss.uid, ")..."); + nsk.doSend("POST"); + nsk.doSendRaw(ss.data.getData); + conwriteln("[", name, "]: getting answer..."); + auto ln = nsk.readLine; + conwriteln("[", name, "]: ", ln); // 340 Ok, recommended message-ID + if (ln.length == 0 || ln[0] != '3') throw new Exception(ln.idup); + conwriteln("[", name, "]: closing..."); + nsk.close(); + } + ss.sent = true; + } catch (Exception e) { + conwriteln("SENDING ERROR: ", e.msg); + } + } + + // mark sent messages + transacted!"View"{ + foreach (ref ToSend ss; sendQueue) { + if (ss.sent) { + dbView.statement(` + UPDATE unsent + SET sendtime=CAST(strftime('%s','now') AS INTEGER), lastsendtime=CAST(strftime('%s','now') AS INTEGER) + WHERE uid=:uid + ;`).bind(":uid", ss.uid).doAll(); + } else { + dbView.statement(` + UPDATE unsent + SET lastsendtime=CAST(strftime('%s','now') AS INTEGER) + WHERE uid=:uid + ;`).bind(":uid", ss.uid).doAll(); + } + } + }; + } + + conwriteln("checking account '", name, "' (", accid, ")..."); stmtSetCheckTime.st.bind(":accid", accid).bind(":lastcheck", RunningAverageExp.GetTickCount()+checktime*60).doAll(); -- 2.11.4.GIT