#+ #+ 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()||"

" let p_emailA[p_emailA.getLength() + 1].emailid = emlh.s_emlh let p_emailA[p_emailA.getLength()].emailfrom = sFrom let p_emailA[p_emailA.getLength()].emailsubject = emlh.subject let p_emailA[p_emailA.getLength()].emaildate = sEmailDisplayDate select count(*) into g_exist from teqemla where teqemla.s_emlh = emlh.s_emlh if nvl(g_exist, 0) then let p_emailA[p_emailA.getLength()].paperclip = "PaperClip_Default.png" end if end foreach call p_emailA.sort("emailid", true) if p_emailA.getLength() then let p_emailIndex = 1 call inDialog.setCurrentRow("emailr", p_emailIndex) call eml_displayMessage() end if end function #+ #+ Function to pretify the sent date #+ function eml_getDateFromUTC(sEmailDate string) define sMonth, sDay, sYear string, dDate date, sDate string let sDay = sEmailDate.subString(9,10) let sMonth = sEmailDate.subString(5,7) let sYear = sEmailDate.subString(25,28) case sMonth when "Jan" let sMonth = "01" when "Feb" let sMonth = "02" when "Mar" let sMonth = "03" when "Apr" let sMonth = "04" when "May" let sMonth = "05" when "Jun" let sMonth = "06" when "Jul" let sMonth = "07" when "Aug" let sMonth = "08" when "Sep" let sMonth = "09" when "Oct" let sMonth = "10" when "Nov" let sMonth = "11" when "Dec" let sMonth = "12" end case let sDate = sDay using "&&", "/", sMonth, "/", sYear try let dDate = sDate catch let dDate = null end try return dDate end function #+ #+ Function to create an email #+ function eml_createEmail(inParentClass like teqemlh.parentclass, inParentID like teqemlh.parentid, emailAction integer, messageID integer) define sEmailBody string, emailR record emailto, emailcc, emailsubject string end record, emailattchA dynamic array of record attachment, attachmentpath string, bFromForwardedMessage boolean end record, iThisAccount like tctfplm.s_fplm, sAttachmentDestination, selectedAttachment string, adx integer, bMessageOK boolean, sSubject, sFrom string, nameSeparator char(1) let nameSeparator = "<" open window createEmailW with form "tcfcnvb" let int_flag = false dialog attributes(unbuffered) input by name emailR.* after field emailto if emailR.emailto.getLength() then if not sys_isValidEmailAddress(emailR.emailto) then call sys_showMessage(%"Error", "Invalid email address", "") next field emailto end if end if after field emailcc if emailR.emailcc.getLength() then if not sys_isValidEmailAddress(emailR.emailcc) then call sys_showMessage(%"Error", "Invalid email address", "") next field emailcc end if end if on action zoom infield emailto if rcp_getRecipients(emailR.emailto, iThisAccount) then let emailR.emailto = rcp_getRecipientString() end if on action zoom infield emailcc if rcp_getRecipients(emailR.emailcc, iThisAccount) then let emailR.emailcc = rcp_getRecipientString() end if end input input by name sEmailBody attributes (without defaults) end input display array emailattchA to emailattchR.* on action del_attachment let adx = arr_curr() if adx then call emailattchA.deleteElement(adx) end if end display on action add_attachment call ui.Interface.frontCall("standard", "openfile", ["", %"All Files", "*.*", %"Attach File"], selectedAttachment) if selectedAttachment is not null then let emailattchA[emailattchA.getLength() + 1].attachment = os.Path.baseName(selectedAttachment) let emailattchA[emailattchA.getLength()].attachmentpath = selectedAttachment let emailattchA[emailattchA.getLength()].bFromForwardedMessage = false end if on action accept if not sys_isValidEmailAddress(emailR.emailto) then call sys_showMessage(%"Error", "Invalid recipient email address", "") next field emailto end if if emailR.emailcc.getLength() then if not sys_isValidEmailAddress(emailR.emailcc) then call sys_showMessage(%"Error", "Invalid CC email address", "") next field emailcc end if end if if not emailR.emailsubject.getLength() then call sys_showMessage(%"Error", "Please enter a subject", "") next field emailsubject end if if emailAction = emailActionCompose and not sEmailBody.getLength() and not emailattchA.getLength() then call sys_showMessage(%"Error", "The message appears to be empty", "") continue dialog end if let int_flag = false exit dialog on action close let int_flag = true exit dialog before dialog call emailattchA.clear() case inParentClass when emailClassPerson select email into emailR.emailto from tctfper where tctfper.s_fper = inParentID let p_email.saveEmail = false when emailClassActivity if emailAction != emailActionCompose then call eml_getEmailHeader(messageID) returning emlh.* if emlh.s_emlh is null then call sys_showMessage(%"Warning", "The message being forwarded or replied to cannot be found", "") let emailAction = emailActionCompose end if end if if emailAction = emailActionCompose then select tctfact.s_fplm, tctfper.email into iThisAccount, emailR.emailto from tctfact inner join tctfper on (tctfper.s_fper = tctfact.s_fper) where tctfact.s_fact = inParentID else let sSubject = emlh.subject let sFrom = emlh.emailfrom let emailR.emailsubject = emlh.subject let adx = sFrom.getIndexOf(nameSeparator, 1) if adx then let sFrom = sFrom.subString(adx+1,sFrom.getLength()-1) end if let emailR.emailto = sFrom if emailAction = emailActionForward then if sSubject.subString(1,3).toUpperCase() != "FW:" then let emailR.emailsubject = "FW: "||emailR.emailsubject end if let emailattchA = eml_loadForwardMessageAttachments(messageID) else if sSubject.subString(1,3).toUpperCase() != "RE:" then let emailR.emailsubject = "RE: "||emailR.emailsubject end if end if end if let p_email.saveEmail = true end case if emailR.emailto is null then next field emailto else if emailR.emailsubject is null then next field emailsubject else next field sEmailBody end if end if end dialog if int_flag then close window createEmailW return false end if let bMessageOK = true let p_email.sender = g_user.useremail let p_email.sendername = usr_getUserName(g_user.userid, 0) let p_email.recipient = emailR.emailto let p_email.recipientcc = emailR.emailcc let p_email.recipientbcc = null let p_email.subject = emailR.emailsubject let p_email.messagebody = null let p_email.attachments = null let p_email.messageHTMLBody = null let p_email.saveEmail = true let p_email.emailAction = emailAction let p_email.sourceMessageID = 0 call p_email.attachmentsA.clear() call p_email.headerAttributes.clear() if sEmailBody.getLength() then let p_email.messageHTMLBody = ""||sEmailBody.trim()||"" end if case emailAction when emailActionReply let p_email.sourceMessageID = nvl(emlh.documentid, 0) when emailActionReplyAll let p_email.sourceMessageID = nvl(emlh.documentid, 0) when emailActionForward let p_email.sourceMessageID = nvl(emlh.documentid, 0) otherwise let p_email.sourceMessageID = 0 end case if emailattchA.getLength() then whenever error continue for adx = 1 to emailattchA.getLength() if not emailattchA[adx].bFromForwardedMessage then let sAttachmentDestination = g_tmpServerDir.trim(), sys_getRandomInteger() using "<<<<<<<<&" call fgl_getfile(emailattchA[adx].attachmentpath, sAttachmentDestination) if status then call sys_showMessage(%"Error", %"Unable to attach file: "||emailattchA[adx].attachment, "") let bMessageOK = false exit for end if let p_email.attachmentsA[p_email.attachmentsA.getLength() + 1].attachmentpath = sAttachmentDestination let p_email.attachmentsA[p_email.attachmentsA.getLength()].attachmentname = emailattchA[adx].attachment let p_email.attachmentsA[p_email.attachmentsA.getLength()].bFromForwardedMessage = false else let p_email.attachmentsA[p_email.attachmentsA.getLength() + 1].attachmentpath = null let p_email.attachmentsA[p_email.attachmentsA.getLength()].attachmentname = emailattchA[adx].attachment let p_email.attachmentsA[p_email.attachmentsA.getLength()].bFromForwardedMessage = true end if end for whenever error call sys_errorHandler end if case inParentClass when emailClassActivity let p_email.headerAttributes[1].headerName = "X-ActivityLogged" let p_email.headerAttributes[1].headerValue = "true" end case if bMessageOK then call sys_notifyUser(%"Sending email...") if p_email.emailAction = emailActionCompose then case inParentClass when emailClassActivity let p_email.subject = eml_setSubjectWithCaseID(p_email.subject, inParentID) end case end if if eml_sendEmail() then call sys_notifyUser(null) close window createEmailW return true else call sys_notifyUser(null) call sys_showMessage(%"Error", %"Unable to send email", "") close window createEmailW return false end if else close window createEmailW return false end if end function #+ #+ Load the attachments in an array for the selected message #+ function eml_loadForwardMessageAttachments(inMessageID integer) define emailattchA dynamic array of record attachment, attachmentpath string, bFromForwardedMessage boolean end record declare aFwdAttC cursor for select * from teqemla where teqemla.s_emlh = inMessageID call emailattchA.clear() foreach aFwdAttC into emla.* let emailattchA[emailattchA.getLength() + 1].attachment = emla.attachment let emailattchA[emailattchA.getLength()].bFromForwardedMessage = true end foreach return emailattchA end function #+ #+ Create an email record after an email has been dropped #+ #+ @param iParentid Integer: Parent document relates to #+ @param sFilepath String: Document path #+ #+ @return boolean #+ function eml_createEmailFromDroppedDocument(iParentid, sFilepath) define props Properties define mailSession Session define currentMessage javax.mail.internet.MimeMessage define msgMessage com.aspose.email.MailMessage define msgLicense com.aspose.email.License define inputStream java.io.FileInputStream define licnseStream java.io.FileInputStream # define msgAddressTo string # define msgAddressToA ia define fileInfo com.aspose.email.FileFormatInfo define msgFilePath, licFilePath string, iParentid like teqemlh.parentid, sFilepath, sSubject string, bOK, bDeleted boolean if not os.Path.exists(sFilepath) then call sys_writeToDailyLog(__LINE__, __FILE__, 0, "Message missing: "||nvl(sFilepath, "NULL")) return false end if # Ascertain the file type - if msg we need to convert, if eml we don't. Anything else them error let fileInfo = com.aspose.email.FileFormatUtil.detectFileFormat(sFilepath) case fileInfo.getFileFormatType() when com.aspose.email.FileFormatType.Msg call sys_writeToDailyLog(__LINE__, __FILE__, 0, "Converting msg message: "||nvl(sFilepath, "NULL")) try let licFilePath = "/opt3/teq/jar/Aspose.Email.Java.lic" let licnseStream = java.io.FileInputStream.create(licFilePath) let msgLicense = com.aspose.email.License.create() call msgLicense.setLicense(licnseStream) catch call sys_writeToDailyLog(__LINE__, __FILE__, 0, "License failed: "||nvl(err_get(STATUS), "NULL")) end try let msgFilePath = sFilepath.trim().append(".eml") let msgMessage = com.aspose.email.MailMessage.load(sFilepath) call msgMessage.save(msgFilePath, com.aspose.email.SaveOptions.getDefaultEml()) when com.aspose.email.FileFormatType.Eml call sys_writeToDailyLog(__LINE__, __FILE__, 0, "Converting eml message: "||nvl(sFilepath, "NULL")) let msgFilePath = sFilepath.trim() otherwise call sys_writeToDailyLog(__LINE__, __FILE__, 0, "Invalid message type: "||nvl(sFilepath, "NULL")||" "||nvl(fileInfo.getFileFormatType(), "NULL")) return false end case # Get the message file let props = System.getProperties() let mailSession = javax.mail.Session.getInstance(props) let msgFilePath = java.io.File.create(msgFilePath) let inputStream = java.io.FileInputStream.create(msgFilePath) let currentMessage = javax.mail.internet.MimeMessage.create(mailSession, inputStream) let sSubject = currentMessage.getSubject() let sSubject = eml_setSubjectWithCaseID(sSubject, iParentID) call currentMessage.setSubject(sSubject) # Set the recipient to be customer service { let msgAddressTo = sys_getControl("MS365", 8, null, null, "pardata") let msgAddressToA = ia.create(1) let msgAddressToA[1] = InternetAddress.create(msgAddressTo) call currentMessage.setRecipients(Message.RecipientType.TO, msgAddressToA) } if eml_processMessage(currentMessage, false) then call evt_addAction(eventSubjectActivity, eventTypeDocument, "Added email: "||sSubject, null, iParentid, null, false) let bOK = true else let bOK = false end if let bDeleted = os.Path.delete(sFilepath) if os.Path.exists(msgFilePath) then let bDeleted = os.Path.delete(msgFilePath) end if return bOK end function #+ #+ Function to prepend the subject with the case number #+ function eml_setSubjectWithCaseID(sSubject string, inParentID integer) define sSubjectPrepend, sSubjectSubstitute string let sSubjectPrepend = sys_getControl("CCANC", 3, null, null, "pardata") let sSubjectSubstitute = sys_getControl("CCANC", 3, null, null, "parref") let sSubjectPrepend = sys_stringReplace(sSubjectPrepend.trim(), sSubjectSubstitute.trim(), inParentID) let sSubject = sSubjectPrepend, " ", nvl(sSubject, " ") return sSubject end function #+ #+ Function to prepend the subject with the case number #+ function eml_removeCaseID(sSubject string, inParentID integer) define sSubjectPrepend, sSubjectSubstitute string let sSubjectPrepend = sys_getControl("CCANC", 3, null, null, "pardata") let sSubjectSubstitute = sys_getControl("CCANC", 3, null, null, "parref") let sSubjectPrepend = sys_stringReplace(sSubjectPrepend.trim(), sSubjectSubstitute.trim(), inParentID) let sSubject = sys_stringReplace(sSubject, sSubjectPrepend, "") return sSubject end function #+ #+ Function to retrieve the email messages for customer service #+ It gets the meta description and checks that the subject contains a valid case number #+ Subject line needs to include a signature, for example ##[Case:XXXXX]## #+ #+ Each message is stored as a .eml file and a record is held created in teqemlh #+ public function eml_processInBox() type t_msg array[] of javax.mail.Message define props Properties define imapSession Session define imapStore javax.mail.Store define inboxFolder javax.mail.Folder define currentMessage javax.mail.Message define msgA t_msg define idx integer define sUserID string define sImapURL string define sImapPort string define iImapPort integer define sException string call sys_writeToDailyLog(__LINE__, __FILE__, 3, "Processing InBox") let sImapURL = sys_getControl("MS365", 7, null, null, "pardata") let sImapPort = sys_getControl("MS365", 7, null, null, "parref") let sUserID = sys_getControl("MS365", 8, null, null, "pardata") let iImapPort = sImapPort let props = System.getProperties() call props.setProperty("mail.imaps.ssl.enable", "true") call props.setProperty("mail.imaps.sasl.enable", "true") call props.setProperty("mail.imaps.auth.login.disable", "true") call props.setProperty("mail.imaps.auth.plain.disable", "true") call props.setProperty("mail.imaps.auth.mechanisms", "XOAUTH2") call props.setProperty("mail.imaps.usesocketchannels", "true") call props.setProperty("mail.event.scope", "session") call props.setProperty("mail.imaps.starttls.enable", "true") call props.setProperty("mail.imaps.sasl.mechanisms", "XOAUTH2") call props.setProperty("mail.imaps.socketFactory.fallback", "false"); call props.setProperty("mail.imaps.port", sImapPort); call props.setProperty("mail.imaps.socketFactory.port", sImapPort); call props.put("mail.imaps.host", sImapURL); let imapSession = javax.mail.Session.getInstance(props) let imapStore = imapSession.getStore("imaps") for idx = 1 to 2 case idx when 1 call eml_readToken() when 2 call eml_getOffice365Token() end case try call imapStore.connect(sImapURL, iImapPort, sUserID, tokenR.access_token) if imapStore.isConnected() then exit for end if catch let sException = err_get(STATUS) call sys_writeToDailyLog( __LINE__, __FILE__, 0, "eml_processInBox: Connection attempt "||idx||" failed: "||nvl(sException, "Undefined")) end try end for if not imapStore.isConnected()then return end if call sys_writeToDailyLog(__LINE__, __FILE__, 3, "Connected to InBox at attempt "||idx) let inboxFolder = imapStore.getFolder("INBOX") call inboxFolder.open(javax.mail.Folder.READ_WRITE) # Get the messages in the inbox and loop through them let msgA = inboxFolder.getMessages() for idx = 1 to msgA.getLength() let currentMessage = msgA[idx] if eml_processMessage(currentMessage, false) then call sys_writeToDailyLog(__LINE__, __FILE__, 0, "Message processed: "||nvl(currentMessage.getSubject(), "No subject")) else call sys_writeToDailyLog(__LINE__, __FILE__, 0, "Message failed: "||nvl(currentMessage.getSubject(), "No subject")) end if call currentMessage.setFlag(Flags.Flag.DELETED, true) end for # Close and expunge the folder and then close the store call inboxFolder.close(true) call imapStore.close() call sys_writeToDailyLog(__LINE__, __FILE__, 3, "Processed: "||msgA.getLength()||" message(s)") end function #+ #+ Function to create a message entry from the passed email #+ public function eml_processMessage(currentMessage javax.mail.Message, bCopyInActivityOwner boolean) type ia array[] of javax.mail.Address define messageMultiPart javax.mail.Multipart define messageBodyPart javax.mail.internet.MimeBodyPart define fromAddressA ia define pdx integer define sFromAddress string define sSubject string define sSentDate string define sOwnerEmail string define iCaseNumber integer define iNumberOfParts integer define sContentType string define fileStream java.io.FileOutputStream define lclFilePath string # Get the message meta data let fromAddressA = currentMessage.getFrom() let sFromAddress = fromAddressA[1].toString() let sSubject = nvl(currentMessage.getSubject(), "No Subject") let sSentDate = currentMessage.getSentDate().toString() call sys_writeToDailyLog(__LINE__, __FILE__, 3, "Processing: "||nvl(sSubject, "NULL")) # Extract from the subject the case or activity ID let iCaseNumber = eml_getCaseFromSubject(sSubject) if not iCaseNumber then call sys_writeToDailyLog(__LINE__, __FILE__, 3, "Invalid messsage") return false end if # Get the list of attachments so they can be recorded as part of the meta data call attachmentsA.clear() let sContentType = currentMessage.getContentType() call sys_writeToDailyLog(__LINE__, __FILE__, 3, "Content Type "||nvl(sContentType, "NULL")) if sContentType.getIndexOf("multipart", 1) then call sys_writeToDailyLog(__LINE__, __FILE__, 3, "Getting attachment list") 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) call eml_getAttachmentList(messageBodyPart) end for end if call sys_writeToDailyLog(__LINE__, __FILE__, 3, "Number of attachments: "||nvl(attachmentsA.getLength(), "NULL")) # Create the new conversation node that is attached to this activity # Record the attachments and save the whole email initialize emlh.* to null let emlh.s_emlh = sys_getAutoNextID("teqemlh") let emlh.parentclass = tcm_doc.documentClassActivity let emlh.parentid = iCaseNumber let emlh.documentid = sys_getDocumentID() let emlh.subject = sSubject let emlh.emailfrom = sFromAddress let emlh.emaildate = sSentDate insert into teqemlh values emlh.* for pdx = 1 to attachmentsA.getLength() call sys_writeToDailyLog(__LINE__, __FILE__, 3, "Got attachment: "||pdx||" "||nvl(attachmentsA[pdx].sFileName, "NULL")) if attachmentsA[pdx].sFileName.getLength() then let emla.s_emla = sys_getAutoNextID("teqemla") let emla.s_emlh = emlh.s_emlh let emla.attachment = attachmentsA[pdx].sFileName insert into teqemla values emla.* end if end for # Save the actual email for later viewing let lclFilePath = sys_getDocumentPath(emlh.documentid) let fileStream = java.io.FileOutputStream.create(File.create(lclFilePath)) call currentMessage.writeTo(fileStream) call fileStream.close() # Copy the message to the person who owns the activity if bCopyInActivityOwner then select tctfact.subject, tequser.useremail into sSubject, sOwnerEmail from tctfact inner join tequser on (tequser.userid = tctfact.owner) where tctfact.s_fact = iCaseNumber if sOwnerEmail.getLength() then let p_email.sender = sys_getControl("MS365", 8, null, null, "pardata") let p_email.recipient = sOwnerEmail let p_email.recipientcc = null let p_email.recipientbcc = null let p_email.subject = "New Reply for Case "||iCaseNumber let p_email.messagebody = null let p_email.attachments = null let p_email.messageHTMLBody = "

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("
") let addressA = sourceMessage.getFrom() if addressA is not null then let sBodyContent = sBodyContent.append("From: "||addressA[1].toString()||"
") end if let sBodyContent = sBodyContent.append("Sent: "||sourceMessage.getSentDate()||"
") let addressA = sourceMessage.getRecipients(Message.RecipientType.TO) if addressA is not null then let sBodyContent = sBodyContent.append("To: "||addressA[1].toString()||"
") end if let sBodyContent = sBodyContent.append("Subject: "||sourceMessage.getSubject()||"
") let sBodyContent = sBodyContent.append("
") let sBodyContent = sBodyContent.append(eml_getBodyContent(sourceMessage)) end if # Create the body text message part if sBodyContent.getLength() then let outboundMessageBodyPart = MimeBodyPart.create() call outboundMessageBodyPart.setContent(sBodyContent, "text/html;charset=UTF-8") call outboundMessageMultiPart.addBodyPart(outboundMessageBodyPart) end if # If we are replying or forwarding, then we need to add as body parts any inline content or attachments from the original message if p_email.emailAction != emailActionCompose then let sContentType = sourceMessage.getContentType() if sContentType.getIndexOf("multipart", 1) then let sourceMultiPart = cast(sourceMessage.getContent() as javax.mail.internet.MimeMultipart) let iNumberOfParts = sourceMultiPart.getCount() for idx = 0 to iNumberOfParts - 1 let sourceBodyPart = cast(sourceMultiPart.getBodyPart(idx) as javax.mail.internet.MimeBodyPart) if p_email.emailAction = emailActionForward then call eml_getSourceMessageContent(sourceBodyPart, false) else call eml_getSourceMessageContent(sourceBodyPart, false) end if end for end if end if # Now add the attachments for idx = 1 to smtpAttachmentsA.getLength() if os.Path.exists(smtpAttachmentsA[idx]) then let outboundMessageBodyPart = MimeBodyPart.create() call outboundMessageBodyPart.attachFile(smtpAttachmentsA[idx]) call outboundMessageMultiPart.addBodyPart(outboundMessageBodyPart) else call sys_writeToDailyLog( __LINE__, __FILE__, 0, "eml_sendEmail: Missing attachment "||smtpAttachmentsA[idx]) end if end for for idx = 1 to p_email.attachmentsA.getLength() if not p_email.attachmentsA[idx].bFromForwardedMessage then if os.Path.exists(p_email.attachmentsA[idx].attachmentpath) then let outboundMessageBodyPart = MimeBodyPart.create() call outboundMessageBodyPart.attachFile(p_email.attachmentsA[idx].attachmentpath) call outboundMessageBodyPart.setFileName(p_email.attachmentsA[idx].attachmentname) call outboundMessageMultiPart.addBodyPart(outboundMessageBodyPart) else call sys_writeToDailyLog( __LINE__, __FILE__, 0, "eml_sendEmail: Missing attachment "||smtpAttachmentsA[idx]) end if end if end for call outboundMessage.setContent(outboundMessageMultiPart) call outboundMessage.saveChanges() let smtpTransport = smtpSession.getTransport(smtpProtocol) call smtpTransport.connect() call smtpTransport.sendMessage(outboundMessage, outboundMessage.getAllRecipients()) call smtpTransport.close() # If this is case/activity related then we need to record a copy againt the case/activity if nvl(p_email.saveEmail, false) then if eml_processMessage(outboundMessage, false) then call sys_writeToDailyLog(__LINE__, __FILE__, 0, "Message saved: "||nvl(outboundMessage.getSubject(), "No subject")) else call sys_writeToDailyLog(__LINE__, __FILE__, 0, "Message failed to save: "||nvl(outboundMessage.getSubject(), "No subject")) end if end if catch let bSentOK = false let sException = err_get(STATUS) call sys_writeToDailyLog( __LINE__, __FILE__, 0, "eml_sendEmail: An exception occured: "||nvl(sException, "Undefined")) call sys_writeToDailyLog( __LINE__, __FILE__, 0, "eml_sendEmail: Sender: "||nvl(p_email.sender, "NULL")) call sys_writeToDailyLog( __LINE__, __FILE__, 0, "eml_sendEmail: Sendername: "||nvl(p_email.sendername, "NULL")) call sys_writeToDailyLog( __LINE__, __FILE__, 0, "eml_sendEmail: Recipient: "||nvl(p_email.recipient, "NULL")) call sys_writeToDailyLog( __LINE__, __FILE__, 0, "eml_sendEmail: Recipientcc: "||nvl(p_email.recipientcc, "NULL")) call sys_writeToDailyLog( __LINE__, __FILE__, 0, "eml_sendEmail: Recipientbcc: "||nvl(p_email.recipientbcc, "NULL")) call sys_writeToDailyLog( __LINE__, __FILE__, 0, "eml_sendEmail: Attachments: "||nvl(p_email.attachments, "NULL")) call sys_writeToDailyLog( __LINE__, __FILE__, 0, "eml_sendEmail: Subject: "||nvl(p_email.subject, "NULL")) call sys_writeToDailyLog( __LINE__, __FILE__, 0, "eml_sendEmail: Messagebody: "||nvl(p_email.messagebody, "NULL")) call sys_writeToDailyLog( __LINE__, __FILE__, 0, "eml_sendEmail: Attachments: "||p_email.attachmentsA.getLength()) call sys_writeToDailyLog( __LINE__, __FILE__, 0, "eml_sendEmail: HeaderAttributes: "||p_email.headerAttributes.getLength()) call sys_writeToDailyLog( __LINE__, __FILE__, 0, "eml_sendEmail: MessageHTMLBody: "||p_email.messageHTMLBody.getLength()) call sys_writeToDailyLog( __LINE__, __FILE__, 0, "eml_sendEmail: SaveEmail: "||nvl(p_email.saveEmail, "NULL")) call sys_writeToDailyLog( __LINE__, __FILE__, 0, "eml_sendEmail: EmailAction: "||nvl(p_email.emailAction, "NULL")) call sys_writeToDailyLog( __LINE__, __FILE__, 0, "eml_sendEmail: SourceMessageID: "||nvl(p_email.sourceMessageID, "NULL")) end try return bSentOK end function