feat: expose domain dns records via api (#259)
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
---
|
||||
openapi: get /v1/domains
|
||||
openapi: get /v1/domains/{id}
|
||||
---
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
openapi: get /v1/domains
|
||||
---
|
||||
@@ -24,7 +24,7 @@
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Retrieve the user",
|
||||
"description": "Retrieve domains accessible by the API key",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
@@ -101,6 +101,73 @@
|
||||
"subdomain": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"verificationError": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"lastCheckedTime": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"dnsRecords": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"MX",
|
||||
"TXT"
|
||||
],
|
||||
"description": "DNS record type",
|
||||
"example": "TXT"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "DNS record name",
|
||||
"example": "mail"
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"description": "DNS record value",
|
||||
"example": "v=spf1 include:amazonses.com ~all"
|
||||
},
|
||||
"ttl": {
|
||||
"type": "string",
|
||||
"description": "DNS record TTL",
|
||||
"example": "Auto"
|
||||
},
|
||||
"priority": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "DNS record priority",
|
||||
"example": "10"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"NOT_STARTED",
|
||||
"PENDING",
|
||||
"SUCCESS",
|
||||
"FAILED",
|
||||
"TEMPORARY_FAILURE"
|
||||
]
|
||||
},
|
||||
"recommended": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the record is recommended"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"name",
|
||||
"value",
|
||||
"ttl",
|
||||
"status"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -110,7 +177,8 @@
|
||||
"status",
|
||||
"publicKey",
|
||||
"createdAt",
|
||||
"updatedAt"
|
||||
"updatedAt",
|
||||
"dnsRecords"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -219,6 +287,73 @@
|
||||
"subdomain": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"verificationError": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"lastCheckedTime": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"dnsRecords": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"MX",
|
||||
"TXT"
|
||||
],
|
||||
"description": "DNS record type",
|
||||
"example": "TXT"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "DNS record name",
|
||||
"example": "mail"
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"description": "DNS record value",
|
||||
"example": "v=spf1 include:amazonses.com ~all"
|
||||
},
|
||||
"ttl": {
|
||||
"type": "string",
|
||||
"description": "DNS record TTL",
|
||||
"example": "Auto"
|
||||
},
|
||||
"priority": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "DNS record priority",
|
||||
"example": "10"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"NOT_STARTED",
|
||||
"PENDING",
|
||||
"SUCCESS",
|
||||
"FAILED",
|
||||
"TEMPORARY_FAILURE"
|
||||
]
|
||||
},
|
||||
"recommended": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the record is recommended"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"name",
|
||||
"value",
|
||||
"ttl",
|
||||
"status"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -228,7 +363,8 @@
|
||||
"status",
|
||||
"publicKey",
|
||||
"createdAt",
|
||||
"updatedAt"
|
||||
"updatedAt",
|
||||
"dnsRecords"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -253,7 +389,7 @@
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Create a new domain",
|
||||
"description": "Verify domain",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
@@ -269,6 +405,219 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden - API key doesn't have access to this domain",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"error"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Domain not found",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"error"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/domains/{id}": {
|
||||
"get": {
|
||||
"parameters": [
|
||||
{
|
||||
"schema": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"example": 1
|
||||
},
|
||||
"required": false,
|
||||
"name": "id",
|
||||
"in": "path"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Retrieve the domain",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"description": "The ID of the domain",
|
||||
"example": 1
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the domain",
|
||||
"example": "example.com"
|
||||
},
|
||||
"teamId": {
|
||||
"type": "number",
|
||||
"description": "The ID of the team",
|
||||
"example": 1
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"NOT_STARTED",
|
||||
"PENDING",
|
||||
"SUCCESS",
|
||||
"FAILED",
|
||||
"TEMPORARY_FAILURE"
|
||||
]
|
||||
},
|
||||
"region": {
|
||||
"type": "string",
|
||||
"default": "us-east-1"
|
||||
},
|
||||
"clickTracking": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"openTracking": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"publicKey": {
|
||||
"type": "string"
|
||||
},
|
||||
"dkimStatus": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"spfDetails": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"updatedAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"dmarcAdded": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"isVerifying": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"errorMessage": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"subdomain": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"verificationError": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"lastCheckedTime": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"dnsRecords": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"MX",
|
||||
"TXT"
|
||||
],
|
||||
"description": "DNS record type",
|
||||
"example": "TXT"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "DNS record name",
|
||||
"example": "mail"
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"description": "DNS record value",
|
||||
"example": "v=spf1 include:amazonses.com ~all"
|
||||
},
|
||||
"ttl": {
|
||||
"type": "string",
|
||||
"description": "DNS record TTL",
|
||||
"example": "Auto"
|
||||
},
|
||||
"priority": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "DNS record priority",
|
||||
"example": "10"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"NOT_STARTED",
|
||||
"PENDING",
|
||||
"SUCCESS",
|
||||
"FAILED",
|
||||
"TEMPORARY_FAILURE"
|
||||
]
|
||||
},
|
||||
"recommended": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the record is recommended"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"name",
|
||||
"value",
|
||||
"ttl",
|
||||
"status"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"teamId",
|
||||
"status",
|
||||
"publicKey",
|
||||
"createdAt",
|
||||
"updatedAt",
|
||||
"dnsRecords"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -396,7 +745,8 @@
|
||||
"CLICKED",
|
||||
"COMPLAINED",
|
||||
"FAILED",
|
||||
"CANCELLED"
|
||||
"CANCELLED",
|
||||
"SUPPRESSED"
|
||||
]
|
||||
},
|
||||
"createdAt": {
|
||||
@@ -659,7 +1009,8 @@
|
||||
"CLICKED",
|
||||
"COMPLAINED",
|
||||
"FAILED",
|
||||
"CANCELLED"
|
||||
"CANCELLED",
|
||||
"SUPPRESSED"
|
||||
]
|
||||
},
|
||||
"scheduledAt": {
|
||||
@@ -743,14 +1094,12 @@
|
||||
"replyTo": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "email"
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "email"
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -758,14 +1107,12 @@
|
||||
"cc": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "email"
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "email"
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -773,14 +1120,12 @@
|
||||
"bcc": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "email"
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "email"
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -897,14 +1242,12 @@
|
||||
"replyTo": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "email"
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "email"
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -912,14 +1255,12 @@
|
||||
"cc": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "email"
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "email"
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -927,14 +1268,12 @@
|
||||
"bcc": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "email"
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "email"
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
+22
-5
@@ -34,15 +34,23 @@
|
||||
},
|
||||
{
|
||||
"group": "Self Hosting",
|
||||
"pages": ["self-hosting/overview", "self-hosting/railway"]
|
||||
"pages": [
|
||||
"self-hosting/overview",
|
||||
"self-hosting/railway"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Guides",
|
||||
"pages": ["guides/use-with-react-email"]
|
||||
"pages": [
|
||||
"guides/use-with-react-email"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Community SDKs",
|
||||
"pages": ["community-sdk/python", "community-sdk/go"]
|
||||
"pages": [
|
||||
"community-sdk/python",
|
||||
"community-sdk/go"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -51,7 +59,9 @@
|
||||
"groups": [
|
||||
{
|
||||
"group": "API Reference",
|
||||
"pages": ["api-reference/introduction"]
|
||||
"pages": [
|
||||
"api-reference/introduction"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Emails",
|
||||
@@ -79,6 +89,7 @@
|
||||
"group": "Domains",
|
||||
"pages": [
|
||||
"api-reference/domains/get-domain",
|
||||
"api-reference/domains/list-domains",
|
||||
"api-reference/domains/create-domain",
|
||||
"api-reference/domains/verify-domain"
|
||||
]
|
||||
@@ -136,6 +147,12 @@
|
||||
}
|
||||
},
|
||||
"contextual": {
|
||||
"options": ["copy", "view", "chatgpt", "claude", "perplexity"]
|
||||
"options": [
|
||||
"copy",
|
||||
"view",
|
||||
"chatgpt",
|
||||
"claude",
|
||||
"perplexity"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { api } from "~/trpc/react";
|
||||
import { Domain, DomainStatus } from "@prisma/client";
|
||||
import { DomainStatus } from "@prisma/client";
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
@@ -27,7 +27,11 @@ import SendTestMail from "./send-test-mail";
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
import Link from "next/link";
|
||||
import { toast } from "@usesend/ui/src/toaster";
|
||||
import { H1 } from "@usesend/ui";
|
||||
import type { inferRouterOutputs } from "@trpc/server";
|
||||
import type { AppRouter } from "~/server/api/root";
|
||||
|
||||
type RouterOutputs = inferRouterOutputs<AppRouter>;
|
||||
type DomainResponse = NonNullable<RouterOutputs["domain"]["getDomain"]>;
|
||||
|
||||
export default function DomainItemPage({
|
||||
params,
|
||||
@@ -124,98 +128,39 @@ export default function DomainItemPage({
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell className="">MX</TableCell>
|
||||
<TableCell>
|
||||
<TextWithCopyButton
|
||||
value={`mail${domainQuery.data?.subdomain ? `.${domainQuery.data.subdomain}` : ""}`}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="">
|
||||
<TextWithCopyButton
|
||||
value={`feedback-smtp.${domainQuery.data?.region}.amazonses.com`}
|
||||
className="w-[200px] overflow-hidden text-ellipsis text-nowrap"
|
||||
/>
|
||||
{/* <div className="w-[200px] overflow-hidden text-ellipsis text-nowrap">
|
||||
{`feedback-smtp.${domainQuery.data?.region}.amazonses.com`}
|
||||
</div> */}
|
||||
</TableCell>
|
||||
<TableCell className="">Auto</TableCell>
|
||||
<TableCell className="">10</TableCell>
|
||||
<TableCell className="">
|
||||
<DnsVerificationStatus
|
||||
status={domainQuery.data?.spfDetails ?? "NOT_STARTED"}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="">TXT</TableCell>
|
||||
<TableCell>
|
||||
<TextWithCopyButton
|
||||
value={`${domainQuery.data?.dkimSelector ?? "unsend"}._domainkey${domainQuery.data?.subdomain ? `.${domainQuery.data.subdomain}` : ""}`}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="">
|
||||
<TextWithCopyButton
|
||||
value={`p=${domainQuery.data?.publicKey}`}
|
||||
className="w-[200px] overflow-hidden text-ellipsis"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="">Auto</TableCell>
|
||||
<TableCell className=""></TableCell>
|
||||
<TableCell className="">
|
||||
<DnsVerificationStatus
|
||||
status={domainQuery.data?.dkimStatus ?? "NOT_STARTED"}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="">TXT</TableCell>
|
||||
<TableCell>
|
||||
<TextWithCopyButton
|
||||
value={`mail${domainQuery.data?.subdomain ? `.${domainQuery.data.subdomain}` : ""}`}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="">
|
||||
<TextWithCopyButton
|
||||
value={`v=spf1 include:amazonses.com ~all`}
|
||||
className="w-[200px] overflow-hidden text-ellipsis text-nowrap"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="">Auto</TableCell>
|
||||
<TableCell className=""></TableCell>
|
||||
<TableCell className="">
|
||||
<DnsVerificationStatus
|
||||
status={domainQuery.data?.spfDetails ?? "NOT_STARTED"}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="">TXT</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-2 items-center">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
(recommended)
|
||||
</span>
|
||||
<TextWithCopyButton value="_dmarc" />
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="">
|
||||
<TextWithCopyButton
|
||||
value={`v=DMARC1; p=none;`}
|
||||
className="w-[200px] overflow-hidden text-ellipsis text-nowrap"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="">Auto</TableCell>
|
||||
<TableCell className=""></TableCell>
|
||||
<TableCell className="">
|
||||
<DnsVerificationStatus
|
||||
status={
|
||||
domainQuery.data?.dmarcAdded ? "SUCCESS" : "NOT_STARTED"
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{(domainQuery.data?.dnsRecords ?? []).map((record) => {
|
||||
const key = `${record.type}-${record.name}`;
|
||||
const valueClassName = record.name.includes("_domainkey")
|
||||
? "w-[200px] overflow-hidden text-ellipsis"
|
||||
: "w-[200px] overflow-hidden text-ellipsis text-nowrap";
|
||||
|
||||
return (
|
||||
<TableRow key={key}>
|
||||
<TableCell className="">{record.type}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-2 items-center">
|
||||
{record.recommended ? (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
(recommended)
|
||||
</span>
|
||||
) : null}
|
||||
<TextWithCopyButton value={record.name} />
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="">
|
||||
<TextWithCopyButton
|
||||
value={record.value}
|
||||
className={valueClassName}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="">{record.ttl}</TableCell>
|
||||
<TableCell className="">{record.priority ?? ""}</TableCell>
|
||||
<TableCell className="">
|
||||
<DnsVerificationStatus status={record.status} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
@@ -228,7 +173,7 @@ export default function DomainItemPage({
|
||||
);
|
||||
}
|
||||
|
||||
const DomainSettings: React.FC<{ domain: Domain }> = ({ domain }) => {
|
||||
const DomainSettings: React.FC<{ domain: DomainResponse }> = ({ domain }) => {
|
||||
const updateDomain = api.domain.updateDomain.useMutation();
|
||||
const utils = api.useUtils();
|
||||
|
||||
@@ -303,7 +248,7 @@ const DomainSettings: React.FC<{ domain: Domain }> = ({ domain }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const DnsVerificationStatus: React.FC<{ status: string }> = ({ status }) => {
|
||||
const DnsVerificationStatus: React.FC<{ status: DomainStatus }> = ({ status }) => {
|
||||
let badgeColor = "bg-gray/10 text-gray border-gray/10"; // Default color
|
||||
switch (status) {
|
||||
case DomainStatus.SUCCESS:
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
import { Button } from "@usesend/ui/src/button";
|
||||
import { api } from "~/trpc/react";
|
||||
import React from "react";
|
||||
import { Domain } from "@prisma/client";
|
||||
import { toast } from "@usesend/ui/src/toaster";
|
||||
import { SendHorizonal } from "lucide-react";
|
||||
import type { DomainWithDnsRecords } from "~/types/domain";
|
||||
// Removed dialog and example code. Clicking the button now sends the email directly.
|
||||
|
||||
export const SendTestMail: React.FC<{ domain: Domain }> = ({ domain }) => {
|
||||
export const SendTestMail: React.FC<{ domain: DomainWithDnsRecords }> = ({
|
||||
domain,
|
||||
}) => {
|
||||
const sendTestEmailFromDomainMutation =
|
||||
api.domain.sendTestEmailFromDomain.useMutation();
|
||||
|
||||
|
||||
@@ -3,6 +3,34 @@ import { z } from "zod";
|
||||
|
||||
export const DomainStatusSchema = z.nativeEnum(DomainStatus);
|
||||
|
||||
export const DomainDnsRecordSchema = z.object({
|
||||
type: z.enum(["MX", "TXT"]).openapi({
|
||||
description: "DNS record type",
|
||||
example: "TXT",
|
||||
}),
|
||||
name: z
|
||||
.string()
|
||||
.openapi({ description: "DNS record name", example: "mail" }),
|
||||
value: z
|
||||
.string()
|
||||
.openapi({
|
||||
description: "DNS record value",
|
||||
example: "v=spf1 include:amazonses.com ~all",
|
||||
}),
|
||||
ttl: z
|
||||
.string()
|
||||
.openapi({ description: "DNS record TTL", example: "Auto" }),
|
||||
priority: z
|
||||
.string()
|
||||
.nullish()
|
||||
.openapi({ description: "DNS record priority", example: "10" }),
|
||||
status: DomainStatusSchema,
|
||||
recommended: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.openapi({ description: "Whether the record is recommended" }),
|
||||
});
|
||||
|
||||
export const DomainSchema = z.object({
|
||||
id: z.number().openapi({ description: "The ID of the domain", example: 1 }),
|
||||
name: z
|
||||
@@ -22,4 +50,7 @@ export const DomainSchema = z.object({
|
||||
isVerifying: z.boolean().default(false),
|
||||
errorMessage: z.string().optional().nullish(),
|
||||
subdomain: z.string().optional().nullish(),
|
||||
verificationError: z.string().optional().nullish(),
|
||||
lastCheckedTime: z.string().optional().nullish(),
|
||||
dnsRecords: z.array(DomainDnsRecordSchema),
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
createDomain,
|
||||
deleteDomain,
|
||||
getDomain,
|
||||
getDomains,
|
||||
updateDomain,
|
||||
} from "~/server/service/domain-service";
|
||||
import { sendEmail } from "~/server/service/email-service";
|
||||
@@ -29,7 +30,7 @@ export const domainRouter = createTRPCRouter({
|
||||
ctx.team.id,
|
||||
input.name,
|
||||
input.region,
|
||||
ctx.team.sesTenantId ?? undefined,
|
||||
ctx.team.sesTenantId ?? undefined
|
||||
);
|
||||
}),
|
||||
|
||||
@@ -41,20 +42,11 @@ export const domainRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
domains: teamProcedure.query(async ({ ctx }) => {
|
||||
const domains = await db.domain.findMany({
|
||||
where: {
|
||||
teamId: ctx.team.id,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
return domains;
|
||||
return getDomains(ctx.team.id);
|
||||
}),
|
||||
|
||||
getDomain: domainProcedure.query(async ({ input }) => {
|
||||
return getDomain(input.id);
|
||||
getDomain: domainProcedure.query(async ({ input, ctx }) => {
|
||||
return getDomain(input.id, ctx.team.id);
|
||||
}),
|
||||
|
||||
updateDomain: domainProcedure
|
||||
@@ -62,7 +54,7 @@ export const domainRouter = createTRPCRouter({
|
||||
z.object({
|
||||
clickTracking: z.boolean().optional(),
|
||||
openTracking: z.boolean().optional(),
|
||||
}),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
return updateDomain(input.id, {
|
||||
@@ -104,6 +96,6 @@ export const domainRouter = createTRPCRouter({
|
||||
text: "hello,\n\nuseSend is the best open source sending platform\n\ncheck out https://usesend.com",
|
||||
html: "<p>hello,</p><p>useSend is the best open source sending platform<p><p>check out <a href='https://usesend.com'>usesend.com</a>",
|
||||
});
|
||||
},
|
||||
}
|
||||
),
|
||||
});
|
||||
|
||||
@@ -2,7 +2,6 @@ import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { DomainSchema } from "~/lib/zod/domain-schema";
|
||||
import { PublicAPIApp } from "~/server/public-api/hono";
|
||||
import { createDomain as createDomainService } from "~/server/service/domain-service";
|
||||
import { getTeamFromToken } from "~/server/public-api/auth";
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { DomainSchema } from "~/lib/zod/domain-schema";
|
||||
import { PublicAPIApp } from "~/server/public-api/hono";
|
||||
import { UnsendApiError } from "../../api-error";
|
||||
import { db } from "~/server/db";
|
||||
import { getDomain as getDomainService } from "~/server/service/domain-service";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/v1/domains/{id}",
|
||||
request: {
|
||||
params: z.object({
|
||||
id: z.coerce.number().openapi({
|
||||
param: { name: "id", in: "path" },
|
||||
example: 1,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: DomainSchema,
|
||||
},
|
||||
},
|
||||
description: "Retrieve the domain",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
function getDomain(app: PublicAPIApp) {
|
||||
app.openapi(route, async (c) => {
|
||||
const team = c.var.team;
|
||||
const id = c.req.valid("param").id;
|
||||
|
||||
// Enforce API key domain restriction (if any)
|
||||
if (team.apiKey.domainId && team.apiKey.domainId !== id) {
|
||||
throw new UnsendApiError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Domain not found",
|
||||
});
|
||||
}
|
||||
|
||||
// Re-use service logic to enrich domain (verification status, DNS records, etc.)
|
||||
let enriched;
|
||||
try {
|
||||
enriched = await getDomainService(id, team.id);
|
||||
} catch (e) {
|
||||
throw new UnsendApiError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: e instanceof Error ? e.message : "Internal server error",
|
||||
});
|
||||
}
|
||||
|
||||
return c.json(enriched);
|
||||
});
|
||||
}
|
||||
|
||||
export default getDomain;
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { DomainSchema } from "~/lib/zod/domain-schema";
|
||||
import { PublicAPIApp } from "~/server/public-api/hono";
|
||||
import { db } from "~/server/db";
|
||||
import { getDomains as getDomainsService } from "~/server/service/domain-service";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
@@ -23,11 +23,9 @@ function getDomains(app: PublicAPIApp) {
|
||||
const team = c.var.team;
|
||||
|
||||
// If API key is restricted to a specific domain, only return that domain; else return all team domains
|
||||
const domains = team.apiKey.domainId
|
||||
? await db.domain.findMany({
|
||||
where: { teamId: team.id, id: team.apiKey.domainId },
|
||||
})
|
||||
: await db.domain.findMany({ where: { teamId: team.id } });
|
||||
const domains = await getDomainsService(team.id, {
|
||||
domainId: team.apiKey.domainId ?? undefined,
|
||||
});
|
||||
|
||||
return c.json(domains);
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@ import upsertContact from "./api/contacts/upsert-contact";
|
||||
import createDomain from "./api/domains/create-domain";
|
||||
import deleteContact from "./api/contacts/delete-contact";
|
||||
import verifyDomain from "./api/domains/verify-domain";
|
||||
import getDomain from "./api/domains/get-domain";
|
||||
import sendBatch from "./api/emails/batch-email";
|
||||
|
||||
export const app = getApp();
|
||||
@@ -21,6 +22,7 @@ export const app = getApp();
|
||||
getDomains(app);
|
||||
createDomain(app);
|
||||
verifyDomain(app);
|
||||
getDomain(app);
|
||||
|
||||
/**Email related APIs */
|
||||
getEmail(app);
|
||||
|
||||
@@ -6,8 +6,79 @@ import { db } from "~/server/db";
|
||||
import { SesSettingsService } from "./ses-settings-service";
|
||||
import { UnsendApiError } from "../public-api/api-error";
|
||||
import { logger } from "../logger/log";
|
||||
import { ApiKey } from "@prisma/client";
|
||||
import { ApiKey, DomainStatus, type Domain } from "@prisma/client";
|
||||
import { LimitService } from "./limit-service";
|
||||
import type { DomainDnsRecord } from "~/types/domain";
|
||||
|
||||
const DOMAIN_STATUS_VALUES = new Set(Object.values(DomainStatus));
|
||||
|
||||
function parseDomainStatus(status?: string | null): DomainStatus {
|
||||
if (!status) {
|
||||
return DomainStatus.NOT_STARTED;
|
||||
}
|
||||
|
||||
const normalized = status.toUpperCase();
|
||||
|
||||
if (DOMAIN_STATUS_VALUES.has(normalized as DomainStatus)) {
|
||||
return normalized as DomainStatus;
|
||||
}
|
||||
|
||||
return DomainStatus.NOT_STARTED;
|
||||
}
|
||||
|
||||
function buildDnsRecords(domain: Domain): DomainDnsRecord[] {
|
||||
const subdomainSuffix = domain.subdomain ? `.${domain.subdomain}` : "";
|
||||
const mailDomain = `mail${subdomainSuffix}`;
|
||||
const dkimSelector = domain.dkimSelector ?? "usesend";
|
||||
|
||||
const spfStatus = parseDomainStatus(domain.spfDetails);
|
||||
const dkimStatus = parseDomainStatus(domain.dkimStatus);
|
||||
const dmarcStatus = domain.dmarcAdded
|
||||
? DomainStatus.SUCCESS
|
||||
: DomainStatus.NOT_STARTED;
|
||||
|
||||
return [
|
||||
{
|
||||
type: "MX",
|
||||
name: mailDomain,
|
||||
value: `feedback-smtp.${domain.region}.amazonses.com`,
|
||||
ttl: "Auto",
|
||||
priority: "10",
|
||||
status: spfStatus,
|
||||
},
|
||||
{
|
||||
type: "TXT",
|
||||
name: `${dkimSelector}._domainkey${subdomainSuffix}`,
|
||||
value: `p=${domain.publicKey}`,
|
||||
ttl: "Auto",
|
||||
status: dkimStatus,
|
||||
},
|
||||
{
|
||||
type: "TXT",
|
||||
name: mailDomain,
|
||||
value: "v=spf1 include:amazonses.com ~all",
|
||||
ttl: "Auto",
|
||||
status: spfStatus,
|
||||
},
|
||||
{
|
||||
type: "TXT",
|
||||
name: "_dmarc",
|
||||
value: "v=DMARC1; p=none;",
|
||||
ttl: "Auto",
|
||||
status: dmarcStatus,
|
||||
recommended: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function withDnsRecords<T extends Domain>(
|
||||
domain: T
|
||||
): T & { dnsRecords: DomainDnsRecord[] } {
|
||||
return {
|
||||
...domain,
|
||||
dnsRecords: buildDnsRecords(domain),
|
||||
};
|
||||
}
|
||||
|
||||
const dnsResolveTxt = util.promisify(dns.resolveTxt);
|
||||
|
||||
@@ -128,21 +199,27 @@ export async function createDomain(
|
||||
region,
|
||||
sesTenantId,
|
||||
dkimSelector,
|
||||
dkimStatus: DomainStatus.NOT_STARTED,
|
||||
spfDetails: DomainStatus.NOT_STARTED,
|
||||
},
|
||||
});
|
||||
|
||||
return domain;
|
||||
return withDnsRecords(domain);
|
||||
}
|
||||
|
||||
export async function getDomain(id: number) {
|
||||
export async function getDomain(id: number, teamId: number) {
|
||||
let domain = await db.domain.findUnique({
|
||||
where: {
|
||||
id,
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!domain) {
|
||||
throw new Error("Domain not found");
|
||||
throw new UnsendApiError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Domain not found",
|
||||
});
|
||||
}
|
||||
|
||||
if (domain.isVerifying) {
|
||||
@@ -178,17 +255,30 @@ export async function getDomain(id: number) {
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
const normalizedDomain = {
|
||||
...domain,
|
||||
dkimStatus: dkimStatus?.toString() ?? null,
|
||||
spfDetails: spfDetails?.toString() ?? null,
|
||||
verificationError: verificationError?.toString() ?? null,
|
||||
lastCheckedTime,
|
||||
dmarcAdded: dmarcRecord ? true : false,
|
||||
} satisfies Domain;
|
||||
|
||||
const domainWithDns = withDnsRecords(normalizedDomain);
|
||||
const normalizedLastCheckedTime =
|
||||
lastCheckedTime instanceof Date
|
||||
? lastCheckedTime.toISOString()
|
||||
: (lastCheckedTime ?? null);
|
||||
|
||||
return {
|
||||
...domainWithDns,
|
||||
dkimStatus: normalizedDomain.dkimStatus,
|
||||
spfDetails: normalizedDomain.spfDetails,
|
||||
verificationError: verificationError?.toString() ?? null,
|
||||
lastCheckedTime: normalizedLastCheckedTime,
|
||||
dmarcAdded: normalizedDomain.dmarcAdded,
|
||||
};
|
||||
}
|
||||
|
||||
return domain;
|
||||
return withDnsRecords(domain);
|
||||
}
|
||||
|
||||
export async function updateDomain(
|
||||
@@ -225,15 +315,21 @@ export async function deleteDomain(id: number) {
|
||||
return deletedRecord;
|
||||
}
|
||||
|
||||
export async function getDomains(teamId: number) {
|
||||
return db.domain.findMany({
|
||||
export async function getDomains(
|
||||
teamId: number,
|
||||
options?: { domainId?: number }
|
||||
) {
|
||||
const domains = await db.domain.findMany({
|
||||
where: {
|
||||
teamId,
|
||||
...(options?.domainId ? { id: options.domainId } : {}),
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
return domains.map((d) => withDnsRecords(d));
|
||||
}
|
||||
|
||||
async function getDmarcRecord(domain: string) {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { Domain, DomainStatus } from "@prisma/client";
|
||||
|
||||
export type DomainDnsRecord = {
|
||||
type: "MX" | "TXT";
|
||||
name: string;
|
||||
value: string;
|
||||
ttl: string;
|
||||
priority?: string | null;
|
||||
status: DomainStatus;
|
||||
recommended?: boolean;
|
||||
};
|
||||
|
||||
export type DomainWithDnsRecords = Domain & {
|
||||
dnsRecords: DomainDnsRecord[];
|
||||
verificationError?: string | null;
|
||||
lastCheckedTime?: Date | string | null;
|
||||
};
|
||||
+1
-1
@@ -31,7 +31,7 @@
|
||||
"@usesend/eslint-config": "workspace:*",
|
||||
"@usesend/typescript-config": "workspace:*",
|
||||
"dotenv-cli": "^8.0.0",
|
||||
"mintlify": "4.0.510",
|
||||
"mintlify": "4.2.128",
|
||||
"prettier": "^3.5.3"
|
||||
},
|
||||
"packageManager": "pnpm@8.9.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "usesend"
|
||||
version = "0.2.3"
|
||||
version = "0.2.4"
|
||||
description = "Python SDK for the UseSend API"
|
||||
authors = ["UseSend"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Python client for the UseSend API."""
|
||||
|
||||
from .usesend import UseSend, UseSendHTTPError
|
||||
from .domains import Domains # type: ignore
|
||||
from . import types
|
||||
|
||||
__all__ = ["UseSend", "UseSendHTTPError", "types"]
|
||||
__all__ = ["UseSend", "UseSendHTTPError", "types", "Domains"]
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
"""Domain resource client using TypedDict shapes (no Pydantic)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional, Tuple, List
|
||||
|
||||
from .types import (
|
||||
APIError,
|
||||
Domain,
|
||||
DomainCreate,
|
||||
DomainCreateResponse,
|
||||
DomainVerifyResponse,
|
||||
)
|
||||
|
||||
|
||||
class Domains:
|
||||
"""Client for `/domains` endpoints."""
|
||||
|
||||
def __init__(self, usesend: "UseSend") -> None:
|
||||
self.usesend = usesend
|
||||
|
||||
def list(self) -> Tuple[Optional[List[Domain]], Optional[APIError]]:
|
||||
data, err = self.usesend.get("/domains")
|
||||
return (data, err) # type: ignore[return-value]
|
||||
|
||||
def create(self, payload: DomainCreate) -> Tuple[Optional[DomainCreateResponse], Optional[APIError]]:
|
||||
data, err = self.usesend.post("/domains", payload)
|
||||
return (data, err) # type: ignore[return-value]
|
||||
|
||||
def verify(self, domain_id: int) -> Tuple[Optional[DomainVerifyResponse], Optional[APIError]]:
|
||||
data, err = self.usesend.put(f"/domains/{domain_id}/verify", {})
|
||||
return (data, err) # type: ignore[return-value]
|
||||
|
||||
def get(self, domain_id: int) -> Tuple[Optional[Domain], Optional[APIError]]:
|
||||
data, err = self.usesend.get(f"/domains/{domain_id}")
|
||||
return (data, err) # type: ignore[return-value]
|
||||
|
||||
from .usesend import UseSend # noqa: E402 pylint: disable=wrong-import-position
|
||||
@@ -22,6 +22,21 @@ DomainStatus = Literal[
|
||||
'TEMPORARY_FAILURE',
|
||||
]
|
||||
|
||||
DNSRecordType = Literal['MX', 'TXT']
|
||||
|
||||
|
||||
class DNSRecord(TypedDict, total=False):
|
||||
type: DNSRecordType
|
||||
name: str
|
||||
value: str
|
||||
ttl: str
|
||||
priority: Optional[str]
|
||||
status: DomainStatus
|
||||
recommended: Optional[bool]
|
||||
|
||||
|
||||
DNSRecords = List[DNSRecord]
|
||||
|
||||
|
||||
class Domain(TypedDict, total=False):
|
||||
id: float
|
||||
@@ -40,6 +55,9 @@ class Domain(TypedDict, total=False):
|
||||
isVerifying: bool
|
||||
errorMessage: Optional[str]
|
||||
subdomain: Optional[str]
|
||||
verificationError: Optional[str]
|
||||
lastCheckedTime: Optional[str]
|
||||
dnsRecords: DNSRecords
|
||||
|
||||
|
||||
DomainList = List[Domain]
|
||||
@@ -67,6 +85,9 @@ class DomainCreateResponse(TypedDict, total=False):
|
||||
isVerifying: bool
|
||||
errorMessage: Optional[str]
|
||||
subdomain: Optional[str]
|
||||
verificationError: Optional[str]
|
||||
lastCheckedTime: Optional[str]
|
||||
dnsRecords: DNSRecords
|
||||
|
||||
|
||||
class DomainVerifyResponse(TypedDict):
|
||||
|
||||
@@ -71,6 +71,7 @@ class UseSend:
|
||||
# Lazily initialise resource clients.
|
||||
self.emails = Emails(self)
|
||||
self.contacts = Contacts(self)
|
||||
self.domains = Domains(self)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Internal request helper
|
||||
@@ -123,3 +124,4 @@ class UseSend:
|
||||
# Import here to avoid circular dependency during type checking
|
||||
from .emails import Emails # noqa: E402 pylint: disable=wrong-import-position
|
||||
from .contacts import Contacts # noqa: E402 pylint: disable=wrong-import-position
|
||||
from .domains import Domains # type: ignore # noqa: E402
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "usesend-js",
|
||||
"version": "1.5.2",
|
||||
"version": "1.5.3",
|
||||
"description": "",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
|
||||
@@ -29,6 +29,14 @@ type VerifyDomainResponse = {
|
||||
type VerifyDomainResponseSuccess =
|
||||
paths["/v1/domains/{id}/verify"]["put"]["responses"]["200"]["content"]["application/json"];
|
||||
|
||||
type GetDomainResponse = {
|
||||
data: GetDomainResponseSuccess | null;
|
||||
error: ErrorResponse | null;
|
||||
};
|
||||
|
||||
type GetDomainResponseSuccess =
|
||||
paths["/v1/domains/{id}"]["get"]["responses"]["200"]["content"]["application/json"];
|
||||
|
||||
export class Domains {
|
||||
constructor(private readonly usesend: UseSend) {
|
||||
this.usesend = usesend;
|
||||
@@ -54,4 +62,12 @@ export class Domains {
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
async get(id: number): Promise<GetDomainResponse> {
|
||||
const data = await this.usesend.get<GetDomainResponseSuccess>(
|
||||
`/domains/${id}`
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,6 +112,7 @@ export class Emails {
|
||||
const data = await this.usesend.get<GetEmailResponseSuccess>(
|
||||
`/emails/${id}`
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
Vendored
+245
-6
@@ -20,7 +20,7 @@ export interface paths {
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Retrieve the user */
|
||||
/** @description Retrieve domains accessible by the API key */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
@@ -61,6 +61,40 @@ export interface paths {
|
||||
isVerifying: boolean;
|
||||
errorMessage?: string | null;
|
||||
subdomain?: string | null;
|
||||
verificationError?: string | null;
|
||||
lastCheckedTime?: string | null;
|
||||
dnsRecords: {
|
||||
/**
|
||||
* @description DNS record type
|
||||
* @example TXT
|
||||
* @enum {string}
|
||||
*/
|
||||
type: "MX" | "TXT";
|
||||
/**
|
||||
* @description DNS record name
|
||||
* @example mail
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* @description DNS record value
|
||||
* @example v=spf1 include:amazonses.com ~all
|
||||
*/
|
||||
value: string;
|
||||
/**
|
||||
* @description DNS record TTL
|
||||
* @example Auto
|
||||
*/
|
||||
ttl: string;
|
||||
/**
|
||||
* @description DNS record priority
|
||||
* @example 10
|
||||
*/
|
||||
priority?: string | null;
|
||||
/** @enum {string} */
|
||||
status: "NOT_STARTED" | "PENDING" | "SUCCESS" | "FAILED" | "TEMPORARY_FAILURE";
|
||||
/** @description Whether the record is recommended */
|
||||
recommended?: boolean;
|
||||
}[];
|
||||
}[];
|
||||
};
|
||||
};
|
||||
@@ -124,6 +158,40 @@ export interface paths {
|
||||
isVerifying: boolean;
|
||||
errorMessage?: string | null;
|
||||
subdomain?: string | null;
|
||||
verificationError?: string | null;
|
||||
lastCheckedTime?: string | null;
|
||||
dnsRecords: {
|
||||
/**
|
||||
* @description DNS record type
|
||||
* @example TXT
|
||||
* @enum {string}
|
||||
*/
|
||||
type: "MX" | "TXT";
|
||||
/**
|
||||
* @description DNS record name
|
||||
* @example mail
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* @description DNS record value
|
||||
* @example v=spf1 include:amazonses.com ~all
|
||||
*/
|
||||
value: string;
|
||||
/**
|
||||
* @description DNS record TTL
|
||||
* @example Auto
|
||||
*/
|
||||
ttl: string;
|
||||
/**
|
||||
* @description DNS record priority
|
||||
* @example 10
|
||||
*/
|
||||
priority?: string | null;
|
||||
/** @enum {string} */
|
||||
status: "NOT_STARTED" | "PENDING" | "SUCCESS" | "FAILED" | "TEMPORARY_FAILURE";
|
||||
/** @description Whether the record is recommended */
|
||||
recommended?: boolean;
|
||||
}[];
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -154,7 +222,7 @@ export interface paths {
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Create a new domain */
|
||||
/** @description Verify domain */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
@@ -165,6 +233,28 @@ export interface paths {
|
||||
};
|
||||
};
|
||||
};
|
||||
/** @description Forbidden - API key doesn't have access to this domain */
|
||||
403: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": {
|
||||
error: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
/** @description Domain not found */
|
||||
404: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": {
|
||||
error: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
post?: never;
|
||||
@@ -174,6 +264,112 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/v1/domains/{id}": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
id: number | null;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Retrieve the domain */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": {
|
||||
/**
|
||||
* @description The ID of the domain
|
||||
* @example 1
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* @description The name of the domain
|
||||
* @example example.com
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* @description The ID of the team
|
||||
* @example 1
|
||||
*/
|
||||
teamId: number;
|
||||
/** @enum {string} */
|
||||
status: "NOT_STARTED" | "PENDING" | "SUCCESS" | "FAILED" | "TEMPORARY_FAILURE";
|
||||
/** @default us-east-1 */
|
||||
region: string;
|
||||
/** @default false */
|
||||
clickTracking: boolean;
|
||||
/** @default false */
|
||||
openTracking: boolean;
|
||||
publicKey: string;
|
||||
dkimStatus?: string | null;
|
||||
spfDetails?: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
/** @default false */
|
||||
dmarcAdded: boolean;
|
||||
/** @default false */
|
||||
isVerifying: boolean;
|
||||
errorMessage?: string | null;
|
||||
subdomain?: string | null;
|
||||
verificationError?: string | null;
|
||||
lastCheckedTime?: string | null;
|
||||
dnsRecords: {
|
||||
/**
|
||||
* @description DNS record type
|
||||
* @example TXT
|
||||
* @enum {string}
|
||||
*/
|
||||
type: "MX" | "TXT";
|
||||
/**
|
||||
* @description DNS record name
|
||||
* @example mail
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* @description DNS record value
|
||||
* @example v=spf1 include:amazonses.com ~all
|
||||
*/
|
||||
value: string;
|
||||
/**
|
||||
* @description DNS record TTL
|
||||
* @example Auto
|
||||
*/
|
||||
ttl: string;
|
||||
/**
|
||||
* @description DNS record priority
|
||||
* @example 10
|
||||
*/
|
||||
priority?: string | null;
|
||||
/** @enum {string} */
|
||||
status: "NOT_STARTED" | "PENDING" | "SUCCESS" | "FAILED" | "TEMPORARY_FAILURE";
|
||||
/** @description Whether the record is recommended */
|
||||
recommended?: boolean;
|
||||
}[];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/v1/emails/{emailId}": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -214,7 +410,7 @@ export interface paths {
|
||||
emailEvents: {
|
||||
emailId: string;
|
||||
/** @enum {string} */
|
||||
status: "SCHEDULED" | "QUEUED" | "SENT" | "DELIVERY_DELAYED" | "BOUNCED" | "REJECTED" | "RENDERING_FAILURE" | "DELIVERED" | "OPENED" | "CLICKED" | "COMPLAINED" | "FAILED" | "CANCELLED";
|
||||
status: "SCHEDULED" | "QUEUED" | "SENT" | "DELIVERY_DELAYED" | "BOUNCED" | "REJECTED" | "RENDERING_FAILURE" | "DELIVERED" | "OPENED" | "CLICKED" | "COMPLAINED" | "FAILED" | "CANCELLED" | "SUPPRESSED";
|
||||
createdAt: string;
|
||||
data?: unknown;
|
||||
}[];
|
||||
@@ -268,7 +464,52 @@ export interface paths {
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
get: {
|
||||
parameters: {
|
||||
query?: {
|
||||
page?: string;
|
||||
limit?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
domainId?: string | string[];
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Retrieve a list of emails */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": {
|
||||
data: {
|
||||
id: string;
|
||||
to: string | string[];
|
||||
replyTo?: string | string[] | unknown;
|
||||
cc?: string | string[] | unknown;
|
||||
bcc?: string | string[] | unknown;
|
||||
from: string;
|
||||
subject: string;
|
||||
html: string | null;
|
||||
text: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
/** @enum {string|null} */
|
||||
latestStatus: "SCHEDULED" | "QUEUED" | "SENT" | "DELIVERY_DELAYED" | "BOUNCED" | "REJECTED" | "RENDERING_FAILURE" | "DELIVERED" | "OPENED" | "CLICKED" | "COMPLAINED" | "FAILED" | "CANCELLED" | "SUPPRESSED" | null;
|
||||
/** Format: date-time */
|
||||
scheduledAt: string | null;
|
||||
domainId: number | null;
|
||||
}[];
|
||||
count: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
put?: never;
|
||||
post: {
|
||||
parameters: {
|
||||
@@ -281,7 +522,6 @@ export interface paths {
|
||||
content: {
|
||||
"application/json": {
|
||||
to: string | string[];
|
||||
/** Format: email */
|
||||
from: string;
|
||||
/** @description Optional when templateId is provided */
|
||||
subject?: string;
|
||||
@@ -345,7 +585,6 @@ export interface paths {
|
||||
content: {
|
||||
"application/json": {
|
||||
to: string | string[];
|
||||
/** Format: email */
|
||||
from: string;
|
||||
/** @description Optional when templateId is provided */
|
||||
subject?: string;
|
||||
|
||||
Generated
+1066
-368
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user