Skip to content

Client API

The MisfinClient is the high-level async client for sending Misfin messages.

MisfinClient

MisfinClient

MisfinClient(
    timeout: float = REQUEST_TIMEOUT,
    port: int = DEFAULT_PORT,
    ssl_context: SSLContext | None = None,
    trust_on_first_use: bool = True,
    tofu_db_path: Path | None = None,
    client_cert: Path | str | None = None,
    client_key: Path | str | None = None,
    max_retries: int = 3,
    retry_base_delay: float = 1.0,
    misfin_b_fallback: bool = True,
)
Source code in src/titlani/client/session.py
def __init__(
    self,
    timeout: float = REQUEST_TIMEOUT,
    port: int = DEFAULT_PORT,
    ssl_context: ssl.SSLContext | None = None,
    trust_on_first_use: bool = True,
    tofu_db_path: Path | None = None,
    client_cert: Path | str | None = None,
    client_key: Path | str | None = None,
    max_retries: int = 3,
    retry_base_delay: float = 1.0,
    misfin_b_fallback: bool = True,
) -> None:
    self.timeout = timeout
    self.port = port
    self.trust_on_first_use = trust_on_first_use
    self.max_retries = max_retries
    self.retry_base_delay = retry_base_delay
    self.misfin_b_fallback = misfin_b_fallback

    if client_cert and not client_key:
        raise ValueError("client_key is required when client_cert is provided")
    if client_key and not client_cert:
        raise ValueError("client_cert is required when client_key is provided")

    if self.trust_on_first_use:
        self.tofu_db: TOFUDatabase | None = TOFUDatabase(tofu_db_path)
    else:
        self.tofu_db = None

    if ssl_context is None:
        self.ssl_context = create_client_context(
            verify_mode=ssl.CERT_NONE,
            check_hostname=False,
            certfile=(str(client_cert) if client_cert else None),
            keyfile=(str(client_key) if client_key else None),
        )
    else:
        self.ssl_context = ssl_context

send async

send(
    to: str,
    body: str,
    subject: str | None = None,
    sender: MisfinAddress | None = None,
    recipients: list[MisfinAddress] | None = None,
) -> MisfinResponse

Send a message to a Misfin address.

Parameters:

Name Type Description Default
to str

Recipient address in "mailbox@hostname" format.

required
body str

Message body in gemtext format.

required
subject str | None

Optional subject (prepended as # heading).

None
sender MisfinAddress | None

Optional sender address.

None
recipients list[MisfinAddress] | None

Optional list of recipient addresses.

None
Source code in src/titlani/client/session.py
async def send(
    self,
    to: str,
    body: str,
    subject: str | None = None,
    sender: MisfinAddress | None = None,
    recipients: list[MisfinAddress] | None = None,
) -> MisfinResponse:
    """Send a message to a Misfin address.

    Args:
        to: Recipient address in "mailbox@hostname" format.
        body: Message body in gemtext format.
        subject: Optional subject (prepended as # heading).
        sender: Optional sender address.
        recipients: Optional list of recipient addresses.
    """
    if "@" not in to:
        raise ValueError(f"Invalid address: {to!r}")

    mailbox, hostname = to.rsplit("@", 1)
    recipient = MisfinAddress(mailbox=mailbox, hostname=hostname)

    # Build message body
    full_body = ""
    if subject:
        full_body = f"# {subject}\n\n"
    full_body += body
    if not full_body.endswith("\n"):
        full_body += "\n"

    message = GemmailMessage(
        senders=[sender] if sender else [],
        recipients=recipients or [recipient],
        timestamps=[datetime.now(UTC)],
        body=full_body,
    )

    logger.debug("client_send", to=to)
    message_bytes = message.to_bytes()
    return await self.send_raw(to, message_bytes)

send_raw async

send_raw(
    to: str, message_bytes: bytes, _redirect_depth: int = 0
) -> MisfinResponse

Send pre-formatted message bytes.

Source code in src/titlani/client/session.py
async def send_raw(
    self,
    to: str,
    message_bytes: bytes,
    _redirect_depth: int = 0,
) -> MisfinResponse:
    """Send pre-formatted message bytes."""
    if "@" not in to:
        raise ValueError(f"Invalid address: {to!r}")

    mailbox, hostname = to.rsplit("@", 1)

    request = MisfinRequest(
        mailbox=mailbox,
        hostname=hostname,
        content_length=len(message_bytes),
        raw_message=message_bytes,
    )

    return await self._send_request(request, hostname, _redirect_depth)