fastsmtp-cli 0.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastsmtp_cli/__init__.py +3 -0
- fastsmtp_cli/client.py +681 -0
- fastsmtp_cli/commands/__init__.py +13 -0
- fastsmtp_cli/commands/auth.py +130 -0
- fastsmtp_cli/commands/config.py +136 -0
- fastsmtp_cli/commands/domains.py +365 -0
- fastsmtp_cli/commands/operations.py +174 -0
- fastsmtp_cli/commands/options.py +84 -0
- fastsmtp_cli/commands/recipients.py +223 -0
- fastsmtp_cli/commands/rules.py +469 -0
- fastsmtp_cli/commands/users.py +178 -0
- fastsmtp_cli/config.py +218 -0
- fastsmtp_cli/main.py +59 -0
- fastsmtp_cli/output.py +700 -0
- fastsmtp_cli-0.6.0.dist-info/METADATA +78 -0
- fastsmtp_cli-0.6.0.dist-info/RECORD +19 -0
- fastsmtp_cli-0.6.0.dist-info/WHEEL +4 -0
- fastsmtp_cli-0.6.0.dist-info/entry_points.txt +2 -0
- fastsmtp_cli-0.6.0.dist-info/licenses/LICENSE +661 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""Operations commands (health, logs, test webhook)."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from fastsmtp_cli.client import APIError, FastSMTPClient
|
|
8
|
+
from fastsmtp_cli.commands.options import IncludeDeletedLogs
|
|
9
|
+
from fastsmtp_cli.output import (
|
|
10
|
+
print_delivery_log,
|
|
11
|
+
print_delivery_logs_table,
|
|
12
|
+
print_error,
|
|
13
|
+
print_health,
|
|
14
|
+
print_ready,
|
|
15
|
+
print_success,
|
|
16
|
+
print_test_webhook_result,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
app = typer.Typer(help="Operations (health, logs, test)")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@app.command("health")
|
|
23
|
+
def health(
|
|
24
|
+
profile: Annotated[
|
|
25
|
+
str | None,
|
|
26
|
+
typer.Option("--profile", "-p", help="Profile to use"),
|
|
27
|
+
] = None,
|
|
28
|
+
) -> None:
|
|
29
|
+
"""Check server health."""
|
|
30
|
+
try:
|
|
31
|
+
with FastSMTPClient(profile_name=profile) as client:
|
|
32
|
+
result = client.health()
|
|
33
|
+
print_health(result)
|
|
34
|
+
except APIError as e:
|
|
35
|
+
print_error(e.detail)
|
|
36
|
+
raise typer.Exit(1) from e
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@app.command("ready")
|
|
40
|
+
def ready(
|
|
41
|
+
profile: Annotated[
|
|
42
|
+
str | None,
|
|
43
|
+
typer.Option("--profile", "-p", help="Profile to use"),
|
|
44
|
+
] = None,
|
|
45
|
+
) -> None:
|
|
46
|
+
"""Check server readiness (includes database check)."""
|
|
47
|
+
try:
|
|
48
|
+
with FastSMTPClient(profile_name=profile) as client:
|
|
49
|
+
result = client.ready()
|
|
50
|
+
print_ready(result)
|
|
51
|
+
except APIError as e:
|
|
52
|
+
print_error(e.detail)
|
|
53
|
+
raise typer.Exit(1) from e
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# Delivery log commands
|
|
57
|
+
log_app = typer.Typer(help="Delivery log management")
|
|
58
|
+
app.add_typer(log_app, name="log")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@log_app.command("list")
|
|
62
|
+
def list_logs(
|
|
63
|
+
domain_id: Annotated[str, typer.Argument(help="Domain ID")],
|
|
64
|
+
status: Annotated[
|
|
65
|
+
str | None,
|
|
66
|
+
typer.Option("--status", "-s", help="Filter by status"),
|
|
67
|
+
] = None,
|
|
68
|
+
message_id: Annotated[
|
|
69
|
+
str | None,
|
|
70
|
+
typer.Option("--message-id", "-m", help="Filter by message ID"),
|
|
71
|
+
] = None,
|
|
72
|
+
limit: Annotated[int, typer.Option("--limit", "-l", help="Max results")] = 50,
|
|
73
|
+
offset: Annotated[int, typer.Option("--offset", "-o", help="Offset")] = 0,
|
|
74
|
+
include_deleted: IncludeDeletedLogs = False,
|
|
75
|
+
profile: Annotated[
|
|
76
|
+
str | None,
|
|
77
|
+
typer.Option("--profile", "-p", help="Profile to use"),
|
|
78
|
+
] = None,
|
|
79
|
+
) -> None:
|
|
80
|
+
"""List delivery logs for a domain (--include-deleted reads a deleted domain's history)."""
|
|
81
|
+
try:
|
|
82
|
+
with FastSMTPClient(profile_name=profile) as client:
|
|
83
|
+
logs = client.list_delivery_logs(
|
|
84
|
+
domain_id,
|
|
85
|
+
status=status,
|
|
86
|
+
message_id=message_id,
|
|
87
|
+
limit=limit,
|
|
88
|
+
offset=offset,
|
|
89
|
+
include_deleted=include_deleted,
|
|
90
|
+
)
|
|
91
|
+
if not logs:
|
|
92
|
+
print_error("No delivery logs found")
|
|
93
|
+
return
|
|
94
|
+
print_delivery_logs_table(logs)
|
|
95
|
+
except APIError as e:
|
|
96
|
+
print_error(e.detail)
|
|
97
|
+
raise typer.Exit(1) from e
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@log_app.command("get")
|
|
101
|
+
def get_log(
|
|
102
|
+
log_id: Annotated[str, typer.Argument(help="Delivery log ID")],
|
|
103
|
+
profile: Annotated[
|
|
104
|
+
str | None,
|
|
105
|
+
typer.Option("--profile", "-p", help="Profile to use"),
|
|
106
|
+
] = None,
|
|
107
|
+
) -> None:
|
|
108
|
+
"""Get delivery log details with payload."""
|
|
109
|
+
try:
|
|
110
|
+
with FastSMTPClient(profile_name=profile) as client:
|
|
111
|
+
log = client.get_delivery_log(log_id)
|
|
112
|
+
print_delivery_log(log)
|
|
113
|
+
except APIError as e:
|
|
114
|
+
print_error(e.detail)
|
|
115
|
+
raise typer.Exit(1) from e
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@log_app.command("retry")
|
|
119
|
+
def retry_log(
|
|
120
|
+
log_id: Annotated[str, typer.Argument(help="Delivery log ID")],
|
|
121
|
+
profile: Annotated[
|
|
122
|
+
str | None,
|
|
123
|
+
typer.Option("--profile", "-p", help="Profile to use"),
|
|
124
|
+
] = None,
|
|
125
|
+
) -> None:
|
|
126
|
+
"""Retry a failed, exhausted or cancelled delivery."""
|
|
127
|
+
try:
|
|
128
|
+
with FastSMTPClient(profile_name=profile) as client:
|
|
129
|
+
result = client.retry_delivery(log_id)
|
|
130
|
+
print_success(result.get("message", "Delivery queued for retry"))
|
|
131
|
+
except APIError as e:
|
|
132
|
+
print_error(e.detail)
|
|
133
|
+
raise typer.Exit(1) from e
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# Test webhook command
|
|
137
|
+
@app.command("test-webhook")
|
|
138
|
+
def test_webhook(
|
|
139
|
+
webhook_url: Annotated[str, typer.Argument(help="Webhook URL to test")],
|
|
140
|
+
from_addr: Annotated[
|
|
141
|
+
str,
|
|
142
|
+
typer.Option("--from", "-f", help="From address"),
|
|
143
|
+
] = "test@example.com",
|
|
144
|
+
to_addr: Annotated[
|
|
145
|
+
str,
|
|
146
|
+
typer.Option("--to", "-t", help="To address"),
|
|
147
|
+
] = "recipient@example.com",
|
|
148
|
+
subject: Annotated[
|
|
149
|
+
str,
|
|
150
|
+
typer.Option("--subject", "-s", help="Subject"),
|
|
151
|
+
] = "Test Email",
|
|
152
|
+
body: Annotated[
|
|
153
|
+
str,
|
|
154
|
+
typer.Option("--body", "-b", help="Body text"),
|
|
155
|
+
] = "This is a test email from FastSMTP.",
|
|
156
|
+
profile: Annotated[
|
|
157
|
+
str | None,
|
|
158
|
+
typer.Option("--profile", "-p", help="Profile to use"),
|
|
159
|
+
] = None,
|
|
160
|
+
) -> None:
|
|
161
|
+
"""Test a webhook URL by sending a test payload."""
|
|
162
|
+
try:
|
|
163
|
+
with FastSMTPClient(profile_name=profile) as client:
|
|
164
|
+
result = client.test_webhook(
|
|
165
|
+
webhook_url=webhook_url,
|
|
166
|
+
from_address=from_addr,
|
|
167
|
+
to_address=to_addr,
|
|
168
|
+
subject=subject,
|
|
169
|
+
body=body,
|
|
170
|
+
)
|
|
171
|
+
print_test_webhook_result(result)
|
|
172
|
+
except APIError as e:
|
|
173
|
+
print_error(e.detail)
|
|
174
|
+
raise typer.Exit(1) from e
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Shared option aliases and helpers for the command modules."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from fastsmtp_cli.client import UNSET, NullableStr
|
|
8
|
+
|
|
9
|
+
#: ``--include-deleted`` on every list/get that can show tombstones. The server
|
|
10
|
+
#: gates the flag at the delete role for that resource: below it a caller with
|
|
11
|
+
#: no access at all gets the same 403 whether or not a tombstone exists, and a
|
|
12
|
+
#: member of the domain gets a 404 once it is deleted.
|
|
13
|
+
IncludeDeleted = Annotated[
|
|
14
|
+
bool,
|
|
15
|
+
typer.Option("--include-deleted", help="Also show deleted (restorable) entries"),
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
#: The same flag on ``auth keys``: keys are never restorable, and the listing
|
|
19
|
+
#: also covers keys retired before soft delete existed.
|
|
20
|
+
IncludeDeletedKeys = Annotated[
|
|
21
|
+
bool,
|
|
22
|
+
typer.Option(
|
|
23
|
+
"--include-deleted", help="Also show deleted and retired keys (keys cannot be restored)"
|
|
24
|
+
),
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
#: The same flag on ``ops log list``: log rows are never deleted, so the flag
|
|
28
|
+
#: only resolves a deleted domain (owner or superuser) to read its history.
|
|
29
|
+
IncludeDeletedLogs = Annotated[
|
|
30
|
+
bool,
|
|
31
|
+
typer.Option(
|
|
32
|
+
"--include-deleted",
|
|
33
|
+
help="Read the history of a deleted domain (owner or superuser); log rows themselves "
|
|
34
|
+
"are never deleted",
|
|
35
|
+
),
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
#: ``--purge`` on every delete. A purge is a second step: it only works on a row
|
|
39
|
+
#: that is already deleted (the server answers 409 otherwise), and it runs the
|
|
40
|
+
#: hard-delete cascades that soft delete exists to avoid.
|
|
41
|
+
Purge = Annotated[
|
|
42
|
+
bool,
|
|
43
|
+
typer.Option(
|
|
44
|
+
"--purge",
|
|
45
|
+
help="Permanently delete an already-deleted entry (superuser only, cannot be undone)",
|
|
46
|
+
),
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def confirm_delete(kind: str, label: str, *, force: bool, purge: bool) -> None:
|
|
51
|
+
"""Ask before a delete unless ``--force``; exit 0 when the operator declines.
|
|
52
|
+
|
|
53
|
+
Shared by the user, domain and recipient ``delete`` commands. A soft
|
|
54
|
+
delete keeps the pre-0.5.0 prompt; a purge gets its own, spelling out that
|
|
55
|
+
it cannot be undone.
|
|
56
|
+
"""
|
|
57
|
+
if force:
|
|
58
|
+
return
|
|
59
|
+
if purge:
|
|
60
|
+
prompt = f"Permanently delete {kind} {label}? This cannot be undone."
|
|
61
|
+
else:
|
|
62
|
+
prompt = f"Delete {kind} {label}?"
|
|
63
|
+
if not typer.confirm(prompt):
|
|
64
|
+
raise typer.Exit(0)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def clearable_str(option: str | None) -> NullableStr:
|
|
68
|
+
"""Map a nullable-string option onto the client's tri-state value.
|
|
69
|
+
|
|
70
|
+
CLI-wide convention for options backed by a nullable string column: leaving
|
|
71
|
+
the option off (``None``) leaves the column untouched, passing an empty
|
|
72
|
+
string (``--option ''``) clears it (the client sends an explicit JSON
|
|
73
|
+
null), and any other value sets it.
|
|
74
|
+
|
|
75
|
+
This is the string counterpart of the ``true``/``false``/``inherit``
|
|
76
|
+
tri-state used for nullable boolean flags (see ``commands/domains.py``);
|
|
77
|
+
strings need no enum because the empty string is not an otherwise
|
|
78
|
+
meaningful value for these columns.
|
|
79
|
+
"""
|
|
80
|
+
if option is None:
|
|
81
|
+
return UNSET
|
|
82
|
+
if option == "":
|
|
83
|
+
return None
|
|
84
|
+
return option
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""Recipient management commands.
|
|
2
|
+
|
|
3
|
+
Recipients are nested under a domain on the server, so every command that
|
|
4
|
+
addresses a single recipient takes the domain ID as well as the recipient ID.
|
|
5
|
+
|
|
6
|
+
``delete`` is a soft delete: the address stops receiving mail and its queued
|
|
7
|
+
deliveries are cancelled, but ``restore`` brings it back (cancelled deliveries
|
|
8
|
+
stay cancelled; re-queue them with ``fsmtp ops log retry``). ``delete
|
|
9
|
+
--purge`` removes an already-deleted recipient for good.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import Annotated
|
|
13
|
+
|
|
14
|
+
import typer
|
|
15
|
+
|
|
16
|
+
from fastsmtp_cli.client import APIError, FastSMTPClient
|
|
17
|
+
from fastsmtp_cli.commands.options import IncludeDeleted, Purge, clearable_str, confirm_delete
|
|
18
|
+
from fastsmtp_cli.output import (
|
|
19
|
+
print_deleted,
|
|
20
|
+
print_error,
|
|
21
|
+
print_recipient,
|
|
22
|
+
print_recipients_table,
|
|
23
|
+
print_success,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
app = typer.Typer(help="Recipient management")
|
|
27
|
+
|
|
28
|
+
HeaderOption = Annotated[
|
|
29
|
+
list[str] | None,
|
|
30
|
+
typer.Option("--header", "-H", help="Webhook header as KEY=VALUE (can be repeated)"),
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def parse_headers(headers: list[str] | None) -> dict[str, str] | None:
|
|
35
|
+
"""Parse repeated ``KEY=VALUE`` options into a headers mapping.
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
typer.Exit: with status 1 if an entry is not ``KEY=VALUE``
|
|
39
|
+
"""
|
|
40
|
+
if not headers:
|
|
41
|
+
return None
|
|
42
|
+
parsed: dict[str, str] = {}
|
|
43
|
+
for entry in headers:
|
|
44
|
+
key, separator, value = entry.partition("=")
|
|
45
|
+
if not separator or not key.strip():
|
|
46
|
+
print_error(f"Invalid header '{entry}'. Use KEY=VALUE.")
|
|
47
|
+
raise typer.Exit(1)
|
|
48
|
+
parsed[key.strip()] = value.strip()
|
|
49
|
+
return parsed
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@app.command("list")
|
|
53
|
+
def list_recipients(
|
|
54
|
+
domain_id: Annotated[str, typer.Argument(help="Domain ID")],
|
|
55
|
+
include_deleted: IncludeDeleted = False,
|
|
56
|
+
profile: Annotated[
|
|
57
|
+
str | None,
|
|
58
|
+
typer.Option("--profile", "-p", help="Profile to use"),
|
|
59
|
+
] = None,
|
|
60
|
+
) -> None:
|
|
61
|
+
"""List recipients for a domain."""
|
|
62
|
+
try:
|
|
63
|
+
with FastSMTPClient(profile_name=profile) as client:
|
|
64
|
+
recipients = client.list_recipients(domain_id, include_deleted=include_deleted)
|
|
65
|
+
if not recipients:
|
|
66
|
+
print_error("No recipients found")
|
|
67
|
+
return
|
|
68
|
+
print_recipients_table(recipients)
|
|
69
|
+
except APIError as e:
|
|
70
|
+
print_error(e.detail)
|
|
71
|
+
raise typer.Exit(1) from e
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@app.command("get")
|
|
75
|
+
def get_recipient(
|
|
76
|
+
domain_id: Annotated[str, typer.Argument(help="Domain ID")],
|
|
77
|
+
recipient_id: Annotated[str, typer.Argument(help="Recipient ID")],
|
|
78
|
+
include_deleted: IncludeDeleted = False,
|
|
79
|
+
profile: Annotated[
|
|
80
|
+
str | None,
|
|
81
|
+
typer.Option("--profile", "-p", help="Profile to use"),
|
|
82
|
+
] = None,
|
|
83
|
+
) -> None:
|
|
84
|
+
"""Get recipient details (a deleted recipient is not found without --include-deleted)."""
|
|
85
|
+
try:
|
|
86
|
+
with FastSMTPClient(profile_name=profile) as client:
|
|
87
|
+
recipient = client.get_recipient(
|
|
88
|
+
domain_id, recipient_id, include_deleted=include_deleted
|
|
89
|
+
)
|
|
90
|
+
print_recipient(recipient)
|
|
91
|
+
except APIError as e:
|
|
92
|
+
print_error(e.detail)
|
|
93
|
+
raise typer.Exit(1) from e
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@app.command("create")
|
|
97
|
+
def create_recipient(
|
|
98
|
+
domain_id: Annotated[str, typer.Argument(help="Domain ID")],
|
|
99
|
+
webhook_url: Annotated[str, typer.Argument(help="Webhook URL")],
|
|
100
|
+
local_part: Annotated[
|
|
101
|
+
str | None,
|
|
102
|
+
typer.Option("--local", "-l", help="Local part (omit for catch-all)"),
|
|
103
|
+
] = None,
|
|
104
|
+
headers: HeaderOption = None,
|
|
105
|
+
profile: Annotated[
|
|
106
|
+
str | None,
|
|
107
|
+
typer.Option("--profile", "-p", help="Profile to use"),
|
|
108
|
+
] = None,
|
|
109
|
+
) -> None:
|
|
110
|
+
"""Create a new recipient."""
|
|
111
|
+
webhook_headers = parse_headers(headers)
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
with FastSMTPClient(profile_name=profile) as client:
|
|
115
|
+
recipient = client.create_recipient(
|
|
116
|
+
domain_id=domain_id,
|
|
117
|
+
webhook_url=webhook_url,
|
|
118
|
+
local_part=local_part,
|
|
119
|
+
webhook_headers=webhook_headers,
|
|
120
|
+
)
|
|
121
|
+
address = local_part if local_part else "* (catch-all)"
|
|
122
|
+
print_success(f"Recipient '{address}' created")
|
|
123
|
+
print_recipient(recipient)
|
|
124
|
+
except APIError as e:
|
|
125
|
+
print_error(e.detail)
|
|
126
|
+
raise typer.Exit(1) from e
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@app.command("update")
|
|
130
|
+
def update_recipient(
|
|
131
|
+
domain_id: Annotated[str, typer.Argument(help="Domain ID")],
|
|
132
|
+
recipient_id: Annotated[str, typer.Argument(help="Recipient ID")],
|
|
133
|
+
local_part: Annotated[
|
|
134
|
+
str | None,
|
|
135
|
+
typer.Option("--local", "-l", help="Local part; pass '' to clear it (catch-all)"),
|
|
136
|
+
] = None,
|
|
137
|
+
webhook_url: Annotated[
|
|
138
|
+
str | None,
|
|
139
|
+
typer.Option("--webhook", "-w", help="Webhook URL"),
|
|
140
|
+
] = None,
|
|
141
|
+
enabled: Annotated[
|
|
142
|
+
bool | None,
|
|
143
|
+
typer.Option("--enabled/--disabled", help="Enable or disable recipient"),
|
|
144
|
+
] = None,
|
|
145
|
+
headers: HeaderOption = None,
|
|
146
|
+
profile: Annotated[
|
|
147
|
+
str | None,
|
|
148
|
+
typer.Option("--profile", "-p", help="Profile to use"),
|
|
149
|
+
] = None,
|
|
150
|
+
) -> None:
|
|
151
|
+
"""Update a recipient."""
|
|
152
|
+
if all(value is None for value in [local_part, webhook_url, enabled, headers]):
|
|
153
|
+
print_error("At least one option must be provided")
|
|
154
|
+
raise typer.Exit(1)
|
|
155
|
+
|
|
156
|
+
webhook_headers = parse_headers(headers)
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
with FastSMTPClient(profile_name=profile) as client:
|
|
160
|
+
recipient = client.update_recipient(
|
|
161
|
+
domain_id=domain_id,
|
|
162
|
+
recipient_id=recipient_id,
|
|
163
|
+
local_part=clearable_str(local_part),
|
|
164
|
+
webhook_url=webhook_url,
|
|
165
|
+
is_enabled=enabled,
|
|
166
|
+
webhook_headers=webhook_headers,
|
|
167
|
+
)
|
|
168
|
+
print_success("Recipient updated")
|
|
169
|
+
print_recipient(recipient)
|
|
170
|
+
except APIError as e:
|
|
171
|
+
print_error(e.detail)
|
|
172
|
+
raise typer.Exit(1) from e
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@app.command("delete")
|
|
176
|
+
def delete_recipient(
|
|
177
|
+
domain_id: Annotated[str, typer.Argument(help="Domain ID")],
|
|
178
|
+
recipient_id: Annotated[str, typer.Argument(help="Recipient ID")],
|
|
179
|
+
force: Annotated[
|
|
180
|
+
bool,
|
|
181
|
+
typer.Option("--force", "-f", help="Skip confirmation"),
|
|
182
|
+
] = False,
|
|
183
|
+
purge: Purge = False,
|
|
184
|
+
profile: Annotated[
|
|
185
|
+
str | None,
|
|
186
|
+
typer.Option("--profile", "-p", help="Profile to use"),
|
|
187
|
+
] = None,
|
|
188
|
+
) -> None:
|
|
189
|
+
"""Delete a recipient (restorable), or permanently purge an already-deleted one."""
|
|
190
|
+
confirm_delete("recipient", recipient_id, force=force, purge=purge)
|
|
191
|
+
|
|
192
|
+
try:
|
|
193
|
+
with FastSMTPClient(profile_name=profile) as client:
|
|
194
|
+
client.delete_recipient(domain_id, recipient_id, purge=purge)
|
|
195
|
+
print_deleted(
|
|
196
|
+
"recipient",
|
|
197
|
+
recipient_id,
|
|
198
|
+
purge=purge,
|
|
199
|
+
restore_command=f"fsmtp recipient restore {domain_id} {recipient_id}",
|
|
200
|
+
)
|
|
201
|
+
except APIError as e:
|
|
202
|
+
print_error(e.detail)
|
|
203
|
+
raise typer.Exit(1) from e
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@app.command("restore")
|
|
207
|
+
def restore_recipient(
|
|
208
|
+
domain_id: Annotated[str, typer.Argument(help="Domain ID")],
|
|
209
|
+
recipient_id: Annotated[str, typer.Argument(help="Recipient ID")],
|
|
210
|
+
profile: Annotated[
|
|
211
|
+
str | None,
|
|
212
|
+
typer.Option("--profile", "-p", help="Profile to use"),
|
|
213
|
+
] = None,
|
|
214
|
+
) -> None:
|
|
215
|
+
"""Restore a deleted recipient (the domain itself must not be deleted)."""
|
|
216
|
+
try:
|
|
217
|
+
with FastSMTPClient(profile_name=profile) as client:
|
|
218
|
+
recipient = client.restore_recipient(domain_id, recipient_id)
|
|
219
|
+
print_success("Recipient restored")
|
|
220
|
+
print_recipient(recipient)
|
|
221
|
+
except APIError as e:
|
|
222
|
+
print_error(e.detail)
|
|
223
|
+
raise typer.Exit(1) from e
|