1use std::io::Read;
2use std::sync::Arc;
3use std::{fmt, fs::File};
4
5#[cfg(feature = "blocking")]
6use reqwest::blocking::multipart::{Form, Part};
7#[cfg(not(feature = "blocking"))]
8use reqwest::multipart::{Form, Part};
9
10use reqwest::Method;
11
12use self::types::UpdateContactResponse;
13use crate::{
14 Config, Error, Result,
15 contacts::types::ContactPropertyChanges,
16 list_opts::ListOptions,
17 types::{
18 AddContactSegmentResponse, ContactImport, ContactProperty, ContactTopic,
19 CreateContactImportOptions, CreateContactImportResponse, CreateContactPropertyOptions,
20 CreateContactPropertyResponse, DeleteContactPropertyResponse, RemoveContactSegmentResponse,
21 Segment, UpdateContactPropertyResponse, UpdateContactTopicOptions,
22 },
23};
24use crate::{
25 list_opts::ListResponse,
26 types::{Contact, ContactChanges, ContactId, CreateContactOptions},
27};
28
29#[derive(Clone)]
31pub struct ContactsSvc(pub(crate) Arc<Config>);
32
33impl ContactsSvc {
34 #[maybe_async::maybe_async]
38 pub async fn create(&self, contact: CreateContactOptions) -> Result<ContactId> {
39 let path = contact.audience_id.as_ref().map_or_else(
40 || "/contacts".to_string(),
41 |audience_id| format!("/audiences/{audience_id}/contacts"),
42 );
43
44 let request = self.0.build(Method::POST, &path);
45 let response = self.0.send(request.json(&contact)).await?;
46 let content = response.json::<types::CreateContactResponse>().await?;
47
48 Ok(content.id)
49 }
50
51 #[maybe_async::maybe_async]
55 pub async fn get(&self, contact_id_or_email: &str) -> Result<Contact> {
56 let contact_id_or_email = urlencoding::encode(contact_id_or_email);
57 let path = format!("/contacts/{contact_id_or_email}");
58
59 let request = self.0.build(Method::GET, &path);
60 let response = self.0.send(request).await?;
61 let content = response.json::<Contact>().await?;
62
63 Ok(content)
64 }
65
66 #[maybe_async::maybe_async]
70 pub async fn update(
71 &self,
72 contact_id_or_email: &str,
73 update: ContactChanges,
74 ) -> Result<UpdateContactResponse> {
75 let contact_id_or_email = urlencoding::encode(contact_id_or_email);
76 let path = format!("/contacts/{contact_id_or_email}");
77
78 let request = self.0.build(Method::PATCH, &path);
79 let response = self.0.send(request.json(&update)).await?;
80 let content = response.json::<UpdateContactResponse>().await?;
81
82 Ok(content)
83 }
84
85 #[maybe_async::maybe_async]
89 pub async fn delete(&self, contact_id_or_email: &str) -> Result<bool> {
90 let contact_id_or_email = urlencoding::encode(contact_id_or_email);
91 let path = format!("/contacts/{contact_id_or_email}");
92
93 let request = self.0.build(Method::DELETE, &path);
94 let response = self.0.send(request).await?;
95 let content = response.json::<types::DeleteContactResponse>().await?;
96
97 Ok(content.deleted)
98 }
99
100 #[maybe_async::maybe_async]
106 pub async fn list<T>(
107 &self,
108 audience: &str,
109 list_opts: ListOptions<T>,
110 ) -> Result<ListResponse<Contact>> {
111 let path = format!("/audiences/{audience}/contacts");
112
113 let request = self.0.build(Method::GET, &path).query(&list_opts);
114 let response = self.0.send(request).await?;
115 let content = response.json::<ListResponse<Contact>>().await?;
116
117 Ok(content)
118 }
119
120 #[maybe_async::maybe_async]
124 pub async fn get_contact_topics<T>(
125 &self,
126 contact_id_or_email: &str,
127 list_opts: ListOptions<T>,
128 ) -> Result<ListResponse<ContactTopic>> {
129 let contact_id_or_email = urlencoding::encode(contact_id_or_email);
130 let path = format!("/contacts/{contact_id_or_email}/topics");
131
132 let request = self.0.build(Method::GET, &path).query(&list_opts);
133 let response = self.0.send(request).await?;
134 let content = response.json::<ListResponse<ContactTopic>>().await?;
135
136 Ok(content)
137 }
138
139 #[maybe_async::maybe_async]
143 pub async fn update_contact_topics(
144 &self,
145 contact_id_or_email: &str,
146 topics: impl Into<Vec<UpdateContactTopicOptions>>,
147 ) -> Result<UpdateContactResponse> {
148 let contact_id_or_email = urlencoding::encode(contact_id_or_email);
149 let path = format!("/contacts/{contact_id_or_email}/topics");
150
151 let request = self.0.build(Method::PATCH, &path);
152 let response = self.0.send(request.json(&topics.into())).await?;
153 let content = response.json::<UpdateContactResponse>().await?;
154
155 Ok(content)
156 }
157
158 #[maybe_async::maybe_async]
162 pub async fn add_contact_segment(
163 &self,
164 contact_id_or_email: &str,
165 segment_id: &str,
166 ) -> Result<AddContactSegmentResponse> {
167 let contact_id_or_email = urlencoding::encode(contact_id_or_email);
168 let path = format!("/contacts/{contact_id_or_email}/segments/{segment_id}");
169
170 let request = self.0.build(Method::POST, &path);
171 let response = self.0.send(request).await?;
172 let content = response.json::<AddContactSegmentResponse>().await?;
173
174 Ok(content)
175 }
176
177 #[maybe_async::maybe_async]
181 pub async fn delete_contact_segment(
182 &self,
183 contact_id_or_email: &str,
184 segment_id: &str,
185 ) -> Result<RemoveContactSegmentResponse> {
186 let contact_id_or_email = urlencoding::encode(contact_id_or_email);
187 let path = format!("/contacts/{contact_id_or_email}/segments/{segment_id}");
188
189 let request = self.0.build(Method::DELETE, &path);
190 let response = self.0.send(request).await?;
191 let content = response.json::<RemoveContactSegmentResponse>().await?;
192
193 Ok(content)
194 }
195
196 #[maybe_async::maybe_async]
200 pub async fn list_contact_segment<T>(
201 &self,
202 contact_id_or_email: &str,
203 list_opts: ListOptions<T>,
204 ) -> Result<ListResponse<Segment>> {
205 let contact_id_or_email = urlencoding::encode(contact_id_or_email);
206 let path = format!("/contacts/{contact_id_or_email}/segments/");
207
208 let request = self.0.build(Method::GET, &path).query(&list_opts);
209 let response = self.0.send(request).await?;
210 let content = response.json::<ListResponse<Segment>>().await?;
211
212 Ok(content)
213 }
214
215 #[maybe_async::maybe_async]
219 pub async fn create_property(
220 &self,
221 contact_property: CreateContactPropertyOptions,
222 ) -> Result<CreateContactPropertyResponse> {
223 let path = "/contact-properties";
224
225 let request = self.0.build(Method::POST, path);
226 let response = self.0.send(request.json(&contact_property)).await?;
227 let content = response.json::<CreateContactPropertyResponse>().await?;
228
229 Ok(content)
230 }
231
232 #[maybe_async::maybe_async]
236 pub async fn get_property(&self, contact_property_id: &str) -> Result<ContactProperty> {
237 let path = format!("/contact-properties/{contact_property_id}");
238
239 let request = self.0.build(Method::GET, &path);
240 let response = self.0.send(request).await?;
241 let content = response.json::<ContactProperty>().await?;
242
243 Ok(content)
244 }
245
246 #[maybe_async::maybe_async]
250 pub async fn update_property(
251 &self,
252 contact_property_id: &str,
253 update: ContactPropertyChanges,
254 ) -> Result<UpdateContactPropertyResponse> {
255 let path = format!("/contact-properties/{contact_property_id}");
256
257 let request = self.0.build(Method::PATCH, &path);
258 let response = self.0.send(request.json(&update)).await?;
259 let content = response.json::<UpdateContactPropertyResponse>().await?;
260
261 Ok(content)
262 }
263
264 #[maybe_async::maybe_async]
268 pub async fn delete_property(
269 &self,
270 contact_property_id: &str,
271 ) -> Result<DeleteContactPropertyResponse> {
272 let path = format!("/contact-properties/{contact_property_id}");
273
274 let request = self.0.build(Method::DELETE, &path);
275 let response = self.0.send(request).await?;
276 let content = response.json::<DeleteContactPropertyResponse>().await?;
277
278 Ok(content)
279 }
280
281 #[maybe_async::maybe_async]
287 pub async fn list_properties<T>(
288 &self,
289 list_opts: ListOptions<T>,
290 ) -> Result<ListResponse<ContactProperty>> {
291 let path = "/contact-properties";
292
293 let request = self.0.build(Method::GET, path).query(&list_opts);
294 let response = self.0.send(request).await?;
295 let content = response.json::<ListResponse<ContactProperty>>().await?;
296
297 Ok(content)
298 }
299
300 #[maybe_async::maybe_async]
309 pub async fn create_import(
310 &self,
311 mut file: File,
312 contact_import: CreateContactImportOptions,
313 ) -> Result<CreateContactImportResponse> {
314 let path = "/contacts/imports";
315
316 let mut contents = String::new();
317 #[allow(clippy::verbose_file_reads)]
318 let _size = file
319 .read_to_string(&mut contents)
320 .map_err(|err| Error::Parse {
321 message: "Error while reading file".to_owned(),
322 source: Some(Box::new(err)),
323 })?;
324
325 let mut form = Form::new().part(
326 "file",
327 Part::text(contents)
328 .mime_str("text/csv")?
329 .file_name("foo.csv"),
330 );
331 form = Self::add_non_json_part(form, "on_conflict", contact_import.on_conflict.as_ref())?;
332 form = Self::add_non_json_part(form, "segments", contact_import.segments.as_ref())?;
333 form = Self::add_non_json_part(form, "topics", contact_import.topics.as_ref())?;
334
335 if let Some(column_map) = contact_import.column_map {
336 let json = serde_json::to_string(&column_map).map_err(|e| Error::Parse {
337 message: "Could not convert field to JSON".to_owned(),
338 source: Some(Box::new(e)),
339 })?;
340 form = form.part("column_map".to_owned(), Part::text(json));
341 }
342
343 let request = self.0.build(Method::POST, path).multipart(form);
344 let response = self.0.send(request).await?;
345 let content = response.json::<CreateContactImportResponse>().await?;
346
347 Ok(content)
348 }
349
350 fn add_non_json_part<T: serde::Serialize>(
351 form: Form,
352 field_name: &str,
353 value: Option<&T>,
354 ) -> Result<Form, Error> {
355 match value {
356 Some(v) => {
357 let val = serde_json::to_value(v).map_err(|e| Error::Parse {
358 message: "Could not convert field to JSON".to_owned(),
359 source: Some(Box::new(e)),
360 })?;
361
362 let json = match val {
363 serde_json::Value::String(s) => s,
364 other => other.to_string(),
365 };
366
367 Ok(form.part(field_name.to_owned(), Part::text(json)))
368 }
369 None => Ok(form),
370 }
371 }
372
373 #[maybe_async::maybe_async]
377 pub async fn get_import(&self, contact_import_id: &str) -> Result<ContactImport> {
378 let path = format!("/contacts/imports/{contact_import_id}");
379
380 let request = self.0.build(Method::GET, &path);
381 let response = self.0.send(request).await?;
382 let content = response.json::<ContactImport>().await?;
383
384 Ok(content)
385 }
386
387 #[maybe_async::maybe_async]
393 pub async fn list_imports<T>(
394 &self,
395 list_opts: ListOptions<T>,
396 ) -> Result<ListResponse<ContactImport>> {
397 let path = "/contacts/imports".to_string();
398
399 let request = self.0.build(Method::GET, &path).query(&list_opts);
400 let response = self.0.send(request).await?;
401 let content = response.json::<ListResponse<ContactImport>>().await?;
402
403 Ok(content)
404 }
405}
406
407impl fmt::Debug for ContactsSvc {
408 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
409 fmt::Debug::fmt(&self.0, f)
410 }
411}
412
413#[allow(unreachable_pub)]
414pub mod types {
415 use std::collections::HashMap;
416
417 use serde::{Deserialize, Serialize};
418
419 use crate::{
420 topics::types::TopicId,
421 types::{SegmentId, SubscriptionType},
422 };
423
424 crate::define_id_type!(ContactId);
425 crate::define_id_type!(ContactPropertyId);
426 crate::define_id_type!(ContactImportId);
427
428 #[derive(Debug, Clone, Serialize, Deserialize)]
429 pub struct SegmentObject {
430 pub id: SegmentId,
431 }
432
433 #[must_use]
435 #[derive(Debug, Clone, Serialize)]
436 pub struct CreateContactOptions {
437 email: String,
439
440 pub(crate) audience_id: Option<String>,
442
443 #[serde(skip_serializing_if = "Option::is_none")]
445 first_name: Option<String>,
446 #[serde(skip_serializing_if = "Option::is_none")]
448 last_name: Option<String>,
449 #[serde(skip_serializing_if = "Option::is_none")]
451 unsubscribed: Option<bool>,
452 #[serde(skip_serializing_if = "Option::is_none")]
454 properties: Option<HashMap<String, String>>,
455 #[serde(skip_serializing_if = "Option::is_none")]
457 segments: Option<Vec<SegmentObject>>,
458 #[serde(skip_serializing_if = "Option::is_none")]
460 topics: Option<Vec<UpdateContactTopicOptions>>,
461 }
462
463 impl CreateContactOptions {
464 pub fn new(email: &str) -> Self {
466 Self {
467 email: email.to_owned(),
468 audience_id: None,
469 first_name: None,
470 last_name: None,
471 unsubscribed: None,
472 properties: None,
473 segments: None,
474 topics: None,
475 }
476 }
477
478 #[inline]
480 pub fn with_audience_id(mut self, audience_id: &str) -> Self {
481 self.audience_id = Some(audience_id.to_owned());
482 self
483 }
484
485 #[inline]
487 pub fn with_first_name(mut self, name: &str) -> Self {
488 self.first_name = Some(name.to_owned());
489 self
490 }
491
492 #[inline]
494 pub fn with_last_name(mut self, name: &str) -> Self {
495 self.last_name = Some(name.to_owned());
496 self
497 }
498
499 #[inline]
501 pub const fn with_unsubscribed(mut self, unsubscribed: bool) -> Self {
502 self.unsubscribed = Some(unsubscribed);
503 self
504 }
505
506 #[inline]
508 pub fn with_property(mut self, key: &str, value: &str) -> Self {
509 let properties = self.properties.get_or_insert_with(HashMap::new);
510 let _old = properties.insert(key.to_owned(), value.to_owned());
511 self
512 }
513
514 #[inline]
516 pub fn with_properties(mut self, properties: HashMap<String, String>) -> Self {
517 let self_properties = self.properties.get_or_insert_with(HashMap::new);
518 self_properties.extend(properties);
519 self
520 }
521
522 #[inline]
524 pub fn with_segment(mut self, id: &str) -> Self {
525 let id = SegmentObject {
526 id: SegmentId::new(id),
527 };
528 let segments = self.segments.get_or_insert_with(Vec::new);
529 segments.push(id);
530 self
531 }
532
533 #[inline]
535 pub fn with_segments(mut self, ids: &[String]) -> Self {
536 let segments = self.segments.get_or_insert_with(Vec::new);
537 for id in ids {
538 let id = SegmentObject {
539 id: SegmentId::new(id),
540 };
541 segments.push(id);
542 }
543 self
544 }
545
546 #[inline]
548 pub fn with_topic(mut self, topic: UpdateContactTopicOptions) -> Self {
549 let topics = self.topics.get_or_insert_with(Vec::new);
550 topics.push(topic);
551 self
552 }
553
554 #[inline]
556 pub fn with_topics(mut self, topics: &[UpdateContactTopicOptions]) -> Self {
557 let topics_vec = self.topics.get_or_insert_with(Vec::new);
558 topics_vec.extend_from_slice(topics);
559 self
560 }
561 }
562
563 #[derive(Debug, Clone, Serialize, Deserialize)]
564 pub struct CreateContactResponse {
565 pub id: ContactId,
567 }
568
569 #[must_use]
571 #[derive(Debug, Clone, Serialize, Deserialize)]
572 pub struct Contact {
573 pub id: ContactId,
575 pub email: String,
577 pub first_name: Option<String>,
579 pub last_name: Option<String>,
581 pub unsubscribed: bool,
583 pub created_at: String,
585 #[serde(default)]
587 pub properties: Option<HashMap<String, ContactPropertyResponse>>,
588 }
589
590 #[must_use]
592 #[derive(Debug, Default, Clone, Serialize)]
593 pub struct ContactChanges {
594 #[serde(skip_serializing_if = "Option::is_none")]
596 first_name: Option<String>,
597 #[serde(skip_serializing_if = "Option::is_none")]
599 last_name: Option<String>,
600 #[serde(skip_serializing_if = "Option::is_none")]
602 unsubscribed: Option<bool>,
603 }
604
605 impl ContactChanges {
606 #[inline]
608 pub fn new() -> Self {
609 Self::default()
610 }
611
612 #[inline]
614 pub fn with_first_name(mut self, name: &str) -> Self {
615 self.first_name = Some(name.to_owned());
616 self
617 }
618
619 #[inline]
621 pub fn with_last_name(mut self, name: &str) -> Self {
622 self.last_name = Some(name.to_owned());
623 self
624 }
625
626 #[inline]
628 pub const fn with_unsubscribed(mut self, unsubscribed: bool) -> Self {
629 self.unsubscribed = Some(unsubscribed);
630 self
631 }
632 }
633
634 #[derive(Debug, Clone, Serialize, Deserialize)]
635 pub struct UpdateContactResponse {
636 pub id: ContactId,
638 }
639
640 #[derive(Debug, Clone, Serialize, Deserialize)]
641 pub struct DeleteContactResponse {
642 #[allow(dead_code)]
644 pub contact: ContactId,
645 pub deleted: bool,
647 }
648
649 #[derive(Serialize, Deserialize, Debug, Clone)]
650 pub struct ContactTopic {
651 pub id: TopicId,
652 pub name: String,
653 pub description: Option<String>,
654 pub subscription: SubscriptionType,
655 pub created_at: String,
656 }
657
658 #[must_use]
662 #[derive(Debug, Clone, Serialize)]
663 pub struct UpdateContactTopicOptions {
664 id: String,
665 subscription: SubscriptionType,
666 }
667
668 impl UpdateContactTopicOptions {
669 pub fn new(id: impl Into<String>, subscription: SubscriptionType) -> Self {
672 Self {
673 id: id.into(),
674 subscription,
675 }
676 }
677 }
678
679 #[must_use]
680 #[derive(Debug, Clone, Serialize, Deserialize)]
681 pub struct AddContactSegmentResponse {
682 pub id: SegmentId,
683 }
684
685 #[must_use]
686 #[derive(Debug, Clone, Serialize, Deserialize)]
687 pub struct RemoveContactSegmentResponse {
688 pub id: SegmentId,
689 pub deleted: bool,
690 }
691
692 #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy)]
693 #[must_use]
694 #[serde(rename_all = "snake_case")]
695 pub enum PropertyType {
696 String,
697 Number,
698 }
699
700 #[must_use]
702 #[derive(Debug, Clone, Serialize)]
703 pub struct CreateContactPropertyOptions {
704 key: String,
705 #[serde(rename = "type")]
706 r#type: PropertyType,
707 fallback_value: Option<serde_json::Value>,
708 }
709
710 impl CreateContactPropertyOptions {
711 pub fn new(key: impl Into<String>, r#type: PropertyType) -> Self {
715 Self {
716 key: key.into(),
717 r#type,
718 fallback_value: None,
719 }
720 }
721
722 pub fn with_fallback(mut self, fallback: impl Into<serde_json::Value>) -> Self {
725 self.fallback_value = Some(fallback.into());
726 self
727 }
728 }
729
730 #[derive(Debug, Clone, Serialize, Deserialize)]
731 pub struct CreateContactPropertyResponse {
732 pub id: ContactPropertyId,
733 }
734
735 #[derive(Debug, Clone, Serialize, Deserialize)]
736 pub struct ContactProperty {
737 pub id: ContactPropertyId,
738 pub created_at: String,
739 pub key: String,
740 #[serde(rename = "type")]
741 pub r#type: PropertyType,
742 pub fallback_value: Option<serde_json::Value>,
743 }
744
745 #[derive(Debug, Clone, Serialize, Deserialize)]
746 pub struct ContactPropertyResponse {
747 pub value: String,
748 #[serde(rename = "type")]
749 pub r#type: PropertyType,
750 }
751
752 #[must_use]
754 #[derive(Debug, Default, Clone, Serialize)]
755 pub struct ContactPropertyChanges {
756 #[serde(skip_serializing_if = "Option::is_none")]
757 fallback_value: Option<serde_json::Value>,
758 }
759
760 impl ContactPropertyChanges {
761 pub fn with_fallback(mut self, fallback: impl Into<serde_json::Value>) -> Self {
764 self.fallback_value = Some(fallback.into());
765 self
766 }
767 }
768
769 #[derive(Debug, Clone, Serialize, Deserialize)]
770 pub struct UpdateContactPropertyResponse {
771 pub id: ContactPropertyId,
773 }
774
775 #[derive(Debug, Clone, Serialize, Deserialize)]
776 pub struct DeleteContactPropertyResponse {
777 #[allow(dead_code)]
778 pub id: ContactPropertyId,
779 pub deleted: bool,
780 }
781
782 #[must_use]
783 #[derive(Debug, Clone, Serialize, Default)]
784 pub struct CreateContactImportOptions {
785 #[serde(skip_serializing_if = "Option::is_none")]
786 pub(crate) column_map: Option<ContactImportColumnMap>,
787 #[serde(skip_serializing_if = "Option::is_none")]
788 pub(crate) on_conflict: Option<ContactImportOnConflict>,
789 #[serde(skip_serializing_if = "Option::is_none")]
790 pub(crate) segments: Option<Vec<SegmentObject>>,
791 #[serde(skip_serializing_if = "Option::is_none")]
792 pub(crate) topics: Option<Vec<ContactImportTopic>>,
793 }
794
795 impl CreateContactImportOptions {
796 pub fn new() -> Self {
798 Self {
799 column_map: None,
800 on_conflict: None,
801 segments: None,
802 topics: None,
803 }
804 }
805
806 #[inline]
807 pub fn with_column_map(mut self, column_map: ContactImportColumnMap) -> Self {
808 self.column_map = Some(column_map);
809 self
810 }
811
812 #[inline]
813 pub fn with_on_conflict(mut self, on_conflict: ContactImportOnConflict) -> Self {
814 self.on_conflict = Some(on_conflict);
815 self
816 }
817
818 #[inline]
820 pub fn with_segment(mut self, id: &str) -> Self {
821 let id = SegmentObject {
822 id: SegmentId::new(id),
823 };
824 let segments = self.segments.get_or_insert_with(Vec::new);
825 segments.push(id);
826 self
827 }
828
829 #[inline]
831 pub fn with_segments(mut self, ids: &[String]) -> Self {
832 let segments = self.segments.get_or_insert_with(Vec::new);
833 for id in ids {
834 let id = SegmentObject {
835 id: SegmentId::new(id),
836 };
837 segments.push(id);
838 }
839 self
840 }
841
842 #[inline]
844 pub fn with_topic(mut self, topic: ContactImportTopic) -> Self {
845 let topics = self.topics.get_or_insert_with(Vec::new);
846 topics.push(topic);
847 self
848 }
849
850 #[inline]
852 pub fn with_topics(mut self, topics: &[ContactImportTopic]) -> Self {
853 let topics_vec = self.topics.get_or_insert_with(Vec::new);
854 topics_vec.extend_from_slice(topics);
855 self
856 }
857 }
858
859 #[must_use]
860 #[derive(Debug, Clone, Serialize, Default)]
861 pub struct ContactImportColumnMap {
862 #[serde(skip_serializing_if = "Option::is_none")]
863 email: Option<String>,
864 #[serde(skip_serializing_if = "Option::is_none")]
865 first_name: Option<String>,
866 #[serde(skip_serializing_if = "Option::is_none")]
867 last_name: Option<String>,
868 #[serde(skip_serializing_if = "Option::is_none")]
869 unsubscribed: Option<String>,
870 #[serde(skip_serializing_if = "Option::is_none")]
871 properties: Option<HashMap<String, ContactImportPropertyMapping>>,
872 }
873
874 impl ContactImportColumnMap {
875 pub fn new() -> Self {
876 Self {
877 email: None,
878 first_name: None,
879 last_name: None,
880 unsubscribed: None,
881 properties: None,
882 }
883 }
884
885 pub fn with_email(mut self, email: &str) -> Self {
886 self.email = Some(email.to_owned());
887 self
888 }
889
890 pub fn with_first_name(mut self, first_name: &str) -> Self {
891 self.first_name = Some(first_name.to_owned());
892 self
893 }
894
895 pub fn with_last_name(mut self, last_name: &str) -> Self {
896 self.last_name = Some(last_name.to_owned());
897 self
898 }
899
900 pub fn with_unsubscribed(mut self, unsubscribed: &str) -> Self {
901 self.unsubscribed = Some(unsubscribed.to_owned());
902 self
903 }
904
905 #[inline]
907 pub fn with_property(mut self, key: &str, value: ContactImportPropertyMapping) -> Self {
908 let properties = self.properties.get_or_insert_with(HashMap::new);
909 let _old = properties.insert(key.to_owned(), value);
910 self
911 }
912
913 #[inline]
915 pub fn with_properties(
916 mut self,
917 properties: HashMap<String, ContactImportPropertyMapping>,
918 ) -> Self {
919 let self_properties = self.properties.get_or_insert_with(HashMap::new);
920 self_properties.extend(properties);
921 self
922 }
923 }
924
925 #[must_use]
926 #[derive(Debug, Clone, Serialize)]
927 pub struct ContactImportPropertyMapping {
928 pub column: String,
929 #[serde(rename = "type")]
930 pub r#type: ContactImportPropertyType,
931 }
932
933 #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy)]
934 #[must_use]
935 #[serde(rename_all = "snake_case")]
936 pub enum ContactImportPropertyType {
937 String,
938 Number,
939 Boolean,
940 }
941
942 #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy)]
943 #[must_use]
944 #[serde(rename_all = "snake_case")]
945 pub enum ContactImportOnConflict {
946 Upsert,
947 Skip,
948 }
949
950 #[derive(Debug, Clone, Serialize, Deserialize)]
951 pub struct CreateContactImportResponse {
952 pub id: ContactImportId,
954 }
955
956 #[derive(Debug, Clone, Serialize, Deserialize)]
957 pub struct ContactImportTopic {
958 pub id: String,
959 pub subscription: ContactImportTopicSubscription,
960 }
961
962 #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy)]
963 #[must_use]
964 #[serde(rename_all = "snake_case")]
965 pub enum ContactImportTopicSubscription {
966 OptIn,
967 OptOut,
968 }
969
970 #[derive(Debug, Clone, Serialize, Deserialize)]
972 pub struct ContactImport {
973 pub id: ContactImportId,
974 pub status: ContactImportStatus,
975 pub created_at: String,
976 pub completed_at: Option<String>,
977 pub counts: ContactImportCounts,
978 }
979
980 #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy)]
981 #[must_use]
982 #[serde(rename_all = "snake_case")]
983 pub enum ContactImportStatus {
984 Queued,
985 InProgress,
986 Completed,
987 Failed,
988 }
989
990 #[derive(Debug, Clone, Serialize, Deserialize, Copy)]
991 pub struct ContactImportCounts {
992 total: u32,
993 created: u32,
994 updated: u32,
995 skipped: u32,
996 failed: u32,
997 }
998}
999
1000#[cfg(test)]
1001#[allow(clippy::unwrap_used, clippy::needless_return)]
1002mod test {
1003 use std::collections::HashMap;
1004
1005 #[cfg(not(feature = "blocking"))]
1006 use crate::{
1007 list_opts::ListOptions,
1008 test::{CLIENT, DebugResult},
1009 types::{ContactChanges, CreateTopicOptions, SubscriptionType, UpdateContactTopicOptions},
1010 };
1011
1012 use crate::types::{Contact, ContactProperty, CreateContactOptions};
1013
1014 #[tokio_shared_rt::test(shared = true)]
1015 #[serial_test::serial]
1016 #[cfg(not(feature = "blocking"))]
1017 async fn no_audience() -> DebugResult<()> {
1018 let resend = &*CLIENT;
1019
1020 let contact = CreateContactOptions::new("steve.wozniak@gmail.com")
1021 .with_first_name("Steve")
1022 .with_last_name("Wozniak")
1023 .with_unsubscribed(false);
1024 let id = resend.contacts.create(contact).await?;
1025 std::thread::sleep(std::time::Duration::from_secs(4));
1026
1027 let deleted = resend.contacts.delete(&id).await?;
1028 assert!(deleted);
1029
1030 std::thread::sleep(std::time::Duration::from_secs(4));
1031
1032 Ok(())
1033 }
1034
1035 #[tokio_shared_rt::test(shared = true)]
1036 #[serial_test::serial]
1037 #[cfg(not(feature = "blocking"))]
1038 async fn all() -> DebugResult<()> {
1039 let resend = &*CLIENT;
1040 let audience = "test_contacts";
1041
1042 let audience = resend.segments.create(audience).await?;
1044 let audience_id = audience.id;
1045 std::thread::sleep(std::time::Duration::from_secs(4));
1046
1047 let contact = CreateContactOptions::new("antonios.barotsis@pm.me")
1049 .with_first_name("Antonios")
1050 .with_last_name("Barotsis")
1051 .with_unsubscribed(false)
1052 .with_audience_id(&audience_id);
1053 let id = resend.contacts.create(contact).await?;
1054 std::thread::sleep(std::time::Duration::from_secs(4));
1055
1056 let topics = resend
1058 .contacts
1059 .get_contact_topics(&id, ListOptions::default())
1060 .await?;
1061 assert!(topics.data.is_empty());
1062
1063 let topic = resend
1065 .topics
1066 .create(CreateTopicOptions::new(
1067 "Weekly Newsletter",
1068 SubscriptionType::OptIn,
1069 ))
1070 .await?;
1071 let topics = [UpdateContactTopicOptions::new(
1072 topic.id.to_string(),
1073 SubscriptionType::OptIn,
1074 )];
1075 let _topics = resend.contacts.update_contact_topics(&id, topics).await?;
1076 std::thread::sleep(std::time::Duration::from_secs(4));
1077
1078 let topics = resend
1080 .contacts
1081 .get_contact_topics(&id, ListOptions::default())
1082 .await?;
1083 assert!(!topics.data.is_empty());
1084
1085 let changes = ContactChanges::new().with_unsubscribed(true);
1087 let _contact = resend.contacts.update(&id, changes).await?;
1088 std::thread::sleep(std::time::Duration::from_secs(4));
1089
1090 let contact = resend.contacts.get(&id).await?;
1092 assert!(contact.unsubscribed);
1093
1094 let contact = resend.contacts.get("antonios.barotsis@pm.me").await?;
1096 assert!(contact.unsubscribed);
1097
1098 let contacts = resend
1100 .contacts
1101 .list(&audience_id, ListOptions::default())
1102 .await?;
1103 assert_eq!(contacts.len(), 1);
1104
1105 let deleted = resend.contacts.delete(&id).await?;
1107 assert!(deleted);
1108
1109 let deleted = resend.segments.delete(&audience_id.clone()).await?;
1111 assert!(deleted);
1112 std::thread::sleep(std::time::Duration::from_secs(1));
1113
1114 let deleted = resend.topics.delete(&topic.id).await?;
1116 assert!(deleted.deleted);
1117
1118 let contacts = resend
1120 .contacts
1121 .list(&audience_id, ListOptions::default())
1122 .await?;
1123 assert!(contacts.is_empty());
1124
1125 std::thread::sleep(std::time::Duration::from_secs(4));
1126
1127 Ok(())
1128 }
1129
1130 #[tokio_shared_rt::test(shared = true)]
1131 #[serial_test::serial]
1132 #[cfg(not(feature = "blocking"))]
1133 async fn contact_properties() -> DebugResult<()> {
1134 use crate::types::CreateContactPropertyOptions;
1135
1136 let resend = &*CLIENT;
1137
1138 let contact_property =
1139 CreateContactPropertyOptions::new("key", crate::types::PropertyType::String);
1140 let contact_property = resend.contacts.create_property(contact_property).await?;
1141
1142 let contact = CreateContactOptions::new("steve.wozniak@gmail.com")
1143 .with_first_name("Steve")
1144 .with_last_name("Wozniak")
1145 .with_unsubscribed(false)
1146 .with_property("key", "value");
1147
1148 let contact = resend.contacts.create(contact).await?;
1149
1150 let contact = resend.contacts.get(&contact).await?;
1151
1152 let deleted = resend.contacts.delete(&contact.id).await?;
1153 assert!(deleted);
1154 let deleted = resend
1155 .contacts
1156 .delete_property(&contact_property.id)
1157 .await?;
1158 assert!(deleted.deleted);
1159
1160 Ok(())
1161 }
1162
1163 #[tokio_shared_rt::test(shared = true)]
1164 #[serial_test::serial]
1165 #[cfg(not(feature = "blocking"))]
1166 async fn segments() -> DebugResult<()> {
1167 let resend = &*CLIENT;
1168
1169 let segment = resend.segments.create("registered users").await?;
1171 std::thread::sleep(std::time::Duration::from_secs(2));
1172
1173 let contact = CreateContactOptions::new("antonios.barotsis@pm.me")
1175 .with_first_name("Antonios")
1176 .with_last_name("Barotsis");
1177 let contact_id = resend.contacts.create(contact).await?;
1178 std::thread::sleep(std::time::Duration::from_secs(2));
1179
1180 let _added = resend
1181 .contacts
1182 .add_contact_segment(&contact_id, &segment.id)
1183 .await?;
1184 std::thread::sleep(std::time::Duration::from_secs(4));
1185
1186 let list = resend
1187 .contacts
1188 .list_contact_segment(&contact_id, ListOptions::default())
1189 .await?;
1190 assert!(!list.data.is_empty());
1191
1192 let deleted = resend
1193 .contacts
1194 .delete_contact_segment(&contact_id, &segment.id)
1195 .await?;
1196 assert!(deleted.deleted);
1197 std::thread::sleep(std::time::Duration::from_secs(2));
1198
1199 let deleted = resend.contacts.delete(&contact_id).await?;
1201 assert!(deleted);
1202 let deleted = resend.segments.delete(&segment.id).await?;
1203 assert!(deleted);
1204
1205 std::thread::sleep(std::time::Duration::from_secs(4));
1206
1207 Ok(())
1208 }
1209
1210 #[tokio_shared_rt::test(shared = true)]
1211 #[serial_test::serial]
1212 #[cfg(not(feature = "blocking"))]
1213 async fn properties() -> DebugResult<()> {
1214 use crate::{
1215 contacts::types::ContactPropertyChanges,
1216 types::{CreateContactPropertyOptions, PropertyType},
1217 };
1218
1219 let resend = &*CLIENT;
1220
1221 let contact_property =
1223 CreateContactPropertyOptions::new("company_name", PropertyType::String)
1224 .with_fallback("Acme Corp");
1225 let contact_property = resend.contacts.create_property(contact_property).await?;
1226 std::thread::sleep(std::time::Duration::from_secs(2));
1227
1228 let contact_property = resend.contacts.get_property(&contact_property.id).await?;
1230
1231 let update = ContactPropertyChanges::default().with_fallback("Example Company");
1233 let contact_property = resend
1234 .contacts
1235 .update_property(&contact_property.id, update)
1236 .await?;
1237
1238 let contact_properties = resend
1240 .contacts
1241 .list_properties(ListOptions::default())
1242 .await?;
1243 assert!(!contact_properties.is_empty());
1244
1245 let deleted = resend
1247 .contacts
1248 .delete_property(&contact_property.id)
1249 .await?;
1250 assert!(deleted.deleted);
1251
1252 std::thread::sleep(std::time::Duration::from_secs(4));
1253
1254 Ok(())
1255 }
1256
1257 #[tokio_shared_rt::test(shared = true)]
1258 #[serial_test::serial]
1259 #[cfg(not(feature = "blocking"))]
1260 async fn contact_import() -> DebugResult<()> {
1261 use crate::{
1262 contacts::types::{
1263 ContactImportColumnMap, ContactImportOnConflict::Upsert,
1264 ContactImportPropertyMapping, ContactImportPropertyType::String,
1265 },
1266 types::CreateContactImportOptions,
1267 };
1268 use std::fs::File;
1269 use std::io::prelude::*;
1270
1271 let resend = &*CLIENT;
1272
1273 let mut file = File::create("foo.csv").unwrap();
1275 file.write_all(
1276 b"Email,First Name,Last Name,Plan\nonboarding@resend.dev,John,Onboarding,El Plan",
1277 )
1278 .unwrap();
1279
1280 let import = CreateContactImportOptions::new()
1281 .with_column_map(
1282 ContactImportColumnMap::new()
1283 .with_email("Email")
1284 .with_first_name("First Name")
1285 .with_last_name("Last Name")
1286 .with_property(
1287 "plan",
1288 ContactImportPropertyMapping {
1289 column: "Plan".to_owned(),
1290 r#type: String,
1291 },
1292 ),
1293 )
1294 .with_on_conflict(Upsert);
1295
1296 let import = resend
1297 .contacts
1298 .create_import(File::open("foo.csv").unwrap(), import)
1299 .await?;
1300
1301 std::thread::sleep(std::time::Duration::from_secs(2));
1302
1303 let _import = resend.contacts.get_import(&import.id).await?;
1305
1306 let imports = resend.contacts.list_imports(ListOptions::default()).await?;
1308 assert!(!imports.is_empty());
1309
1310 let deleted = resend.contacts.delete("onboarding@resend.dev").await?;
1312 assert!(deleted);
1313 let properties = resend
1314 .contacts
1315 .list_properties(ListOptions::default())
1316 .await?;
1317 let deleted = resend.contacts.delete_property(&properties[0].id).await?;
1318 assert!(deleted.deleted);
1319
1320 std::fs::remove_file("foo.csv").unwrap();
1321
1322 Ok(())
1323 }
1324
1325 #[test]
1326 fn deserialize_test() {
1327 let contact_property = r#"{
1328 "object": "contact_property",
1329 "id": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e",
1330 "key": "company_name",
1331 "type": "string",
1332 "fallback_value": "Acme Corp",
1333 "created_at": "2023-04-08 00:11:13.110779+00"
1334}"#;
1335
1336 let res = serde_json::from_str::<ContactProperty>(contact_property);
1337 assert!(res.is_ok());
1338 }
1339
1340 #[test]
1341 fn deserialize_test2() {
1342 let contact = r#"{
1343 "object": "contact",
1344 "id": "e169aa45-1ecf-4183-9955-b1499d5701d3",
1345 "email": "steve.wozniak@gmail.com",
1346 "first_name": "Steve",
1347 "last_name": "Wozniak",
1348 "created_at": "2026-10-06 23:47:56.678+00",
1349 "unsubscribed": false,
1350 "properties": {
1351 "key": {
1352 "value": "value",
1353 "type": "string"
1354 }
1355 }
1356 }"#;
1357
1358 let res = serde_json::from_str::<Contact>(contact);
1359 assert!(res.is_ok());
1360 }
1361
1362 #[test]
1363 fn deserialize_test3() {
1364 let contact = r#"{
1365 "object": "contact",
1366 "id": "e169aa45-1ecf-4183-9955-b1499d5701d3",
1367 "email": "steve.wozniak@gmail.com",
1368 "first_name": null,
1369 "last_name": null,
1370 "created_at": "2026-10-06 23:47:56.678+00",
1371 "unsubscribed": false,
1372 "properties": {
1373 "key": {
1374 "value": "value",
1375 "type": "string"
1376 }
1377 }
1378 }"#;
1379
1380 let res = serde_json::from_str::<Contact>(contact);
1381 assert!(res.is_ok());
1382 }
1383
1384 #[test]
1385 #[allow(clippy::indexing_slicing)]
1386 fn serialize_create_contact_with_extras() {
1387 use crate::types::{SubscriptionType, UpdateContactTopicOptions};
1388
1389 let topic = UpdateContactTopicOptions::new("topic_123", SubscriptionType::OptIn);
1390 let mut properties = HashMap::new();
1391 let _old = properties.insert("company".to_owned(), "Acme Corp".to_owned());
1392
1393 let contact = CreateContactOptions::new("test@example.com")
1394 .with_first_name("John")
1395 .with_last_name("Doe")
1396 .with_property("department", "Sales")
1397 .with_properties(properties)
1398 .with_segment("segment_123")
1399 .with_segments(&["segment_456".to_string()])
1400 .with_topic(topic)
1401 .with_topics(&[UpdateContactTopicOptions::new(
1402 "topic_789",
1403 SubscriptionType::OptOut,
1404 )]);
1405
1406 let json = serde_json::to_value(&contact).expect("Failed to serialize");
1407
1408 assert_eq!(json["email"], "test@example.com");
1410 assert_eq!(json["first_name"], "John");
1411 assert_eq!(json["last_name"], "Doe");
1412
1413 assert!(json["properties"].is_object());
1415 let properties = json["properties"]
1416 .as_object()
1417 .expect("properties should be a map");
1418 assert_eq!(properties.len(), 2);
1419 assert!(properties.contains_key("department"));
1420 assert_eq!(
1421 properties.get("department"),
1422 Some(serde_json::Value::String("Sales".to_owned())).as_ref()
1423 );
1424 assert_eq!(
1425 properties.get("company"),
1426 Some(serde_json::Value::String("Acme Corp".to_owned())).as_ref()
1427 );
1428
1429 assert!(json["segments"].is_array());
1431 let segments = json["segments"]
1432 .as_array()
1433 .expect("segments should be an array");
1434 assert_eq!(segments.len(), 2);
1435 assert_eq!(segments[0], serde_json::json!({"id": "segment_123"}));
1436 assert_eq!(segments[1], serde_json::json!({"id": "segment_456"}));
1437
1438 assert!(json["topics"].is_array());
1440 let topics = json["topics"]
1441 .as_array()
1442 .expect("topics should be an array");
1443 assert_eq!(topics.len(), 2);
1444 assert_eq!(topics[0]["id"], "topic_123");
1445 assert_eq!(topics[0]["subscription"], "opt_in");
1446 assert_eq!(topics[1]["id"], "topic_789");
1447 assert_eq!(topics[1]["subscription"], "opt_out");
1448 }
1449}