Skip to content
Merged
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
1 change: 0 additions & 1 deletion .agents/skills/custom-commands/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,6 @@ Custom commands automatically inherit the CLI's authentication.
The following auth schemes are configured:

- **BearerAuth** (bearer): env `AGENTMAIL_API_KEY`
- **TokenAuth** (bearer): env `AGENTMAIL_TOKEN`

No manual auth wiring is needed in custom command handlers.

Expand Down
3,800 changes: 3,799 additions & 1 deletion .fern/replay.lock

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions .fernignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
CHANGELOG.md
SECURITY.md

# Hand-authored custom command bindings. The scaffold and its docs say
# this file is protected; without this entry a regeneration overwrites it.
# Hand-authored custom command bindings. Generator 0.38.10 emits this
# entry itself; it is repeated here because this patch replaces the whole
# file, and dropping the line would remove protection the generator added.
cli/agentmail/custom.rs
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -262,8 +262,9 @@ jobs:
"version": "${VERSION}",
"description": "Command-line interface for the AgentMail API. Send, receive, reply, and manage threaded email conversations from your terminal.",
"license": "MIT",
"keywords": ["email", "api", "cli", "agent", "agentmail"],
"keywords": ["email","api","cli","agent","agentmail"],
"homepage": "https://agentmail.to",
"author": "AgentMail <support@agentmail.cc>",
"repository": {
"type": "git",
"url": "https://github.com/agentmail-to/agentmail-cli"
Expand Down
48 changes: 48 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "agentmail-cli"
version = "1.1.1"
version = "1.2.0"
edition = "2021"
description = "Command-line interface for the AgentMail API. Send, receive, reply, and manage threaded email conversations from your terminal."
license = "MIT"
Expand Down
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ Set the following environment variable(s) before using the CLI:

```bash
export AGENTMAIL_API_KEY="<your token>"
export AGENTMAIL_TOKEN="<your token>"
```

A `.env` file in the working directory is also supported — the CLI auto-loads it on startup.
Expand Down
44 changes: 44 additions & 0 deletions agentmail-sdk/src/api/resources/domains/domains.rs
Original file line number Diff line number Diff line change
Expand Up @@ -363,4 +363,48 @@ impl DomainsClient {
)
.await
}

