Skip to content

API Reference

FluxMail

FluxMail(object_type: Union[EmailObject, str], host: Union[EmailInstance, str], logger: Optional[logging.Logger] = None, log_level: str = 'WARNING', port: int = 25, username: Optional[str] = None, password: Optional[str] = None, use_tls: bool = False, use_ssl: bool = False, ssl_context: Optional[ssl.SSLContext] = None, timeout: int = 30, max_retries: int = 0, retry_delay: float = 1.0)

Automates email creation and sending using SMTP or Outlook.

PARAMETER DESCRIPTION

object_type

Email object type: smtp or outlook.

TYPE: Union[EmailObject, str]

host

SMTP relay host. Accepts an EmailInstance(relay=..., domain=...) namedtuple, a bare relay hostname string (e.g. "smtp.gmail.com"), or a "relay:domain" string (e.g. "smtp.gmail.com:gmail.com").

TYPE: Union[EmailInstance, str]

logger

Logger instance for logging.

TYPE: Logger DEFAULT: None

log_level

Logging level string (e.g. "DEBUG", "INFO", "WARNING", "ERROR"). Default: "WARNING".

TYPE: str DEFAULT: 'WARNING'

port

SMTP port. Default: 25. Use 587 for STARTTLS.

TYPE: int DEFAULT: 25

username

SMTP login username. When this is a valid email address it also serves as the default From address if sender is not provided in :meth:create.

TYPE: str DEFAULT: None

password

SMTP login password.

TYPE: str DEFAULT: None

use_tls

Enable STARTTLS for the SMTP connection. Default: False.

TYPE: bool DEFAULT: False

use_ssl

Enable implicit TLS (SSL) for the SMTP connection (port 465). Default: False. Mutually exclusive with use_tls.

TYPE: bool DEFAULT: False

ssl_context

Custom SSL context for TLS/SSL connections. Default: None.

TYPE: SSLContext DEFAULT: None

timeout

Connection timeout in seconds. Default: 30.

TYPE: int DEFAULT: 30

max_retries

Number of retry attempts on send failure. 0 disables retries. Default: 0.

TYPE: int DEFAULT: 0

retry_delay

Seconds to wait between retry attempts. Default: 1.0.

TYPE: float DEFAULT: 1.0

RAISES DESCRIPTION
FluxMailException

If host is not provided or Outlook is used on a non-Windows OS.

TypeError

If host is not a string or EmailInstance.

METHOD DESCRIPTION
create

Creates an email with the specified details.

display

Displays or returns an email preview.

send

Sends or previews the email.

send_async

Sends or previews the email asynchronously (SMTP only).

from_env

Create a FluxMail instance from environment variables.

test_connection

Test SMTP connectivity and authentication without sending email.

Source code in src/fluxmail/fluxmail.py
Python
def __init__(
    self,
    object_type: Union[EmailObject, str],
    host: Union[EmailInstance, str],
    logger: Optional[logging.Logger] = None,
    log_level: str = "WARNING",
    port: int = 25,
    username: Optional[str] = None,
    password: Optional[str] = None,
    use_tls: bool = False,
    use_ssl: bool = False,
    ssl_context: Optional[ssl.SSLContext] = None,
    timeout: int = 30,
    max_retries: int = 0,
    retry_delay: float = 1.0,
):
    """Initializes the FluxMail class.

    Parameters
    ----------
    object_type : Union[EmailObject, str]
        Email object type: ``smtp`` or ``outlook``.
    host : Union[EmailInstance, str]
        SMTP relay host. Accepts an ``EmailInstance(relay=..., domain=...)``
        namedtuple, a bare relay hostname string (e.g. ``"smtp.gmail.com"``),
        or a ``"relay:domain"`` string (e.g. ``"smtp.gmail.com:gmail.com"``).
    logger : logging.Logger, optional
        Logger instance for logging.
    log_level : str, optional
        Logging level string (e.g. ``"DEBUG"``, ``"INFO"``, ``"WARNING"``, ``"ERROR"``).
        Default: ``"WARNING"``.
    port : int, optional
        SMTP port. Default: ``25``. Use ``587`` for STARTTLS.
    username : str, optional
        SMTP login username. When this is a valid email address it also
        serves as the default ``From`` address if ``sender`` is not provided
        in :meth:`create`.
    password : str, optional
        SMTP login password.
    use_tls : bool, optional
        Enable STARTTLS for the SMTP connection. Default: ``False``.
    use_ssl : bool, optional
        Enable implicit TLS (SSL) for the SMTP connection (port 465). Default: ``False``.
        Mutually exclusive with ``use_tls``.
    ssl_context : ssl.SSLContext, optional
        Custom SSL context for TLS/SSL connections. Default: ``None``.
    timeout : int, optional
        Connection timeout in seconds. Default: ``30``.
    max_retries : int, optional
        Number of retry attempts on send failure. ``0`` disables retries. Default: ``0``.
    retry_delay : float, optional
        Seconds to wait between retry attempts. Default: ``1.0``.

    Raises
    ------
    FluxMailException
        If ``host`` is not provided or Outlook is used on a non-Windows OS.
    TypeError
        If ``host`` is not a string or ``EmailInstance``.
    """
    if use_ssl and use_tls:
        raise FluxMailException(
            "use_ssl and use_tls are mutually exclusive — use use_ssl for port 465 "
            "implicit TLS, or use_tls for port 587 STARTTLS.",
            code="invalid_config",
        )

    self.object_type = str_to_enum(EmailObject, object_type)
    self.port = port
    self.username = username
    self.password = password
    self.use_tls = use_tls
    self.use_ssl = use_ssl
    self.ssl_context = ssl_context
    self.timeout = timeout
    self.max_retries = max_retries
    self.retry_delay = retry_delay

    if isinstance(host, EmailInstance):
        self.host = host
    elif isinstance(host, str):
        if ":" in host:
            relay, domain = host.split(":", 1)
            self.host = EmailInstance(relay=relay.strip(), domain=domain.strip())
        else:
            self.host = EmailInstance(relay=host.strip())
    else:
        raise TypeError(
            f"host must be an EmailInstance or a string, got {type(host).__name__}"
        )

    if logger and not isinstance(logger, logging.Logger):
        raise FluxMailException(
            "logger must be an instance of logging.Logger. "
            "Use get_logger() from pylogshield or Python's standard logging module.",
            code="invalid_config",
        )

    self.logger = logger or get_logger("fluxmail", log_level=log_level)
    self.log_level = log_level
    self.message = None
    self.is_created = False
    self.plain_body = None
    self.in_reply_to = None
    self.references = None
    self.priority = None
    self.unsubscribe_url = None

    if self.is_smtp():
        self.message = EmailMessage()
        self._transport = _SMTPTransport(
            relay=self.host.relay,
            port=self.port,
            use_ssl=use_ssl,
            use_tls=use_tls,
            ssl_context=ssl_context,
            timeout=timeout,
            username=username,
            password=password,
            logger=self.logger,
        )
        tls_mode = (
            "implicit-TLS" if use_ssl else ("STARTTLS" if use_tls else "plain")
        )
        self.logger.debug(
            "FluxMail[smtp] ready: relay=%s port=%d %s timeout=%ds retries=%d",
            self.host.relay,
            self.port,  # type: ignore[assignment]
            tls_mode,
            timeout,
            max_retries,
        )
    elif self.is_outlook():
        self._transport = None  # type: ignore[assignment]
        self.logger.debug("FluxMail[outlook] ready")
        if win32 is None:
            raise FluxMailException(
                "Outlook is only supported on Windows OS.",
                code="invalid_config",
            )
        ol_app = win32.Dispatch("outlook.application")
        self.message = ol_app.CreateItem(0)

