Skip to main content

axene_mailer/
models.rs

1//! Serde structs for every request and response shape on the Axene Mailer API.
2//!
3//! Wire quirks are honoured here via `#[serde(rename = ...)]`: the sender field
4//! serializes as the literal key `from_`, template bodies map `html`/`text` to
5//! `html_body`/`text_body`, suppression `email` maps to `email_address`, and so
6//! on. Loose or provider-specific shapes are modelled as [`serde_json::Value`].
7
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10use std::collections::HashMap;
11
12// -- shared -----------------------------------------------------------------
13
14/// A recipient or sender address. A bare string is sugar for `{ email }`.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct Address {
17    /// The email address (required).
18    pub email: String,
19    /// Optional display name.
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub name: Option<String>,
22}
23
24impl Address {
25    /// Build an address with just an email.
26    pub fn new(email: impl Into<String>) -> Self {
27        Self {
28            email: email.into(),
29            name: None,
30        }
31    }
32
33    /// Build an address with an email and a display name.
34    pub fn named(email: impl Into<String>, name: impl Into<String>) -> Self {
35        Self {
36            email: email.into(),
37            name: Some(name.into()),
38        }
39    }
40}
41
42impl From<&str> for Address {
43    fn from(s: &str) -> Self {
44        Address::new(s)
45    }
46}
47
48impl From<String> for Address {
49    fn from(s: String) -> Self {
50        Address::new(s)
51    }
52}
53
54impl From<(&str, &str)> for Address {
55    fn from((email, name): (&str, &str)) -> Self {
56        Address::named(email, name)
57    }
58}
59
60/// A file attachment. `content_base64` is raw base64 with no `data:` prefix.
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct Attachment {
63    /// File name (1-255 chars, no `/`, `\`, or NUL).
64    pub filename: String,
65    /// Raw base64-encoded file content.
66    pub content_base64: String,
67    /// MIME type. Defaults server-side to `application/octet-stream`.
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub content_type: Option<String>,
70}
71
72/// A paginated envelope `{ items, total, page, limit }`.
73#[derive(Debug, Clone, Deserialize)]
74pub struct Page<T> {
75    /// The items on this page.
76    pub items: Vec<T>,
77    /// Total number of items across all pages.
78    pub total: u64,
79    /// Zero-based page index.
80    pub page: u64,
81    /// Page size.
82    pub limit: u64,
83}
84
85// -- emails: request --------------------------------------------------------
86
87/// Body of a send / validate request.
88///
89/// Build with [`SendEmail::builder`]. The sender field serializes as `from_`.
90#[derive(Debug, Clone, Serialize)]
91pub struct SendEmail {
92    /// Sender address (serialized as `from_` on the wire).
93    #[serde(rename = "from_")]
94    pub from: Address,
95    /// One or more recipients.
96    pub to: Vec<Address>,
97    /// Subject line.
98    pub subject: String,
99    /// HTML body.
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub html: Option<String>,
102    /// Plain-text body.
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub text: Option<String>,
105    /// CC recipients.
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub cc: Option<Vec<Address>>,
108    /// BCC recipients.
109    #[serde(skip_serializing_if = "Option::is_none")]
110    pub bcc: Option<Vec<Address>>,
111    /// Reply-To address.
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub reply_to: Option<Address>,
114    /// Custom message headers.
115    #[serde(skip_serializing_if = "Option::is_none")]
116    pub headers: Option<HashMap<String, String>>,
117    /// Tags for filtering and analytics.
118    #[serde(skip_serializing_if = "Option::is_none")]
119    pub tags: Option<Vec<String>>,
120    /// Schedule delivery for later (ISO 8601). Starter plan and up.
121    #[serde(skip_serializing_if = "Option::is_none")]
122    pub send_at: Option<String>,
123    /// File attachments.
124    #[serde(skip_serializing_if = "Option::is_none")]
125    pub attachments: Option<Vec<Attachment>>,
126}
127
128impl SendEmail {
129    /// Start building a message with a sender, recipients, and subject.
130    pub fn builder(
131        from: impl Into<Address>,
132        to: impl IntoAddressList,
133        subject: impl Into<String>,
134    ) -> SendEmailBuilder {
135        SendEmailBuilder {
136            inner: SendEmail {
137                from: from.into(),
138                to: to.into_address_list(),
139                subject: subject.into(),
140                html: None,
141                text: None,
142                cc: None,
143                bcc: None,
144                reply_to: None,
145                headers: None,
146                tags: None,
147                send_at: None,
148                attachments: None,
149            },
150        }
151    }
152}
153
154/// Conversion trait so `to`/`cc`/`bcc` accept a single address or a list.
155pub trait IntoAddressList {
156    /// Normalize one-or-many addresses into a vector.
157    fn into_address_list(self) -> Vec<Address>;
158}
159
160impl IntoAddressList for Address {
161    fn into_address_list(self) -> Vec<Address> {
162        vec![self]
163    }
164}
165
166impl IntoAddressList for &str {
167    fn into_address_list(self) -> Vec<Address> {
168        vec![Address::new(self)]
169    }
170}
171
172impl IntoAddressList for String {
173    fn into_address_list(self) -> Vec<Address> {
174        vec![Address::new(self)]
175    }
176}
177
178impl<T: Into<Address>> IntoAddressList for Vec<T> {
179    fn into_address_list(self) -> Vec<Address> {
180        self.into_iter().map(Into::into).collect()
181    }
182}
183
184/// Ergonomic builder for [`SendEmail`].
185#[derive(Debug, Clone)]
186pub struct SendEmailBuilder {
187    inner: SendEmail,
188}
189
190impl SendEmailBuilder {
191    /// Set the HTML body.
192    pub fn html(mut self, html: impl Into<String>) -> Self {
193        self.inner.html = Some(html.into());
194        self
195    }
196
197    /// Set the plain-text body.
198    pub fn text(mut self, text: impl Into<String>) -> Self {
199        self.inner.text = Some(text.into());
200        self
201    }
202
203    /// Set CC recipients.
204    pub fn cc(mut self, cc: impl IntoAddressList) -> Self {
205        self.inner.cc = Some(cc.into_address_list());
206        self
207    }
208
209    /// Set BCC recipients.
210    pub fn bcc(mut self, bcc: impl IntoAddressList) -> Self {
211        self.inner.bcc = Some(bcc.into_address_list());
212        self
213    }
214
215    /// Set the Reply-To address.
216    pub fn reply_to(mut self, reply_to: impl Into<Address>) -> Self {
217        self.inner.reply_to = Some(reply_to.into());
218        self
219    }
220
221    /// Set custom headers.
222    pub fn headers(mut self, headers: HashMap<String, String>) -> Self {
223        self.inner.headers = Some(headers);
224        self
225    }
226
227    /// Set tags.
228    pub fn tags(mut self, tags: Vec<String>) -> Self {
229        self.inner.tags = Some(tags);
230        self
231    }
232
233    /// Schedule delivery for later (ISO 8601 string).
234    pub fn send_at(mut self, send_at: impl Into<String>) -> Self {
235        self.inner.send_at = Some(send_at.into());
236        self
237    }
238
239    /// Set attachments.
240    pub fn attachments(mut self, attachments: Vec<Attachment>) -> Self {
241        self.inner.attachments = Some(attachments);
242        self
243    }
244
245    /// Finish building.
246    pub fn build(self) -> SendEmail {
247        self.inner
248    }
249}
250
251// -- emails: response -------------------------------------------------------
252
253/// Result of a send: the queued message id and its initial status.
254#[derive(Debug, Clone, Deserialize)]
255pub struct SendEmailResponse {
256    /// The new message id.
257    pub id: String,
258    /// Initial status, e.g. `queued`.
259    pub status: String,
260    /// Provider message id, if assigned.
261    pub message_id: Option<String>,
262    /// Reason the message was rejected, if any.
263    pub rejection_reason: Option<String>,
264}
265
266/// One per-message result inside a batch response.
267#[derive(Debug, Clone, Deserialize)]
268pub struct BatchItemResult {
269    /// The new message id, or `None` if the item errored.
270    pub id: Option<String>,
271    /// Per-item status, e.g. `queued` or `error`.
272    pub status: String,
273    /// Reason the item was rejected, if any.
274    pub rejection_reason: Option<String>,
275}
276
277/// Result of a batch send.
278#[derive(Debug, Clone, Deserialize)]
279pub struct BatchResponse {
280    /// Number of messages submitted.
281    pub total: u64,
282    /// Number accepted for delivery.
283    pub sent: u64,
284    /// Number rejected.
285    pub failed: u64,
286    /// One result per submitted message, in order.
287    pub results: Vec<BatchItemResult>,
288}
289
290/// A single reason a message would not send.
291#[derive(Debug, Clone, Deserialize)]
292pub struct ValidationIssue {
293    /// The offending field.
294    pub field: String,
295    /// The error description.
296    pub error: String,
297}
298
299/// Sending-quota usage returned alongside a validation.
300#[derive(Debug, Clone, Deserialize)]
301pub struct ValidationUsage {
302    /// Emails sent today.
303    pub daily: u64,
304    /// Daily send limit.
305    pub daily_limit: u64,
306    /// Emails sent this month.
307    pub monthly: u64,
308    /// Monthly send limit.
309    pub monthly_limit: u64,
310}
311
312/// Result of a dry-run validation.
313#[derive(Debug, Clone, Deserialize)]
314pub struct ValidationResult {
315    /// Whether the request body is well-formed.
316    pub valid: bool,
317    /// Whether the message could actually be sent right now.
318    pub can_send: bool,
319    /// Any issues that would block the send.
320    pub issues: Vec<ValidationIssue>,
321    /// The caller's plan.
322    pub plan: String,
323    /// Current quota usage.
324    pub usage: ValidationUsage,
325}
326
327/// A stored email and its current status.
328#[derive(Debug, Clone, Deserialize)]
329pub struct Email {
330    /// Message id.
331    pub id: String,
332    /// Sender address.
333    pub from_address: String,
334    /// Recipient addresses.
335    pub to_addresses: Vec<String>,
336    /// Subject line.
337    pub subject: Option<String>,
338    /// Current status.
339    pub status: String,
340    /// Where the message originated (e.g. `api`).
341    #[serde(default)]
342    pub source: Option<String>,
343    /// Number of opens.
344    #[serde(default)]
345    pub opened_count: Option<u64>,
346    /// Number of clicks.
347    #[serde(default)]
348    pub clicked_count: Option<u64>,
349    /// Tags.
350    #[serde(default)]
351    pub tags: Option<Vec<String>>,
352    /// Scheduled send time, if any.
353    #[serde(default)]
354    pub scheduled_at: Option<String>,
355    /// Creation time.
356    pub created_at: Option<String>,
357    /// Send time, if sent.
358    #[serde(default)]
359    pub sent_at: Option<String>,
360    /// Delivery time, if delivered.
361    #[serde(default)]
362    pub delivered_at: Option<String>,
363    /// Id of the original message this is a retry of, if any.
364    #[serde(default)]
365    pub retry_of_id: Option<String>,
366}
367
368/// A delivery / open / click / bounce event for a message.
369#[derive(Debug, Clone, Deserialize)]
370pub struct EmailEvent {
371    /// Event id.
372    pub id: String,
373    /// Event type, e.g. `delivered`.
374    pub event_type: String,
375    /// Arbitrary event metadata.
376    #[serde(default)]
377    pub metadata: Option<Value>,
378    /// Creation time.
379    pub created_at: String,
380}
381
382/// A stored email with its bodies and events.
383#[derive(Debug, Clone, Deserialize)]
384pub struct EmailDetail {
385    /// The base email row.
386    #[serde(flatten)]
387    pub email: Email,
388    /// CC addresses.
389    #[serde(default)]
390    pub cc_addresses: Option<Vec<String>>,
391    /// BCC addresses.
392    #[serde(default)]
393    pub bcc_addresses: Option<Vec<String>>,
394    /// Plain-text body.
395    #[serde(default)]
396    pub text_body: Option<String>,
397    /// HTML body.
398    #[serde(default)]
399    pub html_body: Option<String>,
400    /// Custom headers.
401    #[serde(default)]
402    pub headers: Option<Value>,
403    /// Provider message id.
404    #[serde(default)]
405    pub message_id: Option<String>,
406    /// Associated events.
407    #[serde(default)]
408    pub events: Vec<EmailEvent>,
409}
410
411/// A scheduled email awaiting send.
412#[derive(Debug, Clone, Deserialize)]
413pub struct ScheduledEmail {
414    /// Message id.
415    pub id: String,
416    /// Sender address.
417    pub from_address: String,
418    /// Recipient addresses.
419    pub to_addresses: Vec<String>,
420    /// Subject line.
421    pub subject: Option<String>,
422    /// Current status (always `scheduled`).
423    pub status: String,
424    /// Tags.
425    #[serde(default)]
426    pub tags: Option<Vec<String>>,
427    /// Scheduled send time.
428    #[serde(default)]
429    pub scheduled_at: Option<String>,
430    /// Seconds remaining until send.
431    pub seconds_until_send: i64,
432    /// Creation time.
433    pub created_at: Option<String>,
434}
435
436/// A search hit from the email search endpoint.
437#[derive(Debug, Clone, Deserialize)]
438pub struct EmailSearchHit {
439    /// Message id.
440    pub id: String,
441    /// Sender address.
442    pub from_address: String,
443    /// Recipient addresses.
444    pub to_addresses: Vec<String>,
445    /// Subject line.
446    pub subject: Option<String>,
447    /// Current status.
448    pub status: String,
449    /// Tags.
450    #[serde(default)]
451    pub tags: Option<Vec<String>>,
452    /// Where the message originated.
453    #[serde(default)]
454    pub source: Option<String>,
455    /// Creation time.
456    pub created_at: Option<String>,
457    /// Delivery time.
458    #[serde(default)]
459    pub delivered_at: Option<String>,
460}
461
462/// A simple `{ id, status }` response (cancel / send-now).
463#[derive(Debug, Clone, Deserialize)]
464pub struct IdStatus {
465    /// The affected id.
466    pub id: String,
467    /// The resulting status.
468    pub status: String,
469}
470
471// -- domains ----------------------------------------------------------------
472
473/// A row from the domain list: a sending domain and its status.
474#[derive(Debug, Clone, Deserialize)]
475pub struct DomainListItem {
476    /// Domain id.
477    pub id: String,
478    /// Domain name.
479    pub name: String,
480    /// Verification status.
481    pub status: String,
482    /// Creation time.
483    pub created_at: Option<String>,
484    /// Warning if this is a platform/shared domain.
485    #[serde(default)]
486    pub platform_warning: Option<String>,
487}
488
489/// A DNS record the API expects you to publish for a domain.
490#[derive(Debug, Clone, Deserialize)]
491pub struct DnsRecord {
492    /// Record id.
493    pub id: String,
494    /// Record type, e.g. `TXT`.
495    pub record_type: String,
496    /// What the record is for, e.g. `dkim`.
497    pub purpose: String,
498    /// The host/name.
499    pub host: String,
500    /// The expected value.
501    pub value: String,
502    /// Whether the record has been verified.
503    pub is_verified: bool,
504    /// When the record was last checked.
505    #[serde(default)]
506    pub last_checked_at: Option<String>,
507}
508
509/// A sending domain with its DKIM selector and DNS records.
510#[derive(Debug, Clone, Deserialize)]
511pub struct Domain {
512    /// Domain id.
513    pub id: String,
514    /// Domain name.
515    pub name: String,
516    /// Verification status.
517    pub status: String,
518    /// The DKIM selector.
519    pub dkim_selector: String,
520    /// When the domain was verified.
521    #[serde(default)]
522    pub verified_at: Option<String>,
523    /// Creation time.
524    pub created_at: Option<String>,
525    /// DNS records to publish.
526    pub dns_records: Vec<DnsRecord>,
527    /// Warning if this is a platform/shared domain.
528    #[serde(default)]
529    pub platform_warning: Option<String>,
530}
531
532/// One row of a domain health report.
533#[derive(Debug, Clone, Deserialize)]
534pub struct DomainHealthCheck {
535    /// Check key.
536    pub key: String,
537    /// Human-readable label.
538    pub label: String,
539    /// Outcome: `ok`, `warn`, `error`, or `info`.
540    pub status: String,
541    /// Detail message.
542    pub detail: String,
543    /// Recommended fix, if any.
544    #[serde(default)]
545    pub recommendation: Option<String>,
546    /// The associated DNS record, if any.
547    #[serde(default)]
548    pub record: Option<Value>,
549}
550
551/// Tally of health-check outcomes.
552#[derive(Debug, Clone, Deserialize)]
553pub struct DomainHealthSummary {
554    /// Number of `ok` checks.
555    pub ok: u64,
556    /// Number of `warn` checks.
557    pub warn: u64,
558    /// Number of `error` checks.
559    pub error: u64,
560    /// Number of `info` checks.
561    pub info: u64,
562}
563
564/// Result of a domain health report.
565#[derive(Debug, Clone, Deserialize)]
566pub struct DomainHealth {
567    /// Domain name.
568    pub domain: String,
569    /// Per-record checks.
570    pub checks: Vec<DomainHealthCheck>,
571    /// Summary tally.
572    pub summary: DomainHealthSummary,
573}
574
575/// Result of a domain diagnosis. `issues` shapes vary; treated as opaque.
576#[derive(Debug, Clone, Deserialize)]
577pub struct DomainDiagnosis {
578    /// Domain name.
579    pub domain: String,
580    /// The detected issues (loose shape).
581    pub issues: Vec<Value>,
582    /// A 0-100 health score.
583    pub health_score: i64,
584}
585
586/// Result of a DKIM rotation: the new record plus the updated domain.
587#[derive(Debug, Clone, Deserialize)]
588pub struct DkimRotation {
589    /// New DKIM record host.
590    pub dkim_record_host: String,
591    /// New DKIM record value.
592    pub dkim_record_value: String,
593    /// The updated domain.
594    pub domain: Domain,
595}
596
597/// A domain transfer record.
598#[derive(Debug, Clone, Deserialize)]
599pub struct DomainTransfer {
600    /// Transfer id.
601    pub id: String,
602    /// The domain being transferred.
603    pub domain_id: String,
604    /// The domain name.
605    #[serde(default)]
606    pub domain_name: Option<String>,
607    /// A label for the source account.
608    #[serde(default)]
609    pub source_label: Option<String>,
610    /// The recipient email.
611    pub target_email: String,
612    /// Transfer status.
613    pub status: String,
614    /// An optional note.
615    #[serde(default)]
616    pub note: Option<String>,
617    /// Cool-off deadline, if any.
618    #[serde(default)]
619    pub cooloff_until: Option<String>,
620    /// When the transfer was initiated.
621    pub initiated_at: String,
622    /// When the transfer was accepted, if at all.
623    #[serde(default)]
624    pub accepted_at: Option<String>,
625    /// When the transfer completed, if at all.
626    #[serde(default)]
627    pub completed_at: Option<String>,
628    /// When the transfer offer expires.
629    pub expires_at: String,
630}
631
632/// Result of a domain availability check.
633#[derive(Debug, Clone, Deserialize)]
634pub struct DomainAvailability {
635    /// Whether the domain can be added.
636    pub available: bool,
637    /// Why not, if unavailable.
638    pub reason: Option<String>,
639    /// Additional detail.
640    pub detail: Option<String>,
641    /// Count of stale verification tokens.
642    pub stale_tokens: Option<i64>,
643}
644
645/// Result of a domain existence check.
646#[derive(Debug, Clone, Deserialize)]
647pub struct DomainCheck {
648    /// Whether the domain exists in your account.
649    pub exists: bool,
650    /// Whether it is verified.
651    pub verified: bool,
652    /// Status, if present.
653    #[serde(default)]
654    pub status: Option<String>,
655    /// The queried domain name.
656    pub domain: String,
657    /// The domain id, if present.
658    #[serde(default)]
659    pub id: Option<String>,
660}
661
662// -- contacts ---------------------------------------------------------------
663
664/// A subscriber list.
665#[derive(Debug, Clone, Deserialize)]
666pub struct ContactList {
667    /// List id.
668    pub id: String,
669    /// List name.
670    pub name: String,
671    /// Optional description.
672    pub description: Option<String>,
673    /// Avatar seed.
674    pub icon_seed: Option<String>,
675    /// Number of contacts on the list.
676    pub contact_count: u64,
677    /// Creation time.
678    pub created_at: String,
679}
680
681/// A single contact in a list.
682#[derive(Debug, Clone, Deserialize)]
683pub struct Contact {
684    /// Contact id.
685    pub id: String,
686    /// Email address.
687    pub email: String,
688    /// Display name.
689    pub name: Option<String>,
690    /// Arbitrary custom fields.
691    #[serde(default)]
692    pub metadata: Option<Value>,
693    /// Creation time.
694    pub created_at: String,
695}
696
697/// A contact list with a page of its contacts.
698#[derive(Debug, Clone, Deserialize)]
699pub struct ContactListDetail {
700    /// The base list.
701    #[serde(flatten)]
702    pub list: ContactList,
703    /// A page of contacts.
704    pub contacts: Vec<Contact>,
705}
706
707/// Body for creating a contact list. Build with [`CreateList::new`].
708#[derive(Debug, Clone, Serialize, Default)]
709pub struct CreateList {
710    /// List name (required).
711    pub name: String,
712    /// Optional description.
713    #[serde(skip_serializing_if = "Option::is_none")]
714    pub description: Option<String>,
715    /// Avatar seed (serialized as `icon_seed`).
716    #[serde(rename = "icon_seed", skip_serializing_if = "Option::is_none")]
717    pub icon_seed: Option<String>,
718}
719
720impl CreateList {
721    /// Start a create-list body with a name.
722    pub fn new(name: impl Into<String>) -> Self {
723        Self {
724            name: name.into(),
725            description: None,
726            icon_seed: None,
727        }
728    }
729
730    /// Set the description.
731    pub fn description(mut self, description: impl Into<String>) -> Self {
732        self.description = Some(description.into());
733        self
734    }
735
736    /// Set the avatar seed.
737    pub fn icon_seed(mut self, icon_seed: impl Into<String>) -> Self {
738        self.icon_seed = Some(icon_seed.into());
739        self
740    }
741}
742
743/// Partial body for updating a contact list. Defaults to all-unset.
744#[derive(Debug, Clone, Serialize, Default)]
745pub struct UpdateList {
746    /// New name.
747    #[serde(skip_serializing_if = "Option::is_none")]
748    pub name: Option<String>,
749    /// New description.
750    #[serde(skip_serializing_if = "Option::is_none")]
751    pub description: Option<String>,
752    /// New avatar seed (serialized as `icon_seed`).
753    #[serde(rename = "icon_seed", skip_serializing_if = "Option::is_none")]
754    pub icon_seed: Option<String>,
755}
756
757impl UpdateList {
758    /// Set the name.
759    pub fn name(mut self, name: impl Into<String>) -> Self {
760        self.name = Some(name.into());
761        self
762    }
763
764    /// Set the description.
765    pub fn description(mut self, description: impl Into<String>) -> Self {
766        self.description = Some(description.into());
767        self
768    }
769
770    /// Set the avatar seed.
771    pub fn icon_seed(mut self, icon_seed: impl Into<String>) -> Self {
772        self.icon_seed = Some(icon_seed.into());
773        self
774    }
775}
776
777/// Body for adding a contact to a list.
778#[derive(Debug, Clone, Serialize)]
779pub struct AddContact {
780    /// Email address (required).
781    pub email: String,
782    /// Display name.
783    #[serde(skip_serializing_if = "Option::is_none")]
784    pub name: Option<String>,
785    /// Arbitrary custom fields.
786    #[serde(skip_serializing_if = "Option::is_none")]
787    pub metadata: Option<Value>,
788}
789
790impl AddContact {
791    /// Start an add-contact body with an email.
792    pub fn new(email: impl Into<String>) -> Self {
793        Self {
794            email: email.into(),
795            name: None,
796            metadata: None,
797        }
798    }
799
800    /// Set the display name.
801    pub fn name(mut self, name: impl Into<String>) -> Self {
802        self.name = Some(name.into());
803        self
804    }
805
806    /// Set custom fields.
807    pub fn metadata(mut self, metadata: Value) -> Self {
808        self.metadata = Some(metadata);
809        self
810    }
811}
812
813/// Result of a CSV contact import.
814#[derive(Debug, Clone, Deserialize)]
815pub struct CsvImportResult {
816    /// Number of contacts imported.
817    pub imported: u64,
818    /// Number of rows skipped.
819    pub skipped: u64,
820    /// Per-row error messages.
821    pub errors: Vec<String>,
822}
823
824/// Body for a templated bulk send. `contact_list_id` is injected by the SDK.
825#[derive(Debug, Clone, Serialize)]
826pub struct BulkSend {
827    /// The list id (set automatically by the SDK).
828    pub contact_list_id: String,
829    /// The verified sender address id.
830    pub sender_address_id: String,
831    /// Subject line (may use `{{placeholders}}`).
832    pub subject: String,
833    /// HTML body.
834    #[serde(skip_serializing_if = "Option::is_none")]
835    pub html: Option<String>,
836    /// Plain-text body.
837    #[serde(skip_serializing_if = "Option::is_none")]
838    pub text: Option<String>,
839    /// Tags.
840    #[serde(skip_serializing_if = "Option::is_none")]
841    pub tags: Option<Vec<String>>,
842}
843
844impl BulkSend {
845    /// Start a bulk-send body with a sender address id and subject. The list id
846    /// is filled in by [`crate::resources::Contacts::bulk_send`].
847    pub fn new(sender_address_id: impl Into<String>, subject: impl Into<String>) -> Self {
848        Self {
849            contact_list_id: String::new(),
850            sender_address_id: sender_address_id.into(),
851            subject: subject.into(),
852            html: None,
853            text: None,
854            tags: None,
855        }
856    }
857
858    /// Set the HTML body.
859    pub fn html(mut self, html: impl Into<String>) -> Self {
860        self.html = Some(html.into());
861        self
862    }
863
864    /// Set the plain-text body.
865    pub fn text(mut self, text: impl Into<String>) -> Self {
866        self.text = Some(text.into());
867        self
868    }
869
870    /// Set tags.
871    pub fn tags(mut self, tags: Vec<String>) -> Self {
872        self.tags = Some(tags);
873        self
874    }
875}
876
877/// Result of a bulk send.
878#[derive(Debug, Clone, Deserialize)]
879pub struct BulkSendResult {
880    /// Number of messages queued.
881    pub queued: u64,
882    /// Number of contacts skipped.
883    pub skipped: u64,
884    /// Per-contact error messages.
885    pub errors: Vec<String>,
886}
887
888// -- suppressions -----------------------------------------------------------
889
890/// A suppressed recipient address.
891#[derive(Debug, Clone, Deserialize)]
892pub struct Suppression {
893    /// Suppression id.
894    pub id: String,
895    /// The suppressed email (wire field `email_address`).
896    pub email_address: String,
897    /// Why the address was suppressed.
898    pub reason: String,
899    /// Creation time.
900    #[serde(default)]
901    pub created_at: Option<String>,
902}
903
904/// Body for adding a suppression. `email` maps to `email_address` on the wire.
905#[derive(Debug, Clone, Serialize)]
906pub struct AddSuppression {
907    /// The email to suppress (serialized as `email_address`).
908    #[serde(rename = "email_address")]
909    pub email: String,
910    /// The reason (defaults to `manual`).
911    pub reason: String,
912}
913
914impl AddSuppression {
915    /// Build a suppression with the default reason `manual`.
916    pub fn new(email: impl Into<String>) -> Self {
917        Self {
918            email: email.into(),
919            reason: "manual".to_string(),
920        }
921    }
922
923    /// Override the reason.
924    pub fn reason(mut self, reason: impl Into<String>) -> Self {
925        self.reason = reason.into();
926        self
927    }
928}
929
930/// Result of a bulk suppression upload.
931#[derive(Debug, Clone, Deserialize)]
932pub struct BulkSuppressionResult {
933    /// Number of addresses added.
934    pub added: u64,
935    /// Number skipped (already suppressed).
936    pub skipped: u64,
937    /// Total lines processed.
938    pub total_processed: u64,
939}
940
941// -- templates --------------------------------------------------------------
942
943/// A reusable email template. `variables` is server-derived and read-only.
944#[derive(Debug, Clone, Deserialize)]
945pub struct Template {
946    /// Template id.
947    pub id: String,
948    /// Template name.
949    pub name: String,
950    /// Subject line.
951    pub subject: Option<String>,
952    /// HTML body.
953    pub html_body: Option<String>,
954    /// Plain-text body.
955    pub text_body: Option<String>,
956    /// Variables derived from `{{placeholders}}` (read-only).
957    #[serde(default)]
958    pub variables: Option<Vec<String>>,
959    /// Editor block structure, if any.
960    #[serde(default)]
961    pub blocks_json: Option<Value>,
962    /// Creation time.
963    pub created_at: String,
964    /// Last update time.
965    pub updated_at: String,
966}
967
968/// Body for creating a template. `html`/`text` map to `html_body`/`text_body`.
969#[derive(Debug, Clone, Serialize)]
970pub struct CreateTemplate {
971    /// Template name (required).
972    pub name: String,
973    /// Subject line.
974    #[serde(skip_serializing_if = "Option::is_none")]
975    pub subject: Option<String>,
976    /// HTML body (serialized as `html_body`).
977    #[serde(rename = "html_body", skip_serializing_if = "Option::is_none")]
978    pub html: Option<String>,
979    /// Plain-text body (serialized as `text_body`).
980    #[serde(rename = "text_body", skip_serializing_if = "Option::is_none")]
981    pub text: Option<String>,
982    /// Editor block structure (serialized as `blocks_json`).
983    #[serde(rename = "blocks_json", skip_serializing_if = "Option::is_none")]
984    pub blocks_json: Option<Value>,
985}
986
987impl CreateTemplate {
988    /// Start a create-template body with a name.
989    pub fn new(name: impl Into<String>) -> Self {
990        Self {
991            name: name.into(),
992            subject: None,
993            html: None,
994            text: None,
995            blocks_json: None,
996        }
997    }
998
999    /// Set the subject.
1000    pub fn subject(mut self, subject: impl Into<String>) -> Self {
1001        self.subject = Some(subject.into());
1002        self
1003    }
1004
1005    /// Set the HTML body.
1006    pub fn html(mut self, html: impl Into<String>) -> Self {
1007        self.html = Some(html.into());
1008        self
1009    }
1010
1011    /// Set the plain-text body.
1012    pub fn text(mut self, text: impl Into<String>) -> Self {
1013        self.text = Some(text.into());
1014        self
1015    }
1016
1017    /// Set the editor block structure.
1018    pub fn blocks_json(mut self, blocks_json: Value) -> Self {
1019        self.blocks_json = Some(blocks_json);
1020        self
1021    }
1022}
1023
1024/// Partial body for updating a template. Defaults to all-unset.
1025#[derive(Debug, Clone, Serialize, Default)]
1026pub struct UpdateTemplate {
1027    /// New name.
1028    #[serde(skip_serializing_if = "Option::is_none")]
1029    pub name: Option<String>,
1030    /// New subject.
1031    #[serde(skip_serializing_if = "Option::is_none")]
1032    pub subject: Option<String>,
1033    /// New HTML body (serialized as `html_body`).
1034    #[serde(rename = "html_body", skip_serializing_if = "Option::is_none")]
1035    pub html: Option<String>,
1036    /// New plain-text body (serialized as `text_body`).
1037    #[serde(rename = "text_body", skip_serializing_if = "Option::is_none")]
1038    pub text: Option<String>,
1039    /// New editor block structure (serialized as `blocks_json`).
1040    #[serde(rename = "blocks_json", skip_serializing_if = "Option::is_none")]
1041    pub blocks_json: Option<Value>,
1042}
1043
1044impl UpdateTemplate {
1045    /// Set the name.
1046    pub fn name(mut self, name: impl Into<String>) -> Self {
1047        self.name = Some(name.into());
1048        self
1049    }
1050
1051    /// Set the subject.
1052    pub fn subject(mut self, subject: impl Into<String>) -> Self {
1053        self.subject = Some(subject.into());
1054        self
1055    }
1056
1057    /// Set the HTML body.
1058    pub fn html(mut self, html: impl Into<String>) -> Self {
1059        self.html = Some(html.into());
1060        self
1061    }
1062
1063    /// Set the plain-text body.
1064    pub fn text(mut self, text: impl Into<String>) -> Self {
1065        self.text = Some(text.into());
1066        self
1067    }
1068
1069    /// Set the editor block structure.
1070    pub fn blocks_json(mut self, blocks_json: Value) -> Self {
1071        self.blocks_json = Some(blocks_json);
1072        self
1073    }
1074}
1075
1076// -- webhooks ---------------------------------------------------------------
1077
1078/// A configured webhook endpoint. `secret` is returned in plaintext.
1079#[derive(Debug, Clone, Deserialize)]
1080pub struct Webhook {
1081    /// Webhook id.
1082    pub id: String,
1083    /// Destination URL.
1084    pub url: String,
1085    /// Subscribed event names.
1086    pub events: Vec<String>,
1087    /// HMAC signing secret (plaintext on every read).
1088    pub secret: String,
1089    /// Whether the webhook is active.
1090    pub is_active: bool,
1091    /// Creation time.
1092    pub created_at: String,
1093}
1094
1095/// Body for creating a webhook.
1096#[derive(Debug, Clone, Serialize)]
1097pub struct CreateWebhook {
1098    /// Destination URL (required).
1099    pub url: String,
1100    /// Event names to subscribe to (required).
1101    pub events: Vec<String>,
1102}
1103
1104impl CreateWebhook {
1105    /// Build a create-webhook body.
1106    pub fn new(url: impl Into<String>, events: Vec<String>) -> Self {
1107        Self {
1108            url: url.into(),
1109            events,
1110        }
1111    }
1112}
1113
1114/// Partial body for updating a webhook. `is_active` honours the wire name.
1115#[derive(Debug, Clone, Serialize, Default)]
1116pub struct UpdateWebhook {
1117    /// New URL.
1118    #[serde(skip_serializing_if = "Option::is_none")]
1119    pub url: Option<String>,
1120    /// New event list.
1121    #[serde(skip_serializing_if = "Option::is_none")]
1122    pub events: Option<Vec<String>>,
1123    /// New active state (serialized as `is_active`).
1124    #[serde(rename = "is_active", skip_serializing_if = "Option::is_none")]
1125    pub is_active: Option<bool>,
1126}
1127
1128impl UpdateWebhook {
1129    /// Set the URL.
1130    pub fn url(mut self, url: impl Into<String>) -> Self {
1131        self.url = Some(url.into());
1132        self
1133    }
1134
1135    /// Set the event list.
1136    pub fn events(mut self, events: Vec<String>) -> Self {
1137        self.events = Some(events);
1138        self
1139    }
1140
1141    /// Set the active state.
1142    pub fn is_active(mut self, is_active: bool) -> Self {
1143        self.is_active = Some(is_active);
1144        self
1145    }
1146}
1147
1148/// Result of testing a webhook endpoint.
1149#[derive(Debug, Clone, Deserialize)]
1150pub struct WebhookTestResult {
1151    /// Whether a test delivery was queued.
1152    pub queued: bool,
1153    /// The destination URL.
1154    pub url: String,
1155}
1156
1157/// A summary of one webhook delivery attempt.
1158#[derive(Debug, Clone, Deserialize)]
1159pub struct WebhookDelivery {
1160    /// Delivery id.
1161    pub id: String,
1162    /// The webhook it belongs to.
1163    pub webhook_id: String,
1164    /// The event type, if known.
1165    pub event_type: Option<String>,
1166    /// Delivery status.
1167    pub status: String,
1168    /// HTTP status the endpoint returned, if any.
1169    pub response_status: Option<i64>,
1170    /// Attempt number.
1171    pub attempt: i64,
1172    /// When the next retry is scheduled, if any.
1173    pub next_retry_at: Option<String>,
1174    /// Creation time.
1175    pub created_at: Option<String>,
1176}
1177
1178/// A webhook delivery with the full payload and endpoint response.
1179#[derive(Debug, Clone, Deserialize)]
1180pub struct WebhookDeliveryDetail {
1181    /// The base delivery.
1182    #[serde(flatten)]
1183    pub delivery: WebhookDelivery,
1184    /// The delivered payload.
1185    pub payload: Value,
1186    /// The endpoint's response body, if any.
1187    pub response_body: Option<String>,
1188    /// The endpoint URL.
1189    pub endpoint_url: String,
1190}
1191
1192// -- domain transfer params -------------------------------------------------
1193
1194/// Body for initiating a domain transfer.
1195#[derive(Debug, Clone, Serialize)]
1196pub struct TransferDomain {
1197    /// The recipient email (required).
1198    pub target_email: String,
1199    /// An optional note (max 1000 chars).
1200    #[serde(skip_serializing_if = "Option::is_none")]
1201    pub note: Option<String>,
1202}
1203
1204impl TransferDomain {
1205    /// Build a transfer body with a target email.
1206    pub fn new(target_email: impl Into<String>) -> Self {
1207        Self {
1208            target_email: target_email.into(),
1209            note: None,
1210        }
1211    }
1212
1213    /// Attach a note.
1214    pub fn note(mut self, note: impl Into<String>) -> Self {
1215        self.note = Some(note.into());
1216        self
1217    }
1218}