/// Build a one-click DNS setup link for the domain via the Domain Connect standard. When the domain's DNS provider supports Domain Connect and carries the AgentMail template, the response contains a signed URL: opening it lets the domain owner approve the required DNS records at their provider, which writes them automatically — no copy-paste. When the provider does not support it, `supported` is `false` and the domain's `records` should be added manually instead.
///
/// # Arguments
///
/// * `options` - Additional request options such as headers, timeout, etc.
///
/// # Returns
///
/// JSON response from the API
///
/// # Examples
///
/// ```no_run
/// use agentmail_sdk::prelude::*;
///
/// #[tokio::main]
/// async fn main() {
/// let config = ClientConfig {
/// token: Some("<token>".to_string()),
/// ..Default::default()
/// };
/// let client = AgentmailClient::new(config).expect("Failed to build client");
/// client
/// .domains
/// .get_setup_link(&DomainID("domain_id".to_string()), None)
/// .await;
/// }
/// ```
pub async fn get_setup_link(
&self,
domain_id: &DomainId,
options: Option<RequestOptions>,
) -> Result<GetSetupLinkResponse, ApiError> {
self.http_client
.execute_request(
Method::GET,
&format!("v0/domains/{}/setup-link", domain_id.0),
None,
None,
options,
)
.await
}
}
10 changes: 10 additions & 0 deletions agentmail-types/src/types/domain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ pub struct Domain {
#[serde(default)]
pub domain: DomainName,
pub status: Status,
/// Why the domain is not (yet) VERIFIED, when known. `dns_records_missing` / `dns_records_invalid` point at the DNS records. The `ses_*` values mean the records look right and sending-infrastructure validation has not converged: `ses_dkim_pending` / `ses_mail_from_pending` (still checking), `ses_dkim_temporary_failure` / `ses_mail_from_temporary_failure` (a transient error the infrastructure keeps retrying on its own — usually resolves without changes), `ses_dkim_failed` / `ses_mail_from_failed` (a terminal verdict; re-verify after fixing), `ses_dkim_not_started` / `ses_mail_from_not_started` (the attribute was never configured on the identity — re-verify to push it), and `ses_not_verified_for_sending`. Absent when VERIFIED.
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
#[serde(default)]
pub feedback_enabled: FeedbackEnabled,
#[serde(default)]
Expand Down Expand Up @@ -46,6 +49,7 @@ pub struct DomainBuilder {
domain_id: Option<DomainId>,
domain: Option<DomainName>,
status: Option<Status>,
reason: Option<String>,
feedback_enabled: Option<FeedbackEnabled>,
subdomains_enabled: Option<SubdomainsEnabled>,
tracking_enabled: Option<TrackingEnabled>,
Expand Down Expand Up @@ -76,6 +80,11 @@ impl DomainBuilder {
self
}

pub fn reason(mut self, value: impl Into<String>) -> Self {
self.reason = Some(value.into());
self
}

pub fn feedback_enabled(mut self, value: FeedbackEnabled) -> Self {
self.feedback_enabled = Some(value);
self
Expand Down Expand Up @@ -128,6 +137,7 @@ impl DomainBuilder {
domain_id: self.domain_id.ok_or_else(|| BuildError::missing_field("domain_id"))?,
domain: self.domain.ok_or_else(|| BuildError::missing_field("domain"))?,
status: self.status.ok_or_else(|| BuildError::missing_field("status"))?,
reason: self.reason,
feedback_enabled: self.feedback_enabled.ok_or_else(|| BuildError::missing_field("feedback_enabled"))?,
subdomains_enabled: self.subdomains_enabled.ok_or_else(|| BuildError::missing_field("subdomains_enabled"))?,
tracking_enabled: self.tracking_enabled.ok_or_else(|| BuildError::missing_field("tracking_enabled"))?,
Expand Down
98 changes: 98 additions & 0 deletions agentmail-types/src/types/get_setup_link_response.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
pub use crate::prelude::*;
#[allow(unused_imports)]
use super::*;

#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq, Hash)]
pub struct GetSetupLinkResponse {
/// Whether one-click setup is available for this domain. `false` means the domain's DNS provider does not support Domain Connect (or does not carry the AgentMail template yet) — add the domain's `records` manually instead.
#[serde(default)]
pub supported: bool,
/// Display name of the domain's DNS provider, for the setup button label.
#[serde(skip_serializing_if = "Option::is_none")]
pub provider_name: Option<String>,
/// The signed Domain Connect apply URL. Open it in a browser: the domain owner signs in at their DNS provider, reviews the records, and approves — the provider writes them.
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
/// Suggested popup width from the provider, in pixels.
#[serde(skip_serializing_if = "Option::is_none")]
pub width: Option<i64>,
/// Suggested popup height from the provider, in pixels.
#[serde(skip_serializing_if = "Option::is_none")]
pub height: Option<i64>,
/// Opaque value echoed back on the provider's redirect. Store it before opening the URL and compare on return to tie the redirect to this request.
#[serde(skip_serializing_if = "Option::is_none")]
pub state: Option<String>,
/// Set when the domain currently has another email provider's MX records (for example Google Workspace). Applying the template would replace them — warn before proceeding.
#[serde(skip_serializing_if = "Option::is_none")]
pub conflicting_provider: Option<String>,
}

impl GetSetupLinkResponse {
pub fn builder() -> GetSetupLinkResponseBuilder {
<GetSetupLinkResponseBuilder as Default>::default()
}
}

#[derive(Clone, PartialEq, Default, Debug)]
#[non_exhaustive]
pub struct GetSetupLinkResponseBuilder {
supported: Option<bool>,
provider_name: Option<String>,
url: Option<String>,
width: Option<i64>,
height: Option<i64>,
state: Option<String>,
conflicting_provider: Option<String>,
}

impl GetSetupLinkResponseBuilder {
pub fn supported(mut self, value: bool) -> Self {
self.supported = Some(value);
self
}

pub fn provider_name(mut self, value: impl Into<String>) -> Self {
self.provider_name = Some(value.into());
self
}

pub fn url(mut self, value: impl Into<String>) -> Self {
self.url = Some(value.into());
self
}

pub fn width(mut self, value: i64) -> Self {
self.width = Some(value);
self
}

pub fn height(mut self, value: i64) -> Self {
self.height = Some(value);
self
}

pub fn state(mut self, value: impl Into<String>) -> Self {
self.state = Some(value.into());
self
}

pub fn conflicting_provider(mut self, value: impl Into<String>) -> Self {
self.conflicting_provider = Some(value.into());
self
}

/// Consumes the builder and constructs a [`GetSetupLinkResponse`].
/// This method will fail if any of the following fields are not set:
/// - [`supported`](GetSetupLinkResponseBuilder::supported)
pub fn build(self) -> Result<GetSetupLinkResponse, BuildError> {
Ok(GetSetupLinkResponse {
supported: self.supported.ok_or_else(|| BuildError::missing_field("supported"))?,
provider_name: self.provider_name,
url: self.url,
width: self.width,
height: self.height,
state: self.state,
conflicting_provider: self.conflicting_provider,
})
}
}
4 changes: 3 additions & 1 deletion agentmail-types/src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
//!
//! ## Type Categories
//!
//! - **Request/Response Types**: 97 types for API operations
//! - **Request/Response Types**: 98 types for API operations
//! - **Model Types**: 245 types for data representation

pub mod limit;
Expand Down Expand Up @@ -138,6 +138,7 @@ pub mod client_id;
pub mod domain;
pub mod domain_item;
pub mod list_domains_response;
pub mod get_setup_link_response;
pub mod create_domain_request;
pub mod update_domain_request;
pub mod draft_id;
Expand Down Expand Up @@ -481,6 +482,7 @@ pub use client_id::ClientId;
pub use domain::Domain;
pub use domain_item::DomainItem;
pub use list_domains_response::ListDomainsResponse;
pub use get_setup_link_response::GetSetupLinkResponse;
pub use create_domain_request::CreateDomainRequest;
pub use update_domain_request::UpdateDomainRequest;
pub use draft_id::DraftId;
Expand Down
10 changes: 10 additions & 0 deletions agentmail-types/src/types/verification_record.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ pub struct VerificationRecord {
/// The priority of the MX record.
#[serde(skip_serializing_if = "Option::is_none")]
pub priority: Option<i64>,
/// Why the record is INVALID, when known. `duplicate_records` means the expected value is present but extra records coexist at the same name; `value_mismatch` means a record exists but does not match the expected value.
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}

impl VerificationRecord {
Expand All @@ -33,6 +36,7 @@ pub struct VerificationRecordBuilder {
value: Option<String>,
status: Option<RecordStatus>,
priority: Option<i64>,
reason: Option<String>,
}

impl VerificationRecordBuilder {
Expand Down Expand Up @@ -61,6 +65,11 @@ impl VerificationRecordBuilder {
self
}

pub fn reason(mut self, value: impl Into<String>) -> Self {
self.reason = Some(value.into());
self
}

/// Consumes the builder and constructs a [`VerificationRecord`].
/// This method will fail if any of the following fields are not set:
/// - [`r#type`](VerificationRecordBuilder::r#type)
Expand All @@ -74,6 +83,7 @@ impl VerificationRecordBuilder {
value: self.value.ok_or_else(|| BuildError::missing_field("value"))?,
status: self.status.ok_or_else(|| BuildError::missing_field("status"))?,
priority: self.priority,
reason: self.reason,
})
}
}
1 change: 0 additions & 1 deletion cli/agentmail/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ use fern_cli_sdk::auth::{BearerAuth};
fn main() {
let app = CliApp::new("agentmail")
.auth(BearerAuth::new("BearerAuth").env("AGENTMAIL_API_KEY"))
.auth(BearerAuth::new("TokenAuth").env("AGENTMAIL_TOKEN"))
.binding(
OpenApiBinding::new()
.spec(include_str!("openapi0.json"))
Expand Down
2 changes: 1 addition & 1 deletion cli/agentmail/openapi0.json

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,16 @@ agentmail domains get --domain-id <domain_id>
|------|------|----------|-------------|
| `--domain-id` | `DomainId` | Yes | |

#### `agentmail domains get-setup-link`

Build a one-click DNS setup link for the domain via the Domain Connect standard. When the domain's DNS provider supports Domain Connect and carries the AgentMail template, the response contains a signed URL: opening it lets the domain owner approve the required DNS records at their provider, which writes them automatically — no copy-paste. When the provider does not support it, `supported` is `false` and the domain's `records` should be added manually instead.

`GET /v0/domains/{domain_id}/setup-link`

| Flag | Type | Required | Description |
|------|------|----------|-------------|
| `--domain-id` | `DomainId` | Yes | |

#### `agentmail domains get-zone-file`

**CLI:**
Expand Down
Loading
Loading