Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
4 changes: 4 additions & 0 deletions .envrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export MAILTRAP_ACCOUNT_ID="op://Mailtrap Dev/Mailtrap SDK Dev API Key/account_id"
export MAILTRAP_ORGANIZATION_ID="op://Mailtrap Dev/Mailtrap SDK Dev API Key/organization_id"
export MAILTRAP_API_KEY="op://Mailtrap Dev/Mailtrap SDK Dev API Key/account_api_token"
export MAILTRAP_ORGANIZATION_API_KEY="op://Mailtrap Dev/Mailtrap SDK Dev API Key/organization_api_token"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 change: 1 addition & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dotnet 10.0.102
Comment thread
IgorDobryn marked this conversation as resolved.
Outdated
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Changelog

## [Unreleased]

### Features

- **API Tokens API** — Added full CRUD support:
- `IAccountResource.ApiTokens().GetAll()` — `GET /api/accounts/{account_id}/api_tokens`
- `IAccountResource.ApiTokens().Create(request)` — `POST /api/accounts/{account_id}/api_tokens`
- `IAccountResource.ApiToken(id).GetDetails()` — `GET /api/accounts/{account_id}/api_tokens/{id}`
- `IAccountResource.ApiToken(id).Delete()` — `DELETE /api/accounts/{account_id}/api_tokens/{id}`
- `IAccountResource.ApiToken(id).Reset()` — `POST /api/accounts/{account_id}/api_tokens/{id}/reset`
- **Organizations API** — Added new top-level `IMailtrapOrganizationClient` (spawned via `MailtrapClientFactory.CreateOrganizationClient()`) for organization-scoped operations:
- `IOrganizationResource.SubAccounts().GetAll()` — `GET /api/organizations/{organization_id}/sub_accounts`
- `IOrganizationResource.SubAccounts().Create(request)` — `POST /api/organizations/{organization_id}/sub_accounts`

## [3.1.1] - 2026-03-30

### Fixes & Maintenance
Expand Down
53 changes: 53 additions & 0 deletions src/Mailtrap.Abstractions/Accounts/IAccountResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -246,4 +246,57 @@ public interface IAccountResource : IRestResource
/// When <paramref name="sendingMessageId"/> is null or empty.
/// </exception>
public IEmailLogResource EmailLog(string sendingMessageId);

/// <summary>
/// Gets API token collection resource for the account, represented by this resource instance.
/// </summary>
///
/// <returns>
/// API token collection resource for the account, represented by this resource instance.
/// </returns>
public IApiTokenCollectionResource ApiTokens();

/// <summary>
/// Gets resource for specific API token, identified by <paramref name="apiTokenId"/>.
/// </summary>
///
/// <param name="apiTokenId">
/// ID of API token to get resource for.
/// </param>
///
/// <returns>
/// Resource for the API token with specified ID.
/// </returns>
///
/// <exception cref="ArgumentOutOfRangeException">
/// When <paramref name="apiTokenId"/> is less than or equal to zero.
/// </exception>
public IApiTokenResource ApiToken(long apiTokenId);


/// <summary>
/// Gets webhook collection resource for the account, represented by this resource instance.
/// </summary>
///
/// <returns>
/// Webhook collection resource for the account, represented by this resource instance.
/// </returns>
public IWebhookCollectionResource Webhooks();

