From 84b6284b18403add9041c1d7b92ad271c3665244 Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Tue, 16 Jun 2020 16:58:56 +0100 Subject: [PATCH 1/5] Added SES Event type --- Sources/AWSLambdaEvents/SES.swift | 101 ++++++++++++++ .../AWSLambdaEvents/Utils/DateWrappers.swift | 29 ++++ Tests/AWSLambdaEventsTests/SESTests.swift | 128 ++++++++++++++++++ 3 files changed, 258 insertions(+) create mode 100644 Sources/AWSLambdaEvents/SES.swift create mode 100644 Tests/AWSLambdaEventsTests/SESTests.swift diff --git a/Sources/AWSLambdaEvents/SES.swift b/Sources/AWSLambdaEvents/SES.swift new file mode 100644 index 00000000..5253b49b --- /dev/null +++ b/Sources/AWSLambdaEvents/SES.swift @@ -0,0 +1,101 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import struct Foundation.Date + +// https://docs.aws.amazon.com/lambda/latest/dg/services-ses.html + +public enum SES { + public struct Event: Decodable { + public struct Record: Decodable { + public let eventSource: String + public let eventVersion: String + public let ses: Message + } + + public let records: [Record] + + public enum CodingKeys: String, CodingKey { + case records = "Records" + } + } + + public struct Message: Decodable { + public let mail: Mail + public let receipt: Receipt + } + + public struct Mail: Decodable { + public let commonHeaders: CommonHeaders + public let destination: [String] + public let headers: [Header] + public let headersTruncated: Bool + public let messageId: String + public let source: String + @ISO8601WithFractionalSecondsCoding public var timestamp: Date + } + + public struct CommonHeaders: Decodable { + public let bcc: [String]? + public let cc: [String]? + @DateTimeCoding public var date: Date + public let from: [String] + public let messageId: String + public let returnPath: String? + public let subject: String? + public let to: [String]? + } + + public struct Header: Decodable { + public let name: String + public let value: String + } + + public struct Receipt: Decodable { + public let action: Action + public let dmarcPolicy: DMARCPolicy? + public let dmarcVerdict: Verdict? + public let dkimVerdict: Verdict + public let processingTimeMillis: Int + public let recipients: [String] + public let spamVerdict: Verdict + public let spfVerdict: Verdict + @ISO8601WithFractionalSecondsCoding public var timestamp: Date + public let virusVerdict: Verdict + } + + public struct Action: Decodable { + public let functionArn: String + public let invocationType: String + public let type: String + } + + public struct Verdict: Decodable { + public let status: Status + } + + public enum DMARCPolicy: String, Decodable { + case none + case quarantine + case reject + } + + public enum Status: String, Decodable { + case pass = "PASS" + case fail = "FAIL" + case gray = "GRAY" + case processingFailed = "PROCESSING_FAILED" + } +} + diff --git a/Sources/AWSLambdaEvents/Utils/DateWrappers.swift b/Sources/AWSLambdaEvents/Utils/DateWrappers.swift index 4e24946c..62800e60 100644 --- a/Sources/AWSLambdaEvents/Utils/DateWrappers.swift +++ b/Sources/AWSLambdaEvents/Utils/DateWrappers.swift @@ -13,7 +13,9 @@ //===----------------------------------------------------------------------===// import struct Foundation.Date +import class Foundation.DateFormatter import class Foundation.ISO8601DateFormatter +import struct Foundation.Locale @propertyWrapper public struct ISO8601Coding: Decodable { @@ -67,3 +69,30 @@ public struct ISO8601WithFractionalSecondsCoding: Decodable { return formatter } } + +@propertyWrapper +public struct DateTimeCoding: Decodable { + public let wrappedValue: Date + + public init(wrappedValue: Date) { + self.wrappedValue = wrappedValue + } + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let dateString = try container.decode(String.self) + guard let date = Self.dateFormatter.date(from: dateString) else { + throw DecodingError.dataCorruptedError(in: container, debugDescription: + "Expected date to be in iso8601 date format with fractional seconds, but `\(dateString)` does not forfill format") + } + self.wrappedValue = date + } + + private static let dateFormatter: DateFormatter = Self.createDateFormatter() + private static func createDateFormatter() -> DateFormatter { + let formatter = DateFormatter() + formatter.dateFormat = "EEE, d MMM yyy HH:mm:ss z" + formatter.locale = Locale(identifier: "en_US_POSIX") + return formatter + } +} diff --git a/Tests/AWSLambdaEventsTests/SESTests.swift b/Tests/AWSLambdaEventsTests/SESTests.swift new file mode 100644 index 00000000..b5c1a7f9 --- /dev/null +++ b/Tests/AWSLambdaEventsTests/SESTests.swift @@ -0,0 +1,128 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@testable import AWSLambdaEvents +import XCTest + +class SESTests: XCTestCase { + static let eventBody = """ + { + "Records": [ + { + "eventSource": "aws:ses", + "eventVersion": "1.0", + "ses": { + "mail": { + "commonHeaders": { + "date": "Wed, 7 Oct 2015 12:34:56 -0700", + "from": [ + "Jane Doe " + ], + "messageId": "<0123456789example.com>", + "returnPath": "janedoe@example.com", + "subject": "Test Subject", + "to": [ + "johndoe@example.com" + ] + }, + "destination": [ + "johndoe@example.com" + ], + "headers": [ + { + "name": "Return-Path", + "value": "" + }, + { + "name": "Received", + "value": "from mailer.example.com (mailer.example.com [203.0.113.1]) by inbound-smtp.eu-west-1.amazonaws.com with SMTP id o3vrnil0e2ic28trm7dfhrc2v0cnbeccl4nbp0g1 for johndoe@example.com; Wed, 07 Oct 2015 12:34:56 +0000 (UTC)" + } + ], + "headersTruncated": true, + "messageId": "5h5auqp1oa1bg49b2q8f8tmli1oju8pcma2haao1", + "source": "janedoe@example.com", + "timestamp": "1970-01-01T00:00:00.000Z" + }, + "receipt": { + "action": { + "functionArn": "arn:aws:lambda:eu-west-1:123456789012:function:Example", + "invocationType": "Event", + "type": "Lambda" + }, + "dkimVerdict": { + "status": "PASS" + }, + "processingTimeMillis": 574, + "recipients": [ + "test@swift-server.com", + "test2@swift-server.com" + ], + "spamVerdict": { + "status": "PASS" + }, + "spfVerdict": { + "status": "PROCESSING_FAILED" + }, + "timestamp": "1970-01-01T00:00:00.000Z", + "virusVerdict": { + "status": "FAIL" + } + } + } + } + ] + } + """ + + func testSimpleEventFromJSON() { + let data = Data(SESTests.eventBody.utf8) + var event: SES.Event? + XCTAssertNoThrow(event = try JSONDecoder().decode(SES.Event.self, from: data)) + + guard let record = event?.records.first else { + XCTFail("Expected to have one record") + return + } + + XCTAssertEqual(record.eventSource, "aws:ses") + XCTAssertEqual(record.eventVersion, "1.0") + XCTAssertEqual(record.ses.mail.commonHeaders.date.description, "2015-10-07 19:34:56 +0000") + XCTAssertEqual(record.ses.mail.commonHeaders.from[0], "Jane Doe ") + XCTAssertEqual(record.ses.mail.commonHeaders.messageId, "<0123456789example.com>") + XCTAssertEqual(record.ses.mail.commonHeaders.returnPath, "janedoe@example.com") + XCTAssertEqual(record.ses.mail.commonHeaders.subject, "Test Subject") + XCTAssertEqual(record.ses.mail.commonHeaders.to?[0], "johndoe@example.com") + XCTAssertEqual(record.ses.mail.destination[0], "johndoe@example.com") + XCTAssertEqual(record.ses.mail.headers[0].name, "Return-Path") + XCTAssertEqual(record.ses.mail.headers[0].value, "") + XCTAssertEqual(record.ses.mail.headers[1].name, "Received") + XCTAssertEqual(record.ses.mail.headers[1].value, "from mailer.example.com (mailer.example.com [203.0.113.1]) by inbound-smtp.eu-west-1.amazonaws.com with SMTP id o3vrnil0e2ic28trm7dfhrc2v0cnbeccl4nbp0g1 for johndoe@example.com; Wed, 07 Oct 2015 12:34:56 +0000 (UTC)") + XCTAssertEqual(record.ses.mail.headersTruncated, true) + XCTAssertEqual(record.ses.mail.messageId, "5h5auqp1oa1bg49b2q8f8tmli1oju8pcma2haao1") + XCTAssertEqual(record.ses.mail.source, "janedoe@example.com") + XCTAssertEqual(record.ses.mail.timestamp.description, "1970-01-01 00:00:00 +0000") + + XCTAssertEqual(record.ses.receipt.action.functionArn, "arn:aws:lambda:eu-west-1:123456789012:function:Example") + XCTAssertEqual(record.ses.receipt.action.invocationType, "Event") + XCTAssertEqual(record.ses.receipt.action.type, "Lambda") + XCTAssertEqual(record.ses.receipt.dkimVerdict.status, .pass) + XCTAssertEqual(record.ses.receipt.processingTimeMillis, 574) + XCTAssertEqual(record.ses.receipt.recipients[0], "test@swift-server.com") + XCTAssertEqual(record.ses.receipt.recipients[1], "test2@swift-server.com") + XCTAssertEqual(record.ses.receipt.spamVerdict.status, .pass) + XCTAssertEqual(record.ses.receipt.spfVerdict.status, .processingFailed) + XCTAssertEqual(record.ses.receipt.timestamp.description, "1970-01-01 00:00:00 +0000") + XCTAssertEqual(record.ses.receipt.virusVerdict.status, .fail) + } +} From 9962becf007ca1c8320d7dbef07fc764b9a4890b Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Tue, 16 Jun 2020 17:58:16 +0100 Subject: [PATCH 2/5] Formatting --- Sources/AWSLambdaEvents/SES.swift | 17 ++- Tests/AWSLambdaEventsTests/SESTests.swift | 126 +++++++++++----------- 2 files changed, 71 insertions(+), 72 deletions(-) diff --git a/Sources/AWSLambdaEvents/SES.swift b/Sources/AWSLambdaEvents/SES.swift index 5253b49b..9c8a0f06 100644 --- a/Sources/AWSLambdaEvents/SES.swift +++ b/Sources/AWSLambdaEvents/SES.swift @@ -35,7 +35,7 @@ public enum SES { public let mail: Mail public let receipt: Receipt } - + public struct Mail: Decodable { public let commonHeaders: CommonHeaders public let destination: [String] @@ -45,7 +45,7 @@ public enum SES { public let source: String @ISO8601WithFractionalSecondsCoding public var timestamp: Date } - + public struct CommonHeaders: Decodable { public let bcc: [String]? public let cc: [String]? @@ -56,12 +56,12 @@ public enum SES { public let subject: String? public let to: [String]? } - + public struct Header: Decodable { public let name: String public let value: String } - + public struct Receipt: Decodable { public let action: Action public let dmarcPolicy: DMARCPolicy? @@ -74,23 +74,23 @@ public enum SES { @ISO8601WithFractionalSecondsCoding public var timestamp: Date public let virusVerdict: Verdict } - + public struct Action: Decodable { public let functionArn: String public let invocationType: String public let type: String } - + public struct Verdict: Decodable { public let status: Status } - + public enum DMARCPolicy: String, Decodable { case none case quarantine case reject } - + public enum Status: String, Decodable { case pass = "PASS" case fail = "FAIL" @@ -98,4 +98,3 @@ public enum SES { case processingFailed = "PROCESSING_FAILED" } } - diff --git a/Tests/AWSLambdaEventsTests/SESTests.swift b/Tests/AWSLambdaEventsTests/SESTests.swift index b5c1a7f9..0f4b417d 100644 --- a/Tests/AWSLambdaEventsTests/SESTests.swift +++ b/Tests/AWSLambdaEventsTests/SESTests.swift @@ -17,74 +17,74 @@ import XCTest class SESTests: XCTestCase { static let eventBody = """ + { + "Records": [ { - "Records": [ - { - "eventSource": "aws:ses", - "eventVersion": "1.0", - "ses": { - "mail": { - "commonHeaders": { - "date": "Wed, 7 Oct 2015 12:34:56 -0700", - "from": [ - "Jane Doe " - ], - "messageId": "<0123456789example.com>", - "returnPath": "janedoe@example.com", - "subject": "Test Subject", - "to": [ - "johndoe@example.com" - ] - }, - "destination": [ - "johndoe@example.com" - ], - "headers": [ - { - "name": "Return-Path", - "value": "" - }, - { - "name": "Received", - "value": "from mailer.example.com (mailer.example.com [203.0.113.1]) by inbound-smtp.eu-west-1.amazonaws.com with SMTP id o3vrnil0e2ic28trm7dfhrc2v0cnbeccl4nbp0g1 for johndoe@example.com; Wed, 07 Oct 2015 12:34:56 +0000 (UTC)" - } - ], - "headersTruncated": true, - "messageId": "5h5auqp1oa1bg49b2q8f8tmli1oju8pcma2haao1", - "source": "janedoe@example.com", - "timestamp": "1970-01-01T00:00:00.000Z" + "eventSource": "aws:ses", + "eventVersion": "1.0", + "ses": { + "mail": { + "commonHeaders": { + "date": "Wed, 7 Oct 2015 12:34:56 -0700", + "from": [ + "Jane Doe " + ], + "messageId": "<0123456789example.com>", + "returnPath": "janedoe@example.com", + "subject": "Test Subject", + "to": [ + "johndoe@example.com" + ] + }, + "destination": [ + "johndoe@example.com" + ], + "headers": [ + { + "name": "Return-Path", + "value": "" }, - "receipt": { - "action": { - "functionArn": "arn:aws:lambda:eu-west-1:123456789012:function:Example", - "invocationType": "Event", - "type": "Lambda" - }, - "dkimVerdict": { - "status": "PASS" - }, - "processingTimeMillis": 574, - "recipients": [ - "test@swift-server.com", - "test2@swift-server.com" - ], - "spamVerdict": { - "status": "PASS" - }, - "spfVerdict": { - "status": "PROCESSING_FAILED" - }, - "timestamp": "1970-01-01T00:00:00.000Z", - "virusVerdict": { - "status": "FAIL" - } + { + "name": "Received", + "value": "from mailer.example.com (mailer.example.com [203.0.113.1]) by inbound-smtp.eu-west-1.amazonaws.com with SMTP id o3vrnil0e2ic28trm7dfhrc2v0cnbeccl4nbp0g1 for johndoe@example.com; Wed, 07 Oct 2015 12:34:56 +0000 (UTC)" } + ], + "headersTruncated": true, + "messageId": "5h5auqp1oa1bg49b2q8f8tmli1oju8pcma2haao1", + "source": "janedoe@example.com", + "timestamp": "1970-01-01T00:00:00.000Z" + }, + "receipt": { + "action": { + "functionArn": "arn:aws:lambda:eu-west-1:123456789012:function:Example", + "invocationType": "Event", + "type": "Lambda" + }, + "dkimVerdict": { + "status": "PASS" + }, + "processingTimeMillis": 574, + "recipients": [ + "test@swift-server.com", + "test2@swift-server.com" + ], + "spamVerdict": { + "status": "PASS" + }, + "spfVerdict": { + "status": "PROCESSING_FAILED" + }, + "timestamp": "1970-01-01T00:00:00.000Z", + "virusVerdict": { + "status": "FAIL" } } - ] + } } - """ - + ] + } + """ + func testSimpleEventFromJSON() { let data = Data(SESTests.eventBody.utf8) var event: SES.Event? @@ -112,7 +112,7 @@ class SESTests: XCTestCase { XCTAssertEqual(record.ses.mail.messageId, "5h5auqp1oa1bg49b2q8f8tmli1oju8pcma2haao1") XCTAssertEqual(record.ses.mail.source, "janedoe@example.com") XCTAssertEqual(record.ses.mail.timestamp.description, "1970-01-01 00:00:00 +0000") - + XCTAssertEqual(record.ses.receipt.action.functionArn, "arn:aws:lambda:eu-west-1:123456789012:function:Example") XCTAssertEqual(record.ses.receipt.action.invocationType, "Event") XCTAssertEqual(record.ses.receipt.action.type, "Lambda") From e7eea00bab50eb47b90bb9de61f416098221f8dd Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Tue, 16 Jun 2020 18:11:12 +0100 Subject: [PATCH 3/5] Fix error message typo --- Sources/AWSLambdaEvents/Utils/DateWrappers.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/AWSLambdaEvents/Utils/DateWrappers.swift b/Sources/AWSLambdaEvents/Utils/DateWrappers.swift index 62800e60..fbef9763 100644 --- a/Sources/AWSLambdaEvents/Utils/DateWrappers.swift +++ b/Sources/AWSLambdaEvents/Utils/DateWrappers.swift @@ -83,7 +83,7 @@ public struct DateTimeCoding: Decodable { let dateString = try container.decode(String.self) guard let date = Self.dateFormatter.date(from: dateString) else { throw DecodingError.dataCorruptedError(in: container, debugDescription: - "Expected date to be in iso8601 date format with fractional seconds, but `\(dateString)` does not forfill format") + "Expected date to be in date-time format with fractional seconds, but `\(dateString)` does not forfill format") } self.wrappedValue = date } From 18c129dc95ebe0d3a8f9a1f32f34363c12ebffd5 Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Wed, 17 Jun 2020 14:48:54 +0100 Subject: [PATCH 4/5] Renamed DateTimeCoding to RFC5322DateTimeCoding --- Sources/AWSLambdaEvents/SES.swift | 2 +- Sources/AWSLambdaEvents/Utils/DateWrappers.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/AWSLambdaEvents/SES.swift b/Sources/AWSLambdaEvents/SES.swift index 9c8a0f06..4c5b3719 100644 --- a/Sources/AWSLambdaEvents/SES.swift +++ b/Sources/AWSLambdaEvents/SES.swift @@ -49,7 +49,7 @@ public enum SES { public struct CommonHeaders: Decodable { public let bcc: [String]? public let cc: [String]? - @DateTimeCoding public var date: Date + @RFC5322DateTimeCoding public var date: Date public let from: [String] public let messageId: String public let returnPath: String? diff --git a/Sources/AWSLambdaEvents/Utils/DateWrappers.swift b/Sources/AWSLambdaEvents/Utils/DateWrappers.swift index fbef9763..81ed03ec 100644 --- a/Sources/AWSLambdaEvents/Utils/DateWrappers.swift +++ b/Sources/AWSLambdaEvents/Utils/DateWrappers.swift @@ -71,7 +71,7 @@ public struct ISO8601WithFractionalSecondsCoding: Decodable { } @propertyWrapper -public struct DateTimeCoding: Decodable { +public struct RFC5322DateTimeCoding: Decodable { public let wrappedValue: Date public init(wrappedValue: Date) { @@ -83,7 +83,7 @@ public struct DateTimeCoding: Decodable { let dateString = try container.decode(String.self) guard let date = Self.dateFormatter.date(from: dateString) else { throw DecodingError.dataCorruptedError(in: container, debugDescription: - "Expected date to be in date-time format with fractional seconds, but `\(dateString)` does not forfill format") + "Expected date to be in RFC5322 date-time format with fractional seconds, but `\(dateString)` does not forfill format") } self.wrappedValue = date } From 3475d08bc1ae1db9c4e5a7dbfd03270bf396bf34 Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Wed, 17 Jun 2020 16:20:21 +0100 Subject: [PATCH 5/5] Added tests for RFC5322DateTimeCoding --- .../Utils/DateWrapperTests.swift | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/Tests/AWSLambdaEventsTests/Utils/DateWrapperTests.swift b/Tests/AWSLambdaEventsTests/Utils/DateWrapperTests.swift index b55e4bcb..88646479 100644 --- a/Tests/AWSLambdaEventsTests/Utils/DateWrapperTests.swift +++ b/Tests/AWSLambdaEventsTests/Utils/DateWrapperTests.swift @@ -79,4 +79,36 @@ class DateWrapperTests: XCTestCase { XCTAssertNil(context.underlyingError) } } + + func testRFC5322DateTimeCodingWrapperSuccess() { + struct TestEvent: Decodable { + @RFC5322DateTimeCoding + var date: Date + } + + let json = #"{"date":"Thu, 5 Apr 2012 23:47:37 +0200"}"# + var event: TestEvent? + XCTAssertNoThrow(event = try JSONDecoder().decode(TestEvent.self, from: json.data(using: .utf8)!)) + + XCTAssertEqual(event?.date.description, "2012-04-05 21:47:37 +0000") + } + + func testRFC5322DateTimeCodingWrapperFailure() { + struct TestEvent: Decodable { + @RFC5322DateTimeCoding + var date: Date + } + + let date = "Thu, 5 Apr 2012 23:47 +0200" // missing seconds + let json = #"{"date":"\#(date)"}"# + XCTAssertThrowsError(_ = try JSONDecoder().decode(TestEvent.self, from: json.data(using: .utf8)!)) { error in + guard case DecodingError.dataCorrupted(let context) = error else { + XCTFail("Unexpected error: \(error)"); return + } + + XCTAssertEqual(context.codingPath.compactMap { $0.stringValue }, ["date"]) + XCTAssertEqual(context.debugDescription, "Expected date to be in RFC5322 date-time format with fractional seconds, but `\(date)` does not forfill format") + XCTAssertNil(context.underlyingError) + } + } }