use std::time::Duration;
use base64::Engine;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HelloResult {
#[serde(default)]
pub timestamp: String,
#[serde(default)]
pub client_ip: String,
#[serde(default)]
pub hai_public_key_fingerprint: String,
#[serde(default)]
pub message: String,
#[serde(default)]
pub hai_signed_ack: String,
#[serde(default)]
pub hello_id: String,
#[serde(default)]
pub test_scenario: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CreateAgentOptions {
pub name: String,
pub password: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub algorithm: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data_directory: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub key_directory: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub config_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub agent_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub domain: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default_storage: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CreateAgentResult {
#[serde(default)]
pub agent_id: String,
#[serde(default)]
pub name: String,
#[serde(default)]
pub public_key_path: String,
#[serde(default)]
pub config_path: String,
#[serde(default)]
pub version: String,
#[serde(default)]
pub algorithm: String,
#[serde(default)]
pub private_key_path: String,
#[serde(default)]
pub data_directory: String,
#[serde(default)]
pub key_directory: String,
#[serde(default)]
pub domain: String,
#[serde(default)]
pub dns_record: String,
}
#[derive(Debug, Clone, Default)]
pub struct RotateKeysOptions {
pub register_with_hai: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RotationResult {
pub jacs_id: String,
pub old_version: String,
pub new_version: String,
pub new_public_key_hash: String,
pub registered_with_hai: bool,
pub signed_agent_json: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateAgentResult {
pub jacs_id: String,
pub old_version: String,
pub new_version: String,
pub signed_agent_json: String,
#[serde(default)]
pub registered_with_hai: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MigrateAgentResult {
pub jacs_id: String,
pub old_version: String,
pub new_version: String,
pub patched_fields: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct RegisterAgentOptions {
pub agent_json: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub public_key_pem: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub owner_email: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub domain: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub registration_key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_mediator: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct RegistrationResult {
#[serde(default)]
pub success: bool,
#[serde(default)]
pub agent_id: String,
#[serde(default)]
pub jacs_id: String,
#[serde(default)]
pub dns_verified: bool,
#[serde(default)]
pub registrations: Vec<RegistrationEntry>,
#[serde(default)]
pub registered_at: String,
#[serde(default)]
pub message: Option<String>,
#[serde(default)]
pub email: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegistrationEntry {
#[serde(default)]
pub key_id: String,
#[serde(default)]
pub algorithm: String,
#[serde(default)]
pub signature_json: String,
#[serde(default)]
pub signed_at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VerifyAgentResult {
#[serde(default)]
pub jacs_id: String,
#[serde(default)]
pub registered: bool,
#[serde(default)]
pub registrations: Vec<RegistrationEntry>,
#[serde(default)]
pub dns_verified: bool,
#[serde(default)]
pub registered_at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JobResponseResult {
#[serde(default)]
pub success: bool,
#[serde(default)]
pub job_id: String,
#[serde(default)]
pub message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct UpdateUsernameResult {
#[serde(default)]
pub username: String,
#[serde(default)]
pub email: String,
#[serde(default)]
pub previous_username: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct DeleteUsernameResult {
#[serde(default)]
pub released_username: String,
#[serde(default)]
pub cooldown_until: String,
#[serde(default)]
pub message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum FieldStatus {
Pass,
Modified,
Fail,
Unverifiable,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct FieldResult {
#[serde(default)]
pub field: String,
pub status: FieldStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub original_hash: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub current_hash: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub original_value: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub current_value: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ChainEntry {
#[serde(default)]
pub signer: String,
#[serde(default)]
pub jacs_id: String,
#[serde(default)]
pub valid: bool,
#[serde(default)]
pub forwarded: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmailVerificationResultV2 {
pub valid: bool,
#[serde(default)]
pub jacs_id: String,
#[serde(default)]
pub algorithm: String,
#[serde(default)]
pub reputation_tier: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub dns_verified: Option<bool>,
#[serde(default)]
pub field_results: Vec<FieldResult>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub chain: Vec<ChainEntry>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub agent_status: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub benchmarks_completed: Vec<String>,
}
impl EmailVerificationResultV2 {
pub fn err(jacs_id: &str, reputation_tier: &str, error: &str) -> Self {
Self {
valid: false,
jacs_id: jacs_id.to_string(),
algorithm: String::new(),
reputation_tier: reputation_tier.to_string(),
dns_verified: None,
field_results: Vec::new(),
chain: Vec::new(),
error: Some(error.to_string()),
agent_status: None,
benchmarks_completed: Vec::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmailAttachment {
pub filename: String,
pub content_type: String,
#[serde(skip)]
pub data: Vec<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data_base64: Option<String>,
}
impl EmailAttachment {
pub fn new(filename: String, content_type: String, data: Vec<u8>) -> Self {
Self {
filename,
content_type,
data,
data_base64: None,
}
}
pub fn effective_data(&self) -> Vec<u8> {
if !self.data.is_empty() {
return self.data.clone();
}
if let Some(ref b64) = self.data_base64 {
if let Ok(decoded) = base64::engine::general_purpose::STANDARD.decode(b64) {
return decoded;
}
}
Vec::new()
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SendEmailOptions {
pub to: String,
pub subject: String,
pub body: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub cc: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub bcc: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub in_reply_to: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub attachments: Vec<EmailAttachment>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub labels: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub append_footer: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SendEmailResult {
#[serde(default)]
pub message_id: String,
#[serde(default)]
pub status: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ListMessagesOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub offset: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub direction: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_read: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub folder: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub has_attachments: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmailMessage {
#[serde(default)]
pub id: String,
#[serde(default)]
pub direction: String,
#[serde(default)]
pub from_address: String,
#[serde(default)]
pub to_address: String,
#[serde(default)]
pub subject: String,
#[serde(default)]
pub body_text: String,
#[serde(default)]
pub message_id: Option<String>,
#[serde(default)]
pub in_reply_to: Option<String>,
#[serde(default)]
pub is_read: bool,
#[serde(default)]
pub delivery_status: String,
#[serde(default)]
pub created_at: String,
#[serde(default)]
pub read_at: Option<String>,
#[serde(default)]
pub jacs_verified: Option<bool>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub cc_addresses: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub labels: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub trust_score: Option<f32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub body_text_clean: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub quoted_text: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thread: Option<Vec<EmailMessage>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SearchOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub q: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub direction: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub from_address: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub to_address: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub since: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub until: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_read: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub jacs_verified: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub folder: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub has_attachments: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub offset: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmailStatus {
#[serde(default)]
pub email: String,
#[serde(default)]
pub status: String,
#[serde(default)]
pub tier: String,
#[serde(default)]
pub billing_tier: String,
#[serde(default)]
pub messages_sent_24h: i32,
#[serde(default)]
pub daily_limit: i32,
#[serde(default)]
pub daily_used: i32,
#[serde(default)]
pub resets_at: String,
#[serde(default)]
pub messages_sent_total: i32,
#[serde(default)]
pub external_enabled: bool,
#[serde(default)]
pub external_sends_today: i32,
#[serde(default)]
pub last_tier_change: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub volume: Option<EmailVolumeInfo>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub delivery: Option<EmailDeliveryInfo>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reputation: Option<EmailReputationInfo>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct EmailVolumeInfo {
#[serde(default)]
pub sent_total: i64,
#[serde(default)]
pub received_total: i64,
#[serde(default)]
pub sent_24h: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct EmailDeliveryInfo {
#[serde(default)]
pub bounce_count: i32,
#[serde(default)]
pub spam_report_count: i32,
#[serde(default)]
pub delivery_rate: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct EmailReputationInfo {
#[serde(default)]
pub score: f64,
#[serde(default)]
pub tier: String,
#[serde(default)]
pub email_score: f64,
#[serde(default)]
pub hai_score: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Contact {
#[serde(default)]
pub email: String,
#[serde(default)]
pub display_name: Option<String>,
#[serde(default)]
pub last_contact: String,
#[serde(default)]
pub jacs_verified: bool,
#[serde(default)]
pub reputation_tier: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeyRegistryResponse {
#[serde(default)]
pub email: String,
#[serde(default)]
pub jacs_id: String,
#[serde(default)]
pub public_key: String,
#[serde(default)]
pub algorithm: String,
#[serde(default)]
pub reputation_tier: String,
#[serde(default)]
pub registered_at: String,
#[serde(default)]
pub agent_status: Option<String>,
#[serde(default)]
pub benchmarks_completed: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PublicKeyInfo {
#[serde(default)]
pub jacs_id: String,
#[serde(default)]
pub version: String,
#[serde(default)]
pub public_key: String,
#[serde(default)]
pub public_key_raw_b64: String,
#[serde(default)]
pub algorithm: String,
#[serde(default)]
pub public_key_hash: String,
#[serde(default)]
pub status: String,
#[serde(default)]
pub dns_verified: bool,
#[serde(default)]
pub created_at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentKeyHistory {
#[serde(default)]
pub jacs_id: String,
#[serde(default)]
pub keys: Vec<PublicKeyInfo>,
#[serde(default)]
pub total: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct DocumentVerificationResult {
#[serde(default)]
pub valid: bool,
#[serde(default)]
pub verified_at: String,
#[serde(default)]
pub document_type: String,
#[serde(default)]
pub issuer_verified: bool,
#[serde(default)]
pub signature_verified: bool,
#[serde(default)]
pub signer_id: String,
#[serde(default)]
pub signed_at: String,
#[serde(default)]
pub error: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SignedPayload {
pub signed_document: String,
pub agent_jacs_id: String,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase")]
pub enum TransportType {
#[default]
Sse,
Ws,
}
impl TransportType {
pub fn as_str(self) -> &'static str {
match self {
Self::Sse => "sse",
Self::Ws => "ws",
}
}
}
#[derive(Debug, Clone)]
pub struct ProRunOptions {
pub transport: TransportType,
pub poll_interval: Duration,
pub poll_timeout: Duration,
}
impl Default for ProRunOptions {
fn default() -> Self {
Self {
transport: TransportType::Sse,
poll_interval: Duration::from_secs(2),
poll_timeout: Duration::from_secs(300),
}
}
}
pub type DnsCertifiedRunOptions = ProRunOptions;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct TranscriptMessage {
#[serde(default)]
pub role: String,
#[serde(default)]
pub content: String,
#[serde(default)]
pub timestamp: String,
#[serde(default)]
pub annotations: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct FreeChaoticResult {
#[serde(default)]
pub success: bool,
#[serde(default)]
pub run_id: String,
#[serde(default)]
pub transcript: Vec<TranscriptMessage>,
#[serde(default)]
pub upsell_message: String,
#[serde(default)]
pub raw_response: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ProRunResult {
#[serde(default)]
pub success: bool,
#[serde(default)]
pub run_id: String,
#[serde(default)]
pub score: f64,
#[serde(default)]
pub transcript: Vec<TranscriptMessage>,
#[serde(default)]
pub payment_id: String,
#[serde(default)]
pub raw_response: Value,
}
pub type DnsCertifiedResult = ProRunResult;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SignedDocument {
pub key: String,
pub json: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DocSearchResults {
pub results: Vec<DocSearchHit>,
pub total_count: usize,
pub method: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DocSearchHit {
pub key: String,
pub json: String,
pub score: f64,
pub matched_fields: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct StorageCapabilities {
pub fulltext: bool,
pub vector: bool,
pub query_by_field: bool,
pub query_by_type: bool,
pub pagination: bool,
pub tombstone: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DocVerificationResult {
pub key: String,
pub valid: bool,
pub error: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub signer_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timestamp: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub signer_name: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct VerificationStatus {
#[serde(default)]
pub jacs_valid: bool,
#[serde(default)]
pub dns_valid: bool,
#[serde(default)]
pub hai_registered: bool,
#[serde(default)]
pub badge: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct AgentVerificationResult {
#[serde(default)]
pub agent_id: String,
#[serde(default)]
pub verification: VerificationStatus,
#[serde(default)]
pub hai_signatures: Vec<String>,
#[serde(default)]
pub verified_at: String,
#[serde(default)]
pub errors: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct VerifyAgentDocumentRequest {
pub agent_json: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub public_key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub domain: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct HaiEvent {
#[serde(default)]
pub event_type: String,
#[serde(default)]
pub data: Value,
#[serde(default)]
pub id: Option<String>,
#[serde(default)]
pub raw: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmailTemplate {
pub id: String,
pub agent_id: String,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub how_to_send: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub how_to_respond: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub goal: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rules: Option<String>,
pub created_at: String,
pub updated_at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CreateEmailTemplateOptions {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub how_to_send: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub how_to_respond: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub goal: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rules: Option<String>,
}
fn serialize_double_option<S>(value: &Option<Option<String>>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match value {
Some(inner) => inner.serialize(serializer),
None => serializer.serialize_none(),
}
}
fn deserialize_double_option<'de, D>(deserializer: D) -> Result<Option<Option<String>>, D::Error>
where
D: Deserializer<'de>,
{
let value: Option<String> = Option::deserialize(deserializer)?;
Ok(Some(value))
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct UpdateEmailTemplateOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
serialize_with = "serialize_double_option",
deserialize_with = "deserialize_double_option"
)]
pub how_to_send: Option<Option<String>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
serialize_with = "serialize_double_option",
deserialize_with = "deserialize_double_option"
)]
pub how_to_respond: Option<Option<String>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
serialize_with = "serialize_double_option",
deserialize_with = "deserialize_double_option"
)]
pub goal: Option<Option<String>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
serialize_with = "serialize_double_option",
deserialize_with = "deserialize_double_option"
)]
pub rules: Option<Option<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ListEmailTemplatesOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub offset: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub q: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListEmailTemplatesResult {
pub templates: Vec<EmailTemplate>,
pub total: i64,
pub limit: i64,
pub offset: i64,
}
#[derive(Debug, Clone, Deserialize)]
struct RawEmailWire {
#[serde(default)]
pub message_id: String,
#[serde(default)]
pub rfc_message_id: Option<String>,
#[serde(default)]
pub available: bool,
#[serde(default)]
pub raw_email_b64: Option<String>,
#[serde(default)]
pub size_bytes: Option<usize>,
#[serde(default)]
pub omitted_reason: Option<String>,
}
#[derive(Debug, Clone)]
pub struct RawEmailResponse {
pub message_id: String,
pub rfc_message_id: Option<String>,
pub available: bool,
pub raw_email: Option<Vec<u8>>,
pub size_bytes: Option<usize>,
pub omitted_reason: Option<String>,
}
impl TryFrom<RawEmailWire> for RawEmailResponse {
type Error = crate::error::HaiError;
fn try_from(wire: RawEmailWire) -> std::result::Result<Self, Self::Error> {
let raw_email = match wire.raw_email_b64 {
Some(b64) if !b64.is_empty() => {
let bytes = base64::engine::general_purpose::STANDARD
.decode(b64.as_bytes())
.map_err(|e| crate::error::HaiError::Message(format!(
"invalid base64 in raw_email_b64: {e}"
)))?;
Some(bytes)
}
_ => None,
};
Ok(RawEmailResponse {
message_id: wire.message_id,
rfc_message_id: wire.rfc_message_id,
available: wire.available,
raw_email,
size_bytes: wire.size_bytes,
omitted_reason: wire.omitted_reason,
})
}
}
impl RawEmailResponse {
pub fn from_wire_json(value: serde_json::Value) -> crate::error::Result<Self> {
let wire: RawEmailWire = serde_json::from_value(value)?;
Self::try_from(wire)
}
pub fn to_wire_json(&self) -> serde_json::Value {
let b64 = self
.raw_email
.as_ref()
.map(|bytes| base64::engine::general_purpose::STANDARD.encode(bytes));
serde_json::json!({
"message_id": self.message_id,
"rfc_message_id": self.rfc_message_id,
"available": self.available,
"raw_email_b64": b64,
"size_bytes": self.size_bytes,
"omitted_reason": self.omitted_reason,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn registration_result_deserializes_email() {
let json = r#"{"success": true, "agent_id": "a1", "jacs_id": "j1", "email": "bot@hai.ai"}"#;
let result: RegistrationResult = serde_json::from_str(json).expect("deserialize");
assert_eq!(result.email, Some("bot@hai.ai".to_string()));
}
#[test]
fn registration_result_email_absent_is_none() {
let json = r#"{"success": true, "agent_id": "a1", "jacs_id": "j1"}"#;
let result: RegistrationResult = serde_json::from_str(json).expect("deserialize");
assert_eq!(result.email, None);
}
#[test]
fn raw_email_wire_deserializes_available_true() {
let input = b"raw MIME bytes \r\n with CRLF and \x00 NUL";
let b64 = base64::engine::general_purpose::STANDARD.encode(input);
let json = serde_json::json!({
"message_id": "m-1",
"rfc_message_id": "<a@b>",
"available": true,
"raw_email_b64": b64,
"size_bytes": input.len(),
"omitted_reason": serde_json::Value::Null,
});
let resp = RawEmailResponse::from_wire_json(json).expect("parse");
assert_eq!(resp.message_id, "m-1");
assert_eq!(resp.rfc_message_id.as_deref(), Some("<a@b>"));
assert!(resp.available);
assert_eq!(resp.raw_email.as_deref(), Some(input.as_slice()));
assert_eq!(resp.size_bytes, Some(input.len()));
assert_eq!(resp.omitted_reason, None);
}
#[test]
fn raw_email_wire_deserializes_not_stored() {
let json = serde_json::json!({
"message_id": "m-2",
"available": false,
"raw_email_b64": serde_json::Value::Null,
"omitted_reason": "not_stored",
});
let resp = RawEmailResponse::from_wire_json(json).expect("parse");
assert!(!resp.available);
assert_eq!(resp.raw_email, None);
assert_eq!(resp.omitted_reason.as_deref(), Some("not_stored"));
assert_eq!(resp.size_bytes, None);
}
#[test]
fn raw_email_wire_deserializes_oversize() {
let json = serde_json::json!({
"message_id": "m-3",
"available": false,
"raw_email_b64": serde_json::Value::Null,
"omitted_reason": "oversize",
});
let resp = RawEmailResponse::from_wire_json(json).expect("parse");
assert!(!resp.available);
assert_eq!(resp.raw_email, None);
assert_eq!(resp.omitted_reason.as_deref(), Some("oversize"));
}
#[test]
fn raw_email_invalid_base64_returns_typed_error() {
let json = serde_json::json!({
"message_id": "m-4",
"available": true,
"raw_email_b64": "!!!not-base64!!!",
});
let err = RawEmailResponse::from_wire_json(json).expect_err("should fail");
let msg = format!("{err}");
assert!(msg.contains("base64"), "expected base64 error, got: {msg}");
}
#[test]
fn raw_email_to_wire_json_roundtrips_bytes() {
let bytes = b"binary: \r\n\x00\xff".to_vec();
let resp = RawEmailResponse {
message_id: "m-5".into(),
rfc_message_id: None,
available: true,
raw_email: Some(bytes.clone()),
size_bytes: Some(bytes.len()),
omitted_reason: None,
};
let wire = resp.to_wire_json();
let parsed = RawEmailResponse::from_wire_json(wire).expect("parse");
assert_eq!(parsed.raw_email, Some(bytes));
}
}