Hardcore Clarion - Email-enabling your Applications with Simple MAPI
Posted March 1 1999
With computers and the Internet becoming more and more prevalent in society, people are becoming more accustomed to using email, surfing the 'Web etc. One of the things that you as a developer can do (and that I've been asked for more than once) is to add email support to your application.
As with most things in life there are many means to obtain the same goal. In the case of email support the easiest is to use one of the 3rd party tools that already exist. However, being the sort of person I am, I'd rather implement my own interface - it may take more time than I'd like, but I have exact control over everything and get to understand the underlying philosophy as well.
Unfortunately, as with most things, there can be quite a lot to learn. Once you understand the basic principles it's easy, but getting there can be tough, so this month I'm just going to provide a quick overview on the basic API calls you'll need to use to implement email support.
Which interface?
There are a number of different interfaces that a developer can use to add email functionality to his application. Unfortunately not all of these options are available to a Clarion developer. Here's what you can and can't use, and why:MAPI (Mail API)
The base mail system upon which the other 3 options are based. It offers very high control over all aspects of the mail system, but is also fully a object-orientated COM-compliant implementation. What this means to us as developers (bearing in mind that Clarion currently isn't COM-compliant) is that basically we can't use it. If there are features that you want that the other options can't address, then there's probably a VBX or OCX that can.
CMC (Common Messaging Calls)
Simple MAPI (Simple Mail API)
CMC and SMAPI provide a simple set of about a dozen high-level API calls that Clarion developers can use. The features provided by SMAPI and CMC are nearly identical, but CMC is slightly more compact, combining multiple features in a single function. Although the Microsoft implementation is based on MAPI, the CMC API itself was developed in conjunction with the X.400 API Association standards organization, and is supported on Windows, MS-DOS, OS/2, Macintosh, and UNIX platforms.
AML (Active Message Library)
AML is an OLE automation programming interface that is a component of the MAPI interface. The AML furnishes programmable objects, like Microsoft Excel objects and Microsoft Access objects, that make available properties and methods that can then be managed by Visual Basic (VB) and Visual Basic for Applications (VBA) programs. Again, because Clarion is COM and OLE challenged (I'm being PC here), it means we can't use it.
As far as I can tell, that leaves us with the choice of CMC or SMAPI. As I described above, they are basically identical except that CMC is cross-platform and SMAPI isn't. Seeing as how we can only create Windows applications anyway, choosing CMC for it's cross platform support isn't high on my decision tree.
With that in mind, I'm going to describe the basics of using
Simple MAPI (SMAPI) from a Clarion program. There are 12 Simple
MAPI functions - I'm going to use 5 of them to send and receive
mail. They're all basically the same, so if you want to use the
ones I don't, they'll take similar parameters to the ones you'll
see listed below.
|
Simple MAPI
function
|
Description
|
| MAPIAddress | Addresses a message. |
| MAPIDeleteMail | Deletes a message. |
| MAPIDetails | Displays a recipient-details dialog box. |
| MAPIFindNext | Returns the identifier of the first or next message of a specified type. |
| MAPIFreeBuffer | Frees memory allocated by the messaging system. |
| MAPILogoff | Ends a session with the messaging system. |
| MAPILogon | Establishes a messaging session. |
| MAPIReadMail | Reads a message. |
| MAPIResolveName | Displays a dialog box to resolve an ambiguous recipient name. |
| MAPISaveMail | Saves a message. |
| MAPISendDocuments | Sends a standard message using a dialog box. |
| MAPISendMail | Sends a message, allowing greater flexibility than MAPISendDocuments() in message generation. |
Is our client interface installed?
Although we've just decided on our mail interface, not all computers may have it installed, so we should really determine if it is and disable the relevant menu options if not. We can test for the presence of each of the 4 mail interfaces by using the GETINI procedure on WIN.INI. The relevant section will resemble the following - if the interface is installed, it's option will be set to 1. If it is not installed, it's option either won't be there, or will be set to 0[Mail] ;MAPI MAPIX=0 or 1 ;CMC CMC=0 or 1 ;Simple MAPI MAPI=0 or 1 ;AML OLEMessaging=0 or 1What our application should really do is test for the presence of the desired interface and, once detected, call the Windows API functions LoadLibrary() and GetProcAddress() on the relevant DLL's. Once we've done that, we can call the procedures we need. On the other hand, it's a lot easier as a developer if we link in the MAPI import library to our application. Of course, if we do that and MAPI isn't installed, the application will fail to run. So, what do we do? Morally we should not link against the MAPI DLL (in case it's not installed), but technically it's a lot easier to.
My theory on this is two-fold. Firstly, virtually every new PC comes pre-installed with some flavour of Windows, and email support is virtually guaranteed. Secondly, if you are developing email support in your application at the request of a client, then he should have some email system installed anyway.
As you can probably tell from my reasoning, I link my application against an import library. Don't go looking for one on your machine, because you probably won't find it. It's not shipped with Clarion because Clarion doesn't offer SMAPI support, and it's not in your \WINDOWS directory because Microsoft isn't in the habit of shipping import libraries for it's DLLs. To get the .LIB file we need, you'll have to run LibMaker. This handy utility ships with nearly every version of Clarion in one form or another, and simply creates a .LIB so that you can link against the original .DLL. To make your .LIB, run LibMaker and go hunt for MAPI32.DLL (or MAPI.DLL if you're coding 16-bit). It'll probably be in \WINDOWS\SYSTEM (or \WINNT\SYSTEM if you're running Windows NT).
Sessions
The interface between your application and the SMAPI subsystem is based on the concept of sessions. Before your client application can call an underlying messaging system, it must establish a session, or connection, with the MAPI subsystem. Sessions are initiated when a user logs on, a process that consists of accessing a valid profile, validating messaging system and messaging service credentials, and insuring that all of the profile's message services are properly configured.Clients can specify one of two types of sessions to be established in the logon call: an individual session or a shared session. Individual sessions are private connections; there is a one-to-one relationship between a client application and the session it is using. As a consequence, client applications sharing a session also share a profile. Shared sessions are established once but can be "used" by other client applications who need to use them.
Logging on and creating a new MAPI session
To create a new session you will need to use the MAPILogon() function. It's API prototype is:ULONG FAR PASCAL MAPILogon( ULONG ulUIParam, LPTSTR lpszProfileName, LPTSTR lpszPassword, FLAGS flFlags, ULONG ulReserved, LPLHANDLE lplhSession )
Its Clarion prototypes are:MAPILogon(ULONG,ULONG,ULONG,ULONG,ULONG,*ULONG),ULONG,RAW,PASCAL
The parameters take some explaining:
- ulUIParam is the Prop:Handle of the owner window, or 0 if you want a application modal dialog to appear.
- lpszProfileName is a pointer to a null-terminated profile name string. This is the profile to use when logging on. If the lpszProfileName parameter is NULL or points to an empty string, and the flFlags parameter is set to MAPI_LOGON_UI, MAPILogon() displays a logon dialog box with an empty name field
- lpszPassword is a pointer to a null-terminated password. If the messaging system does not require password credentials, or if it requires that the user enter them, the lpszPassword parameter should be NULL or point to an empty string. When the user must enter credentials, the flFlags parameter must be set to MAPI_LOGON_UI to allow a logon dialog box to be displayed.
- LpFlags is a bitmask of flag options:
| MAPI_FORCE_DOWNLOAD | Attempt to download all of the user's messages before returning. If the MAPI_FORCE_DOWNLOAD flag is not set, messages can be downloaded in the background after the function call returns. |
| MAPI_NEW_SESSION | Attempt to create a new session rather than acquire the environment's shared session. If the MAPI_NEW_SESSION flag is not set, MAPILogon uses an existing shared session. |
| MAPI_LOGON_UI | A logon dialog box should be displayed to prompt the user for logon information. If the user needs to provide a password and profile name to enable a successful logon, MAPI_LOGON_UI must be set. |
| MAPI_PASSWORD_UI | MAPILogon should only prompt for a password and not allow the user to change the profile name. Either MAPI_PASSWORD_UI or MAPI_LOGON_UI should not be set, since the intent is to select between two different dialog boxes for logon. |
- lplhSession contains the session handle on a successful logon, and you'll need this (obviously) for all the subsequent MAPI functions.
LogOn FUNCTION (Prm:UserName,Prm:Password)
LocalVars Group,Pre(Loc)
ulMapiStatus ULong
ulMapiSession ULong
End
CODE
Loc:ulMapiStatus = MAPILogon( 0,|
Address(Prm:Username),|
Address(Prm:Password),|
MAPI_LOGON_UI + MAPI_NEW_SESSION,|
0,|
Loc:ulMapiSession)
IF Loc:ulMapiStatus = SUCCESS_SUCCESS Then
Return(Loc:ulMapiSession)
Else
Return(0)
End
This has the effect of logging you on to MAPI. It always creates a
new session and always displaying a dialog box to the user. If the
logon is successful, you'll return the session handle back to the
calling procedure.
If you always know what the username and password are, you can change the flFlags parameter to not display a UI, but to simply create a new session. However, in my experience it's always easier (at least while you're still developing the application) to simply show a dialog box and let the user fill it in. That way you don't have to worry about the user incorrectly specifying the username and password in your setup information, and the problems that you'll get from that.
Sending email
To send email we use the MAPISendMail() procedure. It's API prototype isULONG FAR PASCAL MAPISendMail( LHANDLE lhSession, ULONG ulUIParam, lpMapiMessage lpMessage, FLAGS flFlags, ULONG ulReserved )It's Clarion prototype is:
MAPISendMail(ULONG,ULONG,*GROUP,ULONG,ULONG),ULONG,RAW,PASCAL
Again, the parameters need some explaining:
- lhSession is the handle to the session (returned by MAPILogon())
- ulUIParam is the Prop:Handle of the owner window, or 0 if you want a application modal dialog to appear.
- lpMessage is a pointer to a MapiMessage structure containing the message to be sent. If the MAPI_DIALOG flag is not set, the nRecipCount and lpRecips members must be valid for successful message delivery. All unused pointers should be NULL
- flFlags is a bitmask of option flags. It's 3 options (MAPI_DIALOG, MAPI_LOGON_UI and MAPI_NEW_SESSION ) are the same as for MAPILogon().
- ulReserved is reserved and must be zero.
lpMessge is a group, whose structure is
MapiMessageType GROUP,TYPE
ulReserved ULONG ! Reserved, must be 0.
lpszSubject ULONG ! Pointer to subject, CSTRING max 256 chars.
lpszNoteText ULONG ! Pointer to text, CSTRING, CR at paragraphs.
lpszMessageType ULONG ! Pointer to message type, null is default.
lpszDateReceived ULONG ! Pointer to string in YYY/MM/DD format.
lpszConversationID ULONG ! Ignored.
flFlags ULONG ! 1 - MAPI_UNREAD
! 2 - MAPI_RECEIPT_REQUESTED
! 3 - MAPI_SENT
lpOriginator ULONG ! Pointer to MapiRecipDesc group.
nRecipCount ULONG ! Count of groups pointed to by lpRecips.
lpRecips ULONG ! Pointer to array of recipient descriptor groups.
nFileCount ULONG ! Count of file attachment groups pointed to by lpFiles.
lpFiles ULONG ! Pointer to array of file attachment groups.
END
When sending email, you must first determine if your application is
responsible for prompting the user for subject, destination address
and mail text , or whether you want MAPI to display it's own
window. If the former, you must fill in the respective fields of
the group and call MAPISendMail() without theMAPI_DIALOGflag set.
If you want MAPI to do all the work for you (and why not?) then
simply set theMAPI_DIALOGflag. That will display the standard
Microsoft email window, and frees you from the burden of writing
yet another window. One thing I've found in the past is that even
if you do set the MAPI_DIALOG flag, you still have to provide one
destination address (described below) otherwise SendMail() returns
an 'invalid recipients' error.
If you don't want MAPI to do all the work, it gets rather messy rather quickly. You have to prime the lpszSubject and lpszNoteText fields with the ADDRESS() of the respective CSTRING, and you have to manage the destination addresses manually. They work on the principle of having a count (the number of people the mail is going to), and a pointer to an array of groups (one for each person). So, to send a mail message to someone, you have to set nRecipCount to 1, and set lpRecips to the ADDRESS() of structure holding the address of the person you are sending it to. That structure looks like this:
MapiRecipDescType GROUP,TYPE ! RECIPIENT DESCRIPTOR
ulReserved ULONG ! Reserved, must be 0.
ulRecipClass ULONG ! Recipients role in message
! 0 - MAPI_ORIG
! 1 - MAPI_TO
! 2 - MAPI_CC
! 3 - MAPI_BCC
lpszName ULONG ! Pointer to diplayed name of recipient.
lpszAddress ULONG ! Pointer to recipient's mail address.
ulEIDSize ULONG ! Size (bytes) of binary data in lpEntryID.
lpEntryID ULONG ! Pointer to binary data representing the recipient.
END
In practice I've found the only fields you need to set in this
structure are ulRecipClass (to MAPI_TO) and lpszName as the
ADDRESS() of the CSTRING holding their name. If you also want to CC
or BCC someone else, you'll have to increment the nRecipCount in
the original group, and set the ulRecipClass to MAPI_CC or MAPI_BCC
in the nth copy of the group. Everything else you can leave
as 0 or NULL.
If you are sending attachments, it gets messier still. Attachments work on the same principle as destination addresses - you have a count (the number of attachments), and a pointer to an array of groups (one for each attachment). The attachment group is as follows:
MapiFileDescType GROUP,TYPE ! FILE DESCRIPTOR
ulReserved ULONG(0) ! Reserved, must be 0.
flFlags ULONG(0) ! Ignored.
nPosition ULONG ! where in the message text to place the file
lpszPathName ULONG ! Pointer to full pathname of attached file.
lpszFileName ULONG ! Pointer to file name seen by user.
lpFileType ULONG(0) ! Reserved, must be NULL.
END
As you can see, you can leave virtually all the fields blank,
except for lpszPathName, nPosition and lpszFileName. I have found
in the past that it's a good idea not to pass fully qualified
pathnames in lpszPathName, because you get problems on the
receiving end if the path doesn't exist. Instead, I'd try using
PATH() to save the current path, SETPATH() to the path where the
file is, then SETPATH() back again once you've called
MAPISendMail().
The only other problem with attachments is in setting the nPosition field. This field determines where the attachment is placed, so if you set it to 1, the attachment overwrites the first character of the text. What I've done in the past is add an extra character to the end of the text, then set the nPosition field to the position of that character.
Checking for / receiving email
One of the drawbacks of Simple MAPI is it's lack of features (Duh! that's why it's called Simple).This lack of features shows itself in the fact that you can only check for mail in the main incoming mail directory. If you're a power user who uses Inbox Assistant to redirect your mail to different folders, Simple MAPI won't be able to read the mail in those folders, only the main inbox.
However, if that's all you want to do, then this is how you do it. This time, thought, you'll need three functions:
ULONG FAR PASCAL MAPIFindNext( LHANDLE lhSession, ULONG ulUIParam, LPTSTR lpszMessageType, LPTSTR lpszSeedMessageID, FLAGS flFlags, ULONG ulReserved, LPTSTR lpszMessageID ) ULONG FAR PASCAL MAPIReadMail( LHANDLE lhSession, ULONG ulUIParam, LPTSTR lpszMessageID, FLAGS flFlags, ULONG ulReserved, lpMapiMessage FAR * lppMessage ) ULONG FAR PASCAL MAPIFreeBuffer( LPVOID pv )Their respective Clarion prototypes are:
MAPIFindNext(ULONG,ULONG,ULONG,ULONG,ULONG,ULONG,ULONG),ULONG,RAW,PASCAL MAPIReadMail(ULONG,ULONG,ULONG,ULONG,ULONG,ULONG),ULONG,RAW,PASCAL MAPIFreeBuffer(ULONG),ULONG,RAW,PASCALMAPIFindNext() finds the next unread email, and passes back a message identifier. MAPIReadMail() takes that identifier and actually gets the mail (marking it as read in the process). MAPIFreeBuffer() frees the buffer that was allocated for the mail. Not freeing the buffer leads to memory leaks, which over time can bring the machine to it's knees. Always remember to free the buffer!
We start the process by calling MAPIFindNext():
- lhSession is the handle to the session (returned by MAPILogon())
- ulUIParam is the Prop:Handle of the owner window, or 0 if you want a application modal dialog to appear.
- lpszMessageType is a pointer to a string identifying the message class to search. To find an interpersonal message (IPM), specify NULL in the lpszMessageType parameter or have it point to an empty string.
- LpszMessageSeedID is a pointer to a string containing the message identifier seed for the request. If the lpszSeedMessageID parameter is NULL or points to an empty string, MAPIFindNext retrieves the first message that matches the type specified in the lpszMessageType parameter.
- flFlags is yet another bitmask of flags.
|
The message identifiers returned should be in the order of time received. MAPIFindNext calls can take longer if this flag is set. Some implementations cannot honor this request and return the MAPI_E_NO_SUPPORT value. |
| MAPI_LONG_MSGID | The returned message identifier can be
as long as 512 characters. If this flag is set, the
lpszMessageID parameter must be large enough to accomodate
512 characters.
Older versions of MAPI supported smaller message identifiers (64 bytes) and did not include this flag. MAPIFindNext will succeed without this flag set as long as lpszMessageID is large enough to hold the message identifier. If lpszMessageID cannot hold the message identifier, MAPIFindNext will fail. |
| MAPI_UNREAD_ONLY | Only unread messages of the specified type should be enumerated. If this flag is not set, MAPIFindNext can return any message of the specified type. |
- LpszMessageID is returned, and is the identifier for the first unread message found.
FindNextUnread FUNCTION
LocalVars Group,Pre(Loc)
ulMapiStatus ULong
MessageType CString(10)
MessageID CString(515)
End
CODE
Loc:MessageType = ''
Loc:ulMapiStatus = MapiFindNext(SMAPI:Session,|
0,|
0,|
0,|
MAPI_UNREAD_ONLY+MAP
0,|
Address(Loc:MessageI
If Loc:ulMapiStatus <> SUCCESS_SUCCESS Then
Clear(Loc:MessageID)
End
Return(Clip(Loc:MessageID))
- Once we've found the next new email, the next thing we need to do is actually read the message. The parameters for MAPIReadMail() are:
- lhSession is the handle to the session (returned by MAPILogon())
- ulUIParam is the Prop:Handle of the owner window, or 0 if you want a application modal dialog to appear.
- lpszMessageID is a pointer to a message identifier string (IE the identifier)
- flFlags is yet another bitmask of flags.
|
|
|
|
|
|
|
|
lppMessage is a pointer to a message structure (the same structure we used when sending the email). The structure can be freed with a single call to the MAPIFreeBuffer() function. When MAPI_ENVELOPE_ONLY and MAPI_SUPPRESS_ATTACH are not set, attachments are written to temporary files pointed to by the lpFiles member of the MapiMessage structure. It is the caller's responsibility to delete these files when they are no longer needed.
The code looks something similar to:
ReadMail FUNCTION (Prm:MessageID)
LocalVars Group,Pre(Loc)
ulMapiStatus ULong
MessageType CString(10)
MessageID CString(100)
MailTitle CString(255)
MailSender CString(255)
MailText CString(8192)
MessagePtrPtr ULong
MessagePtr ULong
End
RxMessage Like(MapiMessageType) ! message info
RxSender Like(MapiRecipDescType) ! sender info
RxFile Like(MapiFileDescType) ! attached file info
CODE
If SMAPI:LoggedOn Then
Loc:MessageID = Clip(Prm:MessageID) & Chr(0)
Clear(RxMessage)
Loc:MessagePtrPtr = Address(RxMessage)
Loc:ulMapiStatus = MAPIReadMail( SMAPI:Session,|
0,|
Address(Loc:Me
MAPI_SUPPRESS_
0,|
Loc:MessagePtr
If Loc:ulMapiStatus = SUCCESS_SUCCESS Then
MemCpy( Address(Loc:MessagePtr) , Loc:MessagePtrPtr , 4)
MemCpy( Address(RxMessage) , Loc:MessagePtr , Size(RxMessage) )
If RxMessage:lpszSubject <> 0 Then
MemCpy( Address(Loc:MailTitle) , RxMessage:lpszSubject , 100 )
Else
Loc:MailTitle = 'Untitled'
End
If RxMessage:lpszNoteText <> 0 Then
memcpy( Address(Loc:MailText) , RxMessage:lpszNoteText , 100 )
Else
Clear(Loc:MailText)
End
If RxMessage:lpOriginator <> 0 Then
memcpy( Address(RxSender), RxMessage:lpOriginator, Size(RxSender) )
If RxSender:lpszAddress <> 0 Then
memcpy( Address(Loc:MailSender), RxSender:lpszAddress, 100 )
End
Else
Clear(Loc:MailSender)
End
SMAPI:Rx:MailTitle = Clip(Loc:MailTitle)
SMAPI:Rx:MailText = Clip(Loc:MailText)
SMAPI:Rx:MailSender = Clip(Loc:MailSender)
Loc:ulMapiStatus = MAPIFreeBuffer(Loc:MessagePtr)
If Loc:ulMapiStatus <> SUCCESS_SUCCESS Then
SMAPI:ErrorString = MapiError(Loc:ulMapiStatus)
End
End
End
Return(Loc:ulMapiStatus)
- If you look carefully at the code, you'll see that we end up with a pointer to a pointer to the message structure. Once we know that we read the message successfully, we use MemCpy to copy the address of the structure back into the first pointer. We copy 4 bytes because that's the size of a long, which is basically what a pointer is in Clarion. Once we have a pointer to the message structure, we de-reference it again and actually copy the data from memory into a message structure so that's its easier to work with. Once the email has been read (and replied to if necessary), we need to free the message structure.
Logging off from MAPI
The last thing you have to do is logoff and close the session. The API prototype is:ULONG FAR PASCAL MAPILogoff ( LHANDLE lhSession, ULONG ulUIParam, FLAGS flFlags, ULONG ulReserved )It's Clarion prototype is:
MAPILogoff(ULONG,ULONG,ULONG,ULONG),ULONG,PASCAL,RAW
Compared to everything else to do with MAPI, the code to implement it is very easy.
LogOff PROCEDURE
LocalVars Group,Pre(Loc)
ulMapiStatus ULong
End
CODE
Loc:ulMapiStatus = MAPILogoff( SMAPI:Session,|
0,|
0,|
0)
IF Loc:ulMapiStatus = SUCCESS_SUCCESS then
SMAPI:LoggedOn = False
End
Summary
Once you understand the principles involved, using Simple MAPI to add email support to your application is quite easy. There's a lot that I haven't been able to cover, just because the subject is rather complicated, but this should definitely get you started.Of course, by it's nature Simple MAPI is limited in features, but at least this gives you some idea of the work involved. If you don't think Simple MAPI fits your situation, try one of the 3rd party templates or VBX/OCX controls - I'm sure they'll have the functionality you're looking for.
Next Time ...
How to locate GPF's in your application.
And Remember ...
The best place for information is MSDN. If you don't subscribe, you can still get to the information via the web at http://msdn.microsoft.com/
Article comments
Post a comment
You must be logged on to post comments.
Talk To Us!
Search ClarionMag
From the archives
Sending Clarion Reports as Email Attachments (Part 1)
1/9/2001 12:00:00 AM
The email capability in version 5.5 is a nice addition to the Clarion toolset. What is still missing however, is the ability to easily send a report as an email attachment. In this article David Potter demonstrates one possible solution to this problem. Part 1 of 2.

by Carl Barnes on March 28 2002 (comment link)
Watch out! When you MAPILogon it may change your current directory to the mail client. So save your PATH() and SETPATH(). (Changed for me with OE5 under Win2K.)
Another confusing point is the Attachment structure _appears_ to split the attachment file into path and file name:
MapiFileDescType GROUP,TYPE ! FILE DESCRIPTOR
ulReserved ULONG(0)
flFlags ULONG(0) ! Ignored.
nPosition ULONG ! Ignored.
lpszPathName ULONG
lpszFileName ULONG
lpFileType ULONG(0) ! must be NULL
END
what you want is:
lpszPathName = path+FILENAME / source computer file name that MAPI needs to attach, must have path AND name
lpszFileName = file name receiver sees and is optional
so all you have to do is define
lpszPathName = 'C:mypathmyfiletosend.txt'
and leave lpszFileName=0 and you'll get 'myfiletosend.txt'
The sample app goes to some work splitting the path and file name and changing paths when it does not have to, this simpler code worked for me:
If ~Omitted(Omitted:Prm:Attachment) AND |
Clip(Prm:Attachment) <> '' Then
Loc:Attachment = Clip(Prm:Attachment) & Chr(0)
If EXISTS(Loc:Attachment)
TxMessage:lpFiles = Address(TXFile)
TxMessage:nFileCount += 1
Clear(TXFile)
TXFile:nPosition = Len(Clip(Loc:Text)) + 1
Loc:Text = Clip(Loc:Text) & ' '
TXFile:lpszPathName = Address(Loc:Attachment)
TXFile:lpszFileName = 0 !Take name from above
ELSE
Message('Unable to find attachment file|' & CLIP(Loc:Attachment) & '|' & CLIP(Prm:Attachment) )
End
End