{"id":13782915,"url":"https://github.com/barryp/py-exim-localscan","last_synced_at":"2025-05-11T16:33:35.457Z","repository":{"id":23325754,"uuid":"26685890","full_name":"barryp/py-exim-localscan","owner":"barryp","description":"Embeds a Python interpreter into Exim 4.x. to allow Exim local_scan() functions to be written in Python","archived":false,"fork":false,"pushed_at":"2022-07-11T11:14:25.000Z","size":135,"stargazers_count":1,"open_issues_count":2,"forks_count":3,"subscribers_count":3,"default_branch":"master","last_synced_at":"2024-04-24T16:14:49.498Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/barryp.png","metadata":{"files":{"readme":"README","changelog":"CHANGES","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2014-11-15T16:34:35.000Z","updated_at":"2020-05-29T03:11:09.000Z","dependencies_parsed_at":"2022-08-30T17:40:50.891Z","dependency_job_id":null,"html_url":"https://github.com/barryp/py-exim-localscan","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/barryp%2Fpy-exim-localscan","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/barryp%2Fpy-exim-localscan/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/barryp%2Fpy-exim-localscan/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/barryp%2Fpy-exim-localscan/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/barryp","download_url":"https://codeload.github.com/barryp/py-exim-localscan/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":213838108,"owners_count":15645779,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-03T18:01:47.932Z","updated_at":"2024-08-03T18:11:00.665Z","avatar_url":"https://github.com/barryp.png","language":"C","funding_links":[],"categories":["C"],"sub_categories":[],"readme":"Python Local Scan for Exim 4.x\n===========================\n\n    2003-07-05   Barry Pederson \u003cbp@barryp.org\u003e\n\nThis software embeds a Python interpreter into Exim 4.x, for running a \nPython-based local_scan function against incoming messages.\n\n---------\nCOMPILING\n---------\nFirst, make sure you can build and run a plain Exim installation before \nattempting to add Python support.  Start by reading the toplevel Exim \nREADME file.  \n\nEmbedding Perl into Exim may cause linking conflicts with Python, you've \nbeen warned.\n\nOnce you've successfully built Exim, you may try patching the Exim \nLocal/Makefile by running the patch_exim_makefile.py script included in \nthis distribution.  The script takes one argument, the path to your\ntoplevel Exim build directory (that contains the \"Local\" directory the\nExim install docs told you to create).  The script will patch the Local/Makefile\nand symlink the C sourcefile for the local_scan function (which should live\nin the same directory as the patch script).\n\nRebuild Exim using the patched Makefile, if you don't see any errors then\nyou should be in business.  Install the new binary the same way as you did\nwith plain Exim.\n\n----------------\nCONFIGURING EXIM\n----------------\n\nThere are a few options for this software that you may set in the\nExim 'configure' file, in the 'local_scan' section. \n\n    expy_enabled\n   \n       Type: boolean\n       Default: true\n\n       Gives you a way to cause this software to not try and execute\n       any Python code, if you needed to disable it for some reason.\n       For example:\n\n           expy_enabled = false\n\n    expy_exim_module\n   \n       Type: string\n       Default: exim\n\n       Specifies the name of the Python module that this software creates \n       to hold the builtin Exim functions, constants, and variables \n       described below.\n\n    expy_path_add\n\n       Type: string\n       Default: unset\n\n       Append one directory name to the Python sys.path list,\n       useful for specifying where your local_scan module resides.  \n       For example:\n\n           expy_path_add = /foo/bar/mystuff\n\n       If this variable isn't set, you'll have to place your module\n       somewhere in the default Python path, such as the \n       site-packages directory.  \n\n    expy_scan_module\n   \n       Type: string\n       Default: exim_local_scan\n\n       Name of the Python module you're supplying that contains\n       a local_scan function.\n\n    expy_scan_function\n   \n       Type: string\n       Default: local_scan\n\n       Name of the function within your module that this\n       software will try and execute to perform the local_scan.\n\n    expy_scan_failure\n\n       Type: string\n       Default: defer\n\n       Return code in case the local_scan functions fails. Possible values:\n       \"accept\", \"defer\", \"deny\".\n\nexpy_path_add is probably the only one you'll really need. The others\nare handy if you don't care for their default values.\n\n\n----------\nRUNNING\n----------\n\nFor every message that comes in, under the default Exim local_scan \nconfiguration settings described above, Exim will now call on embedded \nPython to import a module named 'exim_local_scan', and run a function \nin that module named 'local_scan'.  \n\nHere is a sample \"hello world\" local scan module for Python:\n\n------------\nimport exim\n\ndef local_scan():\n    exim.log('Hello from Python')\n    return exim.LOCAL_SCAN_ACCEPT\n\n------------\n\nIf this works, you'll find a \"Hello from Python\" line added to each \nmessage entry in your Exim mainlog.\n\n\n------------------------------------\nWRITING A PYTHON LOCAL_SCAN FUNCTION\n------------------------------------\n\nYour \"local_scan\" function is called without any arguments, and should\nreturn one of the LOCAL_SCAN_* constants (see below).  If you want to specify\nsome return_text, you may do so by returning a tuple containing the \nLOCAL_SCAN_* constant, along with a string.  For example:\n\n    def local_scan():\n        return exim.LOCAL_SCAN_TEMPREJECT, 'Come back later'\n\nSeveral Exim functions, constants, and variables are available through\na module named 'exim' (that name is set by the expy_exim_module setting\ndescribed above), which user-supplied modules will want to import\n(as in the \"hello world\" example above).  (Most of these descriptions\nare basically copied from the Exim Local Scan documentation chapter 38, but\naltered for Python)\n\n    Functions\n    ----------\n\n        add_header(string):\n\n            Adds a header to the message being scanned.  A newline\n            is automatically added if necessary. Example:\n\n                exim.add_header('X-Python: scanned')\n\n            Please note that a header that's been folded into multiple\n            lines of text must be added with a single function call. \n\n            For example, instead of:\n \n                exim.add_header('x-foo: bar')\n                exim.add_header(' baz')\n\n            you'd need to use:\n\n                exim.add_header('x-foo: bar\\n baz')\n\n\n        debug_print(string):\n        \n            If this function is called when Exim is not in debugging mode, it \n            does nothing. In debugging mode, it adds to the debugging output. \n            If the \"pid\" and/or \"timestamp\" debugging selectors are set, the pid \n            and/or timestamp are automatically added to each debug output line.\n                        \n            Newlines are NOT added automatically.\n            \n                exim.debug_print('some debug message\\n')  \n                    \n\n        expand(string):\n\n            Perform an Exim string-expansion.  For example:\n\n                spooldir = exim.expand('$spool_directory')\n\n            If the expansion fails, a ValueError exception is raised and\n            the exception's error message includes the string Exim returns\n            in C through expand_string_message.\n        \n        \n        log(string [, which=LOG_MAIN]):\n\n            Add a string to the specified log (defaults to LOG_MAIN). \n            For example:\n\n                exim.log('Scanned by Python')\n                exim.log('Rejected by Python', exim.LOG_REJECT)\n                exim.log(\"We're freaking out here!', exim.LOG_PANIC)\n\n        child_open(argv, envp, umask[, make_leader=False]):\n\n           Create a child process that runs the command specified.\n           \"argv\" and \"envp\" must be tuples of strings.\n\n           The return value is (stdout, stdin, pid), where stdout and stdin are\n           file descriptors to the appropriate pipes (stderr is joined with\n           stdout). The environment may be specified, and a new umask supplied.\n\n        child_open_exim(message[, sender=\"\", sender_authentication=None])\n\n           Submit a new message to Exim, returning the PID of the subprocess.\n           Essentially, this is running 'exim -t -oem -oi -f sender -oMas auth'\n           (-oMas is omitted if no authentication is provided).\n\n        child_close(pid[, timeout=0])\n\n           Wait for a child process to terminate, or for a timeout (in seconds)\n           to expire. A timeout of zero (the default) means wait as long as it\n           takes. The return value is the process ending status.\n\n    Constants\n    ----------\n    (Python doesn't really have constants, you can assign other values to \n     these names if you really want to confuse yourself)\n\n        LOG_MAIN\n        LOG_PANIC\n        LOG_REJECTLOG\n            Used as the second argument to the log() function to specify\n            which Exim log you want to write to.  Example:\n\n                exim.log('Python was here', exim.LOG_MAIN)\n\n        LOCAL_SCAN_ACCEPT\n        LOCAL_SCAN_ACCEPT_FREEZE\n        LOCAL_SCAN_ACCEPT_QUEUE\n        LOCAL_SCAN_REJECT\n        LOCAL_SCAN_REJECT_NOLOGHDR\n        LOCAL_SCAN_TEMPREJECT\n        LOCAL_SCAN_TEMPREJECT_NOLOGHDR\n            Used one of these for the return value of your local scan function\n         \n        D_v\n        D_local_scan\n            Bitmasks for checking the debug_selector variable (see below)\n\n        MESSAGE_ID_LENGTH\n            The length of message identification strings. This is the id used internally\n            by exim. The external version for use in Received: strings has a leading 'E'\n            added to ensure it starts with a letter. \n\n        SPOOL_DATA_START_OFFSET\n            The offset to the start of the data in the data file - this allows for\n            the name of the data file to be present in the first line. \n\n        \n\n    Variables\n    ----------        \n\n        debug_selector      (an integer)\n        \n            zero when no debugging is taking place. Otherwise, it is a bitmap\n            of debugging selectors. Two bits are identified for use in local_scan()\n            and defined as constants (see above):\n                        \n            The D_v bit is set when -v was present on the command line. This is a\n            testing option that is not privileged - any caller may set it. All the\n            other selector bits can be set only by admin users.\n                                                \n            The D_local_scan bit is provided for use by local_scan(); it is set by\n            the +local_scan debug selector. It is not included in the default set\n            of debugging bits.\n                                                                        \n\n        fd                  (an integer)\n\n            The file descriptor for the file that contains the body of the \n            message (the -D file). The descriptor is positioned at the first \n            character of the body itself, having skipped over the beginning\n            of the file which contains the message_id or the name of the data \n            file in the first line.\n            \n            The file is open for reading and writing, but updating it \n            is not recommended.\n\n            Here is an example snippet of code that copies the body of the \n            message being scanned to a file named 'my_body':\n\n                import os\n                f = open('my_body', 'w')\n                while 1:\n                    s = os.read(exim.fd, 16384)\n                    if not s:\n                        break\n                    f.write(s)   \n        \n        sender_address      (a string)\n\n            The envelope sender address. For bounce messages this is \n            the empty string.\n\n        headers\n\n            A tuple of header_line objects.  Each header_line object has two \n            attributes: '.text', and '.type'.  \n\n            The .text attribute is the entire text of the header line, which \n            may contain internal newlines, and should end in a newline.  It is\n            not changable.\n\n            The .type attribute is the one-character code Exim uses to identify \n            certain header lines (see chapter 48 of Exim manual).  It is changable, \n            but only to single-character values.  Normally you'd set it to '*' to \n            mark a header line as being deleted.\n\n            Here's an example bit of code that deletes headers beginning with 'x-spam':\n\n                for h in exim.headers:\n                    if h.type != '*' and h.text.lower().startswith('x-spam'):\n                        h.type = '*'\n\n            Use the add_header() function (see above) to add new header lines, although\n            you won't see them appear in this tuple.\n\n        host_checking       (an integer)\n        \n            true during a -bh session\n\n        interface_address   (a string)\n\n            The IP address of the interface that received the message, as \n            a string.  This is None for locally submitted messages.\n\n        interface_port      (an integer)\n\n            The port on which this message was received.\n\n        message_id          (a string)\n        \n            Message id of the message we're scanning\n         \n        received_protocol   (a string)\n\n            The name of the protocol by which the message was received.\n\n        recipients\n\n            The list of accepted recipients.  You may modify or replace\n            this list.  To blackhole a message you can use any of these \n            methods:\n\n                exim.recipients = None\n                exim.recipients = []\n                del exim.recipients\n\n            You could append a new address with:\n\n                exim.recipients.append('postmaster@foobar.com')\n\n            Or replace the list alltogether with:\n\n                exim.recipients = ['quarantine@foobar.com', 'postmaster@foobar.com']\n\n        sender_host_address         (a string)\n\n            The IP address of the sending host, as a string. This is None for \n            locally-submitted messages.\n\n        sender_host_authenticated   (a string)\n\n            The name of the authentication mechanism that was used, or None if \n            the message was not received over an authenticated SMTP connection.\n\n        sender_host_name            (a string)\n\n            The name of the sending host, if known.\n\n        sender_host_port            (an integer)\n\n            The port on the sending host.        \n\n\nPlease note that the 'recipients' variable is the only one for which\nmodifications have any effect on Exim.  \n\n------------------------\nMORE ELABORATE EXAMPLES\n------------------------\n\nHere is a fancier local_scan function, that calls a \nseparate 'scanner' module and catches any exceptions \nraised and logs them in the Exim rejectlog\n\n-----------------------------------\nimport sys\nimport exim\n\n# Wrap up the real scanning function in a tight\n# try-except block, dump any raised python\n# exceptions into the paniclog, and temporarily\n# reject the message\n#\ndef local_scan():\n    try:\n        import scanner\n        return scanner.local_scan()\n    except:\n        pass\n\n    #\n    # Get the exception and traceback, format and convert\n    # to individual lines, feed each line into exim log\n    #\n    import traceback\n    einfo = sys.exc_info()\n    lines = traceback.format_exception(einfo[0], einfo[1], einfo[2])\n    lines = ''.join(lines).split('\\n')\n    for line in lines:\n        exim.log(line, exim.LOG_PANIC)\n\n    return exim.LOCAL_SCAN_TEMPREJECT\n------------------------------------\n\n\n\nAnother useful bit of code for writing out a complete copy of a message\nto a given filename (perhaps to feed into antivirus or spam-recognition \nsoftware).\n\n-----------\ndef copy_message(msg_filename):\n    f = open(msg_filename, 'w')\n\n    for h in exim.headers:\n        if h.type != '*':\n            f.write(h.text)\n\n    f.write('\\n')\n\n    while 1:\n        s = os.read(exim.fd, 16384)\n        if not s:\n            break\n        f.write(s)\n\n    f.close()\n--------------------\n\n### EOF ###\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbarryp%2Fpy-exim-localscan","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbarryp%2Fpy-exim-localscan","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbarryp%2Fpy-exim-localscan/lists"}