PL/SQL package to talk to the AgentMail API from Oracle Database: list inboxes, list/fetch messages, send and reply, and download attachments into local tables.
Get your AgentMail API key from the AgentMail Console; all calls use Bearer token authentication.
- Oracle Database 12c or later (uses
JSON_TABLE,UTL_HTTP,JSON_OBJECT_T/JSON_ARRAY_T) - Oracle APEX installed in the database (for
apex_web_service,apex_util,apex_debug) - Network ACL that allows outbound HTTPS to AgentMail hosts (see Network ACL)
Run everything from the schema that will own agentmail (unless noted as DBA-only).
- (DBA) Configure network ACL
As a DBA (or privileged user) run
run_as_admin.sqlin the CDB/PDB that will host AgentMail: This grants the owning schema HTTP access to:
api.agentmail.tostorage.agentmail.tocdn.agentmail.to
- Configure basic settings (optional)
Edit
agentmail_config.sql(or create your ownsecrets.sql) to set values like: For production, prefer putting real values in a local, uncommittedsecrets.sqlthat overridesagentmail_config. - Install all AgentMail objects Connect as the target schema and run: This will:
- Run
agentmail_config.sql - Create utility helpers (
drop_object,does_table_exist,add_primary_key, etc.) viaagentmail_utils.sql - Create tables and type via
agentmail_schema.sql:agentmail_api_tokenagentmail_messagesagentmail_recipientsagentmail_attachmentsagentmail_message_detailagentmail_attachment_contentagentmail_inboxeslabel_arraytype
- Compile the
agentmailpackage spec/body - Compile the
agentmail_testpackage (smoke tests) - Optionally run
secrets.sqlat the end if the file exists
- Store your API key
After install, update the default token row in
agentmail_api_token(see Storing your API key).
The package uses the **agentmail_api_token** table and the **agentmail.get_default_token** function for authentication.
- Find or create the default token row
The install scripts create
agentmail_api_tokenwith primary keytoken_name. You can insert or update a row named'DEFAULT': - How authentication works
agentmail.get_default_tokenreadstoken_valuefrom the'DEFAULT'row.- Each API call sets the header
Authorization: Bearer <get_default_token()>. - You do not need to set any package-level
api_keyvariable; the table is the single source of truth.
- Keeping secrets out of git
- Never commit real API keys into SQL scripts.
- Use a
secrets.sqlfile that is not committed and is already included in.gitignore. - In your deployment pipeline you can run an update similar to the merge above from a secure location.
Public members (simplified):
| Member | Type | Description |
|---|---|---|
**last_inbox_id** |
variable | Last created inbox ID from create_inbox. |
**last_response_json** |
variable | Last HTTP response body (CLOB). |
**last_status_code** |
variable | Last HTTP status code. |
**last_response_message** |
variable | Short message / summary for the last call. |
**get_default_token** |
function | Returns the token_value for 'DEFAULT' from agentmail_api_token. |
**list_messages** |
procedure | GET /v0/inboxes/{inbox_id}/messages → upserts into agentmail_messages, agentmail_recipients, agentmail_attachments. |
**get_message** |
procedure | GET /v0/inboxes/{inbox_id}/messages/{message_id} → upserts into agentmail_message_detail and related tables. |
**get_attachment** |
procedure | GET attachment metadata, then download binary via download_url → upserts into agentmail_attachment_content. |
**list_inboxes** |
procedure | GET /v0/inboxes → upserts into agentmail_inboxes. |
**create_inbox** |
procedure | POST /v0/inboxes → creates a new inbox and merges it into agentmail_inboxes. |
**send_message** |
procedure | POST /v0/inboxes/{inbox_id}/messages/send to send an email. |
**reply_to_message** |
procedure | POST /v0/inboxes/{inbox_id}/messages/{message_id}/reply to reply to a message. |
**parse_list_messages_response** |
procedure | Internal helper used by list_messages. |
**parse_list_inboxes_response** |
procedure | Internal helper used by list_inboxes. |
**agentmail_messages**– Inbox message metadata (inbox_id, thread_id, message_id, labels JSON string, subject, preview, created/updated timestamps, etc.).**agentmail_message_detail**– Full message payload fromget_message(bodies, headers, recipients JSON, attachments JSON, etc.).**agentmail_recipients**– NormalizedTO/CC/BCCrecipients per message (one row per recipient).**agentmail_attachments**– Attachment metadata per message (filename, content type, content_id, size, etc.).**agentmail_attachment_content**– Downloaded attachment binary and metadata fromget_attachment(BLOB content).**agentmail_inboxes**– Inboxes returned bylist_inboxesandcreate_inbox.**agentmail_api_token**– API tokens keyed bytoken_name(usually just'DEFAULT').**label_array**– Nested table type used for label and address arrays.
List inboxes
BEGIN
agentmail.list_inboxes(p_limit => 50);
END;
/ List recent messages for an inbox
DECLARE
l_inbox_id VARCHAR2(100) := agentmail_config.default_inbox_id;
BEGIN
agentmail.list_messages(
p_inbox_id => l_inbox_id,
p_limit => 20,
p_after => SYSTIMESTAMP - INTERVAL '1' DAY
);
END;
/ Fetch a single message and its detail
DECLARE
l_inbox_id VARCHAR2(100) := agentmail_config.default_inbox_id;
l_message_id VARCHAR2(100) := 'msg_xxx';
BEGIN
agentmail.get_message(
p_inbox_id => l_inbox_id,
p_message_id => l_message_id
);
END;
/ Send a message
DECLARE
l_inbox_id VARCHAR2(100) := agentmail_config.default_inbox_id;
BEGIN
agentmail.send_message(
p_inbox_id => l_inbox_id,
p_email => l_inbox_id,
p_subject => 'Test from PL/SQL',
p_body => 'Hello from PL/SQL',
p_to_email => 'someone@example.com',
p_labels => label_array('outbound', 'test')
);
END;
/ Reply to a message
DECLARE
l_inbox_id VARCHAR2(100) := agentmail_config.default_inbox_id;
l_message_id VARCHAR2(100);
BEGIN
-- Example: pick a recent 'received' message from AGENTMAIL_MESSAGES
SELECT MAX(message_id) KEEP (DENSE_RANK LAST ORDER BY NVL(timestamp_, created_at) NULLS LAST)
INTO l_message_id
FROM agentmail_messages
WHERE labels IS NOT NULL
AND (',' || REPLACE(REPLACE(REPLACE(REPLACE(labels, '[', ''), ']', ''), '"', ''), ' ', '') || ','
LIKE '%,received,%');
agentmail.reply_to_message(
p_inbox_id => l_inbox_id,
p_message_id => l_message_id,
p_text => 'Reply from Oracle PL/SQL',
p_reply_all => 'Y',
p_labels => label_array('reply', 'test')
);
END;
/ The agentmail_test package provides a small integration test harness.
Run all smoke tests
BEGIN
agentmail_test.run_all(
p_inbox_id => agentmail_config.test_inbox_id
);
END;
/ This will:
- List inboxes via
agentmail.list_inboxes - List messages for
p_inbox_id - Fetch the latest message
- Send a reply to that message
- Download the latest attachment (if present)
Individual procedures are also available:
smoke_list_messages(p_inbox_id)smoke_get_latest_message(p_inbox_id)smoke_reply_latest_message(p_inbox_id, p_text)smoke_download_latest_attachment(p_inbox_id)smoke_list_inboxes(p_limit)
There is also a agentmail_test_suite_beta procedure that exercises send_message against a hard-coded test inbox for ad‑hoc testing.
The run_as_admin.sql script must be run by a DBA (or equivalent). It:
- Creates or reuses an ACL
agentmail_http_acl.xml. - Grants
connectandresolveprivileges to the AgentMail-owning schema (&principal). - Assigns the ACL to
api.agentmail.to. - Adds host ACEs so
UTL_HTTPcan call:
api.agentmail.to:443storage.agentmail.to:443cdn.agentmail.to:443
You can verify ACL grants with the final SELECT in run_as_admin.sql (querying dba_host_aces).
Without this step, REST calls and attachment downloads will fail with UTL_HTTP / network ACL errors.
To drop all AgentMail objects (but not the network ACL), connect as the owning schema and run:
@agentmail_drop.sqlThis drops:
agentmailpackage (spec and body)agentmail_testandagentmail_test_suite_betaagentmail_config- All AgentMail tables and type (
agentmail_*,label_array) - Utility helpers (
drop_table,add_primary_key,does_*)
If you also want to remove the network ACL itself, that must be done manually as a DBA.
- AgentMail – email for agents and APIs
- AgentMail API / Console – API key and docs
- API base URL:
https://api.agentmail.to/v0