#+ #+ A library of functions related to the management and sending of emails #+ #+ It serves two primary purposes: #+ 1. To send system generated emails (invoices etc.) that can be submitted directly or via one of the background threads #+ 2. To manage conversations related to prospect/customer activities #+ import java java.lang.Object import java java.lang.System import java java.lang.Exception import java java.lang.Throwable import java java.lang.String import java java.text.DateFormat import java java.io.InputStream import java java.io.FileOutputStream import java java.io.FileInputStream import java java.io.File import java java.util.Properties import java java.util.Date import java java.util.regex.Matcher import java java.util.regex.Pattern import java javax.mail.Part import java javax.mail.Flags import java javax.mail.Flags.Flag import java javax.mail.Message import java javax.mail.Message.RecipientType import java javax.mail.Multipart import java javax.mail.Address import java javax.mail.internet.InternetAddress import java javax.mail.internet.MimeBodyPart import java javax.mail.internet.MimeMultipart import java javax.mail.internet.ContentType import java javax.mail.internet.MimeMessage import java javax.mail.Session import java javax.mail.internet.MimeMessage import java javax.mail.internet.MimeUtility import java javax.mail.Transport import java javax.mail.SendFailedException import java javax.mail.MessagingException import java javax.mail.Service import java javax.mail.Store import java javax.mail.Folder import java org.apache.commons.mail.util.MimeMessageParser import java com.aspose.email.MailMessage import java com.aspose.email.SaveOptions import java com.aspose.email.FileFormatInfo import java com.aspose.email.FileFormatUtil import java com.aspose.email.FileFormatType import java com.aspose.email.License import com import os import xml import util import fgl tcm_var import fgl tcm_sys import fgl tcm_usr import fgl tcm_xml import fgl tcm_doc import fgl tcm_evt import fgl tcm_rcp schema teqc -- Types and Record Definitions. public define emailBodyHTML boolean define smtpToA dynamic array of string define smtpCcA dynamic array of string define smtpBccA dynamic array of string define smtpAttachmentsA dynamic array of string define smtpSession javax.mail.Session define smtpProtocol string define outboundMessage javax.mail.internet.MimeMessage define outboundMessageMultiPart javax.mail.internet.MimeMultipart type ia array[] of javax.mail.internet.InternetAddress public define p_email record sender, # The sender's email adress sendername, # The sender's name recipient, # The recipient(s) email address recipientcc, # The CC(s) email address recipientbcc, # The BCC(s) email address attachments, # A list of ; separated attachments. Full path with extension subject, # The subject messagebody string, # Path to the body text attachmentsA dynamic array of record # Array of attachments attachmentpath, # Attachment path on the server attachmentname string, # Attachment full name bFromForwardedMessage boolean # Is this attachment related to a message being forwarded end record, headerAttributes dynamic array of record # Array of header option headerName, headerValue string end record, messageHTMLBody string, # The html body as a string saveEmail boolean, # Flag to indicate if we should save the email as a case conversation emailAction integer, # What are we doing - creating, replying etc. sourceMessageID integer # The message ID of that we are replying to or forwarding end record define tokenR record # A record to hold the OAuth2 token for M365 authentication token_type, scope, expires_in, ext_expires_in, access_token, refresh_token, id_token string end record define inlineContentA dynamic array of record # An array holding inline content. Used to replace the CID with a web server URL sFileName string, # The content ID of the inline element sFilePath string, # The path on the web server sFileURL string # The web server URL end record define attachmentsA dynamic array of record # A general array to hold the attachments when processing emails sFileName string end record public define p_emailA dynamic array of record # An array for the display array of a conversation emailfrom, paperclip, emailsubject, emaildate string, emailid integer end record define emlh record like teqemlh.* define emla record like teqemla.* define adx integer public define p_emailIndex integer public define p_bCanDelete boolean public constant emailActionCompose = 1 public constant emailActionReply = 2 public constant emailActionReplyAll = 3 public constant emailActionForward = 4 public constant emailActionCopyIn = 5 public constant emailClassActivity = "ACT" public constant emailClassPerson = "PER" #+ #+ Sub dialog for displaying email conversations #+ dialog eml_emailHeadersDialog() display array p_emailA to emailR.* attributes(focusonfield) before row call dialog.setActionActive("delete_email", p_emailA.getLength() and p_bCanDelete) call attachmentsA.clear() let p_emailIndex = arr_curr() if p_emailIndex then call eml_displayMessage() end if on action delete_email let p_emailIndex = arr_curr() if p_emailIndex then if sys_askQuestion(%"Confirm", %"Delete selected message?", "No", "Yes|No", "") = "Yes" then let emlh.s_emlh = p_emailA[p_emailIndex].emailid call eml_getEmailHeader(emlh.s_emlh) returning emlh.* call eml_deleteMessage(emlh.s_emlh) call attachmentsA.clear() call eml_loadMessages(dialog, emlh.parentclass, emlh.parentid) if not p_emailA.getLength() then call sys_setNodeAttribute(null,null,"Label","lblsubject","text","") call sys_setNodeAttribute(null,null,"Label","lblfrom","text","") call sys_setNodeAttribute(null,null,"Label","lbldate","text","") display "" to emailview end if call dialog.setActionActive("delete_email", p_emailA.getLength() and p_bCanDelete) end if end if before display call attachmentsA.clear() call dialog.setActionActive("delete_email", false) end display end dialog #+ #+ Function to display the currently selected message #+ function eml_displayMessage() call eml_getEmailHeader(p_emailA[p_emailIndex].emailid) returning emlh.* call sys_setNodeAttribute(null,null,"Label","lblsubject","text",emlh.subject) call sys_setNodeAttribute(null,null,"Label","lblfrom","text",emlh.emailfrom) call sys_setNodeAttribute(null,null,"Label","lbldate","text",emlh.emaildate) display eml_prepareEmailForViewing(emlh.documentid) to emailview call eml_loadAttachments(emlh.s_emlh) end function #+ #+ Get an email header #+ function eml_getEmailHeader(messageID like teqemlh.s_emlh) define emlh record like teqemlh.* select * into emlh.* from teqemlh where teqemlh.s_emlh = messageID if status = notfound then initialize emlh.* to null end if return emlh.* end function #+ #+ Sub dialog for displaying email attachments #+ dialog eml_emailAttachmentsDialog() display array attachmentsA to attchR.* on action get_attachment let adx = arr_curr() if adx then call eml_getAttachment(emlh.documentid, attachmentsA[adx].sFileName) end if end display end dialog #+ #+ Load the attachments in an array for the selected message #+ function eml_loadAttachments(inMessageID integer) declare a2C cursor for select * from teqemla where teqemla.s_emlh = inMessageID call attachmentsA.clear() foreach a2C into emla.* let attachmentsA[attachmentsA.getLength() + 1].sFileName = emla.attachment end foreach end function #+ #+ Get the attachment from the passed message ID and attachment name #+ function eml_getAttachment(inMessageID integer, inAttachmentName string) define sFileSource, sFileName, sFileDestination string whenever error continue let sFileName = eml_getEmailAttachment(inMessageID, inAttachmentName) let sFileDestination = g_tmpClientDir.trim(), g_ClientPathSeperator.trim(), sFileName.trim() let sFileSource = g_tmpServerDir.trim(), sFileName call fgl_putfile(sFileSource, sFileDestination) if status then call sys_showMessage(%"Error", %"Unable to download file: "||sFileSource||"\rTo: "||sFileDestination, "") whenever error call sys_errorHandler return end if whenever error call sys_errorHandler call sys_launchDocument(sFileDestination) end function #+ #+ function to delete an email and any associated attachments #+ function eml_deleteMessage(inMessageID integer) define emlh record like teqemlh.* call eml_getEmailHeader(inMessageID) returning emlh.* if sys_deleteDocumentByID(emlh.documentid) then delete from teqemla where teqemla.s_emlh = emlh.s_emlh delete from teqemlh where teqemlh.s_emlh = emlh.s_emlh end if end function #+ #+ Delete a set of messages for a given parent #+ #+ This function allows for the deletion of all the messages associated with the passed parent class and ID #+ #+ @param ins_fdoc Integer: Existing document to delete #+ #+ @return Nothing #+ function eml_deleteParentMessages(inParentClass like teqemlh.parentclass, inParentID like teqemlh.parentid) define emlh record like teqemlh.* declare deleteParentMessagesC cursor for select * from teqemlh where parentclass = inParentClass and parentid = inParentID foreach deleteParentMessagesC into emlh.* if sys_deleteDocumentByID(emlh.documentid) then delete from teqemla where teqemla.s_emlh = emlh.s_emlh delete from teqemlh where teqemlh.s_emlh = emlh.s_emlh end if end foreach end function #+ #+ Load the email messages associated with the passed class and parent #+ function eml_loadMessages(inDialog ui.Dialog, inParentClass like teqemlh.parentclass, inParentID like teqemlh.parentid) define nameSeparator char(1), sFrom string, idx integer, dEmailDate, dStartOfWeek date, sEmailDisplayDate char(10), sEmailDate, sSubject, sCurrentDate, sSql string declare messageC cursor for select * from teqemlh where teqemlh.parentclass = inParentClass and teqemlh.parentid = inParentID call p_emailA.clear() let nameSeparator = "<" let dStartOfWeek = today while weekday(dStartOfWeek) != 1 let dStartOfWeek = dStartOfWeek - 1 end while let sSql = "select date_format(current_timestamp, \"%a %b %d\")" prepare getDateQ from sSql declare getDateC cursor for getDateQ open getDateC fetch getDateC into sCurrentDate close getDateC foreach messageC into emlh.* let sEmailDate = emlh.emaildate if sEmailDate.subString(1,11) = sCurrentDate then let sEmailDisplayDate = sEmailDate.subString(12,16) else let dEmailDate = eml_getDateFromUTC(sEmailDate) if dEmailDate is not null then if dEmailDate >= dStartOfWeek then let sEmailDisplayDate = sEmailDate.subString(1,3), " ", sEmailDate.subString(12,16) else let sEmailDisplayDate = sEmailDate.subString(1,3), " ", dEmailDate using "dd/mm" end if else let sEmailDisplayDate = sEmailDate.subString(5,10) end if end if let sFrom = emlh.emailfrom let idx = sFrom.getIndexOf(nameSeparator, 1) if idx then let sFrom = sFrom.subString(1,idx-1) end if let sSubject = eml_removeCaseID(emlh.subject, emlh.parentid) let sFrom = "
",sFrom.trim()," | ",
"
"||sSubject.trim()||" | ", "
A new email has been received for case: "||iCaseNumber||"
"||nvl(sSubject, "-")||"
" let p_email.saveEmail = false let p_email.emailAction = emailActionCopyIn let p_email.sourceMessageID = emlh.documentid if not eml_sendEmail() then call sys_writeToDailyLog(__LINE__, __FILE__, 3, "Failed to send copy email") end if end if end if return true end function #+ #+ Function to extract the case or activity ID from the subject and ensure that a valid case exists #+ function eml_getCaseFromSubject(sSubject string) define iCaseNumber integer, sCaseNumber, sSubjectAnchor, sSubjectAnchorBase string, sdx, ndx integer define pattern java.util.regex.Pattern define matcher java.util.regex.Matcher let sSubjectAnchor = sys_getControl("CCANC", 1, null, null, "pardata") let sSubjectAnchorBase = sys_getControl("CCANC", 2, null, null, "pardata") let pattern = java.util.regex.Pattern.compile(sSubjectAnchor) let matcher = pattern.matcher(sSubject) if matcher.find() then let sCaseNumber = "" let sdx = sSubject.getIndexOf(sSubjectAnchorBase, 1) if sdx then let sdx = sdx + sSubjectAnchorBase.getLength() for ndx = sdx to sSubject.getLength() if not sys_isAnInteger(sSubject.getCharAt(ndx)) then exit for end if let sCaseNumber = sCaseNumber.append(sSubject.getCharAt(ndx)) end for if sys_isAnInteger(sCaseNumber) then select s_fact into iCaseNumber from tctfact where tctfact.s_fact = sCaseNumber if status = notfound then let iCaseNumber = 0 call sys_writeToDailyLog(__LINE__, __FILE__, 3, "Cound not find activity with ID "||nvl(sCaseNumber, "NULL")) end if else call sys_writeToDailyLog(__LINE__, __FILE__, 3, "Invalid case ID "||nvl(sCaseNumber, "NULL")) end if else call sys_writeToDailyLog(__LINE__, __FILE__, 3, "Invalid case ID "||nvl(sCaseNumber,"NULL")) end if else call sys_writeToDailyLog(__LINE__, __FILE__, 3, "Could not find anchor in "||nvl(sSubject,"NULL")||" with "||nvl(pattern.pattern(), "NULL")) end if return iCaseNumber end function #+ #+ Function to prepare a message for viewing #+ #+ This takes a message id and then: #+ 1. Gets the message and loads it into a java message object #+ 2. Extracts all of the inline content and stores them as documents in the web server folder #+ 3. Extracts the body and replaces the img src anchors for the inline content to point to those copied to the web server #+ 4. Returns the amended html body #+ public function eml_prepareEmailForViewing(msgDocumentID integer) define props Properties define mailSession Session define currentMessage javax.mail.internet.MimeMessage define messageMultiPart javax.mail.internet.MimeMultipart define messageBodyPart javax.mail.internet.MimeBodyPart define inputStream java.io.FileInputStream define pdx, idx, iNumberOfParts integer define sContentType string define sBody string define msgFileName string define msgFilePath string if not nvl(msgDocumentID, 0) then return sBody end if let msgFileName = sys_getDocumentPath(msgDocumentID) if not os.Path.exists(msgFileName) then call sys_writeToDailyLog(__LINE__, __FILE__, 0, "Message missing: "||nvl(msgFileName, "NULL")) return sBody end if # Get the message file let props = System.getProperties() let mailSession = javax.mail.Session.getInstance(props) let msgFilePath = java.io.File.create(msgFileName) let inputStream = java.io.FileInputStream.create(msgFilePath) let currentMessage = javax.mail.internet.MimeMessage.create(mailSession, inputStream) # Parse the parts looking for in line content call inlineContentA.clear() let sContentType = currentMessage.getContentType() if sContentType.getIndexOf("multipart", 1) then let messageMultiPart = cast(currentMessage.getContent() as javax.mail.internet.MimeMultipart) let iNumberOfParts = messageMultiPart.getCount() for idx = 0 to iNumberOfParts - 1 let messageBodyPart = cast(messageMultiPart.getBodyPart(idx) as javax.mail.internet.MimeBodyPart) call eml_getInlineContentLinks(messageBodyPart) end for end if # Get the message content let sBody = eml_getBodyContent(currentMessage) # Loop through the inline content and amend the body for pdx = 1 to inlineContentA.getLength() if inlineContentA[pdx].sFileName.getLength() then call sys_writeToDailyLog(__LINE__, __FILE__, 3, "Replacing: "||nvl("cid:"||inlineContentA[pdx].sFileName, "NULL")||" with "||nvl(inlineContentA[pdx].sFileURL, "NULL")) let sBody = sys_stringReplace(sBody, "cid:"||inlineContentA[pdx].sFileName, inlineContentA[pdx].sFileURL) end if end for return sBody end function #+ #+ Function to actually extract the inline content links of the message #+ Passed a body part, it will recursively look for inline content #+ #+ Each inline attachment is extracted and saved to a file in the var/www directory of the web server #+ The inlinecontentA array is used to record the cid and the file location on the web server #+ function eml_getInlineContentLinks(part javax.mail.internet.MimeBodyPart) define childMultiPart javax.mail.internet.MimeMultipart, childPart javax.mail.internet.MimeBodyPart, idx integer, sBaseDir, sBaseURL, sValidDisposition, sDisposition, sAttachmentName string let sValidDisposition = javax.mail.internet.MimeBodyPart.INLINE.toLowerCase() let sBaseURL = sys_getControl("WADDR", 1, null, null, "pardata") let sBaseDir = sys_getControl("WADDR", 2, null, null, "pardata") if (eml_isMimeType(part, "multipart/*")) then let childMultiPart = cast(part.getContent() as javax.mail.internet.MimeMultipart) for idx = 0 to childMultiPart.getCount() - 1 let childPart = cast(childMultiPart.getBodyPart(idx) as javax.mail.internet.MimeBodyPart) call eml_getInlineContentLinks(childPart) end for else let sDisposition = part.getDisposition() let sAttachmentName = part.getFileName() if sDisposition = sValidDisposition or (not sDisposition.getLength() and sAttachmentName.getLength()) then call sys_writeToDailyLog(__LINE__, __FILE__, 3, "Got inline content: "||nvl(part.getContentID(), "NULL")) let inlineContentA[inlineContentA.getLength() + 1].sFileName = eml_stripContentId(part.getContentID()) let inlineContentA[inlineContentA.getLength()].sFilePath = sBaseDir.append(eml_sanitiseFilename(inlineContentA[inlineContentA.getLength()].sFileName)) let inlineContentA[inlineContentA.getLength()].sFileURL = sBaseURL.append(eml_sanitiseFilename(inlineContentA[inlineContentA.getLength()].sFileName)) call part.saveFile(inlineContentA[inlineContentA.getLength()].sFilePath) end if end if end function #+ #+ Function to extract the inline content of the message when replying or forwarding a message #+ #+ To preserve the fidelity of the reply or forwarded message the original message is appended to the new one #+ As a result any inline content or attachments (for forwarded messages) need to be extracted from the original #+ message and added as body parts to the new one #+ function eml_getSourceMessageContent(thisPart javax.mail.internet.MimeBodyPart, bInlineContentOnly boolean) define childMultiPart javax.mail.internet.MimeMultipart, childPart javax.mail.internet.MimeBodyPart, adx, idx integer, sAttachmentDisposition, sInlineDisposition, sDisposition, sAttachmentName string let sInlineDisposition = javax.mail.internet.MimeBodyPart.INLINE.toLowerCase() let sAttachmentDisposition = javax.mail.internet.MimeBodyPart.ATTACHMENT.toLowerCase() if (eml_isMimeType(thisPart, "multipart/*")) then let childMultiPart = cast(thisPart.getContent() as javax.mail.internet.MimeMultipart) for idx = 0 to childMultiPart.getCount() - 1 let childPart = cast(childMultiPart.getBodyPart(idx) as javax.mail.internet.MimeBodyPart) call eml_getSourceMessageContent(childPart, bInlineContentOnly) end for else let sDisposition = thisPart.getDisposition() let sAttachmentName = thisPart.getFileName() if sDisposition = sInlineDisposition or (not sDisposition.getLength() and sAttachmentName.getLength()) then call sys_writeToDailyLog(__LINE__, __FILE__, 3, "Got inline content: "||nvl(thisPart.getContentID(), "NULL")) call outboundMessageMultiPart.addBodyPart(thisPart) else if not bInlineContentOnly then for adx = 1 to p_email.attachmentsA.getLength() call sys_writeToDailyLog(__LINE__, __FILE__, 3, "Checking inline content: "||p_email.attachmentsA[adx].bFromForwardedMessage||" "||p_email.attachmentsA[adx].attachmentname||nvl(sAttachmentName, "NULL")) if p_email.attachmentsA[adx].bFromForwardedMessage and p_email.attachmentsA[adx].attachmentname = sAttachmentName then call sys_writeToDailyLog(__LINE__, __FILE__, 3, "Got inline content: "||nvl(thisPart.getContentID(), "NULL")) call outboundMessageMultiPart.addBodyPart(thisPart) end if end for end if end if end if end function #+ #+ Function to get the body content of the passed message #+ function eml_getBodyContent(thisMessage javax.mail.internet.MimeMessage) define sBody string, mailParser org.apache.commons.mail.util.MimeMessageParser let mailParser = org.apache.commons.mail.util.MimeMessageParser.create(thisMessage) call mailParser.parse() if mailParser.hasHtmlContent() then let sBody = mailParser.getHtmlContent() else let sBody = mailParser.getPlainContent() end if return sBody end function #+ #+ Function to validate a message body part type #+ private function eml_isMimeType(part javax.mail.internet.MimeBodyPart, mimeType string) define thisType javax.mail.internet.ContentType try let thisType = javax.mail.internet.ContentType.create(part.getDataHandler().getContentType()) return thisType.match(mimeType) catch return part.getContentType().equalsIgnoreCase(mimeType) end try end function #+ #+ Function to get an email attachment #+ #+ This takes a message id and an attachment name and: #+ 1. Gets the message and loads it into a java message object #+ 2. Extracts the specified attachment #+ 3. Returns the sanitised file name #+ public function eml_getEmailAttachment(msgDocumentID integer, msgAttachment string) define props Properties define mailSession Session define inputStream java.io.FileInputStream define messageMultiPart javax.mail.internet.MimeMultipart define messageBodyPart javax.mail.internet.MimeBodyPart define currentMessage javax.mail.internet.MimeMessage define pdx integer define iNumberOfParts integer define sContentType string define attFileName string define msgFileName string define msgFilePath string let msgFileName = sys_getDocumentPath(msgDocumentID) let attFileName = null if not os.Path.exists(msgFileName) then call sys_writeToDailyLog(__LINE__, __FILE__, 3, "Missing message: "||nvl(msgFileName, "NULL")) return attFileName end if call sys_writeToDailyLog(__LINE__, __FILE__, 3, "Looking for: "||nvl(msgAttachment, "NULL")) call sys_writeToDailyLog(__LINE__, __FILE__, 3, "Looking in: "||nvl(msgFileName, "NULL")) # Get the message file let props = System.getProperties() let mailSession = javax.mail.Session.getInstance(props) let msgFilePath = java.io.File.create(msgFileName) let inputStream = java.io.FileInputStream.create(msgFilePath) let currentMessage = javax.mail.internet.MimeMessage.create(mailSession, inputStream) # Loop through the parts and recursively look at each for the required attchment let sContentType = currentMessage.getContentType() if sContentType.getIndexOf("multipart", 1) then let messageMultiPart = cast(currentMessage.getContent() as javax.mail.internet.MimeMultipart) let iNumberOfParts = messageMultiPart.getCount() for pdx = 0 to iNumberOfParts - 1 let messageBodyPart = cast(messageMultiPart.getBodyPart(pdx) as javax.mail.internet.MimeBodyPart) let attFileName = eml_getAttachmentContent(messageBodyPart, msgAttachment) if attFileName.getLength() then exit for end if end for end if call sys_writeToDailyLog(__LINE__, __FILE__, 3, "Returning: "||nvl(attFileName, "NULL")) return attFileName end function #+ #+ Function to extract the content of the passed attachment name from the passed message part #+ function eml_getAttachmentContent(part javax.mail.internet.MimeBodyPart, msgAttachment string) define childMultiPart javax.mail.internet.MimeMultipart, childPart javax.mail.internet.MimeBodyPart, childMessage javax.mail.internet.MimeMessage, idx integer, msgFileName, attFileName, returnFileName, sValidDisposition, sMessageType, sDisposition string let sValidDisposition = javax.mail.internet.MimeBodyPart.ATTACHMENT if eml_isMimeType(part, "multipart/*") then let childMultiPart = cast(part.getContent() as javax.mail.internet.MimeMultipart) for idx = 0 to childMultiPart.getCount() - 1 let childPart = cast(childMultiPart.getBodyPart(idx) as javax.mail.internet.MimeBodyPart) let attFileName = eml_getAttachmentContent(childPart, msgAttachment) if attFileName.getLength() then exit for end if end for else let returnFileName = null let sDisposition = part.getDisposition() if sDisposition.getLength() then if sDisposition.getIndexOf(sValidDisposition, 1) then let attFileName = part.getFileName() call sys_writeToDailyLog(__LINE__, __FILE__, 3, "Got attachment: "||nvl(attFileName, "NULL")) if attFileName.getLength() then if attFileName.trim() = msgAttachment.trim() then call sys_writeToDailyLog(__LINE__, __FILE__, 3, "File name match") let attFileName = eml_sanitiseFilename(attFileName) if eml_isMimeType(part, "message/*") then call sys_writeToDailyLog(__LINE__, __FILE__, 3, "Attachment appears to be a message") let sMessageType = sys_getDocumentType(attFileName) if sMessageType.getLength() = 0 or (sMessageType != "eml" and sMessageType != "msg") then let attFileName = attFileName.append(".eml") end if end if let msgFileName = g_tmpServerDir.append(attFileName) call sys_writeToDailyLog(__LINE__, __FILE__, 3, "Saving to: "||nvl(msgFileName, "NULL")) call part.saveFile(msgFileName) let returnFileName = attFileName end if else if eml_isMimeType(part, "message/*") then let childMessage = cast(part.getContent() as javax.mail.internet.MimeMessage) let attFileName = childMessage.getSubject() if attFileName.trim() = msgAttachment.trim() then call sys_writeToDailyLog(__LINE__, __FILE__, 3, "Message subject name match") let attFileName = eml_sanitiseFilename(attFileName.append(".eml")) let msgFileName = g_tmpServerDir.append(attFileName) call sys_writeToDailyLog(__LINE__, __FILE__, 3, "Saving to: "||nvl(msgFileName, "NULL")) call part.saveFile(msgFileName) let returnFileName = attFileName end if end if end if end if end if end if return returnFileName end function #+ #+ Function to extract a list of attachments #+ #+ This is used to populate the message's meta data #+ function eml_getAttachmentList(part javax.mail.internet.MimeBodyPart) define childMultiPart javax.mail.internet.MimeMultipart, childPart javax.mail.internet.MimeBodyPart, childMessage javax.mail.internet.MimeMessage, idx integer, attFileName, sValidDisposition, sDisposition string let sValidDisposition = javax.mail.internet.MimeBodyPart.ATTACHMENT if (eml_isMimeType(part, "multipart/*")) then let childMultiPart = cast(part.getContent() as javax.mail.internet.MimeMultipart) for idx = 0 to childMultiPart.getCount() - 1 let childPart = cast(childMultiPart.getBodyPart(idx) as javax.mail.internet.MimeBodyPart) call eml_getAttachmentList(childPart) if attFileName.getLength() then exit for end if end for else let sDisposition = part.getDisposition() if sDisposition.getLength() then if sDisposition.trim() = sValidDisposition.trim() then let attFileName = part.getFileName() if attFileName.getLength() then call sys_writeToDailyLog(__LINE__, __FILE__, 3, "Got attachment: "||nvl(attFileName, "NULL")) let attachmentsA[attachmentsA.getLength() + 1].sFileName = attFileName else if eml_isMimeType(part, "message/*") then let childMessage = cast(part.getContent() as javax.mail.internet.MimeMessage) let attFileName = childMessage.getSubject() call sys_writeToDailyLog(__LINE__, __FILE__, 3, "Got message attachment: "||nvl(attFileName, "NULL")) let attachmentsA[attachmentsA.getLength() + 1].sFileName = attFileName end if end if end if end if end if end function #+ #+ Function to make a file name safe for saving #+ private function eml_sanitiseFilename(stringToSanitise string) define sJavaString java.lang.String let sJavaString = java.lang.String.create() let sJavaString = stringToSanitise return sJavaString.replaceAll("[\\[\\]:\\\\/*?|<> \"]", "_") end function #+ #+ Function to strip the surrounding < and > from the content id string #+ private function eml_stripContentId(contentID string) define sJavaString java.lang.String let sJavaString = java.lang.String.create() let sJavaString = contentID if not contentID.getLength() then return contentID else return sJavaString.trim().replaceAll("[\\<\\>]", ""); end if end function #+ #+ Function to get the authorisation token #+ function eml_getOffice365Token() define sBaseURL string, requestR record client_id, client_secret, scope, username, password, grant_type string end record, sOutputDir, sOutputFileName, sRequestBody string, outputChannel base.channel define webRequest com.HttpRequest define webResponse com.HttpResponse define p_webService record webStatus integer, response string end record # Get the client ID let sBaseURL = sys_getControl("MS365", 1, null, null, "pardata") # "https://login.microsoftonline.com/ebc1e8d4-b8f2-4c97-93a4-ad781b8ef36e/oauth2/v2.0/token" let requestR.grant_type = "password" let requestR.client_id = sys_getControl("MS365", 2, null, null, "pardata") # "16f772b1-8d24-4ca1-a3be-35467183d622" let requestR.client_secret = sys_getControl("MS365", 3, null, null, "pardata") # "wjO7Q~VX4gzFIYdLuB311Fh1h2TJ74P94uZpI" let requestR.scope = sys_getControl("MS365", 4, null, null, "pardata") # "openid profile https://outlook.office365.com/IMAP.AccessAsUser.All" let requestR.username = sys_getControl("MS365", 5, null, null, "pardata") # "customercare@alsico.co.uk" let requestR.password = sys_getControl("MS365", 6, null, null, "pardata") # "iS0Shy6DHxPA" let sOutputDir = sys_getControl("MS365", 9, null, null, "pardata") let sOutputFileName = sOutputDir||"o365.json" let sRequestBody = "&scope="||requestR.scope let sRequestBody = sRequestBody.append("&grant_type="||requestR.grant_type) let sRequestBody = sRequestBody.append("&client_id="||requestR.client_id) let sRequestBody = sRequestBody.append("&client_secret="||requestR.client_secret) let sRequestBody = sRequestBody.append("&username="||requestR.username) let sRequestBody = sRequestBody.append("&password="||requestR.password) let webRequest = com.HttpRequest.Create(sBaseURL) call webRequest.setMethod("POST") call webRequest.setHeader("Content-Type", "application/x-www-form-urlencoded") call webRequest.doTextRequest(sRequestBody) let webResponse = webRequest.getResponse() let p_webService.webStatus = webResponse.getStatusCode() if p_webService.webStatus = 200 then let p_webService.response = webResponse.getTextResponse() let outputChannel = base.channel.create() call outputChannel.openFile(sOutputFileName, "w") call outputChannel.writeLine(p_webService.response) call outputChannel.close() call util.JSON.parse(p_webService.response, tokenR) call sys_writeToDailyLog(__LINE__, __FILE__, 3, "Authorisation token received") else call sys_writeToDailyLog(__LINE__, __FILE__, 0, "Authorisation Token Error ("||p_webService.webStatus||") "||nvl(webResponse.getTextResponse(), "Unknown Error")) end if end function #+ #+ Function to read the token record #+ The current token is held in a file #+ private function eml_readToken() define sOutputDir, sOutputFileName, sToken string, tokenChannel base.channel let sOutputDir = sys_getControl("MS365", 9, null, null, "pardata") let sOutputFileName = sOutputDir||"o365.json" let tokenChannel = base.channel.create() try call tokenChannel.openFile(sOutputFileName, "r") let sToken = tokenChannel.readLine() call tokenChannel.close() call util.JSON.parse(sToken, tokenR) catch call sys_writeToDailyLog(__LINE__, __FILE__, 0, "Authorisation Token Error - unable to get token file") end try end function #+ #+ Function to send an email via a direct call, i.e. not by one of the background threads #+ #+ The public p_email record will have been populated #+ Two attempts are made to allow for connection issues - server busy etc. #+ public function eml_sendEmailDirectly() call sys_writeToDailyLog(__LINE__, __FILE__, 0, "Sending email directly: "||nvl(p_email.subject, "N/A")||" to "||nvl(p_email.recipient, "NULL")) if not eml_sendEmail() then call sys_writeToDailyLog(__LINE__, __FILE__, 0, "Sending email directly: "||nvl(p_email.subject, "N/A")||" to "||nvl(p_email.recipient, "NULL")||" failed - retrying") if not eml_sendEmail() then call sys_writeToDailyLog(__LINE__, __FILE__, 0, "Sending email directly: "||nvl(p_email.subject, "N/A")||" to "||nvl(p_email.recipient, "NULL")||" failed - aborting") end if end if end function #+ #+ Function to head up the sending of an email via the background thread or directly #+ The public variable p_email will be populated #+ function eml_sendEmail() define cntl record like teqcntl.*, l_ret boolean, tok base.StringTokenizer, idx integer, bDivertEmails boolean, sDivertEmailsTo string # Apply any diversion if required call sys_getControl("DVEML", null, null, null, "*") returning cntl.* let bDivertEmails = nvl(cntl.parnum, false) let sDivertEmailsTo = cntl.pardata if bDivertEmails and sDivertEmailsTo.getLength() then let p_email.recipient = sDivertEmailsTo let p_email.recipientcc = null let p_email.recipientbcc = null end if # Populate the recipients arrays with the various addresses call smtpToA.clear() call smtpCcA.clear() call smtpBccA.clear() call smtpAttachmentsA.clear() let tok = base.StringTokenizer.create(p_email.recipient, ";") while tok.hasMoreTokens() let smtpToA[smtpToA.getLength() + 1] = tok.nextToken() end while let tok = base.StringTokenizer.create(p_email.recipientcc, ";") while tok.hasMoreTokens() let smtpCcA[smtpCcA.getLength() + 1] = tok.nextToken() end while let tok = base.StringTokenizer.create(p_email.recipientbcc, ";") while tok.hasMoreTokens() let smtpBccA[smtpBccA.getLength() + 1] = tok.nextToken() end while # Get the attachments. These can be provided as a tokenised list from which an array is populated # The attacments need to be on the server and have an appropriate file type let tok = base.StringTokenizer.create(p_email.attachments, ";") while tok.hasMoreTokens() let smtpAttachmentsA[smtpAttachmentsA.getLength() + 1] = tok.nextToken() end while # Compose and send the email and if successful, remove the temporary files if eml_finaliseEmail() then for idx = 1 to smtpAttachmentsA.getLength() if os.Path.exists(smtpAttachmentsA[idx]) then let l_ret = os.Path.delete(smtpAttachmentsA[idx]) end if end for for idx = 1 to p_email.attachmentsA.getLength() if os.Path.exists(p_email.attachmentsA[idx].attachmentpath) then let l_ret = os.Path.delete(p_email.attachmentsA[idx].attachmentpath) end if end for if os.Path.exists(p_email.messagebody) then let l_ret = os.Path.delete(p_email.messagebody) end if return true else return false end if end function #+ #+ SMTP session initialisation #+ private function eml_initialiseSession() define smtp_props Properties, sProp string, mailConfiguration om.domNode define smtpHost string define smtpUser string define smtpUserPassword string define smtpDefaultFrom string define smtpDebug boolean define smtpStartTLS boolean # Get the config details let mailConfiguration = xml_openFile(fgl_getEnv("FJS_SMTPCONFIG")) if mailConfiguration is null then return false end if let smtpUser = xml_getValue("SmtpUser", mailConfiguration) let smtpUserPassword = xml_getValue("SmtpPass", mailConfiguration) let smtpDefaultFrom = xml_getValue("SmtpFrom", mailConfiguration) let smtpDebug = xml_getValue("SmtpDebug", mailConfiguration) let smtpProtocol = xml_getValue("SmtpProto", mailConfiguration) let smtpStartTLS = xml_getValue("SmtpTLS", mailConfiguration) # Set the send to the default if required if not p_email.sender.getLength() then let p_email.sender = smtpDefaultFrom let p_email.sendername = null end if # If the sender is the generic noreply@ user then send directly to Mimecast, else send via 365 # This allows us to stamp outbound email messages from real people with their signature as this is part of the M365 mail flow if p_email.sender = smtpDefaultFrom then let smtpHost = xml_getValue("SmtpHost", mailConfiguration) else let smtpHost = xml_getValue("Smtp365Host", mailConfiguration) end if let smtp_props = Properties.create() let sProp = "mail."||smtpProtocol||".host" call smtp_props.put(sProp, smtpHost) if smtpDebug then call smtp_props.put("mail.debug", "true") else call smtp_props.put("mail.debug", "false") end if let sProp = "mail."||smtpProtocol||".auth" call smtp_props.put(sProp, "false") if smtpStartTLS then let sProp = "mail."||smtpProtocol||".starttls.enable" call smtp_props.put(sProp, "true") end if try let smtpSession = Session.getInstance(smtp_props); catch call sys_writeToDailyLog( __LINE__, __FILE__, 0, "eml_initialiseSession: getInstance failed") return false end try call smtpSession.setDebug(smtpDebug) return true end function #+ #+ Compose and send mail via SMTP #+ #+ The public email record will be populated along with the recipient/attachment arrays #+ private function eml_finaliseEmail() type adrr array[] of javax.mail.Address define sBodyContent string, iNumberOfParts, idx integer, ch Base.Channel, sException, tmpStr, sContentType, msgFileName string, bGotBody, bGotAttachment, bSentOK boolean define sourceMessage javax.mail.internet.MimeMessage, msgFilePath string, inputStream java.io.FileInputStream, outboundMessageBodyPart javax.mail.internet.MimeBodyPart, sourceMultiPart javax.mail.internet.MimeMultipart, sourceBodyPart javax.mail.internet.MimeBodyPart, to_addresses, cc_addresses, bcc_addresses ia, addressA adrr, smtpTransport Transport if not eml_initialiseSession() then call sys_writeToDailyLog( __LINE__, __FILE__, 0, "eml_sendEmail: smtpSession is null") end if # Set the action to compose if not already set let p_email.emailAction = nvl(p_email.emailAction, emailActionCompose) # Check we can find what we need if composing if p_email.emailAction = emailActionCompose then let bGotBody = false let bGotAttachment = false if p_email.messagebody.getLength() then if os.Path.exists(p_email.messagebody) then let bGotBody = true end if end if if not bGotBody then if p_email.messageHTMLBody.getLength() then let bGotBody = true end if end if for idx = 1 to smtpAttachmentsA.getLength() if os.Path.exists(smtpAttachmentsA[idx]) then let bGotAttachment = true exit for end if end for if not bGotBody then for idx = 1 to p_email.attachmentsA.getLength() if os.Path.exists(p_email.attachmentsA[idx].attachmentpath) then let bGotAttachment = true exit for end if end for end if if not bGotBody and not bGotAttachment then call sys_writeToDailyLog( __LINE__, __FILE__, 0, "eml_sendEmail: Exception: Missing email body and no attachment") let bSentOK = false return bSentOK end if end if try let bSentOK = true # Get the reply or forward message (if required) and create a message object if p_email.emailAction != emailActionCompose then let msgFileName = sys_getDocumentPath(p_email.sourceMessageID) if not os.Path.exists(msgFileName) then call sys_writeToDailyLog(__LINE__, __FILE__, 0, "Message missing: "||nvl(msgFileName, "NULL")||" defaulting to compose") let p_email.emailAction = emailActionCompose else let msgFilePath = java.io.File.create(msgFileName) let inputStream = java.io.FileInputStream.create(msgFilePath) let sourceMessage = javax.mail.internet.MimeMessage.create(smtpSession, inputStream) end if end if # Create the outbound message object case p_email.emailAction when emailActionCompose let outboundMessage = MimeMessage.create(smtpSession) when emailActionReply let outboundMessage = cast(sourceMessage.reply(false) as javax.mail.internet.MimeMessage) when emailActionReplyAll let outboundMessage = cast(sourceMessage.reply(true) as javax.mail.internet.MimeMessage) when emailActionForward let outboundMessage = MimeMessage.create(smtpSession) when emailActionCopyIn let outboundMessage = MimeMessage.create(smtpSession) end case # Set any header options for idx = 1 to p_email.headerAttributes.getLength() call outboundMessage.setHeader(p_email.headerAttributes[idx].headerName, p_email.headerAttributes[idx].headerValue) end for # Create our multi part object let outboundMessageMultiPart = MimeMultipart.create() # Set the from, to, cc, bcc and subject if p_email.sendername.getLength() = 0 then call outboundMessage.setFrom(InternetAddress.create(p_email.sender)) else call outboundMessage.setFrom(InternetAddress.create(p_email.sender, MimeUtility.encodeText(p_email.sendername))) end if if smtpToA.getLength() then let to_addresses = ia.create(smtpToA.getLength()) for idx = 1 to smtpToA.getLength() let tmpStr = smtpToA[idx].trim() let to_addresses[idx] = InternetAddress.create(tmpStr) end for call outboundMessage.setRecipients(Message.RecipientType.TO, to_addresses) else let bSentOK = false return bSentOK end if if smtpCcA.getLength() then let cc_addresses = ia.create(smtpCcA.getLength()) for idx = 1 to smtpCcA.getLength() let tmpStr = smtpCcA[idx].trim() let cc_addresses[idx] = InternetAddress.create(tmpStr) end for call outboundMessage.setRecipients(Message.RecipientType.CC, cc_addresses) end if if smtpBccA.getLength() then let bcc_addresses = ia.create(smtpBccA.getLength()) for idx = 1 to smtpBccA.getLength() let tmpStr = smtpBccA[idx].trim() let bcc_addresses[idx] = InternetAddress.create(tmpStr) end for call outboundMessage.setRecipients(Message.RecipientType.BCC, bcc_addresses) end if call outboundMessage.setSubject(p_email.subject) call outboundMessage.setSentDate(java.util.Date.create()) # Set the body text. This can come from a body file and/or the the body text in p_email # If replying or forwarding, we also need to append the original meesage let sBodyContent = null if p_email.messagebody.getLength() then if os.Path.exists(p_email.messagebody) then let ch = base.Channel.create() call ch.openFile(p_email.messagebody, "r") while true let tmpStr = ch.readLine() if ch.isEof() then exit while end if let sBodyContent = sBodyContent.append(tmpStr) end while call ch.close() else call sys_writeToDailyLog( __LINE__, __FILE__, 0, "eml_sendEmail: Missing body "||p_email.messagebody) end if end if if p_email.messageHTMLBody.getLength() then let sBodyContent = sBodyContent.append(p_email.messageHTMLBody) end if # Append the original message if replying or forwarding to the body content # Add a delimiter first if p_email.emailAction != emailActionCompose then let sBodyContent = sBodyContent.append("