Send HTML e-mail on IBM i using QtmmSendMail api |
In this article, we discuss about sending emails on the IBM i. There are a lot of ways to do this like SNDDST command but i used QtmmSendMail api to do this as it is more better than the SNDDST command.
SNDDST command does not give us the control over the format of the messages. Think about the way if it is possible for you to embed HTML scripts into your email messages.
QtmmSendMail API
Working of QtmmSendMail API
The QtmmSendMail api works as follows:
Required parameter by QtmmSendMail api
ADDTO100 Format
Format is as follows:
RPGLE code for sending email
H BNDDIR('QC2LE') D SENDEMAIL PR D FromAdddress 100A CONST D FromName 100A CONST D ToAddress CONST LIKEDS(EmailAddress) DIM(20) D Subject 80A CONST D HtmlMessage 5000A CONST // entry parameters to this program D SENDEMAIL PI D FromAddress 100A CONST D FromName 100A CONST D ToAddress CONST LIKEDS(EmailAddress) DIM(20) D Subject 80A CONST D HtmlMessage 5000A CONST // Recipient Email Address Data Structure D EmailAddress DS qualified D unused 4b 0 D type 3A D name 100A D address 50A // QTMmSendMail API Prototypes (Send MIME mail) // Service Program Name: QTCP/QTMMSNDM // Use this api to send e-mail from an IBM i program. // This api supports sending multiple mail attachments at one time // but the SNDDST(Send distribution) command does not. // This api works as follows: // We create an ASCII file with the entire note. // Then call the api and provide it the name of file and address that // the SMTP use to forward the e-mail. D QtmmSendMail PR ExtProc('QtmmSendMail') D FileName 255A const options(*varsize) *Input D FileNameLength... *Input D 10I 0 const D OriginatorAddressSMTP... *Input D 256A const options(*varsize) D LengthofOriginator... *Input D 10I 0 const D FirstRecipientAddressSMTP... *Input D likeds(ADDTO100) D dim(32767) D options(*varsize) D TotalNumberofRecipients... *Input D 10I 0 const D ErrorCode 8000A options(*varsize) *Input/Output // ADDTO100 Format D ADDTO100 ds qualified D based(Template) D OffsettoNextAddressStructure... D 10I 0 D AddressLength 10I 0 D AddressFormatName... D 8A D DistributionType... D 10I 0 D Reserved 10I 0 D Address 256A // Recipient Email Address Data Structure used by QTMMSENDMAIL D recipientList ds likeds(ADDTO100) D dim(%elem(ToAddress)) // variables D recipientCount s 3 0 D temporaryFileName... D s 100A D mailDate s 30A // C Language IFS Prototypes // Creates temporary file name for IFS file system // the file name is 10 chars and has naming convention // as (QACXxxxxxx). The directory for the file is _/tmp_. // Example: /tmp/QACX01YTRS // BNDDIR('QC2LE') in H-specs so that it find function // Note that _C_IFS_tmpnam function only generates a temporary // file name for a file in the corresponding file system. The // file will not be created by this function. // Also, as this function returns pointer to the file name // we can use the %str built-in function to get the value instead. DGenerateTemporaryStreamFileName... D PR * extproc('_C_IFS_tmpnam') D buffer 39A options(*omit) // Deletes the defined streamed file. DremoveStreamFile... D PR 10I 0 extproc('_C_IFS_remove') D filename * VALUE OPTIONS( *String) // Open File for buffered reading/writing // filename = (input) path to file in the IFS // mode = (input) various open mode flags. // returns *NULL upon error, or a pointer to a FILE structure DopenStreamFile PR extproc('_C_IFS_fopen') D like(Filepointer) D filename * value options(*string) D mode * value options(*string) // Close File // stream = (input) pointer to FILE structure to close DcloseStreamFile PR 10i 0 extproc('_C_IFS_fclose') D streamFile like(FILEpointer) value D filepointer s * based(prototype_only) D filedescriptor s like(openStreamFile) D index s 3 0 inz(0) D header s 32767a // fputs(): Write string // string = (input) string to write to file // stream = (input) FILE structure designating the file to // write to. // returns a non-negative value if successful // or -1 upon error DfputsStreamFile PR 10i 0 extproc('_C_IFS_fputs') D String * value options(*string) D fileStream like(FILEpointer) value * Get current local date or time in three formats(all output parms) D CEELOCT PR opdesc D Liliandate 10I 0 *nodayssinc14oct1582 D LilianSeconds 8F *00:00:00 14 Oct1582 D GregorianChar 23A *YYYYMMDDHHMISS999 D fc 12A options(*omit) *12byte feedbackcode * get system offset from UTC(Universal time coordinated) D CEEUTCO PR opdesc D Hours 10I 0 D Minutes 10I 0 D Seconds 8F D fc 12A options(*omit) * convert seconds to character timestamp D CEEDATM PR opdesc D input_secs 8F const D date_format 80A const options(*varsize) D char_date 80A options(*varsize) D feedback 12A options(*omit) // Variables used to generate RFC2822 date format // An Internet Message Format used to uniformly represent date and time, // including in HTTP and email headers. // RFC 2822 includes the shortened day of week, numerical date, // three-letter month abbreviation, year, time and time zone, // displaying as 01 Jun 2016 14:31:46 -0700. D rfc2822format c 'Www, DD Mmm YYYY HH:MI:SS' D junk1 s 8F D junk2 s 10I 0 D junk3 s 23A D hours s 10I 0 D minutes s 10I 0 D timezone_hours s 2P 0 D timezone_minutes... D s 2P 0 D timezone s 5A varying D currentTime s 8F D tempDate s 25A D nullErrorDS ds D BytesProv 10I 0 inz(0) D BytesAvail 10I 0 inz(0) // Email Address Type Constants D CONST_TO_ADDRESS... D c 0 D CONST_CC_ADDRESS... D c 1 D CONST_BCC_ADDRESS... D c 2 // Line Feed Character D CONST_LF c x'25' D CONST_CRLF c x'0d25' /free temporaryFileName = %trim(%str(GenerateTemporaryStreamFileName(*omit))); // create new output file filedescriptor = openStreamFile(%trim(temporaryFileName): 'w codepage=1252'); if (filedescriptor = *NULL); *INLR = *ON; return; endif; // close file & reopen in text mode so that // data will be automatically translated closeStreamFile(filedescriptor); filedescriptor = openStreamFile( %trim(temporaryFileName) : 'a codepage=37'); if (filedescriptor = *NULL); *INLR = *ON; return; endif; // Calculate the Timezone in format '+0000', for example // CST should show up as '-0600' CEEUTCO(hours: minutes: junk1: *omit); timezone_hours = %abs(hours); timezone_minutes = minutes; if (hours < 0); timezone = '-'; else; timezone = '+'; endif; timezone += %editc(timezone_hours:'X') + %editc(timezone_minutes:'X'); // Get the current time and convert it to the format // specified for e-mail in RFC 2822 CEELOCT(junk2: CurrentTime: junk3: *omit); CEEDATM(CurrentTime: rfc2822format: tempDate: *omit); maildate = tempDate + ' ' + timezone; recipientCount = 0; header = 'From: "' + %trim(FromName) + ' "<' + %trim(FromAddress) + '>' + CONST_LF; for index = 1 to %elem(ToAddress); if %trim(ToAddress(index).type) = ''; leave; endif; header = %trim(header) + %trim(ToAddress(index).type) + ': ' + %trim(ToAddress(index).name) + ' <' + %trim(ToAddress(index).address) + '>' + CONST_LF; recipientList(index).OffsettoNextAddressStructure = %size(ADDTO100); recipientList(index).AddressFormatName = 'ADDT0100'; select; when ToAddress(index).type = 'TO'; recipientList(index).DistributionType = CONST_TO_ADDRESS; when ToAddress(index).type = 'CC'; recipientList(index).DistributionType = CONST_CC_ADDRESS; when ToAddress(index).type = 'BCC'; recipientList(index).DistributionType = CONST_BCC_ADDRESS; other; recipientList(index).DistributionType = CONST_TO_ADDRESS; endsl; recipientCount += 1; recipientList(index).Reserved = 0; recipientList(index).Address = %trim(ToAddress(index).address); recipientList(index).AddressLength = %len(%trim(ToAddress(index).address)); endfor; header = %trim(header) +'Date: ' + maildate + CONST_LF +'Subject: ' + Subject + CONST_LF +'MIME-Version: 1.0' + CONST_LF +'Content-Type: multipart/related; boundary="MSG_PART"' + CONST_LF + CONST_LF + '--MSG_PART' + CONST_LF +'Content-Type: text/html' + CONST_LF +'Content-Disposition: inline;' + CONST_LF + CONST_LF + CONST_LF; fputsStreamFile(%trim(header): filedescriptor); fputsStreamFile(%trim(HtmlMessage) + CONST_LF: filedescriptor); fputsStreamFile('--MSG_PART--' + CONST_CRLF: filedescriptor); closeStreamFile(filedescriptor); // Use the QtmmSendMail() API to send the // IFS file via SMTP QtmmSendMail( %trim(temporaryFileName): %len(%trim(temporaryFileName)) : %trim(FromAddress): %len(%trim(FromAddress)) : recipientList: recipientCount: nullErrorDS); removeStreamFile(%trim(temporaryFileName)); *inlr = *on; return; /end-free
Compile command for SendEMail program
We will create module and program object of SendEmail.
CRTSQLRPGI OBJ(EASYCLASS1/SENDEMAIL) SRCFILE(EASYCLASS1/SEP2023) SRCMBR(SENDEMAIL) COMMIT(*NONE) OBJTYPE(*MODULE) REPLACE(*YES) DBGVIEW(*SOURCE)
CRTPGM PGM(EASYCLASS1/SENDEMAIL) MODULE(EASYCLASS1/SENDEMAIL) BNDSRVPGM((QTCP/QTMMSNDM)) ACTGRP(*NEW)
RPGLE code for sending email: Explanation
H BNDDIR('QC2LE')
this will bind the QC2LE binding directory present in QSYS to this program as we are calling various C IFS apis like fclose, fopen, fputs, _C_IFS_tmpnam. So, to get their definition we wrote above line in H specs. Service program QC2IFS in library QSYS is part of the QC2LE binding directory that has these procedures existence. we can do dspsrvpgm qsys/QC2IFS to see procedure names.
D SENDEMAIL PR D FromAdddress 100A CONST D FromName 100A CONST D ToAddress CONST LIKEDS(EmailAddress) DIM(20) D Subject 80A CONST D HtmlMessage 5000A CONST // entry parameters to this program D SENDEMAIL PI D FromAddress 100A CONST D FromName 100A CONST D ToAddress CONST LIKEDS(EmailAddress) DIM(20) D Subject 80A CONST D HtmlMessage 5000A CONST
we defined the procedure prototype and procedure Interface for SENDEMAIL program which is the replacement for *entry parameter list to this program. PR is definition and PI is parameters Input. We are taking following input parameters from the user to this program. So, whosoever wants to call this SENDEMAIL program has to pass the following input parameters to it.
// Recipient Email Address Data Structure D EmailAddress DS qualified D unused 4b 0 D type 3A D name 100A D address 50A
Declared the EmailAddress data Structure that is referred by program input parameter ToAddress. Here, ds is qualified and main subfields are type, name and address. Here type is the distribution type i.e. 0 if TO(normal), 1 if CC(Carbon Copy) and 2 if BCC(Blind Carbon Copy). Name is the Recipient Name and Address is the recipient SMTP email address.
D QtmmSendMail PR ExtProc('QtmmSendMail') D FileName 255A const options(*varsize) D FileNameLength... D 10I 0 const D OriginatorAddressSMTP... D 256A const options(*varsize) D LengthofOriginator... D 10I 0 const D FirstRecipientAddressSMTP... D likeds(ADDTO100) D dim(32767) D options(*varsize) D TotalNumberofRecipients... D 10I 0 const D ErrorCode 8000A options(*varsize)
This is the IBM api which is used to send MIMI email message. The parameter list is already described above in section 1(ii).
// ADDTO100 Format D ADDTO100 ds qualified D based(Template) D OffsettoNextAddressStructure... D 10I 0 D AddressLength 10I 0 D AddressFormatName... D 8A D DistributionType... D 10I 0 D Reserved 10I 0 D Address 256A
This is the format used by QTmmSendMail api. The format ADDTO100 is already described above in section 1(iii).
// Recipient Email Address Data Structure used by QTMMSENDMAIL D recipientList ds likeds(ADDTO100) D dim(%elem(ToAddress))
We defined another data structure named recipientList same as DS format ADDTO100 defined above and set %elem(ToAddress) in DIM keyword which is 20 elements.
// variables D recipientCount s 3 0 D temporaryFileName... D s 100A D mailDate s 30A
declare some variables like recipient count, temporaryfileName and maildate to be using in the main program logic.
DGenerateTemporaryStreamFileName... D PR * extproc('_C_IFS_tmpnam') D buffer 39A options(*omit)
Prototype declaration for _C_IFS_tmpnam function. This function is C IFS function and its definition can be found by binding the QC2LE binding directory to the program. _C_IFS_tmpnam api generates temporary file name for the IFS file system, the file name is of 10 characters and has naming convention like QACXxxxxxx. The directory for the file is _/tmp_. Example : /tmp/QACX01YTRS. Please note that it only generates a temporary file name for a file in the corresponding file system. The file will not be created by this function. Also, as this function returns pointer to the file name we can use the %str built-in function to get the value instead.
DremoveStreamFile... D PR 10I 0 extproc('_C_IFS_remove') D filename * VALUE OPTIONS( *String)
Prototype declaration for _C_IFS_remove C function. It deletes the defined streamed file.
DopenStreamFile PR extproc('_C_IFS_fopen') D like(Filepointer) D filename * value options(*string) D mode * value options(*string)
Prototype declaration for _C_IFS_fopen C function. It Open File for buffered reading/writing. This will create the file as well if not already present.
DcloseStreamFile PR 10i 0 extproc('_C_IFS_fclose') D streamFile like(FILEpointer) value
Prototype declaration for _C_IFS_fclose C function. It Closes File.
D filepointer s * based(prototype_only) D filedescriptor s like(openStreamFile) D index s 3 0 inz(0) D header s 32767a
declared filepointer variable as pointer variable based on prototype_only, filedescriptor again as pointer variable using like(openStreamFile) as openStreamfile procedure returns the pointer variable. declared index variable for loop processing and header variable for preparing email header text.
DfputsStreamFile PR 10i 0 extproc('_C_IFS_fputs') D String * value options(*string) D fileStream like(FILEpointer) value
Prototype declaration for _C_IFS_fputs C function.It is used to write string on the ifs file.
D CEELOCT PR opdesc D Liliandate 10I 0 *nodayssinc14oct1582 D LilianSeconds 8F *00:00:00 14 Oct1582 D GregorianChar 23A *YYYYMMDDHHMISS999 D fc 12A options(*omit) *12byte feedbackcode
Prototype declaration for CEELOCT function. It is used to get current local date or time. All the above parameters are output parameters to this program.
CEELOCT returns the current local date or time in three formats as follows:
Also, if you notice tha opdesc is specified with the CEELOCT api.The OPDESC keyword specifies that operational descriptors are to be passed with the parameters that are defined within a prototype. The keyword applies both to a prototype definition and to a procedure-interface definition. It cannot be used with the EXTPGM keyword. Sometime it is mandatory to pass some procedure parameters even though the data type is not precisely known to the called procedure. In that case you can use operational descriptors to provide descriptive information to the called procedure regarding the form of the parameter. The additional information allows the procedure to properly interpret the string. We should only use the opdesc when they are actually expected by the called procedure. Many ILE apis expect opdesc. If any parameter is defined as 'by descriptor', then we should pass opdesc to the API.
D CEEUTCO PR opdesc D Hours 10I 0 D Minutes 10I 0 D Seconds 8F D fc 12A options(*omit)
Prototype declaration for CEEUTCO function. It is used to get system offset from UTC(Universal time coordinated).
D CEEDATM PR opdesc D input_secs 8F const D date_format 80A const options(*varsize) D char_date 80A options(*varsize) D feedback 12A options(*omit)
Prototype declaration for CEEDATM function. It is used to convert seconds to character timestamp. The second and third parameters require an operational descriptor.
D rfc2822format c 'Www, DD Mmm YYYY HH:MI:SS' D junk1 s 8F D junk2 s 10I 0 D junk3 s 23A D hours s 10I 0 D minutes s 10I 0 D timezone_hours s 2P 0 D timezone_minutes... D s 2P 0 D timezone s 5A varying D currentTime s 8F D tempDate s 25A
Variables used to generate RFC2822 date format. An Internet Message Format used to uniformly represent date and time, including in HTTP and email headers. RFC 2822 includes the shortened day of week, numerical date, three-letter month abbreviation, year, time and time zone, displaying as 01 Jun 2016 14:31:46 -0700.
D nullErrorDS ds D BytesProv 10I 0 inz(0) D BytesAvail 10I 0 inz(0)
declare nullErrorDs data structure for getting error information from the QtmmSendMail api.
D CONST_TO_ADDRESS... D c 0 D CONST_CC_ADDRESS... D c 1 D CONST_BCC_ADDRESS... D c 2 D CONST_LF c x'25' D CONST_CRLF c x'0d25'
declare constants to hold values for TO, CC and BCC distribution type and LF and CRLF hex value is defined.
/free temporaryFileName = %trim(%str(GenerateTemporaryStreamFileName(*omit))); // create new output file filedescriptor = openStreamFile(%trim(temporaryFileName): 'w codepage=1252'); if (filedescriptor = *NULL); *INLR = *ON; return; endif;
Main logic starts from here. First we called procedure GenerateTemporaryStreamFileName to generate the temporary file name and returned into variable temporaryFileName. Then we create the IFS stream file by calling procedure openStreamFile and pasing path to temporary filename and mode. we create the file and opened it in write mode with codepage 1252. Once, the file is created and opened successfully the filedescriptor is assigned to it and if its not created or opened then file descriptor will be *NULL and in that case no need to proceed further and return from the program by setting the last record indicator to *ON.
closeStreamFile(filedescriptor); filedescriptor = openStreamFile( %trim(temporaryFileName) : 'a codepage=37'); if (filedescriptor = *NULL); *INLR = *ON; return; endif;
Once, the file is created and opened then we close that file by calling procedure closeStreamFile and passing file descriptor to it. Once file is closed, we will reopen that file by calling procedure openStreamFile and passing path to temporaryFileName and mode as append with codepage 37 i.e. reopen the file in text mode so that data will be automatically translated. Once the file is reopened successfully in text mode a file descriptor is assigned to it and return from the openStreamFile procedure. If is *NULL that means error opening file and return from program by setting *inlr to *ON.
CEEUTCO(hours: minutes: junk1: *omit); timezone_hours = %abs(hours); timezone_minutes = minutes; if (hours < 0); timezone = '-'; else; timezone = '+'; endif; timezone += %editc(timezone_hours:'X') + %editc(timezone_minutes:'X');
Call CEEUTCO procedure and calculate the Timezone in format '+0000', for example CST should show up as '-0600'
CEELOCT(junk2: CurrentTime: junk3: *omit); CEEDATM(CurrentTime: rfc2822format: tempDate: *omit); maildate = tempDate + ' ' + timezone;
Call CEELOCT and CEEDATM procedure to get the current time and convert it to the format specified for e-mail in RFC 2822
recipientCount = 0; header = 'From: "' + %trim(FromName) + ' "<' + %trim(FromAddress) + '>' + CONST_LF;
Set the recipientcount as zero and prepare the header by appending Sender name and sender email address.
for index = 1 to %elem(ToAddress); if %trim(ToAddress(index).type) = ''; leave; endif; header = %trim(header) + %trim(ToAddress(index).type) + ': ' + %trim(ToAddress(index).name) + ' <' + %trim(ToAddress(index).address) + '>' + CONST_LF; recipientList(index).OffsettoNextAddressStructure = %size(ADDTO100); recipientList(index).AddressFormatName = 'ADDT0100'; select; when ToAddress(index).type = 'TO'; recipientList(index).DistributionType = CONST_TO_ADDRESS; when ToAddress(index).type = 'CC'; recipientList(index).DistributionType = CONST_CC_ADDRESS; when ToAddress(index).type = 'BCC'; recipientList(index).DistributionType = CONST_BCC_ADDRESS; other; recipientList(index).DistributionType = CONST_TO_ADDRESS; endsl; recipientCount += 1; recipientList(index).Reserved = 0; recipientList(index).Address = %trim(ToAddress(index).address); recipientList(index).AddressLength = %len(%trim(ToAddress(index).address)); endfor;
Process the for loop until %elem(ToAddress). User can pass 20 recipient email addresses. Once ToAddress(index).type is blank i.e. no more recipient left then stop appending sender information to the header and recipientList data structure. Otherwise append the recipient information in header and recipientList ds in loop.
header = %trim(header) +'Date: ' + maildate + CONST_LF +'Subject: ' + Subject + CONST_LF +'MIME-Version: 1.0' + CONST_LF +'Content-Type: multipart/related; boundary="MSG_PART"' + CONST_LF + CONST_LF + '--MSG_PART' + CONST_LF +'Content-Type: text/html' + CONST_LF +'Content-Disposition: inline;' + CONST_LF + CONST_LF + CONST_LF;
Message header contains the "from" name and address along with the RFC2822 formatted "sent date" and the message subject. The program adds the message headers to identify the content type of the message along with the boundary identifier that is used to define the message body.
fputsStreamFile(%trim(header): filedescriptor); fputsStreamFile(%trim(HtmlMessage) + CONST_LF: filedescriptor); fputsStreamFile('--MSG_PART--' + CONST_CRLF: filedescriptor); closeStreamFile(filedescriptor);
Once all of the message headers have been generated, the string containing those message headers is written out to the temporary file that we created earlier by calling procedure fputsStreamFile and passing header message and file descriptor. After the message headers have been written, the entire content of the HTML message body is written out to the same file, followed by an identifier for the end of this part of the message. After that close the temporary ifs file by calling closeStreamFile procedure.
QtmmSendMail( %trim(temporaryFileName): %len(%trim(temporaryFileName)) : %trim(FromAddress): %len(%trim(FromAddress)) : recipientList: recipientCount: nullErrorDS);
Call the QtmmSendMail API to send our email message. Pass the parameters as temporaryFileName path and its length, Sender address and its length, recipientList data structure, recipient count and nullErrorDS.
Write Calling program CALLPGM to call program SENDEMAIL
DSENDEMAIL PR EXTPGM('SENDEMAIL') D FROMADDR 100A CONST D FROMNAME 100A CONST D TOADDRS CONST LIKEDS(EMAILADDRS) DIM(20) D SUBJECT 80A CONST D HTMLMSG 5000A CONST // Recipient Email Address Data Structure D EmailAddress DS qualified DIM(1) D unused 4b 0 D type 3A D name 100A D address 50A /free emailaddress(1).type = 'TO'; emailaddress(1).name = ' '; emailaddress(1).address = 'myeasyclasses2@gmail.com'; SENDEMAIL('myeasyclasses2@gmail.com': 'MY EASY CLASSES': emailaddress: 'TEST': '<h1>Test message</h1>'); return; /end-free
Defined the prototype of the SendEmail program to be called as procedure and then declare the EmailAddress data structure to pass the recipient list. Assign type as TO, name as blank and recipient email address as myeasyclasses2@gmail.com. Finally call the SENDEMAIL and pass the sender email, sender name, recipient emailaddress ds and Subject and HTML message and then return from the program.
Compile command for SendEMail program
We will create module and program object of CALLPGM.
CRTSQLRPGI OBJ(EASYCLASS1/CALLPGM) SRCFILE(EASYCLASS1/SEP2023) SRCMBR(CALLPGM) COMMIT(*NONE) OBJTYPE(*MODULE) REPLACE(*YES) DBGVIEW(*SOURCE)
CRTPGM PGM(EASYCLASS1/CALLPGM) MODULE(EASYCLASS1/CALLPGM)