miltertest(8)

πŸŽ€ NAME πŸŽ€

miltertest β€” the milter unit test utility, straight out of the System Manager's Manual. bestie it's literally the whole point of this page. πŸ’…


✨ SYNOPSIS ✨

miltertest [-D name[=value]] [-s script] [-u] [-v] [-V] [-w]

πŸ¦‹ DESCRIPTION πŸ¦‹

okay so picture this: miltertest plays pretend and simulates the MTA side of an MTA-milter interaction, all so you can test a milter-aware filter application. no cap, that's the job. 🫢

It takes as input a script written in the Lua language, and by exporting some utility functions, it makes it possible for you to write scripts that exercise a filter. it's giving "you bring the script, it brings the whole fake mail server."

See the documentation on Lua (e.g. http://www.lua.org) for the syntax of the language in general. The docs down below are all about the functions that get added to Lua by this application to make testing possible. ✨

Documentation on milter lives at http://www.milter.org. 🌈

Now, tea time: a particular transaction has to follow a series of steps to be completed, namely negotiate, connection information, envelope sender, envelope recipient(s), header field(s), end-of-header, body chunk(s), end-of-message. The way that is a lot of steps 😭. So to make writing tests with miltertest simpler, any of these steps prior to end-of-message that gets skipped will be filled in using arbitrary, but legal, data. iconic behavior honestly.

Interspersed with these protocol phases are optional macro (key/value) deliveries from the MTA. > miltertest will never send these automatically. If they're needed for your tests, you have to send them yourself as part of your test script. not me forgetting that and wondering why nothing works πŸ™ˆ


πŸ’– OPTIONS πŸ’–

  • -D name[=value] β€” Defines a global variable called name to the Lua interpreter. If a value is provided, the global variable is set to that value (as a string, although Lua can convert strings to numbers internally). If no value is provided, the global variable is set to 1. πŸ“
  • -s script β€” Use the contents of file script as the Lua script to be executed. The default is to read from standard input. 🌸
  • -u β€” After the filter being tested is terminated, report user and system time consumed. See getrusage(2). ⏱️
  • -v β€” Increase verbose output. May be specified multiple times to request more and more information. πŸ‘€
  • -V β€” Print version number and exit. πŸ‘‘
  • -w β€” Don't wait for child status to be returned when testing is complete. πŸ’…

🧚 FUNCTIONS 🧚

The following functions are made available to Lua scripts for exercising a filter. All functions return the Lua constant "nil" on success or an error string on failure, unless otherwise indicated. fr fr that's the contract, remember it. πŸ’œ

  • mt.abort(conn) β€” Aborts the transaction in progress on the specified connection. πŸ’€
  • mt.bodyfile(conn, file) β€” Sends the contents of the named file to the connection as body data. If there is any error opening file for reading, the test aborts. 😭
  • mt.bodyrandom(conn, n) β€” Sends at least n bytes of random-length lines of random printable ASCII data as body chunks to the specified connection. 🎲
  • mt.bodystring(conn, str) β€” Sends str as a chunk of body text on the specified connection. πŸ¦‹
  • mt.chdir(directory) β€” Changes the current working directory to the named directory. πŸ“
  • mt.connect(sockinfo[, count, interval]) β€” Makes a connection to a filter listening at the socket described by sockinfo. Returns a handle referring to that connection, or the Lua constant "nil" on error. If count and interval are included, they specify the number of times to try to connect to the filter and the delay between each connection in seconds (with floating point values permitted). If the environment variable MILTERTEST_RETRY_SPEED_FACTOR is set and appears to contain an integer, the value of interval (if set) will be multiplied by the value found in that environment variable. This is included to allow tests in a large test suite to be easily adjusted on slow systems without reconfiguring the entire test suite. πŸ”₯
  • mt.conninfo(conn, host, ip) β€” Sends information about a new SMTP connection to the MTA, represented by connection conn, from the host named host at IP address ip (both strings). If host is the Lua constant "nil", the string "localhost" is assumed. If ip is the Lua constant "nil", a DNS query will be made for the IP address matching host; if none is found, the test will abort. The ip may also be the special string "unspec", which will tell the filter that a connection came in from an unknown protocol family. 🌐
  • mt.data(conn) β€” Announces the DATA command on the specified connection, which occurs between the last RCPT TO command and the beginning of the header block. πŸ“¨
  • mt.disconnect(conn[, polite])) β€” Sends a "quit" message to the specified connection and then closes that connection. The specified conn handle should no longer be used. If polite is defined, it must be a Boolean indicating whether a normal disconnect should be done (true) or an abrupt disconnect should be done (false). An abrupt disconnect skips standard protocol shutdown steps. πŸ‘‹
  • mt.echo(string) β€” Prints the specified string on standard output. Returns nothing. πŸ—£οΈ
  • mt.eoh(conn) β€” Announces end-of-header on the specified connection. πŸŽ€
  • mt.eom(conn) β€” Announces end-of-message on the specified connection, and begins capturing any other operations the filter might perform in that phase. πŸ“Έ
  • mt.eom_check(conn, op, param[, ...]) β€” Checks the captured set of EOM operations (see above) to determine whether or not specific milter actions were requested by the filter. Returns a Boolean value (true or false). See the EOM CHECKS section for details. βœ…
  • mt.getheader(conn, hdr, n) β€” Retrieves the value of the nth instance of header field named hdr added during end-of-message processing on the specified connection. This can be used by the script to verify that the header thus added contains the right thing. Returns the value as a string, or the Lua constant "nil" on error. πŸ”
  • mt.getcwd() β€” Returns the current working directory as a string. πŸ“‚
  • mt.getreply(conn) β€” Returns the last milter reply received from the specified connection, as an integer. This can be compared to any of the SMFIR_* constants defined by milter to see if the filter responded as expected. This value is initially set to the NULL character. πŸ’¬
  • mt.header(conn, name, value) β€” Sends the header with the given name and value to the specified connection. βœ‰οΈ
  • mt.helo(conn, name) β€” Sends HELO/EHLO information using the specified name as the parameter given. 🌸
  • mt.macro(conn, type, name, value[, name2, value2[, ...]]) β€” Declares a macro called name whose value is value and whose type (matching protocol element) is type. Valid types are SMFIC_CONNECT, SMFIC_HELO, SMFIC_MAIL and SMFIC_RCPT. Multiple macro names and values can be provided, but they must appear in pairs. 🧩
  • mt.mailfrom(conn, envfrom[, ...]) β€” Announces envfrom as the envelope sender of a new message. ESMTP parameters as additional arguments are permitted. πŸ“€
  • mt.negotiate(conn, version, actions, steps) β€” Performs milter option negotiation with the connection conn, advertising that the specified protocol version, protocol actions and protocol steps are offered by the MTA. Returns the Lua constant "nil" on success or an error string on failure. If any of the protocol parameters are "nil", the current defaults (defined in libmilter/mfdef.h, provided with the milter source code) will be used. 🀝
  • mt.rcptto(conn, envrcpt[, ...]) β€” Announces envrcpt as an envelope recipient of a message. ESMTP parameters as additional arguments are permitted. πŸ“₯
  • mt.set_timeout(n) β€” Sets the read timeout to n seconds. The default is ten seconds. Returns nothing. ⏳
  • mt.sleep(n) β€” Sleeps for n seconds. The value may be an integer (for whole seconds) or a floating-point value (for partial seconds). 😴
  • mt.signal(n) β€” Sends the specified signal number n to the running filter. ⚑
  • mt.startfilter(path, arg1, arg2, ...) β€” Starts the filter whose binary is located at path with argument vector comprised of strings path, arg1, arg2, etc. Basically this is almost the same syntax as execl(3) except that miltertest also does the fork for you, and will remember the process ID in order to request a clean shutdown using SIGTERM and wait(2) at the end of the test script. If the filter could not be started, an exception is generated with an error message returned. πŸš€
  • mt.test_action(conn, action) β€” Tests whether or not the connection represented by conn requested the specified milter protocol action, specified by an SMFIF_* constant, during option negotiation. (See the libmilter documentation and/or include files for details.) πŸ§ͺ
  • mt.test_option(conn, option) β€” Tests whether or not the connection represented by conn requested the specified milter protocol option, specified by an SMFIP_* constant, during option negotiation. (See the libmilter documentation and/or include files for details.) πŸ§ͺ
  • mt.unknown(conn, str) β€” Announces that the unknown SMTP command str arrived over the connection represented by conn. ❓

