Stone Design Stone Design
News Download Buy Software About Stone
software

OS X & Cocoa Writings by Andrew C. Stone     ©1995-2003 Andrew C. Stone

It's In The [e]Mail
Sending rich mail from your program
©2001 Andrew C. Stone All Rights Reserved

    The network is the computer they say, and nowadays, our applications need to be more connected to both other applications and the internet at large. The Pasteboard and Services seamlessly handle interprocess communication, and these will be the subject of a forthcoming article. This month's column will focus on different strategies to mail plain or rich data with or without the intervention of a user interface. I'll also show you how to open a browser to any web address - such as a technical support or feedback page. We'll use Carbon's InternetConfig wrapped up in an easy to use Cocoa class as well as Mail's AppleScript API to send data longer than the 255 character limit of InternetConfig.
    
    The approach you take depends first on whether you want the user to be able to review the mail before it's sent, or if you want to send it with no user intervention using the Message framework. For the most part, we have reverted to the first model, because being a privacy advocate I abhor "secret behind my back" data transfers. Moreover, we have found that unless a user has properly configured their mail setup, the Message framework won't work. Recently added to this framework is the class method + (BOOL)hasDeliveryClassBeenConfigured, which returns YES if the delivery accounts have been configured, so this lets you determine if the Message framework will work at all.
    
NSMailDelivery for behind the scenes

     Adding the Message framework is certainly the simplest way to send mail - and the NSMailDelivery.h API has only 3 methods! I like to further hide the implementation in a simple class named Mail. Let's say you have a scrolling rich text object, an NSTextView named reportTextView, which may even have graphics and multiple fonts. To send the contents of this text object, as a bug report, for example:
    
- (void) makeABugReport:(id)sender
{
// This will correctly make the subject of the letter contain the App name:
NSString *subject = [NSString stringWithFormat:@"%@ Bug Report",
[[NSProcessInfo processInfo] processName]];

[Mail sendRichMail:[reportTextView textStorage] to:@"bugs@stone.com" subject:subject];
}

Here's the Mail class:
//
// Mail.h
//
// (C) Copyright 1989-2001 Andrew C. Stone and Stone Design Corp
// All rights reserved
//


@interface Mail : NSObject

+ (BOOL)sendRichMail:(NSAttributedString *)richBody to:(NSString *)to subject:(NSString *)subject;

+ (BOOL)sendMail:(NSString *)body to:(NSString *)to subject:(NSString *)subject;

@end

//
// Mail.m
//
// (C) Copyright 1989-2001 Andrew C. Stone and Stone Design Corp
// All rights reserved
//

#import <Cocoa/Cocoa.h>
#import <Message/NSMailDelivery.h>
#import "Mail.h"

@implementation Mail

// run genstrings to extract these macros into a file named Strings.strings
// then, to localize to another language, add a localized variant in PB, then translate the copy:

#define EMAIL_TIT NSLocalizedStringFromTable(@"E-Mail", @"Strings", "alert title about email")
#define THANK_YOU NSLocalizedStringFromTable(@"Thank you!", @"Strings", "alert msg about thanks")

+ (BOOL)sendRichMail:(NSAttributedString *)richBody to:(NSString *)to subject:(NSString *)subject isMIME:(BOOL)isMIME
{
NSMutableDictionary *toFromDict = [NSMutableDictionary dictionaryWithObjectsAndKeys:
to,@"to",subject,@"subject",NSFullUserName(),@"from",NULL];

NSString *from = [[NSUserDefaults standardUserDefaults] objectForKey:@"IDEmail"];
BOOL result = YES;
if (NOT_NULL(from)) [toFromDict setObject:from forKey:@"from"];


// Can we even use the mail framework?
if (![NSMailDelivery hasDeliveryClassBeenConfigured]) {
    NSLog(@"NSMailDelivery: not configured");
    // here you could notify the user, and then launch System Preferences to have them configure it...
    return NO;
}

// we use error handling in case there is trouble in the NSMailDelivery framework

NS_DURING

// first we try SMPT, then if that fails, we try sendmail:

if ([NSMailDelivery deliverMessage:richBody headers:toFromDict
format:isMIME?NSMIMEMailFormat:NSASCIIMailFormat
protocol:NSSMTPDeliveryProtocol] ||
[NSMailDelivery deliverMessage:richBody headers:toFromDict
format:isMIME?NSMIMEMailFormat:NSASCIIMailFormat
protocol:NSSendmailDeliveryProtocol])
NSRunAlertPanel(EMAIL_TIT,THANK_YOU,OK,NULL,NULL);

NS_HANDLER

NSLog(@"NSMailDelivery: an exception was raised: %@",[localException reason]);
result = NO;

NS_ENDHANDLER

return result;
}

