Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 31 additions & 3 deletions Sources/Base44/Modules/IntegrationsModule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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: "<p>Hello!</p>", text: "Hello!")
///
/// // Dynamic invocation
/// let response = try await base44.integrations.invoke(package: "Core", endpoint: "InvokeLLM", data: ["prompt": "Hi"])
Expand Down Expand Up @@ -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))
Expand Down
43 changes: 43 additions & 0 deletions Tests/Base44Tests/IntegrationsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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: "<p>Hello</p>",
text: "Hello"
)

if let bodyData = MockURLProtocol.lastRequest?.httpBody {
let body = try JSONSerialization.jsonObject(with: bodyData) as? [String: Any]
XCTAssertEqual(body?["html"] as? String, "<p>Hello</p>")
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 {
Expand Down