Repository: Accenture/sfmc-devtools
Pull request #719 (2023-02-07 15:55:52)

4.3.2
release


Patch:
diff --git a/.fork/custom-commands.json b/.fork/custom-commands.json
new file mode 100644
index 00000000..21d1adcc
--- /dev/null
+++ b/.fork/custom-commands.json
@@ -0,0 +1,14 @@
+[
+  {
+    "name": "Create PR to develop branch",
+    "target": "ref",
+    "refTargets": [
+      "localbranch",
+      "remotebranch"
+    ],
+    "action": {
+      "type": "url",
+      "url": "https://github.com/Accenture/sfmc-devtools/compare/develop...$shortname?expand=1"
+    }
+  }
+]
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml
index 0c559c08..faca9d1b 100644
--- a/.github/ISSUE_TEMPLATE/bug.yml
+++ b/.github/ISSUE_TEMPLATE/bug.yml
@@ -39,6 +39,7 @@ body:
           label: Version
           description: What version of our software are you running? (mcdev --version)
           options:
+              - 4.3.2
               - 4.3.1
               - 4.3.0
               - 4.2.1
diff --git a/boilerplate/gitignore-template b/boilerplate/gitignore-template
index 9f3e3d78..5e20f958 100644
--- a/boilerplate/gitignore-template
+++ b/boilerplate/gitignore-template
@@ -7,3 +7,4 @@ node_modules/
 /template/
 **/QueryStudioResults at*
 .mcdev-auth.json
