Skip to content

Commit

Permalink
SendGrid client
Browse files Browse the repository at this point in the history
  • Loading branch information
Toby Griffin committed Feb 16, 2017
1 parent fa13766 commit 7a704f3
Show file tree
Hide file tree
Showing 23 changed files with 755 additions and 0 deletions.
11 changes: 11 additions & 0 deletions Package.swift
Expand Up @@ -2,6 +2,17 @@ import PackageDescription

let package = Package(
name: "Mail",
targets: [
Target(
name: "Mail"
),
Target(
name: "SendGrid",
dependencies: [
"Mail"
]
),
],
dependencies: [
.Package(url: "https://github.com/vapor/vapor.git", majorVersion: 1, minor: 5),
]
Expand Down
47 changes: 47 additions & 0 deletions README.md
Expand Up @@ -6,6 +6,7 @@ Vapor Provider for sending email through swappable backends.

Backends included in this repository:

* `SendGrid`, a fully-featured implementation of the SendGrid V3 Mail Send API.
* `InMemoryMailClient`, a development-only backend which stores emails in memory.
* `ConsoleMailClient`, a development-only backend which outputs emails to the console.

Expand Down Expand Up @@ -50,6 +51,52 @@ if let complicatedMailer = try drop.mailer.make() as? ComplicatedMailClient {
}
```

### SendGrid backend

First, set up the Provider.

```Swift
import Mail
import SendGrid

let drop = Droplet()
try drop.addProvider(Mail.Provider<SendGridClient>.self)
```

SendGrid expects a configuration file named `sendgrid.json` with the following
format, and will throw `.noSendGridConfig` or `.missingConfig(fieldname)` if
configuration was not found.

```json
{
"apiKey": "SG.YOUR_KEY"
}
```

Once installed, you can send simple emails using the following format:

```Swift
let email = Email(from: , to: , subject: , body: )
try drop.mailer?.send(email)
```

However, `SendGrid` supports the full range of options available in SendGrid's
[V3 Mail Send API](https://sendgrid.com/docs/API_Reference/Web_API_v3/Mail/index.html).

```Swift
let email = SendGridEmail(from: "from@test.com", templateId: "welcome_email")
email.personalizations.append(Personalization([
to: "to@test.com"
]))
email.sandboxMode = true
email.openTracking = .enabled(nil)
if let sendgrid = try drop.mailer.make() as? SendGridClient {
sendgrid.send(email)
}
```

See `SendGridEmail.swift` for all configuration options.

### Development backends

There are two options for testing your emails in development.
Expand Down
2 changes: 2 additions & 0 deletions Sources/Mail/ConsoleMailClient/ConsoleMailClient.swift
Expand Up @@ -13,6 +13,8 @@ public final class ConsoleMailClient: MailClientProtocol {

public static func configure(_ config: Config) throws {}

public static func boot(_ drop: Droplet) {}

public init() {}

public func send(_ emails: [SMTP.Email]) throws {
Expand Down
2 changes: 2 additions & 0 deletions Sources/Mail/InMemoryMailClient/InMemoryMailClient.swift
Expand Up @@ -14,6 +14,8 @@ public final class InMemoryMailClient: MailClientProtocol {

public static func configure(_ config: Config) throws {}

public static func boot(_ drop: Droplet) {}

public init() {}

public func send(_ emails: [SMTP.Email]) throws {
Expand Down
6 changes: 6 additions & 0 deletions Sources/Mail/MailClientProtocol.swift
Expand Up @@ -29,6 +29,12 @@ public protocol MailClientProtocol {
*/
static func configure(_ config: Config) throws

/*
Called during the Provider's `boot(_:)` method. Use this method to
store a reference to the Droplet, if you need it.
*/
static func boot(_ drop: Droplet)

/*
MailClient must be able to init without arguments. Store configuration
loaded by the Provider in static vars on your MailClient, and throw if
Expand Down
1 change: 1 addition & 0 deletions Sources/Mail/Provider.swift
Expand Up @@ -10,6 +10,7 @@ public final class Provider<T: MailClientProtocol>: Vapor.Provider {
public init() throws {}

public func boot(_ drop: Droplet) {
T.boot(drop)
if let existing = drop.mailer {
print("\(String(describing: T.self)) will overwrite existing mailer: \(String(describing: existing))")
}
Expand Down
14 changes: 14 additions & 0 deletions Sources/SendGrid/Models/ClickTracking.swift
@@ -0,0 +1,14 @@
public enum ClickTracking {
/*
Do not track clicks
*/
case disabled
/*
Track clicks in HTML emails only
*/
case htmlOnly
/*
Track clicks in HTML and plain text emails
*/
case enabled
}
13 changes: 13 additions & 0 deletions Sources/SendGrid/Models/EmailAddress+Node.swift
@@ -0,0 +1,13 @@
import SMTP
import Vapor

extension EmailAddress: NodeRepresentable {

public func makeNode(context: Context) throws -> Node {
guard let name = name else {
return Node(["email": Node(address)])
}
return Node(["name": Node(name), "email": Node(address)])
}

}
22 changes: 22 additions & 0 deletions Sources/SendGrid/Models/Footer.swift
@@ -0,0 +1,22 @@
/*
Footer to append to the email. Can be either plaintext or HTML.
*/
public struct Footer {
public enum ContentType {
case html, plain
}

public let type: ContentType
public let content: String

public init(type: ContentType, content: String) {
self.type = type
self.content = content
}
}

extension Footer: Equatable {}
public func ==(lhs: Footer, rhs: Footer) -> Bool {
return lhs.type == rhs.type
&& lhs.content == rhs.content
}
15 changes: 15 additions & 0 deletions Sources/SendGrid/Models/GoogleAnalytics.swift
@@ -0,0 +1,15 @@
public struct GoogleAnalytics {
let source: String?
let medium: String?
let term: String?
let content: String?
let campaign: String?

public init(source: String?, medium: String, term: String, content: String?, campaign: String?) {
self.source = source
self.medium = medium
self.term = term
self.content = content
self.campaign = campaign
}
}
12 changes: 12 additions & 0 deletions Sources/SendGrid/Models/OpenTracking.swift
@@ -0,0 +1,12 @@
public enum OpenTracking {
/*
Do not track opens
*/
case disabled
/*
Track opens in emails.
substitutionTag: This tag will be replaced by the open tracking pixel.
*/
case enabled(substitutionTag: String?)
}
68 changes: 68 additions & 0 deletions Sources/SendGrid/Models/Personalization.swift
@@ -0,0 +1,68 @@
import Foundation
import SMTP
import Vapor

public struct Personalization {

/*
The email recipients
*/
public let to: [EmailAddress]
/*
The email copy recipients
*/
public let cc: [EmailAddress] = []
/*
The email blind copy recipients
*/
public let bcc: [EmailAddress] = []
/*
The email subject, overriding that of the Email, if set
*/
public let subject: String? = nil
/*
Custom headers
*/
public let headers: [String: String] = [:]
/*
Custom substitutions in the format ["tag": "value"]
*/
public let substitutions: [String: String] = [:]
/*
Date to send the email, or `nil` if email to be sent immediately
*/
public let sendAt: Date? = nil

public init(to: [EmailAddress]) {
self.to = to
}

}

extension Personalization: NodeRepresentable {

public func makeNode(context: Context) throws -> Node {
var node = Node([:])
node["to"] = try Node(to.map { try $0.makeNode() })
if !cc.isEmpty {
node["cc"] = try Node(cc.map { try $0.makeNode() })
}
if !bcc.isEmpty {
node["bcc"] = try Node(bcc.map { try $0.makeNode() })
}
if let subject = subject {
node["subject"] = Node(subject)
}
if !headers.isEmpty {
node["headers"] = try headers.makeNode()
}
if !substitutions.isEmpty {
node["substitutions"] = try substitutions.makeNode()
}
if let sendAt = sendAt {
node["send_at"] = Node(sendAt.timeIntervalSince1970)
}
return node
}

}
14 changes: 14 additions & 0 deletions Sources/SendGrid/Models/SendGridEmail+Email.swift
@@ -0,0 +1,14 @@
import SMTP

extension SendGridEmail {

/*
Convert a basic Vapor SMTP.Email into a SendGridEmail
*/
public convenience init(from: Email) {
self.init(from: from.from, subject: from.subject, body: from.body)
attachments = from.attachments
personalizations = [Personalization(to: from.to)]
}

}

0 comments on commit 7a704f3

Please sign in to comment.