create(subject: str, recipients: List[str], body: str, sender: Optional[str] = None, cc: Optional[List[str]] = None, bcc: Optional[List[str]] = None, reply_to: Optional[str] = None, attachments: Optional[List[str]] = None, html_body: bool = False, plain_body: Optional[str] = None, in_reply_to: Optional[str] = None, references: Optional[List[str]] = None, priority: Optional[str] = None, unsubscribe_url: Optional[str] = None, inline_images: Optional[Dict[str, str]] = None, inline_css: bool = False) -> FluxMail

Creates an email with the specified details.

PARAMETER DESCRIPTION

subject

Email subject.

TYPE: str

recipients

Recipient email addresses.

TYPE: list of str

body

Email content (plain text or HTML).

TYPE: str

sender

Sender address (SMTP only). Defaults to username if username is a valid email address, otherwise this parameter is required.

TYPE: str DEFAULT: None

cc

CC'd email addresses.

TYPE: list of str DEFAULT: None

bcc

BCC'd email addresses (SMTP only).

TYPE: list of str DEFAULT: None

reply_to

Reply-To address (SMTP only).

TYPE: str DEFAULT: None

attachments

Paths to files to attach.

TYPE: list of str DEFAULT: None

html_body

Use HTML format. Default: False.

TYPE: bool DEFAULT: False

plain_body

Plain-text fallback body. When provided alongside html_body=True the message is built as multipart/alternative.

TYPE: str DEFAULT: None

in_reply_to

Message-ID of the email being replied to (SMTP only).

TYPE: str DEFAULT: None

references

List of Message-IDs forming the thread chain (SMTP only).

TYPE: list of str DEFAULT: None

priority

Message priority: "high", "normal", or "low" (SMTP only).

TYPE: str DEFAULT: None

RETURNS DESCRIPTION
FluxMail

Returns self to support method chaining.

Source code in src/fluxmail/fluxmail.py
Python
def create(
    self,
    subject: str,
    recipients: List[str],
    body: str,
    sender: Optional[str] = None,
    cc: Optional[List[str]] = None,
    bcc: Optional[List[str]] = None,
    reply_to: Optional[str] = None,
    attachments: Optional[List[str]] = None,
    html_body: bool = False,
    plain_body: Optional[str] = None,
    in_reply_to: Optional[str] = None,
    references: Optional[List[str]] = None,
    priority: Optional[str] = None,
    unsubscribe_url: Optional[str] = None,
    inline_images: Optional[Dict[str, str]] = None,
    inline_css: bool = False,
) -> "FluxMail":
    """Creates an email with the specified details.

    Parameters
    ----------
    subject : str
        Email subject.
    recipients : list of str
        Recipient email addresses.
    body : str
        Email content (plain text or HTML).
    sender : str, optional
        Sender address (SMTP only). Defaults to ``username`` if ``username``
        is a valid email address, otherwise this parameter is required.
    cc : list of str, optional
        CC'd email addresses.
    bcc : list of str, optional
        BCC'd email addresses (SMTP only).
    reply_to : str, optional
        Reply-To address (SMTP only).
    attachments : list of str, optional
        Paths to files to attach.
    html_body : bool, optional
        Use HTML format. Default: ``False``.
    plain_body : str, optional
        Plain-text fallback body. When provided alongside ``html_body=True``
        the message is built as ``multipart/alternative``.
    in_reply_to : str, optional
        Message-ID of the email being replied to (SMTP only).
    references : list of str, optional
        List of Message-IDs forming the thread chain (SMTP only).
    priority : str, optional
        Message priority: ``"high"``, ``"normal"``, or ``"low"`` (SMTP only).

    Returns
    -------
    FluxMail
        Returns ``self`` to support method chaining.
    """
    # Reset so create() can be called again on the same instance.
    if self.is_smtp():
        self.message = EmailMessage()  # type: ignore[assignment]
    self.is_created = False

    self.subject = subject
    self.recipients = recipients
    self.body = body  # type: ignore[assignment]
    self.plain_body = plain_body  # type: ignore[assignment]
    self.sender = sender  # type: ignore[assignment]
    self.cc = cc  # type: ignore[assignment]
    self.bcc = bcc  # type: ignore[assignment]
    self.reply_to = reply_to  # type: ignore[assignment]
    self.input_path = attachments
    self.html_body = html_body  # type: ignore[assignment]
    self.in_reply_to = in_reply_to  # type: ignore[assignment]
    self.references = references  # type: ignore[assignment]
    self.priority = priority  # type: ignore[assignment]
    self.unsubscribe_url = unsubscribe_url  # type: ignore[assignment]
    self.inline_images = inline_images
    self.inline_css = inline_css

    self._validate_parameters()
    self._handle_message_id()
    self._handle_sender()
    self._handle_recipient()
    self._handle_cc()
    self._handle_bcc()
    self._handle_reply_to()
    self._handle_unsubscribe()
    self._handle_threading()
    self._handle_priority()
    self._set_content_type()
    self._attach_files()
    self._attach_inline_images()
    self.is_created = True
    self.logger.info(
        "Email created: subject=%r to=%d cc=%d bcc=%d attachments=%d html=%s",
        self.subject,
        len(self.recipients),
        len(self.cc) if self.cc else 0,
        len(self.bcc) if self.bcc else 0,
        len(self.input_path) if self.input_path else 0,
        self.html_body,
    )
    return self

