import("system.logging"); import("system.db"); import("system.mail"); import("system.text"); import("system.vars"); import("system.util"); import("system.tools"); import("system.question"); import("system.datetime"); import("system.translate"); import("Sql_lib"); import("Util_lib"); import("Contact_lib"); import("Keyword_lib"); import("Employee_lib"); import("EmailUtil_lib"); import("ActivityTask_lib"); import("KeywordRegistry_basic"); /** * Class to handle incoming emails. * * @param {Object} pMail <p> * The mail object. * @return {IncomingEmailExecutor} <p> * Returns an IncomingEmailExecutor object. */ function IncomingEmailExecutor(pMail) { //whenver this function is called it may not be in a context where a alias is given: the mail-importing-entity for example has no alias. //therefore set it here manually this._alias = null; this.locale = null;//this is maybe set later when determining an affected user for history this.setAlias(); this.rawMail = pMail; this.mailSubject = this.rawMail[mail.MAIL_SUBJECT] || ""; this.mailSender = this.rawMail[mail.MAIL_SENDER]; this.mailSentDate = this.rawMail[mail.MAIL_SENTDATE]; this.filename = this.rawMail.filename; var mailRecipientsTo = this.rawMail[mail.RECIPIENT_TO].split(";"); var mailRecipientsCc = this.rawMail[mail.RECIPIENT_CC].split(";"); var mailRecipientsBcc = this.rawMail[mail.RECIPIENT_BCC].split(";"); this.mailRecipients = mailRecipientsTo.concat(mailRecipientsCc, mailRecipientsBcc).filter(Utils.isNotNullOrEmptyString); this._senderInfo = null; //activity data and failbackActivityData will be merged later to get all the data we need //we always want to prefer contacts that are active to those who are inactive (that applies to the sender and to the recipients) this.activityData = { links: [] }; this.failbackActivityData = { employeeContactId: "", direction: $KeywordRegistry.activityDirection$incoming() }; this._emailProcessors = []; } IncomingEmailExecutor.prototype.attachProcessor = function (pEmailProcessor) { this._emailProcessors.push(pEmailProcessor); } /** * Sets the contact which will later used as activity responsible. * * @param {String} pContactId <p> * The contac id. * @param {String} pLanguageIso3 <p> * The contac id. * @param {Boolean} pSetAsFailback <p> * In case its true, then . * @return {Void} <p> */ IncomingEmailExecutor.prototype.setActivityEmployeeContact = function(pContactId, pLanguageIso3, pSetAsFailback) { //autodetect language if not given if (pLanguageIso3 == undefined) { var lang = newSelect("CONTACT.ISOLANGUAGE", this._alias) .from("CONTACT") .where("CONTACT.CONTACTID", pContactId) .cell(); if (lang) { pLanguageIso3 = lang; } } if (pSetAsFailback) { this.failbackActivityData.employeeContactId = pContactId; this.failbackActivityData.employeeContactLanguage = pLanguageIso3; } else { this.activityData.employeeContactId = pContactId; this.activityData.links.push(["Person", pContactId]); this.activityData.employeeContactLanguage = pLanguageIso3; } } /** * Sets the database alias. * * @param {String} pAlias <p> * The name of database alias (e.g.: Data_alias). * @return {Void} <p> */ IncomingEmailExecutor.prototype.setAlias = function(pAlias) { this._alias = pAlias || db.getCurrentAlias(); } /** * Returns the mail-text as html. * * @return {String} <p> */ IncomingEmailExecutor.prototype.getMailtextAsHtml = function() { var textInfos = [ translate.withArguments("Sender: %0", [this.rawMail[mail.MAIL_SENDER]], this.locale), translate.withArguments("Recipients: %0", [this.mailRecipients.join(", ")], this.locale) ]; var attachmentInfos = mail.getAttachmentInfos(this.rawMail); var attachmentCount = attachmentInfos.length; if (attachmentCount == 0) { textInfos.push(translate.text("no attachments", this.locale)); } else { if (attachmentCount == 1) textInfos.push(translate.withArguments("%0 attachment:", [attachmentCount], this.locale)); else textInfos.push(translate.withArguments("%0 attachments:", [attachmentCount], this.locale)); var attachmentHtml = ""; var fileName; for (var i = 0; i < attachmentCount; i++) { //don't use a <ul><li.......</ul> here since it does not look good in the client [fileName] = text.decodeMS(attachmentInfos[i]); attachmentHtml += "\n" + (i > 0 ? "<br/>" : "") + "● " + fileName; } textInfos.push(attachmentHtml); } textInfos = textInfos.map(function (el) { return "<p>" + el + "</p><br>"; }); //since the activity has always and only a HTML-content-field we need to ensure that there will be always a HTML-content if (this.rawMail[mail.MAIL_HTMLTEXT]) textInfos.push("<br/>\n" + this.rawMail[mail.MAIL_HTMLTEXT]); else textInfos.push("<br/>\n" + text.text2html(this.rawMail[mail.MAIL_TEXT], true)); var res = textInfos.join("\n"); return res; } /** * Returns the senders info. <br> * <u><i>(contact-id, status, person-id, iso-language)</i></u> * * @return {Array} <p> * Returns an object containing the senders info. */ IncomingEmailExecutor.prototype.getSenderInfo = function() { if (this._senderInfo == null) { this._senderInfo = this.mailSender ? IncomingEmailExecutor.getContactDataByEmail(this.mailSender, this._alias) : []; } return this._senderInfo; } /** * Inserts the mail as unlinked mail into AB_UNLINKEDMAIL. * * @return {Object} <p> * Returns an object containing the uuid (unlinkedMailId: unlinkedMailId). */ IncomingEmailExecutor.prototype.insertUnlinkedMail = function() { var unlinkedMailId = util.getNewUUID(); var cols = ["AB_UNLINKEDMAILID", "SUBJECT", "SENTDATE", "SENDER", "RECIPIENTS", "MAIL", "USER_NEW", "DATE_NEW"]; var vals = [unlinkedMailId, this.mailSubject, this.mailSentDate, this.mailSender, this.mailRecipients.join(", "), mail.toRFC(this.rawMail), vars.get("$sys.user"), datetime.date()]; db.insertData("AB_UNLINKEDMAIL", cols, null, vals, this._alias); return { unlinkedMailId: unlinkedMailId }; } /** * Returns whether the mail is linkable or not.<p> * <i><u>(a e-mail is always then linkable when the sender has an contact in the system)</u></i> * * @return {Boolean|Null} <p> * True in case it's unlinkable, otherwise false and in case an error occured * null will be returned. */ IncomingEmailExecutor.prototype.isUnlinkable = function() { var isUnlinkable; if (this.getSenderInfo()) { isUnlinkable = this.getSenderInfo().length == 0; } else { isUnlinkable = null; } return isUnlinkable; } /** * In case the given e-mail has an corresponding contact in the system, it will be returned * <u><i>otherwise</i></u> an empty array will be returned. * * @param {String} pMailAddress <p> * The mail address which will be used to look up. * @param {String} pAlias <p> * The alias which will be used for db interaction. * @return {Array|Null} <p> * In case the contact was found, the contact id, status, person id, isolang will be returned. * If not an empty array will be returned and in case an error occured null will be returned. */ IncomingEmailExecutor.getContactDataByEmail = function(pMailAddress, pAlias) { var mailAddress = null; try { mailAddress = EmailUtils.extractAddress(pMailAddress).toUpperCase(); mailAddress = mailAddress ? newSelect("CONTACT.CONTACTID, CONTACT.STATUS, CONTACT.PERSON_ID, CONTACT.ISOLANGUAGE", pAlias) .from("COMMUNICATION") .join("CONTACT", "COMMUNICATION.CONTACT_ID = CONTACT.CONTACTID") .where("COMMUNICATION.ADDR", mailAddress, "upper(#) = ?") .table() : []; } catch(pException) { logging.log(translate.text("An error occured during getting contact data."), logging.ERROR); logging.log(pException, logging.ERROR); } return mailAddress; } /** * Creates an corresponding activity, matching to the processed mail. * * @param {Array[]} [pAdditionalLinks] <p> * 2d-array which contains arrays which represents links.<br> * <u>(0: contextname, 1: contactId)</u> * @param {Boolean} pIsError <p> * This variable is used, when importing mails trough the * dashlet, for detecting whether an error is occured or not. * If so, in the end the a corresponding message will appear * in the webclient. * @return {Array} <p> * Array which contains the uuids of the newly created activities. */ IncomingEmailExecutor.prototype.createActivity = function(pAdditionalLinks, pIsError) { var recipient; try { var sendersMail = mail.extractAddress(this.mailSender); var isSenderInternalUser = tools.getUsersByAttribute(tools.EMAIL, [sendersMail]); } catch(pException) { logging.log(translate.text("An error occured during checking whether the sender is an internal user or not."), logging.ERROR); logging.log(pException, logging.ERROR); } var isRecipientInternalUser = this.mailRecipients.some(function(pRecipient){ var isRecipientInternal = false; try { if (!Utils.isNullOrEmpty(tools.getUsersByAttribute(tools.EMAIL, [mail.extractAddress(pRecipient)])) && mail.extractAddress(pRecipient).indexOf("mailbridge") == -1) { recipient = mail.extractAddress(pRecipient); isRecipientInternal = true; } } catch(pException) { logging.log(translate.text("An error occured during checking whether at least one of the recipients is an internal user or not."), logging.ERROR); logging.log(pException, logging.ERROR); } return isRecipientInternal; }); var senderContacts = { prefered: [], failback: [] }; if (Utils.toBoolean(vars.get("$sys.isserver"))) { this.getSenderInfo().forEach(this._getProcessingFunction(true, senderContacts), this); this.activityData.links = this.activityData.links.concat(senderContacts.prefered.length > 0 ? senderContacts.prefered : senderContacts.failback); } for (var i = 0, l = this.mailRecipients.length; i < l; i++) { try { var recipientsMail = mail.extractAddress(this.mailRecipients[i]); var isRecipientInternal = tools.getUsersByAttribute(tools.EMAIL, [recipientsMail]); } catch(pException) { logging.log(translate.text("An error occured during checking whether recipient (recipient: " + recipientsMail + ") is \n\ an internal user or not."), logging.ERROR); logging.log(pException, logging.ERROR); } if (Utils.isNullOrEmpty(isRecipientInternal) && recipientsMail.indexOf("mailbridge") == -1) { try { var recipientsInfo = IncomingEmailExecutor.getContactDataByEmail(this.mailRecipients[i], this._alias); } catch(pException) { logging.log(translate.text("An error occured during getting recipients contact data."), logging.ERROR); logging.log(pException, logging.ERROR); } var recipientContacts = { prefered: [], failback: [] }; recipientsInfo.forEach(this._getProcessingFunction(false, recipientContacts), this); this.activityData.links = this.activityData.links.concat(recipientContacts.prefered.length > 0 ? recipientContacts.prefered : recipientContacts.failback); } } var langIso3 = this.activityData.employeeContactLanguage || this.failbackActivityData.employeeContactLanguage; var langIso2; if (langIso3) { langIso2 = LanguageKeywordUtils.Iso2FromIso3(langIso3, this._alias); if (langIso2) { this.locale = langIso2; } } //collecting all the information and combine it for the creation var activityDataForInsert = { subject: this.mailSubject, entrydate: this.mailSentDate, content: this.getMailtextAsHtml(), categoryKeywordId: $KeywordRegistry.activityCategory$mail(), directionKeywordId: this.activityData.direction || this.failbackActivityData.direction }; if (vars.get("$sys.isclient") == "true") { activityDataForInsert.responsibleContactId = EmployeeUtils.getCurrentContactId(); } else { if (!Utils.isNullOrEmpty(isSenderInternalUser)) { activityDataForInsert.responsibleContactId = ContactUtils.getContactIdByEmail(sendersMail) || ""; } else if(isRecipientInternalUser) { activityDataForInsert.responsibleContactId = ContactUtils.getContactIdByEmail(recipient) || ""; } else { activityDataForInsert.responsibleContactId = ""; } } var activityLinks = this.activityData.links || this.failbackActivityData.links; if (pAdditionalLinks) { activityLinks = activityLinks.concat(pAdditionalLinks); } activityLinks = ArrayUtils.distinct2d(activityLinks);//TODO: better check before adding the elements into the array if it already exists there var activityDocs = null; try { activityDocs = [["Mail.eml", util.encodeBase64String(mail.toRFC(this.rawMail)), true]]; } catch(pException) { logging.log(translate.text("An error occured during getting recipients contact data."), logging.ERROR); logging.log(pException, logging.ERROR); } var activityRes = ActivityUtils.insertNewActivity(activityDataForInsert, activityLinks, activityDocs, this._alias, this.mailSentDate); return activityRes; } /** * Deletes the unlinked mail with the given uuid. * * @param {String} pUnlinkedMailId <p> * The uuid of the record which shall be deleted.<br> * @return {Void} <p> */ IncomingEmailExecutor.prototype.deleteUnlinkedMail = function (pUnlinkedMailId) { newWhereIfSet("AB_UNLINKEDMAIL.AB_UNLINKEDMAILID", pUnlinkedMailId, undefined, undefined, this._alias) .deleteData(true, "AB_UNLINKEDMAIL"); } /** * Process an incoming email (e.g.: from mailbridge, e-mail import dashlet..) * * @param {String} pUnlinkedMailId <p> * The uuid of the record which shall be deleted.<br> * @return {Void} <p> */ IncomingEmailExecutor.prototype.autoProcess = function(pUnlinkedMailId) { let tempResult = { isUnlinkedMail: false, isError: false }; if (this.isUnlinkable()) { tempResult.isUnlinkedMail = true; if (pUnlinkedMailId) { return { unlinkedMailId: pUnlinkedMailId }; } } else if(this.isUnlinkable() == null) { tempResult.isError = true; } if (!tempResult.isError) { tempResult.activityId = this.createActivity().activityId; this.deleteUnlinkedMail(pUnlinkedMailId); this._emailProcessors.forEach(function (emailProcessor) { emailProcessor.process(this.rawMail); }, this); } return tempResult; } /** * Returns an anonymous function which processes ? * * @param {Boolean} pIsSender <p> * Whether the contact is the sender or not.<br> * @param {Array} pTargetArray <p> * The uuid of the record which shall be deleted.<br> * @return {Void} <p> */ IncomingEmailExecutor.prototype._getProcessingFunction = function(pIsSender, pTargetArray) { return function(contactInfoRow){ var [contactId, contactStatus, contactPersonId, languageIso3] = contactInfoRow; //there *should* only exist no or one user per contactid, never two or more - so getUser (not getUsers) should be fine var user = tools.getUserByAttribute(tools.CONTACTID, [contactId]); var isEmployee = user != null; var isContactActive = contactStatus == $KeywordRegistry.contactStatus$active(); //if a user was already found we can skip determining the correct user since only one user can be set as "RESPONSIBLE" in the activity if (isEmployee && !this.activityData.employeeContactId) { var direction = pIsSender ? $KeywordRegistry.activityDirection$outgoing() : $KeywordRegistry.activityDirection$incoming(); if (isContactActive) { this.setActivityEmployeeContact(contactId, languageIso3); this.activityData.direction = direction; } else { //if the user is inactive, we may find a better (=active) user later this.setActivityEmployeeContact(contactId, languageIso3, true); this.failbackActivityData.direction = direction; } } else { var context = contactPersonId == "" ? "Organisation" : "Person" if(context == "Person")//add Organisation to the Activity as Link { var orgData = newSelect(["CONTACT.CONTACTID", "CONTACT.STATUS"], this._alias) .from("CONTACT") .join("CONTACT", "anyContact.ORGANISATION_ID = CONTACT.ORGANISATION_ID and CONTACT.PERSON_ID is null", "anyContact") .whereIfSet(["CONTACT", "CONTACTID", "anyContact"], contactId) .arrayRow() orgData[0] = orgData[0].replace(/\s/g, ''); if(orgData[0] != '0' && orgData[0] != '1') { if (orgData[1] == $KeywordRegistry.contactStatus$active()) { pTargetArray["prefered"].push(["Organisation", orgData[0]]); } else { pTargetArray["failback"].push(["Organisation", orgData[0]]); } } } var link = [context, contactId]; if (isContactActive) { pTargetArray["prefered"].push(link); } else { pTargetArray["failback"].push(link); } } }; }