Skip to content
Draft
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 @@ -249,7 +249,8 @@ The same situation applies to both `client.batch_send()` and `client.sending_api
- Folders management – [`inbound/folders.py`](examples/inbound/folders.py)
- Inboxes management – [`inbound/inboxes.py`](examples/inbound/inboxes.py)
- Messages (list/get/delete + reply/reply_all/forward) – [`inbound/messages.py`](examples/inbound/messages.py)
- Threads (list/get/delete) – [`inbound/threads.py`](examples/inbound/threads.py)
- Threads (list/search/get/delete) – [`inbound/threads.py`](examples/inbound/threads.py)
- Forward rules management – [`inbound/forward_rules.py`](examples/inbound/forward_rules.py)

### Email Campaigns API:
- Email Campaigns (list, create, get, update, delete, lifecycle actions, stats) – [`email_campaigns/email_campaigns.py`](examples/email_campaigns/email_campaigns.py)
Expand Down
63 changes: 63 additions & 0 deletions examples/inbound/forward_rules.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import os

import mailtrap as mt
from mailtrap.models.common import DeletedObject
from mailtrap.models.inbound import InboundForwardRule

API_KEY = os.environ["MAILTRAP_API_KEY"]
INBOX_ID = int(os.environ["MAILTRAP_INBOUND_INBOX_ID"])

client = mt.MailtrapClient(token=API_KEY)
forward_rules_api = client.inbound_api.forward_rules


def list_forward_rules(inbox_id: int) -> list[InboundForwardRule]:
return forward_rules_api.get_list(inbox_id)


def get_forward_rule(inbox_id: int, forward_rule_id: int) -> InboundForwardRule:
return forward_rules_api.get_by_id(inbox_id, forward_rule_id)


def create_forward_rule(inbox_id: int) -> InboundForwardRule:
return forward_rules_api.create(
inbox_id,
mt.CreateInboundForwardRuleParams(
name="Copy billing mail to finance",
conditions=[
mt.InboundForwardRuleConditionParams(
match_type="sender",
operator="ends_with",
value="@billing.example.com",
),
],
destinations=[mt.InboundForwardRuleDestination(email="[email protected]")],
),
)


def update_forward_rule(inbox_id: int, forward_rule_id: int) -> InboundForwardRule:
return forward_rules_api.update(
inbox_id,
forward_rule_id,
mt.UpdateInboundForwardRuleParams(
destinations=[
mt.InboundForwardRuleDestination(email="[email protected]"),
mt.InboundForwardRuleDestination(email="[email protected]"),
],
),
)


def delete_forward_rule(inbox_id: int, forward_rule_id: int) -> DeletedObject:
return forward_rules_api.delete(inbox_id, forward_rule_id)


if __name__ == "__main__":
rule = create_forward_rule(INBOX_ID)
print(rule)

print(list_forward_rules(INBOX_ID))
print(get_forward_rule(INBOX_ID, rule.id))
print(update_forward_rule(INBOX_ID, rule.id))
print(delete_forward_rule(INBOX_ID, rule.id))
5 changes: 5 additions & 0 deletions examples/inbound/threads.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ def list_threads(inbox_id: int) -> InboundThreadsListResponse:
return threads_api.get_list(inbox_id)


def search_threads(inbox_id: int, search: str) -> InboundThreadsListResponse:
return threads_api.get_list(inbox_id, search=search)


def get_thread(inbox_id: int, thread_id: str) -> InboundThread:
return threads_api.get_by_id(inbox_id, thread_id)

Expand All @@ -28,6 +32,7 @@ def delete_thread(inbox_id: int, thread_id: str) -> DeletedObject:
if __name__ == "__main__":
page = list_threads(INBOX_ID)
print(f"{len(page.data)} of {page.total_count} threads")
print(search_threads(INBOX_ID, "invoice"))

if page.data:
thread_id = page.data[0].id
Expand Down
4 changes: 4 additions & 0 deletions mailtrap/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,14 @@
from .models.email_logs import EmailLogsListFilters
from .models.email_logs import EmailLogsListResponse
from .models.inbound import CreateInboundFolderParams
from .models.inbound import CreateInboundForwardRuleParams
from .models.inbound import CreateInboundInboxParams
from .models.inbound import ForwardInboundMessageParams
from .models.inbound import InboundForwardRuleConditionParams
from .models.inbound import InboundForwardRuleDestination
from .models.inbound import ReplyInboundMessageParams
from .models.inbound import UpdateInboundFolderParams
from .models.inbound import UpdateInboundForwardRuleParams
from .models.inbound import UpdateInboundInboxParams
from .models.inboxes import CreateInboxParams
from .models.inboxes import UpdateInboxParams
Expand Down
5 changes: 5 additions & 0 deletions mailtrap/api/inbound.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from mailtrap.api.resources.inbound_folders import InboundFoldersApi
from mailtrap.api.resources.inbound_forward_rules import InboundForwardRulesApi
from mailtrap.api.resources.inbound_inboxes import InboundInboxesApi
from mailtrap.api.resources.inbound_messages import InboundMessagesApi
from mailtrap.api.resources.inbound_threads import InboundThreadsApi
Expand All @@ -24,3 +25,7 @@ def messages(self) -> InboundMessagesApi:
@property
def threads(self) -> InboundThreadsApi:
return InboundThreadsApi(client=self._client)

@property
def forward_rules(self) -> InboundForwardRulesApi:
return InboundForwardRulesApi(client=self._client)
54 changes: 54 additions & 0 deletions mailtrap/api/resources/inbound_forward_rules.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from typing import Optional

from mailtrap.http import HttpClient
from mailtrap.models.common import DeletedObject
from mailtrap.models.inbound import CreateInboundForwardRuleParams
from mailtrap.models.inbound import InboundForwardRule
from mailtrap.models.inbound import InboundForwardRuleResponse
from mailtrap.models.inbound import InboundForwardRulesListResponse
from mailtrap.models.inbound import UpdateInboundForwardRuleParams


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

def get_list(self, inbox_id: int) -> list[InboundForwardRule]:
"""Get all forward rules of an inbox."""
response = self._client.get(self._api_path(inbox_id))
return InboundForwardRulesListResponse(**response).data

def get_by_id(self, inbox_id: int, forward_rule_id: int) -> InboundForwardRule:
"""Get a forward rule by ID."""
response = self._client.get(self._api_path(inbox_id, forward_rule_id))
return InboundForwardRuleResponse(**response).data

def create(
self, inbox_id: int, params: CreateInboundForwardRuleParams
) -> InboundForwardRule:
"""Create a forward rule on an inbox."""
response = self._client.post(self._api_path(inbox_id), json=params.api_data)
return InboundForwardRuleResponse(**response).data

def update(
self,
inbox_id: int,
forward_rule_id: int,
params: UpdateInboundForwardRuleParams,
) -> InboundForwardRule:
"""Update a forward rule by ID."""
response = self._client.patch(
self._api_path(inbox_id, forward_rule_id), json=params.api_data
)
return InboundForwardRuleResponse(**response).data

def delete(self, inbox_id: int, forward_rule_id: int) -> DeletedObject:
"""Delete a forward rule by ID."""
self._client.delete(self._api_path(inbox_id, forward_rule_id))
return DeletedObject(forward_rule_id)

def _api_path(self, inbox_id: int, forward_rule_id: Optional[int] = None) -> str:
path = f"/api/inbound/inboxes/{inbox_id}/forward_rules"
if forward_rule_id:
return f"{path}/{forward_rule_id}"
return path
7 changes: 6 additions & 1 deletion mailtrap/api/resources/inbound_threads.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ def __init__(self, client: HttpClient) -> None:
self._client = client

def get_list(
self, inbox_id: int, last_id: Optional[str] = None
self,
inbox_id: int,
last_id: Optional[str] = None,
search: Optional[str] = None,
) -> InboundThreadsListResponse:
"""
List conversation threads in an inbox. Pass last_id from a previous
Expand All @@ -21,6 +24,8 @@ def get_list(
params: dict[str, Any] = {}
if last_id:
params["last_id"] = last_id
if search:
params["search"] = search
response = self._client.get(self._api_path(inbox_id), params=params or None)
return InboundThreadsListResponse(**response)

Expand Down
93 changes: 90 additions & 3 deletions mailtrap/models/inbound.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
from mailtrap.models.mail.attachment import Attachment

ContentDisposition = Literal["attachment", "inline"]
ForwardRuleMatchType = Literal["sender", "recipient", "header"]
ForwardRuleOperator = Literal[
"equal", "not_equal", "contains", "starts_with", "ends_with", "empty", "not_empty"
]

# --- Attachments ---

Expand Down Expand Up @@ -55,6 +59,21 @@ class InboundInbox:
domain_id: int


# --- Forward outcomes ---


@dataclass
class InboundForwardOutcome:
"""Forward-rule outcome for one destination of a received message."""

rule_id: int
destination: str
status: Literal["forwarded", "rejected"]
rule_name: Optional[str] = None
reason: Optional[str] = None
message_id: Optional[str] = None


# --- Messages ---


Expand All @@ -81,6 +100,7 @@ class InboundMessage(BaseModel):
received_at: str
thread_id: Optional[str] = None
attachments: list[InboundAttachment] = Field(default_factory=list)
forwards: list[InboundForwardOutcome] = Field(default_factory=list)


class InboundMessageDetails(InboundMessage):
Expand Down Expand Up @@ -121,6 +141,16 @@ class InboundThreadSummary(BaseModel):
attachments: list[InboundAttachment] = Field(default_factory=list)


@dataclass
class InboundThreadMessageDelivery:
"""Delivery outcome of a sent (outbound) thread message."""

to: str
status: Literal["delivered", "not_delivered", "enqueued", "opted_out"]
delivered_at: Optional[str] = None
bounced_at: Optional[str] = None


class InboundThreadMessage(BaseModel):
"""
A message inside a thread. Only visibility_status and direction are
Expand All @@ -147,9 +177,8 @@ class InboundThreadMessage(BaseModel):
text_body: Optional[str] = None
html_body: Optional[str] = None
attachments: Optional[list[InboundAttachment]] = None
delivery_status: Optional[str] = None
delivered_at: Optional[str] = None
bounced_at: Optional[str] = None
delivery: Optional[InboundThreadMessageDelivery] = None
forwards: Optional[list[InboundForwardOutcome]] = None


class InboundThread(InboundThreadSummary):
Expand All @@ -174,6 +203,42 @@ class InboundSendResult:
message_ids: list[str] = Field(default_factory=list)


# --- Forward rules ---


@dataclass
class InboundForwardRuleCondition:
match_type: ForwardRuleMatchType
operator: ForwardRuleOperator
value: Optional[str] = None
header_key: Optional[str] = None


@dataclass
class InboundForwardRuleDestination:
email: str


@dataclass
class InboundForwardRule:
id: int
name: str
created_at: str
updated_at: str
conditions: list[InboundForwardRuleCondition] = Field(default_factory=list)
destinations: list[InboundForwardRuleDestination] = Field(default_factory=list)


@dataclass
class InboundForwardRuleResponse:
data: InboundForwardRule


@dataclass
class InboundForwardRulesListResponse:
data: list[InboundForwardRule] = Field(default_factory=list)


# --- Request params ---


Expand Down Expand Up @@ -235,3 +300,25 @@ class ForwardInboundMessageParams(_InboundMessageParams):
def __post_init__(self) -> None:
if not self.to:
raise ValueError("`to` must contain at least one recipient for forward")


@dataclass
class InboundForwardRuleConditionParams(RequestParams):
match_type: ForwardRuleMatchType
operator: ForwardRuleOperator
value: Optional[str] = None
header_key: Optional[str] = None


@dataclass
class CreateInboundForwardRuleParams(RequestParams):
name: str
conditions: Optional[list[InboundForwardRuleConditionParams]] = None
destinations: Optional[list[InboundForwardRuleDestination]] = None


@dataclass
class UpdateInboundForwardRuleParams(RequestParams):
name: Optional[str] = None
conditions: Optional[list[InboundForwardRuleConditionParams]] = None
destinations: Optional[list[InboundForwardRuleDestination]] = None
Loading
Loading