display() -> str

Displays or returns an email preview.

RETURNS DESCRIPTION
str

Email preview string.

RAISES DESCRIPTION
FluxMailException

If create() has not been called or display fails.

Source code in src/fluxmail/fluxmail.py
Python
def display(self) -> str:  # type: ignore[return]
    """Displays or returns an email preview.

    Returns
    -------
    str
        Email preview string.

    Raises
    ------
    FluxMailException
        If ``create()`` has not been called or display fails.
    """
    if not self.is_created:
        raise FluxMailException(
            "Call create() before display().", code="not_created"
        )
    try:
        if self.is_smtp():
            return f"Email Preview:\n{self.message}"
        elif self.is_outlook():
            self.message.Display()  # type: ignore[union-attr]
            return "Outlook email displayed successfully."
    except FluxMailException:
        raise
    except Exception as e:
        msg = f"Display failed: {e}"
        self.logger.error(msg)
        raise FluxMailException(msg, code="display_failed") from e

send(dry_run: bool = False) -> str

Sends or previews the email.

PARAMETER DESCRIPTION

dry_run

If True, display the email instead of sending it. Default: False.

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
str

Success or preview message.

RAISES DESCRIPTION
FluxMailException

If create() has not been called, or on send failure.

Source code in src/fluxmail/fluxmail.py
Python
def send(self, dry_run: bool = False) -> str:  # type: ignore[return]
    """Sends or previews the email.

    Parameters
    ----------
    dry_run : bool, optional
        If ``True``, display the email instead of sending it. Default: ``False``.

    Returns
    -------
    str
        Success or preview message.

    Raises
    ------
    FluxMailException
        If ``create()`` has not been called, or on send failure.
    """
    if not self.is_created:
        raise FluxMailException("Call create() before send().", code="not_created")
    if dry_run:
        return self.display()  # type: ignore[union-attr]
    if self.is_smtp() and not self.host.relay:
        raise FluxMailException("No SMTP relay configured.", code="no_relay")

    self.logger.info(
        "Sending email: subject=%r to=%r relay=%s",
        self.subject,
        self.recipients,
        self.host.relay,
    )
    try:
        if self.is_smtp():
            if self.max_retries > 0:
                for attempt in Retrying(
                    stop=stop_after_attempt(self.max_retries + 1),
                    wait=wait_fixed(self.retry_delay),
                    reraise=True,
                    before_sleep=lambda rs: self.logger.warning(
                        "Send attempt %d failed (%s) — retrying in %.1fs",
                        rs.attempt_number,
                        rs.outcome.exception(),  # type: ignore[union-attr]
                        self.retry_delay,
                    ),
                ):
                    with attempt:
                        self._transport.send(self.message)
            else:
                self._transport.send(self.message)
            self.logger.info(
                "Email sent successfully: subject=%r to=%r",
                self.subject,
                self.recipients,
            )
            return "Email sent successfully via SMTP."
        elif self.is_outlook():
            raise FluxMailException(
                "Outlook requires user interaction to send emails and cannot "
                "send programmatically.",
                code="outlook_no_send",
            )
    except FluxMailException:
        raise
    except Exception as e:
        msg = f"Send failed: {e}"
        self.logger.error(msg)
        raise FluxMailException(msg, code="send_failed") from e

send_async(dry_run: bool = False) -> str async

Sends or previews the email asynchronously (SMTP only).

Note: retry (max_retries) is not supported for async sends.

Source code in src/fluxmail/fluxmail.py
Python
async def send_async(self, dry_run: bool = False) -> str:  # type: ignore[return]
    """Sends or previews the email asynchronously (SMTP only).

    Note: retry (max_retries) is not supported for async sends.
    """
    if not self.is_created:
        raise FluxMailException(
            "Call create() before send_async().", code="not_created"
        )
    if dry_run:
        return self.display()
    if self.is_smtp() and not self.host.relay:
        raise FluxMailException("No SMTP relay configured.", code="no_relay")

    self.logger.info(
        "Sending email async: subject=%r to=%r relay=%s",
        self.subject,
        self.recipients,
        self.host.relay,
    )
    try:
        if self.is_smtp():
            await self._transport.send_async(self.message)
            self.logger.info(
                "Email sent successfully (async): subject=%r to=%r",
                self.subject,
                self.recipients,
            )
            return "Email sent successfully via SMTP."
        elif self.is_outlook():
            raise FluxMailException(
                "Outlook does not support async sending.",
                code="outlook_no_async",
            )
    except FluxMailException:
        raise
    except Exception as e:
        msg = f"Async send failed: {e}"
        self.logger.error(msg)
        raise FluxMailException(msg, code="send_failed") from e

