filtermail-1.06.00/build0000755000175000017500000000702114741231120014012 0ustar frankfrank#!/usr/bin/icmake -t. #define LOGENV "FILTERMAIL" #include "icmconf" list g_log; string g_logPath = getenv(LOGENV)[1], g_logMark, // unique-marker for g_log entries g_cwd = chdir(""); // initial working directory, ends in / int g_echo = ON; // MODIFIED, existing variable int g_installing; // set to 1 by install. int g_lognr; // unique-marker number counter for g_log entries #include "icmake/setopt" #include "icmake/cuteoln" #include "icmake/backtick" #include "icmake/run" #include "icmake/md" #include "icmake/findall" #include "icmake/loginstall" #include "icmake/logzip" #include "icmake/logfile" #include "icmake/uninstall" #include "icmake/pathfile" #include "icmake/special" #include "icmake/clean" #include "icmake/manpage" #include "icmake/install" #include "icmake/gitlab" void main(int argc, list argv) { string option; int idx; for (idx = listlen(argv); idx--; ) { if (argv[idx] == "-q") { g_echo = OFF; argv -= (list)"-q"; } } echo(g_echo); option = argv[1]; if (option == "clean") clean(0); if (option == "distclean") clean(1); if (option == "install") install(argv[2], argv[3]); if (option == "uninstall") uninstall(argv[2]); if (option != "") special(); if (option == "gitlab") gitlab(); if (option == "man") manpage(); if ("VERSION" younger "version/version.h") system("touch version/version.h main.cc usage.cc"); if (option == "library") { system("icmbuild library"); exit(0); } if (option == "program") { system("icmbuild program"); exit(0); } if (option == "xref") { system("icmbuild program"); run("oxref -fxs tmp/lib" LIBRARY ".a > " PROGRAM ".xref"); exit(0); } printf("Usage: build [-q] what\n" "Where\n" " [-q]: run quietly, do not show executed commands\n" "`what' is one of:\n" " clean - clean up remnants of previous " "compilations\n" " distclean - clean + fully remove tmp/\n" " library - build " PROGRAM "'s library\n" " man - build the man-page (requires " "Yodl)\n" " program - build " PROGRAM "\n" " xref - same a `program', also builds " "xref file\n" " using oxref\n" " install selection [base] - to install the software in the \n" " locations defined in the INSTALL.im file,\n" " optionally below base\n" " selection can be\n" " x, to install all components,\n" " or a combination of:\n" " b (binary program),\n" " d (documentation),\n" " m (man-pages)\n" " uninstall logfile - remove files and empty directories listed\n" " in the file 'logfile'\n" " gitlab - prepare gitlab's web-pages update\n" " (internal use only)\n" "\n" ); exit(1); } filtermail-1.06.00/changelog0000644000175000017500000000634514741167404014664 0ustar frankfrankfiltermail (1.06.00) * Requires bobcat >= 6.07.00 and icmake >= 13.00,03. * Changed ArgConfig::None into ArgConfig::NoArg. * Building uses a SPCH and multi-compilation. * The compiler is called using the value of the ${ICMAKE_CPPSTD} environment variable specifying the C++ standard to use. -- Frank B. Brokken Mon, 13 Jan 2025 11:46:20 +0100 filtermail (1.05.00) * The pattern recognizing IP4-addresses in the class Cidr (s_ip4) was modified: it is now surrounded by square brackets, as used by Received: headers. * The option --IP4-pattern (-p) can be used to modify the pattern used by Cidr to recognize IP-4 addresses in mail headers -- Frank B. Brokken Sat, 09 Dec 2023 12:52:37 +0100 filtermail (1.04.00) * Using the 'main' branch as the primary gitlab branch. The 'master' branch is no longer maintained and may be removed at some point in the future. * Added IUO maintenance files 'excluded' and 'sourcetar', removed obsolete file inspect/cidr/cidr.h.bak * Implemented the facilities of 'inspect' in 'filtermail' using option -I, discontinuing the 'inspect' program -- Frank B. Brokken Sat, 16 Sep 2023 19:42:20 +0200 filtermail (1.03.00) * The default location of the configuration file is set to ~/.filtermail/config -- Frank B. Brokken Sat, 16 Sep 2023 09:54:53 +0200 filtermail (1.02.00) * Using :HDRS: instead of :LOG: to append From: and Subject: headers to mail files. * Updated the usage info and filtermail's man page. -- Frank B. Brokken Wed, 16 Aug 2023 13:50:33 -0700 filtermail (1.01.00) * Added option --syntax performing a syntax check of the specified filter rules. * The three e-mail categories (accept, ignore, spam) no longer have default destinations, and support the specification :LOG: resulting in logging the received e-mail's From: and Subject: headers in the log file without actually saving the received e-mail on file. * Cosmetics of inspect's usage info. -- Frank B. Brokken Wed, 09 Aug 2023 12:24:31 -0700 filtermail (1.00.01) * Added the 'inspect' program inspecting the incoming mail Received: headers determining their IP addresses' countries and cidr-ranges * Added the inspect man-page * First release considered operational -- Frank B. Brokken Tue, 27 Jun 2023 12:02:46 +0200 filtermail (0.92.00) * The config-file now uses ~ (instead of @) to refer to the user's home-directory. Updated the man-page. -- Frank B. Brokken Sun, 25 Jun 2023 10:03:47 +0200 filtermail (0.91.00) * Code cleanup -- Frank B. Brokken Thu, 15 Jun 2023 13:46:17 +0200 filtermail (0.90.00) * Removed 'not' operators from the rules file and after the date-stamps of individual rules. Rule expressions can specify 'not' operators between the expreession's matching mode and regex specification * Redefined the logging format. -- Frank B. Brokken Wed, 14 Jun 2023 19:40:53 +0200 filtermail (0.01.00) * Initial Version. -- Frank B. Brokken Mon, 05 Jun 2023 11:09:17 +0200 filtermail-1.06.00/cidr/0000755000175000017500000000000014741231233013713 5ustar frankfrankfiltermail-1.06.00/cidr/data.cc0000644000175000017500000000103414535067734015147 0ustar frankfrank#include "cidr.ih" Pattern Cidr::s_pattern{ R"(((\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3}))(/(\d{1,2}))?)" }; // 12 3 4 5 6 7 // 1: cidr address // 2..5: octets // 6: optional mask // 7: mask # bits to use // // plain IP address //FBB::Pattern Cidr::s_ip4{ // R"(\[((\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3}))\])" // }; // // match indices: // // 1: full IP address // // 2..5: individual octets filtermail-1.06.00/cidr/icmconf0000644000175000017500000000007314535067734015272 0ustar frankfrank#define LIBRARY "cidr" #include "../icmconf.lib" filtermail-1.06.00/cidr/cidr.f0000644000175000017500000000022614535067734015021 0ustar frankfrankinline Cidr::Cidr(std::string const &ip4) : d_ip4(ip4) {} inline bool Cidr::matchIP4() const { return d_cidr == (binary(d_ip4) & d_mask); } filtermail-1.06.00/cidr/binary.cc0000644000175000017500000000076014535067734015527 0ustar frankfrank#define XERR #include "cidr.ih" // pattern like 209.85.218.45 // cf. data.cc: // s_ip4{R"(((\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})))" // 12 3 4 5 // elements 2..5 contain the subsequent elements of the IP address // static uint32_t Cidr::binary(Pattern const &pattern) { uint32_t ret = 0; for (size_t value, idx = 2; idx != 6; ++idx) { ret <<= 8; value = stoul(pattern[idx]); ret |= value; } return ret; } filtermail-1.06.00/cidr/vmatch.cc0000644000175000017500000000075414535067734015530 0ustar frankfrank//#define XERR #include "cidr.ih" // overrides bool Cidr::vMatch(std::string const &headerLine) const { string hdr{ headerLine }; // inspect this hdr line while (d_ip4 << hdr) // find all matches { if (matchIP4()) // this one matches: done. return true; // no match, test beyond the hdr = d_ip4.beyond(); // last match } return false; } filtermail-1.06.00/cidr/vpattern.cc0000644000175000017500000000052314535067734016103 0ustar frankfrank//#define XERR #include "cidr.ih" // pattern is +/- {\d+\.}{3}\d+/\d+ (see data.cc for specific form) // overrides void Cidr::vPattern(std::string const &pattern) { if (not (s_pattern << pattern)) throw Exception{} << "Invalid cidr pattern `" << pattern << '\''; d_cidr = binary(s_pattern); d_mask = mask(); } filtermail-1.06.00/cidr/destructor.cc0000644000175000017500000000010114535067734016426 0ustar frankfrank//#define XERR #include "cidr.ih" // overrides Cidr::~Cidr() {} filtermail-1.06.00/cidr/mask.cc0000644000175000017500000000035514535067734015176 0ustar frankfrank#define XERR #include "cidr.ih" // static uint32_t Cidr::mask() { return s_pattern.end() < 7 ? std::numeric_limits::max() : ~( (1 << uint32_t( 32 - stoul(s_pattern[7]) ) ) - 1 ); } filtermail-1.06.00/cidr/cidr.h0000644000175000017500000000171014535067734015022 0ustar frankfrank#ifndef INCLUDED_CIDR_ #define INCLUDED_CIDR_ #include #include "../specbase/specbase.h" class Cidr: public SpecBase { uint32_t d_cidr = 0; // binary value of the pattern uint32_t d_mask = 0; // #bits to use mutable FBB::Pattern d_ip4; // plain IP4 dotted decimal addr. static FBB::Pattern s_pattern; // to match 'pattern' public: Cidr(std::string const &ip4); // .f ~Cidr() override; private: bool vMatch(std::string const &hdr) const override; void vPattern(std::string const &pattern) override; // true: s_ip4 matches bool matchIP4() const; // d_cidr & d_mask .f static uint32_t binary(FBB::Pattern const &pattern); static uint32_t mask(); // uses s_pattern }; #include "cidr.f" #endif filtermail-1.06.00/cidr/cidr.ih0000644000175000017500000000021214535067734015167 0ustar frankfrank#include "cidr.h" #include "../xerr/xerr.ih" #include #include using namespace std; using namespace FBB; filtermail-1.06.00/cidr/frame0000644000175000017500000000005614535067734014747 0ustar frankfrank//#define XERR #include "cidr.ih" Cidr:: { } filtermail-1.06.00/CLASSES0000644000175000017500000000024214535067734014026 0ustar frankfrankuinput specbase cidr rulepattern specstring stringcase stringnocase script mail expr rule rules condition options log current filter expire inspectcidr inspector filtermail-1.06.00/condition/0000755000175000017500000000000014741231233014760 5ustar frankfrankfiltermail-1.06.00/condition/condition.ih0000644000175000017500000000013514535067734017305 0ustar frankfrank#include "condition.h" #include #include "../xerr/xerr.ih" using namespace std; filtermail-1.06.00/condition/condition.h0000644000175000017500000000076614535067734017146 0ustar frankfrank#ifndef INCLUDED_CONDITION_ #define INCLUDED_CONDITION_ #include class Rules; class Condition { bool d_matched; Rules *d_rules; public: Condition(); bool matched() const; // .f bool matched(bool rulesMatched); void reset(); // d_matched = true, next Cond term .f Rules &rules(Rules &rules); // .f }; #include "condition.f" #endif filtermail-1.06.00/condition/condition1.cc0000644000175000017500000000010014535067734017343 0ustar frankfrank#define XERR #include "condition.ih" Condition::Condition() {} filtermail-1.06.00/condition/matched.cc0000644000175000017500000000030014535067734016703 0ustar frankfrank//#define XERR #include "condition.ih" bool Condition::matched(bool rulesMatched) { return d_matched ? (d_matched = rulesMatched) : false; } filtermail-1.06.00/condition/condition.f0000644000175000017500000000032614535067734017134 0ustar frankfrankinline bool Condition::matched() const { return d_matched; } inline void Condition::reset() { d_matched = true; } inline Rules &Condition::rules(Rules &rules) { return *(d_rules = &rules); } filtermail-1.06.00/condition/icmconf0000644000175000017500000000010014535067734016326 0ustar frankfrank#define LIBRARY "condition" #include "../icmconf.lib" filtermail-1.06.00/condition/frame0000644000175000017500000000007014535067734016010 0ustar frankfrank//#define XERR #include "condition.ih" Condition:: { } filtermail-1.06.00/current/0000755000175000017500000000000014741231233014454 5ustar frankfrankfiltermail-1.06.00/current/log.cc0000644000175000017500000000122514535067734015562 0ustar frankfrank//#define XERR #include "current.ih" namespace { char const *s_action[] = { "ACCEPT", "IGNORE", "SPAM", }; } void Current::log(eAction type) { Log &log = Log::instance(); if (d_rulesLine == string::npos) return; log << "Rules line " << d_rulesLine << " (" << s_action[type] << "): "; for (Data const &data: d_data) { log << data.filename << " header `" << data.header << "' "; if (data.idx == string::npos) log << "no match; "; else log << "matched by rule " << data.idx << "; "; } log << endl; d_rulesLine = string::npos; } filtermail-1.06.00/current/reset.cc0000644000175000017500000000020114535067734016114 0ustar frankfrank//#define XERR #include "current.ih" void Current::reset(size_t rulesLine) { d_rulesLine = rulesLine; d_data.clear(); } filtermail-1.06.00/current/current.h0000644000175000017500000000105614535067734016327 0ustar frankfrank#ifndef INCLUDED_CURRENT_ #define INCLUDED_CURRENT_ #include #include #include "../enums/enums.h" class Current { struct Data { std::string header; size_t idx; std::string filename; }; size_t d_rulesLine = std::string::npos; std::vector d_data; public: void reset(size_t rulesLine); void header(std::string const &hdrName); void ruleFile(std::string const &filename); void matchedRule(size_t idx); void log(eAction type); }; #endif filtermail-1.06.00/current/matchedrule.cc0000644000175000017500000000015514535067734017277 0ustar frankfrank//#define XERR #include "current.ih" void Current::matchedRule(size_t idx) { d_data.back().idx = idx; } filtermail-1.06.00/current/current.ih0000644000175000017500000000010514535067734016472 0ustar frankfrank#include "current.h" #include "../log/log.h" using namespace std; filtermail-1.06.00/current/icmconf0000644000175000017500000000007614535067734016036 0ustar frankfrank#define LIBRARY "current" #include "../icmconf.lib" filtermail-1.06.00/current/current10000644000175000017500000000010114535067734016150 0ustar frankfrank//#define XERR #include "current.ih" Current::Current() //: { } filtermail-1.06.00/current/header.cc0000644000175000017500000000020714535067734016230 0ustar frankfrank//#define XERR #include "current.ih" void Current::header(string const &hdrName) { d_data.push_back({ hdrName, string::npos }); } filtermail-1.06.00/current/rulefile.cc0000644000175000017500000000020014535067734016600 0ustar frankfrank//#define XERR #include "current.ih" void Current::ruleFile(string const &filename) { d_data.back().filename = filename; } filtermail-1.06.00/current/frame0000644000175000017500000000006414535067734015507 0ustar frankfrank//#define XERR #include "current.ih" Current:: { } filtermail-1.06.00/documentation/0000755000175000017500000000000014535067734015661 5ustar frankfrankfiltermail-1.06.00/documentation/man/0000755000175000017500000000000014741231175016423 5ustar frankfrankfiltermail-1.06.00/documentation/man/filtermail.yo0000644000175000017500000006602314535067734021144 0ustar frankfrankincludefile(../../release.yo) gagmacrowarning(filtermail name) htmlstyle(body)(color: #27408B; background: #FFFAF0) whenhtml(mailto(Frank B. Brokken: f.b.brokken@rug.nl)) SUBST(USER)(frank) DEFINEMACRO(lsoption)(3)(\ bf(--ARG1)=tt(ARG3) (bf(-ARG2))\ ) DEFINEMACRO(laoption)(2)(\ bf(--ARG1)=tt(ARG2)\ ) DEFINEMACRO(loption)(1)(\ bf(--ARG1)\ ) DEFINEMACRO(soption)(1)(\ bf(-ARG1)\ ) DEFINEMACRO(fm)(0)(bf(filtermail)) DEFINEMACRO(Fm)(0)(bf(Filtermail)) DELETEMACRO(tt) DEFINEMACRO(tt)(1)(em(ARG1)) DEFINEMACRO(itt)(1)(it() tt(ARG1):) COMMENT( man-request, section, date, distribution file, general name ) manpage(filtermail)(1)(_CurYrs_)(filtermail__CurVers_) (fitermail - extensive mail filter) COMMENT( man-request, larger title ) manpagename(filtermail)(Filter incoming e-mail to accepted, spam or ignored) COMMENT( all other: add after () ) manpagesynopsis() fm() [OPTIONS] tt(base) nl() [OPTIONS] - cf. section bf(OPTIONS)nl() tt(base): the absolute path to the user's home directory When specified paths with options or in the configuration file specifications may start with tt(~/), in which case tt(~) is replaced by tt(base). manpagedescription() Fm() filters incoming e-mail as either accepted, spam, or ignored e-mail. It uses rule files, which are inspected in sequence until the incoming e-mail matches a rule. Once that happens the rule's associated action (accept, spam, or ignore) is executed. If the e-mail is not matched by any rule then the e-mail is accepted. Alternatively, when using option tt(--inspect) fm() can be used to find the domain name of the sender, its IP address, its country of origin and the cidr-range containing the received IP address (see sections bf(OPTIONS) and bf(FILTERMAIL INSPECT) below). Accepted e-mail normally is appended to the mail file which is used by the incoming mail server when receiving mail for the current user. E.g., if the user's username is tt(USER) then incoming mail is appended to the file tt(/var/mail/USER). Users may also define directories to contain saved e-mails (e.g., tt(~/Mail)), and fm() can be configured to append e-mail considered as spam to, e.g., tt(~/Mail/spam). Likewise, e-mail matching the 'ignore' criteria could be appended to tt(~/Mail/ignore). Instead of appending the complete e-mail to its destination file the received e-mail's tt(From:) and tt(Subject:) headers can be appended to its destination file. This is achieved by prefixing tt(:HDRS:) to the name of the destination file. Alternatively, such e-mail can also be ignored, losing it completely, by not specifying a destination file. The option to merely log the received e-mail's tt(From:) and tt(Subject:) headers may come in handy if the received e-mail is also kept elsewhere (e.g., in another account, which forwards the received e-mail to the computer running fm()) and the ignoring rules might result in occasional false positive decisions. (see also section bf(OPTIONS) below). Fm() uses three types of files: itemization( it() The configuration file contains values of options with are generally used (see sections bf(CONFIGURATION) and bf(OPTIONS)); it() Mail filtering rules are hierarchically ordered in the em(rules) file: incoming mail is sequentially matched against the patterns defined in files specified in the rules file until a match is found. Once a match has been found the rule's action (accept, ignore or spam) is executed, ending the filtering process (see section bf(RULES)); it() Each file specified in the rules file defines matching patterns, which are tested sequentially. Testing those patterns ends once the incoming mail matches a pattern. The result of the matching is forwarded to the rules file which may result in executing the rule's action (see section bf(PATTERNS)). ) If fm() detects a syntax error in the rules file or in a rule specification file the incoming mail is accepted. To avoid this situation the tt(--syntax) option (see section bf(OPTIONS)) should be used when modifying, adding or removing rule files to verify that the specified rules were correctly formulated. To use fm() the incoming mail server must recognize it as a valid mail handling program (see section bf(EXAMPLES)). manpagesection(CONFIGURATION) Options (see section bf(OPTIONS)) not flagged with `NO_CONFIG' can also be specified in a configutation file. By default the configuration file tt(~/.filtermail/config) is used. Command line options always take precedence over specifications in the configuration file. The configuration file em(must) exist, but may be empty. It must exist because its directory defines the directory where the files defining the filtering rules are located. Empty lines and the content of lines starting at the tt(#)-character are ignored. Option tt(--expire) is used to remove patterns whose date stamps (cf. section bf(PATTERNS)) indicate dates before the date specified by tt(--expire). The configuration file may contain tt(dont-expire:) lines. Each tt(dont-expire:) line specifies the name of a pattern file whose entries don't expire. Files specified in tt(dont-expire:) lines may not exist, but to avoid inspecting pattern files for expired dates the name(s) of those file(s) must be identical to the names of the file(s) used in the rules file (cf. section bf(RULES) below). Other files in the configuration file may use the tt([~]/path) format or must be plain filenames (i.e., not starting with tt(/)-characters). Plain files are relative to the directory containing the configuration file (cf. section bf(EXAMPLES)). manpagesection(RULES) All mail filtering rules are defined in the tt(rules) file. Mail filtering starts at the first rule until either the incoming e-mail matches a rule, or until all rules have been processed and the e-mail does not match any rule. In the latter case the e-mail is considered accepted. Empty lines and lines whose first non-blank character is a tt(#)-character are ignored. The rules themselves cannot contain tt(#)-characters. Rules are written according to the following syntax (elements between square brackets are optional, the content of bracketed sections followed by a tt(*) character may be repeated (not using the square brackets). Lowercase words are keywords and cannot be used otherwise. Capitalized words are described below. Each rule is specified on its own line. Line continuation (using, e.g., tt(\) at the end of a line) is not supported). Here's the rule's syntax: verb( if Header File [and Header File]* Action ) tt(Header) is the name of a mail header (e.g., tt(From:, Received:)). Header specifications must be identical to the first words of header lines of the received e-mail. So to match the tt(From) header in the e-mail's first line specify tt(From) (i.e., no colon). Some headers have variants. E.g., tt(Received:) and tt(Received-SPF:). To select all headers sharing their initial characters append a tt(+)-character to the initial part (e.g., to select all headers starting with tt(Received) use tt(Received+), and use tt(From+) to select all tt(From:) headers including the e-mail's first line). tt(File) is the name of the file containing patterns to inspect. Filenames must start with tt(./) and define the locations of files below the configuration file's directory. E.g., tt(./spam/subject) specifies the file tt(subject) in a subdirectory tt(spam) containing patterns considered by the rule. tt(Action) specifies the action to execute when e-mail matches a rule. tt(Action) can be itemization( itt(accept) the e-mail is accepted (i.e., appended to the tt(accept) location spcified in the configuration file) itt(ignore) the e-mail is ignored (i.e., appended to the tt(ignore) location spcified in the configuration file) itt(spam) the e-mail is considered spam (i.e., appended to the tt(spam) location spcified in the configuration file) ) manpagesection(PATTERNS) Files specified in tt(rules) files define patterns which may be found in the headers defined by the rules. The header lines which are selected by the tt(Header) specifications in the tt(rules) file are matched against those patterns after removing the header labels from those lines. So tt(Subject: hello world) is passed to the patterns as the (trimmed) line tt(hello world). Once a header's content matches a pattern inspection ends with a successful match (the rule itself may specify tt(not), in which case a successful match results in a failing match of the rule). Pattern files may start with file-specific comment (i.e., empty lines and lines whose first non-blank character is tt(#)) up to a comment line equal to tt(#=). The patterns themselves may also be preceded by comment lines. Once a pattern is matched it is moved one position upward in the pattern file (including its associated comment). Pattern specifications use the following syntax (elements between square brackets are optional (when used, the square brackets are not specified), capitalized words are described below, each pattern is defined on a single line, line continuation is not supported): verb( Nr Date Expression [and Expression]* ) This pattern indicates a match when all tt(Expressions) match. This syntax uses the following elements: itemization( itt(Nr) specifies the number of times e-mail was successfully matched by the pattern. At each successful match the count is incremented (up to a maximum of 999), and the pattern rises one position in the pattern file. The field width of the number field must be at least 3 character positions wide. When adding a pattern use 1; itt(Date) shows the date of the most recent match. Its format is tt(yy-mm-dd), e.g. 23-03-31. When adding a pattern the current date could be used. itt(Expression) defines at least one matching mode and associated expression. ) The tt(Expressions) themselves use the following syntax: verb(MatchMode [not] Spec) The selected headers are matched against tt(Spec) using the specified tt(MatchMode). The tt(not) keyword is optional. When specified (omit the square brackets) the result of the match is negated. When multiple tt(Expression) specifications are joined by tt(and) keywords, then the final pattern results in a match if all tt(Expression) specifications indicate a match. Once an tt(Expression) does not indicate a match, then subsequent tt(Expressions) are not evaluated and headers do not match the pattern. When using tt(not) the e-mail may not match the tt(Spec) specification. E.g, when e-mail should contain a tt(To:) header or a tt(Cc:) header the following rule can be used (cf. section bf(EXAMPLES):) verb( if To: ./match/noto and Cc: ./match/noto spam ) with tt(match/noto): verb( 1 23-05-10 p not '.' ) Note that tt(Spec) must be surrounded by single quotes. To use a single quote inside a tt(Spec) escape it (as tt(\')). In general: the character following a backslash is used as-is, removing the backslash from the tt(Spec) (e.g., to construct a tt(Spec) containing tt('\n) specify tt(\'\\n)). There are five types of tt(MatchModes. MatchModes) using regular expressions use extended regular expression patterns: prefix multipliers and bounding-characters by backslashes when they should be interpreted as ordinary characters (i.e., tt(*, +, ?, ^, $, |, (, ), [, ], {, }) should be escaped when used as literal characters). itemization( itt(c) (cidr) matches tt([d{1.3}.d{1.3}.d{1.3}.d{1.3}]) IP address patterns found in selected headers against the tt(Spec) pattern, e.g., tt('1.22.333.0/24'); itt(i) (ignore case) matches the selected headers case-insensitively against the tt(Spec) pattern, e.g., tt('yes'); itt(n) (no-case pattern) matches the selected headers case-insensitively against the tt(Spec) regular expression pattern, e.g., tt('news.*\.*@reply'); itt(p) (pattern) matches the selected headers (case-sensitively) against the tt(Spec) regular expression pattern; itt(s) (string text) matches the selected headers as specified (case-sensitively) against the tt(Spec) pattern, e.g., tt('noreply@'). This matchmode can also be used to match IP version 6 patterns if the cidr-range uses multiples of 16-bit values, e.g., tt('[2a02:7a60:'); itt($) (script or program): tt(Spec) calls a script or program. The selected headers are passed to the script or program, which must return 0 to indicate a match and any other value to indicate no match. The script or program may specify arguments, but in addition two additional arguments are passed to the script or program, the first one being the name of a file containing the selected e-mail headers, the second one being the name of the file containing the e-mail's content. The script or program does not use the tt(PATH) environment variable, so the path to the script or program (possibly starting with tt(~/)) must be specified. When tt(Spec) starts with the word tt(SHELL]) then the script or program is called as tt(/bin/sh -c 'script/program [arguments]'). Examples: verb( # call program in the user's bin dir. with 4 arguments '~/bin/program arg1, arg2' # call script in the user's bin dir. with 4 arguments # via /bin/sh 'SHELL ~/bin/script arg1, arg2' ) ) manpageoptions() Short options, when defined, are provided between parentheses immediately following their long option equivalents. Several parameters specify locations of files written or used by fm(). If a location specification starts with tt(~/) then the tilde-character is replaced by the em(base) directory specified as fm()'s argument. Otherwise, if the location does not start with a slash (tt(/)) character then the location is prefixed by the path of the directory containing the configuration file. Some options can also be specified in the configuration file (cf. section bf(CONFIGURATION)). Options that cannot be specified in the configuration file are marked as tt(NO-CONFIG). itemization( it() loption(accept)([:HDRS:]path)nl() the path receiving accept-marked mail. Mail not matching any rules is also accepted. Prepend tt(:HDRS:) to tt(path) to log the mail's tt(From:) and tt(Subject:) headers to tt(path). To completely ignore accept-marked mail omit this option (although ignoring accept-marked mail is probably undesirable). Examples (commonly tt(USER) (used in the example) is replaced by the user's username): verb( --accept /var/mail/USER --accept :HDRS:/var/mail/USER) it() loption(cls)nl()nl() By default fm() clears the terminal's screen (calling tt(tput clear)). If the screen should not be cleared option tt(--cls no) can be specified. A default value can also be specified in the configuration file, using verb( cls: no ) in which case option tt(--cls yes) can be specified to overrule the configuration file's specification. This option is only used when the tt(--inspect) option is specified. it() NO-CONFIG lsoption(config)(c)(path)nl() the path to the configuration file (default: ~/etc/mailfilter/config). (see also section tt(CONFIGURATION) below). it() NO-CONFIG lsoption(expire)(e)(date)nl() no mail is read, but patterns having date stamps older than tt(date) are removed from their files and are stored in files having extension tt(.exp) (cf. section bf(PATTERNS) below). Use format tt(yy-mm-dd) when specifying tt(date). This option and tt(--interactive) (see below) cannot both be specified. Example: verb( --date 23-01-31) it() NO-CONFIG loption(help) (soption(h))nl() a summary of fm()'s usage is written to tt(cout) and fm() ends, returning 0 to the shell. it() loption(ignore)([:HDRS:]path)nl() the path receiving ignore-marked mail. Prepend tt(:HDRS:) to tt(path) to log the mail's tt(From:) and tt(Subject:) headers to tt(path). To completely ignore ignore-marked mail omit this option. Examples: verb( --ignore ~/Mail/ignore --ignore :HDRS: ~/Mail/ignore) it() NO-CONFIG loption(inspect) (soption(I))nl() when specifying this option fm() expects a received e-mail file at its standard input showing the domain name of the sender, its IP address, its country of origin and the cidr-range containing the received IP address. E.g., verb( from renxincj.com (unknown [104.223.188.228]) IP = `104.223.188.228', Country: US, CIDR = 104.223.128.0/17 ) See section bf(FILTERMAIL INSPECT) for additional info. it() NO-CONFIG loption(interactive) (soption(i))nl() the matching result of matching patterns is interactively simulated (no mail is read). This option and tt(--expire) cannot both be specified. it() lsoption(IP4-pattern)(p)(regex)nl() by default IP4 patterns are recognized using the regular expression pattern tt(\[((\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3}))\]), matching addresses like tt([123.11.22.33]) in specified headers. This format is commonly encountered in tt(Received:) headers. When different IP4-patterns should be recognized then this option can be used to specify another regular expression for matching IP4-patterns in headers. The required regex is an extended regular expression in which the characters tt(*, +, ?, ^, $, |, (, ), [, ], {,) and tt(}) must be escaped when used as literal characters. Perl-like escape sequences tt(\b) (word-boundary), tt(\d) (digit character), tt(\s) (white-space character), and tt(\w) (alpha-numeric character) are supported. Their capital variants represent their complementary sets. The regexes specified using this option must have at least five elements, addressable using indices 1..5:nl() * element 1 defines the complete IP4 pattern,nl() * element 2 defines the most significant (4th) octet (available in bits 24 thru 31 in the converted binary address),nl() * element 3 defines the 3th IP4 octet (available in bits 16 thru 23 in the converted binary address),nl() * element 4 defines the 2nd IP4 octet (available in bits 8 thru 15 in the converted binary address),nl() * element 5 defines the least significant (1st) IP4 octet (available in bits 0 thru 7 in the converted binary address). it() lsoption(log)(l)(spec)nl() specify the log-facility. By default no logging is used.nl() The tt(spec) argument can be the location of a file receiving the log messages.nl() If tt(spec) uses the format tt(facility:level) (tt(facility) and tt(level) being the names defined in the bf(syslog)(3) man-page omitting their tt(LOG_) prefixes) then the log messages are written by tt(syslog). Examples: verb( --log log/log # uses log/log in the configuration # file's directory --log USER:NOTICE # uses syslog) it() loption(preamble)nl() merely verify that the basic options are correctly specified. No mail is read, but all options and configuration file specifications are processed. When the verification successfully ends the message verb( Preamble successfully completed) is written to tt(cout) and fm() returns 0. it() lsoption(received)(R)(content)nl() before incoming mail reaches your computer your, e.g., organization's mail server may have processed the incoming mail, and it may be OK to inspect only the tt(Received:) headers following that mail server's tt(Received:) header. The content of the tt(Received:) header may (partially) be specified using this option, which can also be provided in the configuration file. E.g., if the tt(Received:) header originating from your organization's mail server contains tt(ourorganization.org) then the option tt(-r ourorganization.org) causes all tt(Received:) headers to be ignored until the tt(Received:) header containing tt(ourorganization.org) has been seen. If the incoming mail doesn't contain such a tt(Received:) header then all tt(Received:) headers are inspected. This option is only used when the tt(--inspect) option is specified. it() lsoption(rules)(r)(path)nl() the location of the rules specification file (cf. section bf(RULES) below). it() loption(spam) tt([:HDRS:]path)nl() the path receiving spam-marked mail. Prepend tt(:HDRS:) to tt(path) to log the mail's tt(From:) and tt(Subject:) headers to tt(path). To completely ignore spam-marked mail omit this option. Examples: verb( --spam ~/Mail/spam --spam :HDRS: ~/Mail/spam) it() NO-CONFIG loption(syntax)nl() perform a syntax check of the specified rules. Encountered syntax errors are logged in the log-file. The begin and end of the syntax check is also logged in the log file. No mail is read from the standard input stream. it() NO-CONFIG loption(version) (soption(v))nl() writes fm()'s version to tt(cout) and ends, returning 0. ) manpagesection(FILTERMAIL INSPECT) When specifying the tt(--inspect) (tt(-I)) option fm() expects a received e-mail file at its standard input showing the domain name of the sender, its IP address, its country of origin and the cidr-range containing the received IP address. E.g., verb( from renxincj.com (unknown [104.223.188.228]) IP = `104.223.188.228', Country: US, CIDR = 104.223.128.0/17 ) Mail handling programs (e.g., bf(mutt)(1)) allow its users to pipe an e-mail file to a program, so the received e-mail can be inspected from inside the mail handling program. E.g., with tt(mutt) typing tt(|) shows the prompt verb( Pipe to command: ) and assuming that the fm() program is available in the user's tt(PATH) environment variable enter `fm() -I' to pass the received e-mail to fm(): verb( Pipe to command: filtermail -I ) Depending on the content of the tt(Received:) headers fm()'s output shows the domain name of the sender, its IP address, its country of origin and the cidr-range containing the received IP address. E.g., verb( from renxincj.com (unknown [104.223.188.228]) IP = `104.223.188.228', Country: US, CIDR = 104.223.128.0/17 ) IP version 6 addresses are also inspected, producing output like verb( from mail.resoascijournal.info (s857e6ba3.fastvps-server.com \ [2a03:f480:2:8::3f]) IP = `2a03:f480:2:8::3f', Country: EE, CIDR = 2a03:f480:2::/48 ) If the received e-mail is considered conspicuous (e.g., spam or mail to ignore) then the cidr range could be added to a file like tt(suspect.cidr). Once more e-mails from the suspected cidr-range are received, the range could be added to, e.g., tt(~/etc/filtermail/spam/cidr) or to tt(~/etc/filtermail/ignore/cidr), using a pattern line like verb( 1 23-05-10 s '2a03:f480:2:' ) When the option tt(--cls) is specified as tt(yes) (either as command-line option or in the configuration file) then the terminal screen will be cleared before showing tt(--inspect's) output. When the option tt(--received) is specified tt(Received:) headers appearing before the tt(Received:) header containing the content specified at the tt(--received) option are ignored. manpagesection(EXAMPLES) itemization( it() tt(configure) fm() tt(as a valid mail handling program:) ) Commonly incoming mail servers define a directory where valid mail handling programs (or links to those programs) are listed. E.g., bf(sendmail)(8) uses the `sendmail restricted shell' (tt(/etc/mail/smrsh)) directory. If fm() is installed in a standard user-accessible directory (e.g., tt(/usr/bin)) then the tt(smrsh) directory should contain the link verb( filtermail -> /usr/bin/filtermail) Once the fm() program is recognized by the incoming mail server users may filter incoming e-mail through fm() using, e.g., a tt(~/.forward) file. Such tt(.forward) files ignore empty lines and end-of-line comment (starting at tt(#)). Assuming a standard fm()-configuration (cf. section bf(CONFIGURATION)) and assuming that user tt(USER's) home-directory is tt(/home/USER), then tt(/home/USER/.forward) should contain the following line: verb( "|/usr/bin/filtermail /home/USER") Note the double quotes: they are required because fm() is called with an argument. itemization( itt(a configuration file) ) The following configuration file specifies that the tt(rules) and log files are located in the configuration file's directory and defines paths for all three mail categories: verb( rules: rules log: log/log accept: /var/spool/mail/USER spam: ~/Mail/spam ignore: ~/Mail/ignore # as illustration of a 'dont-expire:' specification: #dont-expire: ./ignore/from) itemization( itt(rule specifications) ) Filtering rules are defined in the file specified by the tt(--rules) option or in the tt(rules:) line of the configuration file. Note that the pattern files must start with tt(./). verb( if From: ./ignore/from ignore if Subject: ./spam/nolowercase spam # inspect all Received... headers: if Received+ ./spam/cidr spam # a To: or Cc: header is required: if To: ./match/noto and Cc: ./match/noto ignore ) The final rule uses the pattern in tt(./match/tocc) (shown below) specifying the `any character' (tt('.')) regular expression: if the tt(To:) header is empty (which is also true if there is no tt(To:) header, then the tt(not To:) condition matches. The same holds true for the second condition. So if neither condition matches there is neither a tt(To:) nor a tt(Cc:) header, in which case the e-mail is sent to the tt(spam) destination. Also note that, according to em(De Morgan's) rule, a tt(not X and not Y) rule is identical to an tt(X or Y) rule. itemization( itt(pattern specifications) ) verb( # the ./spam/nolowercase pattern: 1 23-05-10 not p '[a-z]' # e.g., the ./match/noto as used in the above example # requiring either a To: or a Cc: header: 1 23-05-10 p not '.' ) manpagefiles() By default the configuration file is expected in the subdirectory tt(etc/filtermail) of the directory specified as fm()'s argument. manpageseealso() bf(mutt)(1), bf(pattern)(3bobcat), bf(regcomp)(3), bf(sendmail)(8), bf(syslog)(3), bf(tput)(1), bf(whois)(1) COMMENT(optional: manpagediagnostics() ) manpagebugs() None reported. manpageauthor() Frank B. Brokken (f.b.brokken@rug.nl). filtermail-1.06.00/enums/0000755000175000017500000000000014535067734014137 5ustar frankfrankfiltermail-1.06.00/enums/enums.h0000644000175000017500000000124014535067734015434 0ustar frankfrank#ifndef INCLUDED_ENUMS_H_ #define INCLUDED_ENUMS_H_ enum eAction // update options/mailfile.cc and current/log.cc { // when changing Action ACCEPT, IGNORE, SPAM, eActionSize }; enum eTruth { USE, REVERSE }; enum eMatchMode // matching types (see also ../config/spec) { CIDR, // c <- letters used in the rules STRING, // s STRING_NOCASE, // i PATTERN, // p PATTERN_NOCASE, // n SCRIPT, // $ eMatchModeSize // recompile expr/data.cc if a mode is *added* }; #endif filtermail-1.06.00/enums/frame0000644000175000017500000000005614535067734015155 0ustar frankfrank//#define XERR #include "global.ih" void { } filtermail-1.06.00/expire/0000755000175000017500000000000014741231233014266 5ustar frankfrankfiltermail-1.06.00/expire/entry.cc0000644000175000017500000000200414535067734015750 0ustar frankfrank//#define XERR #include "expire.ih" Expire::Next Expire::entry() { string line; d_comment = false; while (true) { getline(d_current, line); if (not d_current) break; line = String::trim(line); if (line.empty()) continue; if (line.front() == '#') // comment line { if (line == "#=") // 1st #= defines an entry { if (not d_first) // not the 1st -> plain commnt continue; d_first = false; return { KEEP, d_current.tellg() }; } d_comment = true; continue; // normal comment: read the next line } d_first = false; // real entry: no more first #= checks d_comment = false; return { expired(line) ? EXPIRE : KEEP, d_current.tellg() }; } d_current.clear(); return { DONE, d_current.seekg(0, ios::end).tellg() }; } filtermail-1.06.00/expire/check.cc0000644000175000017500000000075614535067734015700 0ustar frankfrank//#define XERR #include "expire.ih" bool Expire::check() { if (d_expiryDate.empty()) return false; ifstream specs{ d_options.rulesStream() }; string value; while (getline(specs, value)) // all lines in the specification file { value = String::trim(value); if (value.empty() or value.front() == '#') // comment / empty lines: continue; // skipped processRules(value); } return true; } filtermail-1.06.00/expire/processrules.cc0000644000175000017500000000072614535067734017351 0ustar frankfrank#define XERR #include "expire.ih" void Expire::processRules(string const &ifspec) { istringstream in{ ifspec }; // extract elements fm the specification string entry; while (in >> entry) { if ( entry.front() == '.' // a file name and d_skip.find(entry) == d_skip.end() // not yet seen ) { d_skip.insert(entry); ruleFile(entry); } } } filtermail-1.06.00/expire/expire.h0000644000175000017500000000273114535067734015754 0ustar frankfrank#ifndef INCLUDED_EXPIRE_ #define INCLUDED_EXPIRE_ #include #include #include #include #include class Options; class Expire { enum Type { KEEP, EXPIRE, DONE // at EOF }; struct Next { Type type; // type of the next entry std::streamoff end; // end offset of the next entry }; Options &d_options; // skipped and already processed std::unordered_set d_skip; // rule files std::string d_expiryDate; // cf: ruleFile bool d_first; // first time #= is seen in rule() bool d_comment; // true if the entry contains comment std::ifstream d_current; // current rules file std::string const *d_fnamePtr; // and its name std::unique_ptr d_updated; // updated rules file std::unique_ptr d_expired; // expired entries file public: Expire(); bool check(); // true: -e was specified private: void checkFiles(bool expired); Next entry(); bool expired(std::string const &line); void processRules(std::string const &spec); void processCurrent(); void ruleFile(std::string const &fname); void write(Type type, size_t begin, size_t end); }; #endif filtermail-1.06.00/expire/checkfiles.cc0000644000175000017500000000112414535067734016711 0ustar frankfrank//#define XERR #include "expire.ih" void Expire::checkFiles(bool expired) { if (not expired or d_expired) // no need for extra files or they return; // already exist // tmp. file to receive kept entries d_updated.reset(new TempStream{ *d_fnamePtr }); d_expired.reset( // filename to receive expired entries new ofstream{ Exception::factory( *d_fnamePtr + ".exp", ios::in | ios::ate, ios::out ) } ); } filtermail-1.06.00/expire/expired.cc0000644000175000017500000000075614535067734016263 0ustar frankfrank//#define XERR #include "expire.ih" bool Expire::expired(string const &line) { istringstream in{ line }; string date; in >> date >> date; // get the entry's date bool expired = date < d_expiryDate; // entry expired // if not yet available: // create files to receive checkFiles(expired); // expired/updated entries return expired; } filtermail-1.06.00/expire/expire1.cc0000644000175000017500000000130214535067734016164 0ustar frankfrank#define XERR #include "expire.ih" Expire::Expire() : d_options(Options::instance()) { d_expiryDate = d_options.expire(); if (d_expiryDate.empty()) return; Log &log = Log::instance(); auto [begin, end] = d_options.beginEndRE("^\\s*dont-expire:\\s"); if (begin != end) log << "Not inspecting expiration dates of:" << endl; // add ignored files in the config file for (string const &line: ranger(begin, end)) { istringstream in{ line }; string value; in >> value >> value; // 2nd entry is the filename log << " " << value << endl; d_skip.insert(value); } } filtermail-1.06.00/expire/processcurrent.cc0000644000175000017500000000243414535067734017677 0ustar frankfrank//#define XERR #include "expire.ih" // char const *types[] = { "KEEP", "EXPIRE", "DONE" }; void Expire::processCurrent() { Type type = KEEP; // initial entry type size_t begin = 0; // starts at offset 0 size_t end = 0; cout << *d_fnamePtr << '\n'; while (true) { Next next = entry(); // get the next entry if (next.type == DONE) { if (not d_comment) write(type, begin, next.end); else { if (type == EXPIRE) { write(type, begin, end); // the expired entries begin = end; } write(KEEP, begin, next.end); // and the final comment } break; } if (next.type == type) // process series of same { end = next.end; continue; // entry types as a block } write(type, begin, end); // write the 'type' block type = next.type; // and switch block-type begin = end; end = next.end; } } filtermail-1.06.00/expire/icmconf0000644000175000017500000000007514535067734015647 0ustar frankfrank#define LIBRARY "filter" #include "../icmconf.lib" filtermail-1.06.00/expire/expire.ih0000644000175000017500000000047614535067734016131 0ustar frankfrank#include "expire.h" #include "../xerr/xerr.ih" #include #include #include #include #include #include #include "../log/log.h" #include "../options/options.h" using namespace std; using namespace filesystem; using namespace FBB; filtermail-1.06.00/expire/write.cc0000644000175000017500000000107714535067734015752 0ustar frankfrank//#define XERR #include "expire.ih" void Expire::write(Type type, size_t begin, size_t end) { if (not d_expired) // no updates if no expired entries return; size_t nChars = end - begin; unique_ptr buffer{ new char[nChars] }; d_current.seekg(begin); d_current.read(buffer.get(), nChars); if (type == EXPIRE) { d_expired.get()->write(buffer.get(), nChars); d_expired.get()->flush(); } else { d_updated.get()->write(buffer.get(), nChars); d_updated.get()->flush(); } } filtermail-1.06.00/expire/rulefile.cc0000644000175000017500000000157114535067734016426 0ustar frankfrank//#define XERR #include "expire.ih" // rule file processing: // collect all lines until #= or a rule-line // at #=: continue, write lines-so-far to updated, // handle future #= lines as comment // at a rule line: #include void Expire::ruleFile(string const &fname) { d_first = true; // for 1st #= comment //the current rules-file d_current = Exception::factory(fname); d_fnamePtr = &fname; // and its name processCurrent(); if (d_expired) { d_current.close(); d_updated->close(); //cout << "Enter 2: "; //string line; //getline(cin, line); rename(d_updated->fileName(), fname); } d_updated.reset(0); d_expired.reset(0); } filtermail-1.06.00/expire/frame0000644000175000017500000000006214535067734015317 0ustar frankfrank//#define XERR #include "expire.ih" Expire:: { } filtermail-1.06.00/expr/0000755000175000017500000000000014741231233013750 5ustar frankfrankfiltermail-1.06.00/expr/data.cc0000644000175000017500000000041514535067734015206 0ustar frankfrank//#define XERR #include "expr.ih" bool Expr::s_interactive = false; size_t Expr::s_id = 0; vector Expr::s_matchPtr(eMatchModeSize); unique_ptr Expr::s_oneKey; unique_ptr Expr::s_headers; unique_ptr Expr::s_input; filtermail-1.06.00/expr/regex.cc0000644000175000017500000000117614535067734015414 0ustar frankfrank//#define XERR #include "expr.ih" bool Expr::regex(string const &txt) { if (d_available) // Expr was already loaded return d_available; d_regex = txt; d_regex.pop_back(); // remove the final quote size_t pos = 0; // change all \' into ' while ((pos = d_regex.find("\\'", pos)) != string::npos) d_regex.erase(pos, 1); // rexexes must have content, other // than spaces, tabs and quotes return d_available = d_regex.find_first_not_of("' \t") != string::npos; } filtermail-1.06.00/expr/getmatchobject.cc0000644000175000017500000000243714535067734017266 0ustar frankfrank//#define XERR #include "expr.ih" SpecBase *Expr::getMatchObject() { UptrSpecBase &ptr = s_matchPtr[d_matchMode]; // the used match obj. ptr if (not ptr.get()) // not avail.: get it { switch (d_matchMode) { case eMatchMode::CIDR: ptr = unique_ptr{ new Cidr{ Options::instance().ip4() } }; break; case eMatchMode::STRING: ptr = unique_ptr{ new StringCase }; break; case eMatchMode::STRING_NOCASE: ptr = unique_ptr{ new StringNoCase }; break; case eMatchMode::PATTERN: ptr = unique_ptr{ new RulePattern{ true} }; break; case eMatchMode::PATTERN_NOCASE: ptr = unique_ptr{ new RulePattern{ false } }; break; case eMatchMode::SCRIPT: ptr = unique_ptr