Plugin API

Handy functions in the fuglu.shared module


Return the human-readable string for this code

fuglu.shared.apply_template(templatecontent, suspect, values=None, valuesfunction=None)

Replace templatecontent variables as defined in with actual values from suspect the calling function can pass additional values by passing a values dict

if valuesfunction is not none, it is called with the final dict with all built-in and passed values and allows further modifications, like SQL escaping etc

fuglu.shared.default_template_values(suspect, values=None)

Return a dict with default template variables applicable for this suspect if values is not none, fill the values dict instead of returning a new one

fuglu.shared.string_to_actioncode(actionstring, config=None)

return the code for this action

The Suspect class

class fuglu.shared.Suspect(from_address: str, recipients: str | List[str], tempfile: str | None, inbuffer: bytes | None = None, smtp_options: Set | None = None, milter_macros: Dict | None = None, **kwargs)

The suspect represents the message to be scanned. Each scannerplugin will be presented with a suspect and may modify the tags or even the message content itself.

add_header(key: str, value: str, immediate: bool = False) None

adds a header to the message. by default, headers will be added when re-injecting the message back to postfix if you set immediate=True the message source will be replaced immediately. Only set this to true if a header must be visible to later plugins (e.g. for spamassassin rules), otherwise, leave as False which is faster.


To keep track of already added headers (not in self.addheaders)

build_headeronly_message_rep() EmailMessage

Build a python email representation from headers only

check_id(id: str | None = None) bool

verify id is a valid fuglu id (a string of 32 hex characters)

client_info_from_rcvd(ignoreregex: str | None = None, skip: int = 0, skiponerror: bool = False, ignorelineregex: str | None = None, skipsamedomain: bool = False) Tuple[str, str, str] | None

returns information about the client that submitted this message. (helo,ip,reversedns)

This information is extracted from the message Received: headers and therefore probably not 100% reliable all information is returned as-is, this means for example, that non-fcrdns client will show ‘unknown’ as reverse dns value.

if ignoreregex is not None, all results which match this regex in either helo,ip or reversedns will be ignored if ignorelineregex is not None, all results which match this regex will be ignored if skipsamedomain is True, ignore received lines where from & by domain is in same domain

By default, this method starts searching at the top Received Header. Set a higher skip value to start searching further down.

both these arguments can be used to filter received headers from local systems in order to get the information from a boundary MTA

returns None if the client info can not be found or if all applicable values are filtered by skip/ignoreregex


Attachment manager

debug(message: str) None

Add a line to the debug log if debugging is enabled for this message

static decode_msg_header(header: str | bytes | Header, decode_errors: str = 'replace') str

Decode message header from email.message into unicode string


header (str, email.header.Header): the header to decode decode_errors (str): error handling as in standard bytes.decode -> strict, ignore, replace



static generate_id() str

returns a unique id (a string of 32 hex characters)

get_as_attachment(filename: str | None = None) MIMEBase

returns message as multipart attachment :param filename: filename as which to attach. defaults to suspectid.eml :return: mime message object

get_client_info(config: FuConfigParser | None = None) Tuple[str, str, str] | None

returns information about the client that submitted this message. (helo,ip,reversedns)

In before-queue mode this info is extracted using the XFORWARD SMTP protocol extension.

In after-queue mode this information is extracted from the message Received: headers and therefore probably not 100% reliable all information is returned as-is, this means for example, that non-fcrdns client will show ‘unknown’ as reverse dns value.

if no config object is passed, the first parseable Received header is used. otherwise, the config is used to determine the correct boundary MTA (trustedhostsregex / boundarydistance)

get_header(headername: str, fallback: str | None = None, use_cache: bool = True) str | None

Returns content of header. value is cached for fast repeated lookups of header. :param headername: name of header :param fallback: fallback value in case header is not present, defaults to None :param use_cache: set False to get current value. Will update cache with the latest value :return: value of header or fallback value if header not present

get_headers() str

Returns the message headers as string :return: string of all headers

get_message_rep() EmailMessage

returns the python email api representation of this suspect

get_original_source(maxbytes: int | None = None) bytes

returns the original, unmodified message source as bytes

get_sa_temp_headers(plugin: str = 'SAPlugin') bytes

returns temporary pseude headers as a bytes string. :param plugin: name of destination plugin. defaults to SAPlugin :return: bytes: temp headers

get_source(maxbytes: int | None = None) bytes

returns the current message source, possibly changed by plugins

get_tag(key: str, defaultvalue: Any | None = None) Any

returns the tag value. if the tag is not found, return defaultvalue instead (None if no defaultvalue passed)

static getlist_space_comma_separated(inputstring: str) List[str]

Create list from string, splitting at ‘,’ space

has_message_rep() bool

returns true if python email api representation of this suspect exists already

is_blocked() bool

Returns True if ANY plugin tagged this suspect as blocked content

is_blocklisted() bool

Returns True if ANY plugin tagged this suspect as ‘not welcome by recipient’

is_ham() bool

Returns True if message is neither considered to be spam, virus, blocked or blocklisted

is_highspam() bool

Returns True if ANY of the spam engines tagged this suspect as high spam

is_modified() bool

returns true if the message source has been modified

is_spam() bool

Returns True if ANY of the spam engines tagged this suspect as spam

is_virus() bool

Returns True if ANY of the antivirus engines tagged this suspect as infected

is_welcomelisted() bool

Returns True if ANY plugin tagged this suspect as ‘welcome by recipient’


To keep track of modified headers

parse_from_type_header(header: str = 'From', validate_mail: bool = True, recombine: bool = True, use_cache: bool = True) List[Tuple[str, str]]