/// <summary>
/// Gets resource for specific webhook, identified by <paramref name="webhookId"/>.
/// </summary>
///
/// <param name="webhookId">
/// ID of webhook to get resource for.
/// </param>
///
/// <returns>
/// Resource for the webhook with specified ID.
/// </returns>
///
/// <exception cref="ArgumentOutOfRangeException">
/// When <paramref name="webhookId"/> is less than or equal to zero.
/// </exception>
public IWebhookResource Webhook(long webhookId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
namespace Mailtrap.ApiTokens;


/// <summary>
/// Represents API token collection resource.
/// </summary>
public interface IApiTokenCollectionResource : IRestResource
{
/// <summary>
/// List all API tokens visible to the current API token.
/// </summary>
///
/// <param name="cancellationToken">
/// Token to control operation cancellation.
/// </param>
///
/// <returns>
/// Collection of API token details.
/// </returns>
public Task<IList<ApiToken>> GetAll(CancellationToken cancellationToken = default);

/// <summary>
/// Create a new API token.
/// </summary>
///
/// <param name="request">
/// API token creation request.
/// </param>
///
/// <param name="cancellationToken">
/// Token to control operation cancellation.
/// </param>
///
/// <returns>
/// Created API token details, including the full token value.
/// </returns>
///
/// <remarks>
/// The full token value is only returned once at creation — store it securely.
/// </remarks>
public Task<CreateApiTokenResponse> Create(CreateApiTokenRequest request, CancellationToken cancellationToken = default);
}
49 changes: 49 additions & 0 deletions src/Mailtrap.Abstractions/ApiTokens/IApiTokenResource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
namespace Mailtrap.ApiTokens;


/// <summary>
/// Represents a single API token resource.
/// </summary>
public interface IApiTokenResource : IRestResource
{
/// <summary>
/// Get details of the API token represented by this resource.
/// </summary>
///
/// <param name="cancellationToken">
/// Token to control operation cancellation.
/// </param>
///
/// <returns>
/// API token details.
/// </returns>
public Task<ApiToken> GetDetails(CancellationToken cancellationToken = default);

/// <summary>
/// Permanently delete the API token represented by this resource.
/// </summary>
///
/// <param name="cancellationToken">
/// Token to control operation cancellation.
/// </param>
public Task Delete(CancellationToken cancellationToken = default);

/// <summary>
/// Reset the API token represented by this resource.
/// </summary>
///
/// <param name="cancellationToken">
/// Token to control operation cancellation.
/// </param>
///
/// <returns>
/// New API token details, including the full token value.
/// </returns>
///
/// <remarks>
/// Expires the requested token and creates a new token with the same permissions.
/// The old token stops working after a short grace period. The response includes
/// the new token value — store it securely; it is only returned once.
/// </remarks>
public Task<ApiTokenResetResponse> Reset(CancellationToken cancellationToken = default);
}
76 changes: 76 additions & 0 deletions src/Mailtrap.Abstractions/ApiTokens/Models/ApiToken.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
namespace Mailtrap.ApiTokens.Models;


/// <summary>
/// Represents API token details.
/// </summary>
public sealed record ApiToken
{
/// <summary>
/// Gets the API token identifier.
/// </summary>
///
/// <value>
/// API token identifier.
/// </value>
[JsonPropertyName("id")]
[JsonPropertyOrder(1)]
[JsonRequired]
public long Id { get; set; }

/// <summary>
/// Gets the API token display name.
/// </summary>
///
/// <value>
/// API token display name.
/// </value>
[JsonPropertyName("name")]
[JsonPropertyOrder(2)]
public string Name { get; set; } = string.Empty;

/// <summary>
/// Gets the last 4 characters of the token.
/// </summary>
///
/// <value>
/// Last 4 characters of the token. The full token value is only returned on create or reset.
/// </value>
[JsonPropertyName("last_4_digits")]
[JsonPropertyOrder(3)]
public string Last4Digits { get; set; } = string.Empty;

/// <summary>
/// Gets the name of the user or token that created this API token.
/// </summary>
///
/// <value>
/// Creator name or <see langword="null"/> if not available.
/// </value>
[JsonPropertyName("created_by")]
[JsonPropertyOrder(4)]
public string? CreatedBy { get; set; }

/// <summary>
/// Gets the date and time when the API token expires.
/// </summary>
///
/// <value>
/// Expiration date and time, or <see langword="null"/> if the token does not expire.
/// </value>
[JsonPropertyName("expires_at")]
[JsonPropertyOrder(5)]
public DateTimeOffset? ExpiresAt { get; set; }

/// <summary>
/// Gets the resource accesses granted to this API token.
/// </summary>
///
/// <value>
/// Collection of resource accesses.
/// </value>
[JsonPropertyName("resources")]
[JsonPropertyOrder(6)]
[JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)]
public IList<ApiTokenAccess> Resources { get; } = [];
}
42 changes: 42 additions & 0 deletions src/Mailtrap.Abstractions/ApiTokens/Models/ApiTokenAccess.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
namespace Mailtrap.ApiTokens.Models;