from_env() -> FluxMail classmethod

Create a FluxMail instance from environment variables.

Reads FLUXMAIL_TYPE, FLUXMAIL_HOST, FLUXMAIL_PORT, FLUXMAIL_USERNAME, FLUXMAIL_PASSWORD, FLUXMAIL_TLS, FLUXMAIL_SSL, FLUXMAIL_TIMEOUT, FLUXMAIL_MAX_RETRIES, FLUXMAIL_RETRY_DELAY.

RAISES DESCRIPTION
FluxMailException

If FLUXMAIL_HOST is missing when FLUXMAIL_TYPE=smtp.

Source code in src/fluxmail/fluxmail.py
Python
@classmethod
def from_env(cls) -> "FluxMail":
    """Create a ``FluxMail`` instance from environment variables.

    Reads ``FLUXMAIL_TYPE``, ``FLUXMAIL_HOST``, ``FLUXMAIL_PORT``,
    ``FLUXMAIL_USERNAME``, ``FLUXMAIL_PASSWORD``, ``FLUXMAIL_TLS``,
    ``FLUXMAIL_SSL``, ``FLUXMAIL_TIMEOUT``, ``FLUXMAIL_MAX_RETRIES``,
    ``FLUXMAIL_RETRY_DELAY``.

    Raises
    ------
    FluxMailException
        If ``FLUXMAIL_HOST`` is missing when ``FLUXMAIL_TYPE=smtp``.
    """
    object_type = os.environ.get("FLUXMAIL_TYPE", "smtp")
    host_str = os.environ.get("FLUXMAIL_HOST", "")
    if object_type.lower() == "smtp" and not host_str:
        raise FluxMailException(
            "FLUXMAIL_HOST is required when FLUXMAIL_TYPE=smtp",
            code="invalid_config",
        )
    host = host_str if host_str else EmailInstance(relay="")
    return cls(
        object_type=object_type,
        host=host,
        port=int(os.environ.get("FLUXMAIL_PORT", "25")),
        username=os.environ.get("FLUXMAIL_USERNAME"),
        password=os.environ.get("FLUXMAIL_PASSWORD"),
        use_tls=_parse_bool(os.environ.get("FLUXMAIL_TLS", "false")),
        use_ssl=_parse_bool(os.environ.get("FLUXMAIL_SSL", "false")),
        timeout=int(os.environ.get("FLUXMAIL_TIMEOUT", "30")),
        max_retries=int(os.environ.get("FLUXMAIL_MAX_RETRIES", "0")),
        retry_delay=float(os.environ.get("FLUXMAIL_RETRY_DELAY", "1.0")),
    )

test_connection() -> dict

Test SMTP connectivity and authentication without sending email.

RETURNS DESCRIPTION
dict

{"ok": True, "relay": str, "port": int, "latency_ms": int}

RAISES DESCRIPTION
FluxMailException

code="outlook_no_connect" for Outlook instances. code="connection_failed" if connection or auth fails.

Source code in src/fluxmail/fluxmail.py
Python
def test_connection(self) -> dict:
    """Test SMTP connectivity and authentication without sending email.

    Returns
    -------
    dict
        ``{"ok": True, "relay": str, "port": int, "latency_ms": int}``

    Raises
    ------
    FluxMailException
        ``code="outlook_no_connect"`` for Outlook instances.
        ``code="connection_failed"`` if connection or auth fails.
    """
    if self.is_outlook():
        raise FluxMailException(
            "Outlook does not support connection testing.",
            code="outlook_no_connect",
        )
    try:
        start = time.monotonic()
        conn = self._transport._make_connection()
        latency_ms = int((time.monotonic() - start) * 1000)
        try:
            conn.quit()
        except Exception:
            pass
        self.logger.info(
            "Connection test OK: relay=%s port=%d latency=%dms",
            self.host.relay,
            self.port,
            latency_ms,
        )
        return {
            "ok": True,
            "relay": self.host.relay,
            "port": self.port,
            "latency_ms": latency_ms,
        }
    except FluxMailException:
        raise
    except Exception as e:
        msg = f"Connection test failed: {e}"
        self.logger.error(msg)
        raise FluxMailException(msg, code="connection_failed") from e

Constructor

Python
FluxMail(
    object_type,
    host,
    port=25,
    use_tls=False,
    use_ssl=False,
    ssl_context=None,
    timeout=30,
    max_retries=0,
    retry_delay=1.0,
    username=None,
    password=None,
    sender=None,
    log_level="WARNING",
    logger=None,
)

Parameters

Parameter Type Default Description
object_type str \| EmailObject "smtp" or "outlook" (or EmailObject.SMTP / EmailObject.OUTLOOK)
host str \| EmailInstance Relay hostname string, "relay:domain" shorthand, or EmailInstance
port int 25 SMTP port. Use 587 for STARTTLS, 465 for implicit TLS.
use_tls bool False Enable STARTTLS negotiation (port 587). Mutually exclusive with use_ssl.
use_ssl bool False Use implicit TLS via smtplib.SMTP_SSL (port 465). Mutually exclusive with use_tls.
ssl_context ssl.SSLContext \| None None Custom SSL context for certificate customisation (e.g. self-signed certs). Passed to both use_tls and use_ssl paths.
timeout int 30 SMTP connection timeout in seconds.
max_retries int 0 Number of additional attempts after the first send failure. 0 disables retry. Applied to send() only.
retry_delay float 1.0 Seconds to wait between retry attempts (tenacity wait_fixed).
username str \| None None SMTP login username. When a valid email, doubles as the From address.
password str \| None None SMTP login password
sender str \| None None Explicit From address. Overrides username.
log_level str "WARNING" Logging level: "DEBUG", "INFO", "WARNING", "ERROR"
logger logging.Logger \| None None Supply a custom logger instance
Python
from fluxmail import FluxMail

