nylas-types 0.1.1

Type definitions for Nylas API v3
Documentation
//! Message tracking types.
//!
//! Support for tracking message opens, link clicks, and thread activity.

use serde::{Deserialize, Serialize};

/// Message tracking settings.
///
/// Controls whether to track opens and link clicks for sent messages.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct TrackingOptions {
    /// Enable open tracking for this message.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub opens: Option<bool>,

    /// Enable link click tracking for this message.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub links: Option<bool>,

    /// Enable thread reply tracking for this message.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub thread_replies: Option<bool>,

    /// Custom label for tracking this message.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub label: Option<String>,
}

impl TrackingOptions {
    /// Create a new builder for tracking options.
    pub fn builder() -> TrackingOptionsBuilder {
        TrackingOptionsBuilder::default()
    }

    /// Enable all tracking options.
    pub fn all_enabled() -> Self {
        Self {
            opens: Some(true),
            links: Some(true),
            thread_replies: Some(true),
            label: None,
        }
    }

    /// Disable all tracking options.
    pub fn all_disabled() -> Self {
        Self {
            opens: Some(false),
            links: Some(false),
            thread_replies: Some(false),
            label: None,
        }
    }
}

/// Builder for TrackingOptions.
#[derive(Debug, Default)]
pub struct TrackingOptionsBuilder {
    opens: Option<bool>,
    links: Option<bool>,
    thread_replies: Option<bool>,
    label: Option<String>,
}

impl TrackingOptionsBuilder {
    /// Enable or disable open tracking.
    pub fn opens(mut self, enabled: bool) -> Self {
        self.opens = Some(enabled);
        self
    }

    /// Enable or disable link click tracking.
    pub fn links(mut self, enabled: bool) -> Self {
        self.links = Some(enabled);
        self
    }

    /// Enable or disable thread reply tracking.
    pub fn thread_replies(mut self, enabled: bool) -> Self {
        self.thread_replies = Some(enabled);
        self
    }

    /// Set a custom label for this tracking.
    pub fn label(mut self, label: impl Into<String>) -> Self {
        self.label = Some(label.into());
        self
    }

    /// Build the TrackingOptions.
    pub fn build(self) -> TrackingOptions {
        TrackingOptions {
            opens: self.opens,
            links: self.links,
            thread_replies: self.thread_replies,
            label: self.label,
        }
    }
}

/// Open tracking event data.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MessageOpened {
    /// The message ID that was opened.
    pub message_id: String,

    /// Timestamp when the message was opened (Unix timestamp).
    pub timestamp: i64,

    /// IP address of the recipient who opened the message.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ip_address: Option<String>,

    /// User agent of the recipient's email client.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub user_agent: Option<String>,

    /// Count of how many times this message has been opened.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub count: Option<u32>,
}

/// Link click tracking event data.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LinkClicked {
    /// The message ID containing the clicked link.
    pub message_id: String,

    /// The URL that was clicked.
    pub link: String,

    /// Timestamp when the link was clicked (Unix timestamp).
    pub timestamp: i64,

    /// IP address of the recipient who clicked the link.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ip_address: Option<String>,

    /// User agent of the recipient's browser.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub user_agent: Option<String>,

    /// Count of how many times this link has been clicked.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub count: Option<u32>,
}

/// Thread reply tracking event data.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ThreadReplied {
    /// The original message ID.
    pub message_id: String,

    /// The thread ID.
    pub thread_id: String,

    /// The reply message ID.
    pub reply_message_id: String,

    /// Timestamp when the reply was received (Unix timestamp).
    pub timestamp: i64,

    /// Email address of the person who replied.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub from: Option<String>,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_tracking_options_builder() {
        let options = TrackingOptions::builder()
            .opens(true)
            .links(true)
            .thread_replies(false)
            .label("campaign-2024")
            .build();

        assert_eq!(options.opens, Some(true));
        assert_eq!(options.links, Some(true));
        assert_eq!(options.thread_replies, Some(false));
        assert_eq!(options.label, Some("campaign-2024".to_string()));
    }

    #[test]
    fn test_tracking_options_all_enabled() {
        let options = TrackingOptions::all_enabled();

        assert_eq!(options.opens, Some(true));
        assert_eq!(options.links, Some(true));
        assert_eq!(options.thread_replies, Some(true));
        assert_eq!(options.label, None);
    }

    #[test]
    fn test_tracking_options_all_disabled() {
        let options = TrackingOptions::all_disabled();

        assert_eq!(options.opens, Some(false));
        assert_eq!(options.links, Some(false));
        assert_eq!(options.thread_replies, Some(false));
    }

    #[test]
    fn test_tracking_options_serialization() {
        let options = TrackingOptions::builder().opens(true).links(false).build();

        let json = serde_json::to_string(&options).unwrap();
        let deserialized: TrackingOptions = serde_json::from_str(&json).unwrap();

        assert_eq!(options, deserialized);
    }

    #[test]
    fn test_message_opened_serialization() {
        let opened = MessageOpened {
            message_id: "msg_123".to_string(),
            timestamp: 1234567890,
            ip_address: Some("192.168.1.1".to_string()),
            user_agent: Some("Mozilla/5.0".to_string()),
            count: Some(1),
        };

        let json = serde_json::to_string(&opened).unwrap();
        let deserialized: MessageOpened = serde_json::from_str(&json).unwrap();

        assert_eq!(opened, deserialized);
    }

    #[test]
    fn test_link_clicked_serialization() {
        let clicked = LinkClicked {
            message_id: "msg_123".to_string(),
            link: "https://example.com".to_string(),
            timestamp: 1234567890,
            ip_address: Some("192.168.1.1".to_string()),
            user_agent: Some("Mozilla/5.0".to_string()),
            count: Some(2),
        };

        let json = serde_json::to_string(&clicked).unwrap();
        let deserialized: LinkClicked = serde_json::from_str(&json).unwrap();

        assert_eq!(clicked, deserialized);
    }

    #[test]
    fn test_thread_replied_serialization() {
        let replied = ThreadReplied {
            message_id: "msg_123".to_string(),
            thread_id: "thread_456".to_string(),
            reply_message_id: "msg_789".to_string(),
            timestamp: 1234567890,
            from: Some("user@example.com".to_string()),
        };

        let json = serde_json::to_string(&replied).unwrap();
        let deserialized: ThreadReplied = serde_json::from_str(&json).unwrap();

        assert_eq!(replied, deserialized);
    }
}