diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..89991a1 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,24 @@ +--- +dist: trusty + +language: rust + +rust: + - stable + - beta + - nightly + +cache: cargo + +matrix: + allow_failures: + - rust: nightly + fast_finish: true + +install: cargo build --release --verbose +script: cargo test --verbose + +notifications: + email: + on_success: never + on_failure: never diff --git a/Cargo.toml b/Cargo.toml index 6a64be0..51603c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,10 @@ [package] name = "crowbar" version = "0.2.0" -authors = ["Iliana Weller "] +authors = [ + "Iliana Weller ", + "Naftuli Kay " +] description = "Wrapper to simplify writing AWS Lambda functions in Rust (using the Python execution environment)" readme = "README.md" repository = "https://github.com/ilianaw/rust-crowbar" @@ -11,12 +14,15 @@ license = "MIT/Apache-2.0" exclude = [".gitignore", "builder/**", "examples/**", "test/**"] [dependencies] +chrono = { version = "0.4", features = ["serde"] } serde = "1.0" +serde-aux = "0.5" +serde_derive = "1.0" serde_json = "1.0" +serde_qs = "0.4" cpython = { version = "0.1", default-features = false } cpython-json = { version = "0.2", default-features = false } error-chain = { version = "0.11.0", optional = true } [features] default = ["cpython/python3-sys"] - diff --git a/README.md b/README.md index 5e8493f..8de3b8b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # rust-crowbar +[![Build Status][travis.svg]][travis] [![crates.io](https://img.shields.io/crates/v/crowbar.svg)](https://crates.io/crates/crowbar) [![docs.rs](https://docs.rs/crowbar/badge.svg)](https://docs.rs/crowbar) @@ -62,3 +63,6 @@ crowbar welcomes your contributions: * Please submit non-trivial changes as an issue first; send a pull request when the implementation is agreed on crowbar follows a [code of conduct](https://github.com/ilianaw/rust-crowbar/blob/master/CODE_OF_CONDUCT.md); please read it. + + [travis]: https://travis-ci.org/ilianaw/rust-crowbar + [travis.svg]: https://travis-ci.org/ilianaw/rust-crowbar.svg?branch=master diff --git a/src/data/apicall/fixtures/default.json b/src/data/apicall/fixtures/default.json new file mode 100644 index 0000000..f564608 --- /dev/null +++ b/src/data/apicall/fixtures/default.json @@ -0,0 +1,38 @@ +{ + "version": "0", + "id": "36eb8523-97d0-4518-b33d-ee3579ff19f0", + "detail-type": "AWS API Call via CloudTrail", + "source": "aws.s3", + "account": "123456789012", + "time": "2016-02-20T01:09:13Z", + "region": "us-east-1", + "resources": [], + "detail": { + "eventVersion": "1.03", + "userIdentity": { + "type": "Root", + "principalId": "123456789012", + "arn": "arn:aws:iam::123456789012:root", + "accountId": "123456789012", + "sessionContext": { + "attributes": { + "mfaAuthenticated": "false", + "creationDate": "2016-02-20T01:05:59Z" + } + } + }, + "eventTime": "2016-02-20T01:09:13Z", + "eventSource": "s3.amazonaws.com", + "eventName": "CreateBucket", + "awsRegion": "us-east-1", + "sourceIPAddress": "100.100.100.100", + "userAgent": "[S3Console/0.4]", + "requestParameters": { + "bucketName": "bucket-test-iad" + }, + "responseElements": null, + "requestID": "9D767BCC3B4E7487", + "eventID": "24ba271e-d595-4e66-a7fd-9c16cbf8abae", + "eventType": "AwsApiCall" + } +} diff --git a/src/data/apicall/mod.rs b/src/data/apicall/mod.rs new file mode 100644 index 0000000..8b3f8a6 --- /dev/null +++ b/src/data/apicall/mod.rs @@ -0,0 +1,59 @@ +#[cfg(test)] +mod tests; + +use super::*; + +#[derive(Serialize,Deserialize)] +pub struct APICall { + pub version: String, + pub id: String, + pub account: String, + pub time: DateTime, + pub region: String, + pub resources: Vec, + pub detail: APICallDetail, +} + +#[derive(Serialize,Deserialize)] +#[serde(rename_all="camelCase")] +pub struct APICallDetail { + pub event_version: String, + pub user_identity: UserIdentity, + pub event_time: DateTime, + pub event_source: String, + pub event_name: String, + pub aws_region: String, + #[serde(rename="sourceIPAddress")] + pub source_ip_address: String, + pub user_agent: String, + pub request_parameters: Option>, + pub response_elements: Option, + #[serde(rename="requestID")] + pub request_id: String, + #[serde(rename="eventID")] + pub event_id: String, + pub event_type: String, +} + +#[derive(Serialize,Deserialize)] +#[serde(rename_all="camelCase")] +pub struct UserIdentity { + #[serde(rename="type")] + pub user_type: String, + pub principal_id: String, + pub arn: String, + pub account_id: String, + pub session_context: SessionContext, +} + +#[derive(Serialize,Deserialize)] +pub struct SessionContext { + pub attributes: SessionContextAttributes, +} + +#[derive(Serialize,Deserialize)] +#[serde(rename_all="camelCase")] +pub struct SessionContextAttributes { + pub mfa_authenticated: String, + pub creation_date: DateTime, +} diff --git a/src/data/apicall/tests.rs b/src/data/apicall/tests.rs new file mode 100644 index 0000000..d2e9dc3 --- /dev/null +++ b/src/data/apicall/tests.rs @@ -0,0 +1,8 @@ +use super::*; + +use serde_json; + +#[test] +fn test_deserialize() { + let _call: APICall = serde_json::from_str(include_str!("fixtures/default.json")).unwrap(); +} diff --git a/src/data/apigateway/auth.rs b/src/data/apigateway/auth.rs new file mode 100644 index 0000000..49f8d91 --- /dev/null +++ b/src/data/apigateway/auth.rs @@ -0,0 +1,43 @@ +/// API Gateway Custom Authenticator Events +use data::apigateway::HttpEventRequestContext; + +use std::collections::BTreeMap; +use std::fmt; + +#[serde(rename_all="SCREAMING_SNAKE_CASE")] +#[derive(Debug,Eq,PartialEq,Serialize,Deserialize)] +pub enum EventType { + Request, + Token +} + +#[serde(rename_all="camelCase")] +#[derive(Serialize,Deserialize)] +pub struct Event { + pub headers: Option>, + pub http_method: String, + pub method_arn: String, + pub path: String, + pub path_parameters: Option>, + pub query_string_parameters: Option>, + pub resource: String, + pub request_context: HttpEventRequestContext, + pub stage_variables: Option>, + #[serde(rename="type")] + pub event_type: EventType, +} + +#[derive(Serialize,Deserialize)] +pub enum Effect { + Allow, + Deny +} + +impl fmt::Display for Effect { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Effect::Allow => write!(f, "Allow"), + Effect::Deny => write!(f, "Deny") + } + } +} diff --git a/src/data/apigateway/fixtures/authorize.json b/src/data/apigateway/fixtures/authorize.json new file mode 100644 index 0000000..9b39b30 --- /dev/null +++ b/src/data/apigateway/fixtures/authorize.json @@ -0,0 +1,26 @@ +{ + "headers": null, + "httpMethod": "GET", + "methodArn": "arn:aws:execute-api:us-east-1:111111111111:rest-api-id/null/GET/", + "path": "/", + "pathParameters": {}, + "queryStringParameters": {}, + "requestContext": { + "accountId": "111111111111", + "apiId": "rest-api-id", + "httpMethod": "GET", + "identity": { + "apiKey": "test-invoke-api-key", + "apiKeyId": "test-invoke-api-key-id", + "sourceIp": "test-invoke-source-ip" + }, + "path": "/", + "requestId": "test-invoke-request", + "resourceId": "test-invoke-resource-id", + "resourcePath": "/", + "stage": "test-invoke-stage" + }, + "resource": "/", + "stageVariables": {}, + "type": "REQUEST" +} diff --git a/src/data/apigateway/fixtures/request.json b/src/data/apigateway/fixtures/request.json new file mode 100644 index 0000000..c0df4fe --- /dev/null +++ b/src/data/apigateway/fixtures/request.json @@ -0,0 +1,53 @@ +{ + "body": null, + "headers": { + "Accept": "*/*", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Host": "example.com", + "User-Agent": "curl/7.47.0", + "Via": "1.1 deadbeefcafebabebeefbeefdeaddead.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "J0YMU-brgNv9hzadv_rjnICwfLCd4-IYjVz55KdZS6fuGB6xkU65WA==", + "X-Amzn-Trace-Id": "Root=1-5a9095d0-2459dc8c2eead5b445840ca0", + "X-Forwarded-For": "127.0.0.1, 127.0.0.1", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "httpMethod": "GET", + "isBase64Encoded": false, + "path": "/echo.json", + "pathParameters": null, + "queryStringParameters": null, + "requestContext": { + "accountId": "123456789012", + "apiId": "smddwihyy9", + "httpMethod": "GET", + "identity": { + "accessKey": null, + "accountId": null, + "caller": null, + "cognitoAuthenticationProvider": null, + "cognitoAuthenticationType": null, + "cognitoIdentityId": null, + "cognitoIdentityPoolId": null, + "sourceIp": "127.0.0.1", + "user": null, + "userAgent": "curl/7.47.0", + "userArn": null + }, + "path": "/echo.json", + "protocol": "HTTP/1.1", + "requestId": "30e9177b-7253-43c4-95b4-b7c275ad037f", + "requestTime": "1/Jan/1970:00:00:00 +0000", + "requestTimeEpoch": 0, + "resourceId": "abcdef", + "resourcePath": "/echo.json", + "stage": "production" + }, + "resource": "/echo.json", + "stageVariables": null +} diff --git a/src/data/apigateway/fixtures/test.json b/src/data/apigateway/fixtures/test.json new file mode 100644 index 0000000..a2ca432 --- /dev/null +++ b/src/data/apigateway/fixtures/test.json @@ -0,0 +1,36 @@ +{ + "body": null, + "headers": null, + "httpMethod": "DELETE", + "isBase64Encoded": false, + "path": "/user/session.json", + "pathParameters": null, + "queryStringParameters": null, + "requestContext": { + "accountId": "123456789012", + "apiId": "rest-api-id", + "httpMethod": "DELETE", + "identity": { + "accessKey": "AWS_ACCESS_KEY_ID", + "accountId": "123456789012", + "apiKey": "test-invoke-api-key", + "apiKeyId": "test-invoke-api-key-id", + "caller": "AWS_CALLER_ID", + "cognitoAuthenticationProvider": null, + "cognitoAuthenticationType": null, + "cognitoIdentityId": null, + "cognitoIdentityPoolId": null, + "sourceIp": "test-invoke-source-ip", + "user": "AIDAJDXSYAWOHYRSW7IPO", + "userAgent": "Apache-HttpClient/4.5.x (Java/1.8.0_144)", + "userArn": "arn:aws:iam::123456789012:user/example" + }, + "path": "/user/session.json", + "requestId": "test-invoke-request", + "resourceId": "abcdef", + "resourcePath": "/user/session.json", + "stage": "test-invoke-stage" + }, + "resource": "/user/session.json", + "stageVariables": null +} diff --git a/src/data/apigateway/mod.rs b/src/data/apigateway/mod.rs new file mode 100644 index 0000000..981dc4c --- /dev/null +++ b/src/data/apigateway/mod.rs @@ -0,0 +1,170 @@ +#[cfg(test)] +mod tests; + +pub mod auth; + +use chrono::prelude::*; +use std::fmt; +use std::collections::HashMap; +use serde; +use serde_qs as qs; + +#[derive(Debug,Eq,PartialEq,Serialize,Deserialize)] +pub enum HttpMethod { + HEAD, + GET, + POST, + PUT, + OPTIONS, + DELETE, +} + +impl fmt::Display for HttpMethod { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", match *self { + HttpMethod::HEAD => "HEAD", + HttpMethod::GET => "GET", + HttpMethod::POST => "POST", + HttpMethod::PUT => "PUT", + HttpMethod::OPTIONS => "OPTIONS", + HttpMethod::DELETE => "DELETE" + }) + } +} + +#[derive(Serialize,Deserialize)] +pub struct HttpEvent { + pub body: Option, + pub headers: Option>, + #[serde(rename="httpMethod")] + pub http_method: HttpMethod, + #[serde(rename="isBase64Encoded")] + pub is_base64_encoded: bool, + pub path: String, + #[serde(rename="pathParameters")] + pub path_parameters: Option>, + #[serde(rename="queryStringParameters")] + pub query_string_parameters: Option>, + pub resource: String, + #[serde(rename="requestContext")] + pub request_context: HttpEventRequestContext, + #[serde(rename="stageVariables")] + pub stage_variables: Option>, +} + +impl HttpEvent { + + pub fn get_header(&self, key: &str) -> Option<&str> { + match self.headers { + Some(ref h) => h.get(key).map(|s| s.as_str()), + None => None, + } + } +} + +impl fmt::Display for HttpEvent { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{protocol} {method} {path}{querystring} (body? {body})", + protocol = match self.request_context.protocol { + Some(ref s) => s.as_str(), + None => "(unknown)" + }, + method = self.http_method, + path = self.path, + querystring = qs::to_string(&self.query_string_parameters).unwrap_or(String::new()), + body = match self.body { + Some(_) => true, + None => false + } + ) + } +} + +#[derive(Serialize,Deserialize)] +pub struct HttpEventRequestContext { + #[serde(rename="accountId")] + pub account_id: String, + #[serde(rename="apiId")] + pub api_id: String, + #[serde(rename="httpMethod")] + pub http_method: HttpMethod, + pub identity: HashMap>, + pub path: String, + pub protocol: Option, + #[serde(rename="requestId")] + pub request_id: String, + #[serde(rename="requestTime")] + pub request_time: Option, + #[serde(rename="requestTimeEpoch")] + pub request_time_epoch: Option, + #[serde(rename="resourceId")] + pub resource_id: String, + #[serde(rename="resourcePath")] + pub resource_path: String, + pub stage: String, +} + +impl HttpEventRequestContext { + + pub fn time(&self) -> Option> { + // Utc::datetime_from_str(&self.request_time, "").ok()e + None + } +} + +serde_aux_enum_number_declare!(HttpStatus { + OK = 200, + + MovedPermanently = 301, + Found = 302, + TemporaryRedirect = 307, + PermanentRedirect = 308, + + BadRequest = 400, + Unauthorized = 401, + Forbidden = 403, + NotFound = 404, + Gone = 410, + + InternalServerError = 500, + BadGateway = 502, +}); + +#[derive(Serialize,Deserialize)] +pub struct HttpResponse { + #[serde(rename="statusCode")] + pub status: HttpStatus, + pub headers: HashMap, + pub body: Option, + #[serde(rename="isBase64Encoded",default)] + pub is_base64: bool, +} + +impl HttpResponse { + + pub fn empty(status: HttpStatus) -> Self { + HttpResponse { + status: status, + headers: HashMap::new(), + body: None, + is_base64: false, + } + } + + pub fn with_body>(status: HttpStatus, body: S) -> Self { + HttpResponse { + status: status, + headers: HashMap::new(), + body: Some(body.into()), + is_base64: false, + } + } + + pub fn set_header(&mut self, key: &str, value: &str) { + self.headers.insert(String::from(key), String::from(value)); + } + + pub fn success(&self) -> bool { + return self.status >= HttpStatus::OK && self.status < HttpStatus::BadRequest + } +} diff --git a/src/data/apigateway/tests.rs b/src/data/apigateway/tests.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/data/autoscaling/fixtures/instance-launch-failure.json b/src/data/autoscaling/fixtures/instance-launch-failure.json new file mode 100644 index 0000000..b0cc3f5 --- /dev/null +++ b/src/data/autoscaling/fixtures/instance-launch-failure.json @@ -0,0 +1,26 @@ +{ + "id": "1681ab87-4a09-459f-95a2-7fa09403c4b7", + "detail-type": "EC2 Instance Launch Unsuccessful", + "source": "aws.autoscaling", + "account": "123456789012", + "time": "2015-11-11T21:42:36Z", + "region": "us-east-1", + "resources": [ + "arn:aws:autoscaling:us-east-1:123456789012:autoScalingGroup:528ffce5-ef9f-4c1d-8d18-5d005b4a438c:autoScalingGroupName/brokenASG", + "arn:aws:ec2:us-east-1:123456789012:instance/" + ], + "detail": { + "StatusCode": "Failed", + "AutoScalingGroupName": "brokenASG", + "ActivityId": "06076c51-4874-487d-b15b-7895a713ab55", + "Details": { + "Availability Zone": "us-east-1e", + "Subnet ID": "subnet-16c5df2c" + }, + "RequestId": "06076c51-4874-487d-b15b-7895a713ab55", + "EndTime": "2015-11-11T21:42:36.000Z", + "EC2InstanceId": "", + "StartTime": "2015-11-11T21:42:36.698Z", + "Cause": "At 2015-11-11T21:42:09Z a user request update of Auto Scaling group constraints to min: 0, max: 10, desired: 2 changing the desired capacity from 0 to 2. At 2015-11-11T21:42:35Z an instance was started in response to a difference between desired and actual capacity, increasing the capacity from 0 to 2." + } +} diff --git a/src/data/autoscaling/fixtures/instance-launch-success.json b/src/data/autoscaling/fixtures/instance-launch-success.json new file mode 100644 index 0000000..fe205fb --- /dev/null +++ b/src/data/autoscaling/fixtures/instance-launch-success.json @@ -0,0 +1,26 @@ +{ + "id": "3e3c153a-8339-4e30-8c35-687ebef853fe", + "detail-type": "EC2 Instance Launch Successful", + "source": "aws.autoscaling", + "account": "123456789012", + "time": "2015-11-11T21:31:47Z", + "region": "us-east-1", + "resources": [ + "arn:aws:autoscaling:us-east-1:123456789012:autoScalingGroup:eb56d16b-bbf0-401d-b893-d5978ed4a025:autoScalingGroupName/ASGLaunchSuccess", + "arn:aws:ec2:us-east-1:123456789012:instance/i-b188560f" + ], + "detail": { + "StatusCode": "InProgress", + "AutoScalingGroupName": "ASGLaunchSuccess", + "ActivityId": "9cabb81f-42de-417d-8aa7-ce16bf026590", + "Details": { + "Availability Zone": "us-east-1b", + "Subnet ID": "subnet-95bfcebe" + }, + "RequestId": "9cabb81f-42de-417d-8aa7-ce16bf026590", + "EndTime": "2015-11-11T21:31:47.208Z", + "EC2InstanceId": "i-b188560f", + "StartTime": "2015-11-11T21:31:13.671Z", + "Cause": "At 2015-11-11T21:31:10Z a user request created an Auto Scaling group changing the desired capacity from 0 to 1. At 2015-11-11T21:31:11Z an instance was started in response to a difference between desired and actual capacity, increasing the capacity from 0 to 1." + } +} diff --git a/src/data/autoscaling/fixtures/instance-lifecycle-launch.json b/src/data/autoscaling/fixtures/instance-lifecycle-launch.json new file mode 100644 index 0000000..7f46926 --- /dev/null +++ b/src/data/autoscaling/fixtures/instance-lifecycle-launch.json @@ -0,0 +1,20 @@ +{ + "version": "0", + "id": "6a7e8feb-b491-4cf7-a9f1-bf3703467718", + "detail-type": "EC2 Instance-launch Lifecycle Action", + "source": "aws.autoscaling", + "account": "123456789012", + "time": "2015-12-22T18:43:48Z", + "region": "us-east-1", + "resources": [ + "arn:aws:autoscaling:us-east-1:123456789012:autoScalingGroup:59fcbb81-bd02-485d-80ce-563ef5b237bf:autoScalingGroupName/sampleASG" + ], + "detail": { + "LifecycleActionToken": "c613620e-07e2-4ed2-a9e2-ef8258911ade", + "AutoScalingGroupName": "my-asg", + "LifecycleHookName": "my-lifecycle-hook", + "EC2InstanceId": "i-1234567890abcdef0", + "LifecycleTransition": "autoscaling:EC2_INSTANCE_LAUNCHING", + "NotificationMetadata": "additional-info" + } +} diff --git a/src/data/autoscaling/fixtures/instance-lifecycle-terminate.json b/src/data/autoscaling/fixtures/instance-lifecycle-terminate.json new file mode 100644 index 0000000..aaa6f68 --- /dev/null +++ b/src/data/autoscaling/fixtures/instance-lifecycle-terminate.json @@ -0,0 +1,19 @@ +{ + "version": "0", + "id": "468fe059-f4b7-445f-bb22-2a271b94974d", + "detail-type": "EC2 Instance-terminate Lifecycle Action", + "source": "aws.autoscaling", + "account": "123456789012", + "time": "2015-12-22T18:43:48Z", + "region": "us-east-1", + "resources": [ + "arn:aws:autoscaling:us-east-1:123456789012:autoScalingGroup:59fcbb81-bd02-485d-80ce-563ef5b237bf:autoScalingGroupName/sampleASG" + ], + "detail": { + "LifecycleActionToken": "630aa23f-48eb-45e7-aba6-799ea6093a0f", + "AutoScalingGroupName": "sampleASG", + "LifecycleHookName": "SampleLifecycleHook-6789", + "EC2InstanceId": "i-12345678", + "LifecycleTransition": "autoscaling:EC2_INSTANCE_TERMINATING" + } +} diff --git a/src/data/autoscaling/fixtures/instance-terminate-failure.json b/src/data/autoscaling/fixtures/instance-terminate-failure.json new file mode 100644 index 0000000..5bedc4b --- /dev/null +++ b/src/data/autoscaling/fixtures/instance-terminate-failure.json @@ -0,0 +1,28 @@ +{ + "id": "5e3df53a-0239-4e31-7d15-087ebef903ce", + "detail-type": "EC2 Instance Terminate Unsuccessful", + "source": "aws.autoscaling", + "account": "123456789012", + "time": "2015-12-01T23:34:57Z", + "region": "us-east-1", + "resources": [ + "arn:aws:autoscaling:us-east-1:123456789012:autoScalingGroup:cf5ebd9c-8e2a-4197-abe2-2fb94e8d1f87:autoScalingGroupName/ASGTermFail", + "arn:aws:ec2:us-east-1:123456789012:instance/i-b188560f" + ], + "detail": { + "StatusCode": "InProgress", + "Description": "Terminating EC2 instance: i-b188560f", + "AutoScalingGroupName": "ASGTermFail", + "ActivityId": "c1a8f6ce-82e8-4517-96ba-67d1999ceee4", + "Details": { + "Availability Zone": "us-east-1e", + "Subnet ID": "subnet-915643ba" + }, + "RequestId": "c1a8f6ce-82e8-4517-96ba-67d1999ceee4", + "StatusMessage": "", + "EndTime": "2015-12-01T23:34:57.721Z", + "EC2InstanceId": "i-b188560f", + "StartTime": "2015-12-01T23:33:48.489Z", + "Cause": "At 2015-12-01T23:33:41Z a user request explicitly set group desired capacity changing the desired capacity from 2 to 0. At 2015-12-01T23:33:47Z an instance was taken out of service in response to a difference between desired and actual capacity, shrinking the capacity from 2 to 0. At 2015-12-01T23:33:47Z instance i-0867b4292c0cff474 was selected for termination. At 2015-12-01T23:33:48Z instance i-b188560f was selected for termination." + } +} diff --git a/src/data/autoscaling/fixtures/instance-terminate-success.json b/src/data/autoscaling/fixtures/instance-terminate-success.json new file mode 100644 index 0000000..5143ab6 --- /dev/null +++ b/src/data/autoscaling/fixtures/instance-terminate-success.json @@ -0,0 +1,26 @@ +{ + "id": "156d01c9-a6c3-4d7e-b883-5758266b95af", + "detail-type": "EC2 Instance Terminate Successful", + "source": "aws.autoscaling", + "account": "123456789012", + "time": "2015-11-11T21:36:57Z", + "region": "us-east-1", + "resources": [ + "arn:aws:autoscaling:us-east-1:123456789012:autoScalingGroup:eb56d16b-bbf0-401d-b893-d5978ed4a025:autoScalingGroupName/ASGTerminate", + "arn:aws:ec2:us-east-1:123456789012:instance/i-b188560f" + ], + "detail": { + "StatusCode": "InProgress", + "AutoScalingGroupName": "ASGTerminate", + "ActivityId": "56472e79-538a-4ba7-b3cc-768d889194b0", + "Details": { + "Availability Zone": "us-east-1b", + "Subnet ID": "subnet-95bfcebe" + }, + "RequestId": "56472e79-538a-4ba7-b3cc-768d889194b0", + "EndTime": "2015-11-11T21:36:57.498Z", + "EC2InstanceId": "i-b188560f", + "StartTime": "2015-11-11T21:36:12.649Z", + "Cause": "At 2015-11-11T21:36:03Z a user request update of Auto Scaling group constraints to min: 0, max: 1, desired: 0 changing the desired capacity from 1 to 0. At 2015-11-11T21:36:12Z an instance was taken out of service in response to a difference between desired and actual capacity, shrinking the capacity from 1 to 0. At 2015-11-11T21:36:12Z instance i-b188560f was selected for termination." + } +} diff --git a/src/data/autoscaling/mod.rs b/src/data/autoscaling/mod.rs new file mode 100644 index 0000000..297d6a7 --- /dev/null +++ b/src/data/autoscaling/mod.rs @@ -0,0 +1,177 @@ +#[cfg(test)] +mod tests; + +use super::*; + +use chrono::Duration; + +#[derive(Serialize,Deserialize)] +#[serde(untagged)] +pub enum AutoScalingEvent { + Action(LifecycleAction), + Event(LifecycleEvent), +} + +#[derive(Serialize,Deserialize)] +#[serde(rename_all="kebab-case")] +pub struct LifecycleAction { + pub account: String, + pub id: String, + pub detail: ActionDetail, + pub detail_type: String, + pub region: String, + pub resources: Vec, + pub time: DateTime, + pub version: String, +} + +#[derive(Serialize,Deserialize)] +#[serde(rename_all="PascalCase")] +pub struct ActionDetail { + #[serde(rename="AutoScalingGroupName")] + pub autoscaling_group_name: String, + #[serde(rename="EC2InstanceId")] + pub ec2_instance_id: String, + pub lifecycle_action_token: String, + pub lifecycle_hook_name: String, + pub lifecycle_transition: LifecycleTransition, +} + +#[derive(Debug,Eq,PartialEq,Serialize,Deserialize)] +pub enum LifecycleTransition { + #[serde(rename="autoscaling:EC2_INSTANCE_LAUNCHING")] + InstanceLaunching, + #[serde(rename="autoscaling:EC2_INSTANCE_TERMINATING")] + InstanceTerminating, +} + +#[derive(Serialize,Deserialize)] +pub struct LifecycleEvent { + pub id: String, + #[serde(rename="detail-type")] + pub event_type: EventType, + pub account: String, + pub time: DateTime, + pub region: String, + pub resources: Vec, + pub detail: EventDetail, +} + +impl LifecycleEvent { + + pub fn kind(&self) -> EventKind { + self.event_type.kind() + } + + pub fn status(&self) -> EventStatus { + self.event_type.status() + } + + pub fn duration(&self) -> Duration { + self.detail.duration() + } +} + +#[derive(Debug,Eq,PartialEq)] +pub enum EventStatus { + Success, + Failure, + Unknown, +} + +#[derive(Debug,Eq,PartialEq)] +pub enum EventKind { + Launch, + Terminate, + Unknown, +} + +#[derive(Serialize,Deserialize)] +pub enum EventType { + #[serde(rename="EC2 Instance Terminate Unsuccessful")] + TerminateFailed, + #[serde(rename="EC2 Instance Terminate Successful")] + TerminateSuccess, + #[serde(rename="EC2 Instance Launch Successful")] + LaunchSuccess, + #[serde(rename="EC2 Instance Launch Unsuccessful")] + LaunchFailed, + Unknown(String), +} + +impl EventType { + + pub fn status(&self) -> EventStatus { + match self { + &EventType::LaunchFailed => EventStatus::Failure, + &EventType::LaunchSuccess => EventStatus::Success, + &EventType::TerminateFailed => EventStatus::Failure, + &EventType::TerminateSuccess => EventStatus::Success, + &EventType::Unknown(_) => EventStatus::Unknown + } + } + + pub fn kind(&self) -> EventKind { + match self { + &EventType::LaunchFailed => EventKind::Launch, + &EventType::LaunchSuccess => EventKind::Launch, + &EventType::TerminateFailed => EventKind::Terminate, + &EventType::TerminateSuccess => EventKind::Terminate, + &EventType::Unknown(_) => EventKind::Unknown + } + } +} + +#[derive(Serialize,Deserialize)] +#[serde(rename_all="PascalCase")] +pub struct EventDetail { + pub status_code: StatusCode, + #[serde(rename="AutoScalingGroupName")] + pub autoscaling_group_name: String, + pub activity_id: String, + pub request_id: String, + pub end_time: DateTime, + pub start_time: DateTime, + #[serde(rename="EC2InstanceId")] + pub ec2_instance_id: String, + pub cause: String, + pub details: SubnetDetails, +} + +#[derive(Serialize,Deserialize)] +pub enum StatusCode { + InProgress, + Failed, +} + +impl EventDetail { + + /// Return the amount of time this event took from start time to end time. + pub fn duration(&self) -> Duration { + let start_time = Duration::nanoseconds( + (self.start_time.timestamp() as i64 * 1_000_000_000) + + self.start_time.timestamp_subsec_nanos() as i64 + ); + + let end_time = Duration::nanoseconds( + (self.end_time.timestamp() as i64 * 1_000_000_000) + + self.end_time.timestamp_subsec_nanos() as i64 + ); + + let difference = if end_time > start_time { + end_time - start_time + } else { + start_time - end_time + }; + + return difference + } +} + +#[derive(Serialize,Deserialize)] +pub struct SubnetDetails { + #[serde(rename="Availability Zone")] + pub availability_zone: String, + #[serde(rename="Subnet ID")] + pub subnet_id: String, +} diff --git a/src/data/autoscaling/tests.rs b/src/data/autoscaling/tests.rs new file mode 100644 index 0000000..10de208 --- /dev/null +++ b/src/data/autoscaling/tests.rs @@ -0,0 +1,82 @@ +use super::*; + +use serde_json; + +#[test] +fn test_disambiguation() { + match serde_json::from_str(include_str!("fixtures/instance-lifecycle-launch.json")).unwrap() { + AutoScalingEvent::Event(_evt) => panic!("Deserialized an action as an event."), + AutoScalingEvent::Action(_action) => (), + }; + + match serde_json::from_str(include_str!("fixtures/instance-launch-success.json")).unwrap() { + AutoScalingEvent::Event(_evt) => (), + AutoScalingEvent::Action(_action) => panic!("Deserialized an event as an action."), + }; +} + +#[test] +fn test_lifecycle_action_launch() { + let action: LifecycleAction = serde_json::from_str( + include_str!("fixtures/instance-lifecycle-launch.json") + ).unwrap(); + + assert_eq!(action.detail.lifecycle_transition, LifecycleTransition::InstanceLaunching); +} + +#[test] +fn test_lifecycle_action_terminate() { + let action: LifecycleAction = serde_json::from_str( + include_str!("fixtures/instance-lifecycle-terminate.json") + ).unwrap(); + + assert_eq!(action.detail.lifecycle_transition, LifecycleTransition::InstanceTerminating); +} + +#[test] +fn test_lifecycle_event_launch_failure() { + let event: LifecycleEvent = serde_json::from_str( + include_str!("fixtures/instance-launch-failure.json") + ).unwrap(); + + assert_eq!(event.status(), EventStatus::Failure); + assert_eq!(event.kind(), EventKind::Launch); + + assert_eq!(Duration::nanoseconds(698_000_000), event.duration()); +} + +#[test] +fn test_lifecycle_event_launch_success() { + let event: LifecycleEvent = serde_json::from_str( + include_str!("fixtures/instance-launch-success.json") + ).unwrap(); + + assert_eq!(event.status(), EventStatus::Success); + assert_eq!(event.kind(), EventKind::Launch); + + assert_eq!(Duration::nanoseconds(33_537_000_000), event.duration()); +} + +#[test] +fn test_lifecycle_event_terminate_failure() { + let event: LifecycleEvent = serde_json::from_str( + include_str!("fixtures/instance-terminate-failure.json") + ).unwrap(); + + assert_eq!(event.status(), EventStatus::Failure); + assert_eq!(event.kind(), EventKind::Terminate); + + assert_eq!(Duration::nanoseconds(69_232_000_000), event.duration()); +} + +#[test] +fn test_lifecycle_event_terminate_success() { + let event: LifecycleEvent = serde_json::from_str( + include_str!("fixtures/instance-terminate-success.json") + ).unwrap(); + + assert_eq!(event.status(), EventStatus::Success); + assert_eq!(event.kind(), EventKind::Terminate); + + assert_eq!(Duration::nanoseconds(44_849_000_000), event.duration()); +} diff --git a/src/data/batch/fixtures/state-change-failed.json b/src/data/batch/fixtures/state-change-failed.json new file mode 100644 index 0000000..7f6faeb --- /dev/null +++ b/src/data/batch/fixtures/state-change-failed.json @@ -0,0 +1,92 @@ +{ + "version": "0", + "id": "c8f9c4b5-76e5-d76a-f980-7011e206042b", + "detail-type": "Batch Job State Change", + "source": "aws.batch", + "account": "aws_account_id", + "time": "2017-10-23T17:56:03Z", + "region": "us-east-1", + "resources": [ + "arn:aws:batch:us-east-1:aws_account_id:job/4c7599ae-0a82-49aa-ba5a-4727fcce14a8" + ], + "detail": { + "attempts": [ + { + "container": { + "containerInstanceArn": "string", + "exitCode": 0, + "logStreamName": "string", + "reason": "string", + "taskArn": "string" + }, + "startedAt": 1508781340401, + "statusReason": "string", + "stoppedAt": 1508781340401 + } + ], + "container": { + "command": [ "string" ], + "containerInstanceArn": "string", + "environment": [ + { + "name": "string", + "value": "string" + } + ], + "exitCode": 0, + "image": "string", + "jobRoleArn": "string", + "logStreamName": "string", + "memory": 1024, + "mountPoints": [ + { + "containerPath": "string", + "readOnly": false, + "sourceVolume": "string" + } + ], + "privileged": false, + "readonlyRootFilesystem": false, + "reason": "string", + "taskArn": "string", + "ulimits": [ + { + "hardLimit": 1024, + "name": "string", + "softLimit": 1024 + } + ], + "user": "string", + "vcpus": 2, + "volumes": [ + { + "host": { + "sourcePath": "string" + }, + "name": "string" + } + ] + }, + "createdAt": 1508781340401, + "dependsOn": [ + { + "jobId": "string", + "type": "string" + } + ], + "jobDefinition": "string", + "jobId": "string", + "jobName": "string", + "jobQueue": "string", + "parameters": { + "string" : "string" + }, + "retryStrategy": { + "attempts": 5 + }, + "startedAt": 1508781340401, + "status": "FAILED", + "statusReason": "string", + "stoppedAt": 1508781340401 + } +} diff --git a/src/data/batch/fixtures/state-change-pending.json b/src/data/batch/fixtures/state-change-pending.json new file mode 100644 index 0000000..b377349 --- /dev/null +++ b/src/data/batch/fixtures/state-change-pending.json @@ -0,0 +1,92 @@ +{ + "version": "0", + "id": "c8f9c4b5-76e5-d76a-f980-7011e206042b", + "detail-type": "Batch Job State Change", + "source": "aws.batch", + "account": "aws_account_id", + "time": "2017-10-23T17:56:03Z", + "region": "us-east-1", + "resources": [ + "arn:aws:batch:us-east-1:aws_account_id:job/4c7599ae-0a82-49aa-ba5a-4727fcce14a8" + ], + "detail": { + "attempts": [ + { + "container": { + "containerInstanceArn": "string", + "exitCode": 0, + "logStreamName": "string", + "reason": "string", + "taskArn": "string" + }, + "startedAt": 1508781340401, + "statusReason": "string", + "stoppedAt": 1508781340401 + } + ], + "container": { + "command": [ "string" ], + "containerInstanceArn": "string", + "environment": [ + { + "name": "string", + "value": "string" + } + ], + "exitCode": 0, + "image": "string", + "jobRoleArn": "string", + "logStreamName": "string", + "memory": 1024, + "mountPoints": [ + { + "containerPath": "string", + "readOnly": false, + "sourceVolume": "string" + } + ], + "privileged": false, + "readonlyRootFilesystem": false, + "reason": "string", + "taskArn": "string", + "ulimits": [ + { + "hardLimit": 1024, + "name": "string", + "softLimit": 1024 + } + ], + "user": "string", + "vcpus": 2, + "volumes": [ + { + "host": { + "sourcePath": "string" + }, + "name": "string" + } + ] + }, + "createdAt": 1508781340401, + "dependsOn": [ + { + "jobId": "string", + "type": "string" + } + ], + "jobDefinition": "string", + "jobId": "string", + "jobName": "string", + "jobQueue": "string", + "parameters": { + "string" : "string" + }, + "retryStrategy": { + "attempts": 5 + }, + "startedAt": 1508781340401, + "status": "PENDING", + "statusReason": "string", + "stoppedAt": 1508781340401 + } +} diff --git a/src/data/batch/fixtures/state-change-runnable.json b/src/data/batch/fixtures/state-change-runnable.json new file mode 100644 index 0000000..2a0bb66 --- /dev/null +++ b/src/data/batch/fixtures/state-change-runnable.json @@ -0,0 +1,92 @@ +{ + "version": "0", + "id": "c8f9c4b5-76e5-d76a-f980-7011e206042b", + "detail-type": "Batch Job State Change", + "source": "aws.batch", + "account": "aws_account_id", + "time": "2017-10-23T17:56:03Z", + "region": "us-east-1", + "resources": [ + "arn:aws:batch:us-east-1:aws_account_id:job/4c7599ae-0a82-49aa-ba5a-4727fcce14a8" + ], + "detail": { + "attempts": [ + { + "container": { + "containerInstanceArn": "string", + "exitCode": 0, + "logStreamName": "string", + "reason": "string", + "taskArn": "string" + }, + "startedAt": 1508781340401, + "statusReason": "string", + "stoppedAt": 1508781340401 + } + ], + "container": { + "command": [ "string" ], + "containerInstanceArn": "string", + "environment": [ + { + "name": "string", + "value": "string" + } + ], + "exitCode": 0, + "image": "string", + "jobRoleArn": "string", + "logStreamName": "string", + "memory": 1024, + "mountPoints": [ + { + "containerPath": "string", + "readOnly": false, + "sourceVolume": "string" + } + ], + "privileged": false, + "readonlyRootFilesystem": false, + "reason": "string", + "taskArn": "string", + "ulimits": [ + { + "hardLimit": 1024, + "name": "string", + "softLimit": 1024 + } + ], + "user": "string", + "vcpus": 2, + "volumes": [ + { + "host": { + "sourcePath": "string" + }, + "name": "string" + } + ] + }, + "createdAt": 1508781340401, + "dependsOn": [ + { + "jobId": "string", + "type": "string" + } + ], + "jobDefinition": "string", + "jobId": "string", + "jobName": "string", + "jobQueue": "string", + "parameters": { + "string" : "string" + }, + "retryStrategy": { + "attempts": 5 + }, + "startedAt": 1508781340401, + "status": "RUNNABLE", + "statusReason": "string", + "stoppedAt": 1508781340401 + } +} diff --git a/src/data/batch/fixtures/state-change-running.json b/src/data/batch/fixtures/state-change-running.json new file mode 100644 index 0000000..e5bac67 --- /dev/null +++ b/src/data/batch/fixtures/state-change-running.json @@ -0,0 +1,92 @@ +{ + "version": "0", + "id": "c8f9c4b5-76e5-d76a-f980-7011e206042b", + "detail-type": "Batch Job State Change", + "source": "aws.batch", + "account": "aws_account_id", + "time": "2017-10-23T17:56:03Z", + "region": "us-east-1", + "resources": [ + "arn:aws:batch:us-east-1:aws_account_id:job/4c7599ae-0a82-49aa-ba5a-4727fcce14a8" + ], + "detail": { + "attempts": [ + { + "container": { + "containerInstanceArn": "string", + "exitCode": 0, + "logStreamName": "string", + "reason": "string", + "taskArn": "string" + }, + "startedAt": 1508781340401, + "statusReason": "string", + "stoppedAt": 1508781340401 + } + ], + "container": { + "command": [ "string" ], + "containerInstanceArn": "string", + "environment": [ + { + "name": "string", + "value": "string" + } + ], + "exitCode": 0, + "image": "string", + "jobRoleArn": "string", + "logStreamName": "string", + "memory": 1024, + "mountPoints": [ + { + "containerPath": "string", + "readOnly": false, + "sourceVolume": "string" + } + ], + "privileged": false, + "readonlyRootFilesystem": false, + "reason": "string", + "taskArn": "string", + "ulimits": [ + { + "hardLimit": 1024, + "name": "string", + "softLimit": 1024 + } + ], + "user": "string", + "vcpus": 2, + "volumes": [ + { + "host": { + "sourcePath": "string" + }, + "name": "string" + } + ] + }, + "createdAt": 1508781340401, + "dependsOn": [ + { + "jobId": "string", + "type": "string" + } + ], + "jobDefinition": "string", + "jobId": "string", + "jobName": "string", + "jobQueue": "string", + "parameters": { + "string" : "string" + }, + "retryStrategy": { + "attempts": 5 + }, + "startedAt": 1508781340401, + "status": "RUNNING", + "statusReason": "string", + "stoppedAt": 1508781340401 + } +} diff --git a/src/data/batch/fixtures/state-change-starting.json b/src/data/batch/fixtures/state-change-starting.json new file mode 100644 index 0000000..91b628a --- /dev/null +++ b/src/data/batch/fixtures/state-change-starting.json @@ -0,0 +1,92 @@ +{ + "version": "0", + "id": "c8f9c4b5-76e5-d76a-f980-7011e206042b", + "detail-type": "Batch Job State Change", + "source": "aws.batch", + "account": "aws_account_id", + "time": "2017-10-23T17:56:03Z", + "region": "us-east-1", + "resources": [ + "arn:aws:batch:us-east-1:aws_account_id:job/4c7599ae-0a82-49aa-ba5a-4727fcce14a8" + ], + "detail": { + "attempts": [ + { + "container": { + "containerInstanceArn": "string", + "exitCode": 0, + "logStreamName": "string", + "reason": "string", + "taskArn": "string" + }, + "startedAt": 1508781340401, + "statusReason": "string", + "stoppedAt": 1508781340401 + } + ], + "container": { + "command": [ "string" ], + "containerInstanceArn": "string", + "environment": [ + { + "name": "string", + "value": "string" + } + ], + "exitCode": 0, + "image": "string", + "jobRoleArn": "string", + "logStreamName": "string", + "memory": 1024, + "mountPoints": [ + { + "containerPath": "string", + "readOnly": false, + "sourceVolume": "string" + } + ], + "privileged": false, + "readonlyRootFilesystem": false, + "reason": "string", + "taskArn": "string", + "ulimits": [ + { + "hardLimit": 1024, + "name": "string", + "softLimit": 1024 + } + ], + "user": "string", + "vcpus": 2, + "volumes": [ + { + "host": { + "sourcePath": "string" + }, + "name": "string" + } + ] + }, + "createdAt": 1508781340401, + "dependsOn": [ + { + "jobId": "string", + "type": "string" + } + ], + "jobDefinition": "string", + "jobId": "string", + "jobName": "string", + "jobQueue": "string", + "parameters": { + "string" : "string" + }, + "retryStrategy": { + "attempts": 5 + }, + "startedAt": 1508781340401, + "status": "STARTING", + "statusReason": "string", + "stoppedAt": 1508781340401 + } +} diff --git a/src/data/batch/fixtures/state-change-submitted.json b/src/data/batch/fixtures/state-change-submitted.json new file mode 100644 index 0000000..90af29c --- /dev/null +++ b/src/data/batch/fixtures/state-change-submitted.json @@ -0,0 +1,92 @@ +{ + "version": "0", + "id": "c8f9c4b5-76e5-d76a-f980-7011e206042b", + "detail-type": "Batch Job State Change", + "source": "aws.batch", + "account": "aws_account_id", + "time": "2017-10-23T17:56:03Z", + "region": "us-east-1", + "resources": [ + "arn:aws:batch:us-east-1:aws_account_id:job/4c7599ae-0a82-49aa-ba5a-4727fcce14a8" + ], + "detail": { + "attempts": [ + { + "container": { + "containerInstanceArn": "string", + "exitCode": 0, + "logStreamName": "string", + "reason": "string", + "taskArn": "string" + }, + "startedAt": 1508781340401, + "statusReason": "string", + "stoppedAt": 1508781340401 + } + ], + "container": { + "command": [ "string" ], + "containerInstanceArn": "string", + "environment": [ + { + "name": "string", + "value": "string" + } + ], + "exitCode": 0, + "image": "string", + "jobRoleArn": "string", + "logStreamName": "string", + "memory": 1024, + "mountPoints": [ + { + "containerPath": "string", + "readOnly": false, + "sourceVolume": "string" + } + ], + "privileged": false, + "readonlyRootFilesystem": false, + "reason": "string", + "taskArn": "string", + "ulimits": [ + { + "hardLimit": 1024, + "name": "string", + "softLimit": 1024 + } + ], + "user": "string", + "vcpus": 2, + "volumes": [ + { + "host": { + "sourcePath": "string" + }, + "name": "string" + } + ] + }, + "createdAt": 1508781340401, + "dependsOn": [ + { + "jobId": "string", + "type": "string" + } + ], + "jobDefinition": "string", + "jobId": "string", + "jobName": "string", + "jobQueue": "string", + "parameters": { + "string" : "string" + }, + "retryStrategy": { + "attempts": 5 + }, + "startedAt": 1508781340401, + "status": "SUBMITTED", + "statusReason": "string", + "stoppedAt": 1508781340401 + } +} diff --git a/src/data/batch/fixtures/state-change-succeeded.json b/src/data/batch/fixtures/state-change-succeeded.json new file mode 100644 index 0000000..c7649f4 --- /dev/null +++ b/src/data/batch/fixtures/state-change-succeeded.json @@ -0,0 +1,92 @@ +{ + "version": "0", + "id": "c8f9c4b5-76e5-d76a-f980-7011e206042b", + "detail-type": "Batch Job State Change", + "source": "aws.batch", + "account": "aws_account_id", + "time": "2017-10-23T17:56:03Z", + "region": "us-east-1", + "resources": [ + "arn:aws:batch:us-east-1:aws_account_id:job/4c7599ae-0a82-49aa-ba5a-4727fcce14a8" + ], + "detail": { + "attempts": [ + { + "container": { + "containerInstanceArn": "string", + "exitCode": 0, + "logStreamName": "string", + "reason": "string", + "taskArn": "string" + }, + "startedAt": 1508781340401, + "statusReason": "string", + "stoppedAt": 1508781340401 + } + ], + "container": { + "command": [ "string" ], + "containerInstanceArn": "string", + "environment": [ + { + "name": "string", + "value": "string" + } + ], + "exitCode": 0, + "image": "string", + "jobRoleArn": "string", + "logStreamName": "string", + "memory": 1024, + "mountPoints": [ + { + "containerPath": "string", + "readOnly": false, + "sourceVolume": "string" + } + ], + "privileged": false, + "readonlyRootFilesystem": false, + "reason": "string", + "taskArn": "string", + "ulimits": [ + { + "hardLimit": 1024, + "name": "string", + "softLimit": 1024 + } + ], + "user": "string", + "vcpus": 2, + "volumes": [ + { + "host": { + "sourcePath": "string" + }, + "name": "string" + } + ] + }, + "createdAt": 1508781340401, + "dependsOn": [ + { + "jobId": "string", + "type": "string" + } + ], + "jobDefinition": "string", + "jobId": "string", + "jobName": "string", + "jobQueue": "string", + "parameters": { + "string" : "string" + }, + "retryStrategy": { + "attempts": 5 + }, + "startedAt": 1508781340401, + "status": "SUCCEEDED", + "statusReason": "string", + "stoppedAt": 1508781340401 + } +} diff --git a/src/data/batch/mod.rs b/src/data/batch/mod.rs new file mode 100644 index 0000000..fbb8418 --- /dev/null +++ b/src/data/batch/mod.rs @@ -0,0 +1,131 @@ +/// More information available at: https://docs.aws.amazon.com/batch/latest/APIReference/API_DescribeJobs.html +#[cfg(test)] +mod tests; + +use super::*; + +#[derive(Serialize,Deserialize)] +#[serde(rename_all="kebab-case")] +pub struct JobStateChangeEvent { + pub id: String, + pub detail_type: String, + pub source: String, + pub account: String, + pub time: DateTime, + pub region: String, + pub resources: Vec, + pub detail: JobDetail, +} + +#[derive(Serialize,Deserialize)] +#[serde(rename_all="camelCase")] +pub struct JobDetail { + pub job_name: String, + pub job_id: String, + pub job_queue: String, + pub status: JobStatus, + pub attempts: Vec, + pub created_at: Option, + pub started_at: Option, + pub stopped_at: Option, + pub retry_strategy: RetryStrategy, + pub depends_on: Vec, + pub job_definition: String, + pub parameters: BTreeMap, + pub container: ContainerInfo, +} + +#[derive(Serialize,Deserialize)] +#[serde(rename_all="camelCase")] +pub struct JobAttempt { + pub container: JobAttemptContainer, + pub started_at: u64, + pub stopped_at: u64, + pub status_reason: String, +} + +#[derive(Serialize,Deserialize)] +#[serde(rename_all="camelCase")] +pub struct JobAttemptContainer { + pub container_instance_arn: String, + pub exit_code: u16, + pub log_stream_name: String, + pub reason: String, + pub task_arn: String, +} + +#[derive(Serialize,Deserialize)] +#[serde(rename_all="camelCase")] +pub struct JobDependency { + pub job_id: String, + #[serde(rename="type")] + pub job_type: String, +} + +#[derive(Serialize,Deserialize)] +pub struct RetryStrategy { + pub attempts: u64, +} + +#[derive(Serialize,Deserialize)] +#[serde(rename_all="camelCase")] +pub struct ContainerInfo { + pub command: Vec, + pub container_instance_arn: Option, + pub environment: Vec>, + pub exit_code: Option, + pub image: String, + pub job_role_arn: Option, + pub log_stream_name: Option, + pub memory: u64, + pub mount_points: Vec, + pub privileged: bool, + #[serde(rename="readonlyRootFilesystem")] + pub read_only_root_filesystem: bool, + pub reason: String, + pub task_arn: String, + pub ulimits: Vec, + pub user: String, + pub vcpus: u64, + pub volumes: Vec, +} + +#[derive(Serialize,Deserialize)] +#[serde(rename_all="camelCase")] +pub struct Ulimit { + pub hard_limit: u64, + pub soft_limit: u64, + pub name: String, +} + +#[derive(Serialize,Deserialize)] +pub struct ContainerVolume { + pub host: ContainerVolumeHost, + pub name: String, +} + +#[derive(Serialize,Deserialize)] +#[serde(rename_all="camelCase")] +pub struct ContainerVolumeHost { + pub source_path: String, +} + +#[derive(Serialize,Deserialize)] +#[serde(rename_all="camelCase")] +pub struct ContainerMountPoint { + pub container_path: String, + pub read_only: bool, + pub source_volume: String, +} + +#[derive(Debug,Eq,PartialEq,Serialize,Deserialize)] +#[serde(rename_all="SCREAMING_SNAKE_CASE")] +pub enum JobStatus { + Failed, + Pending, + Runnable, + Running, + Starting, + Submitted, + Succeeded, +} diff --git a/src/data/batch/tests.rs b/src/data/batch/tests.rs new file mode 100644 index 0000000..bf77f94 --- /dev/null +++ b/src/data/batch/tests.rs @@ -0,0 +1,66 @@ +use super::*; + +use serde_json; + +#[test] +fn test_state_change_failed() { + let event: JobStateChangeEvent = serde_json::from_str( + include_str!("fixtures/state-change-failed.json") + ).unwrap(); + + assert_eq!(JobStatus::Failed, event.detail.status); +} + +#[test] +fn test_state_change_pending() { + let event: JobStateChangeEvent = serde_json::from_str( + include_str!("fixtures/state-change-pending.json") + ).unwrap(); + + assert_eq!(JobStatus::Pending, event.detail.status); +} + +#[test] +fn test_state_change_runnable() { + let event: JobStateChangeEvent = serde_json::from_str( + include_str!("fixtures/state-change-runnable.json") + ).unwrap(); + + assert_eq!(JobStatus::Runnable, event.detail.status); +} + +#[test] +fn test_state_change_running() { + let event: JobStateChangeEvent = serde_json::from_str( + include_str!("fixtures/state-change-running.json") + ).unwrap(); + + assert_eq!(JobStatus::Running, event.detail.status); +} + +#[test] +fn test_state_change_starting() { + let event: JobStateChangeEvent = serde_json::from_str( + include_str!("fixtures/state-change-starting.json") + ).unwrap(); + + assert_eq!(JobStatus::Starting, event.detail.status); +} + +#[test] +fn test_state_change_submitted() { + let event: JobStateChangeEvent = serde_json::from_str( + include_str!("fixtures/state-change-submitted.json") + ).unwrap(); + + assert_eq!(JobStatus::Submitted, event.detail.status); +} + +#[test] +fn test_state_change_succeeded() { + let event: JobStateChangeEvent = serde_json::from_str( + include_str!("fixtures/state-change-succeeded.json") + ).unwrap(); + + assert_eq!(JobStatus::Succeeded, event.detail.status); +} diff --git a/src/data/cloudwatch/fixtures/ec2/instance-launch-failed.json b/src/data/cloudwatch/fixtures/ec2/instance-launch-failed.json new file mode 100644 index 0000000..fa6a64d --- /dev/null +++ b/src/data/cloudwatch/fixtures/ec2/instance-launch-failed.json @@ -0,0 +1,26 @@ +{ + "id": "1681ab87-4a09-459f-95a2-7fa09403c4b7", + "detail-type": "EC2 Instance Launch Unsuccessful", + "source": "aws.autoscaling", + "account": "123456789012", + "time": "2015-11-11T21:42:36Z", + "region": "us-east-1", + "resources": [ + "arn:aws:autoscaling:us-east-1:123456789012:autoScalingGroup:528ffce5-ef9f-4c1d-8d18-5d005b4a438c:autoScalingGroupName/brokenASG", + "arn:aws:ec2:us-east-1:123456789012:instance/" + ], + "detail": { + "StatusCode": "Failed", + "AutoScalingGroupName": "brokenASG", + "ActivityId": "06076c51-4874-487d-b15b-7895a713ab55", + "Details": { + "Availability Zone": "us-east-1e", + "Subnet ID": "subnet-16c5df2c" + }, + "RequestId": "06076c51-4874-487d-b15b-7895a713ab55", + "EndTime": "1970-01-01T00:00:00.000Z", + "EC2InstanceId": "", + "StartTime": "1970-01-01T00:00:00.000Z", + "Cause": "At 2015-11-11T21:42:09Z a user request update of Auto Scaling group constraints to min: 0, max: 10, desired: 2 changing the desired capacity from 0 to 2. At 2015-11-11T21:42:35Z an instance was started in response to a difference between desired and actual capacity, increasing the capacity from 0 to 2." + } +} diff --git a/src/data/cloudwatch/fixtures/ec2/instance-terminate-success.json b/src/data/cloudwatch/fixtures/ec2/instance-terminate-success.json new file mode 100644 index 0000000..1e4e56d --- /dev/null +++ b/src/data/cloudwatch/fixtures/ec2/instance-terminate-success.json @@ -0,0 +1,26 @@ +{ + "id": "156d01c9-a6c3-4d7e-b883-5758266b95af", + "detail-type": "EC2 Instance Terminate Successful", + "source": "aws.autoscaling", + "account": "123456789012", + "time": "2015-11-11T21:36:57Z", + "region": "us-east-1", + "resources": [ + "arn:aws:autoscaling:us-east-1:123456789012:autoScalingGroup:eb56d16b-bbf0-401d-b893-d5978ed4a025:autoScalingGroupName/ASGTerminate", + "arn:aws:ec2:us-east-1:123456789012:instance/i-b188560f" + ], + "detail": { + "StatusCode": "InProgress", + "AutoScalingGroupName": "ASGTerminate", + "ActivityId": "56472e79-538a-4ba7-b3cc-768d889194b0", + "Details": { + "Availability Zone": "us-east-1b", + "Subnet ID": "subnet-95bfcebe" + }, + "RequestId": "56472e79-538a-4ba7-b3cc-768d889194b0", + "EndTime": "1970-01-01T00:00:00.000Z", + "EC2InstanceId": "i-b188560f", + "StartTime": "1970-01-01T00:00:00.000Z", + "Cause": "At 2015-11-11T21:36:03Z a user request update of Auto Scaling group constraints to min: 0, max: 1, desired: 0 changing the desired capacity from 1 to 0. At 2015-11-11T21:36:12Z an instance was taken out of service in response to a difference between desired and actual capacity, shrinking the capacity from 1 to 0. At 2015-11-11T21:36:12Z instance i-b188560f was selected for termination." + } +} diff --git a/src/data/cloudwatch/fixtures/ec2/instance-terminate-unsuccessful.json b/src/data/cloudwatch/fixtures/ec2/instance-terminate-unsuccessful.json new file mode 100644 index 0000000..e9635a6 --- /dev/null +++ b/src/data/cloudwatch/fixtures/ec2/instance-terminate-unsuccessful.json @@ -0,0 +1,28 @@ +{ + "id": "5e3df53a-0239-4e31-7d15-087ebef903ce", + "detail-type": "EC2 Instance Terminate Unsuccessful", + "source": "aws.autoscaling", + "account": "123456789012", + "time": "2015-12-01T23:34:57Z", + "region": "us-east-1", + "resources": [ + "arn:aws:autoscaling:us-east-1:123456789012:autoScalingGroup:cf5ebd9c-8e2a-4197-abe2-2fb94e8d1f87:autoScalingGroupName/ASGTermFail", + "arn:aws:ec2:us-east-1:123456789012:instance/i-b188560f" + ], + "detail": { + "StatusCode": "InProgress", + "Description": "Terminating EC2 instance: i-b188560f", + "AutoScalingGroupName": "ASGTermFail", + "ActivityId": "c1a8f6ce-82e8-4517-96ba-67d1999ceee4", + "Details": { + "Availability Zone": "us-east-1e", + "Subnet ID": "subnet-915643ba" + }, + "RequestId": "c1a8f6ce-82e8-4517-96ba-67d1999ceee4", + "StatusMessage": "", + "EndTime": "1970-01-01T00:00:00.000Z", + "EC2InstanceId": "i-b188560f", + "StartTime": "1970-01-01T00:00:00.000Z", + "Cause": "At 2015-12-01T23:33:41Z a user request explicitly set group desired capacity changing the desired capacity from 2 to 0. At 2015-12-01T23:33:47Z an instance was taken out of service in response to a difference between desired and actual capacity, shrinking the capacity from 2 to 0. At 2015-12-01T23:33:47Z instance i-0867b4292c0cff474 was selected for termination. At 2015-12-01T23:33:48Z instance i-b188560f was selected for termination." + } +} diff --git a/src/data/cloudwatch/fixtures/ec2/instance-terminate.json b/src/data/cloudwatch/fixtures/ec2/instance-terminate.json new file mode 100644 index 0000000..aaa6f68 --- /dev/null +++ b/src/data/cloudwatch/fixtures/ec2/instance-terminate.json @@ -0,0 +1,19 @@ +{ + "version": "0", + "id": "468fe059-f4b7-445f-bb22-2a271b94974d", + "detail-type": "EC2 Instance-terminate Lifecycle Action", + "source": "aws.autoscaling", + "account": "123456789012", + "time": "2015-12-22T18:43:48Z", + "region": "us-east-1", + "resources": [ + "arn:aws:autoscaling:us-east-1:123456789012:autoScalingGroup:59fcbb81-bd02-485d-80ce-563ef5b237bf:autoScalingGroupName/sampleASG" + ], + "detail": { + "LifecycleActionToken": "630aa23f-48eb-45e7-aba6-799ea6093a0f", + "AutoScalingGroupName": "sampleASG", + "LifecycleHookName": "SampleLifecycleHook-6789", + "EC2InstanceId": "i-12345678", + "LifecycleTransition": "autoscaling:EC2_INSTANCE_TERMINATING" + } +} diff --git a/src/data/cloudwatch/fixtures/scheduled-events/default.json b/src/data/cloudwatch/fixtures/scheduled-events/default.json new file mode 100644 index 0000000..201e743 --- /dev/null +++ b/src/data/cloudwatch/fixtures/scheduled-events/default.json @@ -0,0 +1,11 @@ +{ + "account": "123456789012", + "detail": {}, + "detail-type": "Scheduled Event", + "id": "53dc4d37-cffa-4f76-80c9-8b7d4a4d2eaa", + "region": "us-east-1", + "resources": [ "arn:aws:events:us-east-1:123456789012:rule/MyScheduledRule" ], + "source": "aws.events", + "time": "2015-10-08T16:53:06Z", + "version": "0" +} diff --git a/src/data/cloudwatch/mod.rs b/src/data/cloudwatch/mod.rs new file mode 100644 index 0000000..a3c4597 --- /dev/null +++ b/src/data/cloudwatch/mod.rs @@ -0,0 +1,19 @@ +/// CloudWatch Event Types: https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/EventTypes.html + +use serde_json::Value; +use std::collections::BTreeMap; + +#[derive(Serialize,Deserialize)] +pub struct Event { + pub account: String, + // TODO implement concrete data types maybe using an enum as elsewhere + pub detail: BTreeMap, + #[serde(rename="detail-type")] + pub detail_type: String, + pub id: String, + pub region: String, + pub resources: Vec, + pub source: String, + pub time: String, + pub version: String, +} diff --git a/src/data/mod.rs b/src/data/mod.rs new file mode 100644 index 0000000..fb25867 --- /dev/null +++ b/src/data/mod.rs @@ -0,0 +1,65 @@ +/// Lambda Event Types +/// Defined in https://docs.aws.amazon.com/lambda/latest/dg/eventsources.html +#[cfg(test)] +mod tests; + +pub mod apicall; +pub mod apigateway; +pub mod autoscaling; +pub mod batch; +pub mod cloudwatch; +pub mod s3; +pub mod ses; +pub mod sns; + +use self::apigateway::auth; + +use chrono::prelude::*; + +use std::collections::BTreeMap; + +use serde::de::{self, Deserialize, Deserializer}; + +use serde_json::Value; +use serde_json::Map; + +#[derive(Deserialize)] +#[serde(untagged)] +pub enum Event { + CloudWatch(cloudwatch::Event), + Auth(auth::Event), + Http(apigateway::HttpEvent), + Records(Records), + Unknown(Value), +} + +#[derive(Deserialize)] +#[serde(tag="eventSource", remote="Record")] +pub enum Record { + #[serde(rename="aws:s3")] + S3(s3::Record), + #[serde(rename="aws:ses")] + Ses(ses::Record), + #[serde(rename="aws:sns")] + Sns(sns::Record), + Unknown(Value), +} + +impl<'de> Deserialize<'de> for Record { + fn deserialize(deserializer: D) -> Result + where D: Deserializer<'de> { + // due to case variance (eventSource|EventSource), we transform to regular camel case to make + // things consistent. + let mut map = Map::::deserialize(deserializer)?; + if let Some(event_source) = map.remove("EventSource") { + map.insert("eventSource".to_owned(), event_source); + } + Record::deserialize(Value::Object(map)).map_err(de::Error::custom) + } +} + +#[derive(Deserialize)] +pub struct Records { + #[serde(rename="Records")] + pub entries: Vec, +} diff --git a/src/data/s3/fixtures/delete.json b/src/data/s3/fixtures/delete.json new file mode 100644 index 0000000..e30cc73 --- /dev/null +++ b/src/data/s3/fixtures/delete.json @@ -0,0 +1,36 @@ +{ + "Records": [ + { + "eventName": "ObjectRemoved:Delete", + "eventSource": "aws:s3", + "eventTime": "1970-01-01T00:00:00.000Z", + "eventVersion": "2.0", + "requestParameters": { + "sourceIPAddress": "127.0.0.1" + }, + "s3": { + "configurationId": "testConfigRule", + "object": { + "sequencer": "0A1B2C3D4E5F678901", + "key": "HappyFace.jpg" + }, + "bucket": { + "arn": "bucketarn", + "name": "sourcebucket", + "ownerIdentity": { + "principalId": "EXAMPLE" + } + }, + "s3SchemaVersion": "1.0" + }, + "responseElements": { + "x-amz-id-2": "EXAMPLE123/5678abcdefghijklambdaisawesome/mnopqrstuvwxyzABCDEFGH", + "x-amz-request-id": "EXAMPLE123456789" + }, + "awsRegion": "us-east-1", + "userIdentity": { + "principalId": "EXAMPLE" + } + } + ] +} diff --git a/src/data/s3/fixtures/put.json b/src/data/s3/fixtures/put.json new file mode 100644 index 0000000..506506b --- /dev/null +++ b/src/data/s3/fixtures/put.json @@ -0,0 +1,38 @@ +{ + "Records": [ + { + "awsRegion": "us-east-1", + "eventName": "ObjectCreated:Put", + "eventSource": "aws:s3", + "eventTime": "1970-01-01T00:00:00.000Z", + "eventVersion": "2.0", + "requestParameters": { + "sourceIPAddress": "127.0.0.1" + }, + "responseElements": { + "x-amz-id-2": "qfz+7u72Jst1dTHevQZU1GpBaLaIxU/lcWKPpfw5v3MLtQqSH7hHOZ2Ldh7CL4Xg8V9c9LZC95k=", + "x-amz-request-id": "DC5FD023F465E6EB" + }, + "s3": { + "bucket": { + "arn": "arn:aws:s3:::s3-bucket-name", + "name": "s3-bucket-name", + "ownerIdentity": { + "principalId": "AWS_PRINCIPAL_ID" + } + }, + "configurationId": "123456789012", + "object": { + "eTag": "d8e8fca2dc0f896fd7cb4cb0031ba249", + "key": "path/something.txt", + "sequencer": "005A9B78FD700BAF25", + "size": 5 + }, + "s3SchemaVersion": "1.0" + }, + "userIdentity": { + "principalId": "AWS_PRINCIPAL_ID" + } + } + ] +} diff --git a/src/data/s3/fixtures/record.json b/src/data/s3/fixtures/record.json new file mode 100644 index 0000000..29b9e4b --- /dev/null +++ b/src/data/s3/fixtures/record.json @@ -0,0 +1,32 @@ +{ + "eventName": "ObjectRemoved:Delete", + "eventSource": "aws:s3", + "eventTime": "1970-01-01T00:00:00.000Z", + "eventVersion": "2.0", + "requestParameters": { + "sourceIPAddress": "127.0.0.1" + }, + "s3": { + "configurationId": "testConfigRule", + "object": { + "sequencer": "0A1B2C3D4E5F678901", + "key": "HappyFace.jpg" + }, + "bucket": { + "arn": "bucketarn", + "name": "sourcebucket", + "ownerIdentity": { + "principalId": "EXAMPLE" + } + }, + "s3SchemaVersion": "1.0" + }, + "responseElements": { + "x-amz-id-2": "EXAMPLE123/5678abcdefghijklambdaisawesome/mnopqrstuvwxyzABCDEFGH", + "x-amz-request-id": "EXAMPLE123456789" + }, + "awsRegion": "us-east-1", + "userIdentity": { + "principalId": "EXAMPLE" + } +} diff --git a/src/data/s3/mod.rs b/src/data/s3/mod.rs new file mode 100644 index 0000000..47f713f --- /dev/null +++ b/src/data/s3/mod.rs @@ -0,0 +1,82 @@ +#[cfg(test)] +mod tests; + +use super::*; + +use std::fmt; + +#[derive(Serialize,Deserialize)] +#[serde(rename_all="camelCase")] +pub struct Record { + pub aws_region: String, + pub event_name: ObjectEvent, + pub event_time: String, + pub event_version: String, + pub request_parameters: Option>, + pub response_elements: Option>, + #[serde(rename="s3")] + pub event: Event, + pub user_identity: Option>, +} + +#[derive(Serialize,Deserialize)] +#[serde(rename_all="camelCase")] +pub struct Event { + pub configuration_id: Option, + pub object: Object, + pub bucket: Bucket, + #[serde(rename="s3SchemaVersion")] + pub schema_version: String, +} + +#[derive(Serialize,Deserialize)] +#[serde(rename_all="camelCase")] +pub struct Bucket { + pub arn: String, + pub name: String, + pub owner_identity: BucketOwnerIdentity, +} + +#[derive(Serialize,Deserialize)] +#[serde(rename_all="camelCase")] +pub struct BucketOwnerIdentity { + pub principal_id: String, +} + +#[derive(Serialize,Deserialize)] +pub struct Object { + pub key: String, + pub sequencer: Option, +} + +#[derive(Serialize,Deserialize)] +pub enum ObjectEvent { + #[serde(rename="ObjectCreated:Put")] + Put, + #[serde(rename="ObjectCreated:Post")] + Post, + #[serde(rename="ObjectCreated:Copy")] + Copied, + #[serde(rename="ObjectCreated:CompleteMultipartUpload")] + CompleteMultipartUpload, + #[serde(rename="ObjectRemoved:Delete")] + Delete, + #[serde(rename="ObjectRemoved:DeleteMarkerCreated")] + DeleteMarkerCreated, + #[serde(rename="ReducedRedundancyLostObject")] + LostObject, +} + +impl fmt::Display for ObjectEvent { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + ObjectEvent::Put => write!(f, "s3:ObjectCreated:Put"), + ObjectEvent::Post => write!(f, "s3:ObjectCreated:Post"), + ObjectEvent::Copied => write!(f, "s3:ObjectCreated:Copy"), + ObjectEvent::CompleteMultipartUpload => write!(f, "s3:ObjectCreated:CompleteMultipartUpload"), + ObjectEvent::Delete => write!(f, "s3:ObjectRemoved:Delete"), + ObjectEvent::DeleteMarkerCreated => write!(f, "s3:ObjectRemvoed:DeleteMarkerCreated"), + ObjectEvent::LostObject => write!(f, "s3:ObjectRemoved:LostObject"), + } + } +} diff --git a/src/data/s3/tests.rs b/src/data/s3/tests.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/data/ses/fixtures/event.json b/src/data/ses/fixtures/event.json new file mode 100644 index 0000000..d9fd5e7 --- /dev/null +++ b/src/data/ses/fixtures/event.json @@ -0,0 +1,160 @@ +{ + "Records": [ + { + "eventSource": "aws:ses", + "eventVersion": "1.0", + "ses": { + "mail": { + "commonHeaders": { + "date": "Tue, 6 Mar 2018 17:57:00 -0800", + "from": [ + "Naftuli Kay " + ], + "messageId": "", + "returnPath": "me@example.com", + "subject": "Re: TESTING ONE TWO THREE", + "to": [ + "test@mail.example.com" + ] + }, + "destination": [ + "test@mail.example.com" + ], + "headers": [ + { + "name": "Return-Path", + "value": "" + }, + { + "name": "Received", + "value": "from mail-ot0-f175.google.com (mail-ot0-f175.google.com [127.0.0.1]) by inbound-smtp.us-east-1.amazonaws.com with SMTP id ci63n76f5k2lfqkj48fnvevb5v797og4n6q2c601 for test@mail.mail.example.com; Wed, 07 Mar 2018 01:57:41 +0000 (UTC)" + }, + { + "name": "X-SES-Spam-Verdict", + "value": "PASS" + }, + { + "name": "X-SES-Virus-Verdict", + "value": "PASS" + }, + { + "name": "Received-SPF", + "value": "pass (spfCheck: domain of mail.example.com designates 127.0.0.1 as permitted sender) client-ip=127.0.0.1; envelope-from=me@mail.example.com; helo=mail-ot0-f175.google.com;" + }, + { + "name": "Authentication-Results", + "value": "amazonses.com; spf=pass (spfCheck: domain of mail.example.com designates 127.0.0.1 as permitted sender) client-ip=127.0.0.1; envelope-from=me@mail.example.com; helo=mail-ot0-f175.google.com; dkim=pass header.i=@mail.example.com;" + }, + { + "name": "X-SES-RECEIPT", + "value": "AEFBQUFBQUFBQUFGMyswN3Z0eFFHcGNQR2gzTksvUytPV1pWeE5XRldYdlFBN2NJOVFCTXNkVmNhWGhKQVh0YW1URDYzVlhWdklzcytnWDdlaUZGclp2UFFhRGlLQzJXL2dPcTExUTNrNm42cmZWbk5KZjRwdEhuTHRpYnp2QWZaaUo0Zjh3aGkzNkd5bDlnWGc5VElhSCt5ZWpvZ1VvOXlUL01RS0RDakh5UXpiM2l1L0VVWFlBU0NCWHkyM3Q0N3llTW5OaXpkUDJiNTc2YVFwTTZScFJCeStyVW5sQTNhdFBieWcwOTBLcW4wTWEvTjJVc0ZrYmtFblI3N3NjR1paTmUvMlpyYzJQZ3VjaVM4UlRTNC92bEhOOFUzdjVMOCtjWVVHeDI1MGpibDMrTFR2RjdVaUE9PQ==" + }, + { + "name": "X-SES-DKIM-SIGNATURE", + "value": "a=rsa-sha256; q=dns/txt; b=HTflBGEka0ky28/N38f73LAnutB4otH1DOm3hwu4IueRhNMYGOLS1BU7KH+pEdqNtEGsFlJY6IgOIgicQB1MHjtEQ1olnJayWs380PT6d/zf1dCRzzt3yMiekk/T873uufZUfSnuP5LXV1WlJjVe/DF+aYoXm7pODOr8tUJv4og=; c=relaxed/simple; s=224i4yxa5dv7c2xz3womw6peuasteono; d=amazonses.com; t=1520387862; v=1; bh=3HTCEG39ONL/6tcCoM6yyO/xnYCq1HJfH0P3zClezYA=; h=From:To:Cc:Bcc:Subject:Date:Message-ID:MIME-Version:Content-Type:X-SES-RECEIPT;" + }, + { + "name": "Received", + "value": "by mail-ot0-f175.google.com with SMTP id t2so650813otj.4 for ; Tue, 06 Mar 2018 17:57:41 -0800 (PST)" + }, + { + "name": "DKIM-Signature", + "value": "v=1; a=rsa-sha256; c=relaxed/relaxed; d=mail.example.com; s=google; h=mime-version:in-reply-to:references:from:date:message-id:subject:to; bh=rHpVC4zgea5ZUUJjs7Y1A3lCk/5qay0/PFYapEBN0Ek=; b=Jvnfzfrq2DUAU1RBrUdRKMW9qgVQQsuWqPLdWJThHYMQj/5nXpA0+M+iLCOUyraStuPhdMzzPGbP2ljvSgiHKbz78hO8Wv2sahO0MIdg0M09LPdRskZr3Id/X/iNSW9iKl4buIU2/Wcd9RcbIkgrDfLqifFKX5LRKlsoIJXvJyP/FULZEfWrx4dw0l7dUshmldTUkK+hgdiT6op/fyPSRK9RWC2e1bhPICru4HoxxJT5FMyj9wvwXTCcYFKSS8uMyI7vsppjnGrBdiR34FTMeR0x9fC+5MfdcjIegrRTpOO4zob367dgmg5+/7Njuy1rtuUHqR77J6T9Jb0tugGKXw==" + }, + { + "name": "X-Google-DKIM-Signature", + "value": "v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:mime-version:in-reply-to:references:from:date :message-id:subject:to; bh=rHpVC4zgea5ZUUJjs7Y1A3lCk/5qay0/PFYapEBN0Ek=; b=JOmpunr4avHkkEWiznweya6DXebhBOZC7c+v9bTcsv+Z30iGMFLbaVQAgZhKemjzS5 k+qWL446pkObx8V1RYxN5Sit+AMCbPXIj92M+chJ9XSphkrntT2xz6xCo+kz8QF9B9+w XZGUCc274VLVMGoSTCO72QPxKWC02FltG9qaxX44+Tq/HZvJwGL0HWO1BcL9aSel5ew6 A9IwMBt5uYxIrGWVeCiziGwU1bJyVhq8W+032RgXwMlQsnFCflAaqwBq8g8AiRLnQp+d 02iKIhDKAWDU9g6tiBKl13vu7TJbfn3a8YCL1TPxncWoQ1K0JMSypSOQz1SpRn5BtGQj a7TQ==" + }, + { + "name": "X-Gm-Message-State", + "value": "APf1xPBkd5dMkvjxmnEgXg43SXRN5hFAwZDGrJQLChcu/5y3FFVwJH6s TLD13Dz32NfkGtVbuClIUA39mBhuRhv6LO8WTR23P5+UzTE=" + }, + { + "name": "X-Google-Smtp-Source", + "value": "AG47ELvLeGERB88fCmLVCbsP0QiHPRLt2RMdkKMh9z0TGpJ/dd9bHXzMYX6KcK1BetEketARpa+SHm0Ni2aXz1qgB7E=" + }, + { + "name": "X-Received", + "value": "by 127.0.0.1 with SMTP id z34mr15156190otc.322.1520387860730; Tue, 06 Mar 2018 17:57:40 -0800 (PST)" + }, + { + "name": "MIME-Version", + "value": "1.0" + }, + { + "name": "Received", + "value": "by 127.0.0.1 with HTTP; Tue, 6 Mar 2018 17:57:00 -0800 (PST)" + }, + { + "name": "X-Originating-IP", + "value": "[127.0.0.1]" + }, + { + "name": "In-Reply-To", + "value": "" + }, + { + "name": "References", + "value": " " + }, + { + "name": "From", + "value": "Naftuli Kay " + }, + { + "name": "Date", + "value": "Tue, 6 Mar 2018 17:57:00 -0800" + }, + { + "name": "Message-ID", + "value": "" + }, + { + "name": "Subject", + "value": "Re: TESTING ONE TWO THREE" + }, + { + "name": "To", + "value": "test@mail.mail.example.com" + }, + { + "name": "Content-Type", + "value": "multipart/signed; protocol=\"application/pkcs7-signature\"; micalg=sha-256; boundary=\"94eb2c190ade5f8df00566c8e29d\"" + } + ], + "headersTruncated": false, + "messageId": "ci63n76f5k2lfqkj48fnvevb5v797og4n6q2c601", + "source": "me@mail.example.com", + "timestamp": "2018-03-07T01:57:41.832Z" + }, + "receipt": { + "action": { + "functionArn": "arn:aws:lambda:us-east-1:123456789012:function:lambda-function-name", + "invocationType": "RequestResponse", + "type": "Lambda" + }, + "dkimVerdict": { + "status": "PASS" + }, + "dmarcVerdict": { + "status": "PASS" + }, + "processingTimeMillis": 643, + "recipients": [ + "test@mail.example.com" + ], + "spamVerdict": { + "status": "PASS" + }, + "spfVerdict": { + "status": "PASS" + }, + "timestamp": "1970-01-01T00:00:00.000Z", + "virusVerdict": { + "status": "PASS" + } + } + } + } + ] +} diff --git a/src/data/ses/fixtures/message.json b/src/data/ses/fixtures/message.json new file mode 100644 index 0000000..9a0d914 --- /dev/null +++ b/src/data/ses/fixtures/message.json @@ -0,0 +1,150 @@ +{ + "content": "Return-Path: \r\nReceived: from mail-oi0-f54.google.com (mail-oi0-f54.google.com [127.0.0.1])\r\n by inbound-smtp.us-east-1.amazonaws.com with SMTP id fo4v6ln4j4plquer9mns0ela4709rr4krs9o3fo1\r\n for test@mail.example.com;\r\n Wed, 07 Mar 2018 04:23:42 +0000 (UTC)\r\nX-SES-Spam-Verdict: PASS\r\nX-SES-Virus-Verdict: PASS\r\nReceived-SPF: pass (spfCheck: domain of naftuli.wtf designates 127.0.0.1 as permitted sender) client-ip=127.0.0.1; envelope-from=me@example.com; helo=mail-oi0-f54.google.com;\r\nAuthentication-Results: amazonses.com;\r\n spf=pass (spfCheck: domain of naftuli.wtf designates 127.0.0.1 as permitted sender) client-ip=127.0.0.1; envelope-from=me@example.com; helo=mail-oi0-f54.google.com;\r\n dkim=pass header.i=@naftuli.wtf;\r\nX-SES-RECEIPT: AEFBQUFBQUFBQUFHbE1DclRkbVFqN3RQYzk0UEpUZFI2VUlhVzAzd1lieTJRYjB6NU9qVnZyNERhTUF6ZzZvTklsbkxXN0VhMDJwOUhQT3pUUW1nbGFCTzRCY3dqMWpoNUN5STZ2bHZsSmd5M2xlb2ZIRDJOU1c2TmZ1aENYZDFZY1BhU1M5VFQ2eHFHMFo0cXhXWXpRakhVNGN3SXlaNWV5M2FiNm1mQ3NOalgrWkNHUHp3citOSzJzb0xLcmIvQTM0YjFwMkdmN3h3NkZleks5RVg0VS9hUllkUENpNjNKTFhibjNlUUNhbEVLSjZWaExRelFvakFIVWZRUkwzZkx6Y21YS0sxK0RiT2FxUXZPSytkd2VZYmduZDByQU9wdk9WUk1jR0RpNmRoS0twYkZtUUhRNVE9PQ==\r\nX-SES-DKIM-SIGNATURE: a=rsa-sha256; q=dns/txt; b=f1irw4PJbSjLfytRjUGlVUrmupVYNZo6RAGsreMi5trdeQCcOBivldx+PvyB9ResoyJmNnVGXhB4gdvYA/S9AvLzX/xVKR41P2Wu53AoCLhoo0ZVoUC5/YjYco5TAnrIlHrs1tftajBnRdsjnrvHMFE0/xws3eLQS0OCsHGmnA4=; c=relaxed/simple; s=224i4yxa5dv7c2xz3womw6peuasteono; d=amazonses.com; t=1520396623; v=1; bh=ZC64xB5KnyHpBCtKJ0jxIryzsLfc3uReH51UNNdrKII=; h=From:To:Cc:Bcc:Subject:Date:Message-ID:MIME-Version:Content-Type:X-SES-RECEIPT;\r\nReceived: by mail-oi0-f54.google.com with SMTP id g5so703446oiy.8\r\n for ; Tue, 06 Mar 2018 20:23:42 -0800 (PST)\r\nDKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;\r\n d=naftuli.wtf; s=google;\r\n h=mime-version:from:date:message-id:subject:to;\r\n bh=ZC64xB5KnyHpBCtKJ0jxIryzsLfc3uReH51UNNdrKII=;\r\n b=X5KxbFiaGrvNRX3RHJC1LHZmcxhoHI9+3/IChmbB2Kot4WO/NXGVnWyxb7UVXvCr9r\r\n 1QRMEJj5BkwluTe0RcOJaITEUCHL3w8ShH9qcmxv6FK435mqgnM/hxUdhyaUZtEb9NAz\r\n CgjIRxXox8tR3IXgf75ww8ZfiGXUSo8AnV0FeTXa4XTLhiB5kbZh0AsGj3gzyTgZP+YI\r\n YRhzdiAV6QiAHnl73+1SN89pobzZELaBromPfpZ5drSR9QP79r1mqj2CTLaChdaJQikS\r\n avAz4WIDeAsbr2TIX6q9Kr0/8Liq1rVfUtu2Hms5+nv60WnSbzt9GBA/1PNKo23QCymp\r\n KMPA==\r\nX-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;\r\n d=1e100.net; s=20161025;\r\n h=x-gm-message-state:mime-version:from:date:message-id:subject:to;\r\n bh=ZC64xB5KnyHpBCtKJ0jxIryzsLfc3uReH51UNNdrKII=;\r\n b=d04Uqmt1JZ/gCuHSSyp+Wd0EP7daEjKfDM0+TCL/p6N0e52aDaN6JfHatnWlQ4vACM\r\n H8n6YnL3TQCYpmM861MHcORxOJVKC+48q1lEz5FwDQZHcmfE48u/btD8uZWvagyUs6cf\r\n n9J1G3Nh7tAIej7mof8BtrBhZ/nP5isUfDlIMpqdf5Rw73b1hnM6gzhRJEvbMLSkFwH8\r\n r4BKLlOR2UlFC+VtEvK92AK1RulKsKy3PIye2uKQrjz1Onbpfn4h2/9OzAvYvxJ1Enmp\r\n YuKMgWOlIbcj7SUsmokofxg9u4fv3oic15maXJcB0Q8tYeA935WahSmjr8GZ8u5G8vFc\r\n XyLg==\r\nX-Gm-Message-State: AElRT7FllyLLV0KEYDxXu+mXlBV6cVRwEuQZ8m7Jtx5Nngv7T2YQNYGb\r\n\tbHuoBnVW17dtLMjMfemSE2QAM+RcbHlpC53My33EBGPVr4k=\r\nX-Google-Smtp-Source: AG47ELtctAN0wplb9TMYfqqTpSEj77eugycCNemc6IJ1xVYfi2IZiweAjE18FLvYiICsDQB0+/xQnrmZS7EZvQ/j+s8=\r\nX-Received: by 127.0.0.1 with SMTP id n185mr13705723oib.133.1520396621818;\r\n Tue, 06 Mar 2018 20:23:41 -0800 (PST)\r\nMIME-Version: 1.0\r\nReceived: by 127.0.0.1 with HTTP; Tue, 6 Mar 2018 20:23:41 -0800 (PST)\r\nX-Originating-IP: [127.0.0.1]\r\nReceived: by 127.0.0.1 with HTTP; Tue, 6 Mar 2018 20:23:41 -0800 (PST)\r\nFrom: Naftuli Kay \r\nDate: Tue, 6 Mar 2018 20:23:41 -0800\r\nMessage-ID: \r\nSubject: Block\r\nTo: test@mail.example.com\r\nContent-Type: multipart/signed; protocol=\"application/pkcs7-signature\"; micalg=sha-256;\r\n\tboundary=\"001a113d32a89234370566caec50\"\r\n\r\n--001a113d32a89234370566caec50\r\nContent-Type: multipart/alternative; boundary=\"001a113d32a88fd0090566caec36\"\r\n\r\n--001a113d32a88fd0090566caec36\r\nContent-Type: text/plain; charset=\"UTF-8\"\r\n\r\nMeme\r\n\r\n--001a113d32a88fd0090566caec36\r\nContent-Type: text/html; charset=\"UTF-8\"\r\n\r\n
Meme
\r\n\r\n--001a113d32a88fd0090566caec36--\r\n\r\n--001a113d32a89234370566caec50\r\nContent-Type: application/pkcs7-signature; name=\"smime.p7s\"\r\nContent-Transfer-Encoding: base64\r\nContent-Disposition: attachment; filename=\"smime.p7s\"\r\nContent-Description: S/MIME Cryptographic Signature\r\n\r\nMIIT3gYJKoZIhvcNAQcCoIITzzCCE8sCAQExDzANBglghkgBZQMEAgEFADALBgkqhkiG9w0BBwGg\r\nghDyMIIF5jCCA86gAwIBAgIQapvhODv/K2ufAdXZuKdSVjANBgkqhkiG9w0BAQwFADCBhTELMAkG\r\nA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEa\r\nMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNh\r\ndGlvbiBBdXRob3JpdHkwHhcNMTMwMTEwMDAwMDAwWhcNMjgwMTA5MjM1OTU5WjCBlzELMAkGA1UE\r\nBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgG\r\nA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxPTA7BgNVBAMTNENPTU9ETyBSU0EgQ2xpZW50IEF1dGhl\r\nbnRpY2F0aW9uIGFuZCBTZWN1cmUgRW1haWwgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK\r\nAoIBAQC+s55XrCh2dUAWxzgDmNPGGHYhUPMleQtMtaDRfTpYPpynMS6n9jR22YRq2tA9NEjk6vW7\r\nrN/5sYFLIP1of3l0NKZ6fLWfF2VgJ5cijKYy/qlAckY1wgOkUMgzKlWlVJGyK+UlNEQ1/5ErCsHq\r\n9x9aU/x1KwTdF/LCrT03Rl/FwFrf1XTCwa2QZYL55AqLPikFlgqOtzk06kb2qvGlnHJvijjI03BO\r\nrNpo+kZGpcHsgyO1/u1OZTaOo8wvEU17VVeP1cHWse9tGKTDyUGg2hJZjrqck39UIm/nKbpDSZ0J\r\nsMoIw/JtOOg0JC56VzQgBo7ictReTQE5LFLG3yQK+xS1AgMBAAGjggE8MIIBODAfBgNVHSMEGDAW\r\ngBS7r34CPfqm8TyEjq3uOJjs2TIy1DAdBgNVHQ4EFgQUgq9sjPjF/pZhfOgfPStxSF7Ei8AwDgYD\r\nVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAwEQYDVR0gBAowCDAGBgRVHSAAMEwGA1Ud\r\nHwRFMEMwQaA/oD2GO2h0dHA6Ly9jcmwuY29tb2RvY2EuY29tL0NPTU9ET1JTQUNlcnRpZmljYXRp\r\nb25BdXRob3JpdHkuY3JsMHEGCCsGAQUFBwEBBGUwYzA7BggrBgEFBQcwAoYvaHR0cDovL2NydC5j\r\nb21vZG9jYS5jb20vQ09NT0RPUlNBQWRkVHJ1c3RDQS5jcnQwJAYIKwYBBQUHMAGGGGh0dHA6Ly9v\r\nY3NwLmNvbW9kb2NhLmNvbTANBgkqhkiG9w0BAQwFAAOCAgEAeFyygSg0TzzuX1bOn5dW7I+iaxf2\r\n8/ZJCAbU2C81zd9A/tNx4+jsQgwRGiHjZrAYayZrrm78hOx7aEpkfNPQIHGG6Fvq3EzWf/Lvx7/h\r\nk6zSPwIal9v5IkDcZoFD7f3iT7PdkHJY9B51csvU50rxpEg1OyOT8fk2zvvPBuM4qQNqbGWlnhMp\r\nIMwpWZT89RY0wpJO+2V6eXEGGHsROs3njeP9DqqqAJaBa4wBeKOdGCWn1/Jp2oY6dyNmNppI4ZNM\r\nUH4Tam85S1j6E95u4+1Nuru84OrMIzqvISE2HN/56ebTOWlcrurffade2022O/tUU1gb4jfWCcyv\r\nB8czm12FgX/y/lRjmDbEA08QJNB2729Y+io1IYO3ztveBdvUCIYZojTq/OCR6MvnzS6X72HP0PRL\r\nRTiOSEmIDsS5N5w/8IW1Hva5hEFy6fDAfd9yI+O+IMMAj1KcL/Zo9jzJ16HO5m60ttl1Enk8MQkz\r\n/W3JlHaeI5iKFn4UJu1/cP2YHXYPiWf2JyBzsLBrGk1II+3yL8aorYew6CQvdVifC3HtwlSam9V1\r\nniiCfOBe2C12TdKGu05LWIA3ZkFcWJGaNXOZ6Ggyh/TqvXG5v7zmEVDNXFnHn9tFpMpOUvxhcsjy\r\ncBtH0dZ0WrNw6gH+HF8TIhCnH3+zzWuDN0Rk6h9KVkfKehIwggXYMIIDwKADAgECAhBMqvnK22Nv\r\n4B/3TthbA4adMA0GCSqGSIb3DQEBDAUAMIGFMQswCQYDVQQGEwJHQjEbMBkGA1UECBMSR3JlYXRl\r\nciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFDT01PRE8gQ0EgTGltaXRl\r\nZDErMCkGA1UEAxMiQ09NT0RPIFJTQSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0xMDAxMTkw\r\nMDAwMDBaFw0zODAxMTgyMzU5NTlaMIGFMQswCQYDVQQGEwJHQjEbMBkGA1UECBMSR3JlYXRlciBN\r\nYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFDT01PRE8gQ0EgTGltaXRlZDEr\r\nMCkGA1UEAxMiQ09NT0RPIFJTQSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCAiIwDQYJKoZIhvcN\r\nAQEBBQADggIPADCCAgoCggIBAJHoVJLSClaxrA0k3cXPRGd0mSs3o30jcABxvFPfxPoqEo9LfxBW\r\nvZ9wcrdhf8lLDxenPeOwBGHu/xGXx/SGPgr6Plz5k+Y0etkUa+ecs4Wggnp2r3GQ1+z9DfqcbPrf\r\nsIL0FH75vsSmL09/mX+1/GdDcr0MANaJ62ss0+2PmBwUq37l42782KjkkiTaQ2tiuFX96sG8bLaL\r\n8w6NmuSbbGmZ+HhIMEXVreENPEVg/DKWUSe8Z8PKLrZr6kbHxyCgsR9l3kgIuqROqfKDRjeE6+jM\r\ngUhDZ05yKptcvUwbKIpcInu0q5jZ7uBRg8MJRk5tPpn6lRfafDNXQTyNUe0LtlyvLGMa31fIP7zp\r\nXcSbr0WZ4qNaJLS6qVY9z2+q/0lYvvCo//S4rek3+7q49As6+ehDQh6J2ITLE/HZu+GJYLiMKFas\r\nFB2cCudx688O3T2plqFIvTz3r7UNIkzAEYHsVjv206LiW7eyBCJSlYCTaeiOTGXxkQMtcHQC6otn\r\nFSlpUgK7199QalVGv6CjKGF/cNDDoqosIapHziicBkV2v4IYJ7TVrrTLUOZr9EyGcTDppt8WhuDY\r\n/0Dd+9BCiH+jMzouXB5BEYFjzhhxayvspoq3MVw6akfgw3lZ1iAar/JqmKpyvFdK0kuduxD8sExB\r\n5e0dPV4onZzMv7NR2qdH5YRTAgMBAAGjQjBAMB0GA1UdDgQWBBS7r34CPfqm8TyEjq3uOJjs2TIy\r\n1DAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQwFAAOCAgEACvHV\r\nRoS3rlG7bLJNQRQAk0ycy+XAVM+gJY4C+f2wog31IJg8Ey2sVqKw1n4Rkukuup4umnKxvRlEbGE1\r\nopq0FhJpWozh1z6kGugvA/SuYR0QGyqki3rF/gWm4cDWyP6ero8ruj2Z+NhzCVhGbqac9Ncn05Xa\r\nN4NyHNNz4KJHmQM4XdVJeQApHMfsmyAcByRpV3iyOfw6hKC1nHyNvy6TYie3OdoXGK69PAlo/4Sb\r\nPNXWCwPjV54U99HrT8i9hyO3tklDeYVcuuuSC6HG6GioTBaxGpkK6FMskruhCRh1DGWoe8sjtxrC\r\nKIXDG//QK2LvpHsJkZhnjBQBzWgGamMhdQOAiIpugcaF8qmkLef0pSQQR4PKzfSNeVixBpvnGirZ\r\nnQHXlH3tA0rK8NvoqQE+9VaZyR6OST275Qm54E9Jkj0WgkDMzFnG5jrtEi5pPGyVsf2qHXt/hr4e\r\nDjJG+/sTj3V/TItLRmP+ADRAcMHDuaHdpnDiBLNBvOmAkepknHrhIgOpnG5vDmVPbIeHXvNuoPl1\r\npZtA6FOyJ51KucB3IY3/h/LevIzvF9+3SQvR8m4wCxoOTnbtEfz16Vayfb/HbQqTjKXQwLYdvjpO\r\nlKLXbmwLwop8+iDzxOTlzQ2oy5GSsXyF7LUUaWYOgufNzsgtplF/IcE1U4UGSl2frbsbX3QwggUo\r\nMIIEEKADAgECAhEAyiacgkaDJ4l6QWBDiclWHDANBgkqhkiG9w0BAQsFADCBlzELMAkGA1UEBhMC\r\nR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UE\r\nChMRQ09NT0RPIENBIExpbWl0ZWQxPTA7BgNVBAMTNENPTU9ETyBSU0EgQ2xpZW50IEF1dGhlbnRp\r\nY2F0aW9uIGFuZCBTZWN1cmUgRW1haWwgQ0EwHhcNMTcxMjI0MDAwMDAwWhcNMTgxMjI0MjM1OTU5\r\nWjAfMR0wGwYJKoZIhvcNAQkBFg5tZUBuYWZ0dWxpLnd0ZjCCASIwDQYJKoZIhvcNAQEBBQADggEP\r\nADCCAQoCggEBALWuRKlz/25qkH09RyvbvYcXOpr3EVIopWZeQJ3sohDNL3PFGwaRgPFtrFf+bfWO\r\nS7DWMgMO4Y/nFWArxp3FL8oF2gjagTzFMUCFV3m6OUYBfUhfAsxZ479GBz19zCBZBG7vDkFWvGGc\r\ncU6Tk8e3Tt9ViIeQKRqZ9uoAOoFB1eww1L6DMXBeNvP7sPkSrwjGI28F2jwagBKphrv2Q9ivjqRF\r\nd7G28HvB58JN8tmpgUDHsVOEtiAgQm4ICQZ0bTXusmV4TSGauS+vSemZm/d6vhQw3MBWLxdhpGSp\r\nrcy2Hqxg/trk/v4beTjxL6PrOQQR1J3dq1ZoMEd/LzN5ddKcuNkCAwEAAaOCAeQwggHgMB8GA1Ud\r\nIwQYMBaAFIKvbIz4xf6WYXzoHz0rcUhexIvAMB0GA1UdDgQWBBRcGmu64Vwi0F4AKJ7ExAXXeO7Y\r\naDAOBgNVHQ8BAf8EBAMCBaAwDAYDVR0TAQH/BAIwADAgBgNVHSUEGTAXBggrBgEFBQcDBAYLKwYB\r\nBAGyMQEDBQIwEQYJYIZIAYb4QgEBBAQDAgUgMEYGA1UdIAQ/MD0wOwYMKwYBBAGyMQECAQEBMCsw\r\nKQYIKwYBBQUHAgEWHWh0dHBzOi8vc2VjdXJlLmNvbW9kby5uZXQvQ1BTMFoGA1UdHwRTMFEwT6BN\r\noEuGSWh0dHA6Ly9jcmwuY29tb2RvY2EuY29tL0NPTU9ET1JTQUNsaWVudEF1dGhlbnRpY2F0aW9u\r\nYW5kU2VjdXJlRW1haWxDQS5jcmwwgYsGCCsGAQUFBwEBBH8wfTBVBggrBgEFBQcwAoZJaHR0cDov\r\nL2NydC5jb21vZG9jYS5jb20vQ09NT0RPUlNBQ2xpZW50QXV0aGVudGljYXRpb25hbmRTZWN1cmVF\r\nbWFpbENBLmNydDAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuY29tb2RvY2EuY29tMBkGA1UdEQQS\r\nMBCBDm1lQG5hZnR1bGkud3RmMA0GCSqGSIb3DQEBCwUAA4IBAQB1wfCQqazZv7x2ZycgNZe6lh79\r\nS29zmREf03Lb9ezAap4pkOwaPJqouPrw5XTXSg2rTkAUfsQvXfD3Alalf+bJ2PM9yo2IsYftf9H9\r\n8/PkKLZPci/KttklN4NKe6E2O7bdnLm5uJxMRjS9RIPKgKnIUK54KWLdt6aDNWgJultp5uKczx/n\r\nyhWsobbZFP26bKgT/mEaEnDtO4ykA8Naac8ndgA8SQAwyKnGC1w8xI11hBED9shphZ3L/8QYp+df\r\nWgFdIbHUoTWjbVBBW48eYwo7QIAW3yQzGoRwVsLSG/SFzijJphHUdrIk8/9ph4HGGT9zb++/rkyy\r\nsgHkl2RBz/5UMYICsDCCAqwCAQEwga0wgZcxCzAJBgNVBAYTAkdCMRswGQYDVQQIExJHcmVhdGVy\r\nIE1hbmNoZXN0ZXIxEDAOBgNVBAcTB1NhbGZvcmQxGjAYBgNVBAoTEUNPTU9ETyBDQSBMaW1pdGVk\r\nMT0wOwYDVQQDEzRDT01PRE8gUlNBIENsaWVudCBBdXRoZW50aWNhdGlvbiBhbmQgU2VjdXJlIEVt\r\nYWlsIENBAhEAyiacgkaDJ4l6QWBDiclWHDANBglghkgBZQMEAgEFAKCB1DAvBgkqhkiG9w0BCQQx\r\nIgQgRh2OOZwQVAExwYsMSbkRbprSgIIaS2fOEicem+GIn7kwGAYJKoZIhvcNAQkDMQsGCSqGSIb3\r\nDQEHATAcBgkqhkiG9w0BCQUxDxcNMTgwMzA3MDQyMzQxWjBpBgkqhkiG9w0BCQ8xXDBaMAsGCWCG\r\nSAFlAwQBKjALBglghkgBZQMEARYwCwYJYIZIAWUDBAECMAoGCCqGSIb3DQMHMAsGCSqGSIb3DQEB\r\nCjALBgkqhkiG9w0BAQcwCwYJYIZIAWUDBAIBMA0GCSqGSIb3DQEBAQUABIIBAAVy0rbaqFHHFDY4\r\n8eaRdsfqTKHIjECiZ1+4D59cDyGTbbH2p2uXWzp58+J6E+QysM5TMFBI1bIErUpABOyLJL18TXYU\r\nx6jSJFc+uSSsGnHMib9CuwiiVNnQEFX3rVC68mBRgOzlJ9WJLNt8wXJS1vu/MHZzluQtwg9btIIf\r\nW6H9wZ1PCyKaUrnJZyoSpLWgYrczAssFXHM+cJBNEMpblSMC0vENTS0ScY/xsJkoQNwft2TVZOOz\r\nmGJMvvYw7cN/RqPGzskKzTf0TPmtprkyeCZa5lShrj6ujRd8yJVRKRSno88dSXAuAbLHSVHBlYxR\r\nD06pyifR/nTEGUgOtXMYiw0=\r\n--001a113d32a89234370566caec50--\r\n", + "mail": { + "commonHeaders": { + "date": "Tue, 6 Mar 2018 20:23:41 -0800", + "from": [ + "Naftuli Kay " + ], + "messageId": "", + "returnPath": "me@example.com", + "subject": "Block", + "to": [ + "test@mail.example.com" + ] + }, + "destination": [ + "test@mail.example.com" + ], + "headers": [ + { + "name": "Return-Path", + "value": "" + }, + { + "name": "Received", + "value": "from mail-oi0-f54.google.com (mail-oi0-f54.google.com [127.0.0.1]) by inbound-smtp.us-east-1.amazonaws.com with SMTP id fo4v6ln4j4plquer9mns0ela4709rr4krs9o3fo1 for test@mail.example.com; Wed, 07 Mar 2018 04:23:42 +0000 (UTC)" + }, + { + "name": "X-SES-Spam-Verdict", + "value": "PASS" + }, + { + "name": "X-SES-Virus-Verdict", + "value": "PASS" + }, + { + "name": "Received-SPF", + "value": "pass (spfCheck: domain of naftuli.wtf designates 127.0.0.1 as permitted sender) client-ip=127.0.0.1; envelope-from=me@example.com; helo=mail-oi0-f54.google.com;" + }, + { + "name": "Authentication-Results", + "value": "amazonses.com; spf=pass (spfCheck: domain of naftuli.wtf designates 127.0.0.1 as permitted sender) client-ip=127.0.0.1; envelope-from=me@example.com; helo=mail-oi0-f54.google.com; dkim=pass header.i=@naftuli.wtf;" + }, + { + "name": "X-SES-RECEIPT", + "value": "AEFBQUFBQUFBQUFHbE1DclRkbVFqN3RQYzk0UEpUZFI2VUlhVzAzd1lieTJRYjB6NU9qVnZyNERhTUF6ZzZvTklsbkxXN0VhMDJwOUhQT3pUUW1nbGFCTzRCY3dqMWpoNUN5STZ2bHZsSmd5M2xlb2ZIRDJOU1c2TmZ1aENYZDFZY1BhU1M5VFQ2eHFHMFo0cXhXWXpRakhVNGN3SXlaNWV5M2FiNm1mQ3NOalgrWkNHUHp3citOSzJzb0xLcmIvQTM0YjFwMkdmN3h3NkZleks5RVg0VS9hUllkUENpNjNKTFhibjNlUUNhbEVLSjZWaExRelFvakFIVWZRUkwzZkx6Y21YS0sxK0RiT2FxUXZPSytkd2VZYmduZDByQU9wdk9WUk1jR0RpNmRoS0twYkZtUUhRNVE9PQ==" + }, + { + "name": "X-SES-DKIM-SIGNATURE", + "value": "a=rsa-sha256; q=dns/txt; b=f1irw4PJbSjLfytRjUGlVUrmupVYNZo6RAGsreMi5trdeQCcOBivldx+PvyB9ResoyJmNnVGXhB4gdvYA/S9AvLzX/xVKR41P2Wu53AoCLhoo0ZVoUC5/YjYco5TAnrIlHrs1tftajBnRdsjnrvHMFE0/xws3eLQS0OCsHGmnA4=; c=relaxed/simple; s=224i4yxa5dv7c2xz3womw6peuasteono; d=amazonses.com; t=1520396623; v=1; bh=ZC64xB5KnyHpBCtKJ0jxIryzsLfc3uReH51UNNdrKII=; h=From:To:Cc:Bcc:Subject:Date:Message-ID:MIME-Version:Content-Type:X-SES-RECEIPT;" + }, + { + "name": "Received", + "value": "by mail-oi0-f54.google.com with SMTP id g5so703446oiy.8 for ; Tue, 06 Mar 2018 20:23:42 -0800 (PST)" + }, + { + "name": "DKIM-Signature", + "value": "v=1; a=rsa-sha256; c=relaxed/relaxed; d=naftuli.wtf; s=google; h=mime-version:from:date:message-id:subject:to; bh=ZC64xB5KnyHpBCtKJ0jxIryzsLfc3uReH51UNNdrKII=; b=X5KxbFiaGrvNRX3RHJC1LHZmcxhoHI9+3/IChmbB2Kot4WO/NXGVnWyxb7UVXvCr9r1QRMEJj5BkwluTe0RcOJaITEUCHL3w8ShH9qcmxv6FK435mqgnM/hxUdhyaUZtEb9NAzCgjIRxXox8tR3IXgf75ww8ZfiGXUSo8AnV0FeTXa4XTLhiB5kbZh0AsGj3gzyTgZP+YIYRhzdiAV6QiAHnl73+1SN89pobzZELaBromPfpZ5drSR9QP79r1mqj2CTLaChdaJQikSavAz4WIDeAsbr2TIX6q9Kr0/8Liq1rVfUtu2Hms5+nv60WnSbzt9GBA/1PNKo23QCympKMPA==" + }, + { + "name": "X-Google-DKIM-Signature", + "value": "v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:mime-version:from:date:message-id:subject:to; bh=ZC64xB5KnyHpBCtKJ0jxIryzsLfc3uReH51UNNdrKII=; b=d04Uqmt1JZ/gCuHSSyp+Wd0EP7daEjKfDM0+TCL/p6N0e52aDaN6JfHatnWlQ4vACM H8n6YnL3TQCYpmM861MHcORxOJVKC+48q1lEz5FwDQZHcmfE48u/btD8uZWvagyUs6cf n9J1G3Nh7tAIej7mof8BtrBhZ/nP5isUfDlIMpqdf5Rw73b1hnM6gzhRJEvbMLSkFwH8 r4BKLlOR2UlFC+VtEvK92AK1RulKsKy3PIye2uKQrjz1Onbpfn4h2/9OzAvYvxJ1Enmp YuKMgWOlIbcj7SUsmokofxg9u4fv3oic15maXJcB0Q8tYeA935WahSmjr8GZ8u5G8vFc XyLg==" + }, + { + "name": "X-Gm-Message-State", + "value": "AElRT7FllyLLV0KEYDxXu+mXlBV6cVRwEuQZ8m7Jtx5Nngv7T2YQNYGb bHuoBnVW17dtLMjMfemSE2QAM+RcbHlpC53My33EBGPVr4k=" + }, + { + "name": "X-Google-Smtp-Source", + "value": "AG47ELtctAN0wplb9TMYfqqTpSEj77eugycCNemc6IJ1xVYfi2IZiweAjE18FLvYiICsDQB0+/xQnrmZS7EZvQ/j+s8=" + }, + { + "name": "X-Received", + "value": "by 127.0.0.1 with SMTP id n185mr13705723oib.133.1520396621818; Tue, 06 Mar 2018 20:23:41 -0800 (PST)" + }, + { + "name": "MIME-Version", + "value": "1.0" + }, + { + "name": "Received", + "value": "by 127.0.0.1 with HTTP; Tue, 6 Mar 2018 20:23:41 -0800 (PST)" + }, + { + "name": "X-Originating-IP", + "value": "[127.0.0.1]" + }, + { + "name": "Received", + "value": "by 127.0.0.1 with HTTP; Tue, 6 Mar 2018 20:23:41 -0800 (PST)" + }, + { + "name": "From", + "value": "Naftuli Kay " + }, + { + "name": "Date", + "value": "Tue, 6 Mar 2018 20:23:41 -0800" + }, + { + "name": "Message-ID", + "value": "" + }, + { + "name": "Subject", + "value": "Block" + }, + { + "name": "To", + "value": "test@mail.example.com" + }, + { + "name": "Content-Type", + "value": "multipart/signed; protocol=\"application/pkcs7-signature\"; micalg=sha-256; boundary=\"001a113d32a89234370566caec50\"" + } + ], + "headersTruncated": false, + "messageId": "fo4v6ln4j4plquer9mns0ela4709rr4krs9o3fo1", + "source": "me@example.com", + "timestamp": "1970-01-01T00:00:00.000Z" + }, + "notificationType": "Received", + "receipt": { + "action": { + "encoding": "UTF8", + "topicArn": "arn:aws:sns:us-east-1:123456789012:lambda-function", + "type": "SNS" + }, + "dkimVerdict": { + "status": "PASS" + }, + "dmarcVerdict": { + "status": "PASS" + }, + "processingTimeMillis": 505, + "recipients": [ + "test@mail.example.com" + ], + "spamVerdict": { + "status": "PASS" + }, + "spfVerdict": { + "status": "PASS" + }, + "timestamp": "1970-01-01T00:00:00.000Z", + "virusVerdict": { + "status": "PASS" + } + } +} diff --git a/src/data/ses/fixtures/requestResponse.json b/src/data/ses/fixtures/requestResponse.json new file mode 100644 index 0000000..b81a195 --- /dev/null +++ b/src/data/ses/fixtures/requestResponse.json @@ -0,0 +1,152 @@ +{ + "Records": [ + { + "eventSource": "aws:ses", + "eventVersion": "1.0", + "ses": { + "mail": { + "commonHeaders": { + "date": "Tue, 6 Mar 2018 12:09:50 -0800", + "from": [ + "Naftuli Kay " + ], + "messageId": "", + "returnPath": "me@example.com", + "subject": "TESTING ONE TWO THREE", + "to": [ + "test@mail.example.com" + ] + }, + "destination": [ + "test@mail.example.com" + ], + "headers": [ + { + "name": "Return-Path", + "value": "" + }, + { + "name": "Received", + "value": "from mail-oi0-f42.google.com (mail-oi0-f42.google.com [127.0.0.1]) by inbound-smtp.us-east-1.amazonaws.com with SMTP id qg1eqaumt6vptod2mcv6e686d24t99s38ngn6j81 for test@mail.example.com; Tue, 06 Mar 2018 20:10:32 +0000 (UTC)" + }, + { + "name": "X-SES-Spam-Verdict", + "value": "PASS" + }, + { + "name": "X-SES-Virus-Verdict", + "value": "PASS" + }, + { + "name": "Received-SPF", + "value": "pass (spfCheck: domain of naftuli.wtf designates 127.0.0.1 as permitted sender) client-ip=127.0.0.1; envelope-from=me@example.com; helo=mail-oi0-f42.google.com;" + }, + { + "name": "Authentication-Results", + "value": "amazonses.com; spf=pass (spfCheck: domain of naftuli.wtf designates 127.0.0.1 as permitted sender) client-ip=127.0.0.1; envelope-from=me@example.com; helo=mail-oi0-f42.google.com; dkim=pass header.i=@naftuli.wtf;" + }, + { + "name": "X-SES-RECEIPT", + "value": "AEFBQUFBQUFBQUFFU2JPZWwvaXJFZUs1dGhoeHJzangvY25rdUZjeXNUaW11QU5vU0ZjTEUwZDUxcmtEQXg1TTY1SzJ4Vmp0SG9zNXdERG9udjkweEZxVzhXenVLOXdJSXZZTnY2OGI4T29yd0d6MDhxaVRQdWJhTmdsOXBsVU15Zm51NFdOV0Qxa1dUbTl1QXhWdVFJN2pUZmUyMGt5WUJMaWNYaUpIM0xDSkUyK1EzWTJ6YUpRRVJjNHRhR3lHZUE5S084UzZNWGdrSDhKNFVieExnWXNXK2JxNmJBclZ1Q0JlOWl4dVlmbE1WTk1IbG5EQkE1OVdjUk1MYVV4cENwTDMyclhLYXZoQUxHUUROR3dSR2lkY3hqZFJERjVrK2xhRzVjVWNwczFYVStYeTJaYXE1QkE9PQ==" + }, + { + "name": "X-SES-DKIM-SIGNATURE", + "value": "a=rsa-sha256; q=dns/txt; b=dWF6waH/Vr+9C203YuKj/ekkehl1/uaxJq+KaMpjLYPlo2LrzZIGfHwLEOWCrXvnErkx/N3wGpaH7DHAEAyLgKyX5/RVCCsp/2iXMqE0ZA5WMfpqPQcLYBdf8UDMLRb5KLkMr8sXPLBzwLIyeWvvsqEmMmijVxeNnx5/+3/EFP8=; c=relaxed/simple; s=224i4yxa5dv7c2xz3womw6peuasteono; d=amazonses.com; t=1520367032; v=1; bh=i9U9ahNdtL37cW2lpNW/mhh3GVUn91NKRCKiyEqWyxk=; h=From:To:Cc:Bcc:Subject:Date:Message-ID:MIME-Version:Content-Type:X-SES-RECEIPT;" + }, + { + "name": "Received", + "value": "by mail-oi0-f42.google.com with SMTP id x12so623847oie.13 for ; Tue, 06 Mar 2018 12:10:31 -0800 (PST)" + }, + { + "name": "DKIM-Signature", + "value": "v=1; a=rsa-sha256; c=relaxed/relaxed; d=naftuli.wtf; s=google; h=mime-version:from:date:message-id:subject:to; bh=i9U9ahNdtL37cW2lpNW/mhh3GVUn91NKRCKiyEqWyxk=; b=Zr+3BtIHVKCC6MdBySfouWh+iXDgz3/x9EsDe3Qf2Hnx26MhVWHJ1XSwusQ0kuczM9uHsaJTQQffA1MRpzN4QltC+eEXwLVGNtBgCv00DQou+1eQ2LOKjWbCTvJ6IKXaHjHr9ZYcJOFtyqI6cbuvvG5gRoyOspb4CLujzgd836OAAxF/izQkdNiC80IPoJPIC+3cIY14K71i3cM8aHs4iKRLGzgeuIyLBYvwgzU8dGZFiWtKlHsZPxO78BmxEpSZ6z/EcsWw9lieuRRzovbflcngvAN0IPwiGctbbF5gJ3fh/V6fRMv3k7hnkGXEIOMUfQ/twbvu3gbdVElM0+5b0Q==" + }, + { + "name": "X-Google-DKIM-Signature", + "value": "v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:mime-version:from:date:message-id:subject:to; bh=i9U9ahNdtL37cW2lpNW/mhh3GVUn91NKRCKiyEqWyxk=; b=i+RsiW779nt3mVg5di1laCWasOxQ3jBIRy9VMOwOus5VtSLc4Z/qlCaytkaBJZMvrN iVhIA5wFgA53KvBaeqJo5ijviR+vEJQwoWe8/PLcVmh+UnihXMFF0swIPs1KiDbz1vFJ TiAVnRbWWW0NgA30V1/vBDQUAT7O94Wz7ztAQtpghqYR6z0CHSNVQGs7vLAODnsmYP7z n4to9m9Y5HXYE69xodWRxDIs3VXkc6FO9ZIyLIowX6ypDUAKkB2e7JhZ53xknu6qFGbt TMLMPMsXSWlJDsFTQas2n1CiispZ7BGHsgAPWNCKuqcvFMYsbThTdpMknda+72JTIHN6 K8SA==" + }, + { + "name": "X-Gm-Message-State", + "value": "AElRT7HhlA1ORsaYwOOoLrI6sZcLaJVPGf/f5Su+XHoEonGaQavTXX0q Qe5UiIOfcufJHtlAxOXz0YG0Cx3xbXqZx5g91NkbyyVCvLo=" + }, + { + "name": "X-Google-Smtp-Source", + "value": "AG47ELtzYdVq2GXmgc3uhqRWaVn2TT36qrZ4cH6hS1RW3+vnDnBP+3ERMpUsp+h+hEnhPKj+C3r6HrNi7nWZLeXo6co=" + }, + { + "name": "X-Received", + "value": "by 127.0.0.1 with SMTP id n185mr12950103oib.133.1520367030800; Tue, 06 Mar 2018 12:10:30 -0800 (PST)" + }, + { + "name": "MIME-Version", + "value": "1.0" + }, + { + "name": "Received", + "value": "by 127.0.0.1 with HTTP; Tue, 6 Mar 2018 12:09:50 -0800 (PST)" + }, + { + "name": "X-Originating-IP", + "value": "[127.0.0.1]" + }, + { + "name": "From", + "value": "Naftuli Kay " + }, + { + "name": "Date", + "value": "Tue, 6 Mar 2018 12:09:50 -0800" + }, + { + "name": "Message-ID", + "value": "" + }, + { + "name": "Subject", + "value": "TESTING ONE TWO THREE" + }, + { + "name": "To", + "value": "test@mail.example.com" + }, + { + "name": "Content-Type", + "value": "multipart/signed; protocol=\"application/pkcs7-signature\"; micalg=sha-256; boundary=\"001a113d32a8d18ff30566c40836\"" + } + ], + "headersTruncated": false, + "messageId": "qg1eqaumt6vptod2mcv6e686d24t99s38ngn6j81", + "source": "me@example.com", + "timestamp": "1970-01-01T00:00:00.000Z" + }, + "receipt": { + "action": { + "functionArn": "arn:aws:lambda:us-east-1:123456789012:function:lambda-function", + "invocationType": "RequestResponse", + "type": "Lambda" + }, + "dkimVerdict": { + "status": "PASS" + }, + "dmarcVerdict": { + "status": "PASS" + }, + "processingTimeMillis": 524, + "recipients": [ + "test@mail.example.com" + ], + "spamVerdict": { + "status": "PASS" + }, + "spfVerdict": { + "status": "PASS" + }, + "timestamp": "1970-01-01T00:00:00.000Z", + "virusVerdict": { + "status": "PASS" + } + } + } + } + ] +} diff --git a/src/data/ses/message.rs b/src/data/ses/message.rs new file mode 100644 index 0000000..f497f47 --- /dev/null +++ b/src/data/ses/message.rs @@ -0,0 +1,157 @@ +use super::*; + +#[derive(Serialize,Deserialize)] +#[serde(rename_all="camelCase")] +pub struct Message { + pub content: String, + #[serde(rename="mail")] + pub details: Details, + pub notification_type: NotificationType, + pub receipt: Receipt, +} + +#[derive(Serialize,Deserialize)] +pub enum NotificationType { + Received, +} + +#[derive(Serialize,Deserialize)] +#[serde(rename_all="camelCase")] +pub struct Details { + pub destination: Vec, + pub headers_truncated: bool, + pub message_id: String, + pub source: String, + pub timestamp: DateTime, + pub headers: Vec
, + pub common_headers: CommonHeaders, +} + +#[derive(Serialize,Deserialize)] +pub struct Header { + pub name: String, + pub value: String, +} + +#[derive(Serialize,Deserialize)] +#[serde(rename_all="camelCase")] +pub struct CommonHeaders { + // format: %a, %d %b %Y %H:%M:%S %ze + pub date: String, + pub from: Vec, + pub message_id: String, + pub return_path: String, + pub subject: String, + pub to: Vec, +} + +#[derive(Serialize,Deserialize)] +pub enum ActionType { + Bounce, + Lambda, + S3, + #[serde(rename="SNS")] + Sns, + Stop, + WorkMail, +} + +#[derive(Serialize,Deserialize)] +#[serde(rename_all="camelCase")] +pub struct Receipt { + pub action: Action, + pub dkim_verdict: DkimVerdict, + pub dmarc_policy: Option, + pub dmarc_verdict: DmarcVerdict, + pub processing_time_millis: u64, + pub recipients: Vec, + pub spam_verdict: SpamVerdict, + pub spf_verdict: SpfVerdict, + pub timestamp: DateTime, + pub virus_verdict: VirusVerdict, +} + +#[derive(Serialize,Deserialize)] +#[serde(tag="status")] +pub enum Verdict { + #[serde(rename="FAIL")] + Fail, + #[serde(rename="GRAY")] + Gray, + #[serde(rename="PASS")] + Pass, + #[serde(rename="PROCESSING_FAILED")] + ProcessingFailed, +} + +pub type DkimVerdict = Verdict; +pub type DmarcVerdict = Verdict; +pub type SpamVerdict = Verdict; +pub type SpfVerdict = Verdict; +pub type VirusVerdict = Verdict; + +#[derive(Serialize,Deserialize)] +pub enum DmarcPolicy { + #[serde(rename="NONE")] + Absent, + #[serde(rename="QUARANTINE")] + Quarantine, + #[serde(rename="REJECT")] + Reject, +} + +#[derive(Serialize,Deserialize)] +#[serde(tag="type")] +pub enum Action { + Lambda(LambdaAction), + #[serde(rename="SNS")] + Sns(SnsAction), + S3(S3Action), + Bounce(BounceAction), + Stop(StopAction), + WorkMail(WorkMailAction), +} + +#[derive(Serialize,Deserialize)] +#[serde(rename_all="camelCase")] +pub struct LambdaAction { + pub function_arn: Option, + pub invocation_type: LambdaInvocationType, +} + +#[derive(Eq,PartialEq,Serialize,Deserialize)] +pub enum LambdaInvocationType { + Event, + RequestResponse, +} + +#[derive(Serialize,Deserialize)] +#[serde(rename_all="camelCase")] +pub struct SnsAction { + pub topic_arn: String, +} + +#[derive(Serialize,Deserialize)] +#[serde(rename_all="camelCase")] +pub struct S3Action { + pub bucket_name: String, + pub object_key: String, +} + +#[derive(Serialize,Deserialize)] +#[serde(rename_all="camelCase")] +pub struct BounceAction { + pub smtp_reply_code: String, + pub status_code: String, + pub message: String, + pub sender: String, +} + +#[derive(Serialize,Deserialize)] +pub struct StopAction {} + +#[derive(Serialize,Deserialize)] +#[serde(rename_all="camelCase")] +pub struct WorkMailAction { + pub organization_arn: String, +} diff --git a/src/data/ses/mod.rs b/src/data/ses/mod.rs new file mode 100644 index 0000000..1c696b7 --- /dev/null +++ b/src/data/ses/mod.rs @@ -0,0 +1,62 @@ +/// Data mapped from: https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-notifications-contents.html +#[cfg(test)] +mod tests; + +pub mod message; + +use super::*; + +pub use self::message::Action; +pub use self::message::LambdaInvocationType; +pub use self::message::Message; + +#[derive(Serialize,Deserialize)] +#[serde(rename_all="camelCase")] +pub struct Record { + pub event_version: String, + #[serde(rename="ses")] + pub event: Event, +} + +#[derive(Serialize,Deserialize)] +#[serde(rename_all="camelCase")] +pub struct Event { + #[serde(rename="mail")] + pub details: message::Details, + pub receipt: message::Receipt, +} + +#[derive(Serialize,Deserialize)] +pub struct Response { + #[serde(rename="disposition")] + pub action: ResponseAction, +} + +#[derive(Serialize,Deserialize)] +pub enum ResponseAction { + #[serde(rename="CONTINUE")] + Continue, + #[serde(rename="STOP_RULE")] + StopRule, + #[serde(rename="STOP_RULE_SET")] + StopRuleSet, +} + +impl Response { + + pub fn from(action: ResponseAction) -> Self { + Response { action } + } + + pub fn proceed() -> Self { + Response::from(ResponseAction::Continue) + } + + pub fn stop_rule() -> Self { + Response::from(ResponseAction::StopRule) + } + + pub fn stop_rule_set() -> Self { + Response::from(ResponseAction::StopRuleSet) + } +} diff --git a/src/data/ses/tests.rs b/src/data/ses/tests.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/data/sns/fixtures/actual.json b/src/data/sns/fixtures/actual.json new file mode 100644 index 0000000..6f01d83 --- /dev/null +++ b/src/data/sns/fixtures/actual.json @@ -0,0 +1,22 @@ +{ + "Records": [ + { + "EventSource": "aws:sns", + "EventSubscriptionArn": "arn:aws:sns:us-east-1:961179389914:api-naftuli-wtf:6fbc6fe8-a76a-4377-8fbb-3f8cd8e82959", + "EventVersion": "1.0", + "Sns": { + "Message": "{\"test\": true}", + "MessageAttributes": {}, + "MessageId": "26c8653c-9552-5f96-95fa-3629e96eb125", + "Signature": "Z732pFicUr2YIc2uKaq8ynD0JeVTwm0tNQOnhXT2qHNtJD+OvpjJ0j4dZsSXKA1ElG3vOE/Isqp1cC2MZ9wii55E7Zvpx1avU9n46TGKpEnjeEblSqU2q+dwcoeyULgURuTF5lwRR7jJYoWBOF/1RdrF53CMWha43vvNhs/S6vnNkdvQKNCGE6+YmHu11FUuw43MUM8DCH5Fqrneq/I6al6ZaMLfotjvMlqkkEbfAXPvyR6WQl+Dz3TQwBhlW5ZwjDAJYYARwGjVtTq6aVcEIWLG6H6QojR4e1JjbpE1kQEJJoZjUvuYXo7H6aC0KdJH4J7eFXfWH2f+ceNn1RWKVw==", + "SignatureVersion": "1", + "SigningCertUrl": "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-433026a4050d206028891664da859041.pem", + "Subject": null, + "Timestamp": "2018-03-04T04:45:48.369Z", + "TopicArn": "arn:aws:sns:us-east-1:961179389914:api-naftuli-wtf", + "Type": "Notification", + "UnsubscribeUrl": "https://sns.us-east-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-1:961179389914:api-naftuli-wtf:6fbc6fe8-a76a-4377-8fbb-3f8cd8e82959" + } + } + ] +} diff --git a/src/data/sns/fixtures/no-attributes.json b/src/data/sns/fixtures/no-attributes.json new file mode 100644 index 0000000..1ce457d --- /dev/null +++ b/src/data/sns/fixtures/no-attributes.json @@ -0,0 +1,22 @@ +{ + "Records": [ + { + "EventVersion": "1.0", + "EventSubscriptionArn": "eventsubscriptionarn", + "EventSource": "aws:sns", + "Sns": { + "SignatureVersion": "1", + "Timestamp": "1970-01-01T00:00:00.000Z", + "Signature": "EXAMPLE", + "SigningCertUrl": "EXAMPLE", + "MessageId": "95df01b4-ee98-5cb9-9903-4c221d41eb5e", + "Message": "Hello from SNS!", + "MessageAttributes": {}, + "Type": "Notification", + "UnsubscribeUrl": "EXAMPLE", + "TopicArn": "topicarn", + "Subject": "TestInvoke" + } + } + ] +} diff --git a/src/data/sns/fixtures/no-subject.json b/src/data/sns/fixtures/no-subject.json new file mode 100644 index 0000000..342ac5b --- /dev/null +++ b/src/data/sns/fixtures/no-subject.json @@ -0,0 +1,31 @@ +{ + "Records": [ + { + "EventVersion": "1.0", + "EventSubscriptionArn": "eventsubscriptionarn", + "EventSource": "aws:sns", + "Sns": { + "SignatureVersion": "1", + "Timestamp": "1970-01-01T00:00:00.000Z", + "Signature": "EXAMPLE", + "SigningCertUrl": "EXAMPLE", + "MessageId": "95df01b4-ee98-5cb9-9903-4c221d41eb5e", + "Message": "Hello from SNS!", + "MessageAttributes": { + "Test": { + "Type": "String", + "Value": "TestString" + }, + "TestBinary": { + "Type": "Binary", + "Value": "TestBinary" + } + }, + "Type": "Notification", + "UnsubscribeUrl": "EXAMPLE", + "TopicArn": "topicarn", + "Subject": null + } + } + ] +} diff --git a/src/data/sns/fixtures/record.json b/src/data/sns/fixtures/record.json new file mode 100644 index 0000000..75250bd --- /dev/null +++ b/src/data/sns/fixtures/record.json @@ -0,0 +1,27 @@ +{ + "EventVersion": "1.0", + "EventSubscriptionArn": "eventsubscriptionarn", + "EventSource": "aws:sns", + "Sns": { + "SignatureVersion": "1", + "Timestamp": "1970-01-01T00:00:00.000Z", + "Signature": "EXAMPLE", + "SigningCertUrl": "EXAMPLE", + "MessageId": "95df01b4-ee98-5cb9-9903-4c221d41eb5e", + "Message": "Hello from SNS!", + "MessageAttributes": { + "Test": { + "Type": "String", + "Value": "TestString" + }, + "TestBinary": { + "Type": "Binary", + "Value": "TestBinary" + } + }, + "Type": "Notification", + "UnsubscribeUrl": "EXAMPLE", + "TopicArn": "topicarn", + "Subject": "TestInvoke" + } +} diff --git a/src/data/sns/fixtures/records.json b/src/data/sns/fixtures/records.json new file mode 100644 index 0000000..8720eb1 --- /dev/null +++ b/src/data/sns/fixtures/records.json @@ -0,0 +1,31 @@ +{ + "Records": [ + { + "EventVersion": "1.0", + "EventSubscriptionArn": "eventsubscriptionarn", + "EventSource": "aws:sns", + "Sns": { + "SignatureVersion": "1", + "Timestamp": "1970-01-01T00:00:00.000Z", + "Signature": "EXAMPLE", + "SigningCertUrl": "EXAMPLE", + "MessageId": "95df01b4-ee98-5cb9-9903-4c221d41eb5e", + "Message": "Hello from SNS!", + "MessageAttributes": { + "Test": { + "Type": "String", + "Value": "TestString" + }, + "TestBinary": { + "Type": "Binary", + "Value": "TestBinary" + } + }, + "Type": "Notification", + "UnsubscribeUrl": "EXAMPLE", + "TopicArn": "topicarn", + "Subject": "TestInvoke" + } + } + ] +} diff --git a/src/data/sns/fixtures/ses.json b/src/data/sns/fixtures/ses.json new file mode 100644 index 0000000..ee7aae6 --- /dev/null +++ b/src/data/sns/fixtures/ses.json @@ -0,0 +1,13 @@ +{ + "Message": "{\"notificationType\":\"Received\",\"mail\":{\"timestamp\":\"2018-03-07T04:23:42.729Z\",\"source\":\"me@naftuli.wtf\",\"messageId\":\"fo4v6ln4j4plquer9mns0ela4709rr4krs9o3fo1\",\"destination\":[\"test@mail.naftuli.wtf\"],\"headersTruncated\":false,\"headers\":[{\"name\":\"Return-Path\",\"value\":\"\"},{\"name\":\"Received\",\"value\":\"from mail-oi0-f54.google.com (mail-oi0-f54.google.com [209.85.218.54]) by inbound-smtp.us-east-1.amazonaws.com with SMTP id fo4v6ln4j4plquer9mns0ela4709rr4krs9o3fo1 for test@mail.naftuli.wtf; Wed, 07 Mar 2018 04:23:42 +0000 (UTC)\"},{\"name\":\"X-SES-Spam-Verdict\",\"value\":\"PASS\"},{\"name\":\"X-SES-Virus-Verdict\",\"value\":\"PASS\"},{\"name\":\"Received-SPF\",\"value\":\"pass (spfCheck: domain of naftuli.wtf designates 209.85.218.54 as permitted sender) client-ip=209.85.218.54; envelope-from=me@naftuli.wtf; helo=mail-oi0-f54.google.com;\"},{\"name\":\"Authentication-Results\",\"value\":\"amazonses.com; spf=pass (spfCheck: domain of naftuli.wtf designates 209.85.218.54 as permitted sender) client-ip=209.85.218.54; envelope-from=me@naftuli.wtf; helo=mail-oi0-f54.google.com; dkim=pass header.i=@naftuli.wtf;\"},{\"name\":\"X-SES-RECEIPT\",\"value\":\"AEFBQUFBQUFBQUFHbE1DclRkbVFqN3RQYzk0UEpUZFI2VUlhVzAzd1lieTJRYjB6NU9qVnZyNERhTUF6ZzZvTklsbkxXN0VhMDJwOUhQT3pUUW1nbGFCTzRCY3dqMWpoNUN5STZ2bHZsSmd5M2xlb2ZIRDJOU1c2TmZ1aENYZDFZY1BhU1M5VFQ2eHFHMFo0cXhXWXpRakhVNGN3SXlaNWV5M2FiNm1mQ3NOalgrWkNHUHp3citOSzJzb0xLcmIvQTM0YjFwMkdmN3h3NkZleks5RVg0VS9hUllkUENpNjNKTFhibjNlUUNhbEVLSjZWaExRelFvakFIVWZRUkwzZkx6Y21YS0sxK0RiT2FxUXZPSytkd2VZYmduZDByQU9wdk9WUk1jR0RpNmRoS0twYkZtUUhRNVE9PQ==\"},{\"name\":\"X-SES-DKIM-SIGNATURE\",\"value\":\"a=rsa-sha256; q=dns/txt; b=f1irw4PJbSjLfytRjUGlVUrmupVYNZo6RAGsreMi5trdeQCcOBivldx+PvyB9ResoyJmNnVGXhB4gdvYA/S9AvLzX/xVKR41P2Wu53AoCLhoo0ZVoUC5/YjYco5TAnrIlHrs1tftajBnRdsjnrvHMFE0/xws3eLQS0OCsHGmnA4=; c=relaxed/simple; s=224i4yxa5dv7c2xz3womw6peuasteono; d=amazonses.com; t=1520396623; v=1; bh=ZC64xB5KnyHpBCtKJ0jxIryzsLfc3uReH51UNNdrKII=; h=From:To:Cc:Bcc:Subject:Date:Message-ID:MIME-Version:Content-Type:X-SES-RECEIPT;\"},{\"name\":\"Received\",\"value\":\"by mail-oi0-f54.google.com with SMTP id g5so703446oiy.8 for ; Tue, 06 Mar 2018 20:23:42 -0800 (PST)\"},{\"name\":\"DKIM-Signature\",\"value\":\"v=1; a=rsa-sha256; c=relaxed/relaxed; d=naftuli.wtf; s=google; h=mime-version:from:date:message-id:subject:to; bh=ZC64xB5KnyHpBCtKJ0jxIryzsLfc3uReH51UNNdrKII=; b=X5KxbFiaGrvNRX3RHJC1LHZmcxhoHI9+3/IChmbB2Kot4WO/NXGVnWyxb7UVXvCr9r1QRMEJj5BkwluTe0RcOJaITEUCHL3w8ShH9qcmxv6FK435mqgnM/hxUdhyaUZtEb9NAzCgjIRxXox8tR3IXgf75ww8ZfiGXUSo8AnV0FeTXa4XTLhiB5kbZh0AsGj3gzyTgZP+YIYRhzdiAV6QiAHnl73+1SN89pobzZELaBromPfpZ5drSR9QP79r1mqj2CTLaChdaJQikSavAz4WIDeAsbr2TIX6q9Kr0/8Liq1rVfUtu2Hms5+nv60WnSbzt9GBA/1PNKo23QCympKMPA==\"},{\"name\":\"X-Google-DKIM-Signature\",\"value\":\"v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:mime-version:from:date:message-id:subject:to; bh=ZC64xB5KnyHpBCtKJ0jxIryzsLfc3uReH51UNNdrKII=; b=d04Uqmt1JZ/gCuHSSyp+Wd0EP7daEjKfDM0+TCL/p6N0e52aDaN6JfHatnWlQ4vACM H8n6YnL3TQCYpmM861MHcORxOJVKC+48q1lEz5FwDQZHcmfE48u/btD8uZWvagyUs6cf n9J1G3Nh7tAIej7mof8BtrBhZ/nP5isUfDlIMpqdf5Rw73b1hnM6gzhRJEvbMLSkFwH8 r4BKLlOR2UlFC+VtEvK92AK1RulKsKy3PIye2uKQrjz1Onbpfn4h2/9OzAvYvxJ1Enmp YuKMgWOlIbcj7SUsmokofxg9u4fv3oic15maXJcB0Q8tYeA935WahSmjr8GZ8u5G8vFc XyLg==\"},{\"name\":\"X-Gm-Message-State\",\"value\":\"AElRT7FllyLLV0KEYDxXu+mXlBV6cVRwEuQZ8m7Jtx5Nngv7T2YQNYGb bHuoBnVW17dtLMjMfemSE2QAM+RcbHlpC53My33EBGPVr4k=\"},{\"name\":\"X-Google-Smtp-Source\",\"value\":\"AG47ELtctAN0wplb9TMYfqqTpSEj77eugycCNemc6IJ1xVYfi2IZiweAjE18FLvYiICsDQB0+/xQnrmZS7EZvQ/j+s8=\"},{\"name\":\"X-Received\",\"value\":\"by 10.202.89.194 with SMTP id n185mr13705723oib.133.1520396621818; Tue, 06 Mar 2018 20:23:41 -0800 (PST)\"},{\"name\":\"MIME-Version\",\"value\":\"1.0\"},{\"name\":\"Received\",\"value\":\"by 10.157.14.244 with HTTP; Tue, 6 Mar 2018 20:23:41 -0800 (PST)\"},{\"name\":\"X-Originating-IP\",\"value\":\"[107.77.227.71]\"},{\"name\":\"Received\",\"value\":\"by 10.157.14.244 with HTTP; Tue, 6 Mar 2018 20:23:41 -0800 (PST)\"},{\"name\":\"From\",\"value\":\"Naftuli Kay \"},{\"name\":\"Date\",\"value\":\"Tue, 6 Mar 2018 20:23:41 -0800\"},{\"name\":\"Message-ID\",\"value\":\"\"},{\"name\":\"Subject\",\"value\":\"Block\"},{\"name\":\"To\",\"value\":\"test@mail.naftuli.wtf\"},{\"name\":\"Content-Type\",\"value\":\"multipart/signed; protocol=\\\"application/pkcs7-signature\\\"; micalg=sha-256; boundary=\\\"001a113d32a89234370566caec50\\\"\"}],\"commonHeaders\":{\"returnPath\":\"me@naftuli.wtf\",\"from\":[\"Naftuli Kay \"],\"date\":\"Tue, 6 Mar 2018 20:23:41 -0800\",\"to\":[\"test@mail.naftuli.wtf\"],\"messageId\":\"\",\"subject\":\"Block\"}},\"receipt\":{\"timestamp\":\"2018-03-07T04:23:42.729Z\",\"processingTimeMillis\":505,\"recipients\":[\"test@mail.naftuli.wtf\"],\"spamVerdict\":{\"status\":\"PASS\"},\"virusVerdict\":{\"status\":\"PASS\"},\"spfVerdict\":{\"status\":\"PASS\"},\"dkimVerdict\":{\"status\":\"PASS\"},\"dmarcVerdict\":{\"status\":\"PASS\"},\"action\":{\"type\":\"SNS\",\"topicArn\":\"arn:aws:sns:us-east-1:961179389914:api-naftuli-wtf\",\"encoding\":\"UTF8\"}},\"content\":\"Return-Path: \\r\\nReceived: from mail-oi0-f54.google.com (mail-oi0-f54.google.com [209.85.218.54])\\r\\n by inbound-smtp.us-east-1.amazonaws.com with SMTP id fo4v6ln4j4plquer9mns0ela4709rr4krs9o3fo1\\r\\n for test@mail.naftuli.wtf;\\r\\n Wed, 07 Mar 2018 04:23:42 +0000 (UTC)\\r\\nX-SES-Spam-Verdict: PASS\\r\\nX-SES-Virus-Verdict: PASS\\r\\nReceived-SPF: pass (spfCheck: domain of naftuli.wtf designates 209.85.218.54 as permitted sender) client-ip=209.85.218.54; envelope-from=me@naftuli.wtf; helo=mail-oi0-f54.google.com;\\r\\nAuthentication-Results: amazonses.com;\\r\\n spf=pass (spfCheck: domain of naftuli.wtf designates 209.85.218.54 as permitted sender) client-ip=209.85.218.54; envelope-from=me@naftuli.wtf; helo=mail-oi0-f54.google.com;\\r\\n dkim=pass header.i=@naftuli.wtf;\\r\\nX-SES-RECEIPT: AEFBQUFBQUFBQUFHbE1DclRkbVFqN3RQYzk0UEpUZFI2VUlhVzAzd1lieTJRYjB6NU9qVnZyNERhTUF6ZzZvTklsbkxXN0VhMDJwOUhQT3pUUW1nbGFCTzRCY3dqMWpoNUN5STZ2bHZsSmd5M2xlb2ZIRDJOU1c2TmZ1aENYZDFZY1BhU1M5VFQ2eHFHMFo0cXhXWXpRakhVNGN3SXlaNWV5M2FiNm1mQ3NOalgrWkNHUHp3citOSzJzb0xLcmIvQTM0YjFwMkdmN3h3NkZleks5RVg0VS9hUllkUENpNjNKTFhibjNlUUNhbEVLSjZWaExRelFvakFIVWZRUkwzZkx6Y21YS0sxK0RiT2FxUXZPSytkd2VZYmduZDByQU9wdk9WUk1jR0RpNmRoS0twYkZtUUhRNVE9PQ==\\r\\nX-SES-DKIM-SIGNATURE: a=rsa-sha256; q=dns/txt; b=f1irw4PJbSjLfytRjUGlVUrmupVYNZo6RAGsreMi5trdeQCcOBivldx+PvyB9ResoyJmNnVGXhB4gdvYA/S9AvLzX/xVKR41P2Wu53AoCLhoo0ZVoUC5/YjYco5TAnrIlHrs1tftajBnRdsjnrvHMFE0/xws3eLQS0OCsHGmnA4=; c=relaxed/simple; s=224i4yxa5dv7c2xz3womw6peuasteono; d=amazonses.com; t=1520396623; v=1; bh=ZC64xB5KnyHpBCtKJ0jxIryzsLfc3uReH51UNNdrKII=; h=From:To:Cc:Bcc:Subject:Date:Message-ID:MIME-Version:Content-Type:X-SES-RECEIPT;\\r\\nReceived: by mail-oi0-f54.google.com with SMTP id g5so703446oiy.8\\r\\n for ; Tue, 06 Mar 2018 20:23:42 -0800 (PST)\\r\\nDKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;\\r\\n d=naftuli.wtf; s=google;\\r\\n h=mime-version:from:date:message-id:subject:to;\\r\\n bh=ZC64xB5KnyHpBCtKJ0jxIryzsLfc3uReH51UNNdrKII=;\\r\\n b=X5KxbFiaGrvNRX3RHJC1LHZmcxhoHI9+3/IChmbB2Kot4WO/NXGVnWyxb7UVXvCr9r\\r\\n 1QRMEJj5BkwluTe0RcOJaITEUCHL3w8ShH9qcmxv6FK435mqgnM/hxUdhyaUZtEb9NAz\\r\\n CgjIRxXox8tR3IXgf75ww8ZfiGXUSo8AnV0FeTXa4XTLhiB5kbZh0AsGj3gzyTgZP+YI\\r\\n YRhzdiAV6QiAHnl73+1SN89pobzZELaBromPfpZ5drSR9QP79r1mqj2CTLaChdaJQikS\\r\\n avAz4WIDeAsbr2TIX6q9Kr0/8Liq1rVfUtu2Hms5+nv60WnSbzt9GBA/1PNKo23QCymp\\r\\n KMPA==\\r\\nX-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;\\r\\n d=1e100.net; s=20161025;\\r\\n h=x-gm-message-state:mime-version:from:date:message-id:subject:to;\\r\\n bh=ZC64xB5KnyHpBCtKJ0jxIryzsLfc3uReH51UNNdrKII=;\\r\\n b=d04Uqmt1JZ/gCuHSSyp+Wd0EP7daEjKfDM0+TCL/p6N0e52aDaN6JfHatnWlQ4vACM\\r\\n H8n6YnL3TQCYpmM861MHcORxOJVKC+48q1lEz5FwDQZHcmfE48u/btD8uZWvagyUs6cf\\r\\n n9J1G3Nh7tAIej7mof8BtrBhZ/nP5isUfDlIMpqdf5Rw73b1hnM6gzhRJEvbMLSkFwH8\\r\\n r4BKLlOR2UlFC+VtEvK92AK1RulKsKy3PIye2uKQrjz1Onbpfn4h2/9OzAvYvxJ1Enmp\\r\\n YuKMgWOlIbcj7SUsmokofxg9u4fv3oic15maXJcB0Q8tYeA935WahSmjr8GZ8u5G8vFc\\r\\n XyLg==\\r\\nX-Gm-Message-State: AElRT7FllyLLV0KEYDxXu+mXlBV6cVRwEuQZ8m7Jtx5Nngv7T2YQNYGb\\r\\n\\tbHuoBnVW17dtLMjMfemSE2QAM+RcbHlpC53My33EBGPVr4k=\\r\\nX-Google-Smtp-Source: AG47ELtctAN0wplb9TMYfqqTpSEj77eugycCNemc6IJ1xVYfi2IZiweAjE18FLvYiICsDQB0+/xQnrmZS7EZvQ/j+s8=\\r\\nX-Received: by 10.202.89.194 with SMTP id n185mr13705723oib.133.1520396621818;\\r\\n Tue, 06 Mar 2018 20:23:41 -0800 (PST)\\r\\nMIME-Version: 1.0\\r\\nReceived: by 10.157.14.244 with HTTP; Tue, 6 Mar 2018 20:23:41 -0800 (PST)\\r\\nX-Originating-IP: [107.77.227.71]\\r\\nReceived: by 10.157.14.244 with HTTP; Tue, 6 Mar 2018 20:23:41 -0800 (PST)\\r\\nFrom: Naftuli Kay \\r\\nDate: Tue, 6 Mar 2018 20:23:41 -0800\\r\\nMessage-ID: \\r\\nSubject: Block\\r\\nTo: test@mail.naftuli.wtf\\r\\nContent-Type: multipart/signed; protocol=\\\"application/pkcs7-signature\\\"; micalg=sha-256;\\r\\n\\tboundary=\\\"001a113d32a89234370566caec50\\\"\\r\\n\\r\\n--001a113d32a89234370566caec50\\r\\nContent-Type: multipart/alternative; boundary=\\\"001a113d32a88fd0090566caec36\\\"\\r\\n\\r\\n--001a113d32a88fd0090566caec36\\r\\nContent-Type: text/plain; charset=\\\"UTF-8\\\"\\r\\n\\r\\nMeme\\r\\n\\r\\n--001a113d32a88fd0090566caec36\\r\\nContent-Type: text/html; charset=\\\"UTF-8\\\"\\r\\n\\r\\n
Meme
\\r\\n\\r\\n--001a113d32a88fd0090566caec36--\\r\\n\\r\\n--001a113d32a89234370566caec50\\r\\nContent-Type: application/pkcs7-signature; name=\\\"smime.p7s\\\"\\r\\nContent-Transfer-Encoding: base64\\r\\nContent-Disposition: attachment; filename=\\\"smime.p7s\\\"\\r\\nContent-Description: S/MIME Cryptographic Signature\\r\\n\\r\\nMIIT3gYJKoZIhvcNAQcCoIITzzCCE8sCAQExDzANBglghkgBZQMEAgEFADALBgkqhkiG9w0BBwGg\\r\\nghDyMIIF5jCCA86gAwIBAgIQapvhODv/K2ufAdXZuKdSVjANBgkqhkiG9w0BAQwFADCBhTELMAkG\\r\\nA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEa\\r\\nMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNh\\r\\ndGlvbiBBdXRob3JpdHkwHhcNMTMwMTEwMDAwMDAwWhcNMjgwMTA5MjM1OTU5WjCBlzELMAkGA1UE\\r\\nBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgG\\r\\nA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxPTA7BgNVBAMTNENPTU9ETyBSU0EgQ2xpZW50IEF1dGhl\\r\\nbnRpY2F0aW9uIGFuZCBTZWN1cmUgRW1haWwgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK\\r\\nAoIBAQC+s55XrCh2dUAWxzgDmNPGGHYhUPMleQtMtaDRfTpYPpynMS6n9jR22YRq2tA9NEjk6vW7\\r\\nrN/5sYFLIP1of3l0NKZ6fLWfF2VgJ5cijKYy/qlAckY1wgOkUMgzKlWlVJGyK+UlNEQ1/5ErCsHq\\r\\n9x9aU/x1KwTdF/LCrT03Rl/FwFrf1XTCwa2QZYL55AqLPikFlgqOtzk06kb2qvGlnHJvijjI03BO\\r\\nrNpo+kZGpcHsgyO1/u1OZTaOo8wvEU17VVeP1cHWse9tGKTDyUGg2hJZjrqck39UIm/nKbpDSZ0J\\r\\nsMoIw/JtOOg0JC56VzQgBo7ictReTQE5LFLG3yQK+xS1AgMBAAGjggE8MIIBODAfBgNVHSMEGDAW\\r\\ngBS7r34CPfqm8TyEjq3uOJjs2TIy1DAdBgNVHQ4EFgQUgq9sjPjF/pZhfOgfPStxSF7Ei8AwDgYD\\r\\nVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAwEQYDVR0gBAowCDAGBgRVHSAAMEwGA1Ud\\r\\nHwRFMEMwQaA/oD2GO2h0dHA6Ly9jcmwuY29tb2RvY2EuY29tL0NPTU9ET1JTQUNlcnRpZmljYXRp\\r\\nb25BdXRob3JpdHkuY3JsMHEGCCsGAQUFBwEBBGUwYzA7BggrBgEFBQcwAoYvaHR0cDovL2NydC5j\\r\\nb21vZG9jYS5jb20vQ09NT0RPUlNBQWRkVHJ1c3RDQS5jcnQwJAYIKwYBBQUHMAGGGGh0dHA6Ly9v\\r\\nY3NwLmNvbW9kb2NhLmNvbTANBgkqhkiG9w0BAQwFAAOCAgEAeFyygSg0TzzuX1bOn5dW7I+iaxf2\\r\\n8/ZJCAbU2C81zd9A/tNx4+jsQgwRGiHjZrAYayZrrm78hOx7aEpkfNPQIHGG6Fvq3EzWf/Lvx7/h\\r\\nk6zSPwIal9v5IkDcZoFD7f3iT7PdkHJY9B51csvU50rxpEg1OyOT8fk2zvvPBuM4qQNqbGWlnhMp\\r\\nIMwpWZT89RY0wpJO+2V6eXEGGHsROs3njeP9DqqqAJaBa4wBeKOdGCWn1/Jp2oY6dyNmNppI4ZNM\\r\\nUH4Tam85S1j6E95u4+1Nuru84OrMIzqvISE2HN/56ebTOWlcrurffade2022O/tUU1gb4jfWCcyv\\r\\nB8czm12FgX/y/lRjmDbEA08QJNB2729Y+io1IYO3ztveBdvUCIYZojTq/OCR6MvnzS6X72HP0PRL\\r\\nRTiOSEmIDsS5N5w/8IW1Hva5hEFy6fDAfd9yI+O+IMMAj1KcL/Zo9jzJ16HO5m60ttl1Enk8MQkz\\r\\n/W3JlHaeI5iKFn4UJu1/cP2YHXYPiWf2JyBzsLBrGk1II+3yL8aorYew6CQvdVifC3HtwlSam9V1\\r\\nniiCfOBe2C12TdKGu05LWIA3ZkFcWJGaNXOZ6Ggyh/TqvXG5v7zmEVDNXFnHn9tFpMpOUvxhcsjy\\r\\ncBtH0dZ0WrNw6gH+HF8TIhCnH3+zzWuDN0Rk6h9KVkfKehIwggXYMIIDwKADAgECAhBMqvnK22Nv\\r\\n4B/3TthbA4adMA0GCSqGSIb3DQEBDAUAMIGFMQswCQYDVQQGEwJHQjEbMBkGA1UECBMSR3JlYXRl\\r\\nciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFDT01PRE8gQ0EgTGltaXRl\\r\\nZDErMCkGA1UEAxMiQ09NT0RPIFJTQSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0xMDAxMTkw\\r\\nMDAwMDBaFw0zODAxMTgyMzU5NTlaMIGFMQswCQYDVQQGEwJHQjEbMBkGA1UECBMSR3JlYXRlciBN\\r\\nYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFDT01PRE8gQ0EgTGltaXRlZDEr\\r\\nMCkGA1UEAxMiQ09NT0RPIFJTQSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCAiIwDQYJKoZIhvcN\\r\\nAQEBBQADggIPADCCAgoCggIBAJHoVJLSClaxrA0k3cXPRGd0mSs3o30jcABxvFPfxPoqEo9LfxBW\\r\\nvZ9wcrdhf8lLDxenPeOwBGHu/xGXx/SGPgr6Plz5k+Y0etkUa+ecs4Wggnp2r3GQ1+z9DfqcbPrf\\r\\nsIL0FH75vsSmL09/mX+1/GdDcr0MANaJ62ss0+2PmBwUq37l42782KjkkiTaQ2tiuFX96sG8bLaL\\r\\n8w6NmuSbbGmZ+HhIMEXVreENPEVg/DKWUSe8Z8PKLrZr6kbHxyCgsR9l3kgIuqROqfKDRjeE6+jM\\r\\ngUhDZ05yKptcvUwbKIpcInu0q5jZ7uBRg8MJRk5tPpn6lRfafDNXQTyNUe0LtlyvLGMa31fIP7zp\\r\\nXcSbr0WZ4qNaJLS6qVY9z2+q/0lYvvCo//S4rek3+7q49As6+ehDQh6J2ITLE/HZu+GJYLiMKFas\\r\\nFB2cCudx688O3T2plqFIvTz3r7UNIkzAEYHsVjv206LiW7eyBCJSlYCTaeiOTGXxkQMtcHQC6otn\\r\\nFSlpUgK7199QalVGv6CjKGF/cNDDoqosIapHziicBkV2v4IYJ7TVrrTLUOZr9EyGcTDppt8WhuDY\\r\\n/0Dd+9BCiH+jMzouXB5BEYFjzhhxayvspoq3MVw6akfgw3lZ1iAar/JqmKpyvFdK0kuduxD8sExB\\r\\n5e0dPV4onZzMv7NR2qdH5YRTAgMBAAGjQjBAMB0GA1UdDgQWBBS7r34CPfqm8TyEjq3uOJjs2TIy\\r\\n1DAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQwFAAOCAgEACvHV\\r\\nRoS3rlG7bLJNQRQAk0ycy+XAVM+gJY4C+f2wog31IJg8Ey2sVqKw1n4Rkukuup4umnKxvRlEbGE1\\r\\nopq0FhJpWozh1z6kGugvA/SuYR0QGyqki3rF/gWm4cDWyP6ero8ruj2Z+NhzCVhGbqac9Ncn05Xa\\r\\nN4NyHNNz4KJHmQM4XdVJeQApHMfsmyAcByRpV3iyOfw6hKC1nHyNvy6TYie3OdoXGK69PAlo/4Sb\\r\\nPNXWCwPjV54U99HrT8i9hyO3tklDeYVcuuuSC6HG6GioTBaxGpkK6FMskruhCRh1DGWoe8sjtxrC\\r\\nKIXDG//QK2LvpHsJkZhnjBQBzWgGamMhdQOAiIpugcaF8qmkLef0pSQQR4PKzfSNeVixBpvnGirZ\\r\\nnQHXlH3tA0rK8NvoqQE+9VaZyR6OST275Qm54E9Jkj0WgkDMzFnG5jrtEi5pPGyVsf2qHXt/hr4e\\r\\nDjJG+/sTj3V/TItLRmP+ADRAcMHDuaHdpnDiBLNBvOmAkepknHrhIgOpnG5vDmVPbIeHXvNuoPl1\\r\\npZtA6FOyJ51KucB3IY3/h/LevIzvF9+3SQvR8m4wCxoOTnbtEfz16Vayfb/HbQqTjKXQwLYdvjpO\\r\\nlKLXbmwLwop8+iDzxOTlzQ2oy5GSsXyF7LUUaWYOgufNzsgtplF/IcE1U4UGSl2frbsbX3QwggUo\\r\\nMIIEEKADAgECAhEAyiacgkaDJ4l6QWBDiclWHDANBgkqhkiG9w0BAQsFADCBlzELMAkGA1UEBhMC\\r\\nR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UE\\r\\nChMRQ09NT0RPIENBIExpbWl0ZWQxPTA7BgNVBAMTNENPTU9ETyBSU0EgQ2xpZW50IEF1dGhlbnRp\\r\\nY2F0aW9uIGFuZCBTZWN1cmUgRW1haWwgQ0EwHhcNMTcxMjI0MDAwMDAwWhcNMTgxMjI0MjM1OTU5\\r\\nWjAfMR0wGwYJKoZIhvcNAQkBFg5tZUBuYWZ0dWxpLnd0ZjCCASIwDQYJKoZIhvcNAQEBBQADggEP\\r\\nADCCAQoCggEBALWuRKlz/25qkH09RyvbvYcXOpr3EVIopWZeQJ3sohDNL3PFGwaRgPFtrFf+bfWO\\r\\nS7DWMgMO4Y/nFWArxp3FL8oF2gjagTzFMUCFV3m6OUYBfUhfAsxZ479GBz19zCBZBG7vDkFWvGGc\\r\\ncU6Tk8e3Tt9ViIeQKRqZ9uoAOoFB1eww1L6DMXBeNvP7sPkSrwjGI28F2jwagBKphrv2Q9ivjqRF\\r\\nd7G28HvB58JN8tmpgUDHsVOEtiAgQm4ICQZ0bTXusmV4TSGauS+vSemZm/d6vhQw3MBWLxdhpGSp\\r\\nrcy2Hqxg/trk/v4beTjxL6PrOQQR1J3dq1ZoMEd/LzN5ddKcuNkCAwEAAaOCAeQwggHgMB8GA1Ud\\r\\nIwQYMBaAFIKvbIz4xf6WYXzoHz0rcUhexIvAMB0GA1UdDgQWBBRcGmu64Vwi0F4AKJ7ExAXXeO7Y\\r\\naDAOBgNVHQ8BAf8EBAMCBaAwDAYDVR0TAQH/BAIwADAgBgNVHSUEGTAXBggrBgEFBQcDBAYLKwYB\\r\\nBAGyMQEDBQIwEQYJYIZIAYb4QgEBBAQDAgUgMEYGA1UdIAQ/MD0wOwYMKwYBBAGyMQECAQEBMCsw\\r\\nKQYIKwYBBQUHAgEWHWh0dHBzOi8vc2VjdXJlLmNvbW9kby5uZXQvQ1BTMFoGA1UdHwRTMFEwT6BN\\r\\noEuGSWh0dHA6Ly9jcmwuY29tb2RvY2EuY29tL0NPTU9ET1JTQUNsaWVudEF1dGhlbnRpY2F0aW9u\\r\\nYW5kU2VjdXJlRW1haWxDQS5jcmwwgYsGCCsGAQUFBwEBBH8wfTBVBggrBgEFBQcwAoZJaHR0cDov\\r\\nL2NydC5jb21vZG9jYS5jb20vQ09NT0RPUlNBQ2xpZW50QXV0aGVudGljYXRpb25hbmRTZWN1cmVF\\r\\nbWFpbENBLmNydDAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuY29tb2RvY2EuY29tMBkGA1UdEQQS\\r\\nMBCBDm1lQG5hZnR1bGkud3RmMA0GCSqGSIb3DQEBCwUAA4IBAQB1wfCQqazZv7x2ZycgNZe6lh79\\r\\nS29zmREf03Lb9ezAap4pkOwaPJqouPrw5XTXSg2rTkAUfsQvXfD3Alalf+bJ2PM9yo2IsYftf9H9\\r\\n8/PkKLZPci/KttklN4NKe6E2O7bdnLm5uJxMRjS9RIPKgKnIUK54KWLdt6aDNWgJultp5uKczx/n\\r\\nyhWsobbZFP26bKgT/mEaEnDtO4ykA8Naac8ndgA8SQAwyKnGC1w8xI11hBED9shphZ3L/8QYp+df\\r\\nWgFdIbHUoTWjbVBBW48eYwo7QIAW3yQzGoRwVsLSG/SFzijJphHUdrIk8/9ph4HGGT9zb++/rkyy\\r\\nsgHkl2RBz/5UMYICsDCCAqwCAQEwga0wgZcxCzAJBgNVBAYTAkdCMRswGQYDVQQIExJHcmVhdGVy\\r\\nIE1hbmNoZXN0ZXIxEDAOBgNVBAcTB1NhbGZvcmQxGjAYBgNVBAoTEUNPTU9ETyBDQSBMaW1pdGVk\\r\\nMT0wOwYDVQQDEzRDT01PRE8gUlNBIENsaWVudCBBdXRoZW50aWNhdGlvbiBhbmQgU2VjdXJlIEVt\\r\\nYWlsIENBAhEAyiacgkaDJ4l6QWBDiclWHDANBglghkgBZQMEAgEFAKCB1DAvBgkqhkiG9w0BCQQx\\r\\nIgQgRh2OOZwQVAExwYsMSbkRbprSgIIaS2fOEicem+GIn7kwGAYJKoZIhvcNAQkDMQsGCSqGSIb3\\r\\nDQEHATAcBgkqhkiG9w0BCQUxDxcNMTgwMzA3MDQyMzQxWjBpBgkqhkiG9w0BCQ8xXDBaMAsGCWCG\\r\\nSAFlAwQBKjALBglghkgBZQMEARYwCwYJYIZIAWUDBAECMAoGCCqGSIb3DQMHMAsGCSqGSIb3DQEB\\r\\nCjALBgkqhkiG9w0BAQcwCwYJYIZIAWUDBAIBMA0GCSqGSIb3DQEBAQUABIIBAAVy0rbaqFHHFDY4\\r\\n8eaRdsfqTKHIjECiZ1+4D59cDyGTbbH2p2uXWzp58+J6E+QysM5TMFBI1bIErUpABOyLJL18TXYU\\r\\nx6jSJFc+uSSsGnHMib9CuwiiVNnQEFX3rVC68mBRgOzlJ9WJLNt8wXJS1vu/MHZzluQtwg9btIIf\\r\\nW6H9wZ1PCyKaUrnJZyoSpLWgYrczAssFXHM+cJBNEMpblSMC0vENTS0ScY/xsJkoQNwft2TVZOOz\\r\\nmGJMvvYw7cN/RqPGzskKzTf0TPmtprkyeCZa5lShrj6ujRd8yJVRKRSno88dSXAuAbLHSVHBlYxR\\r\\nD06pyifR/nTEGUgOtXMYiw0=\\r\\n--001a113d32a89234370566caec50--\\r\\n\"}", + "MessageAttributes": {}, + "MessageId": "9ce9c477-7229-57de-a7f8-976e19a6cf94", + "MessageType": null, + "Signature": "cbK9LPdE57i2P4AWWF+hJ/ikHPNmZqh0dlZ9djuDH4/8L4H6P4HRRxJxoXD/R6pio/FeKF6sEIo61KL2GlKXlddgjkgfcmJnXjr9plSBRnaibHLWda9R31W30cK3rr/E1KY95jHFvnJVA2AD/+5pKTd15Epjy90+G6QNI+Ivd7A63xdID8OyqDtGh+23Xq8gv6QMD/BkIewxmN875BwZh++BqhdlU62L2+pMFOhJohlUCr00KgFu6g2xnlToWEu9V+T947vKT3Q20eIz18ZYy9cxSQYqwcxLV5CtF4ZMhwFOumHLtpa/+2coS4tGA+CjhrJI7X0iwgVQnM1yVlqFFg==", + "SignatureVersion": "1", + "SigningCertUrl": "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-433026a4050d206028891664da859041.pem", + "Subject": "Amazon SES Email Receipt Notification", + "Timestamp": "2018-03-07T04:23:43.274Z", + "TopicArn": "arn:aws:sns:us-east-1:961179389914:api-naftuli-wtf", + "UnsubscribeUrl": "https://sns.us-east-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-1:961179389914:api-naftuli-wtf:6fbc6fe8-a76a-4377-8fbb-3f8cd8e82959" +} diff --git a/src/data/sns/mod.rs b/src/data/sns/mod.rs new file mode 100644 index 0000000..9f0c2f7 --- /dev/null +++ b/src/data/sns/mod.rs @@ -0,0 +1,52 @@ +#[cfg(test)] +mod tests; + +use super::*; + +use chrono::prelude::*; + +#[derive(Serialize,Deserialize)] +#[serde(rename_all="PascalCase")] +pub struct MessageAttribute { + #[serde(rename="Type")] + pub attribute_type: MessageAttributeType, + pub value: String, +} + +#[derive(Serialize,Deserialize)] +pub enum MessageAttributeType { + #[serde(rename="String")] + UTF8, + Binary, +} + +#[derive(Serialize,Deserialize)] +#[serde(rename_all="PascalCase")] +pub struct Record { + pub event_version: String, + pub event_subscription_arn: String, + #[serde(rename="Sns")] + pub event: Event, +} + +// See: https://docs.aws.amazon.com/sns/latest/dg/json-formats.html#http-notification-json +#[derive(Serialize,Deserialize)] +#[serde(rename_all="PascalCase")] +pub struct Event { + pub message: String, + pub message_attributes: Option>, + pub message_id: String, + pub message_type: Option, + pub signature: String, + pub signature_version: String, + pub signing_cert_url: String, + pub subject: Option, + pub timestamp: DateTime, + pub topic_arn: String, + pub unsubscribe_url: String, +} + +#[derive(Serialize,Deserialize)] +pub enum EventType { + Notification +} diff --git a/src/data/sns/tests.rs b/src/data/sns/tests.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/data/tests.rs b/src/data/tests.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/lib.rs b/src/lib.rs index ce4f83b..2ef3bfb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -84,15 +84,23 @@ //! cpython = { version = "0.1", default-features = false, features = ["python27-sys"] } //! ``` +extern crate chrono; extern crate cpython; extern crate cpython_json; extern crate serde; +#[macro_use] +extern crate serde_aux; +#[macro_use] +extern crate serde_derive; extern crate serde_json; +extern crate serde_qs; #[cfg(feature = "error-chain")] #[macro_use] extern crate error_chain; +pub mod data; + #[cfg(feature = "error-chain")] mod errors { error_chain!{