{"id":21506614,"url":"https://github.com/afresh1/opensmtpd-filter","last_synced_at":"2025-10-24T15:31:35.421Z","repository":{"id":56835027,"uuid":"337605389","full_name":"afresh1/OpenSMTPd-Filter","owner":"afresh1","description":"This is a barely functional perl module to make writing OpenSMTPd filters easier.","archived":false,"fork":false,"pushed_at":"2023-07-06T01:30:55.000Z","size":107,"stargazers_count":6,"open_issues_count":0,"forks_count":0,"subscribers_count":3,"default_branch":"blead","last_synced_at":"2024-11-13T14:14:39.291Z","etag":null,"topics":["email","filter","mail","openbsd","opensmtpd","opensmtpd-filter"],"latest_commit_sha":null,"homepage":"","language":"Perl","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/afresh1.png","metadata":{"files":{"readme":"README.md","changelog":null,"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":"2021-02-10T03:26:06.000Z","updated_at":"2023-11-17T05:33:08.000Z","dependencies_parsed_at":"2022-09-02T03:40:42.186Z","dependency_job_id":null,"html_url":"https://github.com/afresh1/OpenSMTPd-Filter","commit_stats":null,"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/afresh1%2FOpenSMTPd-Filter","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/afresh1%2FOpenSMTPd-Filter/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/afresh1%2FOpenSMTPd-Filter/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/afresh1%2FOpenSMTPd-Filter/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/afresh1","download_url":"https://codeload.github.com/afresh1/OpenSMTPd-Filter/tar.gz/refs/heads/blead","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":226090712,"owners_count":17572117,"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":["email","filter","mail","openbsd","opensmtpd","opensmtpd-filter"],"created_at":"2024-11-23T19:43:11.511Z","updated_at":"2025-10-24T15:31:35.350Z","avatar_url":"https://github.com/afresh1.png","language":"Perl","funding_links":[],"categories":[],"sub_categories":[],"readme":"# NAME\n\nOpenSMTPd::Filter - Easier filters for OpenSMTPd in perl\n\n# VERSION\n\nversion v0.0.3\n\n# SYNOPSIS\n\n    use OpenSMTPD::Filter;\n    use OpenBSD::Pledge;\n\n    pledge();\n\n    my $filter = OpenSMTPd::Filter-\u003enew(\n        on =\u003e {\n            report =\u003e \\%report_callbacks,\n            filter =\u003e \\%filter_callbacks,\n        },\n    );\n\n    $filter-\u003eready;  # Registers and starts listening for updates\n\n# DESCRIPTION\n\nThis module is a helper to make writing [OpenSMTPd](https://opensmtpd.org)\nfilters in perl easier.\n\nIt should support smtpd API protocol version 0.7 and earlier.\n\n# METHODS\n\n## new\n\n    my $filter = OpenSMTPd::Filter-\u003enew(\n        input  =\u003e \\*STDIN,  # the default\n        output =\u003e \\*STDOUT, # the default\n        debug  =\u003e 0,        # the default,\n\n        on =\u003e \\%callbacks,\n    );\n\nInstantiates a new filter ready to start handling events.\n\n- on\n\n        my $filter = OpenSMTPd::Filter-\u003enew(\n            ...,\n            on =\u003e {\n                report =\u003e { 'smtp-in' =\u003e {\n                    'link-connect' =\u003e \\\u0026lookup_spf_async,\n                } },\n                filter =\u003e { 'smtp-in' =\u003e {\n                    helo =\u003e \\\u0026check_spf,\n                    ehlo =\u003e \\\u0026check_spf,\n                } },\n            },\n        );\n\n    A hashref of events to add callbacks for.\n    The top level is the `stream` to listen on,\n    either `report` or `filter`.\n    The next level is the `subsystem` which must be `smtp-in`.\n    Finally the `event` or `phase` to to handle.\n\n    See [\"REPORT AND FILTER STREAMS\"](#report-and-filter-streams) for details on writing callbacks.\n\n- input\n\n    The filehandle used to receive messages from smtpd.\n    Will be changed to `binmode(\":raw\")`.\n\n    Defaults to `STDIN`.\n\n- output\n\n    The filehandle used to send messages to smtpd.\n    Will be changed to `binmode(\":raw\")`.\n\n    Defaults to `STDOUT`.\n\n- debug\n\n    Set to a true value to enable debugging.\n    Primarily this means copying all lines\n    from [\"input\"](#input) and [\"output\"](#output) to `STDERR`.\n\n## ready\n\n    $filter-\u003eready;\n\nProcesses events on [\"input\"](#input) until it hits `eof`,\nwhich should only happen when smtpd exits.\n\n# REPORT AND FILTER STREAMS\n\n    my $callback = sub {\n        my ( $phase_or_event, $session, @extra ) = @_;\n        ...;\n    };\n\nEach stream triggers events and each event callback adds to a session\nstate as well as a list of events that have been received in that\nsession.\n\nEach callback is called with the report event or filter phase\nthat triggered the callback as the first argument and a session datastructure\nas the second argument.\nThe subsystem can be found in `$session-\u003e{state}-\u003e{subsystem}`.\nSome callbacks will get additional arguments as documented below.\n\nThe `$session` hashref may contain up to three keys:\n\n    $session = {\n        state    =\u003e \\%state,\n        events   =\u003e \\@events,\n        messages =\u003e \\@messages,\n    };\n\nNothing in this session hash is used by this module other than in\neach message, the `data-line` arrayref is passed to the [\"data-lines\"](#data-lines)\nfilter and the `sent-dot` boolean is used to know whether to\ncontinue sending [filter-dataline](https://metacpan.org/pod/filter-dataline) responses.\nThis means your filters can munge the contents or add additional\nentries although care must be taken not to change the expected type\nof an entry, but adding additional keys or changing values will not\ncause issues.\n\n- state\n\n    This is the state filled in by the fields for each received event.\n    Any `tx` events go into [\"messages\"](#messages) instead of this state.\n\n    A state might loook like this, however it only contains the fields\n    recieved at each point in the connection and will contain any fields\n    set by a [\"REPORT EVENT\"](#report-event):\n\n        my $state = {\n            version   =\u003e '0.6',\n            timestamp =\u003e '1613356167.075372',\n            subsystem =\u003e 'smtp-in',\n            event     =\u003e 'timeout',\n            phase     =\u003e 'commit',\n            session   =\u003e '3647ceea74a815de',\n\n            rdns      =\u003e 'localhost',\n            fcrdns    =\u003e 'pass',\n            src       =\u003e '[::1]:37403',\n            dest      =\u003e '[::1]:25',\n\n            hostname  =\u003e 'mail.example.test',\n\n            method    =\u003e 'HELO',\n            identity  =\u003e 'mail.afresh1.test',\n\n            command   =\u003e '.',\n            response  =\u003e '250 2.0.0 5e170a6f Message accepted for delivery',\n\n            message   =\u003e $session-\u003e{messages}-\u003e[-1],\n        };\n\n    See the rest of this section for which events fill in each field.\n\n- events\n\n    This is an arrayref of hashrefs of the fields for each recieved\n    message, each hashref contains all fields supplied by that report\n    event or filter phase.\n    In addition, the event includes a `request` field indicating\n    whether the event was a report or a filter.\n\n        my $event = {\n            request   =\u003e 'report',\n            version   =\u003e '0.5',\n            timestamp =\u003e '1576146008.006099',\n            subsystem =\u003e 'smtp-in',\n            event     =\u003e 'link-connect',\n            session   =\u003e '7641df9771b4ed00',\n            rdns      =\u003e 'mail.openbsd.org',\n            fcrdns    =\u003e 'pass',\n            src       =\u003e '199.185.178.25:33174',\n            dest      =\u003e '45.77.67.80:25',\n        };\n\n- messages\n\n    Message states collect the fields provided by each `tx-*`\n    [\"REPORT EVENT\"](#report-event) for each `message-id` in a session.\n\n        my $message' = {\n            'message-id'  =\u003e '48f59d87',\n            'envelope-id' =\u003e '48f59d87264c2287',\n            'mail-from',  =\u003e 'andrew',\n            'rcpt-to',    =\u003e ['afresh1'],\n            'data-line'   =\u003e [\n                'Received: from mail (localhost [::1])',\n                '   by mail.example.test (OpenSMTPD) with SMTP id 48f59d87',\n                '   for \u003cafresh1@mail.afresh1.test\u003e;',\n                '   Sat, 27 Feb 2021 20:56:38 -0800 (PST)',\n                'From: andrew',\n                'To: afresh1',\n                'Subject: Hai!',\n                '',\n                'Hello There',\n                '.'\n            ],\n            'result'   =\u003e 'ok',\n            'sent-dot' =\u003e 1,\n        };\n\n    The [\"tx-from\"](#tx-from) and [\"tx-rcpt\"](#tx-rcpt) events are handled specially and\n    go into the `mail-from`, `rcpt-to`, and `result` fields.\n    The `rcpt-to` ends up in an arrayref as the message can be destined\n    for multiple recipients.\n    If a [\"data-lines\"](#data-lines) filter exists,\n    the `data-line` field is also an arrayref of each line that has been\n    recieved so far, with the `CR` and `LF` removed.\n    The `sent-dot` field is a boolen indicating whether this message\n    has sent the `.` indicating it is complete.\n\n## REPORT EVENT\n\n    my $callback = sub {\n        my ( $event, $session ) = @_;\n        ...;\n        return 'anything'; # ignored\n    };\n\nAll report events will provide these fields:\n\n- version\n- timestamp\n- subsystem\n- event\n- session\n- suffix\n\nEvents for the subsystem below may include additional fields.\n\n- smtp-in\n    - link-connect\n        - rdns\n        - fcrdns\n        - src\n        - dest\n    - link-greeting\n        - hostname\n    - link-identify\n        - method\n        - identity\n    - link-tls\n        - tls-string\n    - link-disconnect\n    - link-auth\n        - result\n        - username\n    - protocol-client\n        - command\n    - protocol-server\n        - response\n    - filter-report\n        - filter-kind\n        - name\n        - message\n    - filter-response\n        - phase\n        - response\n        - param\n    - timeout\n\n## MESSAGE REPORT EVENTS\n\n    my $callback = sub {\n        my ($event, $session) = @_\n        my $message = $session-\u003e{state}-\u003e{message};\n        ...;\n    };\n\nAll filters that begin with `tx-` include a `message-id` field\nand possibly other fields.\nThese events add to the last item in [\"messages\"](#messages),\nwhich is also added as the `message` field in the `session` [\"state\"](#state).\n\n- message-id\n\nMessage events for the `smtp-in` subsystem may include additional fields.\n\n- tx-reset\n- tx-begin\n- tx-mail\n    - result\n    - mail-from\n\n        The `address` field for a `tx-mail` event is recorded as the\n        `mail-from` in the message.\n- tx-rcpt\n    - result\n    - rcpt-to\n\n        The `address` field for a `tx-rcpt` events are recorded in the\n        `rcpt-to` arrayref in the message.\n- tx-envelope\n    - envelope-id\n- tx-data\n    - result\n- tx-commit\n    - message-size\n- tx-rollback\n\n## FILTER REQUEST\n\n    my $callback = sub {\n        my ( $phase, $session, @data_lines ) = @_;\n        ...;\n        return $response, @params;\n    };\n\nSee [\"FILTER RESPONSE\"](#filter-response) for details about what can be returned.\n\nThe [\"data-line\"](#data-line) and [\"data-lines\"](#data-lines) callbacks are special in that\nthey also recieve the current `data-line` or all lines recieved.\nThey should also return a list of [\"dataline\"](#dataline) responses instead of the\nnormal decision response.\n\nAll filter events have these fields:\n\n- version\n- timestamp\n- subsystem\n- phase\n- session\n- opaque-token\n\nSpecific filter events for each subsystem may include additional\nfields.\n\n- smtp-in\n    - connect\n        - rdns\n        - fcrdns\n        - src\n        - dest\n    - helo\n        - identity\n    - ehlo\n        - identity\n    - starttls\n        - tls-string\n    - auth\n        - auth\n    - mail-from\n        - address\n    - rcpt-to\n        - address\n    - data\n    - commit\n- data-line\n\n    The `data-line` and `data-lines` callbacks are special in that\n    they return a list of [\"dataline\"](#dataline) responses and not a normal\n    [\"FILTER RESPONSE\"](#filter-response).\n\n    The returned lines are split on `\\n` so you can return a single\n    string that is the entire message and it will be split into individual\n    [\"dataline\"](#dataline) responses.\n\n    You can return any number of lines from an individual `data-line`\n    callback until you recieve the single `.` indicating the end of\n    the message.\n    When you recieve the single `.` as the `line` you will need to\n    finish processing the message and return any lines that are still\n    pending.\n\n    - line\n\n- data-lines\n\n    This is a wrapper around the [\"data-line\"](#data-line) callback\n    to make it easier to process the entire message instead\n    of dealing with it on a line-by-line basis and having to\n    store it yourself.\n\n    See the [\"BUGS AND LIMITATIONS\"](#bugs-and-limitations),\n    although this seemed like a good idea,\n    to better support `pledge` it might go away\n    and leave implementing data-line storage to the filter author.\n\n    - lines\n\n        The final argument is an arrayref of all lines in the message.\n\n### FILTER RESPONSE\n\nThe return value from a [\"FILTER REQUEST\"](#filter-request) callback determines what\nwill be done with the message.\n\n- dataline\n\n    This is the special response used by [\"data-line\"](#data-line) filters.\n    There is special processing that if the returned line contains\n    newlines it will be split into multiple responses.\n\n    - line\n\n- proceed\n\n        my $callback = sub {\n            ...;\n            return 'proceed';\n        };\n\n    This is the normal response, it means the message will continue to\n    additional filters and if all filters return `proceed` the message\n    will be accepted.\n\n- junk\n\n        my $callback = sub {\n            ...;\n            return 'junk';\n        };\n\n    Like [\"proceed\"](#proceed) but will add an `X-Spam` header to the message.\n\n- reject\n\n        my $callback = sub {\n            ...;\n            return reject =\u003e \"400 Not Sure\";\n        };\n\n    You must provide a valid SMTP error message as the second argument\n    to the return value including the status code, 5xx or 4xx.\n\n    A 421 status will [\"disconnect\"](#disconnect) the client.\n\n    - error\n\n- disconnect\n\n        my $callback = sub {\n            ...;\n            return disconnect =\u003e \"550 Go Away\";\n        };\n\n    As with [\"reject\"](#reject) the return from this callback must include a\n    valid SMTP error message including the status code.\n    However, like a  `421` [\"reject\"](#reject) status, all messages will\n    disconnect the client.\n\n    - error\n\n- rewrite\n\n        my $callback = sub {\n            my ($phase, $session) = @_;\n            ...;\n            if ( $phase eq 'tx-rcpt' ) {\n                 my $event = $session-\u003e{events}-\u003e[-1];\n                 return rewrite =\u003e 'afresh1'\n                     if $event-\u003e{address} eq 'andrew';\n            }\n            return 'proceed';\n        };\n\n    - parameter\n\n- report\n\n    Generates a [\"filter-report\"](#filter-report) event with the `parameter` as the\n    message that will be reported.\n    I'm not entirely sure where they get reported to,\n    I assume maybe any later filters.\n\n    I believe you would do something like this,\n    and that you could generate any supported event,\n    but I haven't had good luck with it.\n\n        my $s = $_[1]-\u003e{state};\n        printf $output \"%s|\"%010.06f\"|%s|%s|%s|%s\\n\";\n            'report', Time::HiRes::time,\n            $s-\u003e{subsystem}, 'filter-response', $s-\u003e{session},\n            $parameter\n        );\n\n    This is not a result response.\n\n    - parameter\n\n# BUGS AND LIMITATIONS\n\nThe received [\"data-line\"](#data-line) are stored in a list in memory\nif a [\"data-lines\"](#data-lines) filter exists,\nwhich could easily be very large if the message is sizable.\nThese should instead be stored in a temporary file.\n\nThere is currently no way to stop listening for specific report events,\nthis module should provide a way to specify which events it should\nlisten for and gather state from.\n\n# DEPENDENCIES\n\nPerl 5.16 or higher.\n\n# SEE ALSO\n\n[smtpd-filters(7)](https://github.com/openbsd/src/blob/master/usr.sbin/smtpd/smtpd-filters.7)\n\n[OpenBSD::Pledge](http://man.openbsd.org/OpenBSD::Pledge)\n\n[OpenBSD::Unveil](http://man.openbsd.org/OpenBSD::Unveil)\n\n# AUTHOR\n\nAndrew Hewus Fresh \u003candrew@afresh1.com\u003e\n\n# COPYRIGHT AND LICENSE\n\nThis software is Copyright (c) 2021 by Andrew Hewus Fresh \u003candrew@afresh1.com\u003e.\n\nThis is free software, licensed under:\n\n    The MIT (X11) License\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fafresh1%2Fopensmtpd-filter","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fafresh1%2Fopensmtpd-filter","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fafresh1%2Fopensmtpd-filter/lists"}