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