azure-functions 0.11.0

Azure Functions for Rust
Documentation
use crate::{
    rpc::{typed_data::Data, TypedData},
    send_grid::{
        Attachment, Content, EmailAddress, MailSettings, MessageBuilder, Personalization,
        TrackingSettings, UnsubscribeGroup,
    },
    FromVec,
};
use serde::{Deserialize, Serialize};
use serde_json::{to_string, to_value, Value};
use std::collections::HashMap;

/// Represents the SendGrid email message output binding.
///
/// The following binding attributes are supported:
///
/// | Name      | Description                                                                                                                                        |
/// |-----------|----------------------------------------------------------------------------------------------------------------------------------------------------|
/// | `api_key` | The name of an app setting that contains your API key. If not set, the default app setting name is "AzureWebJobsSendGridApiKey".
/// | `to`      | The default recipient's email address.
/// | `from`    | The default sender's email address.
/// | `subject` | The default subject of the email.
/// | `text`    | The default email text content.
///
/// # Examples
/// ```rust
/// use azure_functions::{
///     bindings::{HttpRequest, HttpResponse, SendGridMessage},
///     func,
/// };
///
/// #[func]
/// #[binding(name = "output1", from = "azure.functions.for.rust@example.com")]
/// pub fn send_email(req: HttpRequest) -> (HttpResponse, SendGridMessage) {
///     let params = req.query_params();
///
///     (
///         "The email was sent.".into(),
///         SendGridMessage::build()
///             .to(params.get("to").unwrap().as_str())
///             .subject(params.get("subject").unwrap().as_str())
///             .content(params.get("content").unwrap().as_str())
///             .finish(),
///     )
/// }
/// ```
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct SendGridMessage {
    /// The email address of the sender. If None, the `from` binding attribute is used.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub from: Option<EmailAddress>,
    /// The subject of the email message. If None, the `subject` binding attribute is used.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub subject: Option<String>,
    /// The list of personalized messages and their metadata.
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub personalizations: Vec<Personalization>,
    /// The list of email content.
    #[serde(rename = "content", skip_serializing_if = "Vec::is_empty")]
    pub contents: Vec<Content>,
    /// The list of email attachments.
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub attachments: Vec<Attachment>,
    /// The id of the SendGrid template to use.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub template_id: Option<String>,
    /// The map of key-value pairs of header names and the value to substitute for them.
    #[serde(skip_serializing_if = "HashMap::is_empty")]
    pub headers: HashMap<String, String>,
    /// The map of key-value pairs that define large blocks of content that can be inserted into your emails using substitution tags.
    #[serde(skip_serializing_if = "HashMap::is_empty")]
    pub sections: HashMap<String, String>,
    /// The list of category names for this message.
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub categories: Vec<String>,
    /// 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.
    #[serde(skip_serializing_if = "HashMap::is_empty")]
    pub custom_args: HashMap<String, String>,
    /// The unix timestamp that specifies when the email should be sent from SendGrid.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub send_at: Option<i64>,
    /// The associated unsubscribe group that specifies how to handle unsubscribes.
    #[serde(rename = "asm", skip_serializing_if = "Option::is_none")]
    pub unsubscribe_group: Option<UnsubscribeGroup>,
    /// The id that represents a batch of emails to be associated to each other for scheduling.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub batch_id: Option<String>,
    /// The IP pool that the message should be sent from.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ip_pool_name: Option<String>,
    /// The settings that specify how the email message should be handled.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub mail_settings: Option<MailSettings>,
    /// The settings that specify how the email message should be tracked.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tracking_settings: Option<TrackingSettings>,
    /// The email address and name of the individual who should receive responses to the email message.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub reply_to: Option<EmailAddress>,
}

impl SendGridMessage {
    /// Creates a new [MessageBuilder](../send_grid/struct.MessageBuilder.html) for building a SendGrid message.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use azure_functions::bindings::SendGridMessage;
    ///
    /// let message = SendGridMessage::build()
    ///     .to("foo@example.com")
    ///     .subject("The subject of the message")
    ///     .content("I hope this message finds you well.")
    ///     .finish();
    ///
    /// assert_eq!(message.personalizations[0].to[0].email, "foo@example.com");
    /// assert_eq!(message.personalizations[0].subject, Some("The subject of the message".to_owned()));
    /// assert_eq!(message.contents[0].value, "I hope this message finds you well.");
    /// ```
    pub fn build() -> MessageBuilder {
        MessageBuilder::new()
    }
}

#[doc(hidden)]
impl Into<TypedData> for SendGridMessage {
    fn into(self) -> TypedData {
        TypedData {
            data: Some(Data::Json(
                to_string(&self).expect("failed to convert SendGrid message to JSON string"),
            )),
        }
    }
}

