Behavioural Differences from Oracle¶
Everything a migrated call actually does — sending mail, splitting recipients, routing bodies — matches Oracle. This page is the honest list of what's genuinely different underneath, because a compatibility shim is only useful if you know exactly where the compatibility ends.
Recipient lists and validation¶
Oracle recipient lists are comma-separated VARCHAR2; utl_mail.* accepts exactly that (semicolons are also tolerated), split internally the same way pg_relay_notifier's own send_mail() splits them. Oracle raises ORA-24244 on an invalid recipient; here, a named-field exception is raised at compose time instead — different error text, same "reject the bad address before anything is queued" behaviour.
Return values — a bonus, not a requirement¶
Real Oracle UTL_MAIL.SEND/SEND_ATTACH_RAW/SEND_ATTACH_VARCHAR2 are procedures — they return nothing, and migrated code never captures a value from calling them. utl_mail.* functions here return bigint: the real notification_id (globally unique, pgrelay._next_id()-generated — see Multi-Master and Notification IDs), not a discarded value or a placeholder constant. This is a bonus for new, Postgres-native code that wants to poll pgrelay_notifier.get_status(id) afterward. Migrated code that never assigns the result is completely unaffected either way — it just calls the function as a statement, exactly as it always has.
Synchronous vs. queued — the one difference worth actually thinking about¶
This is the behavioural difference most likely to matter to migrated error-handling code, so read it carefully.
Real Oracle UTL_MAIL.SEND is synchronous. It sends inside the calling session and raises an exception immediately if the SMTP call fails — migrated code that wraps the call in an exception handler and reacts to a failure is reacting to something that happened during that statement.
utl_mail.* here enqueues and returns immediately. The call commits with your transaction; delivery happens afterwards, within about a second, by the pg_relay Processor. A delivery failure surfaces later, via pgrelay_notifier.get_status(id) — never as an exception from the utl_mail.send(...) call itself, because by the time the failure is known, the call has long since returned successfully.
This is a strict reliability upgrade — Oracle can lose an email outright if the SMTP call succeeds but the enclosing transaction later rolls back; here the message and the transaction commit or roll back together, and delivery is retried automatically on transient failure (see Failure Classification and Status Lifecycle). But it does mean: migrated code with an exception handler around UTL_MAIL.SEND expecting to catch a send failure will not see one. The call succeeds at enqueue time regardless of what happens to the message afterward. If that error-handling logic matters, it needs to become a separate check against get_status(), not an exception handler around the call.
Multiple attachments¶
Oracle UTL_MAIL supports exactly one attachment per call. pg_relay_notifier's own two-phase compose()/attach()/dispatch() API supports any number, but utl_mail.* deliberately never exposes that — it exists purely for signature-level fidelity to Oracle, not as a general-purpose sending API. If migrated code needs more than one attachment, that's a sign it should move to pg_relay_notifier's own functions directly (see the User Guide) rather than staying on the UTL_MAIL-compatible surface.
Priority and inline disposition on the wire¶
pg_relay v1.1's transports don't yet emit an X-Priority header or an inline content disposition. Both values are preserved end-to-end in the underlying message JSON (priority, attachments[].inline) as documented forward-compatible keys, so a future pg_relay release can start honouring them with no change to either extension — migrated code that sets priority isn't wasting the argument, it's just not visible on the wire yet.
Charsets¶
Oracle's mime_type charset annotations (; charset=us-ascii, and similar) are accepted verbatim. Bodies are stored as PostgreSQL text (UTF-8), and the Processor sends UTF-8 with quoted-printable encoding, Q-encoding headers as needed — a strict superset of Oracle's us-ascii default, never a narrower one.