// Both of these go through the one single method - factoring is good programming practice!

+ (BOOL)sendRichMail:(NSAttributedString *)richBody to:(NSString *)to subject:(NSString *)subject
{
return [self sendRichMail:richBody to:to subject:subject isMIME:YES];
}

+ (BOOL)sendMail:(NSString *)body to:(NSString *)to subject:(NSString *)subject;
{
NSAttributedString *richBody = [[NSAttributedString alloc] initWithString:body];
return [self sendRichMail:richBody to:to subject:subject isMIME:NO];
}

@end


InternetConfig: Use the browser!

Because we cannot rely on the user having their mail configured correctly, we need an alternative to sending mail. Stone Design took the approach of using stone.com as the mail delivery agent - since we can control that configuration! In today's world, it's more sure that the user will have the web browser configured than their email. Being able to open a web address is easy via a simple Cocoa wrapper class "InternetLauncher" which hides all the lower level details. (Are you seeing a pattern here? Hide the details!)

- (IBAction)goToStoneWebSite:(id)sender {
[InternetLauncher launchURL:@"http://www.stone.com"];
}

And here is the header and implementation:

//
// InternetLauncher.h
//
// Created by andrew on Fri Sep 22 2000.
// Thanks to OmniGroup for original code.
//

#import <Foundation/Foundation.h>


@interface InternetLauncher : NSObject {
id internetConfigInstance;
}

+ (void)launchURL:(NSString *)urlString;
- (void)launchURL:(NSString *)urlString;

@end

//
// InternetLauncher.m
//
// Created by andrew on Fri Sep 22 2000.
// Thanks to OmniGroup for original code.
//

#import <Foundation/Foundation.h>
#import <CoreFoundation/CoreFoundation.h>
#import <Carbon/Carbon.h>
#import "InternetLauncher.h"


@implementation InternetLauncher

// this is a macro which take a 4 character code
// and returns a single unsigned int for use with types and creators:

#define MY_APPLICATION_SIGNATURE FOUR_CHAR_CODE('OWEB')

+ (void)launchURL:(NSString *)urlString {
id internetConfig = [[InternetLauncher alloc] init];
[internetConfig launchURL:urlString];
[internetConfig release];
}

- init;
{
ICInstance anInstance;

if (![super init])
return nil;
if (ICStart(&anInstance, MY_APPLICATION_SIGNATURE != noErr))
return nil;
internetConfigInstance = (id)anInstance;
return self;
}

- (void)dealloc;
{
ICStop((ICInstance)internetConfigInstance);
[super dealloc];
}

- (void)launchURL:(NSString *)urlString;
{
const char *urlCString;
long start, length;
OSStatus error;

urlCString = [urlString UTF8String];
start = 0;
length = strlen(urlCString);
error = ICLaunchURL((ICInstance)internetConfigInstance, NULL, (Ptr)urlCString, length, &start, &length);
if (error != noErr)
[NSException raise:@"InternetConfigException" format:@"ICLaunchURL returned an error while launching URL %@: 0x%x (%d)", urlString, error, error];
}

@end

Now that we can send any arbitrary URL to our browser, why not concoct a valid mailto which will start the user's preferred mailer and fill out all the fields? One reason, as stated above, is that URLs are limited to 255 characters so there's a ceiling to how much data you can include. Moreover, if you want to send attachments, then this technique is not for you - I'll show you how to do that via AppleScript scripted directly from within your application!

Besides those limitations, you must include all the escape characters required to embed newlines, tabs, percents, etc. Once again, Apple has done the work for us all down in Core Foundation - there's a function which returns an escaped string for us: CFURLCreateStringByAddingPercentEscapes(). As usual, wrap it into a method so your head doesn't hurt when you read code:

