Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generic Vapor Email #2

Draft
wants to merge 14 commits into
base: main
Choose a base branch
from
6 changes: 3 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
// swift-tools-version:5.2
// swift-tools-version:5.6
import PackageDescription

let package = Package(
name: "email",
platforms: [
.macOS(.v10_15)
.macOS(.v12),
],
products: [
.library(name: "Email", targets: ["Email"]),
],
dependencies: [
.package(url: "https://github.com/vapor/vapor.git", from: "4.0.0")
.package(url: "https://github.com/vapor/vapor.git", from: "4.66.1"),
],
targets: [
.target(name: "Email", dependencies: [
Expand Down
60 changes: 60 additions & 0 deletions Sources/Email/Application+Emails.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import Vapor

extension Application {
public struct Emails {
public struct Provider {
let run: (Application) -> ()

public init(_ run: @escaping (Application) -> ()) {
self.run = run
}
}

let application: Application

final class Storage {
var makeEmailClient: ((Application) -> EmailClient)?
public init() { }
}

struct Key: StorageKey {
typealias Value = Storage
}

var storage: Storage {
if self.application.storage[Key.self] == nil {
self.initialize()
}

return self.application.storage[Key.self]!
}

func initialize() {
self.application.storage[Key.self] = .init()
}

public func use(_ makeEmailClient: @escaping ((Application) -> EmailClient)) {
self.storage.makeEmailClient = makeEmailClient
}

public func use(_ provider: Provider) {
provider.run(application)
}

public var client: EmailClient {
guard let factory = self.storage.makeEmailClient else {
fatalError("EmailClient is not configured, use: app.emails.use()")
}

return factory(application)
}
}

public var emails: Emails {
.init(application: self)
}

public var email: EmailClient {
emails.client
}
}
9 changes: 9 additions & 0 deletions Sources/Email/Content.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Foundation

extension EmailMessage {
public enum Content: Codable, Equatable {
case text(String)
case html(String)
case universal(text: String, html: String)
}
}
43 changes: 43 additions & 0 deletions Sources/Email/EmailAddress.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import Vapor

public struct EmailAddress: Content, Equatable {
public let email: String
public let name: String?

public init(email: String, name: String? = nil) {
self.email = email
self.name = name
}

/// Returns the email address formated as "Name <[email protected]>"
public var fullAddress: String {
if let name = self.name {
return "\(name) <\(email)>"
}
return email
}
}

extension EmailAddress: CustomStringConvertible {
public var description: String {
self.fullAddress
}
}

extension EmailAddress: ExpressibleByStringLiteral {
madsodgaard marked this conversation as resolved.
Show resolved Hide resolved
public init(stringLiteral value: String) {
// "From: $Name <$Email>"
let split = value.components(separatedBy: " <")
guard
split.count == 2,
let name = split.first,
var emailPart = split.last,
emailPart.removeLast() == ">"
else {
self = .init(email: value)
return
}

self = .init(email: emailPart, name: name)
}
}
15 changes: 15 additions & 0 deletions Sources/Email/EmailAddressRepresentable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
public protocol EmailAddressRepresentable {
var email: String { get }
var name: String? { get }
}

extension EmailAddressRepresentable {
public var emailAddress: EmailAddress {
if let name = self.name {
return .init(email: self.email, name: name)
} else {
return .init(email: self.email)
}
}
}

6 changes: 6 additions & 0 deletions Sources/Email/EmailAttachment.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import Vapor

public enum EmailAttachment: Codable, Equatable {
case attachment(Vapor.File)
case inline(Vapor.File)
}
12 changes: 12 additions & 0 deletions Sources/Email/EmailClient.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Vapor

public protocol EmailClient {
func send(_ messages: [EmailMessage]) -> EventLoopFuture<Void>
func delegating(to eventLoop: EventLoop) -> Self
}

extension EmailClient {
public func send(_ message: EmailMessage) -> EventLoopFuture<Void> {
self.send([message])
}
}
199 changes: 199 additions & 0 deletions Sources/Email/EmailMessage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import Vapor

public struct EmailMessage: Codable, Equatable {
public let from: EmailAddress
public let to: [EmailAddress]
public let replyTo: EmailAddress?
public let cc: [EmailAddress]?
public let bcc: [EmailAddress]?
public let subject: String
public let content: Content
public let attachments: [EmailAttachment]?

/// Intitialize new message with a single recipient.
public init(
from: EmailAddress,
to: EmailAddress,
replyTo: EmailAddress? = nil,
cc: [EmailAddress]? = nil,
bcc: [EmailAddress]? = nil,
subject: String,
content: Content,
attachments: [EmailAttachment]? = nil
) {
self.init(
from: from,
to: [to],
replyTo: replyTo,
cc: cc,
bcc: bcc,
subject: subject,
content: content,
attachments: attachments
)
}

/// Intitialize new message with a single recipient conforming to `EmailAddressRepresentable`
public init<E>(
from: EmailAddress,
to: E,
replyTo: EmailAddress? = nil,
cc: [EmailAddress]? = nil,
bcc: [EmailAddress]? = nil,
subject: String,
content: Content,
attachments: [EmailAttachment]? = nil
)
where E: EmailAddressRepresentable
{
self.init(
from: from,
to: [to],
replyTo: replyTo,
cc: cc,
bcc: bcc,
subject: subject,
content: content,
attachments: attachments
)
}

/// Intitialize new message with multiple recipients conforming to `EmailAddressRepresentable`
public init<E>(
from: EmailAddress,
to: [E],
replyTo: EmailAddress? = nil,
cc: [EmailAddress]? = nil,
bcc: [EmailAddress]? = nil,
subject: String,
content: Content,
attachments: [EmailAttachment]? = nil
)
where E: EmailAddressRepresentable
{
self.init(
from: from,
to: to.map { $0.emailAddress },
replyTo: replyTo,
cc: cc,
bcc: bcc,
subject: subject,
content: content,
attachments: attachments
)
}

/// Intitialize new message a single recipient conforming to `EmailAddressRepresentable`
public init<E>(
from: String,
to: E,
replyTo: String? = nil,
cc: [String]? = nil,
bcc: [String]? = nil,
subject: String,
content: Content,
attachments: [EmailAttachment]? = nil
)
where E: EmailAddressRepresentable
{
self.init(
from: from,
to: [to],
replyTo: replyTo,
cc: cc,
bcc: bcc,
subject: subject,
content: content,
attachments: attachments
)
}

/// Intitialize new message with multiple recipients conforming to `EmailAddressRepresentable`
public init<E>(
from: String,
to: [E],
replyTo: String? = nil,
cc: [String]? = nil,
bcc: [String]? = nil,
subject: String,
content: Content,
attachments: [EmailAttachment]? = nil
)
where E: EmailAddressRepresentable
{
self.init(
from: from,
to: to.map { $0.emailAddress.fullAddress },
replyTo: replyTo,
cc: cc,
bcc: bcc,
subject: subject,
content: content,
attachments: attachments
)
}

public init(
from: String,
to: String,
replyTo: String? = nil,
cc: [String]? = nil,
bcc: [String]? = nil,
subject: String,
content: Content,
attachments: [EmailAttachment]? = nil
) {
self.init(
from: from,
to: [to],
replyTo: replyTo,
cc: cc,
bcc: bcc,
subject: subject,
content: content,
attachments: attachments
)
}

public init(
from: String,
to: [String],
replyTo: String? = nil,
cc: [String]? = nil,
bcc: [String]? = nil,
subject: String,
content: Content,
attachments: [EmailAttachment]? = nil
) {
self.init(
from: EmailAddress(stringLiteral: from),
to: to.map(EmailAddress.init),
replyTo: replyTo.map(EmailAddress.init),
cc: cc?.map(EmailAddress.init),
bcc: bcc?.map(EmailAddress.init),
subject: subject,
content: content,
attachments: attachments
)
}

public init(
from: EmailAddress,
to: [EmailAddress],
replyTo: EmailAddress? = nil,
cc: [EmailAddress]? = nil,
bcc: [EmailAddress]? = nil,
subject: String,
content: Content,
attachments: [EmailAttachment]? = nil
) {
self.from = from
self.to = to
self.replyTo = replyTo
self.cc = cc
self.bcc = bcc
self.subject = subject
self.content = content
self.attachments = attachments
}
}
7 changes: 7 additions & 0 deletions Sources/Email/Request+Email.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Vapor

extension Request {
public var email: EmailClient {
application.email.delegating(to: eventLoop)
}
}
18 changes: 18 additions & 0 deletions Tests/EmailTests/EmailAddressTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import XCTest
@testable import Email
import XCTVapor

final class EmailTests: XCTestCase {
func testInitEmailAdressByString() throws {
let emailAddressWithEmail: EmailAddress = "[email protected]"
XCTAssertEqual(emailAddressWithEmail.email, "[email protected]")
XCTAssertNil(emailAddressWithEmail.name)

let emailAddressWithEmailAndName: EmailAddress = "Vapor <[email protected]>"
XCTAssertEqual(emailAddressWithEmailAndName.email, "[email protected]")
XCTAssertEqual(emailAddressWithEmailAndName.name, "Vapor")

let emailAddressIncorrectFormat: EmailAddress = "name<[email protected]>asd"
XCTAssertEqual(emailAddressIncorrectFormat.email, "name<[email protected]>asd")
}
}