Appearance
Rust Guide
If you haven't already, see Getting Started. The following examples will assume you have already received your API key and created a namespace.
mailisk-rs
Mailisk offers a Rust library mailisk. This is an official typed async client that wraps the API Reference endpoints into Rust methods.
See the README for more usage examples and the API documentation on docs.rs.
Installation
Add the library and a Tokio runtime with Cargo:
shell
cargo add mailisk
cargo add tokio --features macros,rt-multi-threadThe crate requires Rust 1.88 or newer. TLS uses rustls by default. To use the platform TLS library instead, disable the default features and enable native-tls:
toml
mailisk = { version = "0.1", default-features = false, features = ["native-tls"] }Setup Client
Once installed, create the client by passing in your API key.
rust
use mailisk::MailiskClient;
let mailisk = MailiskClient::new("YOUR_API_KEY")?;The library does not load .env files or environment variables by itself. Load configuration however your application prefers, then pass the API key to the client.
rust
use mailisk::MailiskClient;
let api_key = std::env::var("MAILISK_API_KEY").expect("MAILISK_API_KEY is not set");
let mailisk = MailiskClient::new(api_key)?;Every method is async and returns mailisk::Result<T>. MailiskClient is cheap to clone, and clones share one connection pool, so you can create it once and share it between tests.
Request parameters are plain structs that implement Default. Set only the fields you need and fill in the rest with ..Default::default().
Reading Email
You can read emails using the search_inbox method.
- By default it uses the
waitflag. This means the call won't return until at least one email is received. Disabling this flag viawait: Some(false)can cause it to return an empty response immediately. - The request timeout is adjustable with
search_inbox_with_optionsandRequestOptions. By default it uses a timeout of 5 minutes. - By default it returns emails in the last 15 minutes. This ensures that only new emails are returned. Without this, older emails would also be returned, potentially disrupting you if you were waiting for a specific email. This can be overridden by passing the
from_timestampparameter (from_timestamp: Some(0)will disable filtering by email age).
rust
use std::time::Duration;
use mailisk::{RequestOptions, SearchInboxParams};
// timeout of 5 minutes
mailisk.search_inbox(namespace, &SearchInboxParams::default()).await?;
// timeout of 1 minute
mailisk
.search_inbox_with_options(
namespace,
&SearchInboxParams::default(),
&RequestOptions::timeout(Duration::from_secs(60)),
)
.await?;
// returns immediately, even if the result would be empty
mailisk
.search_inbox(
namespace,
&SearchInboxParams {
wait: Some(false),
..Default::default()
},
)
.await?;Filter fields align with the Search Inbox API reference: to_addr_prefix, from_addr_includes, subject_includes, from_timestamp, to_timestamp, limit, offset, and wait.
rust
let response = mailisk
.search_inbox(
namespace,
&SearchInboxParams {
to_addr_prefix: Some(format!("john@{namespace}.mailisk.net")),
subject_includes: Some("password".into()),
..Default::default()
},
)
.await?;
let emails = response.data;The response body will look similar to this:
json
{
"total_count": 1,
"options": {
"limit": 10,
"offset": 0
},
"data": [
{
"id": "1659368409795-42UcuQtMy",
"from": {
"address": "contact@mailisk.com",
"name": ""
},
"to": [
{
"address": "john@sh7o3t3e4jvj.mailisk.net",
"name": ""
}
],
"subject": "Test - Welcome to Mailisk",
"html": "<html>...",
"text": "*Welcome to Mailisk*\n\nHello there\nWelcome to Mailisk! We're excited to have you!\n\nGo ahead and ...",
"received_date": "2022-08-01T15:40:09.000Z",
"received_timestamp": 1659368409,
"expires_timestamp": 1659372009
}
]
}The client deserializes it into a SearchInboxResponse whose fields have the same names, so the example above is available as response.total_count, response.data[0].subject, and so on. Optional fields such as subject, html, and text are Option<String>, and lists such as cc and attachments are empty when the email has none.
The total_count tells us the total number of emails that match our query. Depending on limit and offset, a subset of this will be returned. See the Search Inbox API reference for more information on the other fields.
Sending Email
Outbound email
Use send_email when you want Mailisk to create and queue an outbound message through the API. Recipients must be verified external destinations or addresses under the same namespace. Messages sent only to the same namespace do not consume outbound usage because they are counted as inbound email when delivered.
rust
use mailisk::{OutboundEmailAddress, SendEmailParams};
let namespace = "mynamespace";
let outbound_email = mailisk
.send_email(
namespace,
&SendEmailParams {
from: Some(OutboundEmailAddress::with_name(
format!("support@{namespace}.mailisk.net"),
"Support",
)),
to: vec![format!("customer@{namespace}.mailisk.net")],
subject: "Hello from Mailisk".into(),
text: Some("This is the plain text body.".into()),
html: Some("<p>This is the HTML body.</p>".into()),
..Default::default()
},
)
.await?;
println!("{} {:?}", outbound_email.id, outbound_email.status);Fetch delivery details with get_outbound_email.
rust
let detail = mailisk.get_outbound_email(&outbound_email.id).await?;
println!("{:?}", detail.delivery_summary);
println!("{:?}", detail.recipients);You can send attachments by base64-encoding the file content, for example with the base64 crate. Use application/octet-stream when you need the file to remain a downloadable attachment across mail clients and parsers.
rust
use base64::Engine;
use base64::engine::general_purpose::STANDARD as BASE64;
use mailisk::{OutboundEmailAttachment, SendEmailParams};
let content = std::fs::read("report.txt")?;
mailisk
.send_email(
namespace,
&SendEmailParams {
to: vec![format!("customer@{namespace}.mailisk.net")],
subject: "Report".into(),
text: Some("Attached.".into()),
attachments: vec![OutboundEmailAttachment {
filename: "report.txt".into(),
content_type: "application/octet-stream".into(),
content_base64: BASE64.encode(&content),
content_id: None,
disposition: None,
}],
..Default::default()
},
)
.await?;To verify the attachment arrived, search the inbox and download the attachment by ID.
rust
let response = mailisk
.search_inbox(
namespace,
&SearchInboxParams {
subject_includes: Some("Report".into()),
to_addr_prefix: Some(format!("customer@{namespace}.mailisk.net")),
..Default::default()
},
)
.await?;
let attachment = &response.data[0].attachments[0];
let downloaded = mailisk.download_attachment(&attachment.id).await?;
assert_eq!(downloaded, content, "Attachment content did not match");Reply and forward
Use reply_to_email to send a reply to an inbound message.
rust
use mailisk::ReplyToEmailParams;
let reply = mailisk
.reply_to_email(
&emails[0].id,
&ReplyToEmailParams {
subject: Some("Re: Thanks".into()),
text: Some("We received your message.".into()),
..Default::default()
},
)
.await?;
println!("{} {:?}", reply.id, reply.status);Use forward_email to forward an inbound message to another recipient.
rust
use mailisk::ForwardEmailParams;
let forwarded = mailisk
.forward_email(
&emails[0].id,
&ForwardEmailParams {
to: vec!["verified@example.com".into()],
subject: Some("Fwd: Support request".into()),
text: Some("Forwarding this along.".into()),
..Default::default()
},
)
.await?;
println!("{} {:?}", forwarded.id, forwarded.status);Request SMS Numbers
Request and activate an SMS number from the dashboard (see SMS Testing). Surface it to your Rust tests via configuration, e.g. std::env::var("MAILISK_SMS_NUMBER"), so your tests all hit the same destination.
Listing SMS Numbers
Surface the list of approved numbers tied to your API key with list_sms_numbers(). This is useful for one-time setup scripts or sanity checks in CI before running OTP flows.
rust
use mailisk::SmsNumberStatus;
let sms_numbers = mailisk.list_sms_numbers().await?.data;
assert!(!sms_numbers.is_empty(), "No SMS numbers available");
let active_number = sms_numbers
.iter()
.find(|number| number.status == SmsNumberStatus::Active)
.expect("Activate an SMS number before running tests");
println!("Using SMS number {:?}", active_number.phone_number);Reading SMS
Use search_sms_messages(phone_number, params) to poll for inbound texts. It follows the same defaults as search_inbox:
waitdefaults totrue, holding the request open until at least one SMS matches the filters.search_sms_messages_with_optionslets you override the 5 minute timeout to handle slower OTP flows.- Filter fields align with the Search SMS API:
body,from_number,direction,from_date,to_date,limit,offset, etc. Date filters such asfrom_dateandto_dateshould be ISO timestamp strings.
rust
use std::time::Duration;
use mailisk::{RequestOptions, SearchSmsMessagesParams};
let sms_number = std::env::var("MAILISK_SMS_NUMBER").expect("MAILISK_SMS_NUMBER is not set");
let response = mailisk
.search_sms_messages_with_options(
&sms_number,
&SearchSmsMessagesParams {
body: Some("Your login code".into()),
..Default::default()
},
&RequestOptions::timeout(Duration::from_secs(120)),
)
.await?;
let sms = response.data.first().expect("SMS not received in time");
let otp = sms
.body
.split(|c: char| !c.is_ascii_digit())
.find(|part| part.len() == 6)
.expect("SMS did not contain a 6 digit code");If you need to reuse the same number across test runs, store run-specific state in your application (user IDs, tenants, etc.) and filter using the message body. Alternatively, avoid running parallel tests that share the same number, and set from_date to an ISO timestamp string to exclude messages created before the current test run.
Sending Virtual SMS
To simulate inbound SMS (useful for previewing one-off notifications or driving tests without your production provider), call send_virtual_sms. Provide both numbers in E.164 or local dial format; Mailisk will deliver the SMS into the destination number you own.
rust
use mailisk::SendVirtualSmsParams;
mailisk
.send_virtual_sms(&SendVirtualSmsParams {
from_number: "+15551234567".into(),
to_number: std::env::var("MAILISK_SMS_NUMBER").expect("MAILISK_SMS_NUMBER is not set"),
body: "Your login code is 123456".into(),
})
.await?;Virtual SMS do not consume your regular inbound quota and land in the same search results as provider-delivered messages, so you can keep using search_sms_messages to assert content.
Authenticator TOTP
The Rust client can generate authenticator app one-time passwords and manage saved virtual TOTP devices. The SDK calls the Mailisk API for code generation and device storage; it does not generate TOTP codes locally.
Use the flow that matches your test setup:
| Use case | Method |
|---|---|
| Generate a current code from a known Base32 secret without saving anything | get_totp_otp_by_shared_secret(secret, params) |
| Save a reusable device from a Base32 shared secret with default settings | create_totp_device(params) |
| Save a reusable device with custom TOTP settings | create_custom_totp_device(params) |
| Save a device from a Base32 secret-key field | create_totp_device_from_base32_secret_key(params) |
Save a device from an otpauth://totp/... setup URL | create_totp_device_from_otp_auth_url(params) |
| Generate a code for a saved device | get_totp_otp_by_device_id(device_id, params) |
| List or delete saved devices | list_totp_devices(params), delete_totp_device(device_id) |
Generate a code without saving a device
Use this when your test already has the shared secret and does not need saved-device management.
rust
use mailisk::GetTotpOtpParams;
let otp = mailisk
.get_totp_otp_by_shared_secret("JBSWY3DPEHPK3PXP", &GetTotpOtpParams::default())
.await?;
// enter otp.code into the form under test
println!("Code {} expires at {}", otp.code, otp.expires);This method uses the default TOTP settings: 6 digits, a 30 second period, and SHA1.
Save a device and reuse it later
Use create_totp_device for a saved device with the default TOTP settings.
rust
use mailisk::{CreateTotpDeviceParams, GetTotpOtpParams};
let device = mailisk
.create_totp_device(&CreateTotpDeviceParams {
name: Some("GitHub staging".into()),
shared_secret: "JBSWY3DPEHPK3PXP".into(),
expires_at: Some("2026-06-01T12:00:00.000Z".into()),
})
.await?;
let otp = mailisk
.get_totp_otp_by_device_id(&device.id, &GetTotpOtpParams::default())
.await?;Use create_custom_totp_device when the application under test uses non-default settings or when you want issuer and username metadata on the saved device. The optional device settings are shared by the custom, Base32, and otpauth URL methods through TotpDeviceOptions.
rust
use mailisk::{CreateCustomTotpDeviceParams, TotpAlgorithm, TotpDeviceOptions};
let device = mailisk
.create_custom_totp_device(&CreateCustomTotpDeviceParams {
secret: "JBSWY3DPEHPK3PXP".into(),
options: TotpDeviceOptions {
name: Some("GitHub staging".into()),
username: Some("qa@example.com".into()),
issuer: Some("GitHub".into()),
digits: Some(6),
period: Some(30),
algorithm: Some(TotpAlgorithm::Sha1),
..Default::default()
},
})
.await?;Create a device from a Base32 secret key
Some test setup flows expose the shared secret as a Base32 secret key. Use create_totp_device_from_base32_secret_key when that field name matches your integration.
rust
use mailisk::{CreateBase32SecretKeyTotpDeviceParams, TotpDeviceOptions};
let device = mailisk
.create_totp_device_from_base32_secret_key(&CreateBase32SecretKeyTotpDeviceParams {
base32_secret_key: "JBSWY3DPEHPK3PXP".into(),
options: TotpDeviceOptions {
username: Some("qa@example.com".into()),
issuer: Some("GitHub".into()),
..Default::default()
},
})
.await?;Create a device from an otpauth URL
Use create_totp_device_from_otp_auth_url when your application exposes the authenticator setup URL directly or through a QR-code payload.
rust
use mailisk::{CreateOtpAuthUrlTotpDeviceParams, TotpDeviceOptions};
let device = mailisk
.create_totp_device_from_otp_auth_url(&CreateOtpAuthUrlTotpDeviceParams {
otp_auth_url: "otpauth://totp/GitHub:qa@example.com?secret=JBSWY3DPEHPK3PXP&issuer=GitHub".into(),
options: TotpDeviceOptions {
name: Some("GitHub staging".into()),
..Default::default()
},
})
.await?;The URL must include a secret query parameter. Values inside the URL are used for fields such as issuer, username, digits, period, and algorithm when present.
Avoid near-expired codes
Pass min_seconds_until_expire when your test needs a code with enough validity left for form submission. If the current code has fewer seconds remaining, Mailisk waits for the next TOTP period before returning.
rust
let min_validity = GetTotpOtpParams {
min_seconds_until_expire: Some(10),
};
let otp = mailisk
.get_totp_otp_by_shared_secret("JBSWY3DPEHPK3PXP", &min_validity)
.await?;For saved devices, pass the same option when requesting the code by device ID:
rust
let otp = mailisk.get_totp_otp_by_device_id(&device.id, &min_validity).await?;The value defaults to 0, which returns the current code immediately. It must be lower than the TOTP period.
List and delete saved devices
list_totp_devices returns active, non-expired devices for the organisation. issuer and username filters are case-insensitive partial matches.
rust
use mailisk::ListTotpDevicesParams;
let result = mailisk
.list_totp_devices(&ListTotpDevicesParams {
limit: Some(20),
offset: Some(0),
issuer: Some("GitHub".into()),
username: Some("qa@example.com".into()),
})
.await?;
println!("{}", result.total_count);
println!("{:?}", result.items);Delete saved devices when they are no longer needed.
rust
mailisk.delete_totp_device(&device.id).await?;Successful deletes return ().
Supported TOTP settings
Default settings:
| Setting | Default |
|---|---|
digits | 6 |
period | 30 seconds |
algorithm | SHA1 |
Supported custom values:
| Field | Accepted values |
|---|---|
digits | 6 or 8 |
period | Integer from 10 to 300 seconds |
algorithm | TotpAlgorithm::Sha1, TotpAlgorithm::Sha256, or TotpAlgorithm::Sha512 |
Secrets are write-only. The API accepts shared secrets when creating devices or generating codes, but saved device responses do not include the secret.
Response shapes
Saved device responses use this shape:
rust
pub struct TotpDevice {
pub id: String,
pub organisation_id: String,
pub name: String,
pub username: Option<String>,
pub issuer: Option<String>,
pub digits: u8,
pub period: u32,
pub algorithm: TotpAlgorithm,
pub source: String,
pub expires_at: Option<String>,
pub created_at: String,
pub updated_at: String,
}OTP responses include the current code and an ISO timestamp for when that code expires:
rust
pub struct TotpOtpResponse {
pub code: String,
pub expires: String,
}For REST request examples and endpoint-level errors, see the Authenticator TOTP API reference.
Errors
When the API responds with an error status, methods return mailisk::Error::Api with the status code and the raw response body. Network failures and timeouts are returned as mailisk::Error::Request.
rust
match mailisk.list_namespaces().await {
Ok(namespaces) => println!("{} namespaces", namespaces.total_count),
Err(mailisk::Error::Api { status, body }) => eprintln!("API error {status}: {body}"),
Err(error) => eprintln!("{error}"),
}Types
Request and response types are exported from the crate root, for example SendEmailParams, SearchInboxResponse, OutboundEmail, OutboundEmailDetail, and TotpOtpResponse. Field names match the JSON fields in the API Reference, except that fields named type are exposed as kind. Status enums such as OutboundEmailStatus include an Unknown variant, so values added to the API later do not break deserialization.