email = FluxMail(
    object_type="smtp",
    host="smtp.gmail.com",
    port=587,
    use_tls=True,
    username="me@gmail.com",
    password="secret",
)
Python
from fluxmail import FluxMail

email = FluxMail(
    object_type="smtp",
    host="smtp.sendgrid.net",
    port=587,
    use_tls=True,
    username="apikey",
    password="SG.xxxx",
    sender="noreply@myapp.com",
)
Python
from fluxmail import FluxMail

email = FluxMail(
    object_type="smtp",
    host="email-smtp.us-east-1.amazonaws.com",
    port=587,
    use_tls=True,
    username="AKIAIOSFODNN7EXAMPLE",
    password="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
    sender="noreply@myapp.com",
)
Python
from fluxmail import FluxMail

email = FluxMail(
    object_type="smtp",
    host="smtp.office365.com",
    port=587,
    use_tls=True,
    username="me@mycompany.com",
    password="secret",
)
Python
from fluxmail import FluxMail, EmailInstance

email = FluxMail(
    object_type="smtp",
    host=EmailInstance(relay="mail.mycompany.com", domain="mycompany.com"),
    port=25,
    username="noreply@mycompany.com",
)
Python
from fluxmail import FluxMail, EmailInstance, EmailObject

email = FluxMail(
    object_type=EmailObject.OUTLOOK,
    host=EmailInstance(relay=""),
)

create()

Build the email message. Returns self for method chaining.

Python
create(
    subject,
    recipients,
    body,
    sender=None,
    cc=None,
    bcc=None,
    attachments=None,
    html_body=False,
    plain_body=None,
    reply_to=None,
    in_reply_to=None,
    references=None,
    priority=None,
    unsubscribe_url=None,
    inline_images=None,
    inline_css=False,
)

Parameters

Parameter Type Default Description
subject str Email subject line
recipients List[str] One or more To addresses
body str Email body — plain text or HTML string
sender str \| None None Override the From address set on the constructor
cc List[str] \| None None CC addresses
bcc List[str] \| None None BCC addresses
attachments List[str] \| None None File paths to attach (MIME type auto-detected)
html_body bool False Treat body as HTML
plain_body str \| None None Plain-text fallback when html_body=True (sends multipart/alternative)
reply_to str \| None None Reply-To header (SMTP only)
in_reply_to str \| None None In-Reply-To header for threading
references List[str] \| None None References header for threading
priority str \| None None "high", "normal", or "low"
unsubscribe_url str \| None None HTTPS URL added as List-Unsubscribe + List-Unsubscribe-Post headers (RFC 8058, SMTP only)
inline_images Dict[str, str] \| None None Map of cid_name → file_path for inline images (requires html_body=True, SMTP only)
inline_css bool False Run premailer.transform() on the HTML body to inline CSS rules (silently skipped when html_body=False)
Python
email.create(
    subject="Hello",
    recipients=["friend@example.com"],
    body="Hi there!",
)
Python
email.create(
    subject="Newsletter",
    recipients=["user@example.com"],
    body="<h1>Hello</h1><p>Welcome to the newsletter.</p>",
    html_body=True,
)
Python
email.create(
    subject="Report",
    recipients=["user@example.com"],
    body="<h1>Monthly Report</h1><p>See figures below.</p>",
    html_body=True,
    plain_body="Monthly Report — open in an HTML-capable client for formatting.",
)
Python
email.create(
    subject="Q1 Summary",
    recipients=["alice@example.com", "bob@example.com"],
    body="Q1 summary is attached.",
    cc=["manager@example.com"],
    bcc=["archive@example.com"],
    attachments=["/path/to/q1-summary.pdf", "/path/to/charts.xlsx"],
)
Python
email.create(
    subject="Re: Original Subject",
    recipients=["user@example.com"],
    body="Following up on your question.",
    in_reply_to="<original-message-id@example.com>",
    references=["<original-message-id@example.com>"],
)
Python
email.create(
    subject="URGENT: Action required",
    recipients=["user@example.com"],
    body="Please respond by end of day.",
    priority="high",   # "high", "normal", or "low"
)
Python
email.create(
    subject="Do not reply to this address",
    recipients=["user@example.com"],
    body="Reply to the address below.",
    reply_to="support@myapp.com",
)

send()

Send the email. Returns self.

Python
send(dry_run=False)
Parameter Type Default Description
dry_run bool False If True, returns a preview string without sending (delegates to display())
Python
email.create(subject="Hi", recipients=["user@example.com"], body="Hello").send()
Python
from fluxmail import FluxMail

FluxMail(
    object_type="smtp",
    host="smtp.gmail.com",
    port=587,
    use_tls=True,
    username="me@gmail.com",
    password="secret",
).create(
    subject="Hello",
    recipients=["friend@example.com"],
    body="Hi there!",
).send()
Python
preview = email.send(dry_run=True)
print(preview)

send_async()

Async equivalent of send(). Delegates to aiosmtplib under the hood. max_retries is not applied — retry in the caller if needed.

Python
await send_async(dry_run=False)
Parameter Type Default Description
dry_run bool False If True, returns a preview string without sending (delegates to display())
Python
import asyncio, os
from fluxmail import FluxMail

async def main():
    email = FluxMail(
        object_type="smtp",
        host="smtp.gmail.com",
        port=587,
        use_tls=True,
        username=os.environ["FLUXMAIL_USERNAME"],
        password=os.environ["FLUXMAIL_PASSWORD"],
    )
    email.create(
        subject="Hello",
        recipients=["user@example.com"],
        body="Sent asynchronously.",
    )
    result = await email.send_async()
    print(result)  # "Email sent successfully via SMTP."