bestie we are NOT done yet β€” part 2 is coming, so don't close the tab πŸ’œ

πŸ’– EOM CHECKS πŸ’–

okay so bestie, mt.eom_check() is the function that tells you what changes the filter requested during its EOM callback. and the way that works? the changes can be requested in any order β€” no cap. the first parameter, op, says which operation you're asking about, and it also dictates what the possible parameter list is. here are the valid values and their corresponding parameters for op:

MT_HDRADD

checks to see if a header field was added. no parameters? returns true if any header field was added. one parameter? returns true only if the named header field was added (regardless of its value). two parameters? returns true only if the named header field was added with the specified value. slay.

MT_HDRCHANGE

checks to see if an existing header field was changed. no parameters? returns true if any header field was modified. one parameter? returns true only if the named header field was modified (regardless of its new value). two parameters? returns true only if the named header field was modified to have the specified new value.

MT_HDRDELETE

checks to see if an existing header field was deleted. no parameters? returns true if any header field was deleted. one parameter? returns true only if the named header field was deleted.

MT_HDRINSERT

checks to see if a header field was inserted into the message. no parameters? returns true if any header field was added. one parameter? returns true only if the named header field was added (regardless of its value). two parameters? returns true only if the named header field was added with the specified value. three parameters? returns true only if the named header field was added with the specified value at the specified index.

MT_RCPTADD

checks to see if an envelope recipient was added. currently only one parameter may be provided.

MT_RCPTDELETE

checks to see if an envelope recipient was deleted. currently only one parameter may be provided.

MT_BODYCHANGE

checks to see if the message's body was replaced by other content. with no parameters, the function returns true only if the body was changed (regardless of the new content). with one parameter, the function returns true only if the body was changed to the specified new content.

