Skip to content
Snippets Groups Projects
process.js 21.01 KiB
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/>" : "") + "&#x25CF; " + 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);
            }
        }
    };
}