asyncio.run(main())
Python
from fastapi import FastAPI
from fluxmail import FluxMail
import os

app = FastAPI()
mailer = FluxMail(
    object_type="smtp",
    host="smtp.gmail.com",
    port=587,
    use_tls=True,
    username=os.environ["FLUXMAIL_USERNAME"],
    password=os.environ["FLUXMAIL_PASSWORD"],
)

@app.post("/notify")
async def notify(user_email: str, message: str):
    mailer.create(subject="Notification", recipients=[user_email], body=message)
    return {"status": await mailer.send_async()}
Python
email.create(subject="Hi", recipients=["user@example.com"], body="Hello")
preview = await email.send_async(dry_run=True)
print(preview)

display()

Return an email preview string (SMTP), or open the Outlook compose window (Outlook).

Python
display()
Python
email.create(subject="Hi", recipients=["user@example.com"], body="Hello")
print(email.display())
Python
# Opens Outlook's compose window — user must click Send manually.
email.create(
    subject="Project Update",
    recipients=["user@example.com"],
    body="Please find the update below.",
)
email.display()

is_smtp() / is_outlook()

Convenience predicates for the configured protocol.

Python
email = FluxMail(object_type="smtp", host="smtp.gmail.com", ...)
email.is_smtp()    # True
email.is_outlook() # False

from_env()

Create a FluxMail instance from FLUXMAIL_* environment variables.

Python
@classmethod
from_env() -> FluxMail

Reads FLUXMAIL_TYPE, FLUXMAIL_HOST, FLUXMAIL_PORT, FLUXMAIL_USERNAME, FLUXMAIL_PASSWORD, FLUXMAIL_TLS, FLUXMAIL_SSL, FLUXMAIL_TIMEOUT, FLUXMAIL_MAX_RETRIES, FLUXMAIL_RETRY_DELAY. See the configuration guide for the full variable reference.

Python
mailer = FluxMail.from_env()

Raises FluxMailException(code="invalid_config") when FLUXMAIL_HOST is missing and FLUXMAIL_TYPE=smtp.


test_connection()

Open, authenticate, and immediately close an SMTP connection — without sending any email. Use for startup health checks and debugging credentials.

Python
test_connection() -> dict

Returns {"ok": True, "relay": str, "port": int, "latency_ms": int} on success.

Raises FluxMailException(code="connection_failed") on network or auth failure; FluxMailException(code="outlook_no_connect") for Outlook instances.

Python
try:
    info = mailer.test_connection()
    print(f"Connected in {info['latency_ms']}ms")
except FluxMailException as e:
    print(f"Failed: {e}")   # e.code == "connection_failed"

Context manager — connection reuse

Use FluxMail as a context manager to hold one SMTP connection open across multiple sends. Useful for bulk sends where opening a new connection per message would be slow.

Python
from fluxmail import FluxMail

recipients = ["a@example.com", "b@example.com", "c@example.com"]

with FluxMail(
    object_type="smtp",
    host="smtp.gmail.com",
    port=587,
    use_tls=True,
    username="me@gmail.com",
    password="secret",
) as mailer:
    for recipient in recipients:
        mailer.create(
            subject="Hello",
            recipients=[recipient],
            body="Hi there!",
        ).send()
Python
from fluxmail import FluxMail, FluxMailException

with FluxMail(object_type="smtp", host="smtp.gmail.com", port=587,
               use_tls=True, username="me@gmail.com", password="secret") as mailer:
    for recipient in recipients:
        try:
            mailer.create(subject="Hi", recipients=[recipient], body="Hello").send()
        except FluxMailException as e:
            print(f"Failed to send to {recipient}: {e}")

EmailTemplate

Jinja2-based email body renderer. Separates template content from sending logic.

Python
from fluxmail import EmailTemplate

EmailTemplate(template, autoescape=False)
Parameter Type Default Description
template str Jinja2 template string
autoescape bool False HTML-escape all variables. Use True for HTML emails.

render()

Python
render(**context) -> str

Returns the rendered string with all {{ variable }} placeholders replaced.

Python
from fluxmail import EmailTemplate

tmpl = EmailTemplate("Hello {{ name }}, order #{{ order_id }} has shipped.")
body = tmpl.render(name="Alice", order_id=12345)
# "Hello Alice, order #12345 has shipped."
Python
tmpl = EmailTemplate(
    "<h1>Hi {{ name }}</h1><p>{{ message }}</p>",
    autoescape=True,
)
body = tmpl.render(name="Bob", message="<script>bad</script>")
# "<h1>Hi Bob</h1><p>&lt;script&gt;bad&lt;/script&gt;</p>"

from_file()

Python
@classmethod
from_file(path, autoescape=False) -> EmailTemplate

Load a template from a UTF-8 file.

Python
tmpl = EmailTemplate.from_file("templates/welcome.html", autoescape=True)
body = tmpl.render(first_name="Alice", company="Acme")

BulkSender

Sends a batch of emails over a single persistent SMTP connection. Significantly faster than reconnecting for each message.

Python
from fluxmail import FluxMail, BulkSender

BulkSender(mailer)
Parameter Type Description
mailer FluxMail Configured FluxMail instance (SMTP only)

send_batch()

Python
send_batch(
    messages,
    *,
    on_success=None,
    on_error=None,
    progress=True,
) -> dict
Parameter Type Default Description
messages list[dict] Each dict is unpacked as **kwargs into FluxMail.create()
on_success Callable[[int, str], None] \| None None Called with (index, result_string) after each successful send
on_error Callable[[int, FluxMailException], None] \| None None Called with (index, exception) after each failed send
progress bool True Show a Rich progress bar in the terminal

Returns a dict with keys:

Key Type Description
sent int Number of messages sent successfully
failed int Number of messages that raised FluxMailException
total int Total messages in the batch
errors list[tuple[int, FluxMailException]] (index, exception) for each failure
Python
import os
from fluxmail import FluxMail, BulkSender