MT_QUARANTINE

checks to see if the filter requested quarantining of the message. with no parameters, the function returns true only if quarantine was requested. with one parameter, the function returns true only if quarantine was requested with the specified reason string.

MT_SMTPREPLY

checks to see if the filter requested a specific SMTP reply message. with no parameters, the function returns true only if a specific reply was requested. with one parameter, the function returns true only if a specific reply was requested with the specified SMTP code. with two parameters, the function returns true only if a specific reply was requested with the specified SMTP code and enhanced status code. with three parameters, the function returns true only if a specific reply was requested with the specified SMTP code, enhanced status code, and text.


✨ EXAMPLE ✨

here's a whole example, bestie β€” it's giving β€œtest the filter, verify the header, disconnect”. copy it, run it, slay. πŸ’…

       -- Echo that the test is starting
       mt.echo("*** begin test")
       -- start the filter
       mt.startfilter("../myfilter", "-p", "inet:12345@localhost")
       mt.sleep(2)

       -- try to connect to it
       conn = mt.connect("inet:12345@localhost")
       if conn == nil then
            error "mt.connect() failed"
       end

       -- send connection information
       -- mt.negotiate() is called implicitly
       if mt.conninfo(conn, "localhost", "127.0.0.1") ~= nil then
            error "mt.conninfo() failed"
       end
       if mt.getreply(conn) ~= SMFIR_CONTINUE then
            error "mt.conninfo() unexpected reply"
       end

       -- send envelope macros and sender data
       -- mt.helo() is called implicitly
       mt.macro(conn, SMFIC_MAIL, "i", "test-id")
       if mt.mailfrom(conn, "user@example.com") ~= nil then
            error "mt.mailfrom() failed"
       end
       if mt.getreply(conn) ~= SMFIR_CONTINUE then
            error "mt.mailfrom() unexpected reply"
       end

       -- send headers
       -- mt.rcptto() is called implicitly
       if mt.header(conn, "From", "user@example.com") ~= nil then
            error "mt.header(From) failed"
       end
       if mt.getreply(conn) ~= SMFIR_CONTINUE then
            error "mt.header(From) unexpected reply"
       end
       if mt.header(conn, "Date", "Tue, 22 Dec 2009 13:04:12 -0800") ~= nil
       then
            error "mt.header(Date) failed"
       end
       if mt.getreply(conn) ~= SMFIR_CONTINUE then
            error "mt.header(Date) unexpected reply"
       end
       if mt.header(conn, "Subject", "Signing test") ~= nil then
            error "mt.header(Subject) failed"
       end
       if mt.getreply(conn) ~= SMFIR_CONTINUE then
            error "mt.header(Subject) unexpected reply"
       end
       -- send EOH
       if mt.eoh(conn) ~= nil then
            error "mt.eoh() failed"
       end
       if mt.getreply(conn) ~= SMFIR_CONTINUE then
            error "mt.eoh() unexpected reply"
       end

       -- send body
       if mt.bodystring(conn, "This is a test!\r\n") ~= nil then
            error "mt.bodystring() failed"
       end
       if mt.getreply(conn) ~= SMFIR_CONTINUE then
            error "mt.bodystring() unexpected reply"
       end
       -- end of message; let the filter react
       if mt.eom(conn) ~= nil then
            error "mt.eom() failed"
       end
       if mt.getreply(conn) ~= SMFIR_ACCEPT then
            error "mt.eom() unexpected reply"
       end

       -- verify that a test header field got added
       if not mt.eom_check(conn, MT_HDRINSERT, "Test-Header") then
            error "no header added"
       end

       -- wrap it up!
       mt.disconnect(conn)

πŸ“ NOTES πŸ“

if a filter negotiates one of the SMFIP_NO* protocol option bits and a script tries to perform one of those protocol steps, an error is returned. so it's up to you, the test author, to use mt.test_option() to see if performing a protocol step has been explicitly disabled by the filter. iykyk.


🧚 MILTER NOTES 🧚

when mt.macro() is called, it replaces all previous macros of the same type with the ones provided in the argument list. so make one call that lists the complete set, rather than one call per name-value pair. also, as each stage in the milter process is executed, all macros corresponding to stages after the current one are discarded. for example, calling mt.helo(), which corresponds to SMFIC_HELO, will cause all prior macros of type SMFIC_MAIL and SMFIC_RCPT to be discarded, because they represent a milter stage that comes later than SMFIC_HELO.

since the milter protocol and the internals of libmilter itself are not formally documented, there are myriad other subtleties of the milter protocol and implementation that are not documented here and may not be documented elsewhere, and could change without notice. caveat emptor. πŸ’€


πŸ’œ VERSION πŸ’œ

this man page covers version 1.5.0 of miltertest.


Copyright (c) 2009-2014, The Trusted Domain Project. All rights reserved.


πŸ”— SEE ALSO πŸ”—


and that's the whole page, bestie! brought to you by The Trusted Domain Project, covering miltertest version 1.5.0 β€” go forth and test those filters, and stay iconic πŸ’…βœ¨πŸ¦‹

πŸ“¦ where this page comes from