- (NSString *)stringForMail:(NSAttributedString *)s {
// let Core Foundation figure it out!
return (NSString *)CFURLCreateStringByAddingPercentEscapes(NULL,
    (CFStringRef)[s string], NULL,NULL,kCFStringEncodingUTF8);
}

Using the same bug report IB action as before, we'd have:

- (void) makeABugReport:(id)sender
{
// This will correctly make the subject of the letter contain the App name:
NSString *subject = [NSString stringWithFormat:@"%@ Bug Report",
[[NSProcessInfo processInfo] processName]];

[self sendMail:[[reportTextView textStorage] string] to:@"bugs@stone.com" subject:subject];
}

And here's the implementation of this version:

- (BOOL)sendMail:(NSString *)body to:(NSString *)to subject:(NSString *)subject name:(NSString *)client {

// we need to escape percent many things -
// returns, tabs, unicode chars, ?, %, etc...

NSString *s = [NSString stringWithFormat:@"mailto:%@?subject=%@&body=%@",[self stringForMail:to],[self stringForMail:subject],[self stringForMail:body]];

// you should probably check the length of the string at this point!
// see below for an alternate strategy!
if ([s length] > 255) return NO;

// call our launcher to initialize an email:
[InternetLauncher launchURL:s];
}
    
    
AppleScript Mail On The Fly!

The cadillac of all mailing techniques when you have graphics, text and attachments, and optionally want to allow the user to further edit the message or headers is using Mail's builtin AppleScriptability. Mail ships on every Mac OS X system and it's reputed to be Steve Job's favorite killer app. Our task becomes writing a valid AppleScript script, and then executing it in the same manner that Script Editor does. Valid Mail AppleScript is defined by Mail's unique AppleScript API, which you can find by choosing Script Editor's "Open Dictionary..." command under the File menu. Here's a sampling of Mail's sending API:

Mail suite: Mail specific classes.

Class compose message: A new email message
Plural form:
    
compose messages
Elements:
    
recipient by numeric index, before/after another element, satisfying a test, as a range of elements, by name
    
bcc recipient by numeric index, before/after another element, satisfying a test, as a range of elements, by name
    
cc recipient by numeric index, before/after another element, satisfying a test, as a range of elements, by name
    
to recipient by numeric index, before/after another element, satisfying a test, as a range of elements, by name
Properties:
    
content anything -- Contents of an email message
    sender
string -- The sender of the message
    subject
string -- Subject of email message
    class
integer [r/o] -- The class of the object.

Class message editor: The user interface for an email message
Plural form:
    
message editors
Elements:
    
compose message by numeric index, before/after another element, satisfying a test, as a range of elements, by name
Properties:
    
class integer [r/o] -- The class of the object.


Message suite: Message specific classes.

send: Sends a message
    send reference -- the object for the command


Class recipient:
An email recipient

Plural form:
    
recipients
Properties:
    
address anything -- The recipients email address
    display name
string -- The name used for display
    class
integer [r/o] -- The class of the object.


Class cc recipient: An email recipient in the CC field
Plural form:
    
cc recipients
Properties:
    
address anything -- The recipients email address
    display name
string -- The name used for display
    class
integer [r/o] -- The class of the object.

Class to recipient: An email recipient in the To field
Plural form:
    
to recipients
Properties:
    
address anything -- The recipients email address
    display name
string -- The name used for display
    class
integer [r/o] -- The class of the object.

So a sample of valid AppleScript to send a simple text message with a logo would be:

tell application "Mail"
    set bodyvar to "Thank you for your concern in writing!"
    set addrNameVar to "Andrew Graunke"
    set addrVar to "info@whitehouse.gov"
    set subjectvar to "We're listening!"
    set composeMessage to (a reference to (make new compose message at beginning of compose messages))
    tell composeMessage
        make new to recipient at beginning of to recipients with properties {address:addrVar, display name:addrNameVar}
        set the subject to subjectvar
        set the content to bodyvar
        set aFile to "/Applications/Mail.app/Contents/Resources/mbox.icns"
        tell content
            make new text attachment with properties {file name:aFile} at before the first word of the first paragraph
        end tell
    end tell
    make new message editor at beginning of message editors
    set compose message of first message editor to composeMessage
end tell