mailer = FluxMail(
    object_type="smtp",
    host="smtp.gmail.com",
    port=587,
    use_tls=True,
    username=os.environ["FLUXMAIL_USERNAME"],
    password=os.environ["FLUXMAIL_PASSWORD"],
)

messages = [
    {"subject": f"Hi {name}", "recipients": [email], "body": f"Hello {name}!"}
    for name, email in [
        ("Alice", "alice@example.com"),
        ("Bob",   "bob@example.com"),
    ]
]

result = BulkSender(mailer).send_batch(messages)
print(result["sent"], "sent,", result["failed"], "failed")
Python
from fluxmail import BulkSender, FluxMailException

def on_success(i: int, result: str) -> None:
    print(f"[{i}] OK")

def on_error(i: int, exc: FluxMailException) -> None:
    print(f"[{i}] FAILED ({exc.code}): {exc}")

result = BulkSender(mailer).send_batch(
    messages,
    on_success=on_success,
    on_error=on_error,
    progress=False,
)

for idx, exc in result["errors"]:
    print(f"  Message {idx}: {exc}")
Python
result = BulkSender(mailer).send_batch(messages, progress=False)

send_batch_async()

Async equivalent of send_batch(). Opens one persistent async SMTP connection for the entire batch using aiosmtplib.

Python
await send_batch_async(
    messages,
    *,
    on_success=None,
    on_error=None,
    progress=True,
    max_per_second=0,
) -> dict
Parameter Type Default Description
messages list[dict] Each dict is unpacked as **kwargs into FluxMail.create()
on_success Callable[[int, str], None] \| None None Called with (index, result_string) after each successful send
on_error Callable[[int, FluxMailException], None] \| None None Called with (index, exception) after each failed send
progress bool True Show a Rich progress bar in the terminal
max_per_second float 0 Maximum sends per second; 0 disables rate limiting. Negative values raise FluxMailException.

Returns the same {"sent", "failed", "total", "errors"} dict as send_batch().

Python
import asyncio
from fluxmail import FluxMail, BulkSender

async def main():
    mailer = FluxMail.from_env()
    result = await BulkSender(mailer).send_batch_async(
        messages, progress=False
    )
    print(result)

asyncio.run(main())
Python
# SendGrid allows ~100 messages/s; SES ~14/s
result = await BulkSender(mailer).send_batch_async(
    messages,
    max_per_second=14,
    progress=False,
)

FluxMailBackend (Django)

Drop-in EMAIL_BACKEND for Django. Reads Django's standard EMAIL_* settings and delegates send to FluxMail's SMTP transport.

Python
# settings.py
EMAIL_BACKEND = "fluxmail.backends.django.FluxMailBackend"
EMAIL_HOST = "smtp.gmail.com"
EMAIL_PORT = 587
EMAIL_HOST_USER = "me@gmail.com"
EMAIL_HOST_PASSWORD = "secret"
EMAIL_USE_TLS = True
EMAIL_TIMEOUT = 30

# Optional
EMAIL_FLUXMAIL_MAX_RETRIES = 3
EMAIL_FLUXMAIL_RETRY_DELAY = 1.0
Django setting FluxMail param Default
EMAIL_HOST host "localhost"
EMAIL_PORT port 25
EMAIL_HOST_USER username None
EMAIL_HOST_PASSWORD password None
EMAIL_USE_TLS use_tls False
EMAIL_USE_SSL use_ssl False
EMAIL_TIMEOUT timeout 30
EMAIL_FLUXMAIL_MAX_RETRIES max_retries 0
EMAIL_FLUXMAIL_RETRY_DELAY retry_delay 1.0

Once configured, all standard Django email functions (send_mail, EmailMessage, EmailMultiAlternatives) route through FluxMail automatically. Django's own MIME builder handles attachments and multipart — no re-implementation.

Thread safety

A single FluxMailBackend instance uses one shared FluxMail object. Do not use the same backend instance concurrently from multiple threads without external synchronisation.


EmailInstance

A namedtuple representing an SMTP relay configuration.

Python
EmailInstance(relay, domain="")
Field Type Default Description
relay str Hostname of the SMTP relay (e.g. smtp.gmail.com)
domain str "" Email domain for the relay (e.g. gmail.com). Optional.
Python
from fluxmail import EmailInstance

host = EmailInstance(relay="smtp.gmail.com")
Python
from fluxmail import EmailInstance

host = EmailInstance(relay="smtp.myrelay.net", domain="mycompany.com")
Python
from fluxmail import FluxMail, EmailInstance

email = FluxMail(
    object_type="smtp",
    host=EmailInstance(relay="smtp.myrelay.net", domain="mycompany.com"),
    port=587,
    use_tls=True,
    username="me@mycompany.com",
)

EmailInstance = namedtuple('EmailInstance', ['relay', 'domain'], defaults=['']) module-attribute

Named tuple representing an SMTP relay host.

PARAMETER DESCRIPTION

relay

SMTP relay hostname (e.g. "smtp.gmail.com").

TYPE: str

domain

Sender domain used for display or routing (e.g. "gmail.com"). Defaults to "".

TYPE: str


Host formats

host on the FluxMail constructor accepts three forms:

Python
# relay hostname only — domain defaults to ""
FluxMail(object_type="smtp", host="smtp.gmail.com", ...)
Python
# CLI-friendly shorthand
FluxMail(object_type="smtp", host="smtp.gmail.com:gmail.com", ...)
Python
from fluxmail import EmailInstance

FluxMail(object_type="smtp", host=EmailInstance(relay="smtp.myrelay.net", domain="mycompany.com"), ...)

EmailObject

EmailObject

Bases: Enum

Supported email object types.

ATTRIBUTE DESCRIPTION
OUTLOOK

Use outlook object to create and send email.

SMTP

