Skip to the documentation
PHP

Changelog

Every release of the package, newest first.

0.0.3

The third release adds the 73 methods the TypeScript SDK gained since 0.0.2, so it covers all 558 again.

  • threads->mute() and threads->unmute() mute a conversation and take the mute off, as Mute does in the app, and both need threads:write. A muted thread in the inbox moves to the archive, and new mail in it goes straight to the archive, unread, with no push, sound or desktop notification. Muting is for the whole workspace, a muted thread carries the built-in MUTE label, and is:muted finds it in query:.
  • The inbox has tabs: Primary, Promotions, Updates, Social and Forums. threads->list(), listAll() and iterate() take category: to read one tab, and category:promotions filters the same way in query:. threads->counts() returns categories, the conversations and unread conversations of the inbox in each tab.
  • threads->listSenderCategories(), setSenderCategory() and clearSenderCategory() manage the senders whose mail always goes to one tab, whatever the sorting rules would say. Saving a choice also moves the inbox threads whose newest message is from that sender, up to the newest 1,000. A key limited to particular addresses or domains can read the choices but not change them.
  • The new followUps namespace holds the reminders on mail you sent: set() asks for a thread to come back to the top of the inbox, unread, with the built-in FOLLOW_UP label and a push, if nobody replies by remindAt, list() reads the reminders still waiting and the ones that ended, and cancel() cancels one. A real reply closes the reminder by itself, and a thread holds one waiting reminder per person, so setting another moves the time. emails->send() and each message of emails->sendBatch() take followUpAt to set a reminder as the message goes out: a DateTimeInterface, an ISO 8601 instant or a duration such as P3D.
  • The new outOfOffice namespace manages the automatic reply an address sends while its owner is away. set() saves the reply of one address, or of a catch-all with *@example.com, with its message, subject, start, end and contactsOnly, list() reads every reply saved in the workspace, and clear() removes one. While a reply is active, each person who writes gets the message once for the whole away period, and spam, bulk mail, automatic mail and bounces get no reply.
  • Labels can be applied by AI: labels->create() and labels->update() take aiInstructions, what the label is for in plain words, and aiEnabled, which has AI read each arriving message against them and add the label when it fits. It only labels and never archives or moves mail, turning it on needs instructions and a paid plan, and a workspace may have 10 such labels. labels->test() runs the AI over the newest 20 inbox threads and returns the ones the label would catch, without labelling anything, and spends one AI action.
  • settings->get() and settings->update() carry three more account settings: inboxTabs shows the inbox as tabs, primaryOnlyAlerts keeps pushes, sounds and desktop notifications to mail sorted into Primary while tabs are on, and phoneFollowUps sends a push when a reminder comes due.
  • Mail travels in two lanes, so a newsletter cannot hold up or harm app mail. emails->send(), each message of emails->sendBatch() and templates->send() take stream: transactional, the default, for mail one person is waiting for, or broadcast for newsletters. Every email carries the stream it went out on, and every copy of a broadcast is on the broadcast lane.
  • The new sending namespace reads the lanes: listStreams() returns both with their state and their numbers for the last 7 days, which are recipients, bounces, complaints and the two rates. The broadcast lane pauses itself above 4% bounces or 0.2% complaints, a send to it is then refused with 409 stream_paused, and app mail keeps going. resumeStream() resumes it, for the workspace owner, and asks an app for a verification code.
  • tools->checkAddress() says whether an address is worth sending to before the first message: valid, risky, invalid or unknown, with the reasons. It checks the syntax, whether the domain takes mail, throwaway domains and shared addresses such as info@, suggests a correction for a likely typo, and with emails:read adds what the workspace knows about the address. deep: true also asks whether the mailbox exists, which is billed per address through pay as you go and needs emails:send, and tools->checkAddresses() checks up to 100 in one call.
  • A workspace on Business or Enterprise that sends at least 100,000 emails a month can buy a dedicated sending IP. For the workspace owner, billing->listAddOns() says whether it is for sale, what it costs, whether the workspace qualifies and whether it has one, billing->startAddOnCheckout() returns the link where the person pays, and billing->cancelAddOn() ends it at the end of the month already paid for. sending->listDedicatedIps() and sending->getDedicatedIp() read each IP with its status and warmupPercent, since a new IP is warmed up over several weeks.
  • domains->getMailAppSettings() returns smtpSubmission, the SMTP server on port 587 for apps, devices and libraries that only offer STARTTLS. Its security is starttls, a new member of MailAppSecurities, and it takes the same username and app password as smtp.
  • imports can read a live mailbox, as the Migrations page of the app does: discoverMailbox() finds the mail server for an address, checkMailbox() signs in once and lists the folders and how many messages an import would bring, and connectMailbox() starts copying the mailbox into one address, with contacts and calendars to bring those across too. The old mailbox is only ever read, a message the address already holds is skipped, and the password is kept sealed until the import finishes and for 30 days at most. An import waits by itself when the old provider slows it down, with the status parked, and when the password stops working it is needs-password and resume() sends a new one.
  • Every import carries source, remote and children, and ImportFormats gains imap and microsoft. The array that holds the password is marked #[\SensitiveParameter], so it stays out of stack traces.
  • calendar->importIcs() and contacts->importVcf() take an iCalendar or vCard file as a string of bytes, a stream resource, an SplFileInfo or a PSR-7 stream and queue it. Events become events of your own and nobody on them is sent anything, cards become contact cards that join no audience, and what is already here is left as it is and counted, so sending a file twice adds nothing. listImports(), listAllImports() and iterateImports() read the imports of each kind, getImport() reads one with its counts and a report of what it left out and why, and undoImport() deletes everything an import added.
  • me->listRequests(), me->listActivity() and me->stats() let a key read its own request log, its own history and its own figures with no scope at all, so a key that can only send can still watch itself. They read the calling key and nothing else: keys->listRequests(), keys->listWorkspaceRequests() and keys->stats() are still the ones that reach other keys, behind keys:read. me->stats() fills sends only when the key holds emails:read and leaves it null otherwise, and an OAuth access token is refused with 403 api_key_only, because calls made with one are not logged.
  • Every reader of the request log, in me and in keys, takes two more filters: statuses: keeps the calls answered with one of up to 20 exact HTTP status codes, and path: keeps the calls to one route, whatever the method. path: matches the route, not the exact path: an id counts as :id and an email address as :address, so /emails/:id reads the calls made to any single email, and a path that ends in * reads every route that starts with it. Both combine with failedOnly:, since: and until:.
  • The new automations namespace holds emails and other steps that run by themselves for each contact: a welcome series when somebody joins an audience, a nudge after a trial starts, a note on a birthday. You edit a draft definition and publish() it, which saves a numbered version and sets the automation live, and each contact stays on the version they entered on. list(), listAll(), iterate(), get(), create(), update() and delete() manage them, listStarters() returns ready-made definitions, pause(), resume(), archive() and duplicate() change their state, sendTest() sends the email of one step to one address, listVersions() and restoreVersion() read the published versions and copy one back into the draft, and stats() returns totals, per-step numbers and a daily series.
  • listEnrollments(), listAllEnrollments(), iterateEnrollments() and getEnrollment() read who is in an automation and what each step did for them, enroll() puts a contact in and exitEnrollment() takes one out. Reading needs automations:read and every change automations:write, and publish(), resume() and sendTest() also need emails:send. A draft that cannot run is refused with 422 invalid_automation, and problems on the error lists every problem that blocks it.
  • The new events namespace records what your contacts did, from your own code: send() stores one event against a contact, starts every live automation whose trigger names it and ends the waits that were holding out for it, sendBatch() records up to 100 in one call and reports per event, and listNames() returns the event names the workspace has seen. Both sends carry an Idempotency-Key, generated per call or passed as idempotencyKey:, so a retry never records an event twice, and a key or an app may send 600 events a minute. contacts->listEvents(), listAllEvents() and iterateEvents() read the events of one contact.
  • WebhookEvents gained AUTOMATION_ENTERED, AUTOMATION_EXITED, AUTOMATION_PAUSED and AUTOMATION_WEBHOOK. Like form.*, they belong to the whole workspace, have to be named in eventTypes, and never reach an endpoint limited to some addresses or domains.
  • On threads->list(), listAll() and iterate() the new category: argument sits before address:, limit:, cursor: and apiKey:, and on the request log readers in keys the new statuses: and path: sit after failedOnly: and before since:, until:, limit:, cursor: and apiKey:. Pass the arguments after them by name.
  • A sign-up form field can fill more of the contact: mapsTo also takes phone, company, jobTitle, website and birthday, saved on the contact card when the sign-up is accepted and only where the card has nothing yet. A single contact now comes with signUps, its last 20 sign-ups with their answers, for a caller that holds forms:read.