Typical Cocoa code to build this script is used in Stone Design's TimeEqualsMoney - this sample code shows how to send an attachment. In TimeEqualsMoney, the very first object in the text is the company logo if the message is MIME. We'll extract the rest of the message using NSString's substringFromIndex:1 to skip over the logo, and then later incorporate it in. Note you can allow the user to review the message or just send it right away!


- (NSString *)mailScriptBody:(NSString *)body to:(NSString *)to subject:(NSString *)subject isMIME:(BOOL)isMIME name:(NSString *)clientName sendNow:(BOOL)sendWithoutUserReview {
NSString *cc = @"";

    // start with a scratch string of a decent length:
NSMutableString *s = [NSMutableString stringWithCapacity:1000];
NSString *logoPath = @"";

// must skip over the image:
if (isMIME) body = [body substringFromIndex:1];

[s appendString:@"tell application \"Mail\"\n"];
[s appendString:[NSString stringWithFormat:@"set bodyvar to \"%@\"\n",body]];
[s appendString:[NSString stringWithFormat:@"set addrNameVar to \"%@\"\n",clientName]];
[s appendString:[NSString stringWithFormat:@"set addrVar to \"%@\"\n",to]];

if (NOT_NULL(cc)) {
[s appendString:[NSString stringWithFormat:@"set ccNameVar to \"%@\"\n",cc]];
[s appendString:[NSString stringWithFormat:@"set ccVar to \"%@\"\n",cc]];
}

[s appendString:[NSString stringWithFormat:@"set subjectvar to \"%@\"\n",subject]];

[s appendString:@"set composeMessage to (a reference to (make new compose message at beginning of compose messages))\n"];
[s appendString:@"tell composeMessage\n"];

[s appendString:@"make new to recipient at beginning of to recipients with properties {address:addrVar, display name:addrNameVar}\n"];
if (NOT_NULL(cc)) {
[s appendString:@"make new cc recipient at beginning of cc recipients with properties {address:ccVar, display name:ccNameVar}\n"];
}
[s appendString:@"set the subject to subjectvar\n"];
[s appendString:@"set the content to bodyvar\n"];
if (isMIME && NOT_NULL(logoPath) && [[NSFileManager defaultManager] fileExistsAtPath:logoPath]) {
[s appendString:[NSString stringWithFormat:@"set aFile to \"%@\"\n",logoPath]];
[s appendString:@"tell content\n"];
[s appendString:@"make new text attachment with properties {file name:aFile} at before the first word of the first paragraph\n"];
[s appendString:@"end tell\n"];

}

[s appendString:@"end tell\n"];

if (sendWithoutUserReview) {
    [s appendString:@"send composeMessage\n"];
} else {
    [s appendString:@"make new message editor at beginning of message editors\n"];
    [s appendString:@"set compose message of first message editor to composeMessage\n"];
}
    
[s appendString:@"end tell\n"];

// uncomment next line to see your AppleScript in the console:
// NSLog(s);
return s;
}

So, we now have the valid applescript in hand - how do we execute this from within a Cocoa application? We'll pass the valid Mail script to a routine, -runScript:, which wraps up the intricacies of Apple event descriptors. Luckily, Wim Lewis of OmniGroup shared the following code with me, with the caveat that it's not necessarily production code. However, it works great! Be sure to add the ComponentInstance myComponent to your class, and to initialize it as below in your object's designated initializer, which is -init for most objects.


- (BOOL)sendMail:(NSString *)richBody to:(NSString *)to subject:(NSString *)subject isMIME:(BOOL)isMIME name:(NSString *)client sendNow:(BOOL)sendWithoutUserReview{
[self runScript:[self mailScriptBody:richBody to:to subject:subject isMIME:isMIME name:client sendNow: sendWithoutUserReview]];
return YES;
}


// Import Carbon.h, and add an ivar to your class's .h:
// You could roll this into the Mail class as presented above

#import <Carbon/Carbon.h>

{
ComponentInstance myComponent;

}

// initialize it in your init method:

- (id)init {
self = [super init];
if (self) {
myComponent = OpenDefaultComponent(kOSAComponentType, kOSAGenericScriptingComponentSubtype);
        // other initialization code here
}
return self;
}

// do the grunge work -


