From 2da6931b88751aca3f388180f5fe45581e58f61a Mon Sep 17 00:00:00 2001 From: Yannick Trinh Date: Tue, 16 Jun 2026 21:10:21 +0200 Subject: [PATCH 1/3] Fix Office365 Graph mail property validation Commit message: Refs #85 Fix Office365 Graph mail property validation (unknown keys + valid Graph fields) PR description: Fixes #85 Reject unknown Graph mail properties with explicit errors (send/update), remove hardcoded attachment hint, and allow valid Graph fields (changeKey, extensions) so valid messages are not blocked. --- Project/Sources/Classes/Office365Mail.4dm | 13 +++- Project/Sources/Classes/_GraphAPI.4dm | 88 +++++++++++++++++++++++ 2 files changed, 99 insertions(+), 2 deletions(-) diff --git a/Project/Sources/Classes/Office365Mail.4dm b/Project/Sources/Classes/Office365Mail.4dm index 75cb8316..fd7469fc 100644 --- a/Project/Sources/Classes/Office365Mail.4dm +++ b/Project/Sources/Classes/Office365Mail.4dm @@ -31,10 +31,11 @@ Class constructor($inProvider : cs.OAuth2Provider; $inParameters : Object) // ---------------------------------------------------- -Function _postJSONMessage($inURL : Text; $inMail : Object; $bSkipMessageEncapsulation : Boolean; $inHeaders : Object) : Object +Function _postJSONMessage($inFunction : Text; $inURL : Text; $inMail : Object; $bSkipMessageEncapsulation : Boolean; $inHeaders : Object) : Object /** * @function _postJSONMessage * @private + * @param {Text} $inFunction - Caller name for error reporting (e.g. `"office365.mail.send"`) * @param {Text} $inURL - Target Graph API endpoint URL * @param {Object} $inMail - Mail object in Microsoft Graph JSON format * @param {Boolean} $bSkipMessageEncapsulation - When `True`, sends `$inMail` as-is; @@ -54,6 +55,10 @@ Function _postJSONMessage($inURL : Text; $inMail : Object; $bSkipMessageEncapsul End if $headers["Content-Type"]:="application/json" + If (Not(This._validateGraphMessageProperties($inMail; $inFunction))) + return This._returnStatus() + End if + var $message : Object var $messageCopy : Object:=This._copyGraphMessage($inMail) If (Not(OB Is defined($inMail; "message")) && Not($bSkipMessageEncapsulation)) @@ -149,7 +154,7 @@ Function _postMessage($inFunction : Text; $inURL : Text; $inMail : Variant; $bSk $status:=This._postMailMIMEMessage($inURL; $inMail) : ((This.mailType="Microsoft") && (Value type($inMail)=Is object)) - $status:=This._postJSONMessage($inURL; $inMail; $bSkipMessageEncapsulation; $inHeader) + $status:=This._postJSONMessage($inFunction; $inURL; $inMail; $bSkipMessageEncapsulation; $inHeader) Else Super._throwError(10; {which: 1; function: $inFunction}) @@ -542,6 +547,10 @@ Function update($inMailId : Text; $inMail : Object) : Object If ((Type($inMail)=Is object) && (Type($inMailId)=Is text) && (Length(String($inMailId))>0)) + If (Not(This._validateGraphMessageProperties($inMail; "office365.mail.update"))) + return This._returnStatus() + End if + var $response : Object var $URL : Text:=Super._getURL() diff --git a/Project/Sources/Classes/_GraphAPI.4dm b/Project/Sources/Classes/_GraphAPI.4dm index 7e6cc751..fa92b13f 100644 --- a/Project/Sources/Classes/_GraphAPI.4dm +++ b/Project/Sources/Classes/_GraphAPI.4dm @@ -76,6 +76,94 @@ Function _copyGraphMessage($inMessage : Object) : Object // ---------------------------------------------------- +Function _validateGraphMessageProperties($inPayload : Object; $inFunction : Text) : Boolean +/** + * @function _validateGraphMessageProperties + * @private + * @param {Object} $inPayload - Graph message object or request envelope (`{message: ...}`) + * @param {Text} $inFunction - Caller function name for error reporting + * @returns {Boolean} `True` when all properties are supported; otherwise `False` + * @description Validates mail payload keys before sending to Microsoft Graph. + * Adds an error to the stack for each unsupported property (for example `attachment` + * instead of `attachments`). + */ + + var $isValid : Boolean:=True + + If (($inPayload#Null) && (Value type($inPayload)=Is object)) + + var $message : Object + var $allowedPayloadKeys : Object:={message: True; saveToSentItems: True; comment: True} + If (OB Is defined($inPayload; "message") && (Value type($inPayload.message)=Is object)) + $message:=$inPayload.message + + var $payloadKey : Text + For each ($payloadKey; OB Keys($inPayload)) + If (Not(OB Is defined($allowedPayloadKeys; $payloadKey))) + This._pushError(12; {function: $inFunction; message: "Unsupported property \""+$payloadKey+"\" in mail payload."}) + $isValid:=False + End if + End for each + Else + $message:=$inPayload + End if + + var $messageKey : Text + For each ($messageKey; OB Keys($message)) + var $isAllowed : Boolean:=False + Case of + : ($messageKey="attachments") + : ($messageKey="bccRecipients") + : ($messageKey="body") + : ($messageKey="bodyPreview") + : ($messageKey="categories") + : ($messageKey="ccRecipients") + : ($messageKey="changeKey") + : ($messageKey="conversationId") + : ($messageKey="conversationIndex") + : ($messageKey="createdDateTime") + : ($messageKey="extensions") + : ($messageKey="flag") + : ($messageKey="from") + : ($messageKey="hasAttachments") + : ($messageKey="id") + : ($messageKey="importance") + : ($messageKey="inferenceClassification") + : ($messageKey="internetMessageHeaders") + : ($messageKey="internetMessageId") + : ($messageKey="isDeliveryReceiptRequested") + : ($messageKey="isDraft") + : ($messageKey="isRead") + : ($messageKey="isReadReceiptRequested") + : ($messageKey="lastModifiedDateTime") + : ($messageKey="multiValueExtendedProperties") + : ($messageKey="parentFolderId") + : ($messageKey="receivedDateTime") + : ($messageKey="replyTo") + : ($messageKey="sender") + : ($messageKey="sentDateTime") + : ($messageKey="singleValueExtendedProperties") + : ($messageKey="subject") + : ($messageKey="toRecipients") + : ($messageKey="uniqueBody") + : ($messageKey="webLink") + $isAllowed:=True + End case + + If (Not($isAllowed)) + This._pushError(12; {function: $inFunction; message: "Unsupported property \""+$messageKey+"\" in mail object."}) + $isValid:=False + End if + End for each + + End if + + return $isValid + + + // ---------------------------------------------------- + + Function _loadFromObject($inObject : Object) /** * @function _loadFromObject From e8deecfb56102348be1d7aa8e1b77f49914df1d0 Mon Sep 17 00:00:00 2001 From: Yannick Trinh Date: Tue, 14 Jul 2026 15:12:04 +0200 Subject: [PATCH 2/3] Enhance Office365 mail property validation with detailed architecture and support for JMAP properties --- Documentation/Classes/Office365.md | 45 ++++++++++- Project/Sources/Classes/Office365Mail.4dm | 10 ++- Project/Sources/Classes/_GraphAPI.4dm | 97 +++++++++++++---------- 3 files changed, 107 insertions(+), 45 deletions(-) diff --git a/Documentation/Classes/Office365.md b/Documentation/Classes/Office365.md index 694b9952..857c984f 100644 --- a/Documentation/Classes/Office365.md +++ b/Documentation/Classes/Office365.md @@ -595,7 +595,50 @@ The `event` object used with Microsoft Calendar methods includes the following m | hideAttendees | | Boolean | (Default: false) If `true`, attendees will only see themselves.|Yes| ## Mail - + +### Mail validation architecture + +When sending or appending a mail message, `Office365Mail` routes the payload through format-specific validation before the HTTP call is made. The diagram below shows the class relationships and the validation flow. + +```mermaid +classDiagram + class _GraphAPI { + +_validateGraphMessageProperties(payload, function) Boolean + +_validateJMAPMessageProperties(mail, function) Boolean + +_copyGraphMessage(message) Object + +_loadFromObject(object) + +_getURLParamsFromObject(parameters, count) Text + } + + class Office365Mail { + +mailType : Text + +userId : Text + +send(mail, options) Object + +append(mail, folderId) Object + +update(mailId, mail) Object + +reply(mailId, mail, options) Object + -_postMessage(function, url, mail, skipEncap, headers) Object + -_postJSONMessage(function, url, mail, skipEncap, headers) Object + -_postMailMIMEMessage(url, mail) Object + } + + _GraphAPI <|-- Office365Mail + + note for Office365Mail "_postMessage dispatch:\n• mailType=Microsoft → _validateGraphMessageProperties → _postJSONMessage\n• mailType=JMAP → _validateJMAPMessageProperties → _postMailMIMEMessage\n• mailType=MIME → _postMailMIMEMessage (no validation)" +``` + +#### Supported Microsoft Graph message properties + +The `_validateGraphMessageProperties` method accepts these Microsoft Graph [message resource](https://learn.microsoft.com/en-us/graph/api/resources/message?view=graph-rest-1.0) keys: + +`attachments`, `bccRecipients`, `body`, `bodyPreview`, `categories`, `ccRecipients`, `changeKey`, `conversationId`, `conversationIndex`, `createdDateTime`, `extensions`, `flag`, `from`, `hasAttachments`, `id`, `importance`, `inferenceClassification`, `internetMessageHeaders`, `internetMessageId`, `isDeliveryReceiptRequested`, `isDraft`, `isRead`, `isReadReceiptRequested`, `lastModifiedDateTime`, `multiValueExtendedProperties`, `parentFolderId`, `receivedDateTime`, `replyTo`, `sender`, `sentDateTime`, `singleValueExtendedProperties`, `subject`, `toRecipients`, `uniqueBody`, `webLink` + +#### Supported JMAP message properties + +The `_validateJMAPMessageProperties` method accepts these 4D email object (RFC 8621) keys: + +`from`, `to`, `cc`, `bcc`, `replyTo`, `sender`, `subject`, `sentAt`, `receivedAt`, `textBody`, `htmlBody`, `bodyValues`, `attachments`, `keywords`, `headers`, `messageId`, `inReplyTo`, `references`, `id`, `threadId`, `preview`, `size`, `hasAttachment`, `mailboxIds` + ### Office365.mail.append() **Office365.mail.append**( *email* : Object ; *folderId* : Text) : Object diff --git a/Project/Sources/Classes/Office365Mail.4dm b/Project/Sources/Classes/Office365Mail.4dm index fd7469fc..93d23840 100644 --- a/Project/Sources/Classes/Office365Mail.4dm +++ b/Project/Sources/Classes/Office365Mail.4dm @@ -133,7 +133,9 @@ Function _postMessage($inFunction : Text; $inURL : Text; $inMail : Variant; $bSk * @param {Object} $inHeader - Additional HTTP headers forwarded to `_postJSONMessage` * @returns {Object} Status object * @description Dispatches to `_postJSONMessage` or `_postMailMIMEMessage` based on `mailType` - * and the actual type of `$inMail`; throws error 10 on type mismatch + * and the actual type of `$inMail`; throws error 10 on type mismatch. + * JMAP objects are validated via `_validateJMAPMessageProperties` before conversion; + * Microsoft Graph objects are validated via `_validateGraphMessageProperties`. */ var $status : Object @@ -151,7 +153,11 @@ Function _postMessage($inFunction : Text; $inURL : Text; $inMail : Variant; $bSk $status:=This._postMailMIMEMessage($inURL; $inMail) : ((This.mailType="JMAP") && (Value type($inMail)=Is object)) - $status:=This._postMailMIMEMessage($inURL; $inMail) + If (This._validateJMAPMessageProperties($inMail; $inFunction)) + $status:=This._postMailMIMEMessage($inURL; $inMail) + Else + $status:=This._returnStatus() + End if : ((This.mailType="Microsoft") && (Value type($inMail)=Is object)) $status:=This._postJSONMessage($inFunction; $inURL; $inMail; $bSkipMessageEncapsulation; $inHeader) diff --git a/Project/Sources/Classes/_GraphAPI.4dm b/Project/Sources/Classes/_GraphAPI.4dm index fa92b13f..60faaa76 100644 --- a/Project/Sources/Classes/_GraphAPI.4dm +++ b/Project/Sources/Classes/_GraphAPI.4dm @@ -85,7 +85,8 @@ Function _validateGraphMessageProperties($inPayload : Object; $inFunction : Text * @returns {Boolean} `True` when all properties are supported; otherwise `False` * @description Validates mail payload keys before sending to Microsoft Graph. * Adds an error to the stack for each unsupported property (for example `attachment` - * instead of `attachments`). + * instead of `attachments`). Uses a Collection-based lookup for O(1)-style + * maintainability and readability over a `Case of` chain. */ var $isValid : Boolean:=True @@ -108,49 +109,20 @@ Function _validateGraphMessageProperties($inPayload : Object; $inFunction : Text $message:=$inPayload End if + var $allowedMessageKeys : Collection:=New collection(\ + "attachments"; "bccRecipients"; "body"; "bodyPreview"; \ + "categories"; "ccRecipients"; "changeKey"; "conversationId"; \ + "conversationIndex"; "createdDateTime"; "extensions"; "flag"; \ + "from"; "hasAttachments"; "id"; "importance"; "inferenceClassification"; \ + "internetMessageHeaders"; "internetMessageId"; "isDeliveryReceiptRequested"; \ + "isDraft"; "isRead"; "isReadReceiptRequested"; "lastModifiedDateTime"; \ + "multiValueExtendedProperties"; "parentFolderId"; "receivedDateTime"; \ + "replyTo"; "sender"; "sentDateTime"; "singleValueExtendedProperties"; \ + "subject"; "toRecipients"; "uniqueBody"; "webLink") + var $messageKey : Text For each ($messageKey; OB Keys($message)) - var $isAllowed : Boolean:=False - Case of - : ($messageKey="attachments") - : ($messageKey="bccRecipients") - : ($messageKey="body") - : ($messageKey="bodyPreview") - : ($messageKey="categories") - : ($messageKey="ccRecipients") - : ($messageKey="changeKey") - : ($messageKey="conversationId") - : ($messageKey="conversationIndex") - : ($messageKey="createdDateTime") - : ($messageKey="extensions") - : ($messageKey="flag") - : ($messageKey="from") - : ($messageKey="hasAttachments") - : ($messageKey="id") - : ($messageKey="importance") - : ($messageKey="inferenceClassification") - : ($messageKey="internetMessageHeaders") - : ($messageKey="internetMessageId") - : ($messageKey="isDeliveryReceiptRequested") - : ($messageKey="isDraft") - : ($messageKey="isRead") - : ($messageKey="isReadReceiptRequested") - : ($messageKey="lastModifiedDateTime") - : ($messageKey="multiValueExtendedProperties") - : ($messageKey="parentFolderId") - : ($messageKey="receivedDateTime") - : ($messageKey="replyTo") - : ($messageKey="sender") - : ($messageKey="sentDateTime") - : ($messageKey="singleValueExtendedProperties") - : ($messageKey="subject") - : ($messageKey="toRecipients") - : ($messageKey="uniqueBody") - : ($messageKey="webLink") - $isAllowed:=True - End case - - If (Not($isAllowed)) + If ($allowedMessageKeys.indexOf($messageKey)<0) This._pushError(12; {function: $inFunction; message: "Unsupported property \""+$messageKey+"\" in mail object."}) $isValid:=False End if @@ -164,6 +136,47 @@ Function _validateGraphMessageProperties($inPayload : Object; $inFunction : Text // ---------------------------------------------------- +Function _validateJMAPMessageProperties($inMail : Object; $inFunction : Text) : Boolean +/** + * @function _validateJMAPMessageProperties + * @private + * @param {Object} $inMail - 4D JMAP email object (as produced by `MAIL Convert from MIME`) + * @param {Text} $inFunction - Caller function name for error reporting + * @returns {Boolean} `True` when all properties are valid RFC 8621 / 4D JMAP properties; + * otherwise `False` + * @description Validates a JMAP mail object against the set of properties supported by + * 4D's `MAIL Convert to MIME` command (RFC 8621). Adds an error to the stack for each + * unsupported property. + */ + + var $isValid : Boolean:=True + + If (($inMail#Null) && (Value type($inMail)=Is object)) + + var $allowedKeys : Collection:=New collection(\ + "from"; "to"; "cc"; "bcc"; "replyTo"; "sender"; \ + "subject"; "sentAt"; "receivedAt"; \ + "textBody"; "htmlBody"; "bodyValues"; \ + "attachments"; "keywords"; "headers"; \ + "messageId"; "inReplyTo"; "references"; \ + "id"; "threadId"; "preview"; "size"; "hasAttachment"; "mailboxIds") + + var $key : Text + For each ($key; OB Keys($inMail)) + If ($allowedKeys.indexOf($key)<0) + This._pushError(12; {function: $inFunction; message: "Unsupported property \""+$key+"\" in JMAP mail object."}) + $isValid:=False + End if + End for each + + End if + + return $isValid + + + // ---------------------------------------------------- + + Function _loadFromObject($inObject : Object) /** * @function _loadFromObject From f65d9267fcc3f2ba9afda5243a9849304178f025 Mon Sep 17 00:00:00 2001 From: Yannick Trinh Date: Tue, 14 Jul 2026 16:46:02 +0200 Subject: [PATCH 3/3] Update UML Diagram and Office365 documentation - Adjusted edge paths in UML Diagram for better alignment. - Increased height of the _GraphAPI rectangle in UML Diagram for clarity. - Updated method signatures in the Office365 documentation to reflect changes in the _postJSONMessage method. - Removed outdated mail validation architecture section from Office365 documentation. --- Documentation/Assets/UML Diagram.svg | 22 +++++++------- Documentation/Classes/Office365.md | 43 ---------------------------- 2 files changed, 12 insertions(+), 53 deletions(-) diff --git a/Documentation/Assets/UML Diagram.svg b/Documentation/Assets/UML Diagram.svg index 83bf4c9d..9554e0db 100644 --- a/Documentation/Assets/UML Diagram.svg +++ b/Documentation/Assets/UML Diagram.svg @@ -37,18 +37,18 @@ text { font-family: 'Segoe UI', Arial, sans-serif; } - + - + - + - - - - + + + + _BaseAPI @@ -160,13 +160,15 @@ text { font-family: 'Segoe UI', Arial, sans-serif; } +appendRequest($inParam : Object) +constructor($inProvider : cs.OAuth2Provider; $inParam : Object) +sendRequestAndWaitResponse() : Collection - + _GraphAPI -_copyGraphMessage($inMessage : Object) : Object -_getURLParamsFromObject($inParameters : Object; $inCount : Boolean) : Text -_loadFromObject($inObject : Object) -+constructor($inProvider : cs.OAuth2Provider) +-_validateGraphMessageProperties($inPayload : Object; $inFunction : Text) : Boolean +-_validateJMAPMessageProperties($inMail : Object; $inFunction : Text) : Boolean ++constructor($inProvider : cs.OAuth2Provider) _GraphBaseList @@ -631,7 +633,7 @@ text { font-family: 'Segoe UI', Arial, sans-serif; } +mailType : Text +userId : Text --_postJSONMessage($inURL : Text; $inMail : Object; $bSkipMessageEncapsulation : Boolean; $inHeaders : Object) : Object +-_postJSONMessage($inFunction : Text; $inURL : Text; $inMail : Object; $bSkipMessageEncapsulation : Boolean; $inHeaders : Object) : Object -_postMailMIMEMessage($inURL : Text; $inMail : Variant) : Object -_postMessage($inFunction : Text; $inURL : Text; $inMail : Variant; $bSkipMessageEncapsulation : Boolean; $inHeader : Object) : Object +append($inMail : Variant; $inFolderId : Text) : Object diff --git a/Documentation/Classes/Office365.md b/Documentation/Classes/Office365.md index 857c984f..a833a474 100644 --- a/Documentation/Classes/Office365.md +++ b/Documentation/Classes/Office365.md @@ -596,49 +596,6 @@ The `event` object used with Microsoft Calendar methods includes the following m ## Mail -### Mail validation architecture - -When sending or appending a mail message, `Office365Mail` routes the payload through format-specific validation before the HTTP call is made. The diagram below shows the class relationships and the validation flow. - -```mermaid -classDiagram - class _GraphAPI { - +_validateGraphMessageProperties(payload, function) Boolean - +_validateJMAPMessageProperties(mail, function) Boolean - +_copyGraphMessage(message) Object - +_loadFromObject(object) - +_getURLParamsFromObject(parameters, count) Text - } - - class Office365Mail { - +mailType : Text - +userId : Text - +send(mail, options) Object - +append(mail, folderId) Object - +update(mailId, mail) Object - +reply(mailId, mail, options) Object - -_postMessage(function, url, mail, skipEncap, headers) Object - -_postJSONMessage(function, url, mail, skipEncap, headers) Object - -_postMailMIMEMessage(url, mail) Object - } - - _GraphAPI <|-- Office365Mail - - note for Office365Mail "_postMessage dispatch:\n• mailType=Microsoft → _validateGraphMessageProperties → _postJSONMessage\n• mailType=JMAP → _validateJMAPMessageProperties → _postMailMIMEMessage\n• mailType=MIME → _postMailMIMEMessage (no validation)" -``` - -#### Supported Microsoft Graph message properties - -The `_validateGraphMessageProperties` method accepts these Microsoft Graph [message resource](https://learn.microsoft.com/en-us/graph/api/resources/message?view=graph-rest-1.0) keys: - -`attachments`, `bccRecipients`, `body`, `bodyPreview`, `categories`, `ccRecipients`, `changeKey`, `conversationId`, `conversationIndex`, `createdDateTime`, `extensions`, `flag`, `from`, `hasAttachments`, `id`, `importance`, `inferenceClassification`, `internetMessageHeaders`, `internetMessageId`, `isDeliveryReceiptRequested`, `isDraft`, `isRead`, `isReadReceiptRequested`, `lastModifiedDateTime`, `multiValueExtendedProperties`, `parentFolderId`, `receivedDateTime`, `replyTo`, `sender`, `sentDateTime`, `singleValueExtendedProperties`, `subject`, `toRecipients`, `uniqueBody`, `webLink` - -#### Supported JMAP message properties - -The `_validateJMAPMessageProperties` method accepts these 4D email object (RFC 8621) keys: - -`from`, `to`, `cc`, `bcc`, `replyTo`, `sender`, `subject`, `sentAt`, `receivedAt`, `textBody`, `htmlBody`, `bodyValues`, `attachments`, `keywords`, `headers`, `messageId`, `inReplyTo`, `references`, `id`, `threadId`, `preview`, `size`, `hasAttachment`, `mailboxIds` - ### Office365.mail.append() **Office365.mail.append**( *email* : Object ; *folderId* : Text) : Object