0.0.2

The second release adds the 55 methods the TypeScript SDK gained since 0.0.1, so it covers all 485 again.

  • contacts->listCards(), listAllCards(), iterateCards(), getCard(), createCard(), updateCard() and deleteCard() read and write the address book as contact cards, with phone numbers, postal addresses, websites, organisation, job title, birthday and the name in parts. Phones and computers that sync the address book over CardDAV get every change on their next sync.
  • calendar->listFeeds(), createFeed(), resetFeed() and deleteFeed() manage secret links that publish the calendar to Google Calendar, Outlook or Apple Calendar, and listSubscriptions(), createSubscription(), updateSubscription(), refreshSubscription() and deleteSubscription() manage the calendars you subscribe to by link. respondToEvent() can propose a new time, and declineProposal() keeps the original one.
  • Every event carries subscriptionId, rdates and recurrenceId, for the subscription it comes from, the occurrences added outside its rule and the start a changed occurrence replaces.
  • domains->getMailAppSettings(), listAppPasswords(), createAppPassword(), updateAppPassword(), deleteAppPassword() and sendMailAppSetup() put an address into Apple Mail, Gmail, Outlook, Thunderbird or any other mail app over IMAP, SMTP and POP3, and its calendar and contacts over CalDAV and CardDAV. updateMailAppSettings() turns each kind of connection on or off.
  • domains->update() takes senderName, the name mail from a domain goes out under when the sending address has no name of its own.
  • knowledge is new: the notes, files and web pages the AI uses when it writes replies, drafts email and answers in the assistant. It lists, searches, adds, refreshes and removes items, suggests notes, flags duplicates and conflicts, and keeps sites, sitemaps, feeds and Zendesk help centers in step through connectors.
  • threads->replySuggestions() reads the replies the reading pane suggests under the latest message of a thread. settings->update() takes replySuggestions, semanticSearch and knowledgeLearning to turn those features off for the workspace.
  • threads->list(), listAll() and iterate() take semantic to match the words of a query by meaning, and address to keep the threads delivered to one address. tracking->list(), listAll(), iterate() and getStats() take address too.
  • emails->compose() takes draft, withSubject, from, bcc, send, timeZone, template, templates and files, so one description can write or change a whole email.
  • tools->readVerificationCode() finds the sign-in or verification code in a message, and tools->estimateCost() prices every plan for a usage. Neither needs a scope.
  • settings->get() and settings->update() reach every setting the app has: @domain sets a whole domain, and null lets a scope inherit again. drafts->create() takes forwardOf to forward a message, and templates->publish() takes expectedVersion.
  • threads->counts() counts drafts and labels too, and the new constant class ImportFormats holds every format a mailbox import reports.
  • The messages threads->get() returns carry sentWith, forwardedFrom and verificationCode, and senders->get() and senders->research() share what is known about a domain with every workspace.
  • drafts->update() keeps the attachments and the forward of a draft, settings->update() with workspaceName renames the workspace, and the security->verifyStepUp() docs list all 40 calls that ask for a verification code.