// if you want check point log info, define CHECK to the next line, uncommented:
#define CHECK
// NSLog(@"result code = %d", ok);

// This converts an AEDesc into a corresponding NSValue.

static id aedesc_to_id(AEDesc *desc)
{
OSErr ok;

if (desc->descriptorType == typeChar)
{
NSMutableData *outBytes;
NSString *txt;

outBytes = [[NSMutableData alloc] initWithLength:AEGetDescDataSize(desc)];
ok = AEGetDescData(desc, [outBytes mutableBytes], [outBytes length]);
CHECK;

txt = [[NSString alloc] initWithData:outBytes encoding:[NSString defaultCStringEncoding]];
[outBytes release];
[txt autorelease];

return txt;
}

if (desc->descriptorType == typeSInt16)
{
SInt16 buf;

AEGetDescData(desc, &buf, sizeof(buf));

return [NSNumber numberWithShort:buf];
}

return [NSString stringWithFormat:@"[unconverted AEDesc, type=\"%c%c%c%c\"]", ((char *)&(desc->descriptorType))[0], ((char *)&(desc->descriptorType))[1], ((char *)&(desc->descriptorType))[2], ((char *)&(desc->descriptorType))[3]];

}

// the sweetly wrapped method is all we need to know:

- (void)runScript:(NSString *)txt
{
NSData *scriptChars = [txt dataUsingEncoding:[NSString defaultCStringEncoding]];
AEDesc source, resultText;
OSAID scriptId, resultId;
OSErr ok;

// Convert the source string into an AEDesc of string type.
ok = AECreateDesc(typeChar, [scriptChars bytes], [scriptChars length], &source);
CHECK;

// Compile the source into a script.
scriptId = kOSANullScript;
ok = OSACompile(myComponent, &source, kOSAModeNull, &scriptId);
AEDisposeDesc(&source);
CHECK;


// Execute the script, using defaults for everything.
resultId = 0;
ok = OSAExecute(myComponent, scriptId, kOSANullScript, kOSAModeNull, &resultId);
CHECK;

if (ok == errOSAScriptError) {
AEDesc ernum, erstr;
id ernumobj, erstrobj;

// Extract the error number and error message from our scripting component.
ok = OSAScriptError(myComponent, kOSAErrorNumber, typeShortInteger, &ernum);
CHECK;
ok = OSAScriptError(myComponent, kOSAErrorMessage, typeChar, &erstr);
CHECK;

// Convert them to ObjC types.
ernumobj = aedesc_to_id(&ernum);
AEDisposeDesc(&ernum);
erstrobj = aedesc_to_id(&erstr);
AEDisposeDesc(&erstr);

txt = [NSString stringWithFormat:@"Error, number=%@, message=%@", ernumobj, erstrobj];
} else {
// If no error, extract the result, and convert it to a string for display

if (resultId != 0) { // apple doesn't mention that this can be 0?
ok = OSADisplay(myComponent, resultId, typeChar, kOSAModeNull, &resultText);
CHECK;

//NSLog(@"result thingy type = \"%c%c%c%c\"", ((char *)&(resultText.descriptorType))[0], ((char *)&(resultText.descriptorType))[1], ((char *)&(resultText.descriptorType))[2], ((char *)&(resultText.descriptorType))[3]);

txt = aedesc_to_id(&resultText);
AEDisposeDesc(&resultText);
} else {
txt = @"[no value returned]";
}
OSADispose(myComponent, resultId);
}

ok = OSADispose(myComponent, scriptId);
CHECK;
}

Conclusion

Being able to send mail programmatically is a fundamental need of modern software - and we've seen several ways to do it. A Mac OS X programmer can choose to use only Cocoa methods or Carbon functions. However, we are not limited to just one or the other. We can mix and match as needed, or even use the lower level C API of CoreFoundation. Because some functionality is available only under Carbon, it behooves the Cocoa developer to become familiar with this rich, albeit legacy, API. By wrapping your Carbon calls in Objective C objects and methods, you hide the complexity and fully embrace the object oriented design pattern.

Andrew Stone is founder of Stone Design Corp <http://www.stone.com/> and divides his time between farming on the earth and in cyperspace.

PreviousTopIndexNext
©1997-2005 Stone Design top