Depending on how much functionality you need, you have a variety of options for sending email from your System i.
Did you know your System i can send email messages? In this article, I'll demonstrate how you can easily integrate email into your existing programs, allowing them to send an email message as simply as (or more simply than) they produce a report.
Four Solutions for One Problem
The four email-sending procedures presented here were created over a couple of years, according to the company's needs. I'll start with the simplest solution--sending short, text-only email--and move up to the most complex--sending formatted text and several types of attachments (text files, Excel spreadsheets, and PDF files) to multiple addresses.
The SndEmail procedure (Figure 1) allows you to send an email message to a maximum of three addresses.
*-------------------------------------------------------------------------*
* Send E-Mail Messag
*-------------------------------------------------------------------------
PSndEMail B EXPOR
D PI
* Input Parm
DP_SendTo 75 Valu
DP_CopyTo 75 Valu
DP_BlindCopyTo 75 Valu
DP_Subject 30 Valu
DP_Body 200 Valu
/FRE
// Set the e-mail destination strin
// (A)
SetDestStr(P_SendTo : P_CopyTo : P_BlindCopyTo : P_DestStr)
// Send messag
// (B)
P_Cmdlin = 'SNDDST TYPE(*LMSG)
+ ' TOINTNET(' + %Trim(P_DestStr) + ')
+ ' DSTD(' + '''' + %Trim(P_Subject) + '''' + ')
+ ' LONGMSG(' + '''' + %Trim(P_Body) + '''' + ')
+ ' PTY(*HIGH)'
// (C)
W_ErrorDS = ExecCmd (%Trim(P_CmdLin))
// If the mail was sent (no error message id), return *Of
If W_ErrorDS.APIMsgId = *Blanks
Return *Off
Else
Return *On
Endif
/END-FRE
PSndEMail E
Figure 1: Use SndEmail to send email to up to three addresses.
The message is constrained by the subject and body lengths (30 and 200 characters, respectively) and doesn't allow any attachments. This might suffice if all you need is to alert a user with a short and simple message.
How does it work? Well, this procedure is actually a programmer-friendly mask to the SNDDST CL command: the procedure's parameters are transformed to fit the command's keywords. The P_SendTo, P_CopyTo, and P_BlindCopyTo parameters are rearranged by the SetDestStr procedure into the destination string that will fit the command's TOINTNET keyword (see A). The P_Subject and P_Body parameters are used in the DSTD and LMSG keywords, respectively (see B). When the CL command is built, the ExecCmd procedure is used to execute it (see C).
Sounds simple, doesn't it? Too simple? You need to send a message and an output file? Use SndFileByMail (Figure 2).
*-------------------------------------------------------------------------*
* Send File By Mai
*-------------------------------------------------------------------------
PSndFileByMail B EXPOR
D PI
* Input Parm
DP_SendTo 75 Valu
DP_CopyTo 75 Valu
DP_BlindCopyTo 75 Valu
DP_Body 200 Valu
DP_FileName 10 Valu
DP_DocName 12 Valu
DP_FolderName 10 Valu
DP_Format 10 Valu
/FRE
// Set the e-mail destination strin
// (A)
SetDestStr(P_SendTo : P_CopyTo : P_BlindCopyTo : P_DestStr)
// Retrieve the field separator for CSV forma
// (B)
In DaFldSep
W_FldSep = DaFldSep
// Set attachment (either copy PF to folder, or use existing doc in folder
// (C)
If P_DocName = *Blanks
P_DocName = %Trim(P_FileName) + '.' + %Trim(P_Format)
EndIf
If P_FileName <> *Blanks
Select
When P_Format = 'TXT'
P_Cmdlin = 'CPYTOPCD FROMFILE(' + %Trim(P_FileName) + ')
+ ' TOFLR(' + %Trim(P_FolderName) + ')
+ ' TODOC(' + '''' + %Trim(P_DocName) + '''' + ')
+ ' REPLACE(*YES)'
When P_Format = 'CSV'
P_Cmdlin = 'CPYTOIMPF FROMFILE(' + %Trim(P_FileName) + ')
+ ' TOSTMF(' + '''' + 'QDLS/
+ %Trim(P_FolderName) + '/' + %Trim(P_DocName) + '''
+ ') MBROPT(*REPLACE) STMFCODPAG(437)
+ ' RCDDLM(*CRLF) STRDLM(' + '''' + ' ' + '''' + ')
+ ' FLDDLM(' + '''' + W_FldSep + '''' + ')'
Other
Return *On
EndSl
W_ErrorDS = ExecCmd (%Trim(P_CmdLin))
// If an error occurred (error message id is not blank), return *O
// (D)
If W_ErrorDS.APIMsgId <> *Blanks
Return *On
EndIf
// Send messag
// (E)
P_Cmdlin = 'SNDDST TYPE(*DOC)'
' TOINTNET(' + %Trim(P_DestStr) + ')
+ ' DSTD(' + '''' + 'DESC' + '''' + ')
+ ' MSG(' + '''' + %Trim(P_Body) + '''' + ')
+ ' DOC(' + '''' + %Trim(P_DocName) + '''' + ')
+ ' FLR(' + %Trim(P_FolderName) + ') PTY(*HIGH)'
W_ErrorDS = ExecCmd (%Trim(P_CmdLin))
// If the mail was sent (no error message id), return *Of
If W_ErrorDS.APIMsgId = *Blanks
Return *Off
Else
Return *On
Endif
Endif
/END-FRE
PSndFileByMail E
Figure 2: Use SndFileByMail to send a message and an output file.
This procedure can be used to send an output file generated by your program in text or comma-separated values (CSV) format to the same three addresses as the previous procedure. Again, the procedure is a mask to the SNDDST CL command: the destination addresses are treated the same way as previously (see A), but the similarity to the first procedure ends here. The CSV conversion functionality requires a separator, usually a comma. Since this separator can vary in different countries or Excel versions, it's stored in a data area (DAFLDSEP). Because of this, the procedure's next step is retrieving the separator character (see B).
The attachment type, passed in parameter P_Format, determines the type of conversion that will be performed. For the text format, the procedure uses the CPYTOPCD CL command, while the CPYTOIMPF command is used to generate the CSV format (see C). If the conversion fails, the procedure ends, returning *On. This is the way to tell the calling program that something went wrong and the email was not sent (see D). Finally, the CL command string is formed and executed (see E). Notice that the P_Body is now placed in the MSG keyword and there is no P_Subject parameter. This happens because we are now using a different type of distribution that forces us to do these changes (see the SNDDST CL command explanation).
What if all you really need is to send a spool file (some sort of business report or purchase order) via email to a supplier or client, without any need for fancy subject or message text because the attachment says it all? In that case, use SndSplfByMail (Figure 3).
*-------------------------------------------------------------------------*
* Send Spoolfile By Mail
*-------------------------------------------------------------------------*
PSndSplfByMail B EXPORT
D PI N
* Input Parms
DP_SplfName 10 Value
DP_SendTo 75 Value
DI_ProdSys S
/FREE
// (A)
// Check if this is the production system.
// If it's not, e-mails won't be sent out of the Company
I_ProdSys = ProductionSys;
// Retrieve the PDF transform printer name
In DaPdfPrt;
W_PdfPrt = DaPdfPrt;
// * Get the user's e-mail address (if internal)
// (B)
P_SendTo = RtvEmailAddr(P_SendTo);
// If this it not the production system
// and the destination mail is not from the company, send it.
// (C)
If Not I_ProdSys And Not InternalAddr(P_SendTo);
Return *On;
EndIf;
// Change spoolfile attributes and redirect to PDF transform printer
// (D)
P_Cmdlin = 'CHGSPLFA FILE(' + %Trim(P_SplfName) + ') SPLNBR(*LAST)'
+ ' OUTQ(*LIBL/' + %Trim(W_PdfPrt) + ')'
+ ' USRDFNDTA(' + ''''
+ 'MAILTAG(' + %Trim(P_SendTo) + ')' + '''' + ')';
W_ErrorDS = ExecCmd (%Trim(P_CmdLin));
// If the mail was sent (no error message id), return *Off
If W_ErrorDS.APIMsgId = *Blanks;
Return *Off;
Else;
Return *On;
Endif;
/END-FREE
PSndSplfByMail E
Figure 3: Use SndSplfByMail to send only a spool file.
This very simple procedure takes advantage of the PDF conversion functionality provided by the InfoPrint Server licensed program. IBM's Printing Redbook explains how to install and configure this functionality, so I won't waste your time with that here; I'll just explain how it works. The conversion is performed by a virtual printer that also sends the generated PDF file via email to the address you specify in the USRDFNDTA keyword of the CHGSPLFA CL command.
Let's go over the code. In the beginning of the procedure (see A), the necessary data is gathered: system type (development, test, or production) and PDF conversion printer are retrieved. In the next step (see B), the P_SendTo parameter is checked and converted to an Internet email address if necessary (read about procedure RtvEmailAddr in the "Other Procedures" sidebar at the end of this article). In C, the address is checked (to prevent sending test data to the "outside world") using the system type information and the destination address (the procedure InternalAddr is also explained in the "Other Procedures" sidebar). Next, in D, the conversion and distribution are performed in one single step: the spool file name (P_SplfName), PDF conversion printer (W_PdfPrtf), and destination address (P_SendTo) are used in the CHGSPLFA CL command to send the spool file to the "printer" that sends it to its destination.
Be aware that there are some factors to take into account: complex printer files (with lines, boxes, or extensive formatting) might not be properly converted or, even worse, not converted at all. Generate the spool file and use the CHGSPLFA command or option 2 in the WRKSPLF command to test it before you use this procedure.
The procedure requires the InfoPrint Server licensed program to work; without it, it's useless.
The conversion is performed by a printer that, even though is a virtual one, might be waiting for a reply to a message or stopped.
Mime & Mail
Eventually, the business started asking for more than these procedures could supply. So we went online looking for a solution and found it in IBM. Not IBM Rochester, as one would expect, but IBM Italy. Confused? It's quite simple: Giovanni B. Perotti, a former IBM Italy developer, created a set of procedures that provide a usable front-end to the QtmmSendMail API, a not-user-friendly-at-all API to send email. This application, named Mime & Mail, is free and open source. It can be downloaded from Giovanni's Web site. You can find out more about this application in "You've Got Mail," an introductory article by Susan Gantner and Jon Paris in the October 2004 edition of IBM Systems Magazine. We used it to create the email sending procedure that I'll present now. The requirement was to enable programs to send several types of output in different formats to multiple destinations.
Procedure SndMulFiles (Figure 4) provides a programmer-friendly interface to the Mime & Mail procedures.
*-------------------------------------------------------------------------*
* Send Multiple files in an E-Mail Message
*-------------------------------------------------------------------------*
PSndMulFiles B EXPORT
DSndMulFiles PI 1
* Input Parms
D P_SName 50a Value
D P_SEmail 50a Value
D P_MailTo 200 Value
D P_MailCC 200 Value
D P_MailBcc 200 Value
D P_Spool 10 Value
D Dim(50)
D P_ImbAtt 1 Value
D Dim(50)
D P_File 10 Value
D Dim(50)
D P_Lib 10 Value
D Dim(50)
D P_FmtFile 1 Value
D Dim(50)
D P_NameFile 12 Value File Name
D Dim(50)
D P_IncFlds 1 Value Include field names
D* 0 - Don't include
D* 1 - Include
D Dim(50)
D P_Zip 12 Value Zipped file name
D Dim(50)
D P_MailSubject 70a Value
D P_MailBody 500 Value
D P_PathFile 60 Value Options(*NoPass)
D Dim(20)
D P_ImbAtt1 S 10i 0 Dim(51) Inz
D FromFName S 512 Inz(*Blanks)
D FromFNameA S 512 Dim(200) Inz
D SplName S 10 Inz(*Blanks)
D ImbAtt S 10i 0 Inz(*Zeros)
D ToAddrArr S 256 Dim(1000) Inz
D ToDistArr S 10i 0 Dim(1000) Inz
D ToNameArr S 50 Dim(1000) Inz
D ReplyTo S 50 Inz(*Blanks)
D W_Order S 3 0 Inz(*Zeros)
D W_ToDist S 1 0 Inz(*Zeros)
D W_Zip S 12 Inz(*Blanks)
D W_File S 10 Inz(*Blanks)
D W_FileSav S 10 Inz(*Blanks)
D W_Lib S 10 Inz(*Blanks)
D W_FromMember S 10 Inz(*Blanks)
D W_ToFile S 64 Inz(*Blanks)
D W_ToDir S 128 Inz(*Blanks)
D W_To S 1 Inz(*Blanks)
D W_Len50 S 5 0 Inz(*Zeros)
D W_Len51 S 5 0 Inz(*Zeros)
D W_Len S 5 0 Inz(*Zeros)
D W_Len1 S 5 0 Inz(*Zeros)
D W_Len2 S 5 0 Inz(*Zeros)
D W_Res S 50 Inz(*Blanks)
D Wx S 5 0 Inz(*Zeros)
D Wy S 5 0 Inz(*Zeros)
D WxSav S 5 0 Inz(*Zeros)
D Wn S 5 0 Inz(*Zeros)
D W_LibSav S 10 Inz(*Blanks)
D W_LibAux S 10 Inz(*Blanks)
D W_String S 1000 Inz(*Blanks)
D W_Pos S 9 0 Inz(*Zeros)
D W_MbrOpt S 11 Inz(*Blanks)
D W_LenT s 4 0 Inz(*Zeros)
D W_LenTSav s 4 0 Inz(*Zeros)
D W_CtlPdf s 1 Inz(*Blanks)
D W_LstEMail S 50 Inz(*Blanks)
/FREE
// Inicialize variables
Exsr Init;
// (A)
// ***************
// PREPARE TO SEND
// ***************
// Prepare e-mail destinations array
ExSr DstMails;
// Import Message Body
ExSr ImpBody;
// Process Spool files to be sent
ExSr TrSpool;
// Process files to be sent
ExSr ProccFiles;
// Add the files to the IFS
ExSr AddIfsFiles;
// (B)
// ***********
// SEND E-MAIL
// ***********
// Invoke the MMailSender procedure to send e-mail
ExSr SendMMAIL;
// (C)
// ********
// CLEAN UP
// ********
// Remove the PDF files created for the message and unlock PDF creation
ExSr RmvPdfs;
// Remove the spool file generated for the message body
ExSr RmvSpl;
// *************
// RETURN STATUS
// *************
// Return: *On means error
If P_Error = '2';
Return *On;
Else;
Return *Off;
EndIf;
//-----------------------------------------------------------------------*
// Process spool files to be sent
//-----------------------------------------------------------------------*
BegSr Init;
// Retrieve the field separator for XLS format
In DaFldSep;
W_FldSep = DaFldSep;
// Read the Address of the Mail Log
// (Used to check if the mail is being correctly sent)
In DaMailLog;
W_MailLog = DaMailLog;
// Retrieve the path for file conversion (pdf, xls, etc.)
In DaCnvPath;
W_ToDir = DaCnvPath;
// Check if this is the production system.
// If it's not, e-mails won't be sent out of the Company
I_ProdSys = ProductionSys;
// Set the priority to HIGH
P_Prio = 2;
// Set the importance to MEDIUM
P_Impo = 1;
EndSr;
//-----------------------------------------------------------------------*
// Process spool files to be sent
//-----------------------------------------------------------------------*
BegSr TrSpool;
Wx = 1;
// Fill the spool file array (used by the mail sending program)
Dow P_Spool(Wx) <> *Blanks Or %Elem(P_Spool) = Wx;
// If the format is PDF, copy file to the IFS
If P_ImbAtt(Wx) = '3';
// Retrieve the PDF file name (Sequence Number) to be used
ExSr RtvSeqNbr;
// Perform PDF conversion
P_Cmdlin = 'MMAIL/CVTSPLFPDF SPLF(' + %Trim(P_Spool(WX)) + ')'
+ ' TOPDF(' + '''' + %Trim(P_Path) +''''+ ')';
W_ErrorDS = ExecCmd (%Trim(P_CmdLin));
// If an error occured, return *ON. Continue otherwise
If W_ErrorDS.APIMsgId <> *Blanks;
Return *On;
Endif;
// Add the file to the Files To Sent array
// Save the index
WxSav = Wx;
// Look up the last array entry
Wx = 1;
Dow FromFNameA(Wx) <> *Blanks;
Wx = Wx + 1;
EndDo;
FromFNameA(Wx) = %Trim(P_Path);
Wx = WxSav;
Wx = Wx + 1;
Iter;
EndIf;
P_SplName(Wx) = P_Spool(Wx);
P_ImbAtt1(Wx) = %Int(P_ImbAtt(Wx));
Wx = Wx + 1;
EndDo;
// Look up the last array entry, to add the body
Wx = 1;
Dow P_SplName(Wx) <> *Blanks;
Wx = Wx + 1;
EndDo;
// Add the body
If P_MailBody <> *Blanks;
P_SplName(Wx) = 'MAILBODYSP';
EndIf;
EndSr;
//-----------------------------------------------------------------------*
// Add the body to the message
//-----------------------------------------------------------------------*
BegSr ImpBody;
// If the message has a body, print it to a spool file and add it as
// an embeded attachment
If P_MailBody = *Blanks;
LeaveSr;
EndIf;
Open MailBodySp;
W_Len = 1;
W_Len2 = 1;
Dow W_Len > *Zero;
W_Len = %Scan(':/P' : P_MailBody : W_Len+1);
If W_Len = *Zero;
Body1 = %Trim(%Subst(P_MailBody : W_Len2 : 500 - W_Len2));
Write Body;
Leave;
EndIf;
If W_Len2 > 1;
Body1 = %Subst(P_MailBody : W_Len2 : W_Len - W_Len2);
Else;
Body1 = %Subst(P_MailBody : W_Len2 : W_Len - 1);
EndIf;
W_Len2 = W_Len + 3;
Write Body;
EndDo;
Close MailBodySp;
EndSr;
//-----------------------------------------------------------------------*
// Remove the spool file created for the body
//-----------------------------------------------------------------------*
BegSr RmvSpl;
// Delete the body spool file
If P_MailBody = *Blanks;
LeaveSr;
EndIf;
P_Cmdlin = 'DLTSPLF FILE(MAILBODYSP) SPLNBR(*LAST)';
W_ErrorDS = ExecCmd (%Trim(P_CmdLin));
// Clear the spool file array
P_SplName = *Blanks;
EndSr;
//-----------------------------------------------------------------------*
// Add the files to the IFS
//-----------------------------------------------------------------------*
BegSr AddIfsFiles;
Wx = 1;
Wy = 1;
// Look up the last filled array position
Dow FromFNameA(Wx) <> *Blanks And %Elem(FromFNameA) < Wx;
Wx = Wx + 1;
EndDo;
// Add the files to be sent to the IFS to the proper array
Dow P_PathFile(Wy) <> *Blanks And %Elem(FromFNameA) > Wx
And %Elem(P_PathFile) > Wy And %Parms >= 16;
FromFNameA(Wx) = P_PathFile(Wy);
Wx = Wx + 1;
Wy = Wy + 1;
EndDo;
EndSr;
//----------------------------------------------------------------------*
// Process files to be sent
//_---------------------------------------------------------------------*
BegSr ProccFiles;
Wx = 1;
Dow P_FmtFile(Wx) <> *Blanks;
Select;
// If the format is XLS
When P_FmtFile(Wx) = 'X' Or P_FmtFile(Wx) = 'T';
W_To = P_FmtFile(Wx);
// Prepare the parameters for the conversion procedure
W_File = P_File(Wx);
W_Lib = P_Lib(Wx);
W_FromMember = '*FIRST';
// If the file name is not filled, use the physical file name
If P_NameFile(Wx) = *Blanks;
W_ToFile = P_File(Wx);
Else;
W_ToFile = %Trim(P_NameFile(Wx));
EndIf;
If P_FmtFile(Wx) = 'X';
// If the field names should be included in the output file
If P_IncFlds(Wx) = '1';
// Write the field names as the file's first record
ExSr RtvFlds;
// Copy the physical file to the IFS as XLS file
// Copy the field names from the file in QTEMP
W_MbrOpt = '*REPLACE';
W_LibSav = W_Lib;
W_FileSav = W_File;
W_Lib = 'QTEMP';
W_File = 'PFFFD';
// Convert files to XLS format
CopyToXls(W_File : W_Lib : W_FromMember :
W_ToFile : W_ToDir : W_To : W_MbrOpt);
// Duplicate the file to QTEMP, to put the field names in
P_Cmdlin = 'DLTF (QTEMP/PFFFD)';
W_ErrorDS = ExecCmd (%Trim(P_CmdLin));
// Add the file with the field names to the previous file
W_MbrOpt = '*ADD';
W_Lib = W_LibSav;
W_File = W_FileSav;
Else;
W_LibSav = W_Lib;
W_FileSav = W_File;
W_MbrOpt = '*REPLACE';
EndIf;
Else;
W_MbrOpt = '*REPLACE';
EndIf;
// Convert files to XLS format
CopyToXls(W_File : W_Lib : W_FromMember :
W_ToFile : W_ToDir : W_To : W_MbrOpt);
// If the output file should be zipped
If P_Zip(Wx) <> *Blanks;
ExSr TrZip;
Else;
// Otherwise attach it as it is
FromFNameA(Wx) = %Trim(W_ToDir) + '/' + %Trim(W_ToFile);
EndIf;
Other;
Return *On;
EndSl;
Wx = Wx + 1;
EndDo;
EndSr;
//----------------------------------------------------------------------*
// Zip files
//----------------------------------------------------------------------*
BegSr TrZip;
If P_Zip(Wx) = *Blanks;
LeaveSr;
EndIf;
// Replace the file extension by ".zip"
W_Len = *Zeros;
W_Len = %Scan('.':P_NameFile(Wx));
If W_Len > *Zero;
W_Zip = %SubSt(P_NameFile(Wx):1:W_Len);
Else;
W_Zip = P_NameFile(Wx);
W_Len = 8;
EndIf;
W_Zip = %Replace('zip' : W_Zip : W_Len + 1 : 3);
P_Cmdlin = 'PAEZIP/PAEZIP OPTION(*ZIP)'
+ ' ZIPFILE(' + ''''+ %Trim(W_ToDir) + '/'
+ %Trim(W_Zip) + '''' + ')'
+ ' FILE(' + '''' + %Trim(W_ToDir) + '/'
+ %Trim(P_NameFile(Wx)) + '''' + ')';
W_ErrorDS = ExecCmd (%Trim(P_CmdLin));
// Add the zip file to the Files to be Sent array
FromFNameA(Wx) = %Trim(W_ToDir) + '/' + %Trim(W_Zip);
EndSr;
//----------------------------------------------------------------------*
// Prepare e-mail destinations array
//----------------------------------------------------------------------*
BegSr DstMails;
Wx = 1;
// To
Dow P_MailTo <> *Blanks;
W_ToDist = *Zero;
W_Len = %Scan(',' : P_MailTo : 1);
If W_Len = *Zeros;
ToAddrArr(Wx)= %Trim(P_MailTo);
// Retrieve mail address (internet e-mail or internal user)
ExSr RtvMail;
Wx = Wx + 1;
// Don't send e-mails to the outside world from the test system
If Not I_ProdSys And Not InternalAddr (P_MailTo);
Wx = Wx - 1;
ToAddrArr(Wx)= *Blanks;
ToNameArr(Wx)= *Blanks;
EndIf;
Leave;
EndIf;
ToAddrArr(Wx)= %Trim(%Subst(P_MailTo : 1 : W_Len-1));
// Remove the address added to the array from the string
P_MailTo = %Replace(' ' : P_Mailto : 1 : W_Len);
// Retrieve mail address (internet e-mail or internal user)
ExSr RtvMail;
Wx = Wx + 1;
// If this it not the production system
// and the destination mail is not from the company, don't send it
If Not I_ProdSys And Not InternalAddr (P_MailTo);
Wx = Wx - 1;
ToAddrArr(Wx)= *Blanks;
ToNameArr(Wx)= *Blanks;
EndIf;
EndDo;
// Cc
Dow P_MailCc <> *Blanks;
W_ToDist = 1;
W_Len = %Scan(',':P_MailCc:1);
If W_Len = *Zeros;
ToAddrArr(Wx)= %Trim(P_MailCc);
// Retrieve mail address (internet e-mail or internal user)
ExSr RtvMail;
Wx = Wx + 1;
// If this it not the production system
// and the destination mail is not from the company, don't send it
If Not I_ProdSys And Not InternalAddr (P_MailCc);
Wx = Wx - 1;
ToAddrArr(Wx)= *Blanks;
ToNameArr(Wx)= *Blanks;
EndIf;
Leave;
EndIf;
ToAddrArr(Wx)= %Trim(%Subst(P_MailCc : 1 : W_Len-1));
// Remove the address added to the array from the string
P_MailCc = %Replace(' ':P_MailCc:1:W_Len);
// Retrieve mail address (internet e-mail or internal user)
ExSr RtvMail;
// If this it not the production system
// and the destination mail is not from the company, don't send it
If Not I_ProdSys And Not InternalAddr (P_MailCc);
Wx = Wx - 1;
ToAddrArr(Wx)= *Blanks;
ToNameArr(Wx)= *Blanks;
EndIf;
Wx = Wx + 1;
EndDo;
// Bcc
// BlindCopy - Send to the Mail Log mailbox
If W_MailLog <> *Blanks;
W_ToDist = 2;
If P_MailBcc <> *Blanks;
P_MailBcc = %Trim(P_MailBcc) + ', ' + %Trim(W_MailLog);
Else;
P_MailBcc = %Trim(P_MailBcc) + %Trim(W_MailLog);
EndIf;
EndIf;
Dow P_MailBcc <> *Blanks;
W_ToDist = 2;
W_Len = %Scan(',' : P_MailBcc : 1);
If W_Len = *Zeros;
ToAddrArr(Wx)= %Trim(P_MailBcc);
// Retrieve mail address (internet e-mail or internal user)
ExSr RtvMail;
Wx = Wx + 1;
// If this it not the production system
// and the destination mail is not from the company, don't send it
If Not I_ProdSys And Not InternalAddr (P_MailBcc);
Wx = Wx - 1;
ToAddrArr(Wx)= *Blanks;
ToNameArr(Wx)= *Blanks;
EndIf;
Leave;
EndIf;
ToAddrArr(Wx)= %Trim(%Subst(P_MailBcc : 1 : W_Len-1));
// Remove the address added to the array from the string
P_MailBcc = %Replace(' ' : P_MailBcc : 1 : W_Len);
// Retrieve mail address (internet e-mail or internal user)
ExSr RtvMail;
// If this it not the production system
// and the destination mail is not from the company, don't send it
If Not I_ProdSys And Not InternalAddr (P_MailBcc);
Wx = Wx - 1;
ToAddrArr(Wx)= *Blanks;
ToNameArr(Wx)= *Blanks;
EndIf;
Wx = Wx + 1;
EndDo;
EndSr;
//----------------------------------------------------------------------*
// Retrieve the user's email address
// look it up in the group and user tables
//----------------------------------------------------------------------*
BegSr RtvMail;
// If it's a group
If %Scan('@':ToAddrArr(Wx)) = *Zero;
P_Group = ToAddrArr(Wx);
ToAddrArr(Wx) = *Blanks;
P_Email = *Blanks;
P_Eof = *Off;
Dow P_Eof = *Off;
RtvGrpAddrs(P_Group : P_Email : P_Emails : P_Names : P_Eof);
// Move the email names and addresses fields to the proper arrays
Wn = 1;
Dow Wn < 11;
If P_Emails(Wn) = *Blanks;
Leave;
EndIf;
ToAddrArr(Wx) = P_Emails(Wn);
ToNameArr(Wx) = P_Names(Wn);
ToDistArr(Wx) = W_ToDist;
W_LstEmail = P_Emails(Wn);
Wn = Wn + 1;
Wx = Wx + 1;
EndDo;
// Re-call the Retrieve address procedure from the last address used,
// because the procedure returns blocks of 10 addresses
P_Email = W_LstEmail;
EndDo;
Wx = Wx - 1;
// If it's not a group, check the address associated to the user
Else;
P_Email = ToAddrArr(Wx);
P_Name = ToNameArr(Wx);
RtvEmailName(P_Email);
EndIf;
EndSr;
//----------------------------------------------------------------------*
// Retrieve the field names to include in the IFS' file header
//----------------------------------------------------------------------*
BegSR RtvFlds;
// Inicialize fields
W_LenTSav = *Zeros;
W_LenT = *Zeros;
W_Len50 = *Zeros;
W_Len51 = *Zeros;
// Create a file in QTEMP with the field definition
P_Cmdlin = 'DSPFFD FILE(' + %Trim(W_File) + ')'
+ ' OUTPUT(*OUTFILE) '
+ ' OUTFILE(QTEMP/PFEMP) '
+ 'OUTMBR(*FIRST *REPLACE)';
W_ErrorDS = ExecCmd (%Trim(P_CmdLin));
// Read the field sizes
Open PfEmp;
Read QWHDRFFD;
// Add the field sizes and for each field add 1, because of the comma
W_Pos = *Zeros;
Dow Not %Eof(PfEmp);
W_Pos = 1;
If W_LenTSav <> *Zeros;
W_LenTSav = W_LenT;
Else;
W_LenTSav = 1;
EndIf;
If WHFLDD = *Zeros;
W_LenT = W_LenT + W_Pos + WHFLDB;
Else;
W_LenT = W_LenT + W_Pos + WHFLDD;
EndIf;
If W_LenTSav <> 1;
W_Len50 = W_LenTSav + 1;
Else;
W_Len50 = 1;
EndIf;
W_Len51 = W_LenT - W_LenTSav - 1;
If W_Len51 > 10;
W_Len51 = 10;
EndIf;
W_String = %Replace(%Subst(WHFLDI : 1 : W_Len51) :
W_String : W_Len50 : W_Len51);
// If the format is XLS, use the user-defined field separator
If P_FmtFile(Wx) = 'X';
W_String = %Replace(W_FldSep : W_String : W_LenT : 1);
Else;
W_String = %Replace(' ' : W_String : W_LenT : 1);
EndIf;
Read QWHDRFFD;
EndDo;
Close PfEmp;
// Duplicate the file to QTEMP to add the field names
P_Cmdlin = 'CRTPF (QTEMP/PFFFD) RCDLEN(1000)';
W_ErrorDS = ExecCmd (%Trim(P_CmdLin));
// Put QTEMP at the beginning of the library list
P_CmdLin = 'ADDLIBLE (QTEMP)';
W_ErrorDS = ExecCmd(P_CmdLin);
// Write the records to the file in QTEMP
Open Pfffd;
FD_Pfffd = %Trim(W_String);
Write PfffdR;
Close Pfffd;
EndSr;
//-----------------------------------------------------------------------*
// Retrieve the PDF file name (Sequence Number) to be used
//-----------------------------------------------------------------------*
BegSr RtvSeqNbr;
// Use the Pdf Lock Data Area to prevent document number duplication
If W_CtlPdf <> '1';
In(E) *Lock DaPdfLck;
Dow %Error = *On;
In(E) *Lock DaPdfLck;
EndDo;
EndIf;
// Retrieve the sequence number to be used
W_CtlPdf = '1';
W_Order = 1;
Dow W_Order < 999;
P_Path = %Trim(W_ToDir) + 'PDF' + %Char(W_Order) + '.pdf';
W_Order = W_Order + 1;
ChkObjInIFS (P_Path : P_Response);
If P_Response = C_NotExists;
Leave;
EndIf;
EndDo;
EndSr;
//-----------------------------------------------------------------------*
// Invoke the MMAILSND procedure to send e-mail
//-----------------------------------------------------------------------*
BegSr SendMMail;
// Add the Mail processing library
P_CmdLin = 'ADDLIBLE (MMAIL)';
W_ErrorDS = ExecCmd(P_CmdLin);
// Call the mail sending program
MMailSender(P_Error : P_SName : P_SEmail : ToNameArr : ToAddrArr :
ToDistArr : ReplyTo : P_MailSubject : P_Impo :
P_Prio : FromFNameA : P_SplName : P_ImbAtt1);
// Remove the Mail processing library
P_CmdLin = 'RMVLIBLE (MMAIL)';
W_ErrorDS = ExecCmd(P_CmdLin);
EndSr;
//-----------------------------------------------------------------------*
// Remove the PDF files created for the message and unlock PDF creation
//-----------------------------------------------------------------------*
BegSr RmvPdfs;
If W_CtlPdf = '1';
P_CmdLin = 'RMVLNK OBJLNK('
+ '''' + %Trim(W_ToDir) + 'PDF*.pdf' + '''' + ')';
W_ErrorDs = ExecCmd(P_CmdLin);
Out DAPdfLck;
W_CtlPdf = *Blanks;
EndIf;
EndSr;
/END-FREE
PSndMulFiles E
Figure 4: SndMulFiles provides a programmer-friendly interface to the Mime & Mail procedures.
SndMulFiles accepts as parameters spool files that can be converted to text or PDF format, physical files that can be transformed into text or Excel files, other files that already exist in your IFS, and a wide range of destinations (internal user IDs, mailing groups, and individual email addresses retrieved from a sort of address book or from the production data). But let's begin with the procedure's parameters:
- P_SName is the sender's name. You might use this to enable your program to send emails under a department's name, such as Accounting, Customer Service, etc.
- P_SEmail is the sender's address. This will be the "Reply To" address.
- P_MailTo is the "To" field, where you can specify user IDs, groups, and names (from the address group) or Internet email addresses.
- P_MailCC is the same as previous for the "Cc" field.
- P_MailBcc is the same as previous for the "Bcc" field.
- P_Spool is an array that can contain up to 50 spool files to convert and attach to the email message.
- P_ImbAtt specifies the conversion to perform in each of the spool files; 1 means imbed in the body, 2 means attach to the body, 3 means convert to PDF and attach to the body.
- P_File is an array that can contain up to 10 physical file names to convert and attach to the email message.
- P_Lib is an array that contains the library of each physical file mentioned in the previous parameter.
- P_FmtFile specifies the conversion to perform in each of the physical files contained in the P_File array: X means Excel, T means text format.
- P_NameFile is an array that contains the name of each of the converted physical files
- P_IncFlds is an array that indicates whether the column names should be included in each of the converted files: 0 means don't include, 1 means include.
- P_Zip is an array that contains the zipped file name of each of the converted files; if an array position is not blank, it means that the corresponding file in array P_File will be zipped and attached to the message with the name specified in the P_Zip array position
- P_MailSubject is the "Subject" field.
- P_MailBody is the "Body" field.
- P_PathFile is an optional parameter to specify additional files to attach that already exist in the IFS.
SndMulFiles is divided in three parts: preparation, execution, and cleanup.
Preparation (see A) begins with the setup of the email destinations array (in routine DstMails, which uses parameters P_MailTo, P_MailCC, and P_MailBcc parameters to fill the ToAddrArr, ToNameArr, and ToDistArr arrays). Note that the distribution type (To, Cc, or Bcc) is set in variable W_ToDist and passed to the ToDistArray in routine RtvMail. This routine retrieves the address or addresses (in case of a group) from the content of each of the ToDistArr array entries.
Since the entry parameters mentioned before can contain an Internet email address, a group name, a user name, or a system user ID, two conversions (procedures RtvGrpAddr and RtvEmailName, explained in the "Other Procedures" sidebar) are necessary to ensure that the name and address arrays are properly filled.
The next step is moving the P_Body parameter to the appropriate place within the email message. Routine ImpBody prints the P_Body parameter to a spool file and routine TrSpool converts it into the message body. This conversion is necessary because in Mime & Mail the body might contain formatting (we don't actually use it, except for the new line, ":/P" special notation that you'll find in the ImpBody routine), and this is the easiest way to ensure that nothing gets lost. The before-mentioned TrSpool routine not only imbeds the body in the message, but also treats any spool files indicated in the P_Spool parameter. This operation consists of converting the spool file to PDF (when indicated in the respective array entry of parameter P_ImbAtt) via the CVTSPLFPDF command provided by Mime & Mail and filling the FromFNameA, P_SplName, and P_ImbAtt1 arrays with the IFS file name, spool file name, and attachment type information, respectively. The next step is attaching any physical files specified in parameter P_File in routine ProccFiles. The resulting output files are then placed in array FromFNameA. There are several options that are driven by P_FmtFile (convert to Excel or Text format), P_IncFlds (include headers in the Excel format), and P_Zip (compress the output file).
The preparation stage ends with the AddIFSFiles routine, which adds to the FromFNameA array any other files (specified in the optional parameter P_PathFile) that already exist in the IFS.
The execution stage (see B) is simply the invocation of the Mime & Mail procedure wrapper (MMailSender) that sends the message. This procedure is explained in the "Other Procedures" sidebar.
Finally, the cleanup (see C) consists of two routines that remove the spool and PDF files (named RmvSpl and RmvPDFs, respectively) generated by the SndMulFiles procedure.
Requirements
In order to take full advantage of these procedures, you need to have several programs installed. The table below indicates the correspondence between procedures and required software:
Required Programs |
|
Procedure |
Additional Software Required |
SndEmail |
None |
SndSplfByMail |
InfoPrint Server Licensed Program |
SndFileByMail |
None |
SndMulFiles* |
Mime&Mail and PAEZIP |
* SndMulFiles will work without PAEZIP if you always leave parameter P_Zip blank.
Keep in mind that you also need to configure your System i in order to be able to send email. Read the sidebar at the end of this article on that subject. Also, SndMulFiles was built using Mime & Mail's version dated April 26, 2005. You might have to perform minor adjustments to parameter sizes and such when you download and install the current version of the application from Giovanni's Web site.
In conclusion, I hope this downloadable code will help you fight, in your own "battleground," the idea that System i is an old-fashioned, antique, good-for-nothing server!
Other Procedures
For the email sending stuff to work, quite a few other procedures are necessary. They are presented here, divided into three groups: email address-related procedures (service program ADDRESS), Mime & Mail sending procedures (service program MMAILSND), and miscellaneous procedures (service program MISC).
Email Address-Related Procedures
RtvGrpAddrs
This procedure retrieves the email addresses that belong to the group specified in parameter P_Group. It can return up to 10 email addresses and email names in parameters P_Emails and P_Names, respectively. The addresses are retrieved from file LFEGRP01, a logical file over PFEGRP (E-Mail Groups file). The names are retrieved via procedure RtvEMailName, explained next. Even though the procedure's arrays return only 10 addresses at a time, there is a way to read a group with more than 10 members: if parameter P_Email is filled, the group's file will be read from that address on.
RtvEMailName
This procedure returns the name associated with the email address passed in parameter P_Email. It can be either the name found for the email address in the LFEMAIL02 file or a logical file over PFEMAIL (E-Mail Addresses file), or it can be composed from the left part (before the @ sign) of the P_Email parameter.
RtvEMailAddr
This procedure retrieves the email address associated with the name passed in parameter P_Name. If the parameter does not contain an Internet email address already, it is looked up in the email addresses file via P_Name, and, if it is not found, RtvSmtpAddr procedure (explained next) is used to find it in the system's directory.
RtvSmtpAddr
This procedure retrieves the email address from the system directory for the specified user ID. It was created by Carsten Flensburg and published in the Think400 site. I've converted it to free-format and performed minor changes. Here's the author's description of the procedure:
"Searches system directory based directory on input search criteria(s) and returns the requested user information for the found entries.
Sequence of events:
1. The API input parameters are initialized
2. The search directory API is called
3. If an error occurs while calling the API or no entry is found blanks are returned to the caller
4. If an entry is found the requested SMTP-address is retrieved, formatted and returned to the caller
Parameters:
P_User (INPUT) - User-id of the directory entry searched. Determined by the presence of the second parameter this can be both a user profile name and the first part of the system directory entry user identifier. The special value *CURRENT will be replaced by the job's current user profile name.
P_Addr (INPUT) - The address qualifier of the directory entry searched.
Return-value (OUTPUT) - The formatted SMTP-address of the system directory entry specified by the input parameter(s). If no matching entry was found or an error occurred blanks are returned to the caller.”
InternalAddr
This is a rather useful yet simple procedure. It returns *On if the email address passed in the P_Address parameter belongs to the company and *Off otherwise. The check is performed based on the email domain name of the company, defined in data area DaMailDmn. Function ScanForString is used to search for the domain in the P_Address parameter.
Mime & Mail Sending Procedures
MMailSender
This procedure invokes all the necessary Mime & Mail APIs to create and send an email message, based on the following input parameters:
- P_Error--Error indicator; '2' means error
- P_SName--Sender's name
- P_SEmail--Sender's email address
- ToNameArr--Array containing the email names of the message's destinations
- ToAddrArr--Array containing the email addresses of the message's destinations
- ToDistArr--Array containing the type of destination (To, Cc, or Bcc) for each entry of the previous arrays
- P_ReplyTo--Reply To address
- P_MailSubject--Subject
- P_Impo--Importance
- P_Prio--Priority
- FromFNameA--Array containing IFS file names to attach (spool files, physical files, and other IFS files)
- P_SplName--Array containing the names of the spool files to be sent as attachments to the message
- P_ImbAtt--Array containing the treatment of each of the previous array's entries
Since this procedure uses Mime & Mail APIs, please refer to the application's documentation
for more information. However, there are some details worth mentioning:
- The DaMimeLck data area is used to prevent a problem with the sending process--some sort of temporary file duplication that causes messages to get mixed up. It's possible that the current Mime & Mail version no longer requires this "trick" to work properly, but the one that we have installed does.
- It is assumed, in this procedure, that the spool file that is supposed to be attached to the message is the last generated.
Miscellaneous Procedures
ExecCmd
This very handy procedure is used to replace the call to the QCMDEXC API. It was created by the bright guys of the Free RPG Tools Web site, and it consists of the invocation of the QCAPCMD API with a programmer-friendly interface. The main advantage over QCMDEXC is that the command is validated before execution, thus ensuring the call won't end in error. If there is an error (usually CPF something), it is returned in ApiError.ApiMsgID. I've initialized some variables early in the code to prevent a bug that occurred when the procedure was invoked multiple times within the same program.
ScanForString
This simple yet extremely useful procedure returns *On if the value passed in the ScanString parameter is found in the Text parameter. It can also convert both parameters to uppercase, a kind of "ignore case search" if the CvtUpC parameter contains Y, and return the position in the Text parameter, where the ScanString parameter begins, if a receiving variable is provided for the fourth parameter (P_Position).
CvtLwcUpc
Another simple yet extremely useful procedure. Using the %XLATE BIF and the W_Up and W_Lo work variables, it converts the TextLower parameter to uppercase. Note that the content of these variables must be adjusted to use your language's special characters in uppercase and lowercase, respectively.
ProductionSys
If you have more than one System i, you're probably familiar with those annoying situations in which, for some reason, someone does something in the production system, mistakenly thinking it's the development or test system. This procedure returns *On if the data area DASYSTYPE is set to P (Production). We use it in the email procedures to avoid having test messages get sent outside the company.
Configuration
The following explanation was adapted from an article written by Steve Miall, on October 4, 2000, from company Genesisv:
Requirements
1. OS/400 release 4.2 or higher
2. An SMTP gateway somewhere in your network
Setup
1. Get the IP address of the email (SMTP) gateway. This is either a PC or network server on your network, or it is the server at your local ISP.
2. Make sure this SMTP gateway is in your System i hosts table:
ADDTCPHTE INTNETADR('192.168.1.50') HOSTNAME('SMTP')
If the Internet address is already in the table, make a note of its host table name and use it from here on instead of "SMTP."
3. Change the SMTP attributes:
CHGSMTPA AUTOSTART(*YES) MAILROUTER('SMTP') FIREWALL(*YES)
4. Add your email users to the SMTP alias names system table via CFGTCPSMTP and option 1. These are the users you want to be able to send emails. This is necessary for the email to have a decent-looking "from" name and for the email replies to get to a real email account.
If you don't add an alias table, your email will be sent from
Use WRKDIRE *ALL to see the user names registered in the directory. Using F19, you'll be able to specify the SMTP use ID and domain to use. I highly recommend that you fill these parameters for all your users, since procedure RtvSmtpAddr will use this information.
To review or change the domain name of the system, use the CHGTCPDMN command (prompt command with F4).
The alias table consists of user ID, address, SMTP user ID, and SMTP domain. For example, this is my entry:
USERID MYUSER
ADDRESS GVUK
SMTP user name support
SMTP Domain GENESISV.COM
With this entry, my mail is sent from
MYUSER=User
GVUK=Address
GVUK=Host name
GENESISV.COM=Domain
5. Add a generic user in the directory to route email. Either use WRKDIRE and take the add option, or use the following:
ADDDIRE (INTERNET SMTPRTE) USRD('Internet generic user') +
SYSNAME(TCPIP) NETUSRID(*USRID) +
MSFSRVLVL(*USRIDX) PREFADR(NETUSRID *IBM ATCONTXT)
6. Add a distribution queue:
ADDDSTQ DSTQ(QSMTPQ) RMTLOCNAME(TCPIP) DSTQTYPE(*RPDS)
7. Add a routing table entry, via CFGDSTSRV then option 2:
Add Routing Table Entry
Type choices, press Enter. (At least one queue name is required.)
System name/Group . . TCPIP
Description . . . . . TCP/IP Routing
Service level:
Fast:
Queue name . . . . QSMTPQ Distribution queue name
Maximum hops . . . *DFT Number of hops, *DFT
Status:
Queue name . . . . QSMTPQ
Maximum hops . . . *DFT
Data high:
Queue name . . . . QSMTPQ
Maximum hops . . . *DFT
Data low:
Queue name . . . . QSMTPQ
Maximum hops . . . *DFT
F3=Exit F12=Cancel
8. Change the distribution attributes:
CHGDSTA KEEPRCP(*BCC) USEMSFLCL(*NO) SMTPRTE(INTERNET SMTPRTE)
9. There can be an authority problem, which can cause the MSF facility to die. This will cause all emailing and all QSNADS message-sending to stop until MSF is restarted (ENDMSF and STRMSF). To get around it, grant the following authorities:
GRTOBJAUT OBJ(QSYS/QZMFARSV) OBJTYPE(*PGM) USER(QTCP) AUT(*USE)
GRTOBJAUT OBJ(QSYS/QZMFARSV) OBJTYPE(*PGM) USER(QMSF) AUT(*USE)
GRTOBJAUT OBJ(QSYS/QZMFASCR) OBJTYPE(*PGM) USER(QTCP) AUT(*USE)
GRTOBJAUT OBJ(QSYS/QZMFASCR) OBJTYPE(*PGM) USER(QMSF) AUT(*USE)
GRTOBJAUT OBJ(QSYS/QZMFACHG) OBJTYPE(*PGM) USER(QTCP) AUT(*USE)
GRTOBJAUT OBJ(QSYS/QZMFACHG) OBJTYPE(*PGM) USER(QMSF) AUT(*USE)
GRTOBJAUT OBJ(QSYS/QZMFACRT) OBJTYPE(*PGM) USER(QTCP) AUT(*USE)
GRTOBJAUT OBJ(QSYS/QZMFACRT) OBJTYPE(*PGM) USER(QMSF) AUT(*USE)
10. Start the server:
STRTCPSVR *SMTP
11. Test to see if it sends:
SNDDST TYPE(*LMSG) TOUSRID((INTERNET SMTPRTE)) +
DSTD('E-mail') +
TOINTNET((
SUBJECT('This is the subject') +
LONGMSG('This is the message')
Then, go to your mailbox and see if you have received the message.
12. To send from a QDLS document, use this code:
SNDDST TYPE(*FILE) TOUSRID((INTERNET SMTPRTE)) +
TOINTNET((
DSTD('Email') +
MSG('This is a test') +
DOC(document.TXT) FLR(folder)
The subject will be the document description; the SUBJECT parameter is accepted but ignored. The MSG parameter is optional. You cannot have a long message with an attached document, and you cannot attach two documents.
13. If you have problems and need additional information on setting up TCP/IP and other related issues, try going to www.easy400.net/tcpcfgs.
LATEST COMMENTS
MC Press Online