#[doc(hidden)]
impl FromVec<SendGridMessage> for TypedData {
    fn from_vec(vec: Vec<SendGridMessage>) -> Self {
        TypedData {
            data: Some(Data::Json(
                Value::Array(vec.into_iter().map(|m| to_value(m).unwrap()).collect()).to_string(),
            )),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::send_grid::{
        BccSettings, BypassListManagement, ClickTracking, FooterSettings, GoogleAnalytics,
        OpenTracking, SandboxMode, SpamCheck, SubscriptionTracking,
    };
    use serde_json::Map;

    #[test]
    fn it_serializes_to_json() {
        let mut headers = HashMap::new();
        headers.insert("foo".to_owned(), "bar".to_owned());

        let mut sections = HashMap::new();
        sections.insert("foo".to_owned(), "bar".to_owned());

        let mut substitutions = HashMap::new();
        substitutions.insert("key".to_owned(), "value".to_owned());

        let mut custom_args = HashMap::new();
        custom_args.insert("baz".to_owned(), "jam".to_owned());

        let mut template_data = Map::<String, Value>::new();
        template_data.insert("hello".to_owned(), Value::String("world".to_owned()));

        let json = to_string(&create_send_grid_message()).unwrap();

        assert_eq!(json, expected_message_json());
    }

    #[test]
    fn it_converts_to_typed_data() {
        let message = create_send_grid_message();

        let data: TypedData = message.into();
        assert_eq!(
            data.data,
            Some(Data::Json(expected_message_json().to_owned()))
        );
    }

    fn create_send_grid_message() -> SendGridMessage {
        let mut headers = HashMap::new();
        headers.insert("foo".to_owned(), "bar".to_owned());

        let mut sections = HashMap::new();
        sections.insert("foo".to_owned(), "bar".to_owned());

        let mut substitutions = HashMap::new();
        substitutions.insert("key".to_owned(), "value".to_owned());

        let mut custom_args = HashMap::new();
        custom_args.insert("baz".to_owned(), "jam".to_owned());

        let mut template_data = Map::<String, Value>::new();
        template_data.insert("hello".to_owned(), Value::String("world".to_owned()));

        SendGridMessage {
            from: Some(EmailAddress {
                email: "foo@example.com".to_owned(),
                name: Some("foo".to_owned()),
            }),
            subject: Some("hello world".to_owned()),
            personalizations: vec![Personalization {
                to: vec![
                    EmailAddress {
                        email: "foo@example.com".to_owned(),
                        ..Default::default()
                    },
                    EmailAddress {
                        email: "bar@example.com".to_owned(),
                        name: Some("Bar Baz".to_owned()),
                    },
                ],
                cc: vec![
                    EmailAddress {
                        email: "baz@example.com".to_owned(),
                        ..Default::default()
                    },
                    EmailAddress {
                        email: "jam@example.com".to_owned(),
                        name: Some("Jam".to_owned()),
                    },
                ],
                bcc: vec![
                    EmailAddress {
                        email: "cake@example.com".to_owned(),
                        ..Default::default()
                    },
                    EmailAddress {
                        email: "lie@example.com".to_owned(),
                        name: Some("Lie".to_owned()),
                    },
                ],
                subject: Some("hello world".to_owned()),
                headers: headers.clone(),
                substitutions,
                custom_args: custom_args.clone(),
                send_at: Some(12345),
                template_data: Some(template_data),
            }],
            contents: vec![Content {
                mime_type: "text/plain".to_owned(),
                value: "hello world".to_owned(),
            }],
            attachments: vec![Attachment {
                content: "aGVsbG8gd29ybGQ=".to_owned(),
                mime_type: "text/plain".to_owned(),
                filename: "foo.txt".to_owned(),
                disposition: None,
                content_id: None,
            }],
            template_id: Some("template".to_owned()),
            headers,
            sections,
            categories: vec!["first".to_owned(), "second".to_owned()],
            custom_args,
            send_at: Some(12345),
            unsubscribe_group: Some(UnsubscribeGroup {
                group_id: 12345,
                groups_to_display: Vec::new(),
            }),
            batch_id: Some("batch".to_owned()),
            ip_pool_name: Some("pool".to_owned()),
            mail_settings: Some(MailSettings {
                bcc: Some(BccSettings {
                    enable: true,
                    email: "foo@example.com".to_owned(),
                }),
                bypass_list_management: Some(BypassListManagement { enable: true }),
                footer: Some(FooterSettings {
                    enable: true,
                    text: "hello".to_owned(),
                    html: "world".to_owned(),
                }),
                sandbox_mode: Some(SandboxMode { enable: true }),
                spam_check: Some(SpamCheck {
                    enable: true,
                    threshold: 7,
                    post_to_url: "https://example.com".to_owned(),
                }),
            }),
            tracking_settings: Some(TrackingSettings {
                click_tracking: Some(ClickTracking {
                    enable: true,
                    enable_text: false,
                }),
                open_tracking: Some(OpenTracking {
                    enable: true,
                    substitution_tag: Some("foo".to_owned()),
                }),
                subscription_tracking: Some(SubscriptionTracking {
                    enable: true,
                    text: "foo".to_owned(),
                    html: "bar".to_owned(),
                    substitution_tag: Some("baz".to_owned()),
                }),
                google_analytics: Some(GoogleAnalytics {
                    enable: true,
                    source: "foo".to_owned(),
                    medium: "bar".to_owned(),
                    term: "baz".to_owned(),
                    content: "jam".to_owned(),
                    campaign: "cake".to_owned(),
                }),
            }),
            reply_to: Some(EmailAddress {
                email: "bar@example.com".to_owned(),
                name: Some("bar".to_owned()),
            }),
        }
    }

    fn expected_message_json() -> &'static str {
        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"}}"#
    }
}