Last active
November 7, 2023 16:32
-
-
Save stephanecopin/9b5da27e06f1f59e07630502d555ed46 to your computer and use it in GitHub Desktop.
A small set of struct/extensions to easily handle `mailto:` links in Swift 5.1.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import MessageUI | |
struct EmailParameters { | |
/// Guaranteed to be non-empty | |
let toEmails: [String] | |
let ccEmails: [String] | |
let bccEmails: [String] | |
let subject: String? | |
let body: String? | |
/// Defaults validation is just verifying that the email is not empty. | |
static func defaultValidateEmail(_ email: String) -> Bool { | |
return !email.isEmpty | |
} | |
/// Returns `nil` if `toEmails` contains at least one email address validated by `validateEmail` | |
/// A "blank" email address is defined as an address that doesn't only contain whitespace and new lines characters, as defined by CharacterSet.whitespacesAndNewlines | |
/// `validateEmail`'s default implementation is `defaultValidateEmail`. | |
init?( | |
toEmails: [String], | |
ccEmails: [String], | |
bccEmails: [String], | |
subject: String?, | |
body: String?, | |
validateEmail: (String) -> Bool = defaultValidateEmail | |
) { | |
func parseEmails(_ emails: [String]) -> [String] { | |
return emails.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }.filter(validateEmail) | |
} | |
let toEmails = parseEmails(toEmails) | |
let ccEmails = parseEmails(ccEmails) | |
let bccEmails = parseEmails(bccEmails) | |
if toEmails.isEmpty { | |
return nil | |
} | |
self.toEmails = toEmails | |
self.ccEmails = ccEmails | |
self.bccEmails = bccEmails | |
self.subject = subject | |
self.body = body | |
} | |
/// Returns `nil` if `scheme` is not `mailto`, or if it couldn't find any `to` email addresses | |
/// `validateEmail`'s default implementation is `defaultValidateEmail`. | |
/// Reference: https://tools.ietf.org/html/rfc2368 | |
init?(url: URL, validateEmail: (String) -> Bool = defaultValidateEmail) { | |
guard let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) else { | |
return nil | |
} | |
let queryItems = urlComponents.queryItems ?? [] | |
func splitEmail(_ email: String) -> [String] { | |
return email.split(separator: ",").map(String.init) | |
} | |
let initialParameters = (toEmails: urlComponents.path.isEmpty ? [] : splitEmail(urlComponents.path), subject: String?(nil), body: String?(nil), ccEmails: [String](), bccEmails: [String]()) | |
let emailParameters = queryItems.reduce(into: initialParameters) { emailParameters, queryItem in | |
guard let value = queryItem.value else { | |
return | |
} | |
switch queryItem.name { | |
case "to": | |
emailParameters.toEmails += splitEmail(value) | |
case "cc": | |
emailParameters.ccEmails += splitEmail(value) | |
case "bcc": | |
emailParameters.bccEmails += splitEmail(value) | |
case "subject" where emailParameters.subject == nil: | |
emailParameters.subject = value | |
case "body" where emailParameters.body == nil: | |
emailParameters.body = value | |
default: | |
break | |
} | |
} | |
self.init( | |
toEmails: emailParameters.toEmails, | |
ccEmails: emailParameters.ccEmails, | |
bccEmails: emailParameters.bccEmails, | |
subject: emailParameters.subject, | |
body: emailParameters.body, | |
validateEmail: validateEmail | |
) | |
} | |
private final class MailComposeDelegate: NSObject, MFMailComposeViewControllerDelegate { | |
static var mailDelegateKey: UInt8 = 0 | |
let finishAction: MailComposeFinishAction | |
init(_ finishAction: @escaping MailComposeFinishAction = { viewController, _ in viewController.dismiss(animated: true) }) { | |
self.finishAction = finishAction | |
} | |
func mailComposeController(_ viewController: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { | |
if let result = MailComposeResult(result) { | |
self.finishAction(viewController, .success(result)) | |
} else { | |
self.finishAction(viewController, .failure(error ?? NSError(domain: MFMailComposeError.errorDomain, code: MFMailComposeError.sendFailed.rawValue))) | |
} | |
} | |
} | |
enum MailComposeResult { | |
case cancelled | |
case saved | |
case sent | |
var toMFMailComposeResult: MFMailComposeResult { | |
switch self { | |
case .cancelled: | |
return .cancelled | |
case .saved: | |
return .saved | |
case .sent: | |
return .sent | |
} | |
} | |
init?(_ result: MFMailComposeResult) { | |
switch result { | |
case .cancelled: | |
self = .cancelled | |
case .saved: | |
self = .saved | |
case .sent: | |
self = .sent | |
case .failed: | |
return nil | |
@unknown default: | |
return nil | |
} | |
} | |
} | |
typealias MailComposeFinishAction = (MFMailComposeViewController, Result<MailComposeResult, Error>) -> Void | |
func showMailCompose<ViewController: UIViewController>( | |
from viewController: ViewController, | |
showAction: (ViewController, MFMailComposeViewController) -> Void = { $0.present($1, animated: true) }, | |
finishAction: @escaping MailComposeFinishAction = { viewController, _ in viewController.dismiss(animated: true) }) | |
{ | |
guard let mailComposeViewController = MFMailComposeViewController.mailComposeViewController(emailParameters: self) else { | |
UIApplication.shared.open(self.mailToURL, options: [:], completionHandler: nil) | |
return | |
} | |
let delegate = MailComposeDelegate(finishAction) | |
mailComposeViewController.mailComposeDelegate = delegate | |
objc_setAssociatedObject(mailComposeViewController, &MailComposeDelegate.mailDelegateKey, delegate, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) | |
showAction(viewController, mailComposeViewController) | |
} | |
var mailToURL: URL { | |
var urlComponents = URLComponents() | |
urlComponents.scheme = "mailto" | |
if toEmails.count == 1 { | |
urlComponents.path = toEmails[0] | |
} | |
var queryItems: [URLQueryItem] = [] | |
if toEmails.count > 1 { | |
queryItems.append(URLQueryItem(name: "to", value: toEmails[1...].joined(separator: ", "))) | |
} | |
if !ccEmails.isEmpty { | |
queryItems.append(URLQueryItem(name: "cc", value: ccEmails[1...].joined(separator: ", "))) | |
} | |
if !bccEmails.isEmpty { | |
queryItems.append(URLQueryItem(name: "bcc", value: bccEmails[1...].joined(separator: ", "))) | |
} | |
if let subject = self.subject { | |
queryItems.append(URLQueryItem(name: "subject", value: subject)) | |
} | |
if let body = self.body { | |
queryItems.append(URLQueryItem(name: "body", value: body)) | |
} | |
if !queryItems.isEmpty { | |
urlComponents.queryItems = queryItems | |
} | |
return urlComponents.url! | |
} | |
} | |
extension URL { | |
/// `validateEmail`'s default implementation is `EmailParameters.defaultValidateEmail`. | |
func parseRFC2368MailTo(validateEmail: (String) -> Bool = EmailParameters.defaultValidateEmail) -> EmailParameters? { | |
return EmailParameters(url: self, validateEmail: validateEmail) | |
} | |
} | |
extension MFMailComposeViewController { | |
/// Returns `nil` if `mailToURL` is not a valid `mailto` URL, or if `MFMailComposeViewController.canSendMail()` returns `false` | |
/// `validateEmail`'s default implementation is `EmailParameters.defaultValidateEmail`. | |
static func mailComposeViewController(mailToURL: URL, validateEmail: (String) -> Bool = EmailParameters.defaultValidateEmail) -> MFMailComposeViewController? { | |
guard let emailParameters = EmailParameters(url: mailToURL, validateEmail: validateEmail) else { | |
return nil | |
} | |
return self.mailComposeViewController(emailParameters: emailParameters) | |
} | |
/// Returns `nil` if `MFMailComposeViewController.canSendMail()` returns `false` | |
static func mailComposeViewController(emailParameters: EmailParameters) -> MFMailComposeViewController? { | |
guard MFMailComposeViewController.canSendMail() else { | |
return nil | |
} | |
let mailComposeViewController = MFMailComposeViewController() | |
mailComposeViewController.setToRecipients(emailParameters.toEmails) | |
mailComposeViewController.setCcRecipients(emailParameters.ccEmails.isEmpty ? nil : emailParameters.ccEmails) | |
mailComposeViewController.setBccRecipients(emailParameters.bccEmails.isEmpty ? nil : emailParameters.bccEmails) | |
if let subject = emailParameters.subject { | |
mailComposeViewController.setSubject(subject) | |
} | |
if let body = emailParameters.body { | |
mailComposeViewController.setMessageBody(body, isHTML: false) | |
} | |
return mailComposeViewController | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment