Skip to main content

nylas_types/
webhook.rs

1//! Webhook types for Nylas API v3
2//!
3//! Webhooks allow you to receive real-time notifications about changes to your Nylas data.
4//! This module provides types for webhook configuration, notifications, and signature verification.
5
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9use crate::common::WebhookId;
10
11/// A webhook configuration.
12///
13/// Webhooks send HTTP POST requests to your specified callback URL when events occur.
14///
15/// # Example
16///
17/// ```
18/// # use nylas_types::{Webhook, WebhookId, WebhookTrigger};
19/// let webhook = Webhook {
20///     id: WebhookId::new("webhook_123"),
21///     description: Some("Production webhook for message events".to_string()),
22///     trigger_types: vec![WebhookTrigger::MessageCreated, WebhookTrigger::MessageUpdated],
23///     webhook_url: "https://api.example.com/webhooks".to_string(),
24///     webhook_secret: Some("secret_key_xyz".to_string()),
25///     notification_email_addresses: vec!["admin@example.com".to_string()],
26///     privacy_mode: None,
27/// };
28/// ```
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30pub struct Webhook {
31    /// Unique identifier for the webhook.
32    pub id: WebhookId,
33
34    /// Description of the webhook.
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub description: Option<String>,
37
38    /// List of trigger types this webhook listens for.
39    pub trigger_types: Vec<WebhookTrigger>,
40
41    /// URL where webhook notifications will be sent.
42    pub webhook_url: String,
43
44    /// Secret key for webhook signature verification.
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub webhook_secret: Option<String>,
47
48    /// Email addresses to notify when webhook fails.
49    #[serde(default)]
50    pub notification_email_addresses: Vec<String>,
51
52    /// Privacy mode settings (NEW in 2025)
53    ///
54    /// When enabled, sensitive information is redacted from webhook
55    /// payloads and server logs.
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub privacy_mode: Option<PrivacyMode>,
58}
59
60/// Privacy mode configuration.
61#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
62pub struct PrivacyMode {
63    /// Enable privacy mode
64    pub enabled: bool,
65
66    /// Redact email addresses
67    #[serde(default)]
68    pub redact_emails: bool,
69
70    /// Redact message bodies
71    #[serde(default)]
72    pub redact_bodies: bool,
73
74    /// Redact participant names
75    #[serde(default)]
76    pub redact_names: bool,
77}
78
79/// Webhook trigger types.
80///
81/// Specifies which events will trigger webhook notifications.
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
83#[serde(rename_all = "kebab-case")]
84pub enum WebhookTrigger {
85    /// Message created
86    MessageCreated,
87    /// Message updated
88    MessageUpdated,
89    /// Message deleted (not sent)
90    MessageDeleted,
91
92    /// Thread created
93    ThreadCreated,
94    /// Thread updated
95    ThreadUpdated,
96
97    /// Draft created
98    DraftCreated,
99    /// Draft updated
100    DraftUpdated,
101    /// Draft deleted (not sent)
102    DraftDeleted,
103
104    /// Event created
105    EventCreated,
106    /// Event updated
107    EventUpdated,
108    /// Event deleted
109    EventDeleted,
110
111    /// Calendar created
112    CalendarCreated,
113    /// Calendar updated
114    CalendarUpdated,
115    /// Calendar deleted
116    CalendarDeleted,
117
118    /// Contact created
119    ContactCreated,
120    /// Contact updated
121    ContactUpdated,
122    /// Contact deleted
123    ContactDeleted,
124
125    /// Folder created
126    FolderCreated,
127    /// Folder updated
128    FolderUpdated,
129    /// Folder deleted
130    FolderDeleted,
131
132    /// Grant created
133    GrantCreated,
134    /// Grant updated
135    GrantUpdated,
136    /// Grant deleted
137    GrantDeleted,
138    /// Grant expired
139    GrantExpired,
140}
141
142/// Webhook notification payload.
143///
144/// This is the payload sent by Nylas to your webhook URL when an event occurs.
145///
146/// # Example
147///
148/// ```
149/// # use nylas_types::WebhookNotification;
150/// # use serde_json::json;
151/// let payload = r#"{
152///     "id": "notif_123",
153///     "grant_id": "grant_456",
154///     "application_id": "app_789",
155///     "trigger": "message.created",
156///     "timestamp": 1609459200,
157///     "data": {"id": "msg_abc", "subject": "Hello"}
158/// }"#;
159///
160/// let notification: WebhookNotification = serde_json::from_str(payload).unwrap();
161/// assert_eq!(notification.trigger, "message.created");
162/// ```
163#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
164pub struct WebhookNotification {
165    /// Unique notification ID.
166    pub id: String,
167
168    /// Grant ID that triggered this notification.
169    pub grant_id: String,
170
171    /// Application ID.
172    pub application_id: String,
173
174    /// Trigger type (e.g., "message.created").
175    pub trigger: String,
176
177    /// Unix timestamp when event occurred.
178    pub timestamp: i64,
179
180    /// Event data payload.
181    pub data: Value,
182
183    /// Calendar ID (for calendar/event triggers).
184    #[serde(skip_serializing_if = "Option::is_none")]
185    pub calendar_id: Option<String>,
186
187    /// Master event ID (for recurring event triggers).
188    #[serde(skip_serializing_if = "Option::is_none")]
189    pub master_event_id: Option<String>,
190}
191
192/// Request to create a new webhook.
193///
194/// # Example
195///
196/// ```
197/// # use nylas_types::{CreateWebhookRequest, WebhookTrigger};
198/// let request = CreateWebhookRequest::builder()
199///     .description("My webhook")
200///     .webhook_url("https://api.example.com/webhooks")
201///     .trigger_types(vec![WebhookTrigger::MessageCreated])
202///     .webhook_secret("my_secret_key")
203///     .build();
204/// ```
205#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
206pub struct CreateWebhookRequest {
207    /// Description of the webhook.
208    #[serde(skip_serializing_if = "Option::is_none")]
209    pub description: Option<String>,
210
211    /// URL where webhook notifications will be sent.
212    pub webhook_url: String,
213
214    /// List of trigger types this webhook listens for.
215    pub trigger_types: Vec<WebhookTrigger>,
216
217    /// Secret key for webhook signature verification.
218    #[serde(skip_serializing_if = "Option::is_none")]
219    pub webhook_secret: Option<String>,
220
221    /// Email addresses to notify when webhook fails.
222    #[serde(skip_serializing_if = "Option::is_none")]
223    pub notification_email_addresses: Option<Vec<String>>,
224
225    /// Privacy mode settings
226    #[serde(skip_serializing_if = "Option::is_none")]
227    pub privacy_mode: Option<PrivacyMode>,
228}
229
230impl CreateWebhookRequest {
231    /// Create a builder for CreateWebhookRequest.
232    pub fn builder() -> CreateWebhookRequestBuilder {
233        CreateWebhookRequestBuilder::default()
234    }
235}
236
237/// Builder for CreateWebhookRequest.
238#[derive(Debug, Clone, Default)]
239pub struct CreateWebhookRequestBuilder {
240    description: Option<String>,
241    webhook_url: Option<String>,
242    trigger_types: Option<Vec<WebhookTrigger>>,
243    webhook_secret: Option<String>,
244    notification_email_addresses: Option<Vec<String>>,
245    privacy_mode: Option<PrivacyMode>,
246}
247
248impl CreateWebhookRequestBuilder {
249    /// Set webhook description.
250    pub fn description(mut self, description: impl Into<String>) -> Self {
251        self.description = Some(description.into());
252        self
253    }
254
255    /// Set webhook URL.
256    pub fn webhook_url(mut self, url: impl Into<String>) -> Self {
257        self.webhook_url = Some(url.into());
258        self
259    }
260
261    /// Set trigger types.
262    pub fn trigger_types(mut self, triggers: Vec<WebhookTrigger>) -> Self {
263        self.trigger_types = Some(triggers);
264        self
265    }
266
267    /// Set webhook secret for signature verification.
268    pub fn webhook_secret(mut self, secret: impl Into<String>) -> Self {
269        self.webhook_secret = Some(secret.into());
270        self
271    }
272
273    /// Set notification email addresses.
274    pub fn notification_email_addresses(mut self, emails: Vec<String>) -> Self {
275        self.notification_email_addresses = Some(emails);
276        self
277    }
278
279    /// Set privacy mode.
280    ///
281    /// # Example
282    ///
283    /// ```
284    /// use nylas_types::{CreateWebhookRequest, PrivacyMode, WebhookTrigger};
285    ///
286    /// let privacy = PrivacyMode {
287    ///     enabled: true,
288    ///     redact_emails: true,
289    ///     redact_bodies: true,
290    ///     redact_names: false,
291    /// };
292    ///
293    /// let request = CreateWebhookRequest::builder()
294    ///     .webhook_url("https://example.com/webhook")
295    ///     .trigger_types(vec![WebhookTrigger::MessageCreated])
296    ///     .privacy_mode(privacy)
297    ///     .build();
298    /// ```
299    pub fn privacy_mode(mut self, mode: PrivacyMode) -> Self {
300        self.privacy_mode = Some(mode);
301        self
302    }
303
304    /// Build the CreateWebhookRequest.
305    ///
306    /// # Panics
307    ///
308    /// Panics if webhook_url or trigger_types are not set.
309    pub fn build(self) -> CreateWebhookRequest {
310        CreateWebhookRequest {
311            description: self.description,
312            webhook_url: self.webhook_url.expect("webhook_url is required"),
313            trigger_types: self.trigger_types.expect("trigger_types is required"),
314            webhook_secret: self.webhook_secret,
315            notification_email_addresses: self.notification_email_addresses,
316            privacy_mode: self.privacy_mode,
317        }
318    }
319}
320
321/// Request to update a webhook.
322///
323/// All fields are optional - only provide the fields you want to update.
324///
325/// # Example
326///
327/// ```
328/// # use nylas_types::{UpdateWebhookRequest, WebhookTrigger};
329/// let update = UpdateWebhookRequest::builder()
330///     .description("Updated description")
331///     .trigger_types(vec![WebhookTrigger::MessageCreated, WebhookTrigger::MessageUpdated])
332///     .build();
333/// ```
334#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
335pub struct UpdateWebhookRequest {
336    /// Update webhook description.
337    #[serde(skip_serializing_if = "Option::is_none")]
338    pub description: Option<String>,
339
340    /// Update webhook URL.
341    #[serde(skip_serializing_if = "Option::is_none")]
342    pub webhook_url: Option<String>,
343
344    /// Update trigger types.
345    #[serde(skip_serializing_if = "Option::is_none")]
346    pub trigger_types: Option<Vec<WebhookTrigger>>,
347
348    /// Update webhook secret.
349    #[serde(skip_serializing_if = "Option::is_none")]
350    pub webhook_secret: Option<String>,
351
352    /// Update notification email addresses.
353    #[serde(skip_serializing_if = "Option::is_none")]
354    pub notification_email_addresses: Option<Vec<String>>,
355}
356
357impl UpdateWebhookRequest {
358    /// Create a builder for UpdateWebhookRequest.
359    pub fn builder() -> UpdateWebhookRequestBuilder {
360        UpdateWebhookRequestBuilder::default()
361    }
362}
363
364/// Builder for UpdateWebhookRequest.
365#[derive(Debug, Clone, Default)]
366pub struct UpdateWebhookRequestBuilder {
367    description: Option<String>,
368    webhook_url: Option<String>,
369    trigger_types: Option<Vec<WebhookTrigger>>,
370    webhook_secret: Option<String>,
371    notification_email_addresses: Option<Vec<String>>,
372}
373
374impl UpdateWebhookRequestBuilder {
375    /// Set webhook description.
376    pub fn description(mut self, description: impl Into<String>) -> Self {
377        self.description = Some(description.into());
378        self
379    }
380
381    /// Set webhook URL.
382    pub fn webhook_url(mut self, url: impl Into<String>) -> Self {
383        self.webhook_url = Some(url.into());
384        self
385    }
386
387    /// Set trigger types.
388    pub fn trigger_types(mut self, triggers: Vec<WebhookTrigger>) -> Self {
389        self.trigger_types = Some(triggers);
390        self
391    }
392
393    /// Set webhook secret.
394    pub fn webhook_secret(mut self, secret: impl Into<String>) -> Self {
395        self.webhook_secret = Some(secret.into());
396        self
397    }
398
399    /// Set notification email addresses.
400    pub fn notification_email_addresses(mut self, emails: Vec<String>) -> Self {
401        self.notification_email_addresses = Some(emails);
402        self
403    }
404
405    /// Build the UpdateWebhookRequest.
406    pub fn build(self) -> UpdateWebhookRequest {
407        UpdateWebhookRequest {
408            description: self.description,
409            webhook_url: self.webhook_url,
410            trigger_types: self.trigger_types,
411            webhook_secret: self.webhook_secret,
412            notification_email_addresses: self.notification_email_addresses,
413        }
414    }
415}
416
417/// Verifies the HMAC signature of a webhook payload.
418///
419/// Use this to verify that a webhook notification actually came from Nylas.
420///
421/// # Arguments
422///
423/// * `payload` - The raw webhook payload bytes
424/// * `signature` - The signature from the X-Nylas-Signature header
425/// * `secret` - Your webhook secret key
426///
427/// # Returns
428///
429/// Returns `true` if the signature is valid, `false` otherwise.
430///
431/// # Example
432///
433/// ```
434/// # use nylas_types::verify_webhook_signature;
435/// let payload = b"{\"id\":\"123\",\"trigger\":\"message.created\"}";
436/// let signature = "abc123def456...";
437/// let secret = "my_webhook_secret";
438///
439/// if verify_webhook_signature(payload, signature, secret) {
440///     println!("Webhook signature is valid!");
441/// } else {
442///     println!("Invalid webhook signature!");
443/// }
444/// ```
445pub fn verify_webhook_signature(payload: &[u8], signature: &str, secret: &str) -> bool {
446    use hmac::{Hmac, Mac};
447    use sha2::Sha256;
448
449    type HmacSha256 = Hmac<Sha256>;
450
451    let mut mac = match HmacSha256::new_from_slice(secret.as_bytes()) {
452        Ok(m) => m,
453        Err(_) => return false,
454    };
455
456    mac.update(payload);
457
458    let result = mac.finalize();
459    let expected = hex::encode(result.into_bytes());
460
461    signature == expected
462}
463
464#[cfg(test)]
465mod tests {
466    use super::*;
467
468    #[test]
469    fn test_webhook_serialization() {
470        let webhook = Webhook {
471            id: WebhookId::new("webhook_123"),
472            description: Some("Test webhook".to_string()),
473            trigger_types: vec![WebhookTrigger::MessageCreated],
474            webhook_url: "https://example.com/webhook".to_string(),
475            webhook_secret: Some("secret".to_string()),
476            notification_email_addresses: vec!["admin@example.com".to_string()],
477            privacy_mode: None,
478        };
479
480        let json = serde_json::to_string(&webhook).unwrap();
481        assert!(json.contains("webhook_123"));
482        assert!(json.contains("https://example.com/webhook"));
483    }
484
485    #[test]
486    fn test_webhook_notification_deserialization() {
487        let json = r#"{
488            "id": "notif_123",
489            "grant_id": "grant_456",
490            "application_id": "app_789",
491            "trigger": "message.created",
492            "timestamp": 1609459200,
493            "data": {"id": "msg_abc", "subject": "Test"}
494        }"#;
495
496        let notification: WebhookNotification = serde_json::from_str(json).unwrap();
497        assert_eq!(notification.id, "notif_123");
498        assert_eq!(notification.trigger, "message.created");
499        assert_eq!(notification.timestamp, 1609459200);
500    }
501
502    #[test]
503    fn test_webhook_trigger_serialization() {
504        let trigger = WebhookTrigger::MessageCreated;
505        let json = serde_json::to_string(&trigger).unwrap();
506        assert_eq!(json, "\"message-created\"");
507
508        let trigger = WebhookTrigger::EventUpdated;
509        let json = serde_json::to_string(&trigger).unwrap();
510        assert_eq!(json, "\"event-updated\"");
511    }
512
513    #[test]
514    fn test_create_webhook_request_builder() {
515        let request = CreateWebhookRequest::builder()
516            .description("My webhook")
517            .webhook_url("https://api.example.com/webhooks")
518            .trigger_types(vec![WebhookTrigger::MessageCreated])
519            .webhook_secret("secret_key")
520            .notification_email_addresses(vec!["admin@example.com".to_string()])
521            .build();
522
523        assert_eq!(request.description, Some("My webhook".to_string()));
524        assert_eq!(request.webhook_url, "https://api.example.com/webhooks");
525        assert_eq!(request.trigger_types.len(), 1);
526        assert_eq!(request.webhook_secret, Some("secret_key".to_string()));
527    }
528
529    #[test]
530    fn test_update_webhook_request_builder() {
531        let update = UpdateWebhookRequest::builder()
532            .description("Updated")
533            .trigger_types(vec![
534                WebhookTrigger::MessageCreated,
535                WebhookTrigger::MessageUpdated,
536            ])
537            .build();
538
539        assert_eq!(update.description, Some("Updated".to_string()));
540        assert_eq!(update.trigger_types.as_ref().unwrap().len(), 2);
541        assert!(update.webhook_url.is_none());
542    }
543
544    #[test]
545    fn test_verify_webhook_signature() {
546        let payload = b"test payload";
547        let secret = "test_secret";
548
549        // Generate a valid signature
550        use hmac::{Hmac, Mac};
551        use sha2::Sha256;
552        type HmacSha256 = Hmac<Sha256>;
553
554        let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).unwrap();
555        mac.update(payload);
556        let result = mac.finalize();
557        let signature = hex::encode(result.into_bytes());
558
559        // Verify it
560        assert!(verify_webhook_signature(payload, &signature, secret));
561
562        // Verify wrong signature fails
563        assert!(!verify_webhook_signature(
564            payload,
565            "wrong_signature",
566            secret
567        ));
568
569        // Verify wrong secret fails
570        assert!(!verify_webhook_signature(
571            payload,
572            &signature,
573            "wrong_secret"
574        ));
575    }
576
577    #[test]
578    fn test_privacy_mode_default() {
579        let mode = PrivacyMode::default();
580        assert!(!mode.enabled);
581        assert!(!mode.redact_emails);
582    }
583
584    #[test]
585    fn test_privacy_mode_enabled() {
586        let mode = PrivacyMode {
587            enabled: true,
588            redact_emails: true,
589            redact_bodies: true,
590            redact_names: true,
591        };
592
593        let json = serde_json::to_string(&mode).unwrap();
594        assert!(json.contains("\"enabled\":true"));
595        assert!(json.contains("\"redact_emails\":true"));
596    }
597
598    #[test]
599    fn test_webhook_with_privacy_mode() {
600        let privacy = PrivacyMode {
601            enabled: true,
602            redact_emails: true,
603            redact_bodies: false,
604            redact_names: false,
605        };
606
607        let request = CreateWebhookRequest::builder()
608            .webhook_url("https://example.com")
609            .trigger_types(vec![WebhookTrigger::MessageCreated])
610            .privacy_mode(privacy.clone())
611            .build();
612
613        assert!(request.privacy_mode.is_some());
614        let mode = request.privacy_mode.unwrap();
615        assert!(mode.enabled);
616        assert!(mode.redact_emails);
617    }
618}