// SNFMilter.cpp // Copyright (C) 2008 ARM Research Labs, LLC. // See www.armresearch.com for the copyright terms. // // This file contains the "guts" of the SNFMilter interface. Specifically, // the SNFMilter() function. #include #include #include #include #include #include "SNFMulti.hpp" #include "configuration.hpp" #include "SNFMilter.hpp" #include "config.h" #include #include #if SMFI_VERSION > 3 #define NEW_LIBMILTER #endif SNFMilterContextPool* MilterContexts = 0; // The global contexts handle. bool MilterDebugMode; // True if debug mode is on. sfsistat SkipReturn = SMFIS_CONTINUE; // libmilter return value when further // callbacks of the same type are to be skipped. /// Get the connection context object. // // \param[in] Ctx is the libmilter context object. // // \returns the pointer to the connection context object. // // \throws runtime_error if the obtained pointer is 0. // SNFMilterContext* getContextFromCtx(SMFICTX* Ctx) { SNFMilterContext* Context = (SNFMilterContext*) smfi_getpriv(Ctx); // Get the context object. if(0 == Context) throw runtime_error("Got NULL from smfi_getpriv()"); return Context; } /// Get a new connection object and assign it to the context. // // \param[in] Ctx is the libmilter context object. // // \returns the pointer to the connection context object. // // \throws runtime_error if the obtained pointer is 0. // SNFMilterContext* assignContextToCtx(SMFICTX* Ctx) { SNFMilterContext* Context = MilterContexts->grab(); // Get any existing context object. if(0 == Context) // Check address. throw runtime_error("Got NULL from MilterContexts->grab()"); smfi_setpriv(Ctx, Context); // Save the context object. Context->ConnectionData.clear(); // Clear the connection context object. return Context; } // Function to output an info message. void logInfo(const string ContextName, int Code, std::string Message) { cout << ContextName << " (code " << Code << "): " << Message << endl; #if 0 syslog(LOG_MAIL | LOG_DEBUG, "%s", Message.c_str()); // Output to system mail log. #endif MilterContexts->logThisInfo(ContextName, Code, Message); // Log message. } // Function to output an error message. void logError(const string &ContextName, const int &Code, const std::string &Message) { cout << ContextName << " (code " << Code << "): " << Message << endl; #if 0 syslog(LOG_MAIL | LOG_DEBUG, "%s", Message.c_str()); // Output to system mail log. #endif MilterContexts->logThisError(ContextName, Code, Message); // Log message. } class ResultActionHandler : public Configurator { private: SNFMilterEngine* Target; public: ResultActionHandler(SNFMilterEngine* T) : Target(T) {} void operator()(ConfigurationElement& E, ConfigurationData& D) { Target->setResultAction( Code, (Action >= Allow && Action <= Quarantine) ? static_cast(Action) : Allow ); } int Code; int Action; }; const int NoSuchCode = -1; // Magic number for no such code const int NoSuchAction = -1; // Magic number for no such action void SNFMilterEngine::readConfiguration() { // Parse the configuration. string NewConfiguration = myRulebase->PlatformConfiguration(); // Get the latest configuration. if(0 != RunningConfiguration.compare(NewConfiguration)) { // If it does not match, read it! RunningConfiguration = NewConfiguration; // Capture the latest. for(int i = 1; i < ResultCodesCount; i++) ResultActions[i] = NoAction; // Init Result/Action config. ResultActions[0] = Allow; NonZeroAction = NoAction; // NoAction if no configuration for // non-zero result. ResultActionHandler ResultActionConfigurator(this); // Create a Result/Action handler. ConfigurationElement Reader("milter"); // Create a configuration reader. Reader .Element("connect") .Element("white") .Attribute("action", reinterpret_cast(WhiteAction), static_cast(Allow)) .Mnemonic("Allow", AllowActionMnemonic) .Mnemonic("Accept", AcceptActionMnemonic) .Mnemonic("Retry", RetryActionMnemonic) .Mnemonic("Reject", RejectActionMnemonic) .Mnemonic("Discard", DiscardActionMnemonic) .Mnemonic("Quarantine", QuarantineActionMnemonic) .End("white") .Element("caution") .Attribute("action", reinterpret_cast(CautionAction), static_cast(Allow)) .Mnemonic("Allow", AllowActionMnemonic) .Mnemonic("Accept", AcceptActionMnemonic) .Mnemonic("Retry", RetryActionMnemonic) .Mnemonic("Reject", RejectActionMnemonic) .Mnemonic("Discard", DiscardActionMnemonic) .Mnemonic("Quarantine", QuarantineActionMnemonic) .End("caution") .Element("black") .Attribute("action", reinterpret_cast(BlackAction), static_cast(Allow)) .Mnemonic("Allow", AllowActionMnemonic) .Mnemonic("Accept", AcceptActionMnemonic) .Mnemonic("Retry", RetryActionMnemonic) .Mnemonic("Reject", RejectActionMnemonic) .Mnemonic("Discard", DiscardActionMnemonic) .Mnemonic("Quarantine", QuarantineActionMnemonic) .End("black") .Element("truncate") .Attribute("action", reinterpret_cast(TruncateAction), static_cast(Allow)) .Mnemonic("Allow", AllowActionMnemonic) .Mnemonic("Accept", AcceptActionMnemonic) .Mnemonic("Retry", RetryActionMnemonic) .Mnemonic("Reject", RejectActionMnemonic) .Mnemonic("Discard", DiscardActionMnemonic) .Mnemonic("Quarantine", QuarantineActionMnemonic) .End("truncate") .End("connect") .Element("scan") .Element("result") .atEndCall(ResultActionConfigurator) .Attribute("code", ResultActionConfigurator.Code, NoSuchCode) .Attribute("action", ResultActionConfigurator.Action, NoSuchAction) .Mnemonic("Allow", AllowActionMnemonic) .Mnemonic("Accept", AcceptActionMnemonic) .Mnemonic("Retry", RetryActionMnemonic) .Mnemonic("Reject", RejectActionMnemonic) .Mnemonic("Discard", DiscardActionMnemonic) .Mnemonic("Quarantine", QuarantineActionMnemonic) .End("result") .Element("nonzero") .Attribute("action", reinterpret_cast(NonZeroAction), static_cast(NoAction)) .Mnemonic("Allow", AllowActionMnemonic) .Mnemonic("Accept", AcceptActionMnemonic) .Mnemonic("Retry", RetryActionMnemonic) .Mnemonic("Reject", RejectActionMnemonic) .Mnemonic("Discard", DiscardActionMnemonic) .Mnemonic("Quarantine", QuarantineActionMnemonic) .End("nonzero") .End("scan") .End("milter"); ConfigurationData ConfigurationData( // Convert our configuration string NewConfiguration.c_str(), // to a configuration data buffer. NewConfiguration.length()); Reader.initialize(); // Initialize the defaults. Reader.interpret(ConfigurationData); // Read the new configuration. } } void SNFMilterEngine::setResultAction(int Result, SNFMilterAction Action) { // Set a result / action pair. if( 0 <= Result && ResultCodesCount > Result // If the Result code is in ) { ResultActions[Result] = Action; } // range then set the action. } void SNFMilterEngine::checkConfiguration() { // Reload the config if it is old. if(ConfigurationCheckTime.isExpired()) readConfiguration(); } SNFMilterEngine::SNFMilterEngine(snf_RulebaseHandler* R) : // Construct the engine. myRulebase(R), // Remember our rulebase. myEngine(0), // We need to set this later. WhiteAction(Allow), // Initialize our default actions. CautionAction(Allow), BlackAction(Allow), TruncateAction(Allow), ConfigurationCheckTime(ConfigurationLifetime) { myEngine = new snf_EngineHandler(); // Create an engine handler. myEngine->open(myRulebase); // Connect it to the rulebase. readConfiguration(); // Read our configuration. } SNFMilterEngine::~SNFMilterEngine() { // Destroy the engine. try { ScopeMutex EngineLock(ConfigMutex); // Don't die while scanning. if(myEngine) { // If we're not dead then die. myEngine->close(); // Close the engine. delete myEngine; // Delete it. myEngine = 0; // Forget it. myRulebase = 0; // Forget (don't delete) this too. } } catch(...) {} // Silently capture exceptions. } SNFMilterAction SNFMilterEngine::scanIP(unsigned long int IP) { // Scans an IP. IPTestRecord Tester(IP); // Make up a test record for this IP. ScopeMutex ConfigurationLock(ConfigMutex); // Lock our configuration. if(0 == myEngine) throw runtime_error("Null engine when scanning IP"); // Skip safely if we're down. checkConfiguration(); // Re-read our config if it is old. myRulebase->performIPTest(Tester); // Tun it past the engine. SNFMilterAction TestResult = Allow; // Allow by default. switch(Tester.R) { // Convert the result to an action. case White: { TestResult = WhiteAction; break; } // If the IP scan range is recognized case Caution: { TestResult = CautionAction; break; } // in our configuration then we will case Black: { TestResult = BlackAction; break; } // return the action code that is case Truncate: { TestResult = TruncateAction; break; } // configured. Otherwise we will return default: break; } // the default "Allow" action. return TestResult; // Tell them what we've got. } SNFMilterAction SNFMilterEngine::scanMessage( // Scans a message. const unsigned char* bfr, // Requires a pointer to the buffer. int length) { // Requires the buffer length. ScopeMutex ConfigurationLock(ConfigMutex); // Lock the configuration. if(0 == myEngine) throw runtime_error("Null engine when scanning message"); // Skip safely if we're down. checkConfiguration(); // Re-read our config if it is old. int R = myEngine->scanMessage(bfr, length, "", 0); // Scan the message & get the result. if(0 > R || ResultCodesCount <= R) return Error; // If R is out of range, return Error. if (0 == R || NoAction != ResultActions[R]) return ResultActions[R]; // Return the translated action. return NonZeroAction; } string SNFMilterEngine::XHeaders() { // Return X headers from last scan. ScopeMutex EngineLock(ConfigMutex); // Use myEngine safely. if(0 == myEngine) return ""; // Return no headers if dead. return myEngine->getXHDRs(); // Simply return them. } //// SNFMilterContext SNFMilterContext::SNFMilterContext(snf_RulebaseHandler *myRulebase) : milterEngine(myRulebase), MessageData(MailBufferReserveSize) {} SNFMilterContext::~SNFMilterContext() {} /// Return the local received header. // // \returns the local received header. // string SNFMilterContext::getLocalReceivedHeader() { string locHeader; locHeader = "Received: from <" + ConnectionData.HostName; locHeader += "> [" + (string) ConnectionData.HostIP; locHeader += "] by " PACKAGE_NAME; return locHeader; } /// Map return value from SNFMilterEngine scan to libmilter return value. // // \param[in] MilterAction is the return from an SNFMilterEngine scan. // // \returns the return value for the libmilter callback. // // \throws std::out_of_range if MilterAction of out of range. // sfsistat SNFMilterContext::smfisReturn(SNFMilterAction MilterAction) { static const sfsistat ReturnValue[] = { SMFIS_CONTINUE, SMFIS_ACCEPT, SMFIS_TEMPFAIL, SMFIS_REJECT, SMFIS_DISCARD, SMFIS_CONTINUE }; if ( (MilterAction < 0) || (MilterAction >= NMilterActions) ) { ostringstream Temp; Temp << "Illegal value of SNFMilterAction in SNFMilterContext::smfisReturn (" << MilterAction << ") while processing message from " << MessageData.SenderAddress << " (connection from " << ConnectionData.HostName << " (" << (string) ConnectionData.HostIP << ")."; throw std::out_of_range(Temp.str()); } return ReturnValue[MilterAction]; } //// SNFMilterContextPool SNFMilterContextPool::SNFMilterContextPool(snf_RulebaseHandler* Rulebase) : // Ctor needs a live rulebase handler. myRulebase(Rulebase), // Capture the rulebase handler. MilterSocketPort(0) { string NewConfiguration = myRulebase->PlatformConfiguration(); // Get the latest configuration. ConfigurationElement Reader("milter"); // Create a configuration reader. Reader .Element("socket") .Attribute("type", reinterpret_cast(MilterSocketType), static_cast(NOMilterSocket)) .Mnemonic("unix", UNIXMilterSocketMnemonic) .Mnemonic("tcp", TCPMilterSocketMnemonic) .Attribute("path", MilterSocketPath, "") .Attribute("group", MilterSocketGroup, "") .Attribute("ip", MilterSocketIP, "") .Attribute("port", MilterSocketPort, 0) .End("socket") .End("milter"); ConfigurationData ConfigurationData( // Convert our configuration string NewConfiguration.c_str(), // to a configuration data buffer. NewConfiguration.length()); Reader.initialize(); // Initialize the defaults. Reader.interpret(ConfigurationData); // Read the new configuration. switch (MilterSocketType) { // Named pipe. case UNIXMilterSocket: if (0 == MilterSocketPath.size()) { // Path specified? ostringstream Temp; // No. Temp << "Path needs to be specified for socket type \"unix\""; throw runtime_error(Temp.str()); } if ( (0 != MilterSocketIP.size()) || // These should not be specified. (0 != MilterSocketPort) ) { ostringstream Temp; Temp << "IP (" << MilterSocketIP << ") and/or port (" << MilterSocketPort << ") were specified for socket type \"unix\". They should not be specified."; throw runtime_error(Temp.str()); } break; case TCPMilterSocket: if (0 == MilterSocketIP.size()) { // IP/host specified? ostringstream Temp; // No. Temp << "Host or IP address needs to be specified for socket type \"inet\""; throw runtime_error(Temp.str()); } if (0 == MilterSocketPort) { // Port specified? ostringstream Temp; // No. Temp << "Port needs to be specified for socket type \"inet\""; throw runtime_error(Temp.str()); } if ( (0 != MilterSocketPath.size()) || // These should not be specified. (0 != MilterSocketGroup.size()) ) { ostringstream Temp; Temp << "Path (" << MilterSocketPath << ") and/or group (" << MilterSocketGroup << ") were specified for socket type \"inet\". They should not be specified."; throw runtime_error(Temp.str()); } break; case NOMilterSocket: { ostringstream Temp; Temp << "The required element was not present in the configuration file."; throw runtime_error(Temp.str()); } break; default: { ostringstream Temp; Temp << "The type of the element configuration file is invalid. " "The type must by \"unix\" or \"inet\""; throw runtime_error(Temp.str()); } } } SNFMilterSocketType SNFMilterContextPool::getSocketType() { return MilterSocketType; } string SNFMilterContextPool::getSocketPath() { return MilterSocketPath; } string SNFMilterContextPool::getSocketGroup() { return MilterSocketGroup; } string SNFMilterContextPool::getSocketIP() { return MilterSocketIP; } int SNFMilterContextPool::getSocketPort() { return MilterSocketPort; } SNFMilterContextPool::~SNFMilterContextPool() { // Dtor gracefully discards contexts. ScopeMutex ContextPoolLock(ContextAllocationControl); // Lock the context allocation system. myRulebase = 0; // Forget our rulebase. We're dead. for( // Loop through the context pool vector::iterator iC = ContextPool.begin(); // and delete any contexts we have iC != ContextPool.end(); // allocated. iC++) { delete (*iC); } } SNFMilterContext* SNFMilterContextPool::grab() { // Get a context to use. ScopeMutex ContextPoolLock(ContextAllocationControl); // Lock the context allocation system. if(0 == myRulebase) return 0; // No contexts left if we're dead. if(1 > AvailableContexts.size()) { // If we need more contexts then SNFMilterContext* N = new SNFMilterContext(myRulebase); // Create a new context, ContextPool.push_back(N); // add it to the pool, AvailableContexts.give(N); // and make it available. } return AvailableContexts.take(); // Return the next avialable context. } void SNFMilterContextPool::drop(SNFMilterContext* E) { // Drop a context after use. // Update context state. E->State = SNFMilterContext::Pooled; AvailableContexts.give(E); // Make this context available. } bool SNFMilterContextPool::allUnused() { return (AvailableContexts.size() == ContextPool.size()); } void SNFMilterContextPool::logThisError(string ContextName, int Code, string Text) { myRulebase->logThisError(ContextName, Code, Text); } void SNFMilterContextPool::logThisInfo(string ContextName, int Code, string Text) { myRulebase->logThisInfo(ContextName, Code, Text); } // End of configuration setup and engine and context interface components //////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// // SNFMilter callback definitions, constants, and global data. extern "C" { // Connection callback. // // This callback is invoked when a new connection is made to the // MTA. It obtains an available connection context object if one // hasn't already been assigned, and saves the IP address and name // of the connecting MTA. Next, it performs an IP scan, and // returns the appropriate response. // // Returns: SMFIS_CONTINUE, SMFIS_ACCEPT, SMFIS_TEMPFAIL, or // SMFIS_REJECT according to the following mapping: // // IPScanResult return mlfi_connect return // // Quarantine, Discard, Error FailSafeMilterResponse // // Anything else SNFMilterContext::smfisReturn(IPScanResult) // sfsistat mlfi_connect(SMFICTX *Ctx, char *HostName, _SOCK_ADDR *HostAddr) { const string ContextName = PACKAGE_NAME "::mlfi_connect"; int ErrorCode = 1; int InfoCode = 1; sfsistat CallbackResult = FailSafeMilterResponse; try { SNFMilterContext *Context = assignContextToCtx(Ctx); // Get the existing context object, // or assign a new context object. Context->State = SNFMilterContext::Connect; // Update context state. sockaddr_in *SaIn = (sockaddr_in *) HostAddr; // Fetch the IP address. Context->ConnectionData.HostName = HostName; // Load the info. Context->ConnectionData.HostIP = ntohl(SaIn->sin_addr.s_addr); if (MilterDebugMode) { ostringstream Temp; Temp << "Connect from " << Context->ConnectionData.HostName << " (" << (string) Context->ConnectionData.HostIP << ")."; logInfo(ContextName, InfoCode, Temp.str()); } SNFMilterAction IpScanResult; // Perform IP scan. IpScanResult = Context->milterEngine.scanIP(Context->ConnectionData.HostIP); if (MilterDebugMode) { ostringstream Temp; Temp << "IP scan result for connection from " << Context->ConnectionData.HostName << " (" << (string) Context->ConnectionData.HostIP << "): " << IpScanResult << "."; logInfo(ContextName, InfoCode, Temp.str()); } if ( (Error == IpScanResult) || // Check for error (Quarantine == IpScanResult) || (Discard == IpScanResult) ) { std::ostringstream Temp; // Illegal result. Temp << "Illegal result from IP scan for " << (string) Context->ConnectionData.HostIP << ": " << IpScanResult; throw std::runtime_error(Temp.str()); } CallbackResult = Context->smfisReturn(IpScanResult); // Load return value. } catch (exception &E) { logError(ContextName, ErrorCode, E.what()); } catch (...) { logError(ContextName, ErrorCode, UnknownExceptionMessage); } return CallbackResult; } // // HELO callback. // // This callback is invoked when the connecting MTA sends a HELO // message. It saves the argument of the HELO command in the // connection context object. // // Returns: SMFIS_CONTINUE if no error, FailSafeMilterResponse // otherwise. // sfsistat mlfi_helo(SMFICTX *Ctx, char *heloHost) { const string ContextName = PACKAGE_NAME "::mlfi_helo"; int ErrorCode = 1; int InfoCode = 1; sfsistat CallbackResult = FailSafeMilterResponse; try { SNFMilterContext *Context = getContextFromCtx(Ctx); // Get the context object. Context->State = SNFMilterContext::Helo; // Update context state. Context->ConnectionData.HostHelo = heloHost; // Save the helo host name. if (MilterDebugMode) { ostringstream Temp; Temp << "HELO " << Context->ConnectionData.HostHelo << " from " << Context->ConnectionData.HostName << " (" << (string) Context->ConnectionData.HostIP << ")."; logInfo(ContextName, InfoCode, Temp.str()); } CallbackResult = SMFIS_CONTINUE; // Load return value. } catch (exception &E) { logError(ContextName, ErrorCode, E.what()); } catch (...) { logError(ContextName, ErrorCode, UnknownExceptionMessage); } return CallbackResult; } // // envfrom callback. // // This callback is invoked to process the envelope from line of the // mail message. It clears the message buffer, and adds the local // received header to the message buffer to scan. The local received // header is added to the email message in mlfi_eom. // // Returns: SMFIS_CONTINUE if no error, FailSafeMilterResponse // otherwise. // sfsistat mlfi_envfrom(SMFICTX *Ctx, char **argv) { const string ContextName = PACKAGE_NAME "::mlfi_envfrom"; int ErrorCode = 1; int InfoCode = 1; sfsistat CallbackResult = FailSafeMilterResponse; try { SNFMilterContext *Context = getContextFromCtx(Ctx); // Get the context object. Context->State = SNFMilterContext::EnvFrom; // Update context state. Context->MessageData.clear(); // This is the beginning of a new message. // Clear data from any previous message. if (MilterDebugMode) { Context->MessageData.SenderAddress = argv[0]; ostringstream Temp; Temp << "Processing sender address " << Context->MessageData.SenderAddress << " (connection from " << Context->ConnectionData.HostName << " (" << (string) Context->ConnectionData.HostIP << ")).\n"; logInfo(ContextName, InfoCode, Temp.str()); } CallbackResult = SMFIS_CONTINUE; // Load return value. } catch (exception &E) { logError(ContextName, ErrorCode, E.what()); } catch (...) { logError(ContextName, ErrorCode, UnknownExceptionMessage); } return CallbackResult; } // // envrcpt callback. // // This callback is invoked to process the envelope receipt line of // the mail message. // // Returns: SMFIS_CONTINUE if no error, FailSafeMilterResponse // otherwise. // sfsistat mlfi_envrcpt(SMFICTX *Ctx, char **argv) { const string ContextName = PACKAGE_NAME "::mlfi_envrcpt"; int ErrorCode = 1; int InfoCode = 1; sfsistat CallbackResult = FailSafeMilterResponse; try { SNFMilterContext *Context = getContextFromCtx(Ctx); // Get the context object. // Update context state. Context->State = SNFMilterContext::EnvRcpt; if (MilterDebugMode) { ostringstream Temp; Temp << "Processing recipient " << argv[0] << " for message from " << Context->MessageData.SenderAddress << " (connection from " << Context->ConnectionData.HostName << " (" << (string) Context->ConnectionData.HostIP << "))."; logInfo(ContextName, InfoCode, Temp.str()); } CallbackResult = SMFIS_CONTINUE; // Load return value. } catch (exception &E) { logError(ContextName, ErrorCode, E.what()); } catch (...) { logError(ContextName, ErrorCode, UnknownExceptionMessage); } return CallbackResult; } // // header callback. // // This callback is invoked to process the a header line of the mail // message. It writes the header line + SMTPENDL to the message buffer // that is scanned. // // Returns: SMFIS_CONTINUE if no error, FailSafeMilterResponse // otherwise. // sfsistat mlfi_header(SMFICTX *Ctx, char *headerf, char *headerv) { const string ContextName = PACKAGE_NAME "::mlfi_header"; int ErrorCode = 1; int InfoCode = 1; sfsistat CallbackResult = FailSafeMilterResponse; try { SNFMilterContext *Context = getContextFromCtx(Ctx); // Get the context object. Context->State = SNFMilterContext::Header; // Update context state. if (MilterDebugMode) { ostringstream Temp; Temp << "Processing header '" << headerf << ": " << headerv << "' for message from " << Context->MessageData.SenderAddress << " (connection from " << Context->ConnectionData.HostName << " (" << (string) Context->ConnectionData.HostIP << "))."; logInfo(ContextName, InfoCode, Temp.str()); } Context->MessageData.MessageBuffer += headerf; // Add the header. Context->MessageData.MessageBuffer += ": "; Context->MessageData.MessageBuffer += headerv; Context->MessageData.MessageBuffer += SMTPENDL; CallbackResult = SMFIS_CONTINUE; // Load return value. } catch (exception &E) { logError(ContextName, ErrorCode, E.what()); } catch (...) { logError(ContextName, ErrorCode, UnknownExceptionMessage); } return CallbackResult; } // // End of header callback. // // This callback is invoked after the last header of the mail // message is sent. It writes SMTPENDL SMTPENDL to the message // buffer that is scanned. // // Returns: SMFIS_CONTINUE if no error, FailSafeMilterResponse // otherwise. // sfsistat mlfi_eoh(SMFICTX *Ctx) { const string ContextName = PACKAGE_NAME "::mlfi_eoh"; int ErrorCode = 1; int InfoCode = 1; sfsistat CallbackResult = FailSafeMilterResponse; try { SNFMilterContext *Context = getContextFromCtx(Ctx); // Get the context object. Context->State = SNFMilterContext::EOH; // Update context state. Context->MessageData.MessageBuffer += SMTPENDL + SMTPENDL; // Add the blank lines.. if (MilterDebugMode) { ostringstream Temp; Temp << "All headers received for message from " << Context->MessageData.SenderAddress << " (connection from " << Context->ConnectionData.HostName << " (" << (string) Context->ConnectionData.HostIP << ")). Message buffer--\n'" << Context->MessageData.MessageBuffer << "'."; logInfo(ContextName, InfoCode, Temp.str()); } CallbackResult = SMFIS_CONTINUE; // Load return value. } catch (exception &E) { logError(ContextName, ErrorCode, E.what()); } catch (...) { logError(ContextName, ErrorCode, UnknownExceptionMessage); } return CallbackResult; } // // message body callback. // // This callback is invoked zero or more times to send the body of the // mail message is sent. It writes the body to the message buffer that // is scanned. // // Returns: SMFIS_CONTINUE if more of the message body is needed, // SkipReturn if more of the message body is not needed, or // FailSafeMilterResponse if an error occurs. Context is the // connection context object. // sfsistat mlfi_body(SMFICTX *Ctx, unsigned char *Bodyp, size_t Bodylen) { const string ContextName = PACKAGE_NAME "::mlfi_body"; int ErrorCode = 1; int InfoCode = 1; sfsistat CallbackResult = FailSafeMilterResponse; try { SNFMilterContext *Context = getContextFromCtx(Ctx); // Get the context object. Context->State = SNFMilterContext::Body; // Update context state. if (Context->MessageData.MessageBuffer.size() >= // Return if there is enough in the MailBufferReserveSize) { // message buffer. return SkipReturn; } if (Context->MessageData.MessageBuffer.size() < // Do we need to copy this? MailBufferReserveSize) { string::size_type NCharToTransfer = MailBufferReserveSize - // Yes. How much more? Context->MessageData.MessageBuffer.size(); if (NCharToTransfer > Bodylen) { // Don't transfer more characters // than are available. NCharToTransfer = Bodylen; } Context->MessageData.MessageBuffer.append((const char *) Bodyp, // Append the message. NCharToTransfer); if (MilterDebugMode) { ostringstream Temp; Temp << "Appended " << NCharToTransfer << " bytes to " << "message from " << Context->MessageData.SenderAddress << " (connection from " << Context->ConnectionData.HostName << " (" << (string) Context->ConnectionData.HostIP << ")). Message length: " << Context->MessageData.MessageBuffer.size() << "."; logInfo(ContextName, InfoCode, Temp.str()); } } else { if (MilterDebugMode) { ostringstream Temp; Temp << "Discarded " << Bodylen << " bytes " << "message from " << Context->MessageData.SenderAddress << " (connection from " << Context->ConnectionData.HostName << " (" << (string) Context->ConnectionData.HostIP << ")) because " << MailBufferReserveSize << " bytes have already been " << "transferred. Message length: " << Context->MessageData.MessageBuffer.size() << "."; logInfo(ContextName, InfoCode, Temp.str()); } } CallbackResult = SMFIS_CONTINUE; // Load return value. } catch (exception &E) { logError(ContextName, ErrorCode, E.what()); } catch (...) { logError(ContextName, ErrorCode, UnknownExceptionMessage); } return CallbackResult; } // // End-Of-Message callback. // // This callback is invoked to when the entire message has been sent // to SNFMilter. It adds the local received header to the email // message, and then scans the message body. If the scan result // indicates that the message is to be quarantined, then the message // is set to be quarantined (using smfi_quarantine). // // Returns: SMFIS_CONTINUE, SMFIS_ACCEPT, SMFIS_TEMPFAIL, or // SMFIS_DISCARD, SMFIS_REJECT, or FailSafeMilterResponse // according to the following mapping: // // MessageScan return mlfi_connect return // // Error FailSafeMilterResponse // // Anything else SNFMilterContext::smfisReturn(IPScanResult) // // Side effect: If the MessageScan result is Quarantine, the // message is quarantined using smfi_quarantine(). // sfsistat mlfi_eom(SMFICTX *Ctx) { const string ContextName = PACKAGE_NAME "::mlfi_eom"; int ErrorCode = 1; int InfoCode = 1; sfsistat CallbackResult = FailSafeMilterResponse; try { SNFMilterContext *Context = getContextFromCtx(Ctx); // Get the context object. Context->State = SNFMilterContext::EOM; // Update context state. if (MilterDebugMode) { ostringstream Temp; Temp << "End of message from " << Context->MessageData.SenderAddress << " (connection from " << Context->ConnectionData.HostName << " (" << (string) Context->ConnectionData.HostIP << "))."; logInfo(ContextName, InfoCode, Temp.str()); } if (PrependLocalReceivedHeader) { Context->MessageData.MessageBuffer. insert(0, Context->getLocalReceivedHeader() + SMTPENDL); // Prepend local received header line // to the message buffer. } SNFMilterAction MsgScanResult; // Perform scan. MsgScanResult = Context->milterEngine.scanMessage((unsigned char *) Context->MessageData.MessageBuffer.c_str(), Context->MessageData.MessageBuffer.size()); if (MilterDebugMode) { ostringstream Temp; Temp << "Message scan result for message from " << Context->MessageData.SenderAddress << " (connection from " << Context->ConnectionData.HostName << " (" << (string) Context->ConnectionData.HostIP << ")): " << MsgScanResult << "."; logInfo(ContextName, InfoCode, Temp.str()); } if (Error == MsgScanResult) { // Check for scan error // Illegal result. std::ostringstream Temp; Temp << "Illegal message scan result for message from " << Context->MessageData.SenderAddress << " (connection from " << Context->ConnectionData.HostName << " (" << (string) Context->ConnectionData.HostIP << ")): " << MsgScanResult; throw std::runtime_error(Temp.str()); } string XHeaders = Context->milterEngine.XHeaders(); // Fetch X-headers to submit. MailHeaders MailHeadersParse; // Object to parse X-headers. MailHeadersParse.loadHeaders(XHeaders); // Load the headers to be parsed. string HeaderName; // Name of X-header. string HeaderBody; // Body of X-header, formatted for libmilter. while (MailHeadersParse.getNameBody(&HeaderName, &HeaderBody)) { // While there is an X-Header... if (MilterDebugMode) { ostringstream Temp; Temp << "Processed X-Header for message from " << Context->MessageData.SenderAddress << " (connection from " << Context->ConnectionData.HostName << " (" << (string) Context->ConnectionData.HostIP << ")). X-Header name: '" << HeaderName << "'. X-Header body: '" << HeaderBody << "'."; logInfo(ContextName, InfoCode, Temp.str()); } if (MI_SUCCESS != smfi_addheader(Ctx, // Add header to the email message. (char *) HeaderName.c_str(), (char *) HeaderBody.c_str())) { ostringstream Temp; Temp << "Error adding X-header to message from " << Context->MessageData.SenderAddress << " (connection from " << Context->ConnectionData.HostName << " (" << (string) Context->ConnectionData.HostIP << ")).\nX-Header name: '" << HeaderName << "'. X-Header body--\n'" << HeaderBody << "'."; logError(ContextName, 1, Temp.str()); } } if (Quarantine == MsgScanResult) { // Quarantine the message? if (MilterDebugMode) { ostringstream Temp; Temp << "Quarantining message from " << Context->MessageData.SenderAddress << " (connection from " << Context->ConnectionData.HostName << " (" << (string) Context->ConnectionData.HostIP << "))."; logInfo(ContextName, InfoCode, Temp.str()); } if (MI_SUCCESS != smfi_quarantine(Ctx, (char *) "Quarantined by " PACKAGE_NAME)) { ostringstream Temp; Temp << "Error quarantining message from " << Context->MessageData.SenderAddress << " (connection from " << Context->ConnectionData.HostName << " (" << (string) Context->ConnectionData.HostIP << "))."; logError(ContextName, 1, Temp.str()); } return SMFIS_CONTINUE; } CallbackResult = Context->smfisReturn(MsgScanResult); // Load return value. } catch (exception &E) { logError(ContextName, ErrorCode, E.what()); } catch (...) { logError(ContextName, ErrorCode, UnknownExceptionMessage); } return CallbackResult; } // // Callback for aborting the message. // // This callback is invoked to when the processing of the current // message is to be aborted. It logs the abort as an info event, // clears the email message buffer, and sets the state to // connected. // // Returns: SMFIS_CONTINUE if no error, FailSafeMilterResponse // otherwise. // sfsistat mlfi_abort(SMFICTX *Ctx) { const string ContextName = PACKAGE_NAME "::mlfi_abort"; int ErrorCode = 1; int InfoCode = 1; sfsistat CallbackResult = FailSafeMilterResponse; try { SNFMilterContext *Context = getContextFromCtx(Ctx); // Get the context object. Context->State = SNFMilterContext::Connect; // Update context state. ostringstream Temp; // Log message. Temp << "Aborted processing of message from " << Context->MessageData.SenderAddress << " (connection from " << Context->ConnectionData.HostName << " (" << (string) Context->ConnectionData.HostIP << "))."; logInfo(ContextName, InfoCode, Temp.str()); Context->MessageData.clear(); // Clear data for this message. CallbackResult = SMFIS_CONTINUE; // Load return value. } catch (exception &E) { logError(ContextName, ErrorCode, E.what()); } catch (...) { logError(ContextName, ErrorCode, UnknownExceptionMessage); } return CallbackResult; } // // Callback for closing the connection. // // This callback is invoked to when the connection with the remote MTA is closed. // It returns the connection context object to the pool. // // Returns: SMFIS_CONTINUE if no error, FailSafeMilterResponse // otherwise. // sfsistat mlfi_close(SMFICTX *Ctx) { const string ContextName = PACKAGE_NAME "::mlfi_close"; int ErrorCode = 1; int InfoCode = 1; sfsistat CallbackResult = FailSafeMilterResponse; try { SNFMilterContext *Context = getContextFromCtx(Ctx); // Get the context object. Context->State = SNFMilterContext::Close; // Update context state. if (MilterDebugMode) { ostringstream Temp; Temp << "Closing connection from " << Context->ConnectionData.HostName << " (" << (string) Context->ConnectionData.HostIP << ")."; logInfo(ContextName, InfoCode, Temp.str()); } MilterContexts->drop(Context); // Return the context object. smfi_setpriv(Ctx, 0); CallbackResult = SMFIS_CONTINUE; // Load return value. } catch (exception &E) { logError(ContextName, ErrorCode, E.what()); } catch (...) { logError(ContextName, ErrorCode, UnknownExceptionMessage); } return CallbackResult; } #ifdef NEW_LIBMILTER // // Callback for unknown SMTP command. // // This callback is invoked to when an unknown SMTP command is // received by the local MTA. The unknown command is logged. // // Returns: SMFIS_CONTINUE if no error, FailSafeMilterResponse // otherwise. // sfsistat mlfi_unknown(SMFICTX *Ctx, const char *Cmd) { const string ContextName = PACKAGE_NAME "::mlfi_unknown"; int ErrorCode = 1; int InfoCode = 1; sfsistat CallbackResult = FailSafeMilterResponse; try { SNFMilterContext *Context = getContextFromCtx(Ctx); // Get the context object. ostringstream Temp; // Log. Temp << "Unknown SMTP command from " << Context->ConnectionData.HostName << " (" << (string) Context->ConnectionData.HostIP << "): '" << *Cmd << "'"; logInfo(ContextName, InfoCode, Temp.str()); CallbackResult = SMFIS_CONTINUE; // Load return value. } catch (exception &E) { logError(ContextName, ErrorCode, E.what()); } catch (...) { logError(ContextName, ErrorCode, UnknownExceptionMessage); } return CallbackResult; } // // data callback. // // This callback is invoked when the connecting MTA sends a DATA // message. // // Returns: SMFIS_CONTINUE if no error, FailSafeMilterResponse // otherwise. // sfsistat mlfi_data(SMFICTX *Ctx) { const string ContextName = PACKAGE_NAME "::mlfi_data"; int ErrorCode = 1; int InfoCode = 1; sfsistat CallbackResult = FailSafeMilterResponse; try { SNFMilterContext *Context = getContextFromCtx(Ctx); // Get the context object. Context->State = SNFMilterContext::Data; // Update context state. if (MilterDebugMode) { ostringstream Temp; Temp << "DATA for message from " << Context->MessageData.SenderAddress << " (connection from " << Context->ConnectionData.HostName << " (" << (string) Context->ConnectionData.HostIP << "))."; logInfo(ContextName, InfoCode, Temp.str()); } CallbackResult = SMFIS_CONTINUE; // Load return value. } catch (exception &E) { logError(ContextName, ErrorCode, E.what()); } catch (...) { logError(ContextName, ErrorCode, UnknownExceptionMessage); } return CallbackResult; } // // Callback for negotiating the capabilities of the MTA. // // This callback is invoked at the start of each SMTP connection. // It obtains an available connection context object if one hasn't // already been assigned, checks whether the MTA can accept an // SMFIS_SKIP return, and configures the connection context object // to return an acceptable value. // // Returns: SMFIS_CONTINUE if no error, FailSafeMilterResponse // otherwise. // sfsistat mlfi_negotiate(SMFICTX *Ctx, unsigned long F0, unsigned long F1, unsigned long F2, unsigned long F3, unsigned long *PF0, unsigned long *PF1, unsigned long *PF2, unsigned long *PF3) { const string ContextName = PACKAGE_NAME "::mlfi_negotiate"; int ErrorCode = 1; int InfoCode = 1; sfsistat CallbackResult = SMFIS_ALL_OPTS; try { bool AcceptsSkip = F1 & SMFIP_SKIP; if (AcceptsSkip) { // MTA accepts SMFIS_SKIP return? SkipReturn = SMFIS_SKIP; // Yes. Use SMFIS_SKIP. } else { SkipReturn = SMFIS_CONTINUE; // No. Use SMFIS_CONTINUE. } if (MilterDebugMode) { ostringstream Temp; // Log message. Temp << "MTA does " << (AcceptsSkip ? "" : "not ") << "accept SMFIS_SKIP."; logInfo(ContextName, InfoCode, Temp.str()); } CallbackResult = SMFIS_ALL_OPTS; // Load return value. } catch (exception &E) { logError(ContextName, ErrorCode, E.what()); } catch (...) { logError(ContextName, ErrorCode, UnknownExceptionMessage); } return CallbackResult; } #endif } // End of SNFMilter callback definitions, constants, and global data. //////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// // SNFMilter setup & run function. // The runLibMilter function establishes the appropriate libmilter call backs and // accepts calls from the MTA via libmilter until it is told to quit. When it // is told to quit it gracefully closes down, reclaims any memory it allocated, // and returns. void runLibMilter(SNFMilterContextPool* Contexts, bool DebugMode) { // Run the milter 'til it's done. MilterContexts = Contexts; // Save the pool of context objects. MilterDebugMode = DebugMode; // Save the debug mode flag. #if 0 if (MilterDebugMode) { openlog(PACKAGE_NAME, LOG_PID | LOG_PERROR, LOG_MAIL); // Initialize system logging to log // messages to mail file. } #endif struct smfiDesc smfilter = { // Load structure containing callbacks. (char *) PACKAGE_NAME, // Filter name. SMFI_VERSION, // Version code -- do not change. SMFIF_ADDHDRS | SMFIF_QUARANTINE, // Flags. mlfi_connect, // Connection info callback. mlfi_helo, // SMTP HELO command callback. mlfi_envfrom, // Envelope sender callback. mlfi_envrcpt, // Envelope recipient callback. mlfi_header, // Header callback. mlfi_eoh, // End of headers callback. mlfi_body, // Body block callback. mlfi_eom, // End of message callback. mlfi_abort, // Message abort callback. mlfi_close // Connection closed callback. #ifdef NEW_LIBMILTER , mlfi_unknown, // Unknown SMTP command callback. mlfi_data, // DATA ccallback. mlfi_negotiate // Negotiation at the start of each SMTP #endif // connection callback. }; string MilterConnSpec; switch (Contexts->getSocketType()) { // Configure the connection. case UNIXMilterSocket: MilterConnSpec = "unix:" + Contexts->getSocketPath(); // Generate the connection spec. break; case TCPMilterSocket: { ostringstream Temp; Temp << "inet:" << Contexts->getSocketPort() << "@" // Generate the connection spec. << Contexts->getSocketIP(); MilterConnSpec = Temp.str(); } break; default: { ostringstream Temp; Temp << PACKAGE " internal error: Invalid socket type from SNFMilterContextPool."; throw logic_error(Temp.str()); } } if (MI_FAILURE == smfi_setconn(const_cast(MilterConnSpec.c_str()))) { ostringstream Temp; Temp << "smfi_setconn failed with input \"" << MilterConnSpec << "\""; throw std::runtime_error(Temp.str()); } if (MI_FAILURE == smfi_register(smfilter)) { // Register the callbacks. string msg = "smfi_register failed"; throw std::runtime_error(msg); } if (UNIXMilterSocket == Contexts->getSocketType()) { if (MI_FAILURE == smfi_opensocket(true)) { // Create the named pipe. ostringstream Temp; Temp << "smfi_opensocket failed for \"" << MilterConnSpec << "\""; throw std::runtime_error(Temp.str()); } string MilterConnPath = Contexts->getSocketPath(); if (chmod(MilterConnPath.c_str(), // Set permissions. S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP) != 0) { ostringstream Temp; Temp << "Error setting permissions of " << MilterConnPath << ": " << strerror(errno); throw runtime_error(Temp.str()); } string GroupName = Contexts->getSocketGroup(); if (0 != GroupName.size()) { // Was a group specified? struct group *Group = getgrnam(GroupName.c_str()); // Get the ID of the group. errno = 0; if (NULL == Group) { ostringstream Temp; if (0 != errno) { // Error? Temp << "Error obtaining the group ID of " << GroupName << ": " << strerror(errno); } else { // Group not found. Temp << "Error obtaining the group ID of " << GroupName << ": " << "No such group"; } throw runtime_error(Temp.str()); } if (chown(MilterConnPath.c_str(), // Set group. -1, Group->gr_gid) != 0) { ostringstream Temp; Temp << "Error setting group of " << MilterConnPath << " to " << Group->gr_name << ": " << strerror(errno); throw runtime_error(Temp.str()); } } } if (MI_FAILURE == smfi_main()) { // Hand control to libmilter string msg = "smfi_main failed"; throw std::runtime_error(msg); } const string ContextName = "--EXITING--"; int ErrorCode = 1; int InfoCode = 1; logInfo(ContextName, InfoCode, "Shutdown command received. Waiting for message processing to complete..."); Sleeper WaitATic; try { WaitATic.setMillisecondsToSleep(ShutdownWaitPeriod_ms); // Learn to wait. } catch(...) { ostringstream Temp; Temp << "Invalid value for ShutdownWaitPeriod_ms: " << ShutdownWaitPeriod_ms << "."; throw out_of_range(Temp.str()); } int iPeriod = 0; // Number of periods waited. while (!MilterContexts->allUnused() && iPeriod < ShutdownPeriods) { iPeriod++; WaitATic(); // Wait a period. } if (!MilterContexts->allUnused()) { logError(ContextName, ErrorCode, "Not all messages finished processing."); } logInfo(ContextName, InfoCode, "Exiting"); WaitATic(); // Wait for messages to be logged. MilterContexts = 0; // Turn off. }