/* * kbiffmonitor.cpp * Copyright (C) 1999-2008 Kurt Granroth * * This file contains the implementation of KBiffMonitor and * associated classes. */ #include "kbiffmonitor.h" #include "kbiffmonitor.moc" #include #include #ifndef __STRICT_ANSI__ #define __STRICT_ANSI__ #include #undef __STRICT_ANSI__ #else #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Needed for CRAM-MD5 and APOP #include #include "kbiffcrypt.h" #define MAXSTR (1024) #define MAIL_STATE_FILE "kbiffstate" #if defined (_HPUX_SOURCE) extern int h_errno; #endif static bool real_from(const TQString& buffer); static const char* compare_header(const char* header, const char* field); KBiffMonitor::KBiffMonitor() : TQObject(), poll(60), oldTimer(0), started(false), newCount(0), curCount(-1), oldCount(-1), firstRun(false), key(""), simpleURL(""), protocol(""), mailbox(""), server(""), user(""), password(""), port(0), preauth(false), keepalive(false), mailState(UnknownState), lastSize(0), imap(0), pop(0), nntp(0) { lastRead.setTime_t(0); lastModified.setTime_t(0); b_new_lastSize = false; b_new_lastRead = false; b_new_lastModified = false; b_new_uidlList = false; } KBiffMonitor::~KBiffMonitor() { if (imap) { delete imap; imap = 0; } if (pop) { delete pop; pop = 0; } if (nntp) { delete nntp; nntp = 0; } } void KBiffMonitor::readConfig() { KSimpleConfig *config = new KSimpleConfig(MAIL_STATE_FILE); config->setDollarExpansion(false); TQString group; group = mailbox + "(" + key + ")"; config->setGroup(group); TQStrList list; mailState = (KBiffMailState)config->readNumEntry("mailState", UnknownState); lastSize = config->readNumEntry("lastSize"); config->readListEntry("lastRead", list); if (list.count()==6) { lastRead.setDate(TQDate(atoi(list.at(0)),atoi(list.at(1)),atoi(list.at(2)))); lastRead.setTime(TQTime(atoi(list.at(3)),atoi(list.at(4)),atoi(list.at(5)))); } config->readListEntry("lastModified", list); if (list.count()==6) { lastModified.setDate(TQDate(atoi(list.at(0)),atoi(list.at(1)),atoi(list.at(2)))); lastModified.setTime(TQTime(atoi(list.at(3)),atoi(list.at(4)),atoi(list.at(5)))); } config->readListEntry("uidlList", list); char *UIDL; uidlList.clear(); for (UIDL = list.first(); UIDL != 0; UIDL = list.next()) { uidlList.append( new TQString(UIDL) ); } newCount = config->readNumEntry("newCount", 0); oldCount = config->readNumEntry("oldCount", -1); delete config; } void KBiffMonitor::saveConfig() { // open the config file KSimpleConfig *config = new KSimpleConfig(MAIL_STATE_FILE); config->setDollarExpansion(false); TQString group; group = mailbox + "(" + key + ")"; config->setGroup(group); TQStringList uidlist; TQString *UIDL; for (UIDL = uidlList.first(); UIDL != 0; UIDL = uidlList.next()) { uidlist.append(*UIDL); } config->writeEntry("mailState", (int)mailState); config->writeEntry("lastSize", lastSize); config->writeEntry("lastRead",lastRead); config->writeEntry("lastModified",lastModified); config->writeEntry("uidlList",uidlist); config->writeEntry("newCount", newCount); config->writeEntry("oldCount", oldCount); delete config; } void KBiffMonitor::onStateChanged() { saveConfig(); } void KBiffMonitor::start() { readConfig(); started = true; firstRun = true; oldTimer = startTimer(poll * 1000); emit(signal_checkMail()); } void KBiffMonitor::stop() { if (oldTimer > 0) killTimer(oldTimer); lastSize = 0; oldTimer = 0; mailState = UnknownState; started = false; lastRead.setTime_t(0); lastModified.setTime_t(0); uidlList.clear(); } void KBiffMonitor::setPollInterval(const int interval) { poll = interval; // Kill any old timers that may be running if (oldTimer > 0) { killTimer(oldTimer); // Start a new timer will the specified time if (started) { oldTimer = startTimer(interval * 1000); emit(signal_checkMail()); } } } void KBiffMonitor::setMailbox(const TQString& url) { KBiffURL kurl(url); setMailbox(kurl); } void KBiffMonitor::setMailbox(KBiffURL& url) { if (imap) { delete imap; imap = 0; } if (pop) { delete pop; pop = 0; } if (nntp) { delete nntp; nntp = 0; } protocol = url.protocol(); if (protocol == "imap4") { disconnect(this); imap = new KBiffImap; connect(this, SIGNAL(signal_checkMail()), SLOT(checkImap())); server = url.host(); user = url.user(); password = url.pass(); mailbox = url.path().right(url.path().length() - 1); port = (url.port() > 0) ? url.port() : 143; preauth = url.searchPar("preauth") == "yes"; keepalive = url.searchPar("keepalive") == "yes"; bool async = url.searchPar("async") == "yes"; imap->setAsync(async); #ifdef USE_SSL imap->setSSL(false); #endif // USE_SSL simpleURL = "imap4://" + server + "/" + mailbox; } #ifdef USE_SSL if (protocol == "imap4s") { disconnect(this); imap = new KBiffImap; connect(this, SIGNAL(signal_checkMail()), SLOT(checkImap())); server = url.host(); user = url.user(); password = url.pass(); mailbox = url.path().right(url.path().length() - 1); port = (url.port() > 0) ? url.port() : 993; preauth = url.searchPar("preauth") == "yes"; keepalive = url.searchPar("keepalive") == "yes"; bool async = url.searchPar("async") == "yes"; imap->setAsync(async); imap->setSSL(true); simpleURL = "imap4s://" + server + "/" + mailbox; } #endif // USE_SSL if (protocol == "pop3") { disconnect(this); pop = new KBiffPop; connect(this, SIGNAL(signal_checkMail()), SLOT(checkPop())); server = url.host(); user = url.user(); password = url.pass(); mailbox = url.user(); port = (url.port() > 0) ? url.port() : 110; keepalive = url.searchPar("keepalive") == "yes"; bool async = url.searchPar("async") == "yes"; pop->setAsync(async); // preserve existing behaviour, prior to adding disable apop, // by setting Apop on, even if no apop parameter is found in the mailbox url bool useApop = !( url.searchPar("apop") == "no" ); pop->setApop( useApop ); #ifdef USE_SSL pop->setSSL(false); #endif // USE_SSL simpleURL = "pop3://" + server + "/" + mailbox; } #ifdef USE_SSL if (protocol == "pop3s") { disconnect(this); pop = new KBiffPop; connect(this, SIGNAL(signal_checkMail()), SLOT(checkPop())); server = url.host(); user = url.user(); password = url.pass(); mailbox = url.user(); port = (url.port() > 0) ? url.port() : 995; keepalive = url.searchPar("keepalive") == "yes"; bool async = url.searchPar("async") == "yes"; pop->setAsync(async); // preserve existing behaviour, prior to adding disable apop, // by setting Apop on, even if no apop parameter is found in the mailbox url bool useApop = !( url.searchPar("apop") == "no" ); pop->setApop( useApop ); pop->setSSL(true); simpleURL = "pop3s://" + server + "/" + mailbox; } #endif // USE_SSL if (protocol == "mbox") { disconnect(this); connect(this, SIGNAL(signal_checkMail()), SLOT(checkMbox())); mailbox = url.path(); simpleURL = "mbox:" + mailbox; } if (protocol == "file") { disconnect(this); connect(this, SIGNAL(signal_checkMail()), SLOT(checkLocal())); mailbox = url.path(); simpleURL = "file:" + mailbox; } if (protocol == "maildir") { disconnect(this); connect(this, SIGNAL(signal_checkMail()), SLOT(checkMaildir())); mailbox = url.path(); simpleURL = "maildir:" + mailbox; } if (protocol == "mh") { disconnect(this); connect(this, SIGNAL(signal_checkMail()), SLOT(checkMHdir())); mailbox = url.path(); simpleURL = "mh:" + mailbox; } if (protocol == "nntp") { disconnect(this); nntp = new KBiffNntp; connect(this, SIGNAL(signal_checkMail()), SLOT(checkNntp())); server = url.host(); user = url.user(); password = url.pass(); mailbox = url.path().right(url.path().length() - 1); port = (url.port() > 0) ? url.port() : 119; keepalive = url.searchPar("keepalive") == "yes"; bool async = url.searchPar("async") == "yes"; nntp->setAsync(async); #ifdef USE_SSL nntp->setSSL(false); #endif // USE_SSL simpleURL = "nntp://" + server + "/" + mailbox; } fetchCommand = url.searchPar("fetch"); } void KBiffMonitor::setMailboxIsRead() { lastRead = TQDateTime::currentDateTime(); if (mailState == NewMail) { if (b_new_lastSize) lastSize = new_lastSize; if (b_new_lastRead) lastRead = new_lastRead; if (b_new_lastModified) lastModified = new_lastModified; if (b_new_uidlList) uidlList = new_uidlList; if (curCount!=-1) curCount+=newCount; newCount = 0; b_new_lastSize = false; b_new_lastRead = false; b_new_lastModified = false; b_new_uidlList = false; determineState(OldMail); } } void KBiffMonitor::checkMailNow() { emit(signal_checkMail()); } void KBiffMonitor::setPassword(const TQString& pass) { password = pass; } void KBiffMonitor::setMailboxKey(const TQString& k) { key = k; } void KBiffMonitor::timerEvent(TQTimerEvent *) { emit(signal_checkMail()); } void KBiffMonitor::checkLocal() { // get the information about this local mailbox TQFileInfo mbox(mailbox); // run external fetch client if (!fetchCommand.isEmpty()) emit(signal_fetchMail(fetchCommand)); // check if we have new mail determineState(mbox.size(), mbox.lastRead(), mbox.lastModified()); firstRun = false; } void KBiffMonitor::checkMbox() { // get the information about this local mailbox TQFileInfo mbox(mailbox); // run external fetch client if (!fetchCommand.isEmpty()) emit(signal_fetchMail(fetchCommand)); // see if the state has changed if ((mbox.lastModified() != lastModified) || (mbox.size() != lastSize) || (mailState == UnknownState) || (oldCount == -1)) { lastModified = mbox.lastModified(); lastSize = mbox.size(); // ok, the state *has* changed. see if the number of // new messages has, too. newCount = mboxMessages(); // Set access time of the file to what it was. If we don't do // this some (all?) MUAs think that the mail has already been // read. { utimbuf buf; buf.actime = mbox.lastRead().toTime_t(); buf.modtime = mbox.lastModified().toTime_t(); utime(TQFile::encodeName(mailbox), &buf); } // if there are any new messages, consider the state New if (newCount > 0) determineState(NewMail); else { if (oldCount == 0) determineState(NoMail); else determineState(OldMail); } } else if (firstRun) { KBiffMailState state(mailState); mailState = UnknownState; determineState(state); } firstRun = false; // handle the NoMail case if ((mbox.size() == 0) || (oldCount == 0)) { newCount = 0; determineState(NoMail); return; } } void KBiffMonitor::checkPop() { firstRun = false; TQString command; // connect to the server unless it is active already if (pop->active() == false) { if(pop->connectSocket(server, port) == false) { determineState(NoConn); return; } // find out if APOP is supported pop->parseBanner(); // find other possibly useful capabilities // we don't care if this fails pop->command("CAPA\r\n"); if (pop->authenticate(user, password) == false ) { pop->close(); invalidLogin(); return; } } command = "UIDL\r\n"; if (pop->command(command) == false) { command = "STAT\r\n"; if (pop->command(command) == false) { command = "LIST\r\n"; if (pop->command(command) == false) { // if this still doesn't work, then we // close this port pop->close(); return; } } } if (command == "UIDL\r\n") { determineState(pop->getUidlList()); curCount = uidlList.count(); } else { determineState(pop->numberOfMessages()); } if (keepalive == false) pop->close(); } void KBiffMonitor::checkImap() { firstRun = false; TQString command; int seq = 1000; bool do_login = false; // run external client (probably to setup SSL) if (!fetchCommand.isEmpty()) { emit(signal_fetchMail(fetchCommand)); // sleep a bit to allow the connection to take place sleep(1); } // connect to the server if (imap->active() == false) { if (imap->connectSocket(server, port) == false) { invalidLogin(); return; } do_login = true; // check the server's capabilities (see RFC 3050, 6.1.1) command = TQString().setNum(seq) + " CAPABILITY\r\n"; if (imap->command(command, seq) == false) { invalidLogin(); return; } seq++; } // if we are preauthorized OR we want to keep the session alive, then // we don't login. Otherwise, we do. if ((preauth == false) && (do_login == true)) { if (imap->authenticate(&seq, user, password) == false) { invalidLogin(); return; } } // reset the numbers from the last check imap->resetNumbers(); // The STATUS COMMAND is documented in RFC2060, 6.3.10 command = TQString().setNum(seq) + " STATUS " + mailbox + " (UNSEEN MESSAGES)\r\n"; if ( ! imap->command(command, seq)) { return; } seq++; // lets not logout if we want to keep the session alive if (keepalive == false) { command = TQString().setNum(seq) + " LOGOUT\r\n"; if (imap->command(command, seq) == false) return; imap->close(); } // what state are we in? if (imap->numberOfMessages() == 0) { newCount = 0; determineState(NoMail); } else { newCount = imap->numberOfNewMessages(); curCount = imap->numberOfMessages() - newCount; if (newCount > 0) determineState(NewMail); else determineState(OldMail); } } void KBiffMonitor::checkMaildir() { firstRun = false; // get the information about this local mailbox TQDir mbox(mailbox); // run external fetch client if (!fetchCommand.isEmpty()) emit(signal_fetchMail(fetchCommand)); // make sure the mailbox exists if (mbox.exists()) { // maildir stores its mail in MAILDIR/new and MAILDIR/cur TQDir new_mailbox(mailbox + "/new"); TQDir cur_mailbox(mailbox + "/cur"); // make sure both exist if (new_mailbox.exists() && cur_mailbox.exists()) { // check only files new_mailbox.setFilter(TQDir::Files); cur_mailbox.setFilter(TQDir::Files); // determining "new" (or "unread") mail in maildir folders // is a *little* tricky. all mail in the 'new' folder are // new, of course... but so is all mail in the 'cur' // folder that doesn't have a ':2,[F|R|S|T]' after it. newCount = new_mailbox.count(); curCount = cur_mailbox.count(); const TQFileInfoList *cur_list = cur_mailbox.entryInfoList(); TQFileInfoListIterator it(*cur_list); TQFileInfo *info; static TQRegExp suffix(":2,?F?R?S?T?$"); while ((info = it.current())) { if (info->fileName().findRev(suffix) == -1) { newCount++; curCount--; } ++it; } // all messages in 'new' are new if (newCount > 0) { determineState(NewMail); } // failing that, we look for any old ones else if (curCount > 0) { determineState(OldMail); } // failing that, we have no mail else determineState(NoMail); } } } void KBiffMonitor::checkNntp() { firstRun = false; TQString command; bool do_login = false; // connect to the server if (nntp->active() == false) { if (nntp->connectSocket(server, port) == false) { determineState(NoConn); return; } do_login = true; } // if we are preauthorized OR we want to keep the session alive, then // we don't login. Otherwise, we do. if ((preauth == false) && (do_login == true)) { if (user.isEmpty() == false) { command = "authinfo user " + user + "\r\n"; if (nntp->command(command) == false) return; } if (password.isEmpty() == false) { command = "authinfo pass " + password + "\r\n"; if (nntp->command(command) == false) return; } } command = "group " + mailbox + "\r\n"; if (nntp->command(command) == false) return; // lets not logout if we want to keep the session alive if (keepalive == false) { command = "QUIT\r\n"; nntp->command(command); nntp->close(); } // now, we process the .newsrc file TQString home(getenv("HOME")); TQString newsrc_path(home + "/.newsrc"); TQFile newsrc(newsrc_path); if (newsrc.open(IO_ReadOnly) == false) { return; } char c_buffer[MAXSTR]; while(newsrc.readLine(c_buffer, MAXSTR) > 0) { // search for our mailbox name TQString str_buffer(c_buffer); if (str_buffer.left(mailbox.length()) != mailbox) continue; // we now have our mailbox. this parsing routine is so // ugly, however, that I could almost cry. it assumes way // too much. the "actual" range MUST be 1-something // continuously and our read sequence MUST be sequentially in // order bool range = false; int last = 1; newCount = 0; char *buffer = c_buffer; // skip over the mailbox name for(; buffer && *buffer != ' '; buffer++) {} // iterate over the sequence until we hit a newline or end of string while (buffer && *buffer != '\n' && *buffer != '\0') { // make sure that this is a digit if (!isdigit(*buffer)) { buffer++; continue; } // okay, what digit are we looking at? atoi() will convert // only those digits it recognizes to an it. this will handily // skip spaces, dashes, commas, etc char *digit = buffer; int current = atoi(digit); // if our current digit is greater than is possible, then we // should just quit while we're (somewhat) ahead if (current > nntp->last()) break; // we treat our sequences different ways if we are in a range // or not. specifically, if we are in the top half of a range, // we don't do anything if (range == false) { if (current > last) newCount += current - last - 1; } else range = false; // set our 'last' one for the next go-round last = current; // skip over all of these digits for(;buffer && isdigit(*buffer); buffer++) {} // is this a range? if (*buffer == '-') range = true; } // get the last few new ones if (last < nntp->last()) newCount += nntp->last() - last; break; } // with newsgroups, it is either new or non-existant. it // doesn't make sense to count the number of read mails if (newCount > 0) determineState(NewMail); else determineState(OldMail); } /* * MH support provided by David Woodhouse */ void KBiffMonitor::checkMHdir() { firstRun = false; // get the information about this local mailbox TQDir mbox(mailbox); char the_buffer[MAXSTR]; char *buffer = the_buffer; // run external fetch client if (!fetchCommand.isEmpty()) emit(signal_fetchMail(fetchCommand)); // make sure the mailbox exists if (mbox.exists()) { TQFile mhseq(mailbox+"/.mh_sequences"); if (mhseq.open(IO_ReadOnly) == true) { // Check the .mh_sequences file for 'unseen:' buffer[MAXSTR-1]=0; while(mhseq.readLine(buffer, MAXSTR-2) > 0) { if (!strchr(buffer, '\n') && !mhseq.atEnd()) { // read till the end of the line int c; while((c=mhseq.getch()) >=0 && c !='\n') {} } if (!strncmp(buffer, "unseen:", 7)) { // There are unseen messages // we will now attempt to count exactly how // many new messages there are // an unseen sequence looks something like so: // unseen: 1, 5-9, 27, 35-41 bool range = false; int last = 0; // initialize the number of new messages newCount = 0; // jump to the correct position and iterate through the // rest of the buffer buffer+=7; while(*buffer != '\n' && buffer) { // is this a digit? if so, it is the first of possibly // several digits if (isdigit(*buffer)) { // whether or not this is a range, we are guaranteed // of at least *one* new message newCount++; // get a handle to this digit. atoi() will convert // only those digits it recognizes to an int. so // atoi("123garbage") would become 123 char *digit = buffer; // if we are in the second half of a range, we need // to compute the number of new messages. if (range) { // remember that we have already counted the // two extremes.. hence we need to subtract one. newCount += atoi(digit) - last - 1; range = false; } // skip over all digits for(;buffer && isdigit(*buffer); buffer++) {} // check if we are in a range if (*buffer == '-') { // save the current digit for later computing last = atoi(digit); range = true; } } else buffer++; } mhseq.close(); determineState(NewMail); return; } } mhseq.close(); } // OK. No new messages listed in .mh_sequences. Check if // there are any old ones. //mbox.setFilter(TQDir::Files); TQStringList mails = mbox.entryList(TQDir::Files); TQStringList::Iterator str; for (str = mails.begin(); str != mails.end(); str++) { uint index; // Check each file in the directory. // If it's a numeric filename, then it's a mail. for (index = 0; index < (*str).length(); index++) { if (!(*str).at(index).isDigit()) break; } if (index >= (*str).length()) { // We found a filename which was entirely // made up of digits - it's a real mail, so // respond accordingly. determineState(OldMail); return; } } // We haven't found any valid filenames. No Mail. determineState(NoMail); } } void KBiffMonitor::determineState(unsigned int size) { // check for no mail if (size == 0) { if (mailState != NoMail) { mailState = NoMail; lastSize = 0; newCount = 0; emit(signal_noMail()); emit(signal_noMail(simpleURL)); onStateChanged(); } emit(signal_currentStatus(newCount, key, mailState)); return; } // check for new mail if (size > lastSize) { if (!b_new_lastSize || size > new_lastSize) { mailState = NewMail; emit(signal_newMail()); emit(signal_newMail(newCount, key)); onStateChanged(); } new_lastSize = size; b_new_lastSize = true; newCount = size - lastSize; emit(signal_currentStatus(newCount, key, mailState)); return; } // if we have *some* mail, but the state is unknown, // then we'll consider it old if (mailState == UnknownState) { mailState = OldMail; lastSize = size; emit(signal_oldMail()); emit(signal_oldMail(simpleURL)); emit(signal_currentStatus(newCount, key, mailState)); onStateChanged(); return; } // check for old mail if (size < lastSize) { if (mailState != OldMail) { mailState = OldMail; lastSize = size; emit(signal_oldMail()); emit(signal_oldMail(simpleURL)); onStateChanged(); } } emit(signal_currentStatus(newCount, key, mailState)); } void KBiffMonitor::determineState(KBiffUidlList uidl_list) { TQString *UIDL; unsigned int messages = 0; // if the uidl_list is empty then the number of messages = 0 if (uidl_list.isEmpty()) { if (mailState != NoMail) { lastSize = newCount = 0; mailState = NoMail; emit(signal_noMail()); emit(signal_noMail(simpleURL)); onStateChanged(); } } else { // if a member of uidl_list is not in the old uidlList then we have // new mail for (UIDL = uidl_list.first(); UIDL != 0; UIDL = uidl_list.next()) { // If we already have new mail use new_uidlList to se if we have // more new messages if (b_new_uidlList) { if (new_uidlList.find(UIDL) == -1) messages++; } else { if (uidlList.find(UIDL) == -1) messages++; } } // if there are any new messages, then notify.. if (messages > 0) { mailState = NewMail; emit(signal_newMail()); emit(signal_newMail(newCount, key)); onStateChanged(); // now update newCount if (b_new_uidlList) { // if we have used new_uidlList for a check newCount += messages; } else { // if we have used uidlList for a check newCount = messages; } new_uidlList = uidl_list; b_new_uidlList = true; } // this is horrible. it will reset kbiff to OldMail the very next // time a pop3 mailbox is checked. i don't know of a way around // this, though :-( // MZ: what's wrong with that? else if ( (!b_new_uidlList) && mailState != OldMail) { newCount = 0; mailState = OldMail; emit(signal_oldMail()); emit(signal_oldMail(simpleURL)); onStateChanged(); } } emit(signal_currentStatus(newCount, key, mailState)); } void KBiffMonitor::determineState(KBiffMailState state) { if ((state == NewMail) && (mailState != NewMail)) { mailState = NewMail; emit(signal_newMail()); emit(signal_newMail(newCount, key)); onStateChanged(); } else if ((state == NoMail) && (mailState != NoMail)) { mailState = NoMail; emit(signal_noMail()); emit(signal_noMail(simpleURL)); onStateChanged(); } else if ((state == OldMail) && (mailState != OldMail)) { mailState = OldMail; emit(signal_oldMail()); emit(signal_oldMail(simpleURL)); onStateChanged(); } else if ((state == NoConn) && (mailState != NoConn)) { mailState = NoConn; emit(signal_noConn()); emit(signal_noConn(simpleURL)); onStateChanged(); } emit(signal_currentStatus(newCount, key, mailState)); } void KBiffMonitor::determineState(unsigned int size, const TQDateTime& last_read, const TQDateTime& last_modified) { // Check for NoMail if (size == 0) { // Is this a new state? if (mailState != NoMail) { // Yes, the user has just nuked the entire mailbox mailState = NoMail; lastRead = last_read; lastSize = 0; // Let the world know of the new state emit(signal_noMail()); emit(signal_noMail(simpleURL)); onStateChanged(); } } else // There is some mail. See if it is new or not. To be new, the // mailbox must have been modified after it was last read AND the // current size must be greater then it was before. if (last_modified>=last_read && size>lastSize) { if (!b_new_lastSize || size>new_lastSize) { mailState = NewMail; // Let the world know of the new state emit(signal_newMail()); emit(signal_newMail(1, key)); onStateChanged(); } new_lastSize = size; b_new_lastSize = true; new_lastRead = last_read; b_new_lastRead = true; newCount = 1; } else // Finally, check if the state needs to change to OldMail if ((mailState != OldMail) && (last_read > lastRead)) { mailState = OldMail; lastRead = last_read; lastSize = size; // Let the world know of the new state emit(signal_oldMail()); emit(signal_oldMail(simpleURL)); onStateChanged(); } // If we get to this point, then the state now is exactly the // same as the state when last we checked. Do nothing at this // point. emit(signal_currentStatus(newCount, key, mailState)); } /** * The following function is lifted from unixdrop.cpp in the korn * distribution. It is (C) Sirtaj Singh Kang and is * used under the GPL license (and the author's permission). It has * been slightly modified for formatting reasons. */ int KBiffMonitor::mboxMessages() { TQFile mbox(mailbox); char buffer[MAXSTR]; int count = 0; int msg_count = 0; bool in_header = false; bool has_content_len = false; bool msg_read = false; long content_length = 0; oldCount = 0; curCount = 0; if (mbox.open(IO_ReadOnly) == false) return 0; buffer[MAXSTR-1] = 0; while (mbox.readLine(buffer, MAXSTR-2) > 0) { // read a line from the mailbox if (!strchr(buffer, '\n') && !mbox.atEnd()) { // read till the end of the line if we // haven't already read all of it. int c; while((c=mbox.getch()) >=0 && c !='\n') {} } if (!in_header && real_from(buffer)) { // check if this is the start of a message has_content_len = false; in_header = true; msg_read = false; } else if (in_header) { // check header fields if we're already in one if (compare_header(buffer, "Content-Length")) { has_content_len = true; content_length = atol(buffer + 15); } // This should handle those folders that double as IMAP or POP // folders. Possibly PINE uses these always if (strcmp(buffer, "Subject: DON'T DELETE THIS MESSAGE -- FOLDER INTERNAL DATA\n") == 0) { oldCount--; curCount--; } else { if (compare_header(buffer, "Status")) { const char *field = buffer; field += 7; while (field && (*field== ' ' || *field == '\t')) field++; if (*field == 'N' || *field == 'U' || *field == 0x0a) msg_read = false; else msg_read = true; } // Netscape *sometimes* uses X-Mozilla-Status to determine // unread vs read mail. The only pattern I could see for // sure, though, was that Read mails started with an '8'. // I make no guarantees on this... else if (compare_header(buffer, "X-Mozilla-Status")) { const char *field = buffer; field += 17; while (field && (*field== ' ' || *field == '\t')) field++; if (*field == '8') msg_read = true; else msg_read = false; } else if (buffer[0] == '\n' ) { if (has_content_len) mbox.at(mbox.at() + content_length); in_header = false; oldCount++; if (!msg_read) { count++; } else { curCount++; } } } }//in header if(++msg_count >= 100 ) { tqApp->processEvents(); msg_count = 0; } }//while mbox.close(); return count; } void KBiffMonitor::invalidLogin() { // first, we stop this monitor to be on the safe side stop(); determineState(NoConn); newCount = -1; emit(signal_invalidLogin(key)); } /////////////////////////////////////////////////////////////////////////// // KBiffSocket /////////////////////////////////////////////////////////////////////////// KBiffSocket::KBiffSocket() : async(false), socketFD(-1), messages(0), newMessages(-1) #ifdef USE_SSL , ssltunnel(0) #endif // USE_SSL { FD_ZERO(&socketFDS); /* * Set the socketTO once and DO NOT use it in any select call as this * may alter its value! */ socketTO.tv_sec = SOCKET_TIMEOUT; socketTO.tv_usec = 0; } KBiffSocket::~KBiffSocket() { close(); #ifdef USE_SSL if (ssltunnel) { delete ssltunnel; ssltunnel = 0; } #endif // USE_SSL } int KBiffSocket::numberOfMessages() { return messages; } int KBiffSocket::numberOfNewMessages() { return (newMessages > -1) ? newMessages : 0; } void KBiffSocket::close() { #ifdef USE_SSL if (isSSL() && (socketFD != -1) && (ssltunnel != 0)) { ssltunnel->close(); } #endif // USE_SSL if (socketFD != -1) ::close(socketFD); socketFD = -1; FD_ZERO(&socketFDS); } bool KBiffSocket::connectSocket(const TQString& host, unsigned short int port) { sockaddr_in sin; hostent *hent; int addr, n; // if we still have a socket, close it if (socketFD != -1) close(); // get the socket socketFD = ::socket(AF_INET, SOCK_STREAM, IPPROTO_IP); // start setting up the socket info memset((char *)&sin, 0, sizeof(sin)); sin.sin_family = AF_INET; sin.sin_port = htons(port); // get the address if ((addr = inet_addr(host.ascii())) == -1) { // get the address by host name if ((hent = gethostbyname(host.ascii())) == 0) { switch (h_errno) { case HOST_NOT_FOUND: break; case NO_ADDRESS: break; case NO_RECOVERY: break; case TRY_AGAIN: break; default: break; } close(); return false; } memcpy((void *)&sin.sin_addr, *(hent->h_addr_list), hent->h_length); } else // get the address by IP memcpy((void *)&sin.sin_addr, (void *)&addr, sizeof(addr)); // Set up non-blocking io if requested if (async) { int flags = fcntl(socketFD, F_GETFL); if (flags < 0 || fcntl(socketFD, F_SETFL, flags | O_NONBLOCK) < 0) { async = false; } } // the socket is correctly setup. now connect if ((n = ::connect(socketFD, (sockaddr *)&sin, sizeof(sockaddr_in))) == -1 && errno != EINPROGRESS) { close(); return false; } // Empty the file descriptor set FD_ZERO(&socketFDS); FD_SET(socketFD, &socketFDS); // For non-blocking io, the connection may need time to finish (n = -1) if (n == -1 && async == true) { struct timeval tv = socketTO; // Wait for the connection to come up if (select(socketFD+1, NULL, &socketFDS, NULL, &tv) != 1) { errno = ETIMEDOUT; close(); return false; } // The connection has finished. Catch any error in a call to readLine() } #ifdef USE_SSL // Initialize SSL tunnel, if needed if (isSSL()) { if (ssltunnel == 0) ssltunnel = new KSSL(true); else ssltunnel->reInitialize(); if (ssltunnel == 0) { close(); return false; } if (ssltunnel->connect(socketFD) != 1) { close(); return false; } } #endif // USE_SSL // we're connected! see if the connection is good TQString line(readLine()); if (line.isNull() || ((line.find("200") == -1 ) && (line.find("OK") == -1) && (line.find("PREAUTH") == -1))) { if (line.isNull()) close(); return false; } // everything is swell banner = line; // save the banner for use by subclasses return true; } bool KBiffSocket::active() { return socketFD != -1; } bool KBiffSocket::isAsync() { return async; } void KBiffSocket::setAsync(bool on) { int flags = 0; async = on; if (active()) { flags = fcntl(socketFD, F_GETFL); switch (async) { case false: if (flags >= 0) fcntl(socketFD, F_SETFL, flags & ~O_NONBLOCK); break; case true: if (flags < 0 || fcntl(socketFD, F_SETFL, flags | O_NONBLOCK) < 0) async = false; break; } } } #ifdef USE_SSL bool KBiffSocket::isSSL() { return usessl; } void KBiffSocket::setSSL(bool on) { if (usessl == on) return; if (!KSSL::doesSSLWork()) { usessl = false; return; } usessl = on; if (active()) { switch (usessl) { case false: ssltunnel->close(); delete ssltunnel; ssltunnel = 0; break; case true: if (ssltunnel == 0) ssltunnel = new KSSL(true); else ssltunnel->reInitialize(); if (ssltunnel == 0) { usessl = false; break; } if (ssltunnel->connect(socketFD) != 1) usessl = false; break; } } } #endif // USE_SSL int KBiffSocket::writeLine(const TQString& line) { int bytes = 0; // Do not try to write to a non active socket. Return error. if (!active()) return -1; #ifdef USE_SSL if (isSSL()) { if ((bytes = ssltunnel->write(line.ascii(), line.length())) <= 0) close(); } else #endif // USE_SSL if ((bytes = ::write(socketFD, line.ascii(), line.length())) <= 0) close(); return bytes; } TQString KBiffSocket::readLine() { TQString fault, response; char buffer; ssize_t bytes = -1; #ifdef USE_SSL if (isSSL()) { while (((bytes = ssltunnel->read(&buffer, 1)) > 0) && (buffer != '\n')) response += buffer; } else #endif // USE_SSL if (!async) while (((bytes = ::read(socketFD, &buffer, 1)) > 0) && (buffer != '\n')) response += buffer; else { while ( (((bytes = ::read(socketFD, &buffer, 1)) > 0) && (buffer != '\n')) || ((bytes < 0) && (errno == EWOULDBLOCK)) ) { if (bytes > 0) response += buffer; else { struct timeval tv = socketTO; if (select(socketFD+1, &socketFDS, NULL, NULL, &tv) != 1) { errno = ETIMEDOUT; break; } } } } if (bytes == -1) { // Close the socket and hope for better luck with a new one close(); return fault; } return response; } /////////////////////////////////////////////////////////////////////////// // KBiffImap /////////////////////////////////////////////////////////////////////////// KBiffImap::KBiffImap() { /* Assume that the IMAP server does no fancy authentication */ auth_cram_md5 = false; } KBiffImap::~KBiffImap() { close(); } bool KBiffImap::command(const TQString& line, unsigned int seq) { TQString messagesListString; TQStringList messagesList; bool tried_cram_md5; // are we trying CRAM-MD5 ? if (writeLine(line) <= 0) { close(); return false; } TQString ok, bad, no, response; ok.sprintf("%d OK", seq); bad.sprintf("%d BAD", seq); no.sprintf("%d NO", seq); // must be case insensitive TQRegExp status("\\* STATUS", FALSE); TQRegExp capability("\\* CAPABILITY", FALSE); TQRegExp cram_md5("AUTHENTICATE CRAM-MD5", FALSE); // are we trying CRAM-MD5 ? tried_cram_md5 = cram_md5.search(line)>=0; cram_md5 = TQRegExp("\\+ ([A-Za-z0-9+/=]+)"); while (!(response = readLine()).isNull()) { // if an error has occurred, we get a null string in return if (response.isNull()) break; // if the response is either good or bad, then return if (response.find(ok) > -1) return true; if ((response.find(bad) > -1) || (response.find(no) > -1)) break; /* The STATUS response is documented in RFC2060, 6.3.10/7.2.4 * Briefly: the response depends on command and looks like * * STATUS "some-imap-folder" ( requested-info ) * for example: * C: . STATUS "INBOX" (UNSEEN MESSAGES) * S: * STATUS "INBOX" (UNSEEN 2 MESSAGES 3) * S: . OK STATUS Completed */ if (status.search(response) >= 0) { TQRegExp unseen("UNSEEN ([0-9]*)", FALSE); if (unseen.search(response) >= 0) { TQString num = unseen.cap(1); newMessages = num.toInt(); } TQRegExp number("MESSAGES ([0-9]*)", FALSE); if (number.search(response) >= 0) { TQString num = number.cap(1); messages = num.toInt(); } } /* The CAPABILITY response is documented in RFC 3050, * sections 6.1.1 and 7.2.1 * An example: * C: . CAPABILITY * S: * CAPABILITY IMAP4rev1 IDLE AUTH=PLAIN AUTH=CRAM-MD5 * S: . OK CAPABILITY completed. */ if (capability.search(response) >= 0) { TQRegExp cram_md5_cap("AUTH=CRAM-MD5", FALSE); if (cram_md5_cap.search(response) >= 0) { auth_cram_md5 = true; } } /* AUTHENTICATE CRAM-MD5 response is documented in * RFC 3050 6.2.2 and RFC 2195 */ if (tried_cram_md5 && cram_md5.search(response)>=0) { chall_cram_md5 = KCodecs::base64Decode(cram_md5.cap(1).local8Bit()); if (chall_cram_md5.isNull()) break; return true; } } close(); return false; } TQString KBiffImap::mungeUserPass(const TQString& old_user) { TQString new_user(old_user); if (new_user.left(1) != "\"") new_user.prepend("\""); if (new_user.right(1) != "\"") new_user.append("\""); return new_user; } void KBiffImap::resetNumbers() { messages = 0; newMessages = 0; } bool KBiffImap::authenticate(int *pseq, const TQString& user, const TQString& pass) { TQString cmd, username, password; // If CRAM-MD5 is available, use it. It's the best we know. // RFC 2195 defines the CRAM-MD5 authentication method // also see RFC 3501 section 6.2.2 for the AUTHENTICATE command if( auth_cram_md5 ) { cmd = TQString("%1 AUTHENTICATE CRAM-MD5\r\n").arg(*pseq); if (command(cmd, *pseq) == false) { return false; } // calculate the real response to the challenge TQString response = user + " " + KBiffCrypt::hmac_md5(chall_cram_md5, pass); response = KCodecs::base64Encode(response.latin1()); // send the response if (command(response+"\r\n", *pseq) == false) { return false; } return true; } else // well, we tried, LOGIN is the best we can do { // imap allows spaces in usernames... we need to take care of that username = mungeUserPass(user); // also asterisks (*) in passwords. maybe it's a good idea // to _always_ munge the user and the password. password = mungeUserPass(pass); cmd = TQString().setNum(*pseq) + " LOGIN " + username + " " + password + "\r\n"; if (command(cmd, *pseq) == false) { return false; } (*pseq)++; } return true; } /////////////////////////////////////////////////////////////////////////// // KBiffPop /////////////////////////////////////////////////////////////////////////// KBiffPop::KBiffPop() : use_apop( true ) { } KBiffPop::~KBiffPop() { close(); } void KBiffPop::close() { command("QUIT\r\n"); KBiffSocket::close(); } void KBiffPop::setApop( bool enabled ) { use_apop = enabled; } bool KBiffPop::command(const TQString& line) { if (writeLine(line) <= 0) return false; TQString response; response = readLine(); // check if the response was bad. if so, return now if (response.isNull() || response.left(4) == "-ERR") { // we used to close the socket here.. but this MAY be // because the server didn't understand UIDL. the server // may react better with LIST or STAT so just fail quitely // thanks to David Barth (dbarth@videotron.ca) return false; } // if the command was UIDL then build up the newUidlList if (line == "UIDL\r\n") { uidlList.clear(); for (response = readLine(); !response.isNull() && response.left(1) != "."; response = readLine()) { uidlList.append(new TQString(response.right(response.length() - response.find(" ") - 1))); } } else // get all response lines from the LIST command // LIST and UIDL are return multilines so we have to loop around if (line == "LIST\r\n") { for (messages = 0, response = readLine(); !response.isNull() && response.left(1) != "."; messages++, response = readLine()) {} } else if (line == "STAT\r\n") { if (!response.isNull()) sscanf(response.ascii(), "+OK %d", &messages); } else // find out what the server is capable of if (line == "CAPA\r\n") { TQRegExp rx("\\bCRAM-MD5\\b"); auth_cram_md5 = false; // assume no support for (response = readLine(); !response.isNull() && response.left(1) != "."; response = readLine()) { if (response.left(4) == "SASL") auth_cram_md5 = response.find(rx) != -1; } } else // look for the CRAM-MD5 challenge if (line == "AUTH CRAM-MD5\r\n") { TQRegExp challenge("\\+ ([A-Za-z0-9+/=]+)"); if (challenge.search(response) == -1 ) { return false; } chall_cram_md5 = KCodecs::base64Decode(challenge.cap(1).local8Bit()); } return !response.isNull(); } KBiffUidlList KBiffPop::getUidlList() const { return uidlList; } /*! This method parses the initial response from the POP3 server. The response is defined in RFC 1939 sections 4 and 7. \fn KBiffPop::parse_banner(void) */ bool KBiffPop::parseBanner(void) { // RFC 1939 section 3 says server MUST use uppercase if( banner.left(3) != "+OK" ) { auth_apop = false; return false; } // Look for the banner part that indicates APOP support TQRegExp rx("(<[a-zA-Z0-9_+.-]+@[a-zA-Z0-9_+.-]+>)"); if( rx.search(banner) == -1 || !use_apop ) { auth_apop = false; } else { chall_apop = rx.cap(1).latin1(); auth_apop = true; } return true; } /*! This method authenticates using the most secure technique available. \fn KBiffPop::authenticate(const TQString& user, const TQString& pass) */ bool KBiffPop::authenticate(const TQString& user, const TQString& pass) { TQString popcommand; // CRAM-MD5 authentication is the most secure we can handle // the use of the AUTH command is documented in RFC 1734 if( auth_cram_md5 ) { if (this->command("AUTH CRAM-MD5\r\n") == false) { return false; } // calculate the real response to the challenge TQString response = user + " " + KBiffCrypt::hmac_md5(chall_cram_md5, pass); response = KCodecs::base64Encode(response.latin1()); // send the response if (this->command(response+"\r\n") == false) { return false; } return true; } // APOP is not as secure as CRAM-MD5 but it's still better // than sending the password in the clear if( auth_apop ) { TQCString digest; KMD5 md5(chall_apop); md5.update(pass); digest = md5.hexDigest(); popcommand = TQString("APOP %1 %2\r\n").arg(user, digest.data()); if (this->command(popcommand) == false) { return false; } return true; } // lastly we'll try regular, plain-text authentication popcommand = "USER " + user + "\r\n"; if (this->command(popcommand) == false) { return false; } popcommand = "PASS " + pass + "\r\n"; if (this->command(popcommand) == false) { return false; } return true; } /////////////////////////////////////////////////////////////////////////// // KBiffNntp /////////////////////////////////////////////////////////////////////////// KBiffNntp::~KBiffNntp() { close(); } bool KBiffNntp::command(const TQString& line) { int bogus; if (writeLine(line) <= 0) return false; TQString response; while (!(response = readLine()).isNull()) { // return if the response is bad if (response.find("500") > -1) { close(); return false; } // find return codes for tcp, user, pass TQString code(response.left(3)); if ((code == "200") || (code == "281") || (code == "381")) return true; // look for the response to the GROUP command // 211 if (code == "211") { sscanf(response.ascii(), "%d %d %d %d", &bogus, &messages, &firstMsg, &lastMsg); return true; } } close(); return false; } int KBiffNntp::first() const { return firstMsg; } int KBiffNntp::last() const { return lastMsg; } ///////////////////////////////////////////////////////////////////////// /* The following is a (C) Sirtaj Singh Kang */ #define whitespace(c) (c == ' ' || c == '\t') #define skip_white(c) while(c && (*c) && whitespace(*c) ) c++ #define skip_nonwhite(c) while(c && (*c) && !whitespace(*c) ) c++ #define skip_token(buf) skip_nonwhite(buf); if(!*buf) return false; \ skip_white(buf); if(!*buf) return false; static const char *month_name[13] = { "jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec", NULL }; static const char *day_name[8] = { "sun", "mon", "tue", "wed", "thu", "fri", "sat", 0 }; static bool real_from(const TQString& orig_buffer) { /* A valid from line will be in the following format: From [TZ1 [TZ2]] */ int day; int i; int found; const char *buffer = (const char*)orig_buffer.ascii(); /* From */ if(!buffer || !*buffer) return false; if (strncmp(buffer, "From ", 5)) return false; buffer += 5; skip_white(buffer); /* */ if(*buffer == 0) return false; skip_token(buffer); /* */ found = 0; for (i = 0; day_name[i] != NULL; i++) found = found || (tqstrnicmp(day_name[i], buffer, 3) == 0); if (!found) return false; skip_token(buffer); /* */ found = 0; for (i = 0; month_name[i] != NULL; i++) found = found || (tqstrnicmp(month_name[i], buffer, 3) == 0); if (!found) return false; skip_token(buffer); /* */ if ( (day = atoi(buffer)) < 0 || day < 1 || day > 31) return false; return true; } static const char* compare_header(const char* header, const char* field) { int len = strlen(field); if (tqstrnicmp(header, field, len)) return NULL; header += len; if( *header != ':' ) return NULL; header++; while( *header && ( *header == ' ' || *header == '\t') ) header++; return header; }