Documentation • Public addon integration
MailboxGUI Developer API
MailboxGUI 6.1.0 • Shared API 2.0 • Premium read-only network API 1
The shared API works with Free and Premium. Every network feature requires Premium. Private DiscBridge coordination is excluded.
Source-reviewed October 10, 2026 • 50 shared public types and 9 Premium read-only network contracts.
Getting started
Contract, editions & dependencies
This is the addon-facing API guide for MailboxGUI 6.1.0. The shared contract reports API_VERSION 2.0; the separate Premium read-only network contract reports version 1. Plugin release numbers and API contract versions are different. Free and Premium provide the shared API; every cross-backend network feature requires Premium.
Compile against io.github.rismr1:mailboxgui-api:6.1.0 with provided scope (Gradle compileOnly). The shared 6.1.0 artifact is available from Maven Central, verified during this audit. Compile for Java 17 or the server-required baseline. Never shade or relocate MailboxGUI’s API classes: service discovery must use the same runtime class identities. MailboxGUI supplies the runtime API; an addon does not install another plugin named mailboxgui-api.
<dependency>
<groupId>io.github.rismr1</groupId>
<artifactId>mailboxgui-api</artifactId>
<version>6.1.0</version>
<scope>provided</scope>
</dependency>repositories { mavenCentral() }
dependencies {
compileOnly("io.github.rismr1:mailboxgui-api:6.1.0")
}name: ExampleAddon
version: 1.0.0
main: example.ExampleAddon
api-version: "1.18"
depend: [MailboxGUI]Use depend when your addon cannot operate without MailboxGUI. Use softdepend and a graceful null check for an optional integration, and keep API-dependent class loading out of the absent-plugin path. The optional Premium network artifact is mailboxgui-network-api:6.1.0 with the same group and provided/compileOnly scope; do not require it for a Free-only addon. Its 6.1.0 coordinate was not available from Maven Central during this audit: obtain the matching module from the developer or build/install it locally. Do not assume adding mavenCentral() alone resolves the network artifact.
Shared API 6.1.0 Maven Central artifact directoryGetting started
Service discovery, threads & authorization
Java snippets are method-body examples inside your JavaPlugin. player is an authorized Player, mailId is a previously obtained real mail ID, api is a discovered MailboxGUIApi and letter is a previously returned MailboxSendResult. Validate type/ownership before acting. Use the imports below for the examples; the network import is only needed for Premium integration.
import com.rismr1.mailboxgui.api.*;
import com.rismr1.mailboxgui.api.model.*;
import com.rismr1.mailboxgui.api.request.*;
import com.rismr1.mailboxgui.api.result.*;
import com.rismr1.mailboxgui.api.type.*;
import com.rismr1.mailboxgui.api.message.MailMessageRules;
import com.rismr1.mailboxgui.api.spi.MailNotificationProvider;
import com.rismr1.mailboxgui.api.network.*; // Optional Premium artifact only
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import java.time.Duration;
import java.util.*;MailboxGUIApi api = MailboxGUIApiProvider.get();
if (api == null) {
getLogger().warning("MailboxGUI service is unavailable; integration disabled.");
return;
}
MailboxApiInfo info = api.getApiInfo();
if (!info.supports(ApiCapability.INBOX_QUERY)) {
getLogger().warning("This provider does not support inbox queries.");
return;
}Discover the service after MailboxGUI enables. Bukkit ServicesManager.load(MailboxGUIApi.class) is equivalent to the provider helper. Treat a missing service as unavailable. Runtime capabilities describe the active provider, not merely an installed jar; older providers can throw UnsupportedOperationException for optional service accessors. Do not infer readiness from an edition string alone.
Calls that query MailboxGUI state, send mail, mutate mail, register providers or report pack status belong on Bukkit’s primary server thread. Marshal asynchronous database/HTTP completions with your plugin scheduler before calling the API. Public Premium synchronous network reads are cache-only; they do not authorize asynchronous Bukkit inventory work. Do not block the server thread with HTTP requests.
Use a current service reference and guard against disable/reload. ItemStacks returned by requests/snapshots are defensive copies, not live editing handles. Obtain fresh data after a mutation or runtime transition rather than retaining stale revisions.
| Public capability | What to check |
|---|---|
| INBOX_QUERY / TRANSACTIONAL_MUTATION | inbox snapshots / revision-aware mutation services |
| DELIVERY_TRACKING / SCHEDULED_MAIL | pending delivery queries / task management |
| MAILBOX_REGISTRY / LIFECYCLE_EVENTS | registration snapshots / public lifecycle events |
| CUSTOM_ITEM_CODECS / NOTIFICATION_PROVIDERS | custom item transforms / generic notification hooks |
| ATTACHED_MESSAGES / MESSAGE_VALIDATION | message attachments / shared paper and attachment validation |
| RESOURCE_PACK_STATUS | provider-owned local pack-load confirmations in both editions |
| VAULT_ECONOMY | a working economy hook is currently available; recheck actual operation results |
| ITEM_CURRENCY_COD / PACKAGE_EXPERIENCE | item-currency COD / XP rewards enabled by configuration |
| NETWORK_DELIVERY_TARGETS — Premium only | Premium targeting contract, not proof that network mode is enabled, ready or healthy |
getApiInfo().primaryThreadRequired() is true. Calling send APIs or stateful service methods asynchronously can throw IllegalStateException instead of returning a result enum. There is no public grant of unrestricted async access just because a synchronous network snapshot is cache-only. Register/unregister your extension providers on the server thread as part of your plugin lifecycle.
Sending mail
Recipients & common builder fields
| Field or policy | Meaning |
|---|---|
| recipient(UUID) / recipient(UUID, name) / recipient(String) / recipients(list) | Select real player UUIDs or names; findRecipientByName returns Optional. Resolve identity deliberately and check failures. |
| REGISTERED_MAILBOX_ONLY | Require a registered personal mailbox. |
| KNOWN_MAILBOXGUI_PLAYER | Default policy: require existing known MailboxGUI player data. |
| CREATE_IF_MISSING | Trusted addon may create recipient data only when a UUID is supplied; a name-only unknown recipient is not created. This is not an identity-verification or permission shortcut. |
| fromDisplayName / sourcePluginName | Displayed sender and addon origin. A cosmetic sender name is not a player identity for COD returns. |
| deliveryDelaySeconds | Default -1 inherits that mail type’s delay; 0 requests immediate delivery; positive values request a delay. |
| notifyRecipients | Defaults true. Controls ordinary delivery notification dispatch, not entitlement or item validity. |
| expirationDurationSeconds / expiresAfter(Duration) | Zero seconds means no expiration; omit expiresAfter for none. expiresAfter requires a positive duration of at least one second. Delayed mail starts expiration at actual delivery, not enqueue time. |
| networkDeliveryTarget | Defaults to wholeNetwork. Explicit backend/group restrictions are Premium network features and are validated at send/claim time. |
API/scheduled sending is distinct from the player-command and Admin Tools send toggles. Do not assume a disabled /mb package command disables trusted addon delivery. Economy, item, message, storage, recipient and active-network safety checks still apply.
MailboxApiRecipient.uuid(UUID), name(String) and of(UUID, String) create recipient values. Empty names, null required UUIDs and missing identities can throw at construction. findRecipientByName searches existing MailboxGUI data, not an authoritative Minecraft account registry. A UUID/name pair must refer to the same real player: resolution can fall back to the supplied name when UUID data is absent. Prefer an authenticated UUID; Citizens NPCs cannot receive player mail. Recipient builder calls append rather than deduplicate, so remove duplicate business recipients yourself.
Request construction and dispatch are separate validation stages. Package/money attachments can throw IllegalArgumentException during message()/build(); expiration helpers can throw for negative, sub-second or overflowing durations. Preserve raw /n syntax and catch construction errors before calling send. Default expiration is zero, and positive Duration values use whole seconds. Blank optional attachments become absent; a paper letter still needs a nonblank valid body.
List<MailboxApiRecipient> recipients = List.of(
MailboxApiRecipient.uuid(player.getUniqueId()));
LetterMailRequest request = LetterMailRequest.builder()
.recipients(recipients)
.recipientPolicy(RecipientPolicy.REGISTERED_MAILBOX_ONLY)
.message("A notice for registered mailbox owners.")
.sourcePluginName(getName())
.build();
MailboxSendResult sent = api.sendLetter(request);Sending mail
Paper letters & book letters
MailboxSendResult letter = api.sendLetter(LetterMailRequest.builder()
.recipient(player.getUniqueId())
.fromDisplayName("&aQuest Office")
.message("Thank you!/nYour reward is ready.")
.deliveryDelaySeconds(0)
.expiresAfter(Duration.ofDays(7))
.sourcePluginName(getName())
.build());
MailboxSendResult book = api.sendBookLetter(BookLetterMailRequest.builder()
.recipient(player.getUniqueId())
.title("Welcome")
.message("Welcome to our server. Read the rules before exploring.")
.sourcePluginName(getName())
.build());Paper letters use message. BookLetterMailRequest uses title and message; there is no pages builder field. MailboxGUI builds the readable book and its controlled information/reply/navigation. Paper/attached-message limits do not describe arbitrary book-page input. The complete builder and getter contracts are in the reference below.
Both book title and body must be nonblank; a blank value returns INVALID_REQUEST. Failure to create the readable item returns SERIALIZATION_FAILED. Book-letter sends and pre-send metadata use MailType.LETTER, while delivery events distinguish MailDeliveryType.BOOK_LETTER. Do not look for a separate BOOK_LETTER value in MailType.
Sending mail
Packages, XP & COD
MailboxSendResult reward = api.sendPackage(PackageMailRequest.builder()
.recipient(player.getUniqueId())
.addItem(new ItemStack(Material.DIAMOND, 2))
.experienceAmount(25)
.message("Quest reward/nEnjoy!")
.deliveryDelaySeconds(0)
.sourcePluginName(getName())
.build());
MailboxSendResult cod = api.sendPackage(PackageMailRequest.builder()
.recipient(player.getUniqueId())
.addItem(new ItemStack(Material.EMERALD, 1))
.codAmount(50.0)
.addCodItemCurrency(new ItemStack(Material.IRON_INGOT, 4))
.codReturnDestination(CodReturnDestination.ADMIN_INBOX)
.returnCodPaymentToAdminInbox(true)
.returnDeclinedOrExpiredToAdminInbox(true)
.sourcePluginName(getName())
.build());getPackageContentLimit reports capacity: the current GUI has 28 content positions. XP is raw experience points, not levels, and its synthetic reward entry uses capacity. Check the actual request result rather than silently trimming stacks. ItemStack metadata is preserved; nested mailbox-package payloads are rejected, loaded shulkers follow configuration, and Premium mixed-version nested payloads need compatible manifests/codecs.
Positive experienceAmount requires the configured package-experience feature; otherwise the send returns DISABLED_BY_CONFIG. An XP reward can make an otherwise item-empty package nonempty. This trusted API adds the specified reward; it does not automatically withdraw those points from an addon-selected sender. Implement any intended funding check separately.
Monetary COD needs Vault and an economy provider; item-only currency does not. Mixed prices require all configured payment components. senderUuid supplies the actual sender UUID when returning to SENDER; fromDisplayName alone cannot route a return. CODReturnDestination is NONE, SENDER or ADMIN_INBOX. Payment-to-admin and declined/expired-return switches are separate policies: set them intentionally and inspect validation results.
contents(list) replaces package contents; addItem appends. codItemCurrencyContents(list) replaces the item price; addCodItemCurrency appends; clearCodItemCurrencyContents removes that part of the price. Null/air stacks are ignored and stored stacks cloned. returnCodPaymentToSender(true) is the legacy alias for SENDER; false sets NONE. The last destination-setting builder call wins. ADMIN_INBOX must be enabled where requested; invalid combinations produce the matching admin-inbox/return-configuration failure.
Sending mail
Money mail & message validation
MailboxSendResult money = api.sendMoney(MoneyMailRequest.builder()
.recipient(player.getUniqueId())
.amount(50.0)
.message("Event reward")
.sourcePluginName(getName())
.build());
MailMessageRules.Validation validation = MailMessageRules.validate("Welcome/nHave fun!");
if (!validation.valid()) getLogger().warning(validation.reason());Use a finite positive amount and a working Vault economy. Handle ECONOMY_UNAVAILABLE or other recipient failures; a formatted currency string is not the amount field. Sending addon money is a trusted administrative reward action, not proof that your addon charged a player: implement any intended funding/authorization separately.
MailMessageRules normalizes /n to a newline and //n to literal /n, accepts actual API/YAML newlines and enforces 256 visible Unicode characters, wrapped width 28 and 17 total lines including blank/trailing breaks. Formatting-only input is also bounded (8192 raw characters). Validation exposes valid, reason, visibleCharacters, lines, manualBreaks and remainingLines(). Remaining lines are max(0, 17 − rendered lines), not a separate allowance of 16 /n tags. Reasons are OK, EMPTY, INPUT_TOO_LONG, TOO_LONG or TOO_MANY_LINES. Long words and non-Latin text wrap without truncation; colors do not consume visible character budget.
MailMessageRules.isValid(raw) is the boolean shortcut. wrapColored(raw) returns wrapped legacy-colored lines, and normalize(raw) decodes line-break syntax. Keep raw input for validate/wrapColored: normalizing twice can turn a literal //n into a break. These rules apply to paper and attachments, not the separate book-letter page rules.
MailTextFormatting.supportedFormats() returns legacy, hex and minimessage-visual. legacy(text) converts supported visual formatting; plain(text) strips formatting without decoding /n; replaceIsolated(template, placeholder, value) inserts separately formatted text while restoring the template’s surrounding style. MiniMessage support is deliberately visual: do not promise click, hover, font or arbitrary interactive tags.
Sending mail
Results, IDs & safe retries
for (MailboxRecipientResult result : letter.getRecipientResults()) {
getLogger().info(result.getStatus().name());
}
getLogger().info("Delivered=" + letter.getDeliveredRecipients()
+ ", queued=" + letter.getQueuedRecipients()
+ ", failed=" + letter.getFailedRecipients());MailboxSendResult exposes per-recipient results, warnings and requested/delivered/queued/failed counts. isSuccessful means all requested recipients were DELIVERED or QUEUED; it does not mean every recipient already has delivered mail. Multi-recipient requests can partially succeed. RecipientStatus lists all send outcomes; ApiOperationStatus is the separate query/mutation/service result vocabulary.
Immediate results expose a mailId and expiration timestamp; delayed results expose a queueId and requested expiration duration. Never use a delay-queue ID as an inbox mail ID. Delayed tracking is not the already-delivered inbox overflow queue. Retain your own business transaction identity: the send API does not provide a universal exactly-once idempotency key.
getDeliveredMailIds(), getQueuedDeliveryIds() and getSuccessfulRecipientMailIds() are convenience views over returned IDs; the recipient map has one value per UUID and cannot represent repeated deliveries to the same recipient. hasExpiringDeliveries() checks the result’s expiration metadata. getRequestedRecipients() is the number of returned recipient-result rows: an early whole-request rejection produces one failure row with no recipient identity, not one row per submitted recipient. Keep your original requested audience separately for reporting.
For REVISION_CONFLICT or BUSY, re-read current state and retry only an authorized still-needed action. For an ambiguous storage/API failure, reconcile whether the effect committed before repeating it. Never blindly repeat grants or assume a timeout rolled back external Vault effects. Report bounded useful status information without logging private message bodies, raw item payloads or credentials.
| Send result family (RecipientStatus) | Action |
|---|---|
| DELIVERED / QUEUED | Persist the returned mailId or queueId and distinguish delivery from scheduling. |
| RECIPIENT_NOT_FOUND / NO_REGISTERED_MAILBOX / MAILBOX_DATA_MISSING | Resolve the correct identity and recipient policy; do not bypass ownership. |
| INVALID_REQUEST / INVALID_EXPIRATION / TOO_MANY_CONTENTS / EMPTY_PACKAGE | Fix the request or payload; construction errors may happen before a result exists. |
| DISABLED_BY_CONFIG / ECONOMY_UNAVAILABLE / ADMIN_INBOX_DISABLED | Check current server settings and dependencies. |
| INVALID_COD_RETURN_DESTINATION / INVALID_COD_RETURN_CONFIGURATION | Correct sender UUID, return destination and independent return flags. |
| NESTED_PACKAGE_BLOCKED / NON_EMPTY_SHULKER_BLOCKED / SERIALIZATION_FAILED | Correct item contents, preservation and configured safety policy. |
| NETWORK_MODE_REQUIRED / UNKNOWN_BACKEND / UNKNOWN_BACKEND_GROUP / NO_COMPATIBLE_BACKEND / BACKEND_TARGET_BLOCKED | Premium only: verify enabled networking and actual configured backend targets. |
| ITEM_UNSUPPORTED_ON_TARGET_BACKEND / COD_PAYMENT_UNSUPPORTED_ON_TARGET_BACKEND / MAIL_RESTRICTED_TO_OTHER_BACKEND | Premium only: do not strip compatibility or backend restrictions to force delivery. |
| CANCELLED_BY_EVENT / INBOX_WRITE_FAILED | Respect cancellation; reconcile uncertain storage effects before repeating rewards. |
Enum declarations include compatibility/future outcomes; not every listed value is emitted by every operation. Use status enums and result counts as the machine contract, not localized/human-readable messages. Zero expiration timestamps mean no absolute expiration; all *Millis fields are Unix milliseconds, while duration/delay fields named Seconds are seconds.
Mail management
Inbox queries & transactional mutations
UUID owner = player.getUniqueId();
InboxPage inbox = api.inbox().query(owner, InboxQuery.firstPage());
Optional<InboxMailEntry> found = api.inbox().findById(owner, mailId);
if (found.isPresent()) {
InboxMailEntry entry = found.get();
ApiOperationStatus opened = api.inbox().openPackageManagement(
owner, entry.mailId(), entry.revision(), player, 0);
getLogger().info(opened.name());
}InboxQuery uses zero-based pages, clamps page size to 1–100 and treats an empty mailTypes set as all types. SortOrder is NEWEST_FIRST, OLDEST_FIRST, TYPE_THEN_NEWEST or EXPIRING_FIRST. includeQueued includes persistent inbox overflow, NOT the delayed delivery queue. includeExpired determines whether expired entries are included. firstPage uses 36 entries with overflow/expired included.
InboxMailEntry is an immutable metadata/item snapshot with mailId, type/status, sender/time/expiry, unread/overflow flags, attached message, package/money summaries, origin backend and revision. Use the owner UUID AND a fresh revision for delete, claimMoney, claimPackageContents and openPackageManagement. Mutations return MailMutationResult (or ApiOperationStatus for opening) and do not edit a snapshot in place.
delete takes a reason; claim methods take the acting Player. MailMutationResult exposes status(), mailId(), mailType(), revision(), remainingMail() and message(); successful() is true only for SUCCESS. remainingMail is the remaining mailbox item count, not the number of stacks left in the claimed package. A revision is an opaque concurrency value, not a timestamp/counter. expectedRevision=0 is the legacy unchecked path: pass the fresh entry.revision() instead of using zero to bypass stale-state protection.
These are trusted addon operations. Authorize owner/actor IDs yourself before queries, deletes and money payouts; claimMoney does not by itself establish that the acting Player owns the supplied mailbox UUID. Package content claims require the owner or permitted administrative inbox editing. openPackageManagement requires the owner Player and uses a zero-based returnPage (negative values become zero). Do not expose a user-controlled mailbox UUID to a reward or payout command.
The backend rechecks expiration, locks, routing, compatibility, cancellation and revision as appropriate. Inventory or storage failure can occur after part of an effect committed; reconcile the stored item and payout before recovery rather than automatically claiming again. A SUCCESS opening the GUI is not a successful claim. MailLifecycleStatus and ApiOperationStatus describe different things.
| Operation result family (ApiOperationStatus) | Handling |
|---|---|
| SUCCESS | Inspect the resulting snapshot/counts; opening a GUI is not a payout. |
| NOT_FOUND / ALREADY_COMPLETED / REVISION_CONFLICT / EXPIRED | Refresh state and determine whether the original action is still needed. |
| INVALID_REQUEST / INVALID_MAIL_TYPE / DISABLED / UNSUPPORTED | Fix inputs, actor permissions, configuration or provider capabilities. |
| ECONOMY_UNAVAILABLE / INSUFFICIENT_FUNDS / INBOX_FULL | Check the relevant transaction’s dependencies/capacity; do not assume each operation emits all of these. |
| UNSUPPORTED_ITEM / BACKEND_RESTRICTED | Retain restrictions and choose a supported backend/workflow. |
| BUSY | Use a bounded retry only after refreshing and reauthorizing the action. |
| STORAGE_FAILURE / INVENTORY_FAILURE / INTERNAL_ERROR | Potentially ambiguous effects: inspect durable state before any replay. |
| CANCELLED_BY_EVENT | No bypass; respect the listener’s decision. |
UUID owner = player.getUniqueId(); // Authenticated mailbox owner
Optional<InboxMailEntry> found = api.inbox().findById(owner, mailId);
if (found.isEmpty() || found.get().mailType() != MailType.MONEY) return;
InboxMailEntry entry = found.get();
MailMutationResult result = api.inbox().claimMoney(
owner, entry.mailId(), entry.revision(), player);
getLogger().info(result.status().name());Mail management
Delayed-mail tracking & lifecycle
List<DelayedMailEntry> pending = api.tracking().getPending(player.getUniqueId());
for (DelayedMailEntry delivery : pending) {
getLogger().info("Due (Unix milliseconds): " + delivery.deliverAtMillis());
}
Optional<MailLifecycleStatus> current = api.tracking()
.getCurrentStatus(player.getUniqueId(), mailId);DelayedMailEntry identifies queue ID, recipient, delivery type, created/due timestamps, expiration duration, origin backend and whether this backend can process it. cancel(queueId) returns an operation status; authorize cancellation before invoking it. getCurrentStatus accepts a mail or queue ID and is a current-state lookup, not a permanent historical audit log.
Lifecycle states include QUEUED, DELIVERED, VIEWED, CLAIMED, DELETED, EXPIRED, RETURNED, CANCELLED and FAILED. Use lifecycle events and your own durable business records if you need longer history; opening a read-only check is not a successful item claim.
getPending(null) lists the pending queue across recipients and must remain staff-only in your addon. getCurrentStatus(null, queueId) can locate a queued entry but cannot resolve a delivered inbox entry without an owner UUID. Inbox snapshots distinguish delivered mail and persistent overflow; an absent ID may mean already removed/claimed or never known, not proof of a specific historical outcome. Cancelling a queue ID is a privileged operation and not an automatic refund of your own external reward/payment.
The root service also offers hasMailboxData, hasRegisteredMailbox, getUncheckedMailCount, getPendingLoginNotificationCount and hasPendingMailNotification. refreshMailNotification and clearMailNotification update notification visuals/state; they do not claim, delete, pay for or grant mail. Pending-login notices, unchecked counts and inbox occupancy are different quantities.
getUncheckedMailCount counts unexpired unchecked items in the delivered inbox, not unchecked overflow or pending-login totals. hasPendingMailNotification also considers overflow, login notices and visual pending state. clearMailNotification acknowledges unread markers in both inbox and overflow, clears pending-login/visual state and persists changed data: it is more than hiding an indicator. It returns true when mailbox data exists, even if nothing changed. refreshMailNotification reconciles visuals and returns false if mailbox data or the visual manager is unavailable.
Mail management
Scheduled-mail service
Map<String, Object> task = Map.of(
"enabled", false,
"type", "PAPER_LETTER",
"send-time", "12:00:00",
"timezone", "server",
"repeat", "daily",
"recipients", Map.of("mode", "PLAYERS", "players", List.of(player.getName())),
"letter", Map.of("message", "Daily news"));
ApiOperationStatus saved = api.scheduledMail().upsertTask("example_daily", task);
getLogger().info(saved.name());getTasks/getTask expose current task snapshots; getTask returns Optional. upsertTask takes one task’s nested definition, not the outer scheduled-mail map. It replaces that definition rather than merging a partial patch: read/copy existing settings first if you intend to change one field. Invalid IDs/timing or save failure return INVALID_REQUEST; a successful save does not prove payload, recipient or economy availability at execution time. valid/nextDueMillis describe scheduling timing, not a guaranteed delivery.
removeTask and setEnabled persist changes; reload rereads scheduled-mail configuration. removeTask deletes the definition but preserves durable occurrence/delivery receipt state. runNow returns ScheduledMailRunResult with operation status and recipient/delivery/queue/failure totals. It is an administrative dispatch, not a dry run, and does not advance the automatic recurrence. In Premium, network ownership/dispatch safeguards still apply. Definition snapshots are not live editing handles; do not mutate nested objects in place.
Use the main wiki’s complete task schema for letter/book/package/money payloads, audiences, timezone, repeat and expiry. Task IDs are limited to letters, digits, underscore and hyphen (maximum 64). Preserve owner configuration and do not silently enable an example. Premium networking adds scope/owner/recipient/target coordination; every participating owner must have compatible definitions and healthy network authority. Read task validity and failure details rather than assuming persistence implies execution.
Complete scheduled-mail setup and YAML examplesExtension services
Registrations & location snapshots
registry().getPlayerMailboxes() returns player registrations; its UUID overload filters one owner. getPublicMailboxes and getPublicAccessNpcs describe public access points, not another shared recipient’s inventory. MailboxLocation records world/backend information so a missing world or remote backend is not mistaken for deletion.
These are read-only snapshots. Registration/unregistration still follows the plugin’s build, permission and Citizens workflows. Do not mutate stored coordinates or count a temporarily unloaded world as proof that a mailbox was removed. Use MailboxRegistryChangedEvent to refresh a cache.
getPlayerMailboxes() lists all owners, whereas getPlayerMailboxes(UUID) returns that owner’s entries. Each RegisteredMailbox identifies ownerUuid, ownerName, mailboxNumber, container and sign locations; it does not expose a style field. PublicMailbox includes its public access ID/name and both locations; PublicAccessNpc includes the NPC ID/name/location. MailboxLocation supplies world(), x(), y(), z(), loaded() and backendId(). An empty backend ID means no ID was supplied for that snapshot; it is not proof that a remote location is local. These methods do not register a new mailbox or spawn an NPC.
Extension services
Custom-item codecs
itemCodecs().register(CustomItemCodec) registers an ID/priority-based provider; unregister(id) removes it. Inspect getRegisteredCodecIds. prepareForStorage and restoreAfterStorage use the codec pipeline; isSupportedOnCurrentBackend checks support. High-priority matching codecs run first, and ItemStacks are cloned rather than exposed as mutable storage references.
Implement getId, getPriority, supports, prepareForStorage and restoreAfterStorage. isSupportedOnCurrentBackend is an optional compatibility hook. Keep IDs stable/unique, preserve enough metadata for lossless restoration, fail safely on unknown payloads and remove only your own registration in onDisable. A permissive compatibility hook is not a license to deserialize unsupported/corrupt items or bypass nested-package/shulker restrictions.
A codec supports the plugin’s item storage lifecycle; it does not automatically register ItemsAdder assets, charge players, replace item ownership checks or reserve resource-pack model selectors. See the exact SPI in the source reference before implementing.
IDs are matched case-insensitively on unregister; registering the same ID replaces the existing codec. Every matching codec can run in descending priority, passing its valid transformed item to the next codec—not only the first match. Transformation exceptions or null/air returns leave the previous item in the chain; they do not reliably fail the whole send. Compatibility exceptions fail closed. Never rely on a throwing transform to reject unsafe input, or register an ID owned by another addon.
Extension services
Public notification providers
notifications().register(MailNotificationProvider), unregister(id) and getRegisteredProviderIds manage generic addon notification providers. Implement getId, getPriority and onDelivery(NotificationContext); onLifecycle is an optional hook. NotificationContext includes recipient identity, mail ID, delivery type/origin, sender and expiration. LifecycleNotificationContext adds status/reason.
Return CONTINUE for ordinary notification processing or SUPPRESS_DEFAULT to suppress the default delivery notification path. Suppression does not erase persistent unread mail or disable floating indicators. Remove your own provider during disable. Copy only needed immutable data for asynchronous external delivery; never perform blocking HTTP work in the synchronous callback.
Register returns false for a null provider or empty ID; the same case-insensitive ID replaces an existing registration. All providers run in descending priority even after one returns SUPPRESS_DEFAULT. Suppression is accumulated, not short-circuiting, and runtime exceptions in one provider do not stop the others. It replaces default chat/sound/pending-login notification handling, not storage, entitlement or unread visuals. Do not log private message bodies in delivery exports.
MailNotificationProvider provider = new MailNotificationProvider() {
public String getId() { return "example-addon:mail-notices"; }
public int getPriority() { return 10; }
public NotificationDecision onDelivery(NotificationContext context) {
// Queue only approved immutable metadata for any async integration.
return NotificationDecision.CONTINUE;
}
};
boolean registered = api.notifications().register(provider);
// In your onDisable cleanup, remove only this addon's ID:
if (registered) api.notifications().unregister(provider.getId());Extension services
Bukkit events & cancellation boundaries
| Event | Use / boundaries |
|---|---|
| MailboxMailPreSendEvent | Cancellable before send processing. Inspect request metadata; do not duplicate a delivery from inside the event. |
| MailboxMailDeliveredEvent | Individual delivery details, including attached message where applicable. Do not assume it also fires separately for each mass recipient. |
| MailboxMassMailDeliveredEvent | Aggregate successful recipients for a mass delivery; not a promise that every requested recipient succeeded. |
| MailboxMailPreMutationEvent | Cancellable before DELETE, CLAIM_MONEY, CLAIM_PACKAGE or ACCEPT_COD effects. |
| MailboxMailLifecycleEvent | Read-only lifecycle change notification. |
| MailboxRegistryChangedEvent | Player mailbox, Public Access Mailbox or NPC registration/unregistration changes. |
Register Bukkit listeners through your JavaPlugin as usual. Respect ignoreCancelled/priority conventions and the event’s actual cancellability. Use provided IDs/snapshots, not internal inventory items. Post-delivery/lifecycle events cannot retroactively roll back a committed delivery. Notification/export listeners must not expose private mail or create recursive sends.
MailboxMailPreSendEvent exposes mail type, recipient values and source plugin name, not the complete request body or an editable replacement request. It is one request-level cancellation, and sourcePluginName is caller-supplied metadata. Immediate multi-recipient API sends use the mass event path; consume recipient UUIDs and per-recipient mail IDs from that event, handle partial success, and avoid counting both event kinds as independent rewards. Delayed enqueue is not a delivery event: observe actual delivery later.
Delivery events expose origin/source/scope, delayed/offline flags, IDs, sender, COD/money summaries and raw attached message where present. Lifecycle callbacks provide owner, mail ID/type, status and reason. The original event constructor overloads remain for addon compatibility; fields may be absent/defaulted in events constructed through older overloads. Inspect available IDs and zero/null values rather than assuming every event has every field.
Extension services
Resource-pack status provider
UUID packId = UUID.fromString("9de43c9b-ef24-45ba-a43c-c96a181b9ce2");
// Only AFTER an authenticated current-session successful-load confirmation:
boolean recorded = api.resourcePacks().report(this, player.getUniqueId(),
packId, true, Duration.ofSeconds(45));
// Refresh the confirmation before expiry while it remains valid.
// Withdraw this provider/pack when no longer loaded:
api.resourcePacks().report(this, player.getUniqueId(), packId, false, Duration.ofSeconds(45));This shared Free/Premium service bridges trusted pack loaders/proxies to the local GUI appearance system. The source plugin must be enabled; the player must be online on this backend and identifiers valid. Call on the main thread. Loaded-confirmation validity is 1 second–2 minutes; refresh before expiry only while the same connection really has the pack loaded. false removes only this provider’s specified pack without creating a lease. clear(source, playerId) and clear(source) remove your provider’s confirmations, including when a player is no longer online.
Authenticate remote reports, bind them to the current player session, reject stale/replayed claims and verify the successful load in your own integration. Never accept an unauthenticated player/client “loaded” flag. Native Bukkit acknowledgements are independent; provider disable, player quit, lease expiry and shutdown clear relevant state. A valid reload may preserve live confirmations.
A report does not send/download/host a pack, inspect asset contents, turn on customization, change a saved preference or license a product. It informs Auto/forced Auto appearance only under normal policy. Off remains original icons. Loaded unrelated packs can lack the required models, so actual rendering must be tested.
Custom GUI, server-pack and ItemsAdder setupPremium only • Premium only
Premium: public read-only network API
MailboxNetworkApi network = getServer().getServicesManager().load(MailboxNetworkApi.class);
if (network == null) return;
NetworkSnapshot snapshot = network.getSnapshot();
if (!snapshot.available()) return;
NetworkEligibilitySnapshot eligibility = network.getEligibility(
NetworkDeliveryTarget.backendGroup("gameplay"));
getLogger().info(eligibility.reason());getApiVersion, getCapabilities and getIdempotencyScope identify the public contract/capabilities/backend scope. That scope is not an exactly-once guarantee. getSnapshot returns capture time, generation, backend identity, enabled/started/available/status, backend/group/presence summaries and online/stale-backend counts. Use availability/readiness and freshness, not a guessed status string.
getPresence(UUID) returns an Optional current cached presence snapshot. Unknown/stale presence does not prove offline. getEligibility(target) reports routing eligibility/reason and eligible backend IDs; it is advisory, not an authorization or item-claim guarantee. getGuiConfiguration returns a cached effective GUI scope/backend/publisher/fingerprint/ready snapshot and may be unsupported on older providers. GUI readiness does not mean a player loaded a pack. MailboxNetworkSnapshotEvent signals a changed snapshot.
NetworkBackendSnapshot identifies server ID/display name, Minecraft/Bukkit/data versions, MailboxGUI/network/package protocol versions, serializer/storage profile, supported economy/package/COD/book/money features, local-backend and stale flags. NetworkGroupSnapshot exposes its name and backend members; it has no online-member count or status field. NetworkPresenceSnapshot exposes player/backend/world identity, online flag, last-seen/heartbeat times and staleness. NetworkSnapshot.onlineCount is cached player presence, not the count of online backends; staleBackendCount counts stale backend profiles.
The provider downgrades an enabled snapshot to stale/unavailable when its generation changes or its capture age exceeds 15 seconds; stale presence is cleared. getPresence returns empty when presence is disabled, the UUID is null or no cached entry exists. Eligibility reason identifiers include eligible, network_disabled, network_unavailable, invalid_target, unknown_backend, unknown_group and no_eligible_backend. eligible=false returns no eligible backend IDs. Do not parse those reason strings as a substitute for eligible()/available().
NetworkGuiSnapshot.scope is disabled, single-server, shared or backend-local. fingerprint is an opaque effective configuration identity; sharedMainBackend identifies the resolved shared publisher when ready, or its configured policy before publication, and is empty outside shared scope. ready means an effective icon configuration exists—not that a player has loaded the models. GUI icon scope does not change mail ownership, permissions, inventory layout or transaction behavior.
NetworkDeliveryTarget is in the shared API: wholeNetwork(), currentBackendOnly(), onlyBackends(list), excludeBackends(list), backendGroup(name). Use configured backend IDs/groups, not Multiverse world names. Pass the target to an ordinary mail builder; the runtime rechecks it during delivery/claim. Free/non-network requests with explicit restrictions fail rather than silently broadcasting unrestricted mail.
Compile optional network-dependent code separately when supporting Free, check service/capabilities and handle disabled/starting/degraded/stale states. The reference publishes only the reviewed read-only network surface and its snapshot types; owner-only companion operations are omitted.
Reference & support
Complete public contract reference
Expand a type to see the source-reviewed public Java declaration, including builders, getters, record fields, enum values, event constructors and provider hooks. The 50 shared types and 9 Premium network contracts are checked against the October 10, 2026 source snapshot. MailboxGUIApi, ApiCapability, MailboxNetworkApi and NetworkCapability are explicitly labeled public excerpts because companion-only members are omitted. Other listed types show complete source. Internal implementation classes and DiscBridge-specific contracts are not published.
Service discovery
MailboxGUIApi
package com.rismr1.mailboxgui.api;
import com.rismr1.mailboxgui.api.request.BookLetterMailRequest;
import com.rismr1.mailboxgui.api.request.LetterMailRequest;
import com.rismr1.mailboxgui.api.request.MailboxApiRecipient;
import com.rismr1.mailboxgui.api.request.MoneyMailRequest;
import com.rismr1.mailboxgui.api.request.PackageMailRequest;
import com.rismr1.mailboxgui.api.result.MailboxSendResult;
import com.rismr1.mailboxgui.api.model.MailboxApiInfo;
import com.rismr1.mailboxgui.api.service.MailboxInboxApi;
import com.rismr1.mailboxgui.api.service.MailboxItemCodecApi;
import com.rismr1.mailboxgui.api.service.MailboxNotificationApi;
import com.rismr1.mailboxgui.api.service.MailboxRegistryApi;
import com.rismr1.mailboxgui.api.service.MailboxScheduledMailApi;
import com.rismr1.mailboxgui.api.service.MailboxTrackingApi;
import java.util.Optional;
import java.util.UUID;
public interface MailboxGUIApi {
String API_VERSION = "2.0";
MailboxSendResult sendLetter(LetterMailRequest request);
MailboxSendResult sendBookLetter(BookLetterMailRequest request);
MailboxSendResult sendPackage(PackageMailRequest request);
MailboxSendResult sendMoney(MoneyMailRequest request);
Optional<MailboxApiRecipient> findRecipientByName(String playerName);
boolean hasMailboxData(UUID playerUuid);
boolean hasRegisteredMailbox(UUID playerUuid);
int getUncheckedMailCount(UUID playerUuid);
int getPendingLoginNotificationCount(UUID playerUuid);
boolean hasPendingMailNotification(UUID playerUuid);
boolean refreshMailNotification(UUID playerUuid);
boolean clearMailNotification(UUID playerUuid);
int getPackageContentLimit();
default MailboxApiInfo getApiInfo() {
return new MailboxApiInfo(API_VERSION, "unknown", "unknown", true, getPackageContentLimit(), java.util.Set.of());
}
default MailboxInboxApi inbox() {
throw new UnsupportedOperationException("Inbox API is not available from this provider.");
}
default MailboxTrackingApi tracking() {
throw new UnsupportedOperationException("Tracking API is not available from this provider.");
}
default MailboxScheduledMailApi scheduledMail() {
throw new UnsupportedOperationException("Scheduled Mail API is not available from this provider.");
}
default MailboxRegistryApi registry() {
throw new UnsupportedOperationException("Mailbox registry API is not available from this provider.");
}
default MailboxItemCodecApi itemCodecs() {
throw new UnsupportedOperationException("Custom item codec API is not available from this provider.");
}
default MailboxNotificationApi notifications() {
throw new UnsupportedOperationException("Notification provider API is not available from this provider.");
}
default com.rismr1.mailboxgui.api.service.MailboxResourcePackApi resourcePacks() {
throw new UnsupportedOperationException("Resource-pack status API is not available from this provider.");
}
}
MailboxGUIApiProvider
package com.rismr1.mailboxgui.api;
import org.bukkit.Bukkit;
public final class MailboxGUIApiProvider {
private MailboxGUIApiProvider() {
}
public static MailboxGUIApi get() {
return Bukkit.getServicesManager().load(MailboxGUIApi.class);
}
}Service interfaces
MailboxInboxApi
package com.rismr1.mailboxgui.api.service;
import com.rismr1.mailboxgui.api.model.InboxMailEntry;
import com.rismr1.mailboxgui.api.model.InboxPage;
import com.rismr1.mailboxgui.api.model.InboxQuery;
import com.rismr1.mailboxgui.api.model.MailMutationResult;
import com.rismr1.mailboxgui.api.type.ApiOperationStatus;
import org.bukkit.entity.Player;
import java.util.Optional;
import java.util.UUID;
public interface MailboxInboxApi {
InboxPage query(UUID ownerUuid, InboxQuery query);
Optional<InboxMailEntry> findById(UUID ownerUuid, String mailId);
MailMutationResult delete(UUID ownerUuid, String mailId, long expectedRevision, String reason);
MailMutationResult claimMoney(UUID ownerUuid, String mailId, long expectedRevision, Player recipient);
MailMutationResult claimPackageContents(UUID ownerUuid, String mailId, long expectedRevision, Player recipient);
/**
* Opens MailboxGUI's secure package workflow, including COD acceptance when applicable.
*/
ApiOperationStatus openPackageManagement(UUID ownerUuid, String mailId, long expectedRevision,
Player recipient, int returnPage);
}
MailboxTrackingApi
package com.rismr1.mailboxgui.api.service;
import com.rismr1.mailboxgui.api.model.DelayedMailEntry;
import com.rismr1.mailboxgui.api.type.ApiOperationStatus;
import com.rismr1.mailboxgui.api.type.MailLifecycleStatus;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public interface MailboxTrackingApi {
List<DelayedMailEntry> getPending(UUID recipientUuid);
/**
* Returns the current status while mail is pending delivery or remains in the inbox.
* Completed historical states are emitted through MailboxMailLifecycleEvent.
*/
Optional<MailLifecycleStatus> getCurrentStatus(UUID recipientUuid, String mailOrQueueId);
ApiOperationStatus cancel(String queueId);
}
MailboxScheduledMailApi
package com.rismr1.mailboxgui.api.service;
import com.rismr1.mailboxgui.api.model.ScheduledMailRunResult;
import com.rismr1.mailboxgui.api.model.ScheduledMailTask;
import com.rismr1.mailboxgui.api.type.ApiOperationStatus;
import java.util.List;
import java.util.Map;
import java.util.Optional;
public interface MailboxScheduledMailApi {
List<ScheduledMailTask> getTasks();
Optional<ScheduledMailTask> getTask(String id);
ApiOperationStatus upsertTask(String id, Map<String, Object> definition);
ApiOperationStatus removeTask(String id);
ApiOperationStatus setEnabled(String id, boolean enabled);
ScheduledMailRunResult runNow(String id);
void reload();
}
MailboxRegistryApi
package com.rismr1.mailboxgui.api.service;
import com.rismr1.mailboxgui.api.model.PublicAccessNpc;
import com.rismr1.mailboxgui.api.model.PublicMailbox;
import com.rismr1.mailboxgui.api.model.RegisteredMailbox;
import java.util.List;
import java.util.UUID;
public interface MailboxRegistryApi {
List<RegisteredMailbox> getPlayerMailboxes();
List<RegisteredMailbox> getPlayerMailboxes(UUID ownerUuid);
List<PublicMailbox> getPublicMailboxes();
List<PublicAccessNpc> getPublicAccessNpcs();
}
MailboxItemCodecApi
package com.rismr1.mailboxgui.api.service;
import com.rismr1.mailboxgui.api.spi.CustomItemCodec;
import org.bukkit.inventory.ItemStack;
import java.util.List;
public interface MailboxItemCodecApi {
boolean register(CustomItemCodec codec);
boolean unregister(String codecId);
List<String> getRegisteredCodecIds();
ItemStack prepareForStorage(ItemStack item);
ItemStack restoreAfterStorage(ItemStack item);
boolean isSupportedOnCurrentBackend(ItemStack item);
}
MailboxNotificationApi
package com.rismr1.mailboxgui.api.service;
import com.rismr1.mailboxgui.api.spi.MailNotificationProvider;
import java.util.List;
public interface MailboxNotificationApi {
boolean register(MailNotificationProvider provider);
boolean unregister(String providerId);
List<String> getRegisteredProviderIds();
}
MailboxResourcePackApi
package com.rismr1.mailboxgui.api.service;
import org.bukkit.plugin.Plugin;
import java.time.Duration;
import java.util.UUID;
/** Optional, provider-owned confirmations for GUI appearance; available in both editions. */
public interface MailboxResourcePackApi {
/**
* Reports a confirmed load (or removes that pack's confirmation) for an online local player.
* Call on the server main thread. Loaded confirmations expire after validity (1 second to 2 minutes);
* refresh them while the pack remains loaded. False removes only this provider's specified pack.
* The supplying plugin must authenticate any remote reports before calling this trusted local API.
* Returns false if the source is disabled, the player is offline, or identifiers/validity are invalid.
* This does not deliver a pack, verify its assets, change player preferences or enable customization.
*/
boolean report(Plugin source, UUID playerId, UUID packId, boolean loaded, Duration validity);
/** Clears this provider's confirmations for one player. Main thread only. */
void clear(Plugin source, UUID playerId);
/** Clears all confirmations owned by this provider. Main thread only. */
void clear(Plugin source);
}
Requests and results
MailboxApiRecipient
package com.rismr1.mailboxgui.api.request;
import java.util.Objects;
import java.util.UUID;
public final class MailboxApiRecipient {
private final UUID uuid;
private final String name;
private MailboxApiRecipient(UUID uuid, String name) {
this.uuid = uuid;
this.name = normalizeName(name);
}
public static MailboxApiRecipient of(UUID uuid, String name) {
if (uuid == null && (name == null || name.isBlank())) {
throw new IllegalArgumentException("A recipient must have a UUID, name, or both.");
}
return new MailboxApiRecipient(uuid, name);
}
public static MailboxApiRecipient uuid(UUID uuid) {
Objects.requireNonNull(uuid, "uuid");
return new MailboxApiRecipient(uuid, null);
}
public static MailboxApiRecipient name(String name) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("name cannot be blank");
}
return new MailboxApiRecipient(null, name);
}
public UUID getUuid() {
return uuid;
}
public String getName() {
return name;
}
public boolean hasUuid() {
return uuid != null;
}
public boolean hasName() {
return name != null && !name.isBlank();
}
private static String normalizeName(String input) {
if (input == null) {
return null;
}
String trimmed = input.trim();
return trimmed.isEmpty() ? null : trimmed;
}
}LetterMailRequest
package com.rismr1.mailboxgui.api.request;
import com.rismr1.mailboxgui.api.type.RecipientPolicy;
import com.rismr1.mailboxgui.api.type.NetworkDeliveryTarget;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
public final class LetterMailRequest {
private final List<MailboxApiRecipient> recipients;
private final String fromDisplayName;
private final String message;
private final int deliveryDelaySeconds;
private final boolean notifyRecipients;
private final RecipientPolicy recipientPolicy;
private final String sourcePluginName;
private final long expirationDurationSeconds;
private final NetworkDeliveryTarget networkDeliveryTarget;
private LetterMailRequest(Builder builder) {
this.recipients = Collections.unmodifiableList(new ArrayList<>(builder.recipients));
this.fromDisplayName = normalize(builder.fromDisplayName);
this.message = builder.message == null ? "" : builder.message;
this.deliveryDelaySeconds = builder.deliveryDelaySeconds;
this.notifyRecipients = builder.notifyRecipients;
this.recipientPolicy = builder.recipientPolicy;
this.sourcePluginName = normalize(builder.sourcePluginName);
this.expirationDurationSeconds = builder.expirationDurationSeconds;
this.networkDeliveryTarget = builder.networkDeliveryTarget == null ? NetworkDeliveryTarget.wholeNetwork() : builder.networkDeliveryTarget;
}
public static Builder builder() {
return new Builder();
}
public List<MailboxApiRecipient> getRecipients() {
return recipients;
}
public String getFromDisplayName() {
return fromDisplayName;
}
public String getMessage() {
return message;
}
public int getDeliveryDelaySeconds() {
return deliveryDelaySeconds;
}
public boolean isNotifyRecipients() {
return notifyRecipients;
}
public RecipientPolicy getRecipientPolicy() {
return recipientPolicy;
}
public String getSourcePluginName() {
return sourcePluginName;
}
/**
* Requested expiration duration in seconds. Zero means non-expiring; delayed mail starts this duration at actual delivery.
*/
public long getExpirationDurationSeconds() {
return expirationDurationSeconds;
}
public boolean hasExpiration() {
return expirationDurationSeconds > 0L;
}
public NetworkDeliveryTarget getNetworkDeliveryTarget() {
return networkDeliveryTarget;
}
private static String normalize(String input) {
if (input == null) {
return null;
}
String trimmed = input.trim();
return trimmed.isEmpty() ? null : trimmed;
}
public static final class Builder {
private final List<MailboxApiRecipient> recipients = new ArrayList<>();
private String fromDisplayName;
private String message;
private int deliveryDelaySeconds = -1;
private boolean notifyRecipients = true;
private RecipientPolicy recipientPolicy = RecipientPolicy.KNOWN_MAILBOXGUI_PLAYER;
private String sourcePluginName;
private long expirationDurationSeconds;
private NetworkDeliveryTarget networkDeliveryTarget = NetworkDeliveryTarget.wholeNetwork();
public Builder recipient(UUID uuid, String name) {
this.recipients.add(MailboxApiRecipient.of(uuid, name));
return this;
}
public Builder recipient(UUID uuid) {
this.recipients.add(MailboxApiRecipient.uuid(uuid));
return this;
}
public Builder recipient(String name) {
this.recipients.add(MailboxApiRecipient.name(name));
return this;
}
public Builder recipients(List<MailboxApiRecipient> recipients) {
if (recipients != null) {
this.recipients.addAll(recipients);
}
return this;
}
public Builder fromDisplayName(String fromDisplayName) {
this.fromDisplayName = fromDisplayName;
return this;
}
public Builder message(String message) {
this.message = message;
return this;
}
public Builder deliveryDelaySeconds(int deliveryDelaySeconds) {
this.deliveryDelaySeconds = deliveryDelaySeconds;
return this;
}
public Builder notifyRecipients(boolean notifyRecipients) {
this.notifyRecipients = notifyRecipients;
return this;
}
public Builder recipientPolicy(RecipientPolicy recipientPolicy) {
if (recipientPolicy != null) {
this.recipientPolicy = recipientPolicy;
}
return this;
}
public Builder sourcePluginName(String sourcePluginName) {
this.sourcePluginName = sourcePluginName;
return this;
}
public Builder expirationDurationSeconds(long seconds) {
this.expirationDurationSeconds = validateExpirationSeconds(seconds);
return this;
}
public Builder expiresAfter(Duration duration) {
this.expirationDurationSeconds = validateExpirationDuration(duration);
return this;
}
public Builder networkDeliveryTarget(NetworkDeliveryTarget target) {
this.networkDeliveryTarget = target == null ? NetworkDeliveryTarget.wholeNetwork() : target;
return this;
}
public LetterMailRequest build() {
return new LetterMailRequest(this);
}
}
private static long validateExpirationSeconds(long seconds) {
if (seconds < 0L) {
throw new IllegalArgumentException("expirationDurationSeconds cannot be negative.");
}
Math.multiplyExact(seconds, 1000L);
return seconds;
}
private static long validateExpirationDuration(Duration duration) {
if (duration == null || duration.isZero() || duration.isNegative()) {
throw new IllegalArgumentException("Expiration duration must be positive.");
}
long seconds = duration.getSeconds();
if (seconds <= 0L) {
throw new IllegalArgumentException("Expiration duration must be at least one second.");
}
return validateExpirationSeconds(seconds);
}
}
BookLetterMailRequest
package com.rismr1.mailboxgui.api.request;
import com.rismr1.mailboxgui.api.type.RecipientPolicy;
import com.rismr1.mailboxgui.api.type.NetworkDeliveryTarget;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
public final class BookLetterMailRequest {
private final List<MailboxApiRecipient> recipients;
private final String fromDisplayName;
private final String title;
private final String message;
private final int deliveryDelaySeconds;
private final boolean notifyRecipients;
private final RecipientPolicy recipientPolicy;
private final String sourcePluginName;
private final long expirationDurationSeconds;
private final NetworkDeliveryTarget networkDeliveryTarget;
private BookLetterMailRequest(Builder builder) {
this.recipients = Collections.unmodifiableList(new ArrayList<>(builder.recipients));
this.fromDisplayName = normalize(builder.fromDisplayName);
this.title = normalize(builder.title);
this.message = builder.message == null ? "" : builder.message;
this.deliveryDelaySeconds = builder.deliveryDelaySeconds;
this.notifyRecipients = builder.notifyRecipients;
this.recipientPolicy = builder.recipientPolicy;
this.sourcePluginName = normalize(builder.sourcePluginName);
this.expirationDurationSeconds = builder.expirationDurationSeconds;
this.networkDeliveryTarget = builder.networkDeliveryTarget == null ? NetworkDeliveryTarget.wholeNetwork() : builder.networkDeliveryTarget;
}
public static Builder builder() {
return new Builder();
}
public List<MailboxApiRecipient> getRecipients() {
return recipients;
}
public String getFromDisplayName() {
return fromDisplayName;
}
public String getTitle() {
return title;
}
public String getMessage() {
return message;
}
public int getDeliveryDelaySeconds() {
return deliveryDelaySeconds;
}
public boolean isNotifyRecipients() {
return notifyRecipients;
}
public RecipientPolicy getRecipientPolicy() {
return recipientPolicy;
}
public String getSourcePluginName() {
return sourcePluginName;
}
/**
* Requested expiration duration in seconds. Zero means non-expiring; delayed mail starts this duration at actual delivery.
*/
public long getExpirationDurationSeconds() {
return expirationDurationSeconds;
}
public boolean hasExpiration() {
return expirationDurationSeconds > 0L;
}
public NetworkDeliveryTarget getNetworkDeliveryTarget() {
return networkDeliveryTarget;
}
private static String normalize(String input) {
if (input == null) {
return null;
}
String trimmed = input.trim();
return trimmed.isEmpty() ? null : trimmed;
}
public static final class Builder {
private final List<MailboxApiRecipient> recipients = new ArrayList<>();
private String fromDisplayName;
private String title;
private String message;
private int deliveryDelaySeconds = -1;
private boolean notifyRecipients = true;
private RecipientPolicy recipientPolicy = RecipientPolicy.KNOWN_MAILBOXGUI_PLAYER;
private String sourcePluginName;
private long expirationDurationSeconds;
private NetworkDeliveryTarget networkDeliveryTarget = NetworkDeliveryTarget.wholeNetwork();
public Builder recipient(UUID uuid, String name) {
this.recipients.add(MailboxApiRecipient.of(uuid, name));
return this;
}
public Builder recipient(UUID uuid) {
this.recipients.add(MailboxApiRecipient.uuid(uuid));
return this;
}
public Builder recipient(String name) {
this.recipients.add(MailboxApiRecipient.name(name));
return this;
}
public Builder recipients(List<MailboxApiRecipient> recipients) {
if (recipients != null) {
this.recipients.addAll(recipients);
}
return this;
}
public Builder fromDisplayName(String fromDisplayName) {
this.fromDisplayName = fromDisplayName;
return this;
}
public Builder title(String title) {
this.title = title;
return this;
}
public Builder message(String message) {
this.message = message;
return this;
}
public Builder deliveryDelaySeconds(int deliveryDelaySeconds) {
this.deliveryDelaySeconds = deliveryDelaySeconds;
return this;
}
public Builder notifyRecipients(boolean notifyRecipients) {
this.notifyRecipients = notifyRecipients;
return this;
}
public Builder recipientPolicy(RecipientPolicy recipientPolicy) {
if (recipientPolicy != null) {
this.recipientPolicy = recipientPolicy;
}
return this;
}
public Builder sourcePluginName(String sourcePluginName) {
this.sourcePluginName = sourcePluginName;
return this;
}
public Builder expirationDurationSeconds(long seconds) {
this.expirationDurationSeconds = validateExpirationSeconds(seconds);
return this;
}
public Builder expiresAfter(Duration duration) {
this.expirationDurationSeconds = validateExpirationDuration(duration);
return this;
}
public Builder networkDeliveryTarget(NetworkDeliveryTarget target) {
this.networkDeliveryTarget = target == null ? NetworkDeliveryTarget.wholeNetwork() : target;
return this;
}
public BookLetterMailRequest build() {
return new BookLetterMailRequest(this);
}
}
private static long validateExpirationSeconds(long seconds) {
if (seconds < 0L) {
throw new IllegalArgumentException("expirationDurationSeconds cannot be negative.");
}
Math.multiplyExact(seconds, 1000L);
return seconds;
}
private static long validateExpirationDuration(Duration duration) {
if (duration == null || duration.isZero() || duration.isNegative()) {
throw new IllegalArgumentException("Expiration duration must be positive.");
}
long seconds = duration.getSeconds();
if (seconds <= 0L) {
throw new IllegalArgumentException("Expiration duration must be at least one second.");
}
return validateExpirationSeconds(seconds);
}
}
PackageMailRequest
package com.rismr1.mailboxgui.api.request;
import com.rismr1.mailboxgui.api.type.RecipientPolicy;
import com.rismr1.mailboxgui.api.type.CodReturnDestination;
import com.rismr1.mailboxgui.api.type.NetworkDeliveryTarget;
import org.bukkit.inventory.ItemStack;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
public final class PackageMailRequest {
private final List<MailboxApiRecipient> recipients;
private final String fromDisplayName;
private final String message;
private final UUID senderUuid;
private final List<ItemStack> contents;
private final int experienceAmount;
private final double codAmount;
private final List<ItemStack> codItemCurrencyContents;
private final boolean returnCodPaymentToSender;
private final CodReturnDestination codReturnDestination;
private final boolean returnCodPaymentToAdminInbox;
private final boolean returnDeclinedOrExpiredToAdminInbox;
private final int deliveryDelaySeconds;
private final boolean notifyRecipients;
private final RecipientPolicy recipientPolicy;
private final String sourcePluginName;
private final long expirationDurationSeconds;
private final NetworkDeliveryTarget networkDeliveryTarget;
private PackageMailRequest(Builder builder) {
this.recipients = Collections.unmodifiableList(new ArrayList<>(builder.recipients));
this.fromDisplayName = normalize(builder.fromDisplayName);
this.message = normalizeMessage(builder.message);
this.senderUuid = builder.senderUuid;
this.contents = Collections.unmodifiableList(cloneContents(builder.contents));
this.experienceAmount = builder.experienceAmount;
this.codAmount = builder.codAmount;
this.codItemCurrencyContents = Collections.unmodifiableList(cloneContents(builder.codItemCurrencyContents));
this.codReturnDestination = builder.codReturnDestination;
this.returnCodPaymentToSender = this.codReturnDestination == CodReturnDestination.SENDER;
this.returnCodPaymentToAdminInbox = builder.returnCodPaymentToAdminInbox;
this.returnDeclinedOrExpiredToAdminInbox = builder.returnDeclinedOrExpiredToAdminInbox;
this.deliveryDelaySeconds = builder.deliveryDelaySeconds;
this.notifyRecipients = builder.notifyRecipients;
this.recipientPolicy = builder.recipientPolicy;
this.sourcePluginName = normalize(builder.sourcePluginName);
this.expirationDurationSeconds = builder.expirationDurationSeconds;
this.networkDeliveryTarget = builder.networkDeliveryTarget == null ? NetworkDeliveryTarget.wholeNetwork() : builder.networkDeliveryTarget;
}
public static Builder builder() {
return new Builder();
}
public List<MailboxApiRecipient> getRecipients() {
return recipients;
}
public String getFromDisplayName() {
return fromDisplayName;
}
public String getMessage() {
return message;
}
public UUID getSenderUuid() {
return senderUuid;
}
public List<ItemStack> getContents() {
return cloneContents(contents);
}
public int getExperienceAmount() {
return experienceAmount;
}
public double getCodAmount() {
return codAmount;
}
public List<ItemStack> getCodItemCurrencyContents() {
return cloneContents(codItemCurrencyContents);
}
public boolean hasCodItemCurrencyContents() {
return !codItemCurrencyContents.isEmpty();
}
public boolean hasCodPrice() {
return codAmount > 0.0D || !codItemCurrencyContents.isEmpty();
}
public boolean isReturnCodPaymentToSender() {
return returnCodPaymentToSender;
}
/**
* Typed COD return destination. The legacy return-to-sender boolean maps to SENDER or NONE.
*/
public CodReturnDestination getCodReturnDestination() {
return codReturnDestination;
}
public boolean isReturnCodPaymentToAdminInbox() {
return returnCodPaymentToAdminInbox;
}
public boolean isReturnDeclinedOrExpiredToAdminInbox() {
return returnDeclinedOrExpiredToAdminInbox;
}
public int getDeliveryDelaySeconds() {
return deliveryDelaySeconds;
}
public boolean isNotifyRecipients() {
return notifyRecipients;
}
public RecipientPolicy getRecipientPolicy() {
return recipientPolicy;
}
public String getSourcePluginName() {
return sourcePluginName;
}
/**
* Requested expiration duration in seconds. Zero means non-expiring; delayed mail starts this duration at actual delivery.
*/
public long getExpirationDurationSeconds() {
return expirationDurationSeconds;
}
public boolean hasExpiration() {
return expirationDurationSeconds > 0L;
}
public NetworkDeliveryTarget getNetworkDeliveryTarget() {
return networkDeliveryTarget;
}
private static List<ItemStack> cloneContents(List<ItemStack> source) {
List<ItemStack> cloned = new ArrayList<>();
if (source == null) {
return cloned;
}
for (ItemStack item : source) {
if (item == null || item.getType().isAir()) {
continue;
}
cloned.add(item.clone());
}
return cloned;
}
private static String normalize(String input) {
if (input == null) {
return null;
}
String trimmed = input.trim();
return trimmed.isEmpty() ? null : trimmed;
}
public static final class Builder {
private final List<MailboxApiRecipient> recipients = new ArrayList<>();
private final List<ItemStack> contents = new ArrayList<>();
private final List<ItemStack> codItemCurrencyContents = new ArrayList<>();
private String fromDisplayName;
private String message;
private UUID senderUuid;
private int experienceAmount;
private double codAmount;
private CodReturnDestination codReturnDestination = CodReturnDestination.NONE;
private boolean returnCodPaymentToAdminInbox;
private boolean returnDeclinedOrExpiredToAdminInbox;
private int deliveryDelaySeconds = -1;
private boolean notifyRecipients = true;
private RecipientPolicy recipientPolicy = RecipientPolicy.KNOWN_MAILBOXGUI_PLAYER;
private String sourcePluginName;
private long expirationDurationSeconds;
private NetworkDeliveryTarget networkDeliveryTarget = NetworkDeliveryTarget.wholeNetwork();
public Builder recipient(UUID uuid, String name) {
this.recipients.add(MailboxApiRecipient.of(uuid, name));
return this;
}
public Builder recipient(UUID uuid) {
this.recipients.add(MailboxApiRecipient.uuid(uuid));
return this;
}
public Builder recipient(String name) {
this.recipients.add(MailboxApiRecipient.name(name));
return this;
}
public Builder recipients(List<MailboxApiRecipient> recipients) {
if (recipients != null) {
this.recipients.addAll(recipients);
}
return this;
}
public Builder fromDisplayName(String fromDisplayName) {
this.fromDisplayName = fromDisplayName;
return this;
}
public Builder message(String message) {
this.message = normalizeMessage(message);
return this;
}
public Builder senderUuid(UUID senderUuid) {
this.senderUuid = senderUuid;
return this;
}
public Builder addItem(ItemStack item) {
if (item != null && !item.getType().isAir()) {
this.contents.add(item.clone());
}
return this;
}
public Builder contents(List<ItemStack> contents) {
this.contents.clear();
this.contents.addAll(cloneContents(contents));
return this;
}
public Builder experienceAmount(int experienceAmount) {
this.experienceAmount = Math.max(0, experienceAmount);
return this;
}
public Builder codAmount(double codAmount) {
this.codAmount = Math.max(0.0D, codAmount);
return this;
}
public Builder addCodItemCurrency(ItemStack item) {
if (item != null && !item.getType().isAir()) {
this.codItemCurrencyContents.add(item.clone());
}
return this;
}
public Builder codItemCurrencyContents(List<ItemStack> codItemCurrencyContents) {
this.codItemCurrencyContents.clear();
this.codItemCurrencyContents.addAll(cloneContents(codItemCurrencyContents));
return this;
}
public Builder clearCodItemCurrencyContents() {
this.codItemCurrencyContents.clear();
return this;
}
public Builder returnCodPaymentToSender(boolean returnCodPaymentToSender) {
this.codReturnDestination = returnCodPaymentToSender ? CodReturnDestination.SENDER : CodReturnDestination.NONE;
return this;
}
public Builder codReturnDestination(CodReturnDestination destination) {
this.codReturnDestination = destination == null ? CodReturnDestination.NONE : destination;
return this;
}
public Builder returnCodPaymentToAdminInbox(boolean enabled) {
this.returnCodPaymentToAdminInbox = enabled;
return this;
}
public Builder returnDeclinedOrExpiredToAdminInbox(boolean enabled) {
this.returnDeclinedOrExpiredToAdminInbox = enabled;
return this;
}
public Builder deliveryDelaySeconds(int deliveryDelaySeconds) {
this.deliveryDelaySeconds = deliveryDelaySeconds;
return this;
}
public Builder notifyRecipients(boolean notifyRecipients) {
this.notifyRecipients = notifyRecipients;
return this;
}
public Builder recipientPolicy(RecipientPolicy recipientPolicy) {
if (recipientPolicy != null) {
this.recipientPolicy = recipientPolicy;
}
return this;
}
public Builder sourcePluginName(String sourcePluginName) {
this.sourcePluginName = sourcePluginName;
return this;
}
public Builder expirationDurationSeconds(long seconds) {
this.expirationDurationSeconds = validateExpirationSeconds(seconds);
return this;
}
public Builder expiresAfter(Duration duration) {
this.expirationDurationSeconds = validateExpirationDuration(duration);
return this;
}
public Builder networkDeliveryTarget(NetworkDeliveryTarget target) {
this.networkDeliveryTarget = target == null ? NetworkDeliveryTarget.wholeNetwork() : target;
return this;
}
public PackageMailRequest build() {
return new PackageMailRequest(this);
}
}
private static long validateExpirationSeconds(long seconds) {
if (seconds < 0L) {
throw new IllegalArgumentException("expirationDurationSeconds cannot be negative.");
}
Math.multiplyExact(seconds, 1000L);
return seconds;
}
private static String normalizeMessage(String input) {
String normalized = input == null || input.isBlank() ? null : input;
if (normalized != null && !com.rismr1.mailboxgui.api.message.MailMessageRules.isValid(normalized)) {
throw new IllegalArgumentException("Package message requires at most 256 visible characters and 17 body lines total, including automatic wrapping and /n.");
}
return normalized;
}
private static long validateExpirationDuration(Duration duration) {
if (duration == null || duration.isZero() || duration.isNegative()) {
throw new IllegalArgumentException("Expiration duration must be positive.");
}
long seconds = duration.getSeconds();
if (seconds <= 0L) {
throw new IllegalArgumentException("Expiration duration must be at least one second.");
}
return validateExpirationSeconds(seconds);
}
}
MoneyMailRequest
package com.rismr1.mailboxgui.api.request;
import com.rismr1.mailboxgui.api.type.RecipientPolicy;
import com.rismr1.mailboxgui.api.type.NetworkDeliveryTarget;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
public final class MoneyMailRequest {
private final List<MailboxApiRecipient> recipients;
private final String fromDisplayName;
private final String message;
private final double amount;
private final int deliveryDelaySeconds;
private final boolean notifyRecipients;
private final RecipientPolicy recipientPolicy;
private final String sourcePluginName;
private final long expirationDurationSeconds;
private final NetworkDeliveryTarget networkDeliveryTarget;
private MoneyMailRequest(Builder builder) {
this.recipients = Collections.unmodifiableList(new ArrayList<>(builder.recipients));
this.fromDisplayName = normalize(builder.fromDisplayName);
this.message = normalizeMessage(builder.message);
this.amount = builder.amount;
this.deliveryDelaySeconds = builder.deliveryDelaySeconds;
this.notifyRecipients = builder.notifyRecipients;
this.recipientPolicy = builder.recipientPolicy;
this.sourcePluginName = normalize(builder.sourcePluginName);
this.expirationDurationSeconds = builder.expirationDurationSeconds;
this.networkDeliveryTarget = builder.networkDeliveryTarget == null ? NetworkDeliveryTarget.wholeNetwork() : builder.networkDeliveryTarget;
}
public static Builder builder() {
return new Builder();
}
public List<MailboxApiRecipient> getRecipients() {
return recipients;
}
public String getFromDisplayName() {
return fromDisplayName;
}
public String getMessage() {
return message;
}
public double getAmount() {
return amount;
}
public int getDeliveryDelaySeconds() {
return deliveryDelaySeconds;
}
public boolean isNotifyRecipients() {
return notifyRecipients;
}
public RecipientPolicy getRecipientPolicy() {
return recipientPolicy;
}
public String getSourcePluginName() {
return sourcePluginName;
}
/**
* Requested expiration duration in seconds. Zero means non-expiring; delayed mail starts this duration at actual delivery.
*/
public long getExpirationDurationSeconds() {
return expirationDurationSeconds;
}
public boolean hasExpiration() {
return expirationDurationSeconds > 0L;
}
public NetworkDeliveryTarget getNetworkDeliveryTarget() {
return networkDeliveryTarget;
}
private static String normalize(String input) {
if (input == null) {
return null;
}
String trimmed = input.trim();
return trimmed.isEmpty() ? null : trimmed;
}
public static final class Builder {
private final List<MailboxApiRecipient> recipients = new ArrayList<>();
private String fromDisplayName;
private String message;
private double amount;
private int deliveryDelaySeconds = -1;
private boolean notifyRecipients = true;
private RecipientPolicy recipientPolicy = RecipientPolicy.KNOWN_MAILBOXGUI_PLAYER;
private String sourcePluginName;
private long expirationDurationSeconds;
private NetworkDeliveryTarget networkDeliveryTarget = NetworkDeliveryTarget.wholeNetwork();
public Builder recipient(UUID uuid, String name) {
this.recipients.add(MailboxApiRecipient.of(uuid, name));
return this;
}
public Builder recipient(UUID uuid) {
this.recipients.add(MailboxApiRecipient.uuid(uuid));
return this;
}
public Builder recipient(String name) {
this.recipients.add(MailboxApiRecipient.name(name));
return this;
}
public Builder recipients(List<MailboxApiRecipient> recipients) {
if (recipients != null) {
this.recipients.addAll(recipients);
}
return this;
}
public Builder fromDisplayName(String fromDisplayName) {
this.fromDisplayName = fromDisplayName;
return this;
}
public Builder message(String message) {
this.message = normalizeMessage(message);
return this;
}
public Builder amount(double amount) {
this.amount = amount;
return this;
}
public Builder deliveryDelaySeconds(int deliveryDelaySeconds) {
this.deliveryDelaySeconds = deliveryDelaySeconds;
return this;
}
public Builder notifyRecipients(boolean notifyRecipients) {
this.notifyRecipients = notifyRecipients;
return this;
}
public Builder recipientPolicy(RecipientPolicy recipientPolicy) {
if (recipientPolicy != null) {
this.recipientPolicy = recipientPolicy;
}
return this;
}
public Builder sourcePluginName(String sourcePluginName) {
this.sourcePluginName = sourcePluginName;
return this;
}
public Builder expirationDurationSeconds(long seconds) {
this.expirationDurationSeconds = validateExpirationSeconds(seconds);
return this;
}
public Builder expiresAfter(Duration duration) {
this.expirationDurationSeconds = validateExpirationDuration(duration);
return this;
}
public Builder networkDeliveryTarget(NetworkDeliveryTarget target) {
this.networkDeliveryTarget = target == null ? NetworkDeliveryTarget.wholeNetwork() : target;
return this;
}
public MoneyMailRequest build() {
return new MoneyMailRequest(this);
}
}
private static long validateExpirationSeconds(long seconds) {
if (seconds < 0L) {
throw new IllegalArgumentException("expirationDurationSeconds cannot be negative.");
}
Math.multiplyExact(seconds, 1000L);
return seconds;
}
private static String normalizeMessage(String input) {
String normalized = input == null || input.isBlank() ? null : input;
if (normalized != null && !com.rismr1.mailboxgui.api.message.MailMessageRules.isValid(normalized)) {
throw new IllegalArgumentException("Money mail message requires at most 256 visible characters and 17 body lines total, including automatic wrapping and /n.");
}
return normalized;
}
private static long validateExpirationDuration(Duration duration) {
if (duration == null || duration.isZero() || duration.isNegative()) {
throw new IllegalArgumentException("Expiration duration must be positive.");
}
long seconds = duration.getSeconds();
if (seconds <= 0L) {
throw new IllegalArgumentException("Expiration duration must be at least one second.");
}
return validateExpirationSeconds(seconds);
}
}
MailboxSendResult
package com.rismr1.mailboxgui.api.result;
import com.rismr1.mailboxgui.api.type.MailType;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;
public final class MailboxSendResult {
private final MailType mailType;
private final List<MailboxRecipientResult> recipientResults;
private final List<String> warnings;
public MailboxSendResult(MailType mailType, List<MailboxRecipientResult> recipientResults, List<String> warnings) {
this.mailType = mailType;
this.recipientResults = Collections.unmodifiableList(new ArrayList<>(recipientResults == null ? List.of() : recipientResults));
this.warnings = Collections.unmodifiableList(new ArrayList<>(warnings == null ? List.of() : warnings));
}
public static MailboxSendResult singleFailure(MailType mailType, RecipientStatus status, String message) {
return new MailboxSendResult(
mailType,
List.of(new MailboxRecipientResult(null, null, status, message)),
List.of()
);
}
public MailType getMailType() {
return mailType;
}
public List<MailboxRecipientResult> getRecipientResults() {
return recipientResults;
}
public List<String> getWarnings() {
return warnings;
}
public boolean isSuccessful() {
return !recipientResults.isEmpty() && recipientResults.stream().allMatch(MailboxRecipientResult::isSuccessful);
}
public int getRequestedRecipients() {
return recipientResults.size();
}
public int getDeliveredRecipients() {
return (int) recipientResults.stream().filter(result -> result.getStatus() == RecipientStatus.DELIVERED).count();
}
public int getQueuedRecipients() {
return (int) recipientResults.stream().filter(result -> result.getStatus() == RecipientStatus.QUEUED).count();
}
public int getFailedRecipients() {
return (int) recipientResults.stream().filter(result -> !result.isSuccessful()).count();
}
public List<String> getDeliveredMailIds() {
List<String> ids = new ArrayList<>();
for (MailboxRecipientResult result : recipientResults) {
if (result != null && result.hasMailId()) {
ids.add(result.getMailId());
}
}
return Collections.unmodifiableList(ids);
}
public List<String> getQueuedDeliveryIds() {
List<String> ids = new ArrayList<>();
for (MailboxRecipientResult result : recipientResults) {
if (result != null && result.hasQueueId()) {
ids.add(result.getQueueId());
}
}
return Collections.unmodifiableList(ids);
}
public boolean hasExpiringDeliveries() {
return recipientResults.stream().anyMatch(MailboxRecipientResult::hasExpiration);
}
public Map<UUID, String> getSuccessfulRecipientMailIds() {
Map<UUID, String> ids = new LinkedHashMap<>();
for (MailboxRecipientResult result : recipientResults) {
if (result != null && result.getRecipientUuid() != null && result.hasMailId()) {
ids.put(result.getRecipientUuid(), result.getMailId());
}
}
return Collections.unmodifiableMap(ids);
}
}
MailboxRecipientResult
package com.rismr1.mailboxgui.api.result;
import java.util.UUID;
public final class MailboxRecipientResult {
private final UUID recipientUuid;
private final String recipientName;
private final RecipientStatus status;
private final String message;
private final String mailId;
private final long expirationTimestampMillis;
private final String queueId;
private final long expirationDurationSeconds;
public MailboxRecipientResult(UUID recipientUuid, String recipientName, RecipientStatus status, String message) {
this(recipientUuid, recipientName, status, message, null, 0L, null, 0L);
}
public MailboxRecipientResult(UUID recipientUuid,
String recipientName,
RecipientStatus status,
String message,
String mailId,
long expirationTimestampMillis,
String queueId,
long expirationDurationSeconds) {
this.recipientUuid = recipientUuid;
this.recipientName = recipientName;
this.status = status == null ? RecipientStatus.INVALID_REQUEST : status;
this.message = message == null ? "" : message;
this.mailId = normalize(mailId);
this.expirationTimestampMillis = Math.max(0L, expirationTimestampMillis);
this.queueId = normalize(queueId);
this.expirationDurationSeconds = Math.max(0L, expirationDurationSeconds);
}
public UUID getRecipientUuid() {
return recipientUuid;
}
public String getRecipientName() {
return recipientName;
}
public RecipientStatus getStatus() {
return status;
}
public String getMessage() {
return message;
}
/**
* Actual stored permanent mail ID for immediate successful deliveries.
*/
public String getMailId() {
return mailId;
}
public boolean hasMailId() {
return mailId != null;
}
/**
* Final absolute expiration timestamp for immediate delivery, or zero for non-expiring/queued results.
*/
public long getExpirationTimestampMillis() {
return expirationTimestampMillis;
}
public boolean hasExpiration() {
return expirationTimestampMillis > 0L || expirationDurationSeconds > 0L;
}
/**
* Delayed queue ID for queued deliveries.
*/
public String getQueueId() {
return queueId;
}
public boolean hasQueueId() {
return queueId != null;
}
/**
* Requested expiration duration for queued delivery; final timestamp is calculated at delivery time.
*/
public long getExpirationDurationSeconds() {
return expirationDurationSeconds;
}
public boolean isSuccessful() {
return status == RecipientStatus.DELIVERED || status == RecipientStatus.QUEUED;
}
private static String normalize(String value) {
if (value == null || value.isBlank()) {
return null;
}
return value.trim();
}
}
RecipientStatus
package com.rismr1.mailboxgui.api.result;
public enum RecipientStatus {
DELIVERED,
QUEUED,
RECIPIENT_NOT_FOUND,
NO_REGISTERED_MAILBOX,
MAILBOX_DATA_MISSING,
INVALID_REQUEST,
CANCELLED_BY_EVENT,
INBOX_WRITE_FAILED,
DISABLED_BY_CONFIG,
ECONOMY_UNAVAILABLE,
TOO_MANY_CONTENTS,
EMPTY_PACKAGE,
INVALID_EXPIRATION,
ADMIN_INBOX_DISABLED,
INVALID_COD_RETURN_DESTINATION,
INVALID_COD_RETURN_CONFIGURATION,
NESTED_PACKAGE_BLOCKED,
NON_EMPTY_SHULKER_BLOCKED,
SERIALIZATION_FAILED,
NETWORK_MODE_REQUIRED,
UNKNOWN_BACKEND,
UNKNOWN_BACKEND_GROUP,
NO_COMPATIBLE_BACKEND,
BACKEND_TARGET_BLOCKED,
ITEM_UNSUPPORTED_ON_TARGET_BACKEND,
COD_PAYMENT_UNSUPPORTED_ON_TARGET_BACKEND,
MAIL_RESTRICTED_TO_OTHER_BACKEND
}
Snapshot models
MailboxApiInfo
package com.rismr1.mailboxgui.api.model;
import com.rismr1.mailboxgui.api.type.ApiCapability;
import java.util.Set;
public record MailboxApiInfo(
String apiVersion,
String pluginVersion,
String edition,
boolean primaryThreadRequired,
int packageContentLimit,
Set<ApiCapability> capabilities
) {
public MailboxApiInfo {
apiVersion = apiVersion == null ? "unknown" : apiVersion;
pluginVersion = pluginVersion == null ? "unknown" : pluginVersion;
edition = edition == null ? "unknown" : edition;
capabilities = capabilities == null ? Set.of() : Set.copyOf(capabilities);
}
public boolean supports(ApiCapability capability) {
return capability != null && capabilities.contains(capability);
}
}
InboxQuery
package com.rismr1.mailboxgui.api.model;
import com.rismr1.mailboxgui.api.type.MailType;
import java.util.Set;
public record InboxQuery(
int page,
int pageSize,
Set<MailType> mailTypes,
boolean includeQueued,
boolean includeExpired,
SortOrder sortOrder
) {
public InboxQuery {
page = Math.max(0, page);
pageSize = Math.max(1, Math.min(100, pageSize));
mailTypes = mailTypes == null ? Set.of() : Set.copyOf(mailTypes);
sortOrder = sortOrder == null ? SortOrder.NEWEST_FIRST : sortOrder;
}
public static InboxQuery firstPage() {
return new InboxQuery(0, 36, Set.of(), true, true, SortOrder.NEWEST_FIRST);
}
public enum SortOrder {
NEWEST_FIRST,
OLDEST_FIRST,
TYPE_THEN_NEWEST,
EXPIRING_FIRST
}
}
InboxPage
package com.rismr1.mailboxgui.api.model;
import java.util.List;
import java.util.UUID;
public record InboxPage(UUID ownerUuid, int page, int pageSize, int totalEntries, int totalPages, List<InboxMailEntry> entries) {
public InboxPage {
entries = entries == null ? List.of() : List.copyOf(entries);
}
}
InboxMailEntry
package com.rismr1.mailboxgui.api.model;
import com.rismr1.mailboxgui.api.type.MailLifecycleStatus;
import com.rismr1.mailboxgui.api.type.MailType;
import org.bukkit.inventory.ItemStack;
public record InboxMailEntry(
String mailId,
MailType mailType,
MailLifecycleStatus status,
String senderName,
long sentAtMillis,
long expiresAtMillis,
boolean unchecked,
boolean queued,
int packageContentCount,
double moneyAmount,
String attachedMessage,
String originBackendId,
long revision,
ItemStack snapshot
) {
public InboxMailEntry {
senderName = senderName == null ? "" : senderName;
attachedMessage = attachedMessage == null ? "" : attachedMessage;
originBackendId = originBackendId == null ? "" : originBackendId;
snapshot = snapshot == null ? null : snapshot.clone();
}
@Override
public ItemStack snapshot() {
return snapshot == null ? null : snapshot.clone();
}
}
MailMutationResult
package com.rismr1.mailboxgui.api.model;
import com.rismr1.mailboxgui.api.type.ApiOperationStatus;
import com.rismr1.mailboxgui.api.type.MailType;
public record MailMutationResult(ApiOperationStatus status, String mailId, MailType mailType, long revision, int remainingMail, String message) {
public MailMutationResult {
status = status == null ? ApiOperationStatus.INTERNAL_ERROR : status;
message = message == null ? "" : message;
}
public boolean successful() {
return status == ApiOperationStatus.SUCCESS;
}
}
DelayedMailEntry
package com.rismr1.mailboxgui.api.model;
import com.rismr1.mailboxgui.api.type.MailDeliveryType;
import java.util.UUID;
public record DelayedMailEntry(String queueId, UUID recipientUuid, String recipientName, MailDeliveryType mailType,
long createdAtMillis, long deliverAtMillis, long expirationDurationSeconds,
String originBackendId, boolean processableOnCurrentBackend) {
}
ScheduledMailTask
package com.rismr1.mailboxgui.api.model;
import java.util.Map;
public record ScheduledMailTask(String id, boolean enabled, String type, String from, boolean notifyRecipients,
long nextDueMillis, boolean valid, boolean completed, long lastRunMillis,
int lastDelivered, int lastQueued, int lastFailed, String lastFailure,
Map<String, Object> definition) {
public ScheduledMailTask {
definition = definition == null ? Map.of() : Map.copyOf(definition);
lastFailure = lastFailure == null ? "" : lastFailure;
}
}
ScheduledMailRunResult
package com.rismr1.mailboxgui.api.model;
import com.rismr1.mailboxgui.api.type.ApiOperationStatus;
public record ScheduledMailRunResult(ApiOperationStatus status, int recipients, int delivered, int queued, int failed, String message) {
public boolean successful() {
return status == ApiOperationStatus.SUCCESS;
}
}
MailboxLocation
package com.rismr1.mailboxgui.api.model;
public record MailboxLocation(String world, int x, int y, int z, boolean loaded, String backendId) {
public MailboxLocation {
world = world == null ? "" : world;
backendId = backendId == null ? "" : backendId;
}
}
RegisteredMailbox
package com.rismr1.mailboxgui.api.model;
import java.util.UUID;
public record RegisteredMailbox(UUID ownerUuid, String ownerName, int mailboxNumber, MailboxLocation container, MailboxLocation sign) {
}
PublicMailbox
package com.rismr1.mailboxgui.api.model;
public record PublicMailbox(String id, String name, MailboxLocation container, MailboxLocation sign) {
}
PublicAccessNpc
package com.rismr1.mailboxgui.api.model;
public record PublicAccessNpc(String id, String name, MailboxLocation location) {
}
Types and policies
ApiCapability
package com.rismr1.mailboxgui.api.type;
public enum ApiCapability {
INBOX_QUERY,
TRANSACTIONAL_MUTATION,
DELIVERY_TRACKING,
SCHEDULED_MAIL,
MAILBOX_REGISTRY,
CUSTOM_ITEM_CODECS,
NOTIFICATION_PROVIDERS,
LIFECYCLE_EVENTS,
ATTACHED_MESSAGES,
MESSAGE_VALIDATION,
NETWORK_DELIVERY_TARGETS,
VAULT_ECONOMY,
ITEM_CURRENCY_COD,
PACKAGE_EXPERIENCE,
RESOURCE_PACK_STATUS
}
ApiOperationStatus
package com.rismr1.mailboxgui.api.type;
public enum ApiOperationStatus {
SUCCESS,
NOT_FOUND,
ALREADY_COMPLETED,
REVISION_CONFLICT,
EXPIRED,
INVALID_REQUEST,
INVALID_MAIL_TYPE,
BUSY,
DISABLED,
UNSUPPORTED,
ECONOMY_UNAVAILABLE,
INSUFFICIENT_FUNDS,
INBOX_FULL,
UNSUPPORTED_ITEM,
BACKEND_RESTRICTED,
STORAGE_FAILURE,
INVENTORY_FAILURE,
CANCELLED_BY_EVENT,
INTERNAL_ERROR
}
RecipientPolicy
package com.rismr1.mailboxgui.api.type;
public enum RecipientPolicy {
REGISTERED_MAILBOX_ONLY,
KNOWN_MAILBOXGUI_PLAYER,
CREATE_IF_MISSING
}CodReturnDestination
package com.rismr1.mailboxgui.api.type;
/**
* Destination for API-created COD package return handling.
*/
public enum CodReturnDestination {
/**
* No explicit COD return destination.
*/
NONE,
/**
* Return COD payments or eligible package contents to the request sender.
*/
SENDER,
/**
* Preserve COD return handling for the Shared Admin Inbox.
*/
ADMIN_INBOX
}
MailType
package com.rismr1.mailboxgui.api.type;
public enum MailType {
LETTER,
PACKAGE,
MONEY
}MailDeliveryType
package com.rismr1.mailboxgui.api.type;
public enum MailDeliveryType {
LETTER,
BOOK_LETTER,
PACKAGE,
COD_PACKAGE,
RETURNED_COD_PACKAGE,
COD_ITEM_CURRENCY_PAYMENT,
MONEY_MAIL
}MailLifecycleStatus
package com.rismr1.mailboxgui.api.type;
public enum MailLifecycleStatus {
QUEUED,
DELIVERED,
VIEWED,
CLAIMED,
DELETED,
EXPIRED,
RETURNED,
CANCELLED,
FAILED
}
MailDeliverySource
package com.rismr1.mailboxgui.api.type;
public enum MailDeliverySource {
LOCAL_DELIVERY,
PLAYER_ACTION,
ADMIN_COMMAND,
API,
DELAYED_QUEUE,
NETWORK_SYNC,
SYSTEM
}MailDeliveryScope
package com.rismr1.mailboxgui.api.type;
public enum MailDeliveryScope {
SINGLE_SERVER,
NETWORK
}MailDeliveryOrigin
package com.rismr1.mailboxgui.api.type;
public enum MailDeliveryOrigin {
PLAYER,
ADMIN,
API,
SYSTEM
}NetworkDeliveryTarget
package com.rismr1.mailboxgui.api.type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
public final class NetworkDeliveryTarget {
public enum Type {
WHOLE_NETWORK,
CURRENT_BACKEND_ONLY,
ONLY_BACKENDS,
EXCLUDE_BACKENDS,
BACKEND_GROUP
}
private static final NetworkDeliveryTarget WHOLE_NETWORK = new NetworkDeliveryTarget(Type.WHOLE_NETWORK, List.of(), List.of(), null);
private static final NetworkDeliveryTarget CURRENT_BACKEND_ONLY = new NetworkDeliveryTarget(Type.CURRENT_BACKEND_ONLY, List.of(), List.of(), null);
private final Type type;
private final List<String> allowedBackends;
private final List<String> excludedBackends;
private final String backendGroup;
private NetworkDeliveryTarget(Type type, List<String> allowedBackends, List<String> excludedBackends, String backendGroup) {
this.type = type == null ? Type.WHOLE_NETWORK : type;
this.allowedBackends = Collections.unmodifiableList(normalizeList(allowedBackends));
this.excludedBackends = Collections.unmodifiableList(normalizeList(excludedBackends));
this.backendGroup = normalizeToken(backendGroup);
}
public static NetworkDeliveryTarget wholeNetwork() {
return WHOLE_NETWORK;
}
public static NetworkDeliveryTarget currentBackendOnly() {
return CURRENT_BACKEND_ONLY;
}
public static NetworkDeliveryTarget onlyBackends(List<String> backendIds) {
return new NetworkDeliveryTarget(Type.ONLY_BACKENDS, backendIds, List.of(), null);
}
public static NetworkDeliveryTarget excludeBackends(List<String> backendIds) {
return new NetworkDeliveryTarget(Type.EXCLUDE_BACKENDS, List.of(), backendIds, null);
}
public static NetworkDeliveryTarget backendGroup(String groupName) {
return new NetworkDeliveryTarget(Type.BACKEND_GROUP, List.of(), List.of(), groupName);
}
public Type getType() {
return type;
}
public List<String> getAllowedBackends() {
return allowedBackends;
}
public List<String> getExcludedBackends() {
return excludedBackends;
}
public String getBackendGroup() {
return backendGroup;
}
public boolean isWholeNetwork() {
return type == Type.WHOLE_NETWORK;
}
private static List<String> normalizeList(List<String> values) {
List<String> normalized = new ArrayList<>();
if (values == null) {
return normalized;
}
for (String value : values) {
String token = normalizeToken(value);
if (token != null && !normalized.contains(token)) {
normalized.add(token);
}
}
return normalized;
}
private static String normalizeToken(String value) {
if (value == null || value.isBlank()) {
return null;
}
return value.trim().toLowerCase(Locale.ROOT);
}
}
Events
MailboxMailPreSendEvent
package com.rismr1.mailboxgui.api.event;
import com.rismr1.mailboxgui.api.request.MailboxApiRecipient;
import com.rismr1.mailboxgui.api.type.MailType;
import org.bukkit.event.Cancellable;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import java.util.List;
public final class MailboxMailPreSendEvent extends Event implements Cancellable {
private static final HandlerList HANDLERS = new HandlerList();
private final MailType mailType;
private final List<MailboxApiRecipient> recipients;
private final String sourcePluginName;
private boolean cancelled;
public MailboxMailPreSendEvent(MailType mailType, List<MailboxApiRecipient> recipients, String sourcePluginName) {
this.mailType = mailType;
this.recipients = recipients == null ? List.of() : List.copyOf(recipients);
this.sourcePluginName = sourcePluginName == null ? "Unknown" : sourcePluginName;
}
public MailType getMailType() { return mailType; }
public List<MailboxApiRecipient> getRecipients() { return recipients; }
public String getSourcePluginName() { return sourcePluginName; }
@Override public boolean isCancelled() { return cancelled; }
@Override public void setCancelled(boolean cancelled) { this.cancelled = cancelled; }
@Override public HandlerList getHandlers() { return HANDLERS; }
public static HandlerList getHandlerList() { return HANDLERS; }
}
MailboxMailDeliveredEvent
package com.rismr1.mailboxgui.api.event;
import com.rismr1.mailboxgui.api.type.MailDeliveryOrigin;
import com.rismr1.mailboxgui.api.type.MailDeliveryScope;
import com.rismr1.mailboxgui.api.type.MailDeliverySource;
import com.rismr1.mailboxgui.api.type.MailDeliveryType;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import java.util.UUID;
public class MailboxMailDeliveredEvent extends Event {
private static final HandlerList HANDLERS = new HandlerList();
private final UUID recipientUuid;
private final String recipientName;
private final UUID senderUuid;
private final String senderName;
private final MailDeliveryOrigin origin;
private final MailDeliveryType deliveryType;
private final double codPrice;
private final String codPriceText;
private final double moneyAmount;
private final boolean delayedDelivery;
private final boolean partOfMassDelivery;
private final String eventId;
private final String mailId;
private final String originServerId;
private final String originServerDisplayName;
private final String currentServerId;
private final String currentServerDisplayName;
private final boolean networkMode;
private final boolean localOrigin;
private final boolean syncOrigin;
private final MailDeliveryScope deliveryScope;
private final MailDeliverySource deliverySource;
private final long expirationTimestampMillis;
private final String attachedMessage;
public MailboxMailDeliveredEvent(
UUID recipientUuid,
String recipientName,
UUID senderUuid,
String senderName,
MailDeliveryOrigin origin,
MailDeliveryType deliveryType,
double codPrice,
double moneyAmount,
boolean delayedDelivery,
boolean partOfMassDelivery
) {
this(
recipientUuid,
recipientName,
senderUuid,
senderName,
origin,
deliveryType,
codPrice,
null,
moneyAmount,
delayedDelivery,
partOfMassDelivery
);
}
public MailboxMailDeliveredEvent(
UUID recipientUuid,
String recipientName,
UUID senderUuid,
String senderName,
MailDeliveryOrigin origin,
MailDeliveryType deliveryType,
double codPrice,
String codPriceText,
double moneyAmount,
boolean delayedDelivery,
boolean partOfMassDelivery
) {
this(
recipientUuid,
recipientName,
senderUuid,
senderName,
origin,
deliveryType,
codPrice,
codPriceText,
moneyAmount,
delayedDelivery,
partOfMassDelivery,
UUID.randomUUID().toString(),
null,
"",
"",
"",
"",
false,
true,
false,
MailDeliveryScope.SINGLE_SERVER,
delayedDelivery ? MailDeliverySource.DELAYED_QUEUE : MailDeliverySource.LOCAL_DELIVERY,
0L
);
}
public MailboxMailDeliveredEvent(
UUID recipientUuid,
String recipientName,
UUID senderUuid,
String senderName,
MailDeliveryOrigin origin,
MailDeliveryType deliveryType,
double codPrice,
double moneyAmount,
boolean delayedDelivery,
boolean partOfMassDelivery,
String eventId,
String mailId,
String originServerId,
String originServerDisplayName,
String currentServerId,
String currentServerDisplayName,
boolean networkMode,
boolean localOrigin,
boolean syncOrigin,
MailDeliveryScope deliveryScope,
MailDeliverySource deliverySource
) {
this(
recipientUuid,
recipientName,
senderUuid,
senderName,
origin,
deliveryType,
codPrice,
null,
moneyAmount,
delayedDelivery,
partOfMassDelivery,
eventId,
mailId,
originServerId,
originServerDisplayName,
currentServerId,
currentServerDisplayName,
networkMode,
localOrigin,
syncOrigin,
deliveryScope,
deliverySource,
0L
);
}
public MailboxMailDeliveredEvent(
UUID recipientUuid,
String recipientName,
UUID senderUuid,
String senderName,
MailDeliveryOrigin origin,
MailDeliveryType deliveryType,
double codPrice,
String codPriceText,
double moneyAmount,
boolean delayedDelivery,
boolean partOfMassDelivery,
String eventId,
String mailId,
String originServerId,
String originServerDisplayName,
String currentServerId,
String currentServerDisplayName,
boolean networkMode,
boolean localOrigin,
boolean syncOrigin,
MailDeliveryScope deliveryScope,
MailDeliverySource deliverySource
) {
this.recipientUuid = recipientUuid;
this.recipientName = sanitizeName(recipientName, "Unknown");
this.senderUuid = senderUuid;
this.senderName = sanitizeName(senderName, "Server");
this.origin = origin != null ? origin : MailDeliveryOrigin.SYSTEM;
this.deliveryType = deliveryType != null ? deliveryType : MailDeliveryType.PACKAGE;
this.codPrice = sanitizeAmount(codPrice);
this.codPriceText = sanitizeCodPriceText(codPriceText, this.codPrice);
this.moneyAmount = sanitizeAmount(moneyAmount);
this.delayedDelivery = delayedDelivery;
this.partOfMassDelivery = partOfMassDelivery;
this.eventId = sanitizeId(eventId);
this.mailId = sanitizeNullableId(mailId);
this.originServerId = sanitizeNullableText(originServerId);
this.originServerDisplayName = sanitizeNullableText(originServerDisplayName);
this.currentServerId = sanitizeNullableText(currentServerId);
this.currentServerDisplayName = sanitizeNullableText(currentServerDisplayName);
this.networkMode = networkMode;
this.localOrigin = localOrigin;
this.syncOrigin = syncOrigin;
this.deliveryScope = deliveryScope != null ? deliveryScope : MailDeliveryScope.SINGLE_SERVER;
this.deliverySource = deliverySource != null ? deliverySource : MailDeliverySource.LOCAL_DELIVERY;
this.expirationTimestampMillis = 0L;
this.attachedMessage = "";
}
public MailboxMailDeliveredEvent(
UUID recipientUuid,
String recipientName,
UUID senderUuid,
String senderName,
MailDeliveryOrigin origin,
MailDeliveryType deliveryType,
double codPrice,
String codPriceText,
double moneyAmount,
boolean delayedDelivery,
boolean partOfMassDelivery,
String eventId,
String mailId,
String originServerId,
String originServerDisplayName,
String currentServerId,
String currentServerDisplayName,
boolean networkMode,
boolean localOrigin,
boolean syncOrigin,
MailDeliveryScope deliveryScope,
MailDeliverySource deliverySource,
long expirationTimestampMillis
) {
this(recipientUuid, recipientName, senderUuid, senderName, origin, deliveryType, codPrice, codPriceText, moneyAmount, delayedDelivery, partOfMassDelivery, eventId, mailId, originServerId, originServerDisplayName, currentServerId, currentServerDisplayName, networkMode, localOrigin, syncOrigin, deliveryScope, deliverySource, expirationTimestampMillis, "");
}
public MailboxMailDeliveredEvent(
UUID recipientUuid,
String recipientName,
UUID senderUuid,
String senderName,
MailDeliveryOrigin origin,
MailDeliveryType deliveryType,
double codPrice,
String codPriceText,
double moneyAmount,
boolean delayedDelivery,
boolean partOfMassDelivery,
String eventId,
String mailId,
String originServerId,
String originServerDisplayName,
String currentServerId,
String currentServerDisplayName,
boolean networkMode,
boolean localOrigin,
boolean syncOrigin,
MailDeliveryScope deliveryScope,
MailDeliverySource deliverySource,
long expirationTimestampMillis,
String attachedMessage
) {
this.recipientUuid = recipientUuid;
this.recipientName = sanitizeName(recipientName, "Unknown");
this.senderUuid = senderUuid;
this.senderName = sanitizeName(senderName, "Server");
this.origin = origin != null ? origin : MailDeliveryOrigin.SYSTEM;
this.deliveryType = deliveryType != null ? deliveryType : MailDeliveryType.PACKAGE;
this.codPrice = sanitizeAmount(codPrice);
this.codPriceText = sanitizeCodPriceText(codPriceText, this.codPrice);
this.moneyAmount = sanitizeAmount(moneyAmount);
this.delayedDelivery = delayedDelivery;
this.partOfMassDelivery = partOfMassDelivery;
this.eventId = sanitizeId(eventId);
this.mailId = sanitizeNullableId(mailId);
this.originServerId = sanitizeNullableText(originServerId);
this.originServerDisplayName = sanitizeNullableText(originServerDisplayName);
this.currentServerId = sanitizeNullableText(currentServerId);
this.currentServerDisplayName = sanitizeNullableText(currentServerDisplayName);
this.networkMode = networkMode;
this.localOrigin = localOrigin;
this.syncOrigin = syncOrigin;
this.deliveryScope = deliveryScope != null ? deliveryScope : MailDeliveryScope.SINGLE_SERVER;
this.deliverySource = deliverySource != null ? deliverySource : MailDeliverySource.LOCAL_DELIVERY;
this.expirationTimestampMillis = Math.max(0L, expirationTimestampMillis);
this.attachedMessage = attachedMessage == null ? "" : attachedMessage;
}
public UUID getRecipientUuid() {
return recipientUuid;
}
/** Raw attached message, including /n syntax; empty when this mail has no attachment. */
public String getAttachedMessage() { return attachedMessage; }
public String getRecipientName() {
return recipientName;
}
public UUID getSenderUuid() {
return senderUuid;
}
public String getSenderName() {
return senderName;
}
public MailDeliveryOrigin getOrigin() {
return origin;
}
public MailDeliveryType getDeliveryType() {
return deliveryType;
}
public double getCodPrice() {
return codPrice;
}
public String getCodPriceText() {
return codPriceText;
}
public double getMoneyAmount() {
return moneyAmount;
}
public boolean isDelayedDelivery() {
return delayedDelivery;
}
public boolean isPartOfMassDelivery() {
return partOfMassDelivery;
}
public String getEventId() {
return eventId;
}
public String getMailId() {
return mailId;
}
public boolean hasMailId() {
return mailId != null && !mailId.isBlank();
}
public String getOriginServerId() {
return originServerId;
}
public String getOriginServerDisplayName() {
return originServerDisplayName;
}
public String getCurrentServerId() {
return currentServerId;
}
public String getCurrentServerDisplayName() {
return currentServerDisplayName;
}
public boolean isNetworkMode() {
return networkMode;
}
public boolean isLocalOrigin() {
return localOrigin;
}
public boolean isSyncOrigin() {
return syncOrigin;
}
public MailDeliveryScope getDeliveryScope() {
return deliveryScope;
}
public MailDeliverySource getDeliverySource() {
return deliverySource;
}
/**
* Final absolute expiration timestamp stored on the delivered mail, or zero for non-expiring mail.
*/
public long getExpirationTimestampMillis() {
return expirationTimestampMillis;
}
public boolean hasExpiration() {
return expirationTimestampMillis > 0L;
}
public boolean isAdminMail() {
return origin == MailDeliveryOrigin.ADMIN;
}
public boolean isPlayerMail() {
return origin == MailDeliveryOrigin.PLAYER;
}
public boolean isApiMail() {
return origin == MailDeliveryOrigin.API;
}
public boolean hasSenderUuid() {
return senderUuid != null;
}
public boolean hasCodPrice() {
return codPrice > 0.0D || !codPriceText.isBlank();
}
public boolean hasCodPriceText() {
return !codPriceText.isBlank();
}
public boolean hasMoneyAmount() {
return moneyAmount > 0.0D;
}
@Override
public HandlerList getHandlers() {
return HANDLERS;
}
public static HandlerList getHandlerList() {
return HANDLERS;
}
private static String sanitizeName(String value, String fallback) {
if (value == null || value.isBlank()) {
return fallback;
}
return value;
}
private static String sanitizeId(String value) {
if (value == null || value.isBlank()) {
return UUID.randomUUID().toString();
}
return value;
}
private static String sanitizeNullableId(String value) {
if (value == null || value.isBlank()) {
return null;
}
return value;
}
private static String sanitizeNullableText(String value) {
if (value == null) {
return "";
}
return value;
}
private static String sanitizeCodPriceText(String value, double fallbackAmount) {
if (value != null && !value.isBlank()) {
return value.trim();
}
if (fallbackAmount > 0.0D) {
return String.valueOf(fallbackAmount);
}
return "";
}
private static double sanitizeAmount(double value) {
if (!Double.isFinite(value) || value < 0.0D) {
return 0.0D;
}
return value;
}
}
MailboxMassMailDeliveredEvent
package com.rismr1.mailboxgui.api.event;
import com.rismr1.mailboxgui.api.type.MailDeliveryOrigin;
import com.rismr1.mailboxgui.api.type.MailDeliveryScope;
import com.rismr1.mailboxgui.api.type.MailDeliverySource;
import com.rismr1.mailboxgui.api.type.MailDeliveryType;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
public class MailboxMassMailDeliveredEvent extends Event {
private static final HandlerList HANDLERS = new HandlerList();
private final UUID senderUuid;
private final String senderName;
private final MailDeliveryOrigin origin;
private final MailDeliveryType deliveryType;
private final int requestedRecipientCount;
private final int successfulRecipientCount;
private final int failedRecipientCount;
private final double codPrice;
private final String codPriceText;
private final double moneyAmount;
private final boolean delayedDelivery;
private final List<UUID> successfulRecipientUuids;
private final String eventId;
private final String originServerId;
private final String originServerDisplayName;
private final String currentServerId;
private final String currentServerDisplayName;
private final boolean networkMode;
private final boolean localOrigin;
private final boolean syncOrigin;
private final MailDeliveryScope deliveryScope;
private final MailDeliverySource deliverySource;
private final long expirationTimestampMillis;
private final String attachedMessage;
private final Map<UUID, String> successfulRecipientMailIds;
public MailboxMassMailDeliveredEvent(
UUID senderUuid,
String senderName,
MailDeliveryOrigin origin,
MailDeliveryType deliveryType,
int requestedRecipientCount,
int successfulRecipientCount,
int failedRecipientCount,
double codPrice,
double moneyAmount,
boolean delayedDelivery,
List<UUID> successfulRecipientUuids
) {
this(
senderUuid,
senderName,
origin,
deliveryType,
requestedRecipientCount,
successfulRecipientCount,
failedRecipientCount,
codPrice,
null,
moneyAmount,
delayedDelivery,
successfulRecipientUuids
);
}
public MailboxMassMailDeliveredEvent(
UUID senderUuid,
String senderName,
MailDeliveryOrigin origin,
MailDeliveryType deliveryType,
int requestedRecipientCount,
int successfulRecipientCount,
int failedRecipientCount,
double codPrice,
String codPriceText,
double moneyAmount,
boolean delayedDelivery,
List<UUID> successfulRecipientUuids
) {
this(
senderUuid,
senderName,
origin,
deliveryType,
requestedRecipientCount,
successfulRecipientCount,
failedRecipientCount,
codPrice,
codPriceText,
moneyAmount,
delayedDelivery,
successfulRecipientUuids,
UUID.randomUUID().toString(),
"",
"",
"",
"",
false,
true,
false,
MailDeliveryScope.SINGLE_SERVER,
delayedDelivery ? MailDeliverySource.DELAYED_QUEUE : MailDeliverySource.LOCAL_DELIVERY,
0L,
Map.of()
);
}
public MailboxMassMailDeliveredEvent(
UUID senderUuid,
String senderName,
MailDeliveryOrigin origin,
MailDeliveryType deliveryType,
int requestedRecipientCount,
int successfulRecipientCount,
int failedRecipientCount,
double codPrice,
double moneyAmount,
boolean delayedDelivery,
List<UUID> successfulRecipientUuids,
String eventId,
String originServerId,
String originServerDisplayName,
String currentServerId,
String currentServerDisplayName,
boolean networkMode,
boolean localOrigin,
boolean syncOrigin,
MailDeliveryScope deliveryScope,
MailDeliverySource deliverySource
) {
this(
senderUuid,
senderName,
origin,
deliveryType,
requestedRecipientCount,
successfulRecipientCount,
failedRecipientCount,
codPrice,
null,
moneyAmount,
delayedDelivery,
successfulRecipientUuids,
eventId,
originServerId,
originServerDisplayName,
currentServerId,
currentServerDisplayName,
networkMode,
localOrigin,
syncOrigin,
deliveryScope,
deliverySource,
0L,
Map.of()
);
}
public MailboxMassMailDeliveredEvent(
UUID senderUuid,
String senderName,
MailDeliveryOrigin origin,
MailDeliveryType deliveryType,
int requestedRecipientCount,
int successfulRecipientCount,
int failedRecipientCount,
double codPrice,
String codPriceText,
double moneyAmount,
boolean delayedDelivery,
List<UUID> successfulRecipientUuids,
String eventId,
String originServerId,
String originServerDisplayName,
String currentServerId,
String currentServerDisplayName,
boolean networkMode,
boolean localOrigin,
boolean syncOrigin,
MailDeliveryScope deliveryScope,
MailDeliverySource deliverySource
) {
this.senderUuid = senderUuid;
this.senderName = sanitizeName(senderName, "Server");
this.origin = origin != null ? origin : MailDeliveryOrigin.SYSTEM;
this.deliveryType = deliveryType != null ? deliveryType : MailDeliveryType.PACKAGE;
this.requestedRecipientCount = Math.max(0, requestedRecipientCount);
this.successfulRecipientCount = Math.max(0, successfulRecipientCount);
this.failedRecipientCount = Math.max(0, failedRecipientCount);
this.codPrice = sanitizeAmount(codPrice);
this.codPriceText = sanitizeCodPriceText(codPriceText, this.codPrice);
this.moneyAmount = sanitizeAmount(moneyAmount);
this.delayedDelivery = delayedDelivery;
this.successfulRecipientUuids = successfulRecipientUuids == null
? Collections.emptyList()
: List.copyOf(successfulRecipientUuids);
this.eventId = sanitizeId(eventId);
this.originServerId = sanitizeNullableText(originServerId);
this.originServerDisplayName = sanitizeNullableText(originServerDisplayName);
this.currentServerId = sanitizeNullableText(currentServerId);
this.currentServerDisplayName = sanitizeNullableText(currentServerDisplayName);
this.networkMode = networkMode;
this.localOrigin = localOrigin;
this.syncOrigin = syncOrigin;
this.deliveryScope = deliveryScope != null ? deliveryScope : MailDeliveryScope.SINGLE_SERVER;
this.deliverySource = deliverySource != null ? deliverySource : MailDeliverySource.LOCAL_DELIVERY;
this.expirationTimestampMillis = 0L;
this.attachedMessage = "";
this.successfulRecipientMailIds = Collections.emptyMap();
}
public MailboxMassMailDeliveredEvent(
UUID senderUuid,
String senderName,
MailDeliveryOrigin origin,
MailDeliveryType deliveryType,
int requestedRecipientCount,
int successfulRecipientCount,
int failedRecipientCount,
double codPrice,
String codPriceText,
double moneyAmount,
boolean delayedDelivery,
List<UUID> successfulRecipientUuids,
String eventId,
String originServerId,
String originServerDisplayName,
String currentServerId,
String currentServerDisplayName,
boolean networkMode,
boolean localOrigin,
boolean syncOrigin,
MailDeliveryScope deliveryScope,
MailDeliverySource deliverySource,
long expirationTimestampMillis,
Map<UUID, String> successfulRecipientMailIds
) {
this(senderUuid, senderName, origin, deliveryType, requestedRecipientCount, successfulRecipientCount, failedRecipientCount, codPrice, codPriceText, moneyAmount, delayedDelivery, successfulRecipientUuids, eventId, originServerId, originServerDisplayName, currentServerId, currentServerDisplayName, networkMode, localOrigin, syncOrigin, deliveryScope, deliverySource, expirationTimestampMillis, successfulRecipientMailIds, "");
}
public MailboxMassMailDeliveredEvent(
UUID senderUuid,
String senderName,
MailDeliveryOrigin origin,
MailDeliveryType deliveryType,
int requestedRecipientCount,
int successfulRecipientCount,
int failedRecipientCount,
double codPrice,
String codPriceText,
double moneyAmount,
boolean delayedDelivery,
List<UUID> successfulRecipientUuids,
String eventId,
String originServerId,
String originServerDisplayName,
String currentServerId,
String currentServerDisplayName,
boolean networkMode,
boolean localOrigin,
boolean syncOrigin,
MailDeliveryScope deliveryScope,
MailDeliverySource deliverySource,
long expirationTimestampMillis,
Map<UUID, String> successfulRecipientMailIds,
String attachedMessage
) {
this.senderUuid = senderUuid;
this.senderName = sanitizeName(senderName, "Server");
this.origin = origin != null ? origin : MailDeliveryOrigin.SYSTEM;
this.deliveryType = deliveryType != null ? deliveryType : MailDeliveryType.PACKAGE;
this.requestedRecipientCount = Math.max(0, requestedRecipientCount);
this.successfulRecipientCount = Math.max(0, successfulRecipientCount);
this.failedRecipientCount = Math.max(0, failedRecipientCount);
this.codPrice = sanitizeAmount(codPrice);
this.codPriceText = sanitizeCodPriceText(codPriceText, this.codPrice);
this.moneyAmount = sanitizeAmount(moneyAmount);
this.delayedDelivery = delayedDelivery;
this.successfulRecipientUuids = successfulRecipientUuids == null
? Collections.emptyList()
: List.copyOf(successfulRecipientUuids);
this.eventId = sanitizeId(eventId);
this.originServerId = sanitizeNullableText(originServerId);
this.originServerDisplayName = sanitizeNullableText(originServerDisplayName);
this.currentServerId = sanitizeNullableText(currentServerId);
this.currentServerDisplayName = sanitizeNullableText(currentServerDisplayName);
this.networkMode = networkMode;
this.localOrigin = localOrigin;
this.syncOrigin = syncOrigin;
this.deliveryScope = deliveryScope != null ? deliveryScope : MailDeliveryScope.SINGLE_SERVER;
this.deliverySource = deliverySource != null ? deliverySource : MailDeliverySource.LOCAL_DELIVERY;
this.expirationTimestampMillis = Math.max(0L, expirationTimestampMillis);
this.successfulRecipientMailIds = sanitizeMailIdMap(successfulRecipientMailIds);
this.attachedMessage = attachedMessage == null ? "" : attachedMessage;
}
public UUID getSenderUuid() {
return senderUuid;
}
/** Common raw attached message for this batch, or empty when there is no attachment. */
public String getAttachedMessage() { return attachedMessage; }
public String getSenderName() {
return senderName;
}
public MailDeliveryOrigin getOrigin() {
return origin;
}
public MailDeliveryType getDeliveryType() {
return deliveryType;
}
public int getRequestedRecipientCount() {
return requestedRecipientCount;
}
public int getSuccessfulRecipientCount() {
return successfulRecipientCount;
}
public int getFailedRecipientCount() {
return failedRecipientCount;
}
public double getCodPrice() {
return codPrice;
}
public String getCodPriceText() {
return codPriceText;
}
public double getMoneyAmount() {
return moneyAmount;
}
public boolean isDelayedDelivery() {
return delayedDelivery;
}
public List<UUID> getSuccessfulRecipientUuids() {
return successfulRecipientUuids;
}
public String getEventId() {
return eventId;
}
public String getOriginServerId() {
return originServerId;
}
public String getOriginServerDisplayName() {
return originServerDisplayName;
}
public String getCurrentServerId() {
return currentServerId;
}
public String getCurrentServerDisplayName() {
return currentServerDisplayName;
}
public boolean isNetworkMode() {
return networkMode;
}
public boolean isLocalOrigin() {
return localOrigin;
}
public boolean isSyncOrigin() {
return syncOrigin;
}
public MailDeliveryScope getDeliveryScope() {
return deliveryScope;
}
public MailDeliverySource getDeliverySource() {
return deliverySource;
}
/**
* Final absolute expiration timestamp shared by this delivery batch, or zero for non-expiring mail.
*/
public long getExpirationTimestampMillis() {
return expirationTimestampMillis;
}
public boolean hasExpiration() {
return expirationTimestampMillis > 0L;
}
public Map<UUID, String> getSuccessfulRecipientMailIds() {
return successfulRecipientMailIds;
}
public boolean isAdminMail() {
return origin == MailDeliveryOrigin.ADMIN;
}
public boolean isApiMail() {
return origin == MailDeliveryOrigin.API;
}
public boolean hasSenderUuid() {
return senderUuid != null;
}
public boolean hasCodPrice() {
return codPrice > 0.0D || !codPriceText.isBlank();
}
public boolean hasCodPriceText() {
return !codPriceText.isBlank();
}
public boolean hasMoneyAmount() {
return moneyAmount > 0.0D;
}
@Override
public HandlerList getHandlers() {
return HANDLERS;
}
public static HandlerList getHandlerList() {
return HANDLERS;
}
private static String sanitizeName(String value, String fallback) {
if (value == null || value.isBlank()) {
return fallback;
}
return value;
}
private static String sanitizeId(String value) {
if (value == null || value.isBlank()) {
return UUID.randomUUID().toString();
}
return value;
}
private static String sanitizeNullableText(String value) {
if (value == null) {
return "";
}
return value;
}
private static String sanitizeCodPriceText(String value, double fallbackAmount) {
if (value != null && !value.isBlank()) {
return value.trim();
}
if (fallbackAmount > 0.0D) {
return String.valueOf(fallbackAmount);
}
return "";
}
private static double sanitizeAmount(double value) {
if (!Double.isFinite(value) || value < 0.0D) {
return 0.0D;
}
return value;
}
private static Map<UUID, String> sanitizeMailIdMap(Map<UUID, String> source) {
if (source == null || source.isEmpty()) {
return Collections.emptyMap();
}
Map<UUID, String> sanitized = new LinkedHashMap<>();
for (Map.Entry<UUID, String> entry : source.entrySet()) {
if (entry.getKey() != null && entry.getValue() != null && !entry.getValue().isBlank()) {
sanitized.put(entry.getKey(), entry.getValue().trim());
}
}
return Collections.unmodifiableMap(sanitized);
}
}
MailboxMailPreMutationEvent
package com.rismr1.mailboxgui.api.event;
import com.rismr1.mailboxgui.api.type.MailType;
import org.bukkit.event.Cancellable;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import java.util.UUID;
/**
* Fired on the server thread before MailboxGUI changes or claims stored mail.
* Cancelling this event prevents the mutation and any related payout or item transfer.
*/
public final class MailboxMailPreMutationEvent extends Event implements Cancellable {
private static final HandlerList HANDLERS = new HandlerList();
private final UUID ownerUuid;
private final UUID actorUuid;
private final String mailId;
private final MailType mailType;
private final Action action;
private final String reason;
private boolean cancelled;
public MailboxMailPreMutationEvent(UUID ownerUuid, UUID actorUuid, String mailId, MailType mailType,
Action action, String reason) {
this.ownerUuid = ownerUuid;
this.actorUuid = actorUuid;
this.mailId = mailId == null ? "" : mailId;
this.mailType = mailType;
this.action = action;
this.reason = reason == null ? "" : reason;
}
public UUID getOwnerUuid() { return ownerUuid; }
public UUID getActorUuid() { return actorUuid; }
public String getMailId() { return mailId; }
public MailType getMailType() { return mailType; }
public Action getAction() { return action; }
public String getReason() { return reason; }
@Override public boolean isCancelled() { return cancelled; }
@Override public void setCancelled(boolean cancelled) { this.cancelled = cancelled; }
@Override public HandlerList getHandlers() { return HANDLERS; }
public static HandlerList getHandlerList() { return HANDLERS; }
public enum Action {
DELETE,
CLAIM_MONEY,
CLAIM_PACKAGE,
ACCEPT_COD
}
}
MailboxMailLifecycleEvent
package com.rismr1.mailboxgui.api.event;
import com.rismr1.mailboxgui.api.type.MailLifecycleStatus;
import com.rismr1.mailboxgui.api.type.MailType;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import java.util.UUID;
public final class MailboxMailLifecycleEvent extends Event {
private static final HandlerList HANDLERS = new HandlerList();
private final UUID ownerUuid;
private final String mailId;
private final MailType mailType;
private final MailLifecycleStatus status;
private final String reason;
public MailboxMailLifecycleEvent(UUID ownerUuid, String mailId, MailType mailType, MailLifecycleStatus status, String reason) {
this.ownerUuid = ownerUuid;
this.mailId = mailId;
this.mailType = mailType;
this.status = status;
this.reason = reason == null ? "" : reason;
}
public UUID getOwnerUuid() { return ownerUuid; }
public String getMailId() { return mailId; }
public MailType getMailType() { return mailType; }
public MailLifecycleStatus getStatus() { return status; }
public String getReason() { return reason; }
@Override public HandlerList getHandlers() { return HANDLERS; }
public static HandlerList getHandlerList() { return HANDLERS; }
}
MailboxRegistryChangedEvent
package com.rismr1.mailboxgui.api.event;
import com.rismr1.mailboxgui.api.model.MailboxLocation;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import java.util.UUID;
public final class MailboxRegistryChangedEvent extends Event {
private static final HandlerList HANDLERS = new HandlerList();
private final EntryType entryType;
private final Action action;
private final String entryId;
private final UUID ownerUuid;
private final String displayName;
private final MailboxLocation location;
public MailboxRegistryChangedEvent(EntryType entryType, Action action, String entryId, UUID ownerUuid,
String displayName, MailboxLocation location) {
this.entryType = entryType;
this.action = action;
this.entryId = entryId == null ? "" : entryId;
this.ownerUuid = ownerUuid;
this.displayName = displayName == null ? "" : displayName;
this.location = location;
}
public EntryType getEntryType() { return entryType; }
public Action getAction() { return action; }
public String getEntryId() { return entryId; }
public UUID getOwnerUuid() { return ownerUuid; }
public String getDisplayName() { return displayName; }
public MailboxLocation getLocation() { return location; }
@Override public HandlerList getHandlers() { return HANDLERS; }
public static HandlerList getHandlerList() { return HANDLERS; }
public enum EntryType { PLAYER_MAILBOX, PUBLIC_MAILBOX, PUBLIC_ACCESS_NPC }
public enum Action { REGISTERED, UNREGISTERED, LOAD_STATE_CHANGED }
}
Extension providers and text
CustomItemCodec
package com.rismr1.mailboxgui.api.spi;
import org.bukkit.inventory.ItemStack;
public interface CustomItemCodec {
String getId();
int getPriority();
boolean supports(ItemStack item);
ItemStack prepareForStorage(ItemStack item);
ItemStack restoreAfterStorage(ItemStack item);
default boolean isSupportedOnCurrentBackend(ItemStack item) {
return true;
}
}
MailNotificationProvider
package com.rismr1.mailboxgui.api.spi;
import com.rismr1.mailboxgui.api.type.MailDeliveryOrigin;
import com.rismr1.mailboxgui.api.type.MailDeliveryType;
import com.rismr1.mailboxgui.api.type.MailLifecycleStatus;
import com.rismr1.mailboxgui.api.type.MailType;
import java.util.UUID;
public interface MailNotificationProvider {
String getId();
int getPriority();
NotificationDecision onDelivery(NotificationContext context);
default void onLifecycle(LifecycleNotificationContext context) {
}
record NotificationContext(UUID recipientUuid, String recipientName, String mailId,
MailDeliveryType deliveryType, MailDeliveryOrigin origin,
String senderName, long expiresAtMillis) {
}
record LifecycleNotificationContext(UUID ownerUuid, String mailId, MailType mailType,
MailLifecycleStatus status, String reason) {
public LifecycleNotificationContext {
mailId = mailId == null ? "" : mailId;
reason = reason == null ? "" : reason;
}
}
enum NotificationDecision {
CONTINUE,
SUPPRESS_DEFAULT
}
}
MailMessageRules
package com.rismr1.mailboxgui.api.message;
import org.bukkit.ChatColor;
import com.rismr1.mailboxgui.api.text.MailTextFormatting;
import java.util.ArrayList;
import java.util.List;
/** Shared rules for paper letters and attached package/money messages, not book pages. */
public final class MailMessageRules {
public static final int MAX_VISIBLE_CHARACTERS = 256;
public static final int LINE_WIDTH = 28;
public static final int MAX_LINES = 17;
/** Theoretical maximum with no automatic wrapping; not a separate /n allowance. */
public static final int MAX_MANUAL_BREAKS = MAX_LINES - 1;
private MailMessageRules() { }
/** Converts /n to a newline, //n to literal /n, and accepts ordinary API/YAML newlines. */
public static String normalize(String raw) {
if (raw == null) return null;
String input = raw.replace("\r\n", "\n").replace('\r', '\n');
StringBuilder result = new StringBuilder();
for (int i = 0; i < input.length();) {
if (input.startsWith("//n", i)) {
result.append("/n");
i += 3;
} else if (input.startsWith("/n", i)) {
result.append('\n');
i += 2;
} else {
result.append(input.charAt(i++));
}
}
return result.toString();
}
public static Validation validate(String raw) {
if (raw == null || raw.isBlank()) return new Validation(false, "EMPTY", 0, 0, 0);
// Bound formatting-only input before tokenization without charging colors to the visible budget.
if (raw.length() > 8192) return new Validation(false, "INPUT_TOO_LONG", 0, 0, 0);
String normalized = normalize(raw);
String plain = MailTextFormatting.plain(normalized);
int breaks = (int) plain.chars().filter(c -> c == '\n').count();
String visible = plain.replace("\n", "");
int count = visible.codePointCount(0, visible.length());
if (visible.isBlank()) return new Validation(false, "EMPTY", count, 0, breaks);
if (count > MAX_VISIBLE_CHARACTERS) return new Validation(false, "TOO_LONG", count, 0, breaks);
int lines = wrapNormalized(plain).size();
return new Validation(lines <= MAX_LINES, lines > MAX_LINES ? "TOO_MANY_LINES" : "OK", count, lines, breaks);
}
public static boolean isValid(String raw) { return validate(raw).valid(); }
/** Wraps supported mixed formatting without truncation; /n syntax is decoded exactly once. */
public static List<String> wrapColored(String rawColored) {
if (rawColored == null) return List.of();
return List.copyOf(wrapNormalized(MailTextFormatting.legacy(normalize(rawColored))));
}
private static List<String> wrapNormalized(String text) {
List<String> lines = new ArrayList<>();
StringBuilder line = new StringBuilder();
int width = 0;
for (int offset = 0; offset < text.length();) {
int cp = text.codePointAt(offset);
if (cp == '\n') {
lines.add(line.toString().stripTrailing());
line = new StringBuilder(lastFormatting(line.toString()));
width = 0;
offset++;
continue;
}
StringBuilder token = new StringBuilder();
int tokenWidth = 0;
boolean space = Character.isWhitespace(cp);
while (offset < text.length()) {
cp = text.codePointAt(offset);
if (cp == ChatColor.COLOR_CHAR && offset + 1 < text.length()) {
token.append(text, offset, offset + 2);
offset += 2;
continue;
}
if (cp == '\n' || Character.isWhitespace(cp) != space) break;
token.appendCodePoint(space ? ' ' : cp);
tokenWidth++;
offset += Character.charCount(cp);
}
if (space) {
if (width > 0 && width + tokenWidth <= LINE_WIDTH) {
line.append(token);
width += tokenWidth;
} else {
// Dropping boundary whitespace must not drop formatting embedded in it.
for (int i = 0; i + 1 < token.length(); i++) {
if (token.charAt(i) == ChatColor.COLOR_CHAR) {
line.append(token, i, i + 2);
i++;
}
}
}
continue;
}
if (width > 0 && width + tokenWidth > LINE_WIDTH) {
lines.add(line.toString().stripTrailing());
line = new StringBuilder(lastFormatting(line.toString()));
width = 0;
}
for (int i = 0; i < token.length();) {
cp = token.codePointAt(i);
if (cp == ChatColor.COLOR_CHAR && i + 1 < token.length()) {
line.append(token, i, i + 2);
i += 2;
continue;
}
if (width == LINE_WIDTH) {
lines.add(line.toString().stripTrailing());
line = new StringBuilder(lastFormatting(line.toString()));
width = 0;
}
line.appendCodePoint(cp);
width++;
i += Character.charCount(cp);
}
}
if (width > 0 || !line.isEmpty() || text.endsWith("\n")) lines.add(line.toString().stripTrailing());
return lines;
}
private static String lastFormatting(String text) {
String color = "";
StringBuilder styles = new StringBuilder();
for (int i = 0; i + 1 < text.length(); i++) {
if (text.charAt(i) != ChatColor.COLOR_CHAR) continue;
char code = Character.toLowerCase(text.charAt(++i));
if (code == 'x' && i + 12 < text.length()) {
String hex = text.substring(i - 1, i + 13);
if (hex.matches("(?i)\u00a7x(?:\u00a7[0-9a-f]){6}")) {
color = hex;
styles.setLength(0);
i += 12;
continue;
}
}
if ("0123456789abcdefr".indexOf(code) >= 0) {
color = "" + ChatColor.COLOR_CHAR + code;
styles.setLength(0);
} else if ("klmno".indexOf(code) >= 0 && styles.indexOf(String.valueOf(code)) < 0) {
styles.append(ChatColor.COLOR_CHAR).append(code);
}
}
return color + styles;
}
public record Validation(boolean valid, String reason, int visibleCharacters, int lines, int manualBreaks) {
/** Remaining rendered body lines, after both automatic wraps and explicit breaks. */
public int remainingLines() { return Math.max(0, MAX_LINES - lines); }
}
}
MailTextFormatting
package com.rismr1.mailboxgui.api.text;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
import net.kyori.adventure.text.minimessage.tag.standard.StandardTags;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import java.util.Locale;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** Platform-independent, non-interactive mail text formatting. Unknown tags remain literal text. */
public final class MailTextFormatting {
private static final Set<String> SUPPORTED_FORMATS = Set.of("legacy", "hex", "minimessage-visual");
private static final TagResolver TAGS = TagResolver.resolver(
StandardTags.color(), StandardTags.decorations(), StandardTags.gradient(),
StandardTags.rainbow(), StandardTags.reset());
private static final MiniMessage MINI = MiniMessage.builder().tags(TAGS).build();
private static final LegacyComponentSerializer LEGACY = LegacyComponentSerializer.builder()
.character('\u00a7').hexColors().useUnusualXRepeatedCharacterHexFormat().build();
private static final Pattern CODES = Pattern.compile(
"(?i)[&\u00a7](?:x(?:[&\u00a7][0-9a-f]){6}|#[0-9a-f]{6}|[0-9a-fk-or])");
private static final String[] COLORS = {"black", "dark_blue", "dark_green", "dark_aqua",
"dark_red", "dark_purple", "gold", "gray", "dark_gray", "blue", "green", "aqua",
"red", "light_purple", "yellow", "white"};
private static final Pattern MINI_TAG = Pattern.compile("</?([!?#a-zA-Z0-9_-]+)(?=[:>])");
private static final Pattern PLACEHOLDERS = Pattern.compile(
"%[a-zA-Z_][a-zA-Z0-9_.-]*%|\\{[a-zA-Z_][a-zA-Z0-9_.-]*\\}|<([a-zA-Z_][a-zA-Z0-9_.-]*)>");
private MailTextFormatting() { }
/** Immutable capability identifiers. MiniMessage support is visual only, never interactive. */
public static Set<String> supportedFormats() {
return SUPPORTED_FORMATS;
}
/** Converts supported formatting to section-sign legacy text, preserving /n and literal placeholders. */
public static String legacy(String input) {
if (input == null || input.isEmpty()) return "";
if (!hasFormattingTags(input)) return legacyCodes(input);
try {
ProtectedInput protectedInput = protectPlaceholders(input);
return protectedInput.restore(LEGACY.serialize(parse(protectedInput.text())));
} catch (RuntimeException invalidFormatting) {
return legacyCodes(input);
}
}
/** Returns visible text without supported formatting; does not decode mail line-break syntax. */
public static String plain(String input) {
if (input == null || input.isEmpty()) return "";
if (!hasFormattingTags(input)) return CODES.matcher(input).replaceAll("");
try {
ProtectedInput protectedInput = protectPlaceholders(input);
return protectedInput.restore(PlainTextComponentSerializer.plainText().serialize(parse(protectedInput.text())));
} catch (RuntimeException invalidFormatting) {
return CODES.matcher(input).replaceAll("");
}
}
/** Inserts independently formatted text without leaking its style into the surrounding template. */
public static String replaceIsolated(String template, String placeholder, String value) {
String rendered = legacy(template);
String replacement = legacy(value);
if (placeholder == null || placeholder.isEmpty()) return rendered;
if (!CODES.matcher(replacement).find()) return rendered.replace(placeholder, replacement);
StringBuilder result = new StringBuilder();
int start = 0;
int index;
while ((index = rendered.indexOf(placeholder, start)) >= 0) {
result.append(rendered, start, index).append(replacement).append("\u00a7r")
.append(activeLegacyStyle(rendered.substring(0, index)));
start = index + placeholder.length();
}
return result.append(rendered.substring(start)).toString();
}
private static String activeLegacyStyle(String prefix) {
Matcher codes = CODES.matcher(prefix);
String color = "";
StringBuilder decorations = new StringBuilder();
while (codes.find()) {
String code = codes.group().substring(1).toLowerCase(Locale.ROOT);
if (code.equals("r")) {
color = "";
decorations.setLength(0);
} else if (code.startsWith("x") || code.startsWith("#") || "0123456789abcdef".contains(code)) {
color = legacyCodes(codes.group());
decorations.setLength(0);
} else if (decorations.indexOf("\u00a7" + code) < 0) {
decorations.append('\u00a7').append(code);
}
}
return color + decorations;
}
// Gradients color each character. One private-use character keeps late String.replace tokens intact.
private static ProtectedInput protectPlaceholders(String input) {
Matcher matcher = PLACEHOLDERS.matcher(input);
Map<String, String> originals = new LinkedHashMap<>();
StringBuilder result = new StringBuilder();
int sentinelCodePoint = 0xe000;
while (matcher.find()) {
String angleName = matcher.group(1);
if (angleName != null && TAGS.has(angleName.toLowerCase(Locale.ROOT))) continue;
String sentinel;
do {
if (sentinelCodePoint == 0xf900) sentinelCodePoint = 0xf0000;
sentinel = new String(Character.toChars(sentinelCodePoint++));
} while (input.contains(sentinel));
originals.put(sentinel, matcher.group());
matcher.appendReplacement(result, Matcher.quoteReplacement(sentinel));
}
matcher.appendTail(result);
return new ProtectedInput(result.toString(), originals);
}
private record ProtectedInput(String text, Map<String, String> originals) {
String restore(String rendered) {
for (var entry : originals.entrySet()) rendered = rendered.replace(entry.getKey(), entry.getValue());
return rendered;
}
}
private static boolean hasFormattingTags(String input) {
Matcher tags = MINI_TAG.matcher(input);
while (tags.find()) {
if (TAGS.has(tags.group(1).toLowerCase(Locale.ROOT))) return true;
}
return false;
}
private static String legacyCodes(String input) {
Matcher matcher = CODES.matcher(input);
StringBuilder result = new StringBuilder();
while (matcher.find()) {
String code = matcher.group().substring(1).toLowerCase(Locale.ROOT);
String replacement;
if (code.charAt(0) == '#') {
StringBuilder hex = new StringBuilder("\u00a7x");
for (int i = 1; i < code.length(); i++) hex.append('\u00a7').append(code.charAt(i));
replacement = hex.toString();
} else {
replacement = "\u00a7" + code.replace('&', '\u00a7');
}
matcher.appendReplacement(result, Matcher.quoteReplacement(replacement));
}
matcher.appendTail(result);
return result.toString();
}
private static Component parse(String input) {
if (input == null || input.isEmpty()) return Component.empty();
Matcher matcher = CODES.matcher(input);
StringBuilder converted = new StringBuilder();
while (matcher.find()) {
String code = matcher.group().substring(1).toLowerCase(Locale.ROOT);
String tag;
if (code.charAt(0) == 'x') {
StringBuilder hex = new StringBuilder("#");
for (int i = 2; i < code.length(); i += 2) hex.append(code.charAt(i));
tag = "<reset><" + hex + ">";
} else if (code.charAt(0) == '#') {
tag = "<reset><" + code + ">";
} else {
int color = "0123456789abcdef".indexOf(code);
tag = color >= 0 ? "<reset><" + COLORS[color] + ">" : switch (code) {
case "k" -> "<obfuscated>";
case "l" -> "<bold>";
case "m" -> "<strikethrough>";
case "n" -> "<underlined>";
case "o" -> "<italic>";
default -> "<reset>";
};
}
matcher.appendReplacement(converted, Matcher.quoteReplacement(tag));
}
matcher.appendTail(converted);
return MINI.deserialize(converted.toString());
}
}
Premium read-only network
MailboxNetworkApi • Premium only
package com.rismr1.mailboxgui.api.network;
import com.rismr1.mailboxgui.api.type.NetworkDeliveryTarget;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
// Read-only public excerpt; owner-only companion methods are deliberately omitted.
public interface MailboxNetworkApi {
int getApiVersion();
Set<NetworkCapability> getCapabilities();
/** Stable backend scope, not an exactly-once delivery guarantee. */
String getIdempotencyScope();
NetworkSnapshot getSnapshot();
/** Read-only cached effective icon configuration. Older providers may not support this accessor. */
default NetworkGuiSnapshot getGuiConfiguration() {
throw new UnsupportedOperationException("gui-configuration-unsupported");
}
Optional<NetworkPresenceSnapshot> getPresence(UUID playerId);
NetworkEligibilitySnapshot getEligibility(NetworkDeliveryTarget target);
}
NetworkCapability • Premium only
package com.rismr1.mailboxgui.api.network;
// Public read-only capabilities; this excerpt omits owner-only companion capabilities.
public enum NetworkCapability {
CACHED_SNAPSHOTS, BACKEND_GROUPS, PLAYER_PRESENCE, ROUTING_ELIGIBILITY,
STABLE_BACKEND_SCOPE, SNAPSHOT_EVENTS, GUI_CONFIGURATION
}
NetworkSnapshot • Premium only
package com.rismr1.mailboxgui.api.network;
import java.util.List;
/** status: disabled, starting, ready, degraded, stopped, stale (legacy: available/unavailable).
* Use available rather than comparing display status strings. Time is Unix milliseconds. */
public record NetworkSnapshot(long capturedAtMillis, long generation, String localBackendId,
boolean enabled, boolean started, boolean available, String status,
List<NetworkBackendSnapshot> backends, List<NetworkGroupSnapshot> groups,
List<NetworkPresenceSnapshot> presence, int onlineCount, int staleBackendCount) {
public NetworkSnapshot {
backends = List.copyOf(backends);
groups = List.copyOf(groups);
presence = List.copyOf(presence);
}
}
NetworkBackendSnapshot • Premium only
package com.rismr1.mailboxgui.api.network;
public record NetworkBackendSnapshot(String serverId, String serverDisplayName,
String minecraftVersion, String bukkitVersion, int bukkitDataVersion,
String mailboxGuiVersion, int mailboxDataVersion, int networkProtocolVersion,
int packageManifestVersion, String serializerProfile, String storageType,
boolean vaultAvailable, boolean packageClaimSupported, boolean codClaimSupported,
boolean bookLetterSupported, boolean moneyMailSupported,
long profileCreatedAtMillis, boolean localBackend, boolean stale) {
}
NetworkGroupSnapshot • Premium only
package com.rismr1.mailboxgui.api.network;
import java.util.List;
public record NetworkGroupSnapshot(String name, List<String> members) {
public NetworkGroupSnapshot { members = List.copyOf(members); }
}
NetworkPresenceSnapshot • Premium only
package com.rismr1.mailboxgui.api.network;
import java.util.UUID;
public record NetworkPresenceSnapshot(UUID playerId, String playerName, String backendId,
String backendDisplayName, String worldName, boolean online,
long lastSeenAtMillis, long heartbeatAtMillis, boolean stale) {
}
NetworkEligibilitySnapshot • Premium only
package com.rismr1.mailboxgui.api.network;
import java.util.List;
/** reason: eligible, network_disabled, network_unavailable, invalid_target,
* unknown_backend, unknown_group, no_eligible_backend. Stale backends are never eligible.
*/
public record NetworkEligibilitySnapshot(long capturedAtMillis, long generation,
boolean eligible, String reason, List<String> eligibleBackendIds) {
public NetworkEligibilitySnapshot { eligibleBackendIds = List.copyOf(eligibleBackendIds); }
}
NetworkGuiSnapshot • Premium only
package com.rismr1.mailboxgui.api.network;
/** Immutable, cache-only effective themed-icon configuration, never layouts or player pack status.
* scope: disabled, single-server, shared, backend-local.
* ready means an effective configuration is available, not merely that customization is enabled.
* Disabled snapshots have ready=false and an empty fingerprint. A shared scope may be unready
* while awaiting its authority. sharedMainBackend is the resolved publisher, or its configured
* policy before publication (empty means first publisher); it is empty outside shared scope.
* Fingerprints are opaque configuration identities.
*/
public record NetworkGuiSnapshot(String scope, String backendId, String sharedMainBackend,
String fingerprint, boolean ready) {
}
MailboxNetworkSnapshotEvent • Premium only
package com.rismr1.mailboxgui.api.network;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
/** Main-thread notification after an immutable snapshot is published. Not cancellable. */
public final class MailboxNetworkSnapshotEvent extends Event {
private static final HandlerList HANDLERS = new HandlerList();
private final NetworkSnapshot snapshot;
public MailboxNetworkSnapshotEvent(NetworkSnapshot snapshot) { this.snapshot = snapshot; }
public NetworkSnapshot getSnapshot() { return snapshot; }
@Override public HandlerList getHandlers() { return HANDLERS; }
public static HandlerList getHandlerList() { return HANDLERS; }
}
Reference & support
Testing checklist & support
- Test service absent, older capability sets, disable/reload and main-thread marshaling.
- Test unknown/offline recipients, each RecipientPolicy, single and partially failing mass sends, inherited/immediate delays and actual-delivery expiration.
- Test message bounds, invalid items, capacity/XP, Vault unavailable, item/money/mixed COD, returns, inventory space, stale revisions, concurrent claims and cancelled events.
- Test tracking cancellation, missing/completed IDs, disabled/invalid schedules, timezone/recurrence and manual versus automatic progression.
- For Premium, test starting/degraded/duplicate-backend/stale snapshots, targets/groups, incompatible nested items and fresh eligibility before claims.
- For pack providers, test authenticated current-session success, false/clear, expiry, disconnect, provider disable, On/Off/Auto and a loaded pack with missing models.
Compile-time contract checks are not a substitute for Minecraft/Vault/MySQL/Redis runtime tests in your own server environment. Log bounded operation/status IDs and useful failure reasons without credentials, private mail, purchase-link codes or full item payloads. Preserve plugin data and backups.
MailboxGUI 6.1.0 user/configuration wikiImagineCraft Plugin Discord support