header (str): name of header to extract, defaults to From validate_mail (bool): base checks for valid mail recombine (bool): recombine displaypart with mailaddress use_cache (bool): do not recalculate if previous result exists

[(displayname,email), … ]
  • displayname (str) : display name

  • email (str) : email address

static prepend_header_to_source(key: str | bytes, value: str | bytes, source: bytes) bytes

Prepend a header to the message


key (str): the header key value (str): the header value source (bytes): the message source


bytes: the new message buffer

remove_headers(key: str) None

Remove existing header(s) with given name


key (string): header key


To keep track of already removed headers

set_header(key: str, value: str) None

Replace existing header or create a new one


key (string): header key value (string): header value

set_message_rep(msgrep: EmailMessage, att_mgr_reset: bool = True) None

replace the message content. this must be a standard python email representation Warning: setting the source via python email representation seems to break dkim signatures!

The attachment manager is build based on the python mail representation. If no message attachments or content is modified there is no need to recreate the attachment manager.


msgrep (email): standard python email representation att_mgr_reset (bool): Reset the attachment manager

set_source(source: bytes | str, encoding: str = 'utf-8', att_mgr_reset: bool = True) None

Store message source. This might be modified by plugins later on…


source (bytes,str): new message source encoding (str): encoding, default is utf-8 att_mgr_reset (bool): Reset the attachment manager

set_tag(key: str, value: Any) None

Set a new tag


holds the message source if set directly

source_stripped_attachments(content: bytes | None = None, maxsize: int | None = None, with_mime_headers: bool = False) bytes

strip all attachments from multipart mails except for plaintext and html text parts. if message is still too long, truncate.


content (string,bytes): message source maxsize (integer): maximum message size accepted with_mime_headers (boolean): add mime headers from attachments


bytes: stripped and truncated message content

property timetracker

returns scansession.TrackTimings object or None

property to_address: str | None

Returns the first recipient address

property to_domain: str | None

Returns the local part of the first recipient

property to_localpart: str | None

Returns the local part of the first recipient

update_subject(subject_cb: Callable, **cb_params) bool

update/alter the message subject :param subject_cb: callback function that alters the subject. must accept a string and return a string :param cb_params: additional parameters to be passed to subject_cb :return: True if subject was altered, False otherwise

wrap(sender: str, recipient: str, subject: str, body: str | None = None, filename: str | None = None, config: ~fuglu.shared.FuConfigParser = <class 'NoneType'>, hdr_autosub: str | None = 'auto-generated', hdr_arsupp: str | None = 'DR, RN, NRN, OOF, AutoReply') MIMEMultipart

attach original source to a new multipart email :param sender: wrapper From header address :param recipient: wrapper To header address :param subject: wrapper Subject header :param body: additional wrapper body, leave empty to omit :param filename: filename as which to attach. defaults to suspectid.eml :param config: fuglu config object, needed for proper evaluation of outgoing helo which is used to create message id header :param hdr_autosub: add auto-submitted header :param hdr_arsupp: add x-auto-response-suppress header :return: mime multipart message object

write_sa_temp_header(header: str, value: str, plugin: str = 'SAPlugin') None

Write a temporary pseudo header. This is used by e.g. SAPlugin to pass extra information to external services :param header: pseudo header name :param value: pseudo header value :param plugin: name of destination plugin. defaults to SAPlugin :return: None

The SuspectFilter class

class fuglu.shared.SuspectFilter(*args, **kwargs)

Allows filtering Suspect based on header/tag/body regexes

get_args(suspect, extended=False)

returns all args of matched regexes in a list if extended=True: returns a list of tuples with all available information: (fieldname, matchedvalue, arg, regex)

get_decoded_textparts(suspect, attachment=None, inline=None)

Get all text parts of suspect as a list. Text parts can be limited by the attachment, inline keywords which checks the Content-Disposition header:

attachment: True/False/None

None: Ignore True: attachment or header not present False: no attachment

inline: True/False/None

None: Ignore True: inline attachment False: no inline attachment or header present, so attached textparts are included


suspect (Suspect, PatchedMessage): Suspect object attachment (bool, NoneType): filter for attachments inline (bool, NoneType): filter for inline attachments

The input should be a Suspect. Due to backward compatibility email.message.Message is still supported and passed to the deprecated routine which will however NOT handle the additional keyword parameters for filtering attachments and inline attachments.


list: List containing decoded text parts


Returns a list of all text contents

get_field(suspect, headername)

return a list of mail header values or special values. If the value can not be found, an empty list is returned.


just the headername or header:<headername> for standard message headers mime:headername for attached mime part headers

envelope data:

envelope_from (or from_address) envelope_to (or to_address) from_domain to_domain clientip clienthostname (fcrdns or ‘unknown’) clienthelo


@tagname @tagname.fieldname (maps to suspect.tags[‘tagname’][‘fieldname’], unless suspect.tags[‘tagname.fieldname’] exists)

body source:

body:full -> (full source, encoded) body:stripped (or just ‘body’) : -> returns text/* bodyparts with tags and newlines stripped body:raw -> decoded raw message body parts


check file and print warnings to console. returns True if everything is ok, False otherwise

matches(suspect, extended=False, verbose=False)

returns (True,arg) if any regex matches, (False,None) otherwise

if extended=True, returns all available info about the match in a tuple: True, (fieldname, matchedvalue, arg, regex)

strip_text(content, remove_tags=None, replace_nbsp=True, use_bfs=True, fugluid: str = '<>')

Strip HTML Tags from content, replace newline with space (like Spamassassin)


(unicode/str) Unicode string (Py3 ‘str’ is unicode string)