/// <summary>
/// Represents an access entry granted to an API token for a specific resource.
/// </summary>
public sealed record ApiTokenAccess
{
/// <summary>
/// Gets the resource type.
/// </summary>
///
/// <value>
/// Resource type.
/// </value>
[JsonPropertyName("resource_type")]
[JsonPropertyOrder(1)]
public ResourceType Type { get; set; } = ResourceType.Unknown;

/// <summary>
/// Gets the resource identifier.
/// </summary>
///
/// <value>
/// Resource identifier.
/// </value>
[JsonPropertyName("resource_id")]
[JsonPropertyOrder(2)]
[JsonRequired]
public long Id { get; set; }

/// <summary>
/// Gets the resource access level.
/// </summary>
///
/// <value>
/// Access level for resource.
/// </value>
[JsonPropertyName("access_level")]
[JsonPropertyOrder(3)]
public AccessLevel AccessLevel { get; set; } = AccessLevel.Indeterminate;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
namespace Mailtrap.ApiTokens.Requests;


/// <summary>
/// Represents an access entry to grant to an API token for a specific resource.
/// </summary>
public sealed record ApiTokenAccessRequest : IValidatable
{
/// <summary>
/// Gets the resource type.
/// </summary>
///
/// <value>
/// Resource type.
/// </value>
[JsonPropertyName("resource_type")]
[JsonPropertyOrder(1)]
public ResourceType ResourceType { get; }

/// <summary>
/// Gets the resource identifier.
/// </summary>
///
/// <value>
/// Resource identifier.
/// </value>
[JsonPropertyName("resource_id")]
[JsonPropertyOrder(2)]
public long ResourceId { get; }

/// <summary>
/// Gets the resource access level.
/// </summary>
///
/// <value>
/// Access level for resource. Allowed values: <see cref="AccessLevel.Viewer"/> or <see cref="AccessLevel.Admin"/>.
/// </value>
[JsonPropertyName("access_level")]
[JsonPropertyOrder(3)]
public AccessLevel AccessLevel { get; }


/// <summary>
/// Primary instance constructor.
/// </summary>
///
/// <param name="resourceType">
/// Type of the resource to grant access to.
/// </param>
///
/// <param name="resourceId">
/// ID of the resource to grant access to.
/// </param>
///
/// <param name="accessLevel">
/// Access level for the resource. Allowed values: <see cref="AccessLevel.Viewer"/> or <see cref="AccessLevel.Admin"/>.
/// </param>
///
/// <exception cref="ArgumentNullException">
/// When <paramref name="resourceType"/> is <see langword="null"/>.
/// </exception>
///
/// <exception cref="ArgumentOutOfRangeException">
/// When <paramref name="resourceId"/> is less than or equal to zero,
/// or <paramref name="accessLevel"/> is not <see cref="AccessLevel.Viewer"/> or <see cref="AccessLevel.Admin"/>.
/// </exception>
public ApiTokenAccessRequest(
ResourceType resourceType,
long resourceId,
AccessLevel accessLevel)
{
Ensure.NotNull(resourceType, nameof(resourceType));
Ensure.GreaterThanZero(resourceId, nameof(resourceId));

if (accessLevel is not AccessLevel.Viewer and not AccessLevel.Admin)
{
throw new ArgumentOutOfRangeException(
nameof(accessLevel),
accessLevel,
"Allowed values are Viewer or Admin");
}

ResourceType = resourceType;
ResourceId = resourceId;
AccessLevel = accessLevel;
}


/// <inheritdoc/>
public ValidationResult Validate()
{
return ApiTokenAccessRequestValidator.Instance
.Validate(this)
.ToMailtrapValidationResult();
}
}
Loading