1use std::{collections::HashMap, env, error, fmt, fmt::Debug, sync::Arc};
24
25use chrono::{DateTime, Utc};
26use reqwest::{header, Client, Method, RequestBuilder, StatusCode, Url};
27use schemars::JsonSchema;
28use serde::{Deserialize, Serialize};
29
30pub struct MailChimp {
32 token: String,
33 refresh_token: String,
36 client_id: String,
37 client_secret: String,
38 redirect_uri: String,
39 endpoint: String,
40
41 client: Arc<Client>,
42}
43
44impl MailChimp {
45 pub fn new<I, K, R, T, Q, C>(
49 client_id: I,
50 client_secret: K,
51 redirect_uri: R,
52 token: T,
53 refresh_token: Q,
54 endpoint: C,
55 ) -> Self
56 where
57 I: ToString,
58 K: ToString,
59 R: ToString,
60 T: ToString,
61 Q: ToString,
62 C: ToString,
63 {
64 let client = Client::builder().build();
65 match client {
66 Ok(c) => {
67 let g = MailChimp {
68 client_id: client_id.to_string(),
69 client_secret: client_secret.to_string(),
70 redirect_uri: redirect_uri.to_string(),
71 token: token.to_string(),
72 refresh_token: refresh_token.to_string(),
73 endpoint: endpoint.to_string(),
74
75 client: Arc::new(c),
76 };
77
78 if g.token.is_empty() {
79 println!("mailchimp consent URL: {}", g.user_consent_url());
82 }
83 g
87 }
88 Err(e) => panic!("creating client failed: {:?}", e),
89 }
90 }
91
92 pub fn new_from_env<T, R, C>(token: T, refresh_token: R, endpoint: C) -> Self
99 where
100 T: ToString,
101 R: ToString,
102 C: ToString,
103 {
104 let client_id = env::var("MAILCHIMP_CLIENT_ID").unwrap();
105 let client_secret = env::var("MAILCHIMP_CLIENT_SECRET").unwrap();
106 let redirect_uri = env::var("MAILCHIMP_REDIRECT_URI").unwrap();
107
108 MailChimp::new(client_id, client_secret, redirect_uri, token, refresh_token, endpoint)
109 }
110
111 fn request<P>(&self, method: Method, path: P) -> RequestBuilder
112 where
113 P: ToString,
114 {
115 let base = Url::parse(&self.endpoint).unwrap();
117 let mut p = path.to_string();
118 if !p.starts_with('/') {
120 p = format!("/{}", p);
121 }
122 let url = base.join(&p).unwrap();
123
124 let bt = format!("Bearer {}", self.token);
125 let bearer = header::HeaderValue::from_str(&bt).unwrap();
126
127 let mut headers = header::HeaderMap::new();
129 headers.append(header::AUTHORIZATION, bearer);
130 headers.append(
131 header::CONTENT_TYPE,
132 header::HeaderValue::from_static("application/json"),
133 );
134
135 self.client.request(method, url).headers(headers)
136 }
137
138 pub fn user_consent_url(&self) -> String {
139 format!(
140 "https://login.mailchimp.com/oauth2/authorize?response_type=code&client_id={}&redirect_uri={}",
141 self.client_id, self.redirect_uri
142 )
143 }
144
145 pub async fn refresh_access_token(&mut self) -> Result<AccessToken, APIError> {
146 let mut headers = header::HeaderMap::new();
147 headers.append(
148 header::CONTENT_TYPE,
149 header::HeaderValue::from_static("application/x-www-form-urlencoded"),
150 );
151
152 let body = format!(
153 "grant_type=refresh_token&client_id={}&client_secret={}&\
154 redirect_uri={}refresh_token={}",
155 self.client_id,
156 self.client_secret,
157 urlencoding::encode(&self.redirect_uri),
158 self.refresh_token
159 );
160
161 let client = reqwest::Client::new();
162 let req = client
163 .post("https://login.mailchimp.com/oauth2/token")
164 .headers(headers)
165 .body(bytes::Bytes::from(body));
166 let resp = req.send().await.unwrap();
167
168 let t: AccessToken = resp.json().await.unwrap();
170
171 self.token = t.access_token.to_string();
172 self.refresh_token = t.refresh_token.to_string();
173
174 Ok(t)
175 }
176
177 pub async fn get_access_token(&mut self, code: &str) -> Result<AccessToken, APIError> {
178 let mut headers = header::HeaderMap::new();
179 headers.append(
180 header::CONTENT_TYPE,
181 header::HeaderValue::from_static("application/x-www-form-urlencoded"),
182 );
183
184 let body = format!(
185 "grant_type=authorization_code&client_id={}&client_secret={}&redirect_uri={}&code={}",
186 self.client_id,
187 self.client_secret,
188 urlencoding::encode(&self.redirect_uri),
189 code
190 );
191
192 let client = reqwest::Client::new();
193 let req = client
194 .post("https://login.mailchimp.com/oauth2/token")
195 .headers(headers)
196 .body(bytes::Bytes::from(body));
197 let resp = req.send().await.unwrap();
198
199 let t: AccessToken = resp.json().await.unwrap();
201
202 self.token = t.access_token.to_string();
203 self.refresh_token = t.refresh_token.to_string();
204
205 Ok(t)
206 }
207
208 pub async fn metadata(&self) -> Result<Metadata, APIError> {
210 let mut headers = header::HeaderMap::new();
211 headers.append(header::ACCEPT, header::HeaderValue::from_static("application/json"));
212 headers.append(
213 header::AUTHORIZATION,
214 header::HeaderValue::from_str(&format!("OAuth {}", self.token)).unwrap(),
215 );
216
217 let client = reqwest::Client::new();
219 let resp = client
220 .get("https://login.mailchimp.com/oauth2/metadata")
221 .headers(headers)
222 .send()
223 .await
224 .unwrap();
225 match resp.status() {
226 StatusCode::OK => (),
227 s => {
228 return Err(APIError {
229 status_code: s,
230 body: resp.text().await.unwrap(),
231 })
232 }
233 };
234
235 Ok(resp.json().await.unwrap())
237 }
238
239 pub async fn get_subscribers(&self, list_id: &str) -> Result<Vec<Member>, APIError> {
241 let per_page = 500;
242 let mut offset: usize = 0;
243
244 let mut members: Vec<Member> = Default::default();
245
246 let mut has_more_rows = true;
247
248 while has_more_rows {
249 let rb = self.request(
251 Method::GET,
252 &format!("3.0/lists/{}/members?count={}&offset={}", list_id, per_page, offset,),
253 );
254 let request = rb.build().unwrap();
255
256 let resp = self.client.execute(request).await.unwrap();
257 match resp.status() {
258 StatusCode::OK => (),
259 s => {
260 return Err(APIError {
261 status_code: s,
262 body: resp.text().await.unwrap(),
263 })
264 }
265 };
266
267 let text = resp.text().await.unwrap();
268
269 let mut r: ListMembersResponse = serde_json::from_str(&text).unwrap();
270
271 has_more_rows = !r.members.is_empty();
272 offset += r.members.len();
273
274 members.append(&mut r.members);
275 }
276
277 Ok(members)
278 }
279}
280
281pub struct APIError {
283 pub status_code: StatusCode,
284 pub body: String,
285}
286
287impl fmt::Display for APIError {
288 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
289 write!(
290 f,
291 "APIError: status code -> {}, body -> {}",
292 self.status_code.to_string(),
293 self.body
294 )
295 }
296}
297
298impl fmt::Debug for APIError {
299 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
300 write!(
301 f,
302 "APIError: status code -> {}, body -> {}",
303 self.status_code.to_string(),
304 self.body
305 )
306 }
307}
308
309impl error::Error for APIError {
311 fn source(&self) -> Option<&(dyn error::Error + 'static)> {
312 None
314 }
315}
316
317#[derive(Debug, JsonSchema, Clone, Default, Serialize, Deserialize)]
318pub struct AccessToken {
319 #[serde(
320 default,
321 skip_serializing_if = "String::is_empty",
322 deserialize_with = "deserialize_null_string::deserialize"
323 )]
324 pub access_token: String,
325 #[serde(
326 default,
327 skip_serializing_if = "String::is_empty",
328 deserialize_with = "deserialize_null_string::deserialize"
329 )]
330 pub token_type: String,
331 #[serde(default)]
332 pub expires_in: i64,
333 #[serde(default)]
334 pub x_refresh_token_expires_in: i64,
335 #[serde(
336 default,
337 skip_serializing_if = "String::is_empty",
338 deserialize_with = "deserialize_null_string::deserialize"
339 )]
340 pub refresh_token: String,
341}
342
343pub mod deserialize_null_string {
344 use serde::{self, Deserialize, Deserializer};
345
346 pub fn deserialize<'de, D>(deserializer: D) -> Result<String, D::Error>
354 where
355 D: Deserializer<'de>,
356 {
357 let s = String::deserialize(deserializer).unwrap_or_default();
358
359 Ok(s)
360 }
361}
362
363#[derive(Debug, Clone, Default, JsonSchema, Deserialize, Serialize)]
364pub struct MergeFields {
365 #[serde(default, skip_serializing_if = "String::is_empty", alias = "FNAME")]
366 pub first_name: String,
367 #[serde(default, skip_serializing_if = "String::is_empty", alias = "LNAME")]
368 pub last_name: String,
369 #[serde(default, skip_serializing_if = "String::is_empty", alias = "NAME")]
370 pub name: String,
371 #[serde(
372 default,
373 skip_serializing_if = "String::is_empty",
374 alias = "COMPANY",
375 alias = "CNAME"
376 )]
377 pub company: String,
378 #[serde(default, skip_serializing_if = "String::is_empty", alias = "CSIZE")]
379 pub company_size: String,
380 #[serde(default, skip_serializing_if = "String::is_empty", alias = "INTEREST")]
381 pub interest: String,
382 #[serde(default, skip_serializing_if = "String::is_empty", alias = "NOTES")]
383 pub notes: String,
384 #[serde(default, skip_serializing_if = "String::is_empty", alias = "BIRTHDAY")]
385 pub birthday: String,
386 #[serde(default, skip_serializing_if = "String::is_empty", alias = "PHONE")]
387 pub phone: String,
388 #[serde(default, alias = "ADDRESS")]
389 pub address: serde_json::Value,
390}
391
392#[derive(Debug, Clone, Default, JsonSchema, Deserialize, Serialize)]
393pub struct Address {
394 #[serde(default, skip_serializing_if = "String::is_empty")]
395 pub addr1: String,
396 #[serde(default, skip_serializing_if = "String::is_empty")]
397 pub addr2: String,
398 #[serde(default, skip_serializing_if = "String::is_empty")]
399 pub city: String,
400 #[serde(default, skip_serializing_if = "String::is_empty")]
401 pub state: String,
402 #[serde(default, skip_serializing_if = "String::is_empty")]
403 pub zip: String,
404 #[serde(default, skip_serializing_if = "String::is_empty")]
405 pub country: String,
406}
407
408#[derive(Debug, Clone, Default, JsonSchema, Deserialize, Serialize)]
409pub struct Location {
410 #[serde(default)]
412 pub latitude: f64,
413 #[serde(default)]
415 pub longitude: f64,
416 #[serde(default)]
418 pub gmtoff: i32,
419 #[serde(default)]
421 pub dstoff: i32,
422 #[serde(default, skip_serializing_if = "String::is_empty")]
424 pub country_code: String,
425 #[serde(default, skip_serializing_if = "String::is_empty")]
427 pub time_zone: String,
428}
429
430#[derive(Debug, Clone, Default, JsonSchema, Deserialize, Serialize)]
431pub struct MarketingPermissions {
432 #[serde(default, skip_serializing_if = "String::is_empty")]
434 pub marketing_permission_id: String,
435 #[serde(default, skip_serializing_if = "String::is_empty")]
437 pub text: String,
438 #[serde(default)]
440 pub enabled: bool,
441}
442
443#[derive(Debug, Clone, Default, JsonSchema, Deserialize, Serialize)]
444pub struct LastNote {
445 #[serde(default)]
447 pub note_id: i64,
448 #[serde(default)]
450 pub created_at: Option<DateTime<Utc>>,
451 #[serde(default, skip_serializing_if = "String::is_empty")]
453 pub created_by: String,
454 #[serde(default, skip_serializing_if = "String::is_empty")]
456 pub note: String,
457}
458
459#[derive(Debug, Clone, Default, JsonSchema, Deserialize, Serialize)]
460pub struct Tag {
461 #[serde(default)]
463 pub id: i64,
464 #[serde(default, skip_serializing_if = "String::is_empty")]
466 pub name: String,
467}
468
469#[derive(Debug, Clone, JsonSchema, Deserialize, Serialize)]
473pub struct Webhook {
474 #[serde(rename = "type")]
475 pub webhook_type: String,
476 #[serde(
477 deserialize_with = "mailchimp_date_format::deserialize",
478 serialize_with = "mailchimp_date_format::serialize"
479 )]
480 pub fired_at: DateTime<Utc>,
481 pub data: WebhookData,
482}
483
484mod mailchimp_date_format {
485 use chrono::{DateTime, TimeZone, Utc};
486 use serde::{self, Deserialize, Deserializer, Serializer};
487
488 const FORMAT: &str = "%Y-%m-%d %H:%M:%S";
489
490 pub fn serialize<S>(date: &DateTime<Utc>, serializer: S) -> Result<S::Ok, S::Error>
498 where
499 S: Serializer,
500 {
501 let s = format!("{}", date.format(FORMAT));
502 serializer.serialize_str(&s)
503 }
504
505 pub fn deserialize<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
513 where
514 D: Deserializer<'de>,
515 {
516 let s = String::deserialize(deserializer).unwrap();
517 Utc.datetime_from_str(&s, FORMAT).map_err(serde::de::Error::custom)
518 }
519}
520
521#[derive(Debug, Clone, JsonSchema, Deserialize, Serialize)]
522pub struct WebhookData {
523 #[serde(skip_serializing_if = "Option::is_none")]
524 pub id: Option<String>,
525 #[serde(skip_serializing_if = "Option::is_none")]
526 pub list_id: Option<String>,
527 #[serde(skip_serializing_if = "Option::is_none")]
528 pub email: Option<String>,
529 #[serde(skip_serializing_if = "Option::is_none")]
530 pub email_type: Option<String>,
531 #[serde(skip_serializing_if = "Option::is_none")]
532 pub ip_opt: Option<String>,
533 #[serde(skip_serializing_if = "Option::is_none")]
534 pub ip_signup: Option<String>,
535 #[serde(skip_serializing_if = "Option::is_none")]
536 pub reason: Option<String>,
537 #[serde(skip_serializing_if = "Option::is_none")]
538 pub status: Option<String>,
539 #[serde(skip_serializing_if = "Option::is_none")]
540 pub web_id: Option<String>,
541 #[serde(skip_serializing_if = "Option::is_none")]
542 pub merges: Option<WebhookMerges>,
543}
544
545#[derive(Debug, Clone, JsonSchema, Deserialize, Serialize)]
546pub struct WebhookMerges {
547 #[serde(skip_serializing_if = "Option::is_none", rename = "FNAME")]
548 pub first_name: Option<String>,
549 #[serde(skip_serializing_if = "Option::is_none", rename = "LNAME")]
550 pub last_name: Option<String>,
551 #[serde(skip_serializing_if = "Option::is_none", rename = "NAME")]
552 pub name: Option<String>,
553 #[serde(skip_serializing_if = "Option::is_none", rename = "EMAIL")]
554 pub email: Option<String>,
555 #[serde(skip_serializing_if = "Option::is_none", rename = "ADDRESS")]
556 pub address: Option<String>,
557 #[serde(skip_serializing_if = "Option::is_none", rename = "PHONE")]
558 pub phone: Option<String>,
559 #[serde(skip_serializing_if = "Option::is_none", alias = "COMPANY", alias = "CNAME")]
560 pub company: Option<String>,
561 #[serde(skip_serializing_if = "Option::is_none", alias = "CSIZE")]
562 pub company_size: Option<String>,
563 #[serde(skip_serializing_if = "Option::is_none", rename = "INTEREST")]
564 pub interest: Option<String>,
565 #[serde(skip_serializing_if = "Option::is_none", rename = "NOTES")]
566 pub notes: Option<String>,
567 #[serde(skip_serializing_if = "Option::is_none", rename = "BIRTHDAY")]
568 pub birthday: Option<String>,
569 #[serde(skip_serializing_if = "Option::is_none", rename = "GROUPINGS")]
570 pub groupings: Option<Vec<WebhookGrouping>>,
571}
572
573#[derive(Debug, Clone, JsonSchema, Deserialize, Serialize)]
574pub struct WebhookGrouping {
575 pub id: String,
576 pub unique_id: String,
577 pub name: String,
578 #[serde(skip_serializing_if = "Option::is_none")]
579 pub groups: Option<String>,
580}
581
582#[derive(Debug, Default, Clone, JsonSchema, Deserialize, Serialize)]
583pub struct Metadata {
584 #[serde(
585 default,
586 skip_serializing_if = "String::is_empty",
587 deserialize_with = "deserialize_null_string::deserialize"
588 )]
589 pub dc: String,
590 #[serde(
591 default,
592 skip_serializing_if = "String::is_empty",
593 deserialize_with = "deserialize_null_string::deserialize"
594 )]
595 pub accountname: String,
596 #[serde(
597 default,
598 skip_serializing_if = "String::is_empty",
599 deserialize_with = "deserialize_null_string::deserialize"
600 )]
601 pub api_endpoint: String,
602 #[serde(default)]
603 pub login: Login,
604}
605
606#[derive(Debug, Default, Clone, JsonSchema, Deserialize, Serialize)]
607pub struct Login {
608 #[serde(
609 default,
610 skip_serializing_if = "String::is_empty",
611 deserialize_with = "deserialize_null_string::deserialize"
612 )]
613 pub avatar: String,
614 #[serde(
615 default,
616 skip_serializing_if = "String::is_empty",
617 deserialize_with = "deserialize_null_string::deserialize"
618 )]
619 pub email: String,
620 #[serde(
621 default,
622 skip_serializing_if = "String::is_empty",
623 deserialize_with = "deserialize_null_string::deserialize"
624 )]
625 pub login_email: String,
626 #[serde(default)]
627 pub login_id: i64,
628 #[serde(
629 default,
630 skip_serializing_if = "String::is_empty",
631 deserialize_with = "deserialize_null_string::deserialize"
632 )]
633 pub login_name: String,
634 #[serde(
635 default,
636 skip_serializing_if = "String::is_empty",
637 deserialize_with = "deserialize_null_string::deserialize"
638 )]
639 pub login_url: String,
640 #[serde(
641 default,
642 skip_serializing_if = "String::is_empty",
643 deserialize_with = "deserialize_null_string::deserialize"
644 )]
645 pub role: String,
646 #[serde(default)]
647 pub user_id: i64,
648}
649
650#[derive(Debug, Clone, Default, JsonSchema, Deserialize, Serialize)]
655pub struct ListMembersResponse {
656 #[serde(default, skip_serializing_if = "Vec::is_empty")]
658 pub members: Vec<Member>,
659 #[serde(default, skip_serializing_if = "String::is_empty")]
660 pub list_id: String,
661 #[serde(default)]
662 pub total_items: i64,
663}
664
665#[derive(Debug, Clone, JsonSchema, Deserialize, Serialize)]
669pub struct Member {
670 #[serde(default, skip_serializing_if = "String::is_empty")]
672 pub id: String,
673 #[serde(default, skip_serializing_if = "String::is_empty")]
675 pub email_address: String,
676 #[serde(default, skip_serializing_if = "String::is_empty")]
678 pub unique_email_id: String,
679 #[serde(default)]
683 pub web_id: i64,
684 #[serde(default, skip_serializing_if = "String::is_empty")]
686 pub email_type: String,
687 #[serde(default, skip_serializing_if = "String::is_empty")]
691 pub status: String,
692 #[serde(default, skip_serializing_if = "String::is_empty")]
694 pub unsubscribe_reason: String,
695 #[serde(default)]
697 pub merge_fields: MergeFields,
698 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
700 pub interests: HashMap<String, bool>,
701 #[serde(default, skip_serializing_if = "String::is_empty")]
703 pub ip_signup: String,
704 #[serde(default, skip_serializing_if = "String::is_empty")]
706 pub timestamp_signup: String,
707 #[serde(default, skip_serializing_if = "String::is_empty")]
709 pub ip_opt: String,
710 #[serde(default, skip_serializing_if = "String::is_empty")]
712 pub timestamp_opt: String,
713 #[serde(default)]
715 pub star_rating: i32,
716 pub last_changed: DateTime<Utc>,
718 #[serde(default, skip_serializing_if = "String::is_empty")]
720 pub language: String,
721 #[serde(default)]
723 pub vip_status: bool,
724 #[serde(default, skip_serializing_if = "String::is_empty")]
726 pub email_client: String,
727 #[serde(default)]
729 pub location: Location,
730 #[serde(default, skip_serializing_if = "Vec::is_empty")]
732 pub marketing_permissions: Vec<MarketingPermissions>,
733 #[serde(default)]
735 pub last_note: LastNote,
736 #[serde(default, skip_serializing_if = "String::is_empty")]
738 pub source: String,
739 #[serde(default, skip_serializing_if = "Vec::is_empty")]
742 pub tags: Vec<Tag>,
743 #[serde(default, skip_serializing_if = "String::is_empty")]
745 pub list_id: String,
746 #[serde(default)]
747 pub stats: Stats,
748}
749
750#[derive(Debug, Clone, Default, JsonSchema, Deserialize, Serialize)]
751pub struct Stats {
752 #[serde(default)]
753 pub avg_open_rate: f32,
754 #[serde(default)]
755 pub avg_click_rate: f32,
756 #[serde(default)]
757 pub ecommerce_data: EcommerceData,
758}
759
760#[derive(Debug, Clone, Default, JsonSchema, Deserialize, Serialize)]
761pub struct EcommerceData {
762 #[serde(default)]
763 pub total_revenue: f32,
764 #[serde(default)]
765 pub number_of_orders: i32,
766 #[serde(default, skip_serializing_if = "String::is_empty")]
767 pub currency_code: String,
768}
769
770#[cfg(test)]
771mod tests {
772 use serde_qs::Config as QSConfig;
773
774 use super::*;
775
776 #[test]
777 fn test_mailchimp_webhook_parsing() {
778 let body = r#"type=subscribe&fired_at=2020-09-07 21:31:09&data[id]=b748506b63&data[email]=example@gmail.com&data[email_type]=html&data[ip_opt]=98.128.229.135&data[web_id]=404947702&data[merges][EMAIL]=example@gmail.com&data[merges][FNAME]=&data[merges][LNAME]=&data[merges][ADDRESS]=&data[merges][PHONE]=&data[merges][BIRTHDAY]=&data[merges][COMPANY]=&data[merges][INTEREST]=8&data[merges][INTERESTS]=Yes&data[merges][GROUPINGS][0][id]=6197&data[merges][GROUPINGS][0][unique_id]=458a556058&data[merges][GROUPINGS][0][name]=Interested in On the Metal podcast updates?&data[merges][GROUPINGS][0][groups]=Yes&data[merges][GROUPINGS][1][id]=6245&data[merges][GROUPINGS][1][unique_id]=f64af23d78&data[merges][GROUPINGS][1][name]=Interested in the Oxide newsletter?&data[merges][GROUPINGS][1][groups]=Yes&data[merges][GROUPINGS][2][id]=7518&data[merges][GROUPINGS][2][unique_id]=a9829c90a6&data[merges][GROUPINGS][2][name]=Interested in product updates?&data[merges][GROUPINGS][2][groups]=Yes&data[list_id]=8a6d823488"#;
779
780 let qs_non_strict = QSConfig::new(10, false);
781
782 let webhook: Webhook = qs_non_strict.deserialize_bytes(body.as_bytes()).unwrap();
784
785 println!("{:#?}", webhook);
786 }
787}