Use smtp object to create and send email.

OUTLOOK = 'outlook' class-attribute instance-attribute

Use outlook object to create and send email.

SMTP = 'smtp' class-attribute instance-attribute

Use smtp object to create and send email.

Python
from fluxmail import EmailObject

FluxMail(object_type=EmailObject.SMTP, ...)
FluxMail(object_type=EmailObject.OUTLOOK, ...)

# String aliases also work:
FluxMail(object_type="smtp", ...)
FluxMail(object_type="outlook", ...)

FluxMailException

FluxMailException(message: str, code: Optional[str] = None)

Bases: Exception

Custom exception class for FluxMail errors.

Source code in src/fluxmail/utils.py
Python
def __init__(self, message: str, code: Optional[str] = None) -> None:
    super().__init__(message)
    self.code = code

All errors raised by FluxMail are FluxMailException instances. Every exception carries an optional code attribute for programmatic error handling.

exc.code Raised when
"not_created" send() / send_async() / display() called before create()
"no_relay" SMTP relay hostname is empty
"sender_required" sender= not provided and username is not a valid email
"invalid_config" use_ssl=True + use_tls=True together, or invalid logger
"invalid_params" Empty subject, non-list recipients/cc/bcc/attachments
"invalid_priority" priority value not "high", "normal", or "low"
"invalid_email" Email address fails format validation
"no_email" Empty string passed to validate_email()
"attachment_not_found" Attachment path does not exist
"read_error" Cannot read an attachment file
"send_failed" SMTP send failure (after all retries)
"display_failed" display() raised an unexpected error
"outlook_no_send" send() called on an Outlook instance
"outlook_no_async" send_async() called on an Outlook instance
"outlook_no_sender" sender= set on an Outlook instance
None Any raise without an assigned code

A single try/except covers both create() and send():

Python
from fluxmail import FluxMail, FluxMailException

try:
    email.send()
except FluxMailException as e:
    if e.code == "send_failed":
        print(f"SMTP error: {e}")
    elif e.code == "not_created":
        print("Call create() first")
    else:
        raise
Python
from fluxmail import FluxMail, FluxMailException

try:
    FluxMail(
        object_type="smtp",
        host="smtp.gmail.com",
        port=587,
        use_tls=True,
        username="me@gmail.com",
        password="secret",
    ).create(
        subject="Test",
        recipients=["user@example.com"],
        body="Hello",
    ).send()
except FluxMailException as e:
    print(f"Email failed: {e}")
Python
# SendGrid uses "apikey" as username — not a valid email address.
# Pass sender= explicitly or FluxMailException is raised.
from fluxmail import FluxMail, FluxMailException

try:
    FluxMail(
        object_type="smtp",
        host="smtp.sendgrid.net",
        port=587,
        use_tls=True,
        username="apikey",
        password="SG.xxxx",
        # sender= required here
    ).create(subject="Hi", recipients=["user@example.com"], body="Hello").send()
except FluxMailException as e:
    print(e)  # "sender is required. Pass sender= explicitly..."
Python
from fluxmail import FluxMail, FluxMailException

try:
    email.create(subject="Hi", recipients=["not-an-email"], body="Hello")
except FluxMailException as e:
    print(f"Validation error: {e}")
Python
from fluxmail import FluxMail, EmailInstance, EmailObject, FluxMailException

try:
    FluxMail(
        object_type=EmailObject.OUTLOOK,
        host=EmailInstance(relay=""),
    ).create(subject="Hi", recipients=["user@example.com"], body="Hello").send()
except FluxMailException as e:
    print(e)  # "Outlook is not supported on this platform."

Full examples

Python
import os
from fluxmail import FluxMail

FluxMail(
    object_type="smtp",
    host="smtp.gmail.com",
    port=587,
    use_tls=True,
    username=os.environ["FLUXMAIL_USERNAME"],
    password=os.environ["FLUXMAIL_PASSWORD"],
).create(
    subject="Hello from FluxMail",
    recipients=["friend@example.com"],
    body="Hi there!",
).send()
Python
import os
from fluxmail import FluxMail

FluxMail(
    object_type="smtp",
    host="smtp.sendgrid.net",
    port=587,
    use_tls=True,
    username="apikey",
    password=os.environ["SENDGRID_API_KEY"],
    sender="noreply@myapp.com",
).create(
    subject="Your order has shipped",
    recipients=["customer@example.com"],
    body="<h1>Order Shipped</h1><p>Your order is on its way.</p>",
    html_body=True,
    plain_body="Your order is on its way.",
).send()
Python
import os
from fluxmail import FluxMail

subscribers = ["alice@example.com", "bob@example.com", "carol@example.com"]

with FluxMail(
    object_type="smtp",
    host="smtp.gmail.com",
    port=587,
    use_tls=True,
    username=os.environ["FLUXMAIL_USERNAME"],
    password=os.environ["FLUXMAIL_PASSWORD"],
) as mailer:
    for email_addr in subscribers:
        mailer.create(
            subject="Monthly Newsletter",
            recipients=[email_addr],
            body="<h1>Newsletter</h1>",
            html_body=True,
        ).send()
Python
import os
from fluxmail import FluxMail

email = FluxMail(
    object_type="smtp",
    host="smtp.gmail.com",
    port=587,
    use_tls=True,
    username=os.environ["FLUXMAIL_USERNAME"],
    password=os.environ["FLUXMAIL_PASSWORD"],
)
email.create(
    subject="Monthly Report",
    recipients=["alice@example.com", "bob@example.com"],
    body="<h1>Report</h1><p>See attached.</p>",
    html_body=True,
    plain_body="Monthly report — see attached.",
    sender="reports@myapp.com",
    cc=["manager@example.com"],
    bcc=["archive@example.com"],
    attachments=["/path/to/report.pdf"],
    reply_to="noreply@myapp.com",
    priority="high",
)
email.send()