Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,8 @@ The same situation applies to both `client.batch_send()` and `client.sending_api
- Verifying webhook signatures – [`webhooks/verify_signature.py`](examples/webhooks/verify_signature.py)

### Suppressions API:
- Suppressions (find & delete) – [`suppressions/suppressions.py`](examples/suppressions/suppressions.py)
- Suppressions (create, find & delete) – [`suppressions/suppressions.py`](examples/suppressions/suppressions.py)
- Tracking Opt-outs (list, create & delete) – [`tracking_opt_outs/tracking_opt_outs.py`](examples/tracking_opt_outs/tracking_opt_outs.py)

### Stats API:
- Sending stats – [`stats/stats.py`](examples/stats/stats.py)
Expand Down
1 change: 0 additions & 1 deletion examples/email_campaigns/email_campaigns.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
API_TOKEN = "YOUR_API_TOKEN"
DOMAIN_ID = 4321

# The Email Campaigns API is token-scoped — no `account_id` is needed.
client = mt.MailtrapClient(token=API_TOKEN)
email_campaigns_api = client.email_campaigns_api.email_campaigns

Expand Down
12 changes: 12 additions & 0 deletions examples/suppressions/suppressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,23 @@ def list_suppressions(email: Optional[str] = None) -> list[Suppression]:
return suppressions_api.get_list(email)


def create_suppression() -> Suppression:
params = mt.CreateSuppressionParams(
email="recipient@example.com",
domain_id=12345,
sending_stream="transactional",
)
return suppressions_api.create(params)


def delete_suppression(suppression_id: str) -> Suppression:
return suppressions_api.delete(suppression_id)


if __name__ == "__main__":
created = create_suppression()
print(created)

suppressions = list_suppressions()
print(suppressions)
if suppressions:
Expand Down
64 changes: 64 additions & 0 deletions examples/tracking_opt_outs/tracking_opt_outs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import os

import mailtrap as mt
from mailtrap.models.tracking_opt_outs import TrackingOptOut
from mailtrap.models.tracking_opt_outs import TrackingOptOutsListResponse

API_KEY = os.environ["MAILTRAP_API_KEY"]

client = mt.MailtrapClient(token=API_KEY)
tracking_opt_outs_api = client.tracking_opt_outs_api.tracking_opt_outs


def list_tracking_opt_outs() -> TrackingOptOutsListResponse:
return tracking_opt_outs_api.get_list()


def search_tracking_opt_outs(email: str) -> TrackingOptOutsListResponse:
params = mt.TrackingOptOutsListParams(
email=email,
start_time="2025-01-01T00:00:00Z",
end_time="2025-12-31T23:59:59Z",
)
return tracking_opt_outs_api.get_list(params)


def list_all_tracking_opt_outs() -> list[TrackingOptOut]:
"""Page through the full list, following the `last_id` cursor."""
opt_outs: list[TrackingOptOut] = []
page = tracking_opt_outs_api.get_list()
opt_outs.extend(page.data)

while page.last_id is not None:
page = tracking_opt_outs_api.get_list(
mt.TrackingOptOutsListParams(last_id=page.last_id)
)
opt_outs.extend(page.data)

return opt_outs


def create_tracking_opt_out() -> TrackingOptOut:
params = mt.CreateTrackingOptOutParams(
email="tracked@example.com",
domain_id=12345,
)
return tracking_opt_outs_api.create(params)


def delete_tracking_opt_out(tracking_opt_out_id: str) -> TrackingOptOut:
return tracking_opt_outs_api.delete(tracking_opt_out_id)


if __name__ == "__main__":
created = create_tracking_opt_out()
print(created)

page = list_tracking_opt_outs()
print(page)

print(search_tracking_opt_outs("tracked@example.com"))
print(list_all_tracking_opt_outs())

deleted = delete_tracking_opt_out(created.id)
print(deleted)
3 changes: 3 additions & 0 deletions mailtrap/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,11 @@
from .models.sending_domains import SendSetupInstructionsParams
from .models.sending_domains import UpdateSendingDomainParams
from .models.stats import StatsFilterParams
from .models.suppressions import CreateSuppressionParams
from .models.templates import CreateEmailTemplateParams
from .models.templates import UpdateEmailTemplateParams
from .models.tracking_opt_outs import CreateTrackingOptOutParams
from .models.tracking_opt_outs import TrackingOptOutsListParams
from .models.webhooks import CreateWebhookParams
from .models.webhooks import UpdateWebhookParams
from .webhooks import verify_signature
2 changes: 0 additions & 2 deletions mailtrap/api/resources/email_campaigns.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,6 @@ def _action(self, email_campaign_id: int, action: str) -> EmailCampaign:
return EmailCampaignResponse(**response).data

def _api_path(self, email_campaign_id: Optional[int] = None) -> str:
# Token-scoped: the account is resolved from the API token server-side,
# so the path takes no account id.
path = "/api/email_campaigns"
if email_campaign_id is not None:
return f"{path}/{email_campaign_id}"
Expand Down
10 changes: 10 additions & 0 deletions mailtrap/api/resources/suppressions.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from typing import Optional

from mailtrap.http import HttpClient
from mailtrap.models.suppressions import CreateSuppressionParams
from mailtrap.models.suppressions import Suppression
from mailtrap.models.suppressions import SuppressionResponse


class SuppressionsApi:
Expand All @@ -18,6 +20,14 @@ def get_list(self, email: Optional[str] = None) -> list[Suppression]:
response = self._client.get(self._api_path(), params=params)
return [Suppression(**suppression) for suppression in response]

def create(self, params: CreateSuppressionParams) -> Suppression:
"""
Add an email address to the account's suppression list. `type` defaults
to "manual import" when omitted.
"""
response = self._client.post(self._api_path(), json=params.api_data)
return SuppressionResponse(**response).data

def delete(self, suppression_id: str) -> Suppression:
"""
Delete a suppression by ID. Mailtrap will no longer prevent
Expand Down
47 changes: 47 additions & 0 deletions mailtrap/api/resources/tracking_opt_outs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from typing import Optional

from mailtrap.http import HttpClient
from mailtrap.models.tracking_opt_outs import CreateTrackingOptOutParams
from mailtrap.models.tracking_opt_outs import TrackingOptOut
from mailtrap.models.tracking_opt_outs import TrackingOptOutResponse
from mailtrap.models.tracking_opt_outs import TrackingOptOutsListParams
from mailtrap.models.tracking_opt_outs import TrackingOptOutsListResponse


class TrackingOptOutsApi:
def __init__(self, client: HttpClient) -> None:
self._client = client

def get_list(
self, params: Optional[TrackingOptOutsListParams] = None
) -> TrackingOptOutsListResponse:
"""
List email addresses that have opted out of open and click tracking.
The endpoint returns up to 1000 records per request; pass the previous
response's `last_id` to fetch the next page.
"""
query_params = params.api_query_params if params is not None else None
response = self._client.get(self._api_path(), params=query_params)
return TrackingOptOutsListResponse(**response)

def create(self, params: CreateTrackingOptOutParams) -> TrackingOptOut:
"""
Add an email address to the tracking opt-out list for a sending domain.
"""
response = self._client.post(self._api_path(), json=params.api_data)
return TrackingOptOutResponse(**response).data

def delete(self, tracking_opt_out_id: str) -> TrackingOptOut:
"""
Remove an email address from the tracking opt-out list so open and
click tracking can apply again.
"""
response = self._client.delete(self._api_path(tracking_opt_out_id))
return TrackingOptOut(**response)

@staticmethod
def _api_path(tracking_opt_out_id: Optional[str] = None) -> str:
path = "/api/tracking_opt_outs"
if tracking_opt_out_id is not None:
return f"{path}/{tracking_opt_out_id}"
return path
11 changes: 11 additions & 0 deletions mailtrap/api/tracking_opt_outs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from mailtrap.api.resources.tracking_opt_outs import TrackingOptOutsApi
from mailtrap.http import HttpClient


class TrackingOptOutsBaseApi:
def __init__(self, client: HttpClient) -> None:
self._client = client

@property
def tracking_opt_outs(self) -> TrackingOptOutsApi:
return TrackingOptOutsApi(client=self._client)
9 changes: 7 additions & 2 deletions mailtrap/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from mailtrap.api.suppressions import SuppressionsBaseApi
from mailtrap.api.templates import EmailTemplatesApi
from mailtrap.api.testing import TestingApi
from mailtrap.api.tracking_opt_outs import TrackingOptOutsBaseApi
from mailtrap.api.webhooks import WebhooksBaseApi
from mailtrap.config import BULK_HOST
from mailtrap.config import GENERAL_HOST
Expand Down Expand Up @@ -126,6 +127,12 @@ def sending_domains_api(self) -> SendingDomainsBaseApi:
client=HttpClient(host=GENERAL_HOST, headers=self.headers),
)

@property
def tracking_opt_outs_api(self) -> TrackingOptOutsBaseApi:
return TrackingOptOutsBaseApi(
client=HttpClient(host=GENERAL_HOST, headers=self.headers),
)

@property
def company_info_api(self) -> CompanyInfoBaseApi:
return CompanyInfoBaseApi(
Expand All @@ -134,8 +141,6 @@ def company_info_api(self) -> CompanyInfoBaseApi:

@property
def email_campaigns_api(self) -> EmailCampaignsBaseApi:
# Token-scoped (`/api/email_campaigns`) — the account is resolved
# server-side from the token, so no `account_id` is required.
return EmailCampaignsBaseApi(
client=HttpClient(host=GENERAL_HOST, headers=self.headers),
)
Expand Down
15 changes: 15 additions & 0 deletions mailtrap/models/suppressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

from pydantic.dataclasses import dataclass

from mailtrap.models.common import RequestParams


@dataclass
class Suppression:
Expand All @@ -23,3 +25,16 @@ class Suppression:
message_recipient_mx_name: Optional[str] = None
message_sender_email: Optional[str] = None
message_subject: Optional[str] = None


@dataclass
class SuppressionResponse:
data: Suppression


@dataclass
class CreateSuppressionParams(RequestParams):
email: str
domain_id: int
sending_stream: str
type: Optional[str] = None
43 changes: 43 additions & 0 deletions mailtrap/models/tracking_opt_outs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from datetime import datetime
from typing import Optional

from pydantic import Field
from pydantic.dataclasses import dataclass

from mailtrap.models.common import RequestParams


@dataclass
class TrackingOptOut:
id: str
email: str
created_at: datetime
domain_name: Optional[str] = None


@dataclass
class TrackingOptOutResponse:
data: TrackingOptOut


@dataclass
class TrackingOptOutsListResponse:
"""A page of tracking opt-outs. `last_id` is the cursor for the next page,
or `None` when there are no more pages."""

data: list[TrackingOptOut] = Field(default_factory=list)
last_id: Optional[str] = None


@dataclass
class TrackingOptOutsListParams(RequestParams):
email: Optional[str] = None
start_time: Optional[str] = None
end_time: Optional[str] = None
last_id: Optional[str] = None


@dataclass
class CreateTrackingOptOutParams(RequestParams):
email: str
domain_id: int
Loading
Loading