From 93e590d5d24ed5bccfd38ff356cba38d7082f6d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Egidijus=20Jucevi=C4=8Dius?= Date: Tue, 1 Sep 2026 10:34:46 +0300 Subject: [PATCH] feat(integrations): add html and text params to sendEmail Mirrors the backend change in base44-dev/apper#22289. body is relaxed from required to optional; html (alias of body) and text (plain-text part) are added as optional parameters. Only non-nil fields are included in the JSON payload. Doc comments updated with truth table of valid/422 combinations. Adds testSendEmailWithHtmlAndText and testSendEmailTextOnly tests. Co-Authored-By: Claude Sonnet 4.6 --- .../Base44/Modules/IntegrationsModule.swift | 34 +++++++++++++-- Tests/Base44Tests/IntegrationsTests.swift | 43 +++++++++++++++++++ 2 files changed, 74 insertions(+), 3 deletions(-) diff --git a/Sources/Base44/Modules/IntegrationsModule.swift b/Sources/Base44/Modules/IntegrationsModule.swift index 96c87f8..5181db5 100644 --- a/Sources/Base44/Modules/IntegrationsModule.swift +++ b/Sources/Base44/Modules/IntegrationsModule.swift @@ -13,7 +13,7 @@ import Foundation /// let uploaded = try await base44.integrations.core.uploadFile(data: imageData, filename: "photo.png", mimeType: "image/png") /// /// // Send email -/// try await base44.integrations.core.sendEmail(to: "user@example.com", subject: "Hi", body: "Hello!") +/// try await base44.integrations.core.sendEmail(to: "user@example.com", subject: "Hi", html: "

Hello!

", text: "Hello!") /// /// // Dynamic invocation /// let response = try await base44.integrations.invoke(package: "Core", endpoint: "InvokeLLM", data: ["prompt": "Hi"]) @@ -148,8 +148,36 @@ public struct CoreIntegrations: Sendable { // MARK: - Email /// Sends an email. - public func sendEmail(to: String, subject: String, body: String, from: String? = nil) async throws -> JSONObject { - var payload: JSONObject = ["to": to, "subject": subject, "body": body] + /// + /// At least one of `body`, `html`, or `text` is required; the backend returns 422 otherwise. + /// + /// | Fields set | Result | + /// |---|---| + /// | `body` or `html` alone | `text/html` email | + /// | `text` alone | `text/plain` email | + /// | `body`/`html` + `text` | `multipart/alternative` — one email, two representations; recipient sees whichever their client prefers — both parts must say the same thing | + /// | `body` + `html` | **422** — same slot, set one not both | + /// + /// - Parameters: + /// - to: Recipient email address. + /// - subject: Email subject line. + /// - body: HTML email body content. Alias of `html` — set one or the other, never both. + /// - html: HTML email body content. The same thing as `body` under an explicit name. + /// - text: Plain-text email body content. On its own sends a `text/plain` email; alongside + /// `body`/`html` produces a `multipart/alternative` email — both parts must say the same thing. + /// - from: The name of the sender. If omitted, the app's name will be used. + public func sendEmail( + to: String, + subject: String, + body: String? = nil, + html: String? = nil, + text: String? = nil, + from: String? = nil + ) async throws -> JSONObject { + var payload: JSONObject = ["to": to, "subject": subject] + if let b = body { payload["body"] = b } + if let h = html { payload["html"] = h } + if let t = text { payload["text"] = t } if let f = from { payload["from"] = f } let bodyData = try JSONSerialization.data(withJSONObject: payload) let data = try await http.post(path: corePath("SendEmail"), body: RawJSONEncodable(data: bodyData)) diff --git a/Tests/Base44Tests/IntegrationsTests.swift b/Tests/Base44Tests/IntegrationsTests.swift index a1fb846..51ba548 100644 --- a/Tests/Base44Tests/IntegrationsTests.swift +++ b/Tests/Base44Tests/IntegrationsTests.swift @@ -170,6 +170,49 @@ final class IntegrationsTests: XCTestCase { } } + func testSendEmailWithHtmlAndText() async throws { + MockURLProtocol.mockJSONObject( + path: "/api/apps/\(appId)/integration-endpoints/Core/SendEmail", + object: ["sent": true] + ) + + let client = makeClient() + _ = try await client.integrations.core.sendEmail( + to: "user@example.com", + subject: "Multipart", + html: "

Hello

", + text: "Hello" + ) + + if let bodyData = MockURLProtocol.lastRequest?.httpBody { + let body = try JSONSerialization.jsonObject(with: bodyData) as? [String: Any] + XCTAssertEqual(body?["html"] as? String, "

Hello

") + XCTAssertEqual(body?["text"] as? String, "Hello") + XCTAssertNil(body?["body"], "body should not be sent when html is used") + } + } + + func testSendEmailTextOnly() async throws { + MockURLProtocol.mockJSONObject( + path: "/api/apps/\(appId)/integration-endpoints/Core/SendEmail", + object: ["sent": true] + ) + + let client = makeClient() + _ = try await client.integrations.core.sendEmail( + to: "user@example.com", + subject: "Plain text", + text: "Hello plain" + ) + + if let bodyData = MockURLProtocol.lastRequest?.httpBody { + let body = try JSONSerialization.jsonObject(with: bodyData) as? [String: Any] + XCTAssertEqual(body?["text"] as? String, "Hello plain") + XCTAssertNil(body?["html"]) + XCTAssertNil(body?["body"]) + } + } + // MARK: - extractDataFromUploadedFile func testExtractDataFromUploadedFile() async throws {