/* fullquottel - a program that helps to distinguish whether an email has the "tofu" style. Copyright (C) 2005 Toastfreeware - Philipp Spitzer and Gregor Herrmann This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ /** \mainpage Documentation for fullquottel * * \section Introduction * * fullquottel is a program that helps to distinguish whether an email has the * "tofu" style (tofu ... german "Text oben, Fullquote unten" for "text above, * fullquote below"; cf. "top posting", "jeopardy style", ...) or not. It takes * the (already decoded) email body as input (standard input) and returns one * of the words: 'Goodquottel' or 'Fullquottel' on stdout. * * * \section Details * * The program performs several tests to decide whether the mail is a tofu mail * or not. Each test produces a score. The final sum of the individual test * scores is compared to a threshold. If it is above it, the mail is * classified as tofu mail ('Fullquottel' is returned). Further more, the * score itself is returned as number and as as row of where each score point * produces one *. * * Each test can be customized: * - It can be turned on and off (all tests are turned on by default). * - Its score can be specified (most of the tests have the score of 1.0 by * default). * - The individual threshold of some tests can be specified. * - Some tests have additional parameters. * * * \section doc_tests The Tests * * \subsection doc_ownSigTest Own Signature Test (ownSigTest) * * This test searches for the own email signature (default in file: * ~/.signature), no matter if it is quoted or not. If the own signature is * included in a reply, this is a strong sign for a tofu mail. * * * \subsection doc_msTest Microsoft Attribution Line Test (msTest) * * People using MS Outlook (Express) often have the original mail at the bottom * of their reply below a line like "----- Original Message -----". This test * searches for this kind of lines. It is possible to compare the line number * where it was found to a certain threshold. If the line number is equal to or * higher than this threshold, the test is positive and the specified score is * added. * * Why the line number? It is possible that a user replies in a non-tofu way * but has this "original message" line at the very beginning of his reply. * * * \subsection doc_quotedToAllTest Ratio Quoted Lines To All Lines Test (quotedToAllTest) * * This test calculates the ratio between the number of quoted lines and the * number of all lines. The ratio has a value between 0 and 1. It is compared * to a threshold (default: 0.5) and in case the ratio is higher than the * threshold the score is added to the final score. Additionally a factor can * be specified that adds an additional weighted score: * * score(quotedToAllTest) = ratio > threshold ? score + ratio * factor : 0 * * * \subsection doc_bottomQuotedToAllTest Ratio Quoted Lines At Bottom Of The Mail To All Lines Test (bottomQuotedToAllTest) * * This test calculates the ratio between the number of quoted lines at the * bottom of the mail and the number of all lines. The ratio has a value * between 0 and 1. It is compared to a threshold (default: 0.5) and in case * the ratio is higher than the threshold the score is added to the final * score. Additionally a factor can be specified that adds an additional * weighted score: * * score(bottomQuotedToAllTest) = ratio > threshold ? score + ratio * factor : 0 * * * \subsection doc_bottomQuotedToQuotedTest Ratio Quoted Lines At Bottom Of The Mail To Quoted Lines Test (bottomQuotedToQuotedTest) * * This test calculates the ratio between the number of quoted lines at the * bottom of the mail and the number of all quoted lines. The ratio has a value * between 0 and 1. It is compared to a threshold (default: 0.5) and in case * the ratio is higher than the threshold the score is added to the final * score. Additionally a factor can be specified that adds an additional * weighted score: * * score(bottomQuotedToQuotedTest) = ratio > threshold ? score + ratio * factor : 0 * * * \subsection doc_singleBottomQuoteTest Single Quote Block At The Bottom of The Mail Test (mailSingleBottomQuote) * * This tests checks if the mail has only one quote block and if this quote block is * at the bottom of the mail - the classical tofu style. * * * \section doc_links Links used for programming * * \subsection links_optionParser option parser * * - http://getpot.sourceforge.net/ * - http://platon.sk/projects/main_page.php?project_id=3 * - http://www.gubbe.ch/code/libcfgparse.php * - http://stlplus.sourceforge.net/stlplus/docs/ini_manager.html * - http://config-plus.sourceforge.net/ * * * \subsection links_mimeDecoder Mime decoder * * - http://directory.fsf.org/libs/cpp/mimetic.html * - http://codesink.org/mimetic_mime_library.html#snippets */ /// \cond doc_programmer #include #include #include #include #include #include #include #include // for setprecision #include #include // for isspace() #include using namespace std; // Error codes const int err_noinput = 255; const int err_nosigfile = 254; const int err_options = 253; /// This type represents text seperated in lines. /// Line delimiters should be preserved so that the unseperated text can be easily reconstructed. typedef vector TextLines; typedef multimap MultiMap; // Command line parsing // -------------------- const char *argp_program_version = "fullquottel 0.1.2"; const char *argp_program_bug_address = ""; static char doc[] = "fullquottel - tool for detecting full quotes in mails or postings"; static char args_doc[] = "< infile > outfile"; static struct argp_option argp_options[] = { {"scorethreshold", 't', "NUMBER", 0, "Threshold for final score that discriminates between Goodquottel and Fullquottel (default: 1.5)", 0 }, {"quotechars", -1, "CHARS", 0, "Chars used for quoting (default: >|#)", 1 }, {"debug", 'd', 0, 0, "Debug option (prints single test results; default: off)", 2 }, {"ownsigtest", -10, "1|0", 0, "Turn on/off ownSigTest (default: on)", 10 }, {"ownsigtestscore", -11, "NUMBER", 0, "Score for ownSigTest (default: 1.0)", 11 }, {"ownsigtestfile", 'f', "SIGFILE", 0, "Signature file to test against (default: ~/.signature)", 12 }, {"mstest", -20, "1|0", 0, "Turn on/off msTest (Microsoft attribution lines; default: on)", 20 }, {"mstestscore", -21, "NUMBER", 0, "Score for msTest (default: 1.0)", 21 }, {"mstestthreshold", -22, "NUMBER", 0, "Threshold for msTest (at or below which line a Microsoft attribution line ist found; default: 2)", 22 }, {"quotedtoalltest", -30, "1|0", 0, "Turn on/off quotedToAllTest (ratio quoted lines to all; default: on)", 30 }, {"quotedtoalltestscore", -31, "NUMBER", 0, "Score for quotedToAllTest (default: 1.0)", 31 }, {"quotedtoalltestfactor", -32, "NUMBER", 0, "Result = score + ratio * FACTOR (default: 0)", 32 }, {"quotedtoalltestthreshold", -33, "NUMBER", 0, "Ratio threshold for activating quotedToAllTest (default: 0.5)", 33 }, {"bottomquotedtoalltest", -40, "1|0", 0, "Turn on/off bottomQuotedToAllTest (ratio quoted lines at bottom to all; default: on)", 40 }, {"bottomquotedtoalltestscore", -41, "NUMBER", 0, "Score for bottomQuotedToAllTest (default: 1.0)", 41 }, {"bottomquotedtoalltestfactor", -42, "NUMBER", 0, "Result = score + ratio * FACTOR (default: 0)", 42 }, {"bottomquotedtoalltestthreshold", -43, "NUMBER", 0, "Ratio threshold for activating bottomQuotedToAllTest (default: 0.5)", 43 }, {"bottomquotedtoquotedtest", -50, "1|0", 0, "Turn on/off bottomQuotedToQuotedTest (ratio quoted lines at bottom to all quoted lines; default: on)", 50 }, {"bottomquotedtoquotedtestscore", -51, "NUMBER", 0, "Score for bottomQuotedToQuotedTest (default: 1.0)", 51 }, {"bottomquotedtoquotedtestfactor", -52, "NUMBER", 0, "Result = score + ratio * FACTOR (default: 0)", 52 }, {"bottomquotedtoquotedtestthreshold", -53, "NUMBER", 0, "Ratio threshold for activating bottomQuotedToQuotedTest (default: 0.5)", 53 }, {"singlebottomquotetest", -60, "1|0", 0, "Turn on/off singleBottomQuoteTest (only one quote block, and at the bottom; default: on)", 60 }, {"singlebottomquotetestscore", -61, "NUMBER", 0, "Score for singleBottomQuoteTest (default: 1.0)", 61 }, {"\nHINT: All long options (without leading --) can be used in ~/.fullquottelrc\n", -70, 0, OPTION_DOC, "", 70 }, { 0 } }; /// Base class of structures that store informations about the rating/test of mail properties. struct RatingBase { bool active; ///< to turn the test on and off. double score; ///< score that should be added if the test succeeds. RatingBase() {active = true; score = 1;} }; /// \brief structure to store the rating of an ratio test: /// /// resultscore = (value >= threshold) ? score + value * factor : 0; struct RatioRating: public RatingBase { double threshold; double factor; double rate(double value) const {return (active && value >= threshold) ? score + value * factor: 0;} RatioRating() {threshold = 0.5; factor = 0;} }; /// \brief Structure to store the rating of an integer test: /// /// resultscore = (value >= threshold) ? score : 0; struct IntegerRating: public RatingBase { int threshold; double rate(int value) const {return (active && value >= threshold) ? score : 0;} }; /// \brief Structure to store the rating of an integer test: /// /// resultscore = value ? score : 0; struct BoolRating: public RatingBase { double rate(bool value) const {return (active && value) ? score : 0;} }; /// Structure for signature test options. struct SigRating: public BoolRating { string file; ///< signature file (with full path. ~ is possible for $HOME) }; /// \brief Structure to store the program options. /// /// It stores all options and parameters that are needed to run the program. The command line options, system wide file options, /// and user file options are transfered within this struct and after that, only this struct is used to access the options. struct Options { double scoreThreshold; ///< Threshold of the added scores of the tests, that decide, whether the mail is a tofu mail or not (default: 0.5). string quoteChars; ///< Character(s) that are used to quote. The default value is ">|#". bool debug; ///< If this option is set, the internal variables that show the mail statistics are print (default: false). TextLines attributionLines; ///< List of Microsoft Attribution Lines (needed for the \ref doc_msTest). // ratings/tests SigRating ownSigTest; ///< Signature filename and rating options for the \ref doc_ownSigTest (default: ~/.signature). IntegerRating msTest; ///< Rating options for the \ref doc_msTest. RatioRating quotedToAllTest; ///< Rating options for the \ref doc_quotedToAllTest. RatioRating bottomQuotedToAllTest; ///< Rating options for the \ref doc_bottomQuotedToAllTest. RatioRating bottomQuotedToQuotedTest; ///< Rating options for the \ref doc_bottomQuotedToQuotedTest. BoolRating singleBottomQuoteTest; ///< Rating options for the \ref doc_singleBottomQuoteTest. /// the constructor initializes the options with their default values. Options() { scoreThreshold = 1.5; quoteChars = ">|#"; debug = false; // should we keep the following? attributionLines.push_back("-----Ursprüngliche Nachricht-----"); attributionLines.push_back("-----Original Message-----"); attributionLines.push_back("----- Ursprüngliche Nachricht -----"); attributionLines.push_back("----- Original Message -----"); // ratings ownSigTest.file = "~/.signature"; msTest.threshold = 2; } }; /// searches for key within the multimap. If found, its value is assigned to option. void setStringOptionFromMultiMap(const MultiMap& mm, const string& key, string& option) { typedef MultiMap::const_iterator CI; pair entries = mm.equal_range(key); if (entries.first != entries.second) option = entries.first->second; } void setDoubleOptionFromMultiMap(const MultiMap& mm, const string& key, double& option) { string value; setStringOptionFromMultiMap(mm, key, value); if (value.empty()) return; istringstream ist(value); ist >> option; } void setIntOptionFromMultiMap(const MultiMap& mm, const string& key, int& option) { string value; setStringOptionFromMultiMap(mm, key, value); if (value.empty()) return; istringstream ist(value); ist >> option; } void setBoolOptionFromMultiMap(const MultiMap& mm, const string& key, bool& option) { string value; setStringOptionFromMultiMap(mm, key, value); if (value.empty()) return; istringstream ist(value); ist >> option; } /// incorporates the multimap to the options void useConfig(Options& options, const MultiMap& mm) { typedef MultiMap::const_iterator CI; setDoubleOptionFromMultiMap(mm, "scorethreshold", options.scoreThreshold); setStringOptionFromMultiMap(mm, "quotechars", options.quoteChars); setBoolOptionFromMultiMap(mm, "debug", options.debug); setBoolOptionFromMultiMap(mm, "ownsigtest", options.ownSigTest.active); setDoubleOptionFromMultiMap(mm, "ownsigtestscore", options.ownSigTest.score); setStringOptionFromMultiMap(mm, "ownsigtestfile", options.ownSigTest.file); setBoolOptionFromMultiMap(mm, "mstest", options.msTest.active); setDoubleOptionFromMultiMap(mm, "mstestscore", options.msTest.score); setIntOptionFromMultiMap(mm, "mstestthreshold", options.msTest.threshold); setBoolOptionFromMultiMap(mm, "quotedtoalltest", options.quotedToAllTest.active); setDoubleOptionFromMultiMap(mm, "quotedtoalltestscore", options.quotedToAllTest.score); setDoubleOptionFromMultiMap(mm, "quotedtoalltestthreshold", options.quotedToAllTest.threshold); setDoubleOptionFromMultiMap(mm, "quotedtoalltestfactor", options.quotedToAllTest.factor); setBoolOptionFromMultiMap(mm, "bottomquotedtoalltest", options.bottomQuotedToAllTest.active); setDoubleOptionFromMultiMap(mm, "bottomquotedtoalltestscore", options.bottomQuotedToAllTest.score); setDoubleOptionFromMultiMap(mm, "bottomquotedtoalltestthreshold", options.bottomQuotedToAllTest.threshold); setDoubleOptionFromMultiMap(mm, "bottomquotedtoalltestfactor", options.bottomQuotedToAllTest.factor); setBoolOptionFromMultiMap(mm, "bottomquotedtoquotedtest", options.bottomQuotedToQuotedTest.active); setDoubleOptionFromMultiMap(mm, "bottomquotedtoquotedtestscore", options.bottomQuotedToQuotedTest.score); setDoubleOptionFromMultiMap(mm, "bottomquotedtoquotedtestthreshold", options.bottomQuotedToQuotedTest.threshold); setDoubleOptionFromMultiMap(mm, "bottomquotedtoquotedtestfactor", options.bottomQuotedToQuotedTest.factor); setBoolOptionFromMultiMap(mm, "singlebottomquotetest", options.singleBottomQuoteTest.active); setDoubleOptionFromMultiMap(mm, "singlebottomquotetestscore", options.singleBottomQuoteTest.score); // add or replace attributionlines bool addAttributionlines = true; // default value setBoolOptionFromMultiMap(mm, "addattributionlines", addAttributionlines); if (!addAttributionlines) options.attributionLines.clear(); // Attribution lines pair entries = mm.equal_range("attributionline"); for (CI i = entries.first; i != entries.second; ++i) options.attributionLines.push_back(i->second); } /// parse function for the program options static error_t parse_opt(int key, char *arg, struct argp_state *state) { struct Options *options = (Options*) state->input; string s; if (arg) s = arg; istringstream ist(s); switch (key) { case 't': ist >> options->scoreThreshold; break; case -1: options->quoteChars = arg; break; case 'd': options->debug = true; break; case -10: ist >> options->ownSigTest.active; break; case -11: ist >> options->ownSigTest.score; break; case 'f': options->ownSigTest.file = arg; break; case -20: ist >> options->msTest.active; break; case -21: ist >> options->msTest.score; break; case -22: ist >> options->msTest.threshold; break; case -30: ist >> options->quotedToAllTest.active; break; case -31: ist >> options->quotedToAllTest.score; break; case -32: ist >> options->quotedToAllTest.factor; break; case -33: ist >> options->quotedToAllTest.threshold; break; case -40: ist >> options->bottomQuotedToAllTest.active; break; case -41: ist >> options->bottomQuotedToAllTest.score; break; case -42: ist >> options->bottomQuotedToAllTest.factor; break; case -43: ist >> options->bottomQuotedToAllTest.threshold; break; case -50: ist >> options->bottomQuotedToQuotedTest.active; break; case -51: ist >> options->bottomQuotedToQuotedTest.score; break; case -52: ist >> options->bottomQuotedToQuotedTest.factor; break; case -53: ist >> options->bottomQuotedToQuotedTest.threshold; break; case -60: ist >> options->singleBottomQuoteTest.active; break; case -61: ist >> options->singleBottomQuoteTest.score; break; case ARGP_KEY_ARG: if (state->arg_num > 0) // we have no arguments argp_usage (state); break; default: return ARGP_ERR_UNKNOWN; } return 0; } /// argp parser. static struct argp argp = {argp_options, parse_opt, args_doc, doc}; // Helper functions // ---------------- /// returns true if substring is part of string bool isSubString(const string &needle, const string &haystack) { return haystack.find(needle) != string::npos; } /// cuts leading whitespace string ltrim(const string& text) { string::size_type i = 0; while (i < text.size() && isspace(text[i])) ++i; return text.substr(i); } /// cuts trailing whitespace string rtrim(const string& text) { string::size_type i = text.size(); while (i != 0 && isspace(text[i-1])) --i; return text.substr(0, i); } /// cuts leading and trailing whitespace string trim(const string& text) { return ltrim(rtrim(text)); } /// loads a file into a TextLines class. TextLines loadTextLines(istream& istr) { TextLines content; while (istr) { string line; getline(istr, line); if (istr) content.push_back(line + '\n'); } return content; } /// Loads a key=value file (lines starting with # or empty lines are ignored) /// Multiple values for one key are allowed /// If an syntax error occurs, an exception is thrown. MultiMap multiMapFromTextLines(const TextLines& lines) throw(runtime_error) { MultiMap mm; for (unsigned i = 0; i != lines.size(); ++i) { string line = trim(lines[i]); if (line.empty()) continue; if (line[0] == '#') continue; string::size_type pos = line.find("="); if (pos == string::npos) throw std::runtime_error("hash parsing error"); // todo: better error message with line number string key = rtrim(line.substr(0, pos)); string value = ltrim(line.substr(pos+1)); mm.insert(make_pair(key, value)); } return mm; } /// expands leading ~ to $HOME string expandTildeToHome(string file) { if (file.size() > 0 && file[0] == '~') { string homeDir; char* home = getenv("HOME"); if (home) homeDir = home; file.replace(0, 1, homeDir); } return file; } /// Testing purposes: Return TextLines on cout void showTextLines(const TextLines& tl, bool addEndl = false) { for (TextLines::size_type i = 0; i != tl.size(); ++i) { cout << tl[i]; if (addEndl) cout << endl; } } /// Testing purposes void showMultiMap(const MultiMap& mm) { for (MultiMap::const_iterator i = mm.begin(); i != mm.end(); ++i) { cout << (*i).first << "==" << (*i).second << "|" << endl; } } /// returns true, if the line begins with the specified Quotestrings bool isQuotedLine(const string &line, const string "eChars) { string text = ltrim(line); if (!text.size()) return false; for (string::size_type i = 0; i != quoteChars.size(); ++i) if (quoteChars[i] == text[0]) return true; return false; } /// cuts the signature out of the body and returns true. If the signature is not found, it returns false and leaves the body untouched. bool cutSignature(TextLines& body, const string "eChars) { for (TextLines::size_type i = body.size(); i != 0; --i) { if (isQuotedLine(body[i-1], quoteChars)) return false; if (body[i-1].find("-- ") == 0) { // found signature body.erase(body.begin()+i-1, body.end()); return true; } } return false; } /// counts quoted lines TextLines::size_type quotedLines(const TextLines &body, const string "eChars) { TextLines::size_type result = 0; for (TextLines::size_type i = 0; i != body.size(); ++i) if (isQuotedLine(body[i], quoteChars)) ++result; return result; } // Score functions // --------------- /// returns true if the signature is found within the body. bool ownSig(const TextLines &body, const TextLines &signature) { if (!signature.size()) return false; TextLines::size_type bodyPos = 0; while (bodyPos != body.size()) { if (!isSubString(signature[0], body[bodyPos])) {++bodyPos; continue;} TextLines::size_type p = bodyPos; bool isSig = true; for (TextLines::size_type sigPos = 1; sigPos < signature.size(); ++sigPos) { ++p; if (p == body.size()) return false; if (!isSubString(signature[sigPos], body[p])) {isSig = false; break;} } if (isSig) return true; ++bodyPos; } return false; } /// returns the amount of quoted lines at the bottom of the message unsigned quotedLinesBottom(const TextLines& bodyNoSig, const string& quoteChars) { unsigned i; unsigned e = 0; // counts the empty lines at the end of the mail for (i = bodyNoSig.size(); i != 0; --i) { if (trim(bodyNoSig[i-1]).size() == 0) {++e; continue;} // ignore empty lines at bottom if (!isQuotedLine(bodyNoSig[i-1], quoteChars)) break; } return bodyNoSig.size() - i - e; } /// Returns the number of the quote blocks unsigned quoteBlockCount(const TextLines& bodyNoSig, const string& quoteChars) { unsigned result = 0; bool wasQuoted = false; for (unsigned i = 0; i != bodyNoSig.size(); ++i) { if (isQuotedLine(bodyNoSig[i], quoteChars)) { if (!wasQuoted) { ++result; wasQuoted = true; } } else wasQuoted = false; } return result; } /// Searches for a line from Outlook-like programs that shows the beginning of the reply /// (like -----Original Message-----) and returns the line number if it is found, 0 otherwise. unsigned microsoftAttributionLineNumber(const TextLines& bodyNoSig, const TextLines& attributionLines) { for (unsigned i = 0; i != bodyNoSig.size(); ++i) { for (unsigned j = 0; j != attributionLines.size(); ++j) if (bodyNoSig[i].find(attributionLines[j]) != string::npos) return i+1; } return 0; } // Main function // ------------- int main(int argc, char *argv[]) { // Settings Options options; // system config file /etc/fullquottelrc ifstream sysconf_stream("/etc/fullquottelrc"); if (sysconf_stream) { TextLines tlSysconf = loadTextLines(sysconf_stream); MultiMap mmSysconf = multiMapFromTextLines(tlSysconf); useConfig(options, mmSysconf); } // system config file ~/.fullquottelrc ifstream userconf_stream(expandTildeToHome("~/.fullquottelrc").c_str()); if (userconf_stream) { TextLines tlUserconf = loadTextLines(userconf_stream); MultiMap mmUserconf = multiMapFromTextLines(tlUserconf); useConfig(options, mmUserconf); } // parse command line options argp_parse (&argp, argc, argv, 0, 0, &options); // Get message body from stdin and store it in body. TextLines body; if (!cin.good()) { cerr << "No input on stdin." << endl; return err_noinput; } body = loadTextLines(cin); // load signature file string sigFile = expandTildeToHome(options.ownSigTest.file); ifstream sig_stream(sigFile.c_str()); // maybe use with option ios_base::binary if (!sig_stream) { cerr << "Sigfile " << options.ownSigTest.file << " missing or unreadable." << endl; return err_nosigfile; } TextLines signature = loadTextLines(sig_stream); // create a non-signature version of the mail and store it in bodyNoSig TextLines bodyNoSig = body; cutSignature(bodyNoSig, options.quoteChars); // debug // showTextLines(options.attributionLines, true); // showTextLines(bodyNoSig); // Analyze mail // basic values and "simple" tests bool mailOwnSig = ownSig(bodyNoSig, signature); unsigned mailLines = bodyNoSig.size(); unsigned mailQuotedLines = quotedLines(bodyNoSig, options.quoteChars); unsigned mailQuotedLinesBottom = quotedLinesBottom(bodyNoSig, options.quoteChars); unsigned mailQuoteBlockCount = quoteBlockCount(bodyNoSig, options.quoteChars); unsigned mailMicrosoftAttributionLineNumber = microsoftAttributionLineNumber(bodyNoSig, options.attributionLines); // "combined" test double mailQuotedToAll = mailLines ? (double) mailQuotedLines / (double) mailLines : 0; double mailBottomQuotedToQuoted = mailQuotedLines ? (double) mailQuotedLinesBottom / (double) mailQuotedLines : 0; double mailBottomQuotedToAll = mailLines ? (double) mailQuotedLinesBottom / (double) mailLines : 0; bool mailSingleBottomQuote = mailQuotedLinesBottom > 0 && mailQuoteBlockCount == 1; // Debug output if (options.debug) { cout << "mailOwnSig: " << mailOwnSig << endl; cout << "mailLines: " << mailLines << endl; cout << "mailQuotedLines: " << mailQuotedLines << endl; cout << "mailQuotedLinesBottom: " << mailQuotedLinesBottom << endl; cout << "mailQuoteBlockCount: " << mailQuoteBlockCount << endl; cout << "mailMicrosoftAttributionLineNumber: " << mailMicrosoftAttributionLineNumber << endl; cout << "mailQuotedToAll: " << mailQuotedToAll << endl; cout << "mailBottomQuotedToQuoted: " << mailBottomQuotedToQuoted << endl; cout << "mailBottomQuotedToAll: " << mailBottomQuotedToAll << endl; cout << "mailSingleBottomQuote: " << mailSingleBottomQuote << endl; } // Scoring double score = 0; if (bodyNoSig.size() == 0) { } else { score += options.ownSigTest.rate(mailOwnSig); score += options.msTest.rate(mailMicrosoftAttributionLineNumber); score += options.quotedToAllTest.rate(mailQuotedToAll); score += options.bottomQuotedToAllTest.rate(mailBottomQuotedToAll); score += options.bottomQuotedToQuotedTest.rate(mailBottomQuotedToQuoted); score += options.singleBottomQuoteTest.rate(mailSingleBottomQuote); } if (options.debug) { cout << "Score: " << score << endl; } bool fullQuottel = score > options.scoreThreshold; string scoreText; for (int s = 1; s <= score+0.5; ++s) scoreText += '*'; // Show result cout << (fullQuottel ? "Fullquottel" : "Goodquottel") << " [" << fixed << setprecision(2) << score << "] (" << scoreText << ")" << endl; return 0; } /// \endcond