/*
    $Revision$
    $Id$

    File:           exchange_graph.js

    Description:    Exchange MS Graph koppeling
                    Dit bestand maakt voor een ruimte-mailbox Arguments(0) de csv
                    met calenderitems aan sinds de laatste sync-actie, Arguments(1)

    Result:         Errorlevel 10 betekent alles goed
                    Errorlevel 1 betekent dat we een foutsituatie hebben ontdekt
                    Errorlevel 0 betekent dat er iets is fout gegaan wat we niet hebben onderkend

    Parameters      (0) e-mail adres van de zaal
                    (1) "EXCHFULL" calendarview-import (in de toekomst tot config.fullfuture)
                        alle andere waarden: synchronisatie-import (voorheen werd hier de syncstate in meegegeven)
*/

/* GLOBALS */
var fso = new ActiveXObject("Scripting.FileSystemObject");

/*////////////////////////////////
//                              //
//        Main functions        //
//                              //
////////////////////////////////*/

/*
    In      zaalemail           res_ruimte_extern_id
            import_app_code     EXCHFULL of EXCHANGE(default)
            as_stream           teruggeven als fileStream, of wegschrijven naar een .csv bestand
    Out     Pre-processed import in csv formaat
*/
function generateCalendarImport(zaalemail, import_app_code, as_stream)
{
    var deltatoken;

    // Determine files wild card
    var import_app_files = "sync_*.csv";
    var import_app_files_pre = "sync_";
    var sql = "SELECT i.fac_import_app_files "
            + "  FROM fac_import_app i"
            + " WHERE i.fac_import_app_code = " + safe.quoted_sql(String(import_app_code));
    var oRs = Oracle.Execute(sql);
    if (!oRs.EOF)
    {
        import_app_files = oRs("fac_import_app_files").Value;
        import_app_files_pre = import_app_files.split("*")[0].split(".")[0];
    }
    oRs.Close();

    __Log("\n\n==== Room: " + zaalemail);
    if (import_app_code != "EXCHFULL") {
        deltatoken = getSyncToken(zaalemail);
    } else {
        // No skiptoken and no deltatoken means retrieve all events
    }

    var csvname = config.xmlfolder + import_app_files_pre + safefilename(zaalemail) + ".csv";
    // Verwijderen: het verwijderen van onze oude sync_*.csv
    try {
        fso.DeleteFile(csvname);
        __Log("Oude syncfile is verwijderd.");
    } catch(e) {
        // Neem aan dat -gelukkig- geen files zijn gevonden
    }

    __Log("Connecting to MS Graph API");
    var response = getCalendarItems(zaalemail, null, deltatoken);
    if (response && response.value)
    {
        var results = response.value;
        while (response.skiptoken) {
            response = getCalendarItems(zaalemail, response.skiptoken, null);
            for (var index = 0; index < response.value.length; index++) {
                results.push(response.value[index]);
            }
        }
        if (!("deltatoken" in response)) {
            // Het laatste antwoord bevatte geen deltatoken, dat hoort er altijd te zijn, waarschijnlijk een MS Graph fout.
            return _handleMsGraphFailure(null, response);
        }

        // deltatoken needs to be saved (asap) to retrieve only changes during a future run
        upsertSyncToken(zaalemail, response.deltatoken);

        // De pre-processed data
        var csvFileText = graphToImport(results, zaalemail);

        // save the CSV file to disc here
        fileStream = new ActiveXObject("ADODB.Stream");
        fileStream.Type = 2; // adTypeBinary eerst nog
        fileStream.Open();
        fileStream.CharSet = "utf-8";
        // save only if records are found
        if (!!csvFileText) {
            fileStream.WriteText(csvFileText);
            if (as_stream) {
                // Leveren we die zo op en gaat die rechtstreeks impReadStream in
            } else {
                fileStream.SaveToFile(csvname, 2); // overwrite
            }
        }

        // Als we hier komen is alles goed
        return fileStream;
    }
}

/*////////////////////////////////
//                              //
//       Helper functions       //
//                              //
////////////////////////////////*/

/* De volgende 2 functies zoeken in de data (Array) die de calendarItems bevat */
function getSeriesMaster(data, seriesMasterId) {
    var result = {};
    for (var i = 0; i < data.length; i++) {
        if (data[i].id == seriesMasterId) {
            result = data[i];
            break;
        }
    }
    return result;
}

function getOccurrencesBySeriesMasterId(data, seriesMasterId) {
    var result = [];
    for (var i = 0; i < data.length; i++) {
        if (data[i].seriesMasterId == seriesMasterId) {
            result.push(data[i]);
        }
    }
    return result;
}

/* Deze functie zoekt in de Facilitor DB */
function getReserveringen(method, searchParam, zaalemail)
{
    // res_rsv_ruimte_externnr bestaat uit 2 delen, gescheiden door een '|'-teken;
    // 1. Het hoofd-id; COALESCE(seriesMasterId, (eigen) id)
    // 2. Als het event onderdeel is van een series, dan is het 2de deel het (eigen) id, anders leeg
    //
    // LET OP deze id's kunnen een '_' bevatten
    var externnr;
    var compareExternnrSql = "";
    if (method === "event") {
        var event = searchParam;
        switch (event.type) {
            case "occurrence":
            case "exception":
                compareExternnrSql = "rr.res_rsv_ruimte_externnr = " + safe.quoted_sql(event.seriesMasterId + "|" + event.id); // Exact
                break;
            case "singleInstance":
                compareExternnrSql = "rr.res_rsv_ruimte_externnr = " + safe.quoted_sql(event.id + "|"); // Exact
                break;
            case "seriesMaster":
                externnr = event.id.replace(/_/g, '\\_'); // Escape de underscore
                compareExternnrSql = "rr.res_rsv_ruimte_externnr LIKE " + safe.quoted_sql(externnr + '|_%') + " ESCAPE '\\'"; // LIKE (de laatste underscore niet escapen)
                break;
            default: /* Een 'deleted' notificatie heeft geen type, dan kan het een occurrence zijn of niet, test met het event.id op beiden */
                externnr = event.id.replace(/_/g, '\\_'); // Escape de underscore
                compareExternnrSql = "rr.res_rsv_ruimte_externnr LIKE " + safe.quoted_sql("%" + externnr + "%") + " ESCAPE '\\'"; // LIKE
                break;
        }
        if ("iCalUId" in event) { // Vanaf 2024.1 (met rich notifications) altijd zo
            compareExternnrSql = "(" + compareExternnrSql + " OR rr.res_rsv_ruimte_externnr = " + safe.quoted_sql("##iCalUId##" + event.iCalUId + "|") + ")";
        }
        compareExternnrSql = "rr.res_rsv_ruimte_externnr IS NOT NULL AND " + compareExternnrSql;
    } else if (method === "res_rsv_ruimte_key") {
        compareExternnrSql = "rr.res_rsv_ruimte_key = " + searchParam; // Exact
    }
    var eventSql = "SELECT rr.res_rsv_ruimte_key"
                 + "     , rr.res_rsv_ruimte_omschrijving"
                 + "     , rr.res_rsv_ruimte_van"
                 + "     , rr.res_rsv_ruimte_tot"
                 + "     , rr.res_rsv_ruimte_externnr"
                 + "     , rr.res_rsv_ruimte_externnr2"
                 + "     , p_host.prs_perslid_email"
                 + "  FROM res_rsv_ruimte rr,"
                 + "       res_ruimte_opstelling ro,"
                 + "       res_ruimte r,"
                 + "       prs_perslid p_host"
                 + " WHERE " + compareExternnrSql
                 + "   AND rr.res_rsv_ruimte_verwijder IS NULL"
                 + "   AND ro.res_ruimte_opstel_key = rr.res_ruimte_opstel_key"
                 + "   AND r.res_ruimte_key = ro.res_ruimte_key"
                 + "   AND r.res_ruimte_verwijder IS NULL"
                 + "   AND rr.res_rsv_ruimte_host_key = p_host.prs_perslid_key"
                 + "   AND UPPER(r.res_ruimte_extern_id) = " + safe.quoted_sql_upper(zaalemail);
    var eventoRs = Oracle.Execute(eventSql);
    var result = [];
    while (!eventoRs.eof) {
        result.push({
            "key":          eventoRs("res_rsv_ruimte_key").Value,
            "description":  eventoRs("res_rsv_ruimte_omschrijving").Value,
            "van":          eventoRs("res_rsv_ruimte_van").Value,
            "tot":          eventoRs("res_rsv_ruimte_tot").Value,
            "externnr":     eventoRs("res_rsv_ruimte_externnr").Value || "",
            "organisatorId":eventoRs("res_rsv_ruimte_externnr2").Value || "",
            "host_mail":    eventoRs("prs_perslid_email").Value || ""
        });
        if (method === "event" && result[result.length - 1].externnr.slice(0, 11) === "##iCalUId##") { // Werk het FCLT-record direct bij zodat de import hem straks ook kan vinden
            updateFcltRecord(result[result.length - 1].key, event, result[result.length - 1].organisatorId);
        }
        eventoRs.moveNext();
    }
    eventoRs.Close();
    return result;
}

/*
    In      Array van calendarItems
    Out     Pre-processed import in csv formaat
*/
function graphToImport(data, zaalemail)
{
    var trs = [];
    var tds = [];
    var index;
    var masterEvent;
    var curDate = new Date();
    var seqNbr = curDate.getTime();
    var millisPerDay = 24 * 60 * 60 * 1000;
    var maxPastMS = curDate.setHours(0, 0, 0, 0) - (config.fullpast * millisPerDay);
    var maxFutureMS = curDate.setHours(23, 59, 59, 999) + (config.fullfuture * millisPerDay);
    var SUBJECT_MAX_LENGTH = 60;

    var _roomFallbackEmail;
    var _unknownFallbackEmail;
    var lcl_res_rsv_private = "";
    if (data.length) {
        var sql = "SELECT prs_perslid_email FROM prs_v_aanwezigperslid WHERE prs_perslid_oslogin = " + safe.quoted_sql_upper("_MSGRAPH_FALLBACK_ROOMS");
        var oRs = Oracle.Execute(sql);
        if (!oRs.EoF) {
            _roomFallbackEmail = oRs("prs_perslid_email").Value;
        }
        oRs.Close();

        sql = "SELECT prs_perslid_email FROM prs_v_aanwezigperslid WHERE prs_perslid_oslogin = " + safe.quoted_sql_upper("_MSGRAPH_FALLBACK_UNKNOWN");
        oRs = Oracle.Execute(sql);
        if (!oRs.EoF) {
            _unknownFallbackEmail = oRs("prs_perslid_email").Value;
        }
        oRs.Close();

        sql = "SELECT lcl.l('lcl_res_rsv_private') lcl_res_rsv_private FROM DUAL";
        oRs = Oracle.Execute(sql);
        if (!oRs.EoF) {
            lcl_res_rsv_private = oRs("lcl_res_rsv_private").Value;
        }
        oRs.Close();
    }

    // Past de organizer.emailAddress.address aan van het meegegeven event indien nodig
    function validateOrganizer(event, occurrence) {
        // Als de organisator niet als prs_perslid in Facilitor bekend is, kennen we 2 fallback-accounts;
        // 1) De organisator is een ruimte; externe software boekt vaak direct in de agenda vd ruimte, zoals Yealink, maar ook Facilitor zelf (bij msgraph_sync_level & 2)
        // 2) De organisator is onbekend; technisch/funcioneel account, of gewoon onbekend in Facilitor
        // Als de relevante fallback-account niet (juist) is geconfigureerd, returnen we een falsy value en wordt de regel niet geimporteerd
        if ((event.organizer.emailAddress.address || "").slice(-2) === "##") {
            // Deze hebben we al aangepast
        } else if (event.isOrganizer) { // Ruimte = organizer (Yealink)
            event.organizer.emailAddress.address = _roomFallbackEmail;
        } else { // Try to find host by email
            var sql = "SELECT COUNT (*) aantal FROM prs_v_aanwezigperslid WHERE UPPER(prs_perslid_email) = " + safe.quoted_sql_upper(event.organizer.emailAddress.address);
            var oRs = Oracle.Execute(sql);
            if (oRs("aantal").Value !== 1) { // Unknown or non-unique organizer
                if (_unknownFallbackEmail) { // _unknownFallbackEmail is gedefinieerd en dus geconfigureerd
                    event.organizer.emailAddress.address = "##" + event.organizer.emailAddress.address + "##"; // Deze pakt de package op
                } else {
                    event.organizer.emailAddress.address = null;
                }
            }
            oRs.Close();
        }
        if (typeof occurrence != "undefined" && occurrence.type === "exception") { // Dan staat hier de(zelfde) organisator
            occurrence.organizer.emailAddress.address = event.organizer.emailAddress.address;
        }
        return event.organizer.emailAddress.address;
    }

    // Header
    var headerRow = [
        "Subject",
        "StartTime",
        "EndTime",
        "Organizer",
        "Email",
        "Name",
        "Modifier",
        "ApptId",
        "RecurId",
        "SeqNr"
    ].join(";");
    // rows in data
    /* types            description
       ===============  ===============================
       singleInstance:  stand alone event
       seriesMaster:    definition of a repeating event
       occurrence:      repeating event occurrence
       exception:       changed repeating occurrence
    */
    for (index = 0; index < data.length; index++)
    {
        var thisEvent = data[index];

        var responseStatus = null;
        var eventForResponseStatus = thisEvent;
        if (!("responseStatus" in thisEvent) && thisEvent.seriesMasterId) { // Exceptions hebben hun eigen responseStatus, occurrences niet (altijd)
            eventForResponseStatus = getSeriesMaster(data, thisEvent.seriesMasterId);
        }
        if ("responseStatus" in eventForResponseStatus) { // Niet elke notificatie heeft een responseStatus, sommigen zijn gewoonweg @removed
            responseStatus = eventForResponseStatus.responseStatus.response;
        }

        // Eerst wat uitzonderingen eruit filteren
        if ((getMSGraphSyncLevel() & 4) && responseStatus == "notResponded")
        {   /*
                Bij S("msgraph_sync_level") & 2 is dit "organizer"      [Deze is vanuit Facilitor,  direct op ruimte in Outlook geboekt]
                Bij Yealink ad-hoc reserveringen is dit ook "organizer" [Deze is vanuit Yealink,    direct op ruimte in Outlook geboekt]
                Bij S("msgraph_sync_level") & 4 is dit de eerste keer "notResponded", en direct daarna "accepted"
            */
            continue; // Hier doen we (nog) niets mee
        }

        var reserveringenFacilitor = getReserveringen("event", thisEvent, zaalemail) || []; // Dit levert meerdere records op bij type = seriesMaster
        if ((   (getMSGraphSyncLevel() & 2) == 2 && // Als wij zelf afspraken op naam van de ruimte boeken, willen we zulke events nooit importeren
                (   (reserveringenFacilitor.length > 0 && reserveringenFacilitor[0].organisatorId == thisEvent.id) || // Deze is bekend in Facilitor en de ruimte is de organisator
                    (reserveringenFacilitor.length === 0 && /* Ooit; !_roomFallbackEmail &&      // Als je de fallback-user gebruikt dan is dit supported, anders niet */
                                                            (thisEvent.isOrganizer ||   // Deze is niet bekend in Facilitor en de ruimte is de organisator
                                                            inArray(thisEvent.type, ["occurrence", "exception"]) &&
                                                            (getSeriesMaster(data, thisEvent.seriesMasterId) || {}).isOrganizer))))) { // idem, maar dan bij de seriesMaster
            // Deze slaan we over
            continue;
        }

        // Facilitor-initiated reserveringen beginnen met alleen het externnr2 gevuld, update voor zover mogelijk hier het externnr
        if (getMSGraphSyncLevel() & 4 && reserveringenFacilitor.length === 0) { // ReadWrite
            var updateResult = updateExternnr(zaalemail, thisEvent.id);
            if (updateResult == -1) { // De reservering is niet gevonden in Facilitor, en ook is de reservering niet geaccepteerd; dan hoeven we hier dus (nog) niets mee
                continue; // Errorcode; skip record (Ruimte heeft nog niet geaccepteerd)
            } else if (updateResult) {
                reserveringenFacilitor = getReserveringen("event", thisEvent, zaalemail) || []; // Probeer nog eens
            }
        }

        // [D]eleted events
        if (thisEvent["@removed"] || thisEvent.isCancelled || responseStatus == "declined")
        {
            if (thisEvent.type == "seriesMaster") {
                continue; // We krijgen per occurrence ook een event binnen en verwijderen die dan
            }

            if (
                   reserveringenFacilitor.length === 0  // Geen Facilitor reservering gevonden die deze notificatie matched
                && thisEvent.type == "singleInstance"
                && !thisEvent.isOrganizer       // Dit event heeft een persoon als organisator
                && responseStatus == "declined" // De ruimte heeft de reservering geweigerd
                && ("organizer" in thisEvent)
                && ("iCalUId" in thisEvent)     // Altijd true voor Rich notifications
            ) {
                // Blijkbaar is het ons hiervoor niet gelukt om de reservering in Facilitor te vinden, maar hij moet nu worden verwijderd, dus laten we extra ons best doen
                var organizerCalendarEvent = getCalendarEventByICalUId(thisEvent.organizer.emailAddress.address, thisEvent.iCalUId, false);
                if (organizerCalendarEvent !== null) {
                    var res_rsv_ruimte_key = getFcltKeyFromEvent(organizerCalendarEvent);
                    if (res_rsv_ruimte_key > -1) {
                        reserveringenFacilitor = getReserveringen("res_rsv_ruimte_key", res_rsv_ruimte_key, zaalemail);
                    }
                }
            }


            // Op dit moment kan het nog steeds een "@removed" seriesMaster zijn, dan moeten we direct de hele serie deleten want we krijgen geen losse notificaties van de occurrences
            for (var f in reserveringenFacilitor) {
                if (new Date(reserveringenFacilitor[f].tot).getTime() >= maxPastMS &&
                    new Date(reserveringenFacilitor[f].van).getTime() <= maxFutureMS) {
                    if (("type" in thisEvent) && thisEvent.type === "singleInstance") { // Sla dan het iCalUId op zodat we deze sneller kunnen terugvinden bij een evt. ruimte-wissel
                        updateFcltRecord(reserveringenFacilitor[f].key, thisEvent, null, { "iCalUId": true, "force": false });
                        var sql = "SELECT res_rsv_ruimte_externnr FROM res_rsv_ruimte WHERE res_rsv_ruimte_key = " + reserveringenFacilitor[f].key;
                        var oRs = Oracle.Execute(sql);
                        var newExternnr = oRs.EoF
                                            ? reserveringenFacilitor[f].externnr
                                            : oRs("res_rsv_ruimte_externnr").Value;
                        oRs.Close();
                        var eventId = newExternnr.split("|")[0];
                        var occurrenceId = newExternnr.split("|")[1] || "";
                        if (eventId.slice(0, 11) === "##iCalUId##") {
                            // Verwijder de id's van de oude verwijderde 'backups', die zijn niet meer nodig
                            var sql = "UPDATE res_rsv_ruimte"
                                    + "   SET res_rsv_ruimte_externnr = NULL"
                                    + " WHERE res_rsv_ruimte_externnr = " + safe.quoted_sql(eventId + "|")
                                    + "   AND res_rsv_ruimte_verwijder IS NOT NULL";
                            Oracle.Execute(sql);
                        }
                    } else {
                        var eventId = reserveringenFacilitor[f].externnr.split("|")[0];
                        var occurrenceId = reserveringenFacilitor[f].externnr.split("|")[1] || "";
                    }
                    tds = [
                        "",
                        "",
                        "",
                        "",
                        "",
                        "",
                        "",
                        "D", // [D]elete
                        safe.csv(eventId),
                        safe.csv(occurrenceId),
                        ++seqNbr
                    ];
                    trs.push(tds.join(";"));
                }
            }
        }
        else
        {
            var _startDate = parseISOString(thisEvent.start.dateTime);
            var _endDate = parseISOString(thisEvent.end.dateTime);
            if (_endDate.getTime() >= maxPastMS && _startDate.getTime() <= maxFutureMS)
            {
                {
                    var modifier = reserveringenFacilitor.length === 0 ? "C" : "U"; // [U]pdate of [C]reate
                    if (thisEvent.type == "singleInstance") // Simpel, single event
                    {
                        if (!validateOrganizer(thisEvent)) {
                            continue;
                        }
                        if (!eventReadyForProcessing(zaalemail, thisEvent)) {
                            continue;
                        }
                        var visibility = thisEvent.sensitivity === "normal" ? 1 : 0; // sensitivity IN ["normal", "personal", "private", "confidential"], we interpreteren die laatste 3 hetzelfde
                        tds = [
                            safe.csv(((visibility ? thisEvent.subject : lcl_res_rsv_private) || "").slice(0, SUBJECT_MAX_LENGTH)),
                            _startDate.toISOString(),
                            _endDate.toISOString(),
                            safe.csv(thisEvent.organizer.emailAddress.address),
                            "",
                            "",
                            safe.csv(visibility),
                            modifier,
                            safe.csv(thisEvent.id),
                            "",
                            ++seqNbr
                        ];
                    }
                    else if (inArray(thisEvent.type, ["occurrence", "exception"])) // Part of event-series (recurring)
                    {
                        masterEvent = getSeriesMaster(data, thisEvent.seriesMasterId) || {};
                        if (masterEvent.isCancelled) {
                            tds = [
                                "",
                                "",
                                "",
                                "",
                                "",
                                "",
                                "",
                                "D", // cancelled or deleted
                                safe.csv(thisEvent.seriesMasterId),
                                safe.csv(thisEvent.id),
                                ++seqNbr
                            ];
                            trs.push(tds.join(";"));
                            continue;
                        } else {
                            if (!validateOrganizer(masterEvent, thisEvent)) {
                                continue;
                            }
                            if (!eventReadyForProcessing(zaalemail, masterEvent)) {
                                continue;
                            }
                            var visibility = masterEvent.sensitivity === "normal" ? 1 : 0; // Gebruik de gegevens van de seriesMaster
                            tds = [
                                safe.csv(((visibility ? masterEvent.subject : lcl_res_rsv_private) || "").slice(0, SUBJECT_MAX_LENGTH)),
                                _startDate.toISOString(),
                                _endDate.toISOString(),
                                safe.csv(masterEvent.organizer.emailAddress.address),
                                "",
                                "",
                                safe.csv(visibility),
                                modifier,
                                safe.csv(thisEvent.seriesMasterId),
                                safe.csv(thisEvent.id),
                                ++seqNbr
                            ];
                        }
                    }
                    else if (thisEvent.type === "seriesMaster")
                    {   // Voer een (import) delete uit op alle occurrences van de seriesMaster die in Facilitor voorkomen maar niet in MS Graph
                        // Creates en Updates gaan vanzelf goed omdat dan de occurrence in de calendarView-response (=data) is meegegeven
                        // Bij deletes is dat niet het geval
                        // Deze calendarView/delta omvat de gehele serie, dus als een occurrence hier niet in staat, bestaat die niet (meer)
                        var reserveringenOutlook = getOccurrencesBySeriesMasterId(data, thisEvent.id);
                        for (var f in reserveringenFacilitor) {
                            var found = false;
                            for (var o in reserveringenOutlook) {
                                if (reserveringenOutlook[o].seriesMasterId === reserveringenFacilitor[f].externnr.split("|")[0] &&  // Match op seriesMasterId
                                    reserveringenOutlook[o].id === reserveringenFacilitor[f].externnr.split("|")[1]) {              // en op het id vd occurrence
                                    found = true;
                                    break;
                                }
                            }
                            if (!found) {
                                tds = [
                                    "",
                                    "",
                                    "",
                                    "",
                                    "",
                                    "",
                                    "",
                                    "D", // [D]eleted
                                    safe.csv(reserveringenFacilitor[f].externnr.split("|")[0]),
                                    safe.csv(reserveringenFacilitor[f].externnr.split("|")[1]),
                                    ++seqNbr
                                ];
                                trs.push(tds.join(";"));
                            }
                        }
                        continue; // Klaar
                    }

                    trs.push(tds.join(";"));

                    if (    thisEvent.type === "occurrence" && "attendees" in masterEvent // Deelnemers van occurrences staan bij de seriesMaster geregistreerd
                        ||  "attendees" in thisEvent)
                    {   // Volgens mij zijn exceptions hier nooit relevant; ze zijn nooit nieuw (want ze begonnen als occurrence) en dus registreren we geen bezoeker-mutaties
                        var eventId = inArray(thisEvent.type, ["occurrence", "exception"]) ? thisEvent.seriesMasterId : thisEvent.id;
                        var occurrenceId = inArray(thisEvent.type, ["occurrence", "exception"]) ? thisEvent.id : "";
                        var thisEvent = ("attendees" in thisEvent) ? thisEvent : masterEvent; // Gebruik bij occurrence de gegevens van de seriesMaster
                        var visibility = thisEvent.sensitivity === "normal" ? 1 : 0;
                        for (var attendee in thisEvent.attendees) {
                            if (thisEvent.attendees[attendee].emailAddress &&
                                thisEvent.attendees[attendee].emailAddress.address &&
                                thisEvent.attendees[attendee].emailAddress.address !== thisEvent.organizer.emailAddress.address &&  // Organisator hoeft niet
                                thisEvent.attendees[attendee].emailAddress.address.toUpperCase() !== zaalemail.toUpperCase()) {     // Ruimte ook niet
                                tds = [
                                    safe.csv(((visibility ? thisEvent.subject : lcl_res_rsv_private) || "").slice(0, SUBJECT_MAX_LENGTH)),
                                    _startDate.toISOString(),
                                    _endDate.toISOString(),
                                    safe.csv(thisEvent.organizer.emailAddress.address),
                                    safe.csv(thisEvent.attendees[attendee].emailAddress.address),
                                    safe.csv(thisEvent.attendees[attendee].emailAddress.name),
                                    safe.csv(visibility),
                                    modifier,
                                    safe.csv(eventId),
                                    safe.csv(occurrenceId),
                                    seqNbr
                                ];
                                trs.push(tds.join(";"));
                            }
                        }
                    }
                }
            }
        }
    }
    if (trs.length)
        trs = [headerRow].concat(trs);
    return trs.join("\r\n");
}
