#include "libskrewt.h" #include "utils.h" #include #include using namespace std; void parse_content(const string type_spec_line, string &maintype, string &boundary) { cerr << "parser called with: " << type_spec_line << endl; string get_type(type_spec_line); size_t where = get_type.find_first_of(" \t;\n"); string rest; if (where == string::npos) { // keep whole string } else { rest = get_type.substr(where+1); get_type = get_type.substr(0,where); } where = get_type.find("/"); if (where == string::npos){ maintype = ""; cerr << "could not find / in " << get_type << endl; } else { maintype = get_type.substr(0, where); } // now need to find boundary string srch = "boundary="; where = rest.find(srch); if (where != string::npos) { where += srch.length(); boundary = rest.substr(where); if (boundary[0] == '"') { boundary = boundary.substr(1); where = boundary.find_first_of("\""); } else { where = boundary.find_first_of(" \t;\n"); } if (where == string::npos) { /* do nothing, boundary=boundary as a whole */ } else { boundary = boundary.substr(0, where); } } else { //xxxxxxx cerr << "boundary= not found in " << type_spec_line << endl; } } int skrewt::krunch_rfrom(){ stringstream parse; parse.str(received_from); string word; parse >> word; if (word != "from") { cerr << progid << " bad 'Received: from' line ... '" << word << "'" << endl; return ex_syserr; } parse >> proximta_rDNS; for (;;) { parse >> word; size_t len = word.length(); if (len < 2) { cerr << progid << " bad 'Received: from' line ;;; short '" << word << "'" << endl; return ex_syserr; } if (word == "by") break; if (word == "(HELO" /*)*/) { parse >> proximta_HELO; proximta_HELO = rtrim(proximta_HELO, "()"); continue; } if (word[0] != '(' || word[len-1] != ')') { cerr << progid << " bad 'Received: from' line ;;; no parens: '" << word << "'" << endl; return ex_syserr; } proximta_IP = word.substr(1, len-2); size_t where = proximta_IP.find("@"); if (where != string::npos){ proximta_AuthUser = proximta_IP.substr(0, where); proximta_IP = proximta_IP.substr(1+where); } } // provide some kind of default? maybe not. if (0) if (proximta_HELO == "") { proximta_HELO = proximta_rDNS + " (+-)"; } return 0; } xstr skrewt::getLine(istream& xin){ string rslt; if (lookahead.err == 0) { rslt = lookahead.str; lookahead.err = 1; return {0, rslt}; } if (getline(xin, rslt).fail()) return {1, ""}; else return {0, rslt}; } xstr skrewt::getRecord(istream& xin){ xstr line = getLine(xin); if (line.err) return line; msgsize += line.str.length()+1; if (msgsize > maxsize) { cerr << progid << " header rejection: bigger than " << maxsize << endl; exeunt(ex_spam); } bigbuf.push_back(line.str); // for a folded record, this is the first line: string headrec = noCR(line.str); // inner loop to build a multi-line record e.g. folded record: for (;;) { if (xin.eof()) break; if (xin.bad()) return {1, ""}; xstr more = getLine(xin); if (more.err) continue; char ch(0); if (more.str != "") ch = more.str[0]; if (ch != ' ' && ch != '\t') { lookahead = more; break; } msgsize += line.str.length()+1; if (msgsize > maxsize) { cerr << progid << " folded header rejection: bigger than " << maxsize << endl; exeunt(ex_spam); } bigbuf.push_back(line.str); headrec += "\n" + noCR(line.str); } return {0, headrec}; } int skrewt::headers(istream& xin){ //xxxx cerr << progid << " begins" << endl; for (;;){ // outer loop over all records in the header if (xin.eof()) break; if (xin.bad()) return 1; xstr rec = getRecord(xin); if (rec.err) break; string headrec = rec.str; // here with a fully assembled header record // headrec (unlike line) contains no DOS CR characters int len = headrec.length(); if (len == 0) { saw_blank_line = 1; break; // no more headers in this message } // here if it's a header line string headword; string rest; size_t where = headrec.find(":"); if (where != string::npos) { headword = headrec.substr(0, where); rest = ltrim(headrec.substr(1+where)); } headword = toLower(headword); if (0){ } else if (headword == "from") { from = rest; } else if (headword == "to") { to = rest; } else if (headword == "return-path") { return_path = rest; } else if (headword == "message-id") { message_id = rest; } // Now pick up the proximal "received: from" line, // i.e. the latest in time, // i.e. the topmost in the file, // i.e. the one that was just now put there by qmail-smtpd: else if (headword == "received") { if (!received_from.length() && prefix("from ", rest)){ received_from = rest; } } else if (headword == "date") { date = rest; } else if (headword == "subject") { subject = rest; } else if (headword == "content-type") { content_type = rest; } else if (headword == "delivered-to") { delivered_to.push_back(rest); } recno++; if (0) if (recno <= 6) cerr << progid << "#" << recno << " " << headrec << endl; } return 0; } int skrewt::dump_headers(std::ostream& xout){ for (vector::const_iterator foo = bigbuf.begin(); foo != bigbuf.end(); foo++){ xout << *foo << endl; } return 0; } int skrewt::interstage(){ if (saw_blank_line) {/* ignore */} // Note that the headers are in reverse-chronological order. // Also note that there probably is no return-path // at the time skrewt normally runs, since it is upstream // of qmail-queue. cerr << progid << " Return-path: " << (return_path.length() ? return_path : "[not set yet]") <"}' | sort | uniq -c | sort -nr 81 17 8 8 7 6 5 4 4 4 3 2 2 2 1 1 1 1 1 1 1 #endif #if 0 /* DKIM users */ 52 d=googlegroups.com; 27 d=barackobama.com; 10 d=gmail.com; 5 d=bronto.com; 5 d=bluehornet.com; 4 d=news.abebooks.com; 2 d=yahoo.co.uk; 2 d=sbcglobal.net; 2 d=embarqmail.com; 2 d=emailms.angieslist.com; 1 d=newsletters.sourceforge.net; 1 d=members.ebay.com; 1 d=info.citibank.com; 1 d=ebay.com; 1 d=commail1.co.za; #endif list badnews; int whitelisted(0); if (subject.find("sesame") != string::npos && subject.find("swordfish") != string::npos) { whitelisted++; } for (list::const_iterator foo = delivered_to.begin(); foo != delivered_to.end(); foo++){ cerr << progid << " Delivered-to: <<<" << *foo << ">>>" << endl; if (toLower(trim(*foo)) == "jean@av8n.com") { badnews.push_back("Looping Delivered-to: " + *foo); } } if (subject.find("-please-bounce-this-") != string::npos) { badnews.push_back("by request"); } if (!date.length()) { badnews.push_back("no date"); } if (mid_required && !message_id.length()) { badnews.push_back("no message-id"); } if (badnews.size() && !whitelisted){ cerr << progid << " " << join(", ", badnews) << endl; if (error_exit){ cerr << progid << " ... badnews from '" << from << "' to '" << to << "'" << endl; exeunt(ex_spam); } } return 0; } class conner{ public: string tp; string bdy; }; int skrewt::body(std::istream& xin, std::ostream& xout){ list sitch; if (content_type.length()) { string tp, bdy; parse_content(content_type, tp, bdy); sitch.push_front({tp, bdy}); } else { // assume single-part, all text if not otherwise specified: sitch.push_front({"text", ""}); } // early-stage thinking has been done. // Now spew the rest of the message // Note that multipart messages can have sub-headers. cerr << "body begins: tp: " << sitch.front().tp << " bdy: " << sitch.front().bdy << " depth: " << sitch.size() << endl; int in_subheads(0); int textlines(0); string sub_conntype(""); for (;;){ // outer loop over all lines in the body if (xin.eof()) break; if (xin.bad()) return 1; string line; // on fail, go back to top of outer loop and check for eof versus bad if (getline(xin, line).fail()) continue; msgsize += line.length()+1; if (msgsize > maxsize) { cerr << progid << " rejection: bigger than " << maxsize << endl; maybe_exeunt(ex_spam, error_exit); } bigbuf.push_back(line); xout << line << endl; if (0) cerr << "+++" << line << endl; if (in_subheads){ if (line == "" || line == "\r") { in_subheads = 0; cerr << "+++ end of subhead" << endl; string tp, bdy; parse_content(sub_conntype, tp, bdy); cerr << tp << " '" << bdy << "'" << endl; sitch.push_front({tp, bdy}); cerr << "payload mode: tp: " << sitch.front().tp << " bdy: " << sitch.front().bdy << " depth: " << sitch.size() << endl; } } if (/* still */in_subheads){ string junk; // FIXME // in principle could worry about folded headers, // but in this application it doesn't actually matter string headword; string rest; size_t where = line.find(":"); if (where != string::npos) { headword = line.substr(0, where); rest = ltrim(line.substr(1+where)); } headword = toLower(headword); if (headword == "content-type") { sub_conntype = rest; } } else { if (sitch.front().tp == "multipart" && line == "--" + sitch.front().bdy) { cerr << "boundary: begin subhead block" << endl; in_subheads = 1; sub_conntype = ""; continue; } if (sitch.front().tp == "multipart" && line == "--" + sitch.front().bdy + "--") { sitch.pop_front(); cerr << "found subhead termination --> " << sitch.front().tp << endl; in_subheads = 1; continue; } if (sitch.front().tp == "text") textlines++; } } if (0) cerr << "textlines: " << textlines << endl; if (!textlines) { cerr << progid << " rejection: no text: " << error_exit << endl; maybe_exeunt(ex_spam, error_exit); } cerr << progid << " normal completion" << endl; return(ex_good); } void maybe_exeunt(const int sts, const int really){ if (!really) return; if (sts == ex_good) exit(sts); const char* foo = getenv("HI_Q_GROUP"); if (!foo) exit(sts); // No point in signalling ourself: sighandler_t rslt = signal(SIGUSR1, SIG_IGN); if (rslt == SIG_ERR) { cerr << "error setting signal" << endl; } int k = kill(-atoi(foo), SIGUSR1); if (k) { cerr << "kill failed on group " << atoi(foo) << " ... "; perror(0); } exit(sts); } void exeunt(const int sts){ maybe_exeunt(sts, 1); }