+*.error.log
diff --git a/docs/dist/documentation.md b/docs/dist/documentation.md
index 89ec2875..cfc68afc 100644
--- a/docs/dist/documentation.md
+++ b/docs/dist/documentation.md
@@ -736,10 +736,7 @@ MessageSendActivity MetadataType
 * [AccountUser](#AccountUser) ⇐ [<code>MetadataType</code>](#MetadataType)
     * [.retrieve(retrieveDir, _, [__], [key])](#AccountUser.retrieve) ⇒ <code>Promise.&lt;TYPE.MetadataTypeMapObj&gt;</code>
     * [.retrieveChangelog()](#AccountUser.retrieveChangelog) ⇒ <code>Promise.&lt;TYPE.MetadataTypeMapObj&gt;</code>
-    * [.timeSinceDate(date)](#AccountUser.timeSinceDate) ⇒ <code>number</code>
-    * [.getBuName(id)](#AccountUser.getBuName) ⇒ <code>string</code>
     * [.document([metadata])](#AccountUser.document) ⇒ <code>Promise.&lt;void&gt;</code>
-    * [._generateDocMd(users, type, columnsToPrint)](#AccountUser._generateDocMd) ⇒ <code>string</code>
     * [.postRetrieveTasks(metadata)](#AccountUser.postRetrieveTasks) ⇒ <code>TYPE.MetadataTypeItem</code>
     * [.parseMetadata(metadata)](#AccountUser.parseMetadata) ⇒ <code>TYPE.MetadataTypeItem</code>
 
@@ -765,28 +762,6 @@ Retrieves SOAP based metadata of metadata type into local filesystem. executes c
 
 **Kind**: static method of [<code>AccountUser</code>](#AccountUser)  
 **Returns**: <code>Promise.&lt;TYPE.MetadataTypeMapObj&gt;</code> - Promise of metadata  
-<a name="AccountUser.timeSinceDate"></a>
-
-### AccountUser.timeSinceDate(date) ⇒ <code>number</code>
-**Kind**: static method of [<code>AccountUser</code>](#AccountUser)  
-**Returns**: <code>number</code> - time difference  
-
-| Param | Type | Description |
-| --- | --- | --- |
-| date | <code>string</code> | first date |
-
-<a name="AccountUser.getBuName"></a>
-
-### AccountUser.getBuName(id) ⇒ <code>string</code>
-helper to print bu names
-
-**Kind**: static method of [<code>AccountUser</code>](#AccountUser)  
-**Returns**: <code>string</code> - "bu name (bu id)""  
-
-| Param | Type | Description |
-| --- | --- | --- |
-| id | <code>number</code> | bu id |
-
 <a name="AccountUser.document"></a>
 
 ### AccountUser.document([metadata]) ⇒ <code>Promise.&lt;void&gt;</code>
@@ -799,18 +774,6 @@ Creates markdown documentation of all roles
 | --- | --- | --- |
 | [metadata] | <code>TYPE.MetadataTypeMap</code> | user list |
 
-<a name="AccountUser._generateDocMd"></a>
-
-### AccountUser.\_generateDocMd(users, type, columnsToPrint) ⇒ <code>string</code>
-**Kind**: static method of [<code>AccountUser</code>](#AccountUser)  
-**Returns**: <code>string</code> - markdown  
-
-| Param | Type | Description |
-| --- | --- | --- |
-| users | <code>Array.&lt;object&gt;</code> | list of users and installed package |
-| type | <code>&#x27;Installed Package&#x27;</code> \| <code>&#x27;User&#x27;</code> | choose what sub type to print |
-| columnsToPrint | <code>Array.&lt;Array&gt;</code> | helper array |
-
 <a name="AccountUser.postRetrieveTasks"></a>
 
 ### AccountUser.postRetrieveTasks(metadata) ⇒ <code>TYPE.MetadataTypeItem</code>
diff --git a/lib/metadataTypes/AccountUser.js b/lib/metadataTypes/AccountUser.js
index f320b6d3..e9157dbc 100644
--- a/lib/metadataTypes/AccountUser.js
+++ b/lib/metadataTypes/AccountUser.js
@@ -60,10 +60,17 @@ class AccountUser extends MetadataType {
         for (const item of resultsBatch) {
             this.userIdBuMap[item.AccountUser.AccountUserID] =
                 this.userIdBuMap[item.AccountUser.AccountUserID] || [];
-            this.userIdBuMap[item.AccountUser.AccountUserID].push({
-                ID: item.Account.ID,
-                Name: item.Account.Name,
-            });
+            // push to array if not already in array
+            if (
+                !this.userIdBuMap[item.AccountUser.AccountUserID].some(
+                    (bu) => bu.ID === item.Account.ID
+                )
+            ) {
+                this.userIdBuMap[item.AccountUser.AccountUserID].push({
+                    ID: item.Account.ID,
+                    Name: item.Account.Name,
+                });
+            }
         }
         // get actual user details
         /** @type {TYPE.SoapRequestParams} */
@@ -114,10 +121,11 @@ class AccountUser extends MetadataType {
     }
     /**
      *
+     * @private
      * @param {string} date first date
      * @returns {number} time difference
      */
-    static timeSinceDate(date) {
+    static _timeSinceDate(date) {
         const interval = 'days';
         const second = 1000,
             minute = second * 60,
@@ -172,13 +180,28 @@ class AccountUser extends MetadataType {
     /**
      * helper to print bu names
      *
+     * @private
      * @param {number} id bu id
      * @returns {string} "bu name (bu id)""
      */
-    static getBuName(id) {
+    static _getBuName(id) {
         const name = this.buObject.eid == id ? '_ParentBU_' : this.buIdName[id];
         return `<nobr>${name} (${id})</nobr>`;
     }
+
+    /**
+     * helper that gets BU names from config
+     *
+     * @private
+     */
+    static _getBuNames() {
+        this.buIdName = {};
+        for (const cred in this.properties.credentials) {
+            for (const buName in this.properties.credentials[cred].businessUnits) {
+                this.buIdName[this.properties.credentials[cred].businessUnits[buName]] = buName;
+            }
+        }
+    }
     /**
      * Creates markdown documentation of all roles
      *
@@ -209,7 +232,7 @@ class AccountUser extends MetadataType {
             }
         }
         // init map of BU Ids > BU Name
-        this.buIdName = {};
+        this._getBuNames();
 
         // initialize permission object
         this.allPermissions = {};
@@ -244,14 +267,19 @@ class AccountUser extends MetadataType {
             }
             let associatedBus = '';
             if (user.AssociatedBusinessUnits__c) {
-                associatedBus = user.AssociatedBusinessUnits__c.map((item) => {
-                    this.buIdName[item.ID] = item.Name;
-                    return this.getBuName(item.ID);
-                })
+                user.AssociatedBusinessUnits__c.push({
+                    ID: user.DefaultBusinessUnit,
+                });
+                // ensure associatedBus have no duplicates
+                associatedBus = [
+                    ...new Set(
+                        user.AssociatedBusinessUnits__c.map((item) => this._getBuName(item.ID))
+                    ),
+                ]
                     .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
                     .join(',<br> ');
             }
-            const defaultBUName = this.getBuName(user.DefaultBusinessUnit);
+            const defaultBUName = this._getBuName(user.DefaultBusinessUnit);
             users.push({
                 TYPE: user.type__c,
                 UserID: user.UserID,
@@ -267,7 +295,7 @@ class AccountUser extends MetadataType {
                 AssociatedBusinessUnits__c: associatedBus,
                 Roles: roles,
                 UserPermissions: userPermissions,
-                LastSuccessfulLogin: this.timeSinceDate(user.LastSuccessfulLogin),
+                LastSuccessfulLogin: this._timeSinceDate(user.LastSuccessfulLogin),
                 CreatedDate: user.CreatedDate.split('T').join(' '),
                 ModifiedDate: user.ModifiedDate.split('T').join(' '),
             });
@@ -325,6 +353,7 @@ class AccountUser extends MetadataType {
     }
     /**
      *
+     * @private
      * @param {object[]} users list of users and installed package
      * @param {'Installed Package'|'User'} type choose what sub type to print
      * @param {Array[]} columnsToPrint helper array
diff --git a/lib/metadataTypes/Asset.js b/lib/metadataTypes/Asset.js
index c96c9f1e..59356078 100644
--- a/lib/metadataTypes/Asset.js
+++ b/lib/metadataTypes/Asset.js
@@ -317,22 +317,30 @@ class Asset extends MetadataType {
                                 } catch (ex) {
                                     failed.push({ item: item, error: ex });
                                 }
+                                // even if the extended file failed, still save the metadata
                                 metadata[item.customerKey] = item;
                             }
                             // this is a complex type which stores data in the asset itself
                             else if (!completed.includes(item.id)) {
-                                const extendedItem = await this.client.rest.get(
-                                    'asset/v1/content/assets/' + item.id
-                                );
-                                metadata[item.customerKey] = extendedItem;
+                                try {
+                                    const extendedItem = await this.client.rest.get(
+                                        'asset/v1/content/assets/' + item.id
+                                    );
+                                    // only save the metadata if we have extended content
+                                    metadata[item.customerKey] = extendedItem;
+                                } catch (ex) {
+                                    failed.push({ item: item, error: ex });
+                                }
                             }
                             completed.push(item.id);
-                            await this.saveResults(
-                                metadata,
-                                retrieveDir,
-                                'asset-' + subType,
-                                templateVariables
-                            );
+                            if (metadata[item.customerKey]) {
+                                await this.saveResults(
+                                    metadata,
+                                    retrieveDir,
+                                    'asset-' + subType,
+                                    templateVariables
+                                );
+                            }
                             // update the current value in your application..
                             extendedBar.increment();
                         })
@@ -346,7 +354,7 @@ class Asset extends MetadataType {
             } catch (ex) {
                 extendedBar.stop();
                 Asset._resetLogLevel(loggerLevelBak, failed);
-                // timeouts should be retried, others can be retried
+                // timeouts should be retried, others can be exited
                 if (ex.code !== 'ETIMEDOUT') {
                     throw ex;
                 }
@@ -394,11 +402,17 @@ class Asset extends MetadataType {
             );
             for (const fail of failed) {
                 Util.logger.warn(
-                    `   -  "${fail.item.name}" (${fail.item.customerKey}) in ${fail.item.r__folder_Path}: ${fail.error.message}`
+                    `   - "${fail.item.name}" (${fail.item.customerKey}): ${fail.error.message} (${
+                        fail.error.code
+                    })${
+                        fail.error.endpoint
+                            ? Util.getGrayMsg(
+                                  ' - ' +
+                                      fail.error.endpoint.split('rest.marketingcloudapis.com')[1]
+                              )
+                            : ''
+                    }`
                 );
-                Util.logger.debug(`-- Error: ${fail.error.message}`);
-                Util.logger.debug(`-- AssetType: ${fail.item.assetType.name}`);
-                Util.logger.debug(`-- fileProperties: ${JSON.stringify(fail.item.fileProperties)}`);
             }
             Util.logger.info(
                 ' - You will still find a JSON file for each of these in the download directory.'
diff --git a/lib/metadataTypes/DataExtensionField.js b/lib/metadataTypes/DataExtensionField.js
index 997a48f6..0ed2758a 100644
--- a/lib/metadataTypes/DataExtensionField.js
+++ b/lib/metadataTypes/DataExtensionField.js
@@ -242,9 +242,11 @@ class DataExtensionField extends MetadataType {
         }
 
         Util.logger.info(
-            ` - Found ${deployColumns.length} added/updated Fields for ${deKey}${
-                deployColumns.length ? ': ' : ''
-            }` + deployColumns.map((item) => item.Name).join(', ')
+            Util.getGrayMsg(
+                ` - Found ${deployColumns.length} added/updated Fields for ${deKey}${
+                    deployColumns.length ? ': ' : ''
+                }` + deployColumns.map((item) => item.Name).join(', ')
+            )
         );
         return existingFieldByName;
     }
diff --git a/lib/metadataTypes/EmailSendDefinition.js b/lib/metadataTypes/EmailSendDefinition.js
index d60e4574..1ea51095 100644
--- a/lib/metadataTypes/EmailSendDefinition.js
+++ b/lib/metadataTypes/EmailSendDefinition.js
@@ -104,15 +104,33 @@ class EmailSendDefinition extends MetadataType {
             delete metadata.r__email_Name;
         } else if (metadata.r__assetMessage_Key) {
             // content builder
-            // * this ignores r__assetMessage_Name on purpose as that is only unique per parent folder but useful during PR reviews
-            metadata.Email.ID = cache.searchForField(
-                'asset',
-                metadata.r__assetMessage_Key,
-                'customerKey',
-                'legacyData.legacyId'
-            );
-            delete metadata.r__assetMessage_Key;
-            delete metadata.r__assetMessage_Name;
+            // * this ignores r__assetMessage_Name on purpose\ as that is only unique per parent folder but useful during PR reviews
+            // will try to find the key with the bu mid at the end, if unable, will try to find the key without it
+            try {
+                // check asset key as provided
+                metadata.Email.ID = cache.searchForField(
+                    'asset',
+                    metadata.r__assetMessage_Key,
+                    'customerKey',
+                    'legacyData.legacyId'
+                );
+                delete metadata.r__assetMessage_Key;
+                delete metadata.r__assetMessage_Name;
+            } catch {
+                // if we deploy to another BU, try applying the BU's MID to the end, which we do in preDeployTasks for assets
+
+                // get suffix to update customer key at the end
+                const suffix = '-' + this.buObject.mid;
+
+                metadata.Email.ID = cache.searchForField(
+                    'asset',
+                    metadata.r__assetMessage_Key.slice(0, Math.max(0, 36 - suffix.length)) + suffix,
+                    'customerKey',
+                    'legacyData.legacyId'
+                );
+                delete metadata.r__assetMessage_Key;
+                delete metadata.r__assetMessage_Name;
+            }
         }
         // Target Audience DataExtension
         // normalize first because this can be an array
@@ -129,7 +147,7 @@ class EmailSendDefinition extends MetadataType {
             if (sdl.r__dataExtension_Key) {
                 if (sdl.DataSourceTypeID !== 'CustomObject') {
                     throw new Error(
-                        `Skipping ${metadata.Name} (${metadata.CustomerKey}): Expecting DataSourceTypeID to equal 'CustomObject' when r__dataExtension_Key is defined; Found '${sdl.DataSourceTypeID}'`
+                        ` ☇ skipping ${this.definition.type} ${metadata.Name} (${metadata.CustomerKey}): Expecting DataSourceTypeID to equal 'CustomObject' when r__dataExtension_Key is defined; Found '${sdl.DataSourceTypeID}'`
                     );
                 }
                 sdl.CustomObjectID = cache.searchForField(
@@ -141,9 +159,13 @@ class EmailSendDefinition extends MetadataType {
                 delete sdl.r__dataExtension_Key;
             } else if (sdl.DataSourceTypeID === 'CustomObject') {
                 throw new Error(
-                    `Skipping ${metadata.Name} (${metadata.CustomerKey}): Expecting r__dataExtension_Key to be defined if DataSourceTypeID='CustomObject'`
+                    ` ☇ skipping ${this.definition.type} ${metadata.Name} (${metadata.CustomerKey}): Expecting r__dataExtension_Key to be defined if DataSourceTypeID='CustomObject'`
                 );
             }
+            if (!sdl.SalesForceObjectID || sdl.SalesForceObjectID === '') {
+                // otherwise this causes error 42117 / invalid ObjectID
+                delete sdl.SalesForceObjectID;
+            }
             // get List (required)
             if (sdl.r__list_PathName) {
                 sdl.List = {
@@ -152,7 +174,7 @@ class EmailSendDefinition extends MetadataType {
                 delete sdl.r__list_PathName;
             } else {
                 throw new Error(
-                    `Field SendDefinitionList.r__list_PathName was not defined. Please try re-retrieving this ESD from your source BU.`
+                    ` ☇ skipping ${this.definition.type} ${metadata.Name} (${metadata.CustomerKey}) Field SendDefinitionList.r__list_PathName was not defined. Please try re-retrieving this ESD from your source BU.`
                 );
             }
         }
@@ -208,7 +230,11 @@ class EmailSendDefinition extends MetadataType {
                     delete metadata.Email;
                 } catch {
                     Util.logger.warn(
-                        ` - ${this.definition.typeName} '${metadata.Name}'/'${metadata.CustomerKey}': Could not find email with ID ${metadata.Email.ID} in Classic nor in Content Builder.`
+                        ` - ${this.definition.type} ${metadata[this.definition.nameField]} (${
+                            metadata[this.definition.keyField]
+                        }): Could not find email with ID ${
+                            metadata.Email.ID
+                        } in Classic nor in Content Builder.`
                     );
                 }
             }
@@ -234,7 +260,11 @@ class EmailSendDefinition extends MetadataType {
                     delete sdl.CustomObjectID;
                 } catch {
                     Util.logger.warn(
-                        ` - ${this.definition.typeName} '${metadata.Name}'/'${metadata.CustomerKey}': Could not find Target Audience (DataExtension) with ObjectID ${sdl.CustomObjectID}.`
+                        ` - ${this.definition.type} ${metadata[this.definition.nameField]} (${
+                            metadata[this.definition.keyField]
+                        }): Could not find Target Audience (DataExtension) with ObjectID ${
+                            sdl.CustomObjectID
+                        }.`
                     );
                 }
             }
@@ -245,10 +275,16 @@ class EmailSendDefinition extends MetadataType {
                     delete sdl.List;
                 } catch (ex) {
                     Util.logger.warn(
-                        ` - ${this.definition.typeName} '${metadata.Name}'/'${metadata.CustomerKey}': ${ex.message}`
+                        ` - ${this.definition.type} ${metadata[this.definition.nameField]} (${
+                            metadata[this.definition.keyField]
+                        }): ${ex.message}`
                     );
                 }
             }
+            if (!sdl.SalesForceObjectID) {
+                // otherwise this causes error 42117 / invalid ObjectID
+                delete sdl.SalesForceObjectID;
+            }
         }
 
         return metadata;
diff --git a/lib/metadataTypes/List.js b/lib/metadataTypes/List.js
index 475aa2db..eda7145c 100644
--- a/lib/metadataTypes/List.js
+++ b/lib/metadataTypes/List.js
@@ -53,21 +53,23 @@ class List extends MetadataType {
      */
     static async retrieveForCache() {
         const results = await this.retrieve();
-        if (!cache.getCache()?.folder) {
-            const subTypeArr = [
-                'list',
-                'mysubs',
-                'suppression_list',
-                'publication',
-                'contextual_suppression_list',
-            ];
-            Util.logger.debug('folders not cached but required for list');
-            Util.logger.info(' - Caching dependent Metadata: folder');
-            Util.logSubtypes(subTypeArr);
-            Folder.client = this.client;
-            Folder.buObject = this.buObject;
-            Folder.properties = this.properties;
-            const result = await Folder.retrieveForCache(null, subTypeArr);
+        const subTypeArr = [
+            'list',
+            'mysubs',
+            'suppression_list',
+            'publication',
+            'contextual_suppression_list',
+        ];
+        Util.logger.debug('folders not cached but required for list');
+        Util.logger.info(' - Caching dependent Metadata: folder');
+        Util.logSubtypes(subTypeArr);
+        Folder.client = this.client;
+        Folder.buObject = this.buObject;
+        Folder.properties = this.properties;
+        const result = await Folder.retrieveForCache(null, subTypeArr);
+        if (cache.getCache()?.folder) {
+            cache.mergeMetadata('folder', result.metadata);
+        } else {
             cache.setMetadata('folder', result.metadata);
         }
         for (const metadataEntry in results.metadata) {
diff --git a/lib/metadataTypes/MetadataType.js b/lib/metadataTypes/MetadataType.js
index 3c80006b..faacb931 100644
--- a/lib/metadataTypes/MetadataType.js
+++ b/lib/metadataTypes/MetadataType.js
@@ -725,7 +725,7 @@ class MetadataType {
                 Util.logger.info(
                     ` - updated ${this.definition.type}: ${
                         metadataEntry[this.definition.keyField]
-                    } /  ${metadataEntry[this.definition.nameField]}`
+                    } / ${metadataEntry[this.definition.nameField]}`
                 );
             }
             return response;
diff --git a/lib/metadataTypes/TriggeredSendDefinition.js b/lib/metadataTypes/TriggeredSendDefinition.js
index 44a1d795..13e4194a 100644
--- a/lib/metadataTypes/TriggeredSendDefinition.js
+++ b/lib/metadataTypes/TriggeredSendDefinition.js
@@ -132,8 +132,9 @@ class TriggeredSendDefinition extends MetadataType {
             this.setFolderPath(metadata);
         } catch {
             Util.logger.verbose(
-                ` - skipping ${this.definition.typeName} '${metadata.Name}'/'${metadata.CustomerKey}': Could not find folder.`
+                ` ☇ skipping ${this.definition.typeName} '${metadata.Name}'/'${metadata.CustomerKey}': Could not find folder.`
             );
+            // do not save this TSD because it would not be visible in the user interface
             return;
         }
 
@@ -163,9 +164,9 @@ class TriggeredSendDefinition extends MetadataType {
                 delete metadata.Email;
             } catch {
                 Util.logger.verbose(
-                    ` - skipping ${this.definition.typeName} '${metadata.Name}'/'${metadata.CustomerKey}': Could not find email with ID ${metadata.Email.ID} in Classic nor in Content Builder.`
+                    ` - ${this.definition.typeName} '${metadata.Name}'/'${metadata.CustomerKey}': Could not find email with ID ${metadata.Email.ID} in Classic nor in Content Builder. This TSD cannot be replublished but potentially restarted with its cached version of the email.`
                 );
-                return;
+                // save this TSD because it could be fixed by the user or potentially restarted without a fix; also, it might be used by a journey
             }
         }
         // List (optional)
@@ -175,9 +176,9 @@ class TriggeredSendDefinition extends MetadataType {
                 delete metadata.List;
             } catch (ex) {
                 Util.logger.verbose(
-                    ` - skipping ${this.definition.typeName} '${metadata.Name}'/'${metadata.CustomerKey}': ${ex.message}`
+                    ` - ${this.definition.typeName} '${metadata.Name}'/'${metadata.CustomerKey}': ${ex.message}`
                 );
-                return;
+                // save this TSD because it could be fixed by the user
             }
         }
 
diff --git a/lib/metadataTypes/definitions/EmailSendDefinition.definition.js b/lib/metadataTypes/definitions/EmailSendDefinition.definition.js
index 9263ef82..e49f8422 100644
--- a/lib/metadataTypes/definitions/EmailSendDefinition.definition.js
+++ b/lib/metadataTypes/definitions/EmailSendDefinition.definition.js
@@ -1,6 +1,6 @@
 module.exports = {
     bodyIteratorField: 'Results',
-    dependencies: ['folder-userinitiatedsends', 'email', 'asset-message', 'dataExtension', 'list'], // filter, SendClassification, SenderProfile, DeliveryProfile
+    dependencies: ['folder-userinitiatedsends', 'email', 'asset-message', 'dataExtension', 'list'], // filter(+), sendClassification(+), SenderProfile(n/a), DeliveryProfile(n/a)
     folderType: 'userinitiatedsends',
     hasExtended: false,
     idField: 'ObjectID',
@@ -22,31 +22,31 @@ module.exports = {
             isCreateable: true,
             isUpdateable: true,
             retrieving: true,
-            templating: false,
+            templating: true,
         },
         AutoBccEmail: {
             isCreateable: true,
             isUpdateable: true,
             retrieving: true,
-            templating: false,
+            templating: true,
         },
         BccEmail: {
             isCreateable: true,
             isUpdateable: true,
             retrieving: true,
-            templating: false,
+            templating: true,
         },
         CategoryID: {
             isCreateable: true,
             isUpdateable: true,
             retrieving: true,
-            templating: false,
+            templating: true,
         },
         CCEmail: {
             isCreateable: true,
             isUpdateable: true,
             retrieving: true,
-            templating: false,
+            templating: true,
         },
         'Client.ID': {
             isCreateable: false,
@@ -61,8 +61,8 @@ module.exports = {
             templating: false,
         },
         CreatedDate: {
-            isCreateable: true,
-            isUpdateable: true,
+            isCreateable: false,
+            isUpdateable: false,
             retrieving: true,
             templating: false,
         },
@@ -70,19 +70,19 @@ module.exports = {
             isCreateable: true,
             isUpdateable: true,
             retrieving: true,
-            templating: false,
+            templating: true,
         },
         DeduplicateByEmail: {
             isCreateable: true,
             isUpdateable: true,
             retrieving: true,
-            templating: false,
+            templating: true,
         },
         'DeliveryProfile.CustomerKey': {
             isCreateable: true,
             isUpdateable: true,
             retrieving: true,
-            templating: false,
+            templating: true,
         },
         'DeliveryProfile.DomainType': {
             isCreateable: false,
@@ -130,13 +130,13 @@ module.exports = {
             isCreateable: true,
             isUpdateable: true,
             retrieving: true,
-            templating: false,
+            templating: true,
         },
         'DeliveryProfile.PrivateIP': {
             isCreateable: true,
             isUpdateable: true,
             retrieving: true,
-            templating: false,
+            templating: true,
         },
         'DeliveryProfile.SourceAddressType': {
             isCreateable: false,
@@ -154,7 +154,7 @@ module.exports = {
             isCreateable: true,
             isUpdateable: true,
             retrieving: true,
-            templating: false,
+            templating: true,
         },
         DomainType: {
             isCreateable: false,
@@ -166,13 +166,13 @@ module.exports = {
             isCreateable: true,
             isUpdateable: true,
             retrieving: true,
-            templating: false,
+            templating: true,
         },
         'Email.ID': {
             isCreateable: true,
             isUpdateable: true,
             retrieving: true,
-            templating: false,
+            templating: true,
         },
         'Email.ObjectID': {
             isCreateable: false,
@@ -186,17 +186,35 @@ module.exports = {
             retrieving: false,
             templating: false,
         },
+        'Email.Name': {
+            isCreateable: true,
+            isUpdateable: true,
+            retrieving: false,
+            templating: false,
+        },
+        'Email.Subject': {
+            isCreateable: true,
+            isUpdateable: true,
+            retrieving: false,
+            templating: false,
+        },
+        'Email.Status': {
+            isCreateable: true,
+            isUpdateable: true,
+            retrieving: false,
+            templating: false,
+        },
         EmailSubject: {
             isCreateable: true,
             isUpdateable: true,
             retrieving: true,
-            templating: false,
+            templating: true,
         },
         ExclusionFilter: {
             isCreateable: true,
             isUpdateable: true,
             retrieving: true,
-            templating: false,
+            templating: true,
         },
         FooterContentArea: {
             isCreateable: false,
@@ -256,7 +274,7 @@ module.exports = {
             isCreateable: true,
             isUpdateable: true,
             retrieving: true,
-            templating: false,
+            templating: true,
         },
         IsPlatformObject: {
             isCreateable: true,
@@ -280,7 +298,7 @@ module.exports = {
             isCreateable: true,
             isUpdateable: true,
             retrieving: true,
-            templating: false,
+            templating: true,
         },
         Keyword: {
             isCreateable: false,
@@ -295,8 +313,8 @@ module.exports = {
             templating: false,
         },
         ModifiedDate: {
-            isCreateable: true,
-            isUpdateable: true,
+            isCreateable: false,
+            isUpdateable: false,
             retrieving: true,
             templating: false,
         },
@@ -304,7 +322,7 @@ module.exports = {
             isCreateable: true,
             isUpdateable: true,
             retrieving: true,
-            templating: false,
+            templating: true,
         },
         ObjectID: {
             isCreateable: false,
@@ -376,7 +394,7 @@ module.exports = {
             isCreateable: true,
             isUpdateable: true,
             retrieving: true,
-            templating: false,
+            templating: true,
         },
         'SendClassification.ObjectID': {
             isCreateable: false,
@@ -400,7 +418,7 @@ module.exports = {
             isCreateable: true,
             isUpdateable: true,
             retrieving: true,
-            templating: false,
+            templating: true,
         },
         'SendDefinitionList[].ObjectID': {
             isCreateable: false,
@@ -414,6 +432,9 @@ module.exports = {
             retrieving: false,
             templating: false,
         },
+        'SendDefinitionList[].CustomObjectID': {
+            skipValidation: true, // cannot be asked for but will be retrieved
+        },
         'SendDefinitionList[].SendDefinitionListType': {
             skipValidation: true, // cannot be asked for but will be retrieved
         },
@@ -448,7 +469,7 @@ module.exports = {
             isCreateable: true,
             isUpdateable: true,
             retrieving: true,
-            templating: false,
+            templating: true,
         },
         'SenderProfile.FromAddress': {
             isCreateable: false,
@@ -478,13 +499,13 @@ module.exports = {
             isCreateable: true,
             isUpdateable: true,
             retrieving: true,
-            templating: false,
+            templating: true,
         },
         SendWindowClose: {
             isCreateable: true,
             isUpdateable: true,
             retrieving: true,
-            templating: false,
+            templating: true,
         },
         SendWindowDelete: {
             isCreateable: false,
@@ -496,7 +517,7 @@ module.exports = {
             isCreateable: true,
             isUpdateable: true,
             retrieving: true,
-            templating: false,
+            templating: true,
         },
         SourceAddressType: {
             isCreateable: false,
@@ -508,13 +529,13 @@ module.exports = {
             isCreateable: true,
             isUpdateable: true,
             retrieving: true,
-            templating: false,
+            templating: true,
         },
         TestEmailAddr: {
             isCreateable: true,
             isUpdateable: true,
             retrieving: true,
-            templating: false,
+            templating: true,
         },
         TimeZone: {
             isCreateable: false,
diff --git a/lib/util/auth.js b/lib/util/auth.js
index 1344531a..64988a46 100644
--- a/lib/util/auth.js
+++ b/lib/util/auth.js
@@ -101,7 +101,7 @@ function setupSDK(sessionKey, authObject) {
                 credentialStore.set(sessionKey, authObject);
             },
             onConnectionError: (ex, remainingAttempts) => {
-                Util.logger.warn(
+                Util.logger.info(
                     ` - Connection problem (Code: ${ex.code}). Retrying ${remainingAttempts} time${
                         remainingAttempts > 1 ? 's' : ''
                     }${
diff --git a/lib/util/devops.js b/lib/util/devops.js
index 4907e1df..788a3a7f 100644
--- a/lib/util/devops.js
+++ b/lib/util/devops.js
@@ -111,6 +111,8 @@ const DevOps = {
             // Filter to only handle files that start with at least one of the passed filterPaths.
             // ! Filter happens after initial parse, because if file was moved its new path has to be calculated
             .filter((file) => filterPaths.some((path) => file.file.startsWith(path)))
+            .filter((file) => !file.file.endsWith('.error.log'))
+            .filter((file) => !file.file.endsWith('.md'))
             // ensure badly named files on unsupported metadata types are not in our subset
             .filter((/** @type {TYPE.DeltaPkgItem} */ file) => {
                 if (MetadataType[file.type]) {
@@ -210,10 +212,12 @@ const DevOps = {
             const copied = delta.map((file) =>
                 File.copyFile(
                     file.file,
-                    file.file.replace(
-                        path.normalize(properties.directories.retrieve),
-                        path.normalize(properties.directories.deploy)
-                    )
+                    path
+                        .normalize(file.file)
+                        .replace(
+                            path.normalize(properties.directories.retrieve),
+                            path.normalize(properties.directories.deploy)
+                        )
                 )
             );
             const results = await Promise.all(copied);
diff --git a/package-lock.json b/package-lock.json
index 95cb5ccd..4e5d7b7b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
     "name": "mcdev",
-    "version": "4.3.1",
+    "version": "4.3.2",
     "lockfileVersion": 2,
     "requires": true,
     "packages": {
         "": {
             "name": "mcdev",
-            "version": "4.3.1",
+            "version": "4.3.2",
             "license": "MIT",
             "dependencies": {
                 "beauty-amp-core": "0.3.7",
diff --git a/package.json b/package.json
index e62b3ac2..f50b1c9c 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
     "name": "mcdev",
-    "version": "4.3.1",
+    "version": "4.3.2",
     "description": "Accenture Salesforce Marketing Cloud DevTools",
     "author": "Accenture: joern.berkefeld, douglas.midgley, robert.zimmermann, maciej.barnas",
     "license": "MIT",


Test patch:
diff --git a/test/mockRoot/.mcdevrc.json b/test/mockRoot/.mcdevrc.json
index 3d436c01..8700a78e 100644
--- a/test/mockRoot/.mcdevrc.json
+++ b/test/mockRoot/.mcdevrc.json
@@ -74,5 +74,5 @@
             "triggeredSendDefinition"
         ]
     },
-    "version": "4.3.1"
+    "version": "4.3.2"
 }