azure_functions/bindings/
send_grid_message.rs

1use crate::{
2    rpc::{typed_data::Data, TypedData},
3    send_grid::{
4        Attachment, Content, EmailAddress, MailSettings, MessageBuilder, Personalization,
5        TrackingSettings, UnsubscribeGroup,
6    },
7    FromVec,
8};
9use serde::{Deserialize, Serialize};
10use serde_json::{to_string, to_value, Value};
11use std::collections::HashMap;
12
13/// Represents the SendGrid email message output binding.
14///
15/// The following binding attributes are supported:
16///
17/// | Name      | Description                                                                                                                                        |
18/// |-----------|----------------------------------------------------------------------------------------------------------------------------------------------------|
19/// | `api_key` | The name of an app setting that contains your API key. If not set, the default app setting name is "AzureWebJobsSendGridApiKey".
20/// | `to`      | The default recipient's email address.
21/// | `from`    | The default sender's email address.
22/// | `subject` | The default subject of the email.
23/// | `text`    | The default email text content.
24///
25/// # Examples
26/// ```rust
27/// use azure_functions::{
28///     bindings::{HttpRequest, HttpResponse, SendGridMessage},
29///     func,
30/// };
31///
32/// #[func]
33/// #[binding(name = "output1", from = "azure.functions.for.rust@example.com")]
34/// pub fn send_email(req: HttpRequest) -> (HttpResponse, SendGridMessage) {
35///     let params = req.query_params();
36///
37///     (
38///         "The email was sent.".into(),
39///         SendGridMessage::build()
40///             .to(params.get("to").unwrap().as_str())
41///             .subject(params.get("subject").unwrap().as_str())
42///             .content(params.get("content").unwrap().as_str())
43///             .finish(),
44///     )
45/// }
46/// ```
47#[derive(Debug, Default, Clone, Serialize, Deserialize)]
48pub struct SendGridMessage {
49    /// The email address of the sender. If None, the `from` binding attribute is used.
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub from: Option<EmailAddress>,
52    /// The subject of the email message. If None, the `subject` binding attribute is used.
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub subject: Option<String>,
55    /// The list of personalized messages and their metadata.
56    #[serde(skip_serializing_if = "Vec::is_empty")]
57    pub personalizations: Vec<Personalization>,
58    /// The list of email content.
59    #[serde(rename = "content", skip_serializing_if = "Vec::is_empty")]
60    pub contents: Vec<Content>,
61    /// The list of email attachments.
62    #[serde(skip_serializing_if = "Vec::is_empty")]
63    pub attachments: Vec<Attachment>,
64    /// The id of the SendGrid template to use.
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub template_id: Option<String>,
67    /// The map of key-value pairs of header names and the value to substitute for them.
68    #[serde(skip_serializing_if = "HashMap::is_empty")]
69    pub headers: HashMap<String, String>,
70    /// The map of key-value pairs that define large blocks of content that can be inserted into your emails using substitution tags.
71    #[serde(skip_serializing_if = "HashMap::is_empty")]
72    pub sections: HashMap<String, String>,
73    /// The list of category names for this message.
74    #[serde(skip_serializing_if = "Vec::is_empty")]
75    pub categories: Vec<String>,
76    /// The map of key-value pairs that are specific to the entire send that will be carried along with the email and its activity data.
77    #[serde(skip_serializing_if = "HashMap::is_empty")]
78    pub custom_args: HashMap<String, String>,
79    /// The unix timestamp that specifies when the email should be sent from SendGrid.
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub send_at: Option<i64>,
82    /// The associated unsubscribe group that specifies how to handle unsubscribes.
83    #[serde(rename = "asm", skip_serializing_if = "Option::is_none")]
84    pub unsubscribe_group: Option<UnsubscribeGroup>,
85    /// The id that represents a batch of emails to be associated to each other for scheduling.
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub batch_id: Option<String>,
88    /// The IP pool that the message should be sent from.
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub ip_pool_name: Option<String>,
91    /// The settings that specify how the email message should be handled.
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub mail_settings: Option<MailSettings>,
94    /// The settings that specify how the email message should be tracked.
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub tracking_settings: Option<TrackingSettings>,
97    /// The email address and name of the individual who should receive responses to the email message.
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub reply_to: Option<EmailAddress>,
100}
101
102impl SendGridMessage {
103    /// Creates a new [MessageBuilder](../send_grid/struct.MessageBuilder.html) for building a SendGrid message.
104    ///
105    /// # Examples
106    ///
107    /// ```rust
108    /// use azure_functions::bindings::SendGridMessage;
109    ///
110    /// let message = SendGridMessage::build()
111    ///     .to("foo@example.com")
112    ///     .subject("The subject of the message")
113    ///     .content("I hope this message finds you well.")
114    ///     .finish();
115    ///
116    /// assert_eq!(message.personalizations[0].to[0].email, "foo@example.com");
117    /// assert_eq!(message.personalizations[0].subject, Some("The subject of the message".to_owned()));
118    /// assert_eq!(message.contents[0].value, "I hope this message finds you well.");
119    /// ```
120    pub fn build() -> MessageBuilder {
121        MessageBuilder::new()
122    }
123}
124
125#[doc(hidden)]
126impl Into<TypedData> for SendGridMessage {
127    fn into(self) -> TypedData {
128        TypedData {
129            data: Some(Data::Json(
130                to_string(&self).expect("failed to convert SendGrid message to JSON string"),
131            )),
132        }
133    }
134}
135
136#[doc(hidden)]
137impl FromVec<SendGridMessage> for TypedData {
138    fn from_vec(vec: Vec<SendGridMessage>) -> Self {
139        TypedData {
140            data: Some(Data::Json(
141                Value::Array(vec.into_iter().map(|m| to_value(m).unwrap()).collect()).to_string(),
142            )),
143        }
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150    use crate::send_grid::{
151        BccSettings, BypassListManagement, ClickTracking, FooterSettings, GoogleAnalytics,
152        OpenTracking, SandboxMode, SpamCheck, SubscriptionTracking,
153    };
154    use serde_json::Map;
155
156    #[test]
157    fn it_serializes_to_json() {
158        let mut headers = HashMap::new();
159        headers.insert("foo".to_owned(), "bar".to_owned());
160
161        let mut sections = HashMap::new();
162        sections.insert("foo".to_owned(), "bar".to_owned());
163
164        let mut substitutions = HashMap::new();
165        substitutions.insert("key".to_owned(), "value".to_owned());
166
167        let mut custom_args = HashMap::new();
168        custom_args.insert("baz".to_owned(), "jam".to_owned());
169
170        let mut template_data = Map::<String, Value>::new();
171        template_data.insert("hello".to_owned(), Value::String("world".to_owned()));
172
173        let json = to_string(&create_send_grid_message()).unwrap();
174
175        assert_eq!(json, expected_message_json());
176    }
177
178    #[test]
179    fn it_converts_to_typed_data() {
180        let message = create_send_grid_message();
181
182        let data: TypedData = message.into();
183        assert_eq!(
184            data.data,
185            Some(Data::Json(expected_message_json().to_owned()))
186        );
187    }
188
189    fn create_send_grid_message() -> SendGridMessage {
190        let mut headers = HashMap::new();
191        headers.insert("foo".to_owned(), "bar".to_owned());
192
193        let mut sections = HashMap::new();
194        sections.insert("foo".to_owned(), "bar".to_owned());
195
196        let mut substitutions = HashMap::new();
197        substitutions.insert("key".to_owned(), "value".to_owned());
198
199        let mut custom_args = HashMap::new();
200        custom_args.insert("baz".to_owned(), "jam".to_owned());
201
202        let mut template_data = Map::<String, Value>::new();
203        template_data.insert("hello".to_owned(), Value::String("world".to_owned()));
204
205        SendGridMessage {
206            from: Some(EmailAddress {
207                email: "foo@example.com".to_owned(),
208                name: Some("foo".to_owned()),
209            }),
210            subject: Some("hello world".to_owned()),
211            personalizations: vec![Personalization {
212                to: vec![
213                    EmailAddress {
214                        email: "foo@example.com".to_owned(),
215                        ..Default::default()
216                    },
217                    EmailAddress {
218                        email: "bar@example.com".to_owned(),
219                        name: Some("Bar Baz".to_owned()),
220                    },
221                ],
222                cc: vec![
223                    EmailAddress {
224                        email: "baz@example.com".to_owned(),
225                        ..Default::default()
226                    },
227                    EmailAddress {
228                        email: "jam@example.com".to_owned(),
229                        name: Some("Jam".to_owned()),
230                    },
231                ],
232                bcc: vec![
233                    EmailAddress {
234                        email: "cake@example.com".to_owned(),
235                        ..Default::default()
236                    },
237                    EmailAddress {
238                        email: "lie@example.com".to_owned(),
239                        name: Some("Lie".to_owned()),
240                    },
241                ],
242                subject: Some("hello world".to_owned()),
243                headers: headers.clone(),
244                substitutions,
245                custom_args: custom_args.clone(),
246                send_at: Some(12345),
247                template_data: Some(template_data),
248            }],
249            contents: vec![Content {
250                mime_type: "text/plain".to_owned(),
251                value: "hello world".to_owned(),
252            }],
253            attachments: vec![Attachment {
254                content: "aGVsbG8gd29ybGQ=".to_owned(),
255                mime_type: "text/plain".to_owned(),
256                filename: "foo.txt".to_owned(),
257                disposition: None,
258                content_id: None,
259            }],
260            template_id: Some("template".to_owned()),
261            headers,
262            sections,
263            categories: vec!["first".to_owned(), "second".to_owned()],
264            custom_args,
265            send_at: Some(12345),
266            unsubscribe_group: Some(UnsubscribeGroup {
267                group_id: 12345,
268                groups_to_display: Vec::new(),
269            }),
270            batch_id: Some("batch".to_owned()),
271            ip_pool_name: Some("pool".to_owned()),
272            mail_settings: Some(MailSettings {
273                bcc: Some(BccSettings {
274                    enable: true,
275                    email: "foo@example.com".to_owned(),
276                }),
277                bypass_list_management: Some(BypassListManagement { enable: true }),
278                footer: Some(FooterSettings {
279                    enable: true,
280                    text: "hello".to_owned(),
281                    html: "world".to_owned(),
282                }),
283                sandbox_mode: Some(SandboxMode { enable: true }),
284                spam_check: Some(SpamCheck {
285                    enable: true,
286                    threshold: 7,
287                    post_to_url: "https://example.com".to_owned(),
288                }),
289            }),
290            tracking_settings: Some(TrackingSettings {
291                click_tracking: Some(ClickTracking {
292                    enable: true,
293                    enable_text: false,
294                }),
295                open_tracking: Some(OpenTracking {
296                    enable: true,
297                    substitution_tag: Some("foo".to_owned()),
298                }),
299                subscription_tracking: Some(SubscriptionTracking {
300                    enable: true,
301                    text: "foo".to_owned(),
302                    html: "bar".to_owned(),
303                    substitution_tag: Some("baz".to_owned()),
304                }),
305                google_analytics: Some(GoogleAnalytics {
306                    enable: true,
307                    source: "foo".to_owned(),
308                    medium: "bar".to_owned(),
309                    term: "baz".to_owned(),
310                    content: "jam".to_owned(),
311                    campaign: "cake".to_owned(),
312                }),
313            }),
314            reply_to: Some(EmailAddress {
315                email: "bar@example.com".to_owned(),
316                name: Some("bar".to_owned()),
317            }),
318        }
319    }
320
321    fn expected_message_json() -> &'static str {
322        r#"{"from":{"email":"foo@example.com","name":"foo"},"subject":"hello world","personalizations":[{"to":[{"email":"foo@example.com"},{"email":"bar@example.com","name":"Bar Baz"}],"cc":[{"email":"baz@example.com"},{"email":"jam@example.com","name":"Jam"}],"bcc":[{"email":"cake@example.com"},{"email":"lie@example.com","name":"Lie"}],"subject":"hello world","headers":{"foo":"bar"},"substitutions":{"key":"value"},"custom_args":{"baz":"jam"},"send_at":12345,"dynamic_template_data":{"hello":"world"}}],"content":[{"type":"text/plain","value":"hello world"}],"attachments":[{"content":"aGVsbG8gd29ybGQ=","type":"text/plain","filename":"foo.txt"}],"template_id":"template","headers":{"foo":"bar"},"sections":{"foo":"bar"},"categories":["first","second"],"custom_args":{"baz":"jam"},"send_at":12345,"asm":{"group_id":12345},"batch_id":"batch","ip_pool_name":"pool","mail_settings":{"bcc":{"enable":true,"email":"foo@example.com"},"bypass_list_management":{"enable":true},"footer":{"enable":true,"text":"hello","html":"world"},"sandbox_mode":{"enable":true},"spam_check":{"enable":true,"threshold":7,"post_to_url":"https://example.com"}},"tracking_settings":{"click_tracking":{"enable":true,"enable_text":false},"open_tracking":{"enable":true,"substitution_tag":"foo"},"subscription_tracking":{"enable":true,"text":"foo","html":"bar","substitution_tag":"baz"},"ganalytics":{"enable":true,"utm_source":"foo","utm_medium":"bar","utm_term":"baz","utm_content":"jam","utm_campaign":"cake"}},"reply_to":{"email":"bar@example.com","name":"bar"}}"#
323    }
324}