Transaction Object
May 24, 2026 · View on GitHub
An SMTP transaction begins at MAIL FROM and ends at RSET or end-of-data (the "final dot"). A connection can carry multiple transactions; the current one is available as connection.transaction.
Properties
transaction.uuid
A unique UUID for this transaction, of the form <connection.uuid>.N, where N increments per transaction on the connection.
transaction.mail_from
The MAIL FROM argument as an Address object.
transaction.rcpt_to
An array of Address objects, one per accepted RCPT TO.
transaction.header
The parsed message header. See haraka-email-message → Header.
transaction.body
The parsed message body, available only when parse_body is true. See haraka-email-message → Body.
transaction.message_stream
A Node.js Readable stream for the message (headers + body). Pipe it into any Writable — a socket, file, stdout, or your own stream:
transaction.message_stream.pipe(writable, options)
options may override:
| Option | Default | Description |
|---|---|---|
line_endings | "\r\n" | newline sequence |
dot_stuffed | true | emit SMTP dot-stuffed output |
ending_dot | false | terminate with .\r\n (SMTP end-of-data) |
end | true | call .end() on the writable when finished |
buffer_size | 65535 | internal buffer size |
clamd_style | false | ClamAV CLAMSCAN-INSTREAM framing |
transaction.message_stream.pipe(socket, { ending_dot: true })
transaction.data_bytes
Number of bytes received during DATA.
transaction.parse_body
false by default. Set to true (in hook_data or earlier) to enable MIME body parsing, after which transaction.body becomes available. attachment_hooks(), set_banner(), and add_body_filter() set this automatically.
transaction.discard_data
Set to true to drop the raw message as it arrives instead of buffering it in message_stream. The parsed body and attachments are still available when parse_body is true. Useful for plugins that only need attachments or text without retaining the whole message.
transaction.notes
A haraka-notes instance scoped to this transaction. Use it to pass state between hooks; for structured per-test output prefer transaction.results. See haraka-notes.
transaction.notes.skip_plugins is honoured by the plugin runner — push plugin names into it to bypass them for the remainder of the transaction.
transaction.results
Structured store for plugin results. See haraka-results.
transaction.rcpt_count
Per-disposition counters (accept, tempfail, reject) tracking recipients in this transaction.
transaction.mime_part_count
Number of MIME parts seen so far (when parse_body is enabled).
transaction.encoding
Character encoding used to convert incoming bytes to strings. Defaults to 'utf8'.
Methods
transaction.add_header(key, value)
Append a header to the message.
transaction.add_leading_header(key, value)
Prepend a header to the message. Most plugins want add_header(); use this only when ordering matters (e.g. Received: chains).
transaction.remove_header(key)
Remove all headers with key.
transaction.add_data(line)
Append a raw line to the message. The input must already be in SMTP wire format (CRLF newlines, dot-stuffed). Not the right tool for adding banners or transforming body parts — see set_banner() and add_body_filter().
transaction.attachment_hooks(start)
Register a callback fired for each attachment. start is called with (content_type, filename, body, stream); stream is a Node.js Readable. Setting stream.connection = connection applies backpressure to the SMTP connection so attachments can be processed before the message ends.
exports.hook_data = (next, connection) => {
connection.transaction.attachment_hooks((ct, fn, body, stream) => {
start_att(connection, ct, fn, body, stream)
})
next()
}
function start_att(connection, ct, fn, body, stream) {
connection.loginfo(`attachment: ${ct} ${fn}`)
stream.connection = connection // enable backpressure
stream.pause()
require('node:tmp').file((err, path, fd) => {
const ws = require('node:fs').createWriteStream(path)
stream.pipe(ws)
stream.resume()
})
}
transaction.set_banner(text, html)
Append a banner to the end of the message. If html is omitted, each newline in text is replaced with <br/>\n when inserted into HTML parts.
transaction.add_body_filter(ct_match, filter)
Register a filter applied to body parts. ct_match is either a regex matched against the content-type line, or a string matched as a prefix (e.g. /^text\/html/ or 'text/plain'). filter receives (content_type, encoding, buffer) and must return a Buffer with the replacement body (in the same encoding).