0.0.1

The first release. It covers the whole TypeScript SDK as it stood when this release was cut: 430 methods in 41 namespaces, each under its TypeScript name, and every one of them sends byte for byte the request its TypeScript twin sends.

  • OpenEmail\OpenEmail has a property for every namespace, from emails, threads and templates to providerImports, dnsConnections and account, and the methods on each keep their TypeScript names.
  • A request body is an associative array whose keys keep the API’s camelCase names, and options are named arguments such as idempotencyKey: and apiKey:. A response comes back as the decoded associative array.
  • Every paginated resource has list, which returns one Page, listAll, which returns every item as an array, and iterate, which returns a Generator that fetches the next page only when it gets there. PeoplePage, TempMessagesPage, AddressBookPage, AddressBook, BatchResult and TemplateSends carry the answers that hold more than a list.
  • An API refusal throws ApiException or its subclass for the error type, with status, type, errorCode, param, docUrl, requestId, retryAfterSeconds, fields and body, and methods such as isValidation() and isStepUpRequired(). No response at all throws NetworkException, with isTimeout() when the deadline passed. A mistake in the call itself throws InvalidArgumentException before anything is sent. Every one implements OpenEmailException.
  • new OpenEmail() and OpenEmail::createClient() read OPENEMAIL_API_KEY, OPENEMAIL_ACCESS_TOKEN and OPENEMAIL_BASE_URL for anything you leave out, and OpenEmail::init(), OpenEmail::getClient() and OpenEmail::resetClient() manage one shared client. OpenEmail::createTempMail() opens disposable inboxes with no credential.
  • accessToken: takes an OAuth access token, or a callable that returns one, called before every request. Every method takes apiKey: to act with another key for one call, and the tempMail methods take inboxToken:. Every parameter that carries a credential is marked #[\SensitiveParameter].
  • OpenEmail::verifyWebhookSignature() checks a delivery in constant time, refuses one more than five minutes old and returns the decoded event. It reads the signature from an array of headers in any case, from $_SERVER, from a Symfony or Laravel header bag, or from a PSR-7 request.
  • OpenEmail::toBase64(), isApiKey(), isAccessToken(), isSealed(), resolveLanguage(), languageByCode() and isRtlLanguage() are the helpers of the TypeScript SDK, and every value set its package root exports is a class in OpenEmail\Constants.
  • It needs PHP 8.2 or later with the curl and json extensions, and nothing else. Requests go through one cURL handle per client, which keeps its connection open between requests and opens a new one after pcntl_fork, and timeout: bounds a whole attempt. Psr18HttpClient sends them through Guzzle, Symfony HttpClient or any other PSR-18 client instead.
  • Reads, sends and every write that is safe to repeat are retried on 408, 500, 502, 503 and 504 with backoff, and on a 429 only when its Retry-After is a minute or less. Every send carries an idempotency key that its retries reuse.
  • The client refuses to send a credential over plain http: to anything but this machine, refuses a base URL on 0.0.0.0, refuses a header that would split a request, and never lets raw->request() leave the base URL’s origin.