Programatically sending a HTML email in macOS
One of the neat things about SwiftUI is that you are able to use some of the built-in wrappers for functionalities we would have previously had to manually write ourselves.
However, the other side of that sentiment is that SwiftUI for macOS compared to iOS or iPadOS feels to be running at a different speed.
macOS still needs to leverage AppKit or Objective-C, which is completely understandable since there is a lot more freedom in what apps exist, how they were installed (or from where), among many other things.
In my localisation app, Get Local, one of the features I wanted to add was a support email function.
The issue I encountered was embedding HTML into the mailto:
string was not rendering correctly, or at least it wasn't converting the HTML into rendered HTML. This occurred for both Mail.app
and for Outlook.app
. Testing this in iOS did render correctly.
What I was starting with
This is the code which I was working with. My two current versions were using the Link()
from SwiftUI and NSSharingService()
from AppKit.
Neither of these did render the HTML
let body = "<strong>Please describe your issue below:</strong><br><br><hr><strong>Operating system:</strong> \(ProcessInfo.processInfo.operatingSystemVersionString)"
// option 1
let mailto = "mailto:me@domain.ltd?subject=Test%20Email&body=\(body)"
Link("Send an email", destination: mailto)
// option 2
func sendEmail(to: [String], subject: String, message: String) {
let service = NSSharingService(named: .composeEmail)!
service.recipients = [to]
service.subject = subject
service.perform(withItems: [message])
}
sendEmail(to: ["me@domain.ltd], subject: "Test Email", message: body)
First attempt
My first intention was to dig into setting a Content-Type
or Mime-Type
when composing the email.
This would let me force the HTML email body type more than letting the systems decide themselves.
Unfortunately, I couldn't find anything that was helpful for SwiftUI.
Research
I did some deep diving, opening every search result into new tabs, and reading everything.
Sometimes it takes going into page 4 or 5 in the search results to find an answer.
I came across this blog post from 2013 about sending rich text emails.
Unfortunately, for us Swift users the post was written in Objective-C. However, fortunately there are many resources for converting Objective-C to Swift - particularly now with ChatGPT, Bard, or other models.
When I originally found this solution though it was 27/10/2022 and ChatGPT came out one month later
Original Objective-C
// create an attributed string
NSString* htmlText = @"<html><body>Hello, <b>World</b>!</body></html>";
NSData* textData = [NSData dataWithBytes:[htmlText UTF8String] length:[htmlText lengthOfBytesUsingEncoding:NSUTF8StringEncoding]];
NSAttributedString* textAttributedString = [[NSAttributedString alloc] initWithHTML:textData options:options documentAttributes:nil];
// create a file to attach
NSUUID* uuid = [NSUUID new];
NSString* tempDir = [NSTemporaryDirectory() stringByAppendingPathComponent:[uuid UUIDString]];
NSFileManager* fm = [NSFileManager new];
[fm createDirectoryAtPath:tempDir withIntermediateDirectories:YES attributes:nil error:nil];
NSString* tempFile = [tempDir stringByAppendingPathComponent:@"report.csv"];
NSURL* tempFileURL = [NSURL fileURLWithPath:tempFile];
NSData* csv = ...; // generate the data here
[csv writeToURL:tempFileURL atomically:NO];
// share it
NSSharingService* mailShare = [NSSharingService sharingServiceNamed:NSSharingServiceNameComposeEmail];
NSArray* shareItems = @[textAttributedString,tempFileURL];
[mailShare performWithItems:shareItems];
Converted to Swift
I manually converted it into Swift which resulted into this:
// Create an attributed string
let htmlText = "<html><body>Hello, <b>World</b>!</body></html>"
guard let textData = htmlText.data(using: .utf8) else {
fatalError("Failed to convert HTML to data")
}
let textAttributedString = try? NSAttributedString(data: textData, options: [.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil)
// Create a file to attach
let uuid = UUID()
let tempDir = NSTemporaryDirectory().appending(uuid.uuidString)
let fm = FileManager()
try? fm.createDirectory(atPath: tempDir, withIntermediateDirectories: true, attributes: nil)
let tempFile = tempDir.appendingPathComponent("report.csv")
let tempFileURL = URL(fileURLWithPath: tempFile)
let csv = Data() // Generate the data here
try? csv.write(to: tempFileURL)
// Share it
guard let mailShare = NSSharingService(named: .composeEmail) else { fatalError("Email sharing service is not available") }
let shareItems: [Any] = [textAttributedString, tempFileURL]
mailShare.perform(withItems: shareItems)
Basic HTML email
This was the code I ended up using for the original example:
let body = "<strong>Please describe your issue below:</strong><br><br><hr><strong>Operating system:</strong> \(ProcessInfo.processInfo.operatingSystemVersionString)"
func sendEmail(to: [String], subject: String, message: String) {
let service = NSSharingService(named: .composeEmail)!
service.recipients = [to]
service.subject = subject
// convert String to NSAttributedString
let data = message.data(using: .utf8)
let attributedString = NSAttributedString(html: data!, documentAttributes: nil)
service.perform(withItems: [attributedString!])
}
sendEmail(to: ["me@domain.ltd], subject: "Test Email", message: body)
If you found this article helpful, you can keep the ideas flowing by supporting me. Buy me a coffee or check out my apps to help me create more content like this!
Coffee Check out my apps