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()andthreads->unmute()mute a conversation and take the mute off, as Mute does in the app, and both needthreads: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-inMUTElabel, andis:mutedfinds it inquery:.- The inbox has tabs: Primary, Promotions, Updates, Social and Forums.
threads->list(),listAll()anditerate()takecategory:to read one tab, andcategory:promotionsfilters the same way inquery:.threads->counts()returnscategories, the conversations and unread conversations of the inbox in each tab. threads->listSenderCategories(),setSenderCategory()andclearSenderCategory()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
followUpsnamespace 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-inFOLLOW_UPlabel and a push, if nobody replies byremindAt,list()reads the reminders still waiting and the ones that ended, andcancel()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 ofemails->sendBatch()takefollowUpAtto set a reminder as the message goes out: aDateTimeInterface, an ISO 8601 instant or a duration such asP3D. - The new
outOfOfficenamespace 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 andcontactsOnly,list()reads every reply saved in the workspace, andclear()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()andlabels->update()takeaiInstructions, what the label is for in plain words, andaiEnabled, 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()andsettings->update()carry three more account settings:inboxTabsshows the inbox as tabs,primaryOnlyAlertskeeps pushes, sounds and desktop notifications to mail sorted into Primary while tabs are on, andphoneFollowUpssends 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 ofemails->sendBatch()andtemplates->send()takestream:transactional, the default, for mail one person is waiting for, orbroadcastfor newsletters. Every email carries thestreamit went out on, and every copy of a broadcast is on the broadcast lane. - The new
sendingnamespace 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 409stream_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,invalidorunknown, with the reasons. It checks the syntax, whether the domain takes mail, throwaway domains and shared addresses such asinfo@, suggests a correction for a likely typo, and withemails:readadds what the workspace knows about the address.deep: truealso asks whether the mailbox exists, which is billed per address through pay as you go and needsemails:send, andtools->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, andbilling->cancelAddOn()ends it at the end of the month already paid for.sending->listDedicatedIps()andsending->getDedicatedIp()read each IP with itsstatusandwarmupPercent, since a new IP is warmed up over several weeks. domains->getMailAppSettings()returnssmtpSubmission, the SMTP server on port 587 for apps, devices and libraries that only offer STARTTLS. Itssecurityisstarttls, a new member ofMailAppSecurities, and it takes the same username and app password assmtp.importscan 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, andconnectMailbox()starts copying the mailbox into one address, withcontactsandcalendarsto 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 thestatusparked, and when the password stops working it isneeds-passwordandresume()sends a new one.- Every import carries
source,remoteandchildren, andImportFormatsgainsimapandmicrosoft. The array that holds the password is marked#[\SensitiveParameter], so it stays out of stack traces. calendar->importIcs()andcontacts->importVcf()take an iCalendar or vCard file as a string of bytes, a stream resource, anSplFileInfoor 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()anditerateImports()read the imports of each kind,getImport()reads one with its counts and a report of what it left out and why, andundoImport()deletes everything an import added.me->listRequests(),me->listActivity()andme->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()andkeys->stats()are still the ones that reach other keys, behindkeys:read.me->stats()fillssendsonly when the key holdsemails:readand leaves itnullotherwise, and an OAuth access token is refused with 403api_key_only, because calls made with one are not logged.- Every reader of the request log, in
meand inkeys, takes two more filters:statuses:keeps the calls answered with one of up to 20 exact HTTP status codes, andpath:keeps the calls to one route, whatever the method.path:matches the route, not the exact path: an id counts as:idand an email address as:address, so/emails/:idreads the calls made to any single email, and a path that ends in*reads every route that starts with it. Both combine withfailedOnly:,since:anduntil:. - The new
automationsnamespace 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 draftdefinitionandpublish()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()anddelete()manage them,listStarters()returns ready-made definitions,pause(),resume(),archive()andduplicate()change their state,sendTest()sends the email of one step to one address,listVersions()andrestoreVersion()read the published versions and copy one back into the draft, andstats()returns totals, per-step numbers and a daily series. listEnrollments(),listAllEnrollments(),iterateEnrollments()andgetEnrollment()read who is in an automation and what each step did for them,enroll()puts a contact in andexitEnrollment()takes one out. Reading needsautomations:readand every changeautomations:write, andpublish(),resume()andsendTest()also needemails:send. A draft that cannot run is refused with 422invalid_automation, andproblemson the error lists every problem that blocks it.- The new
eventsnamespace 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, andlistNames()returns the event names the workspace has seen. Both sends carry anIdempotency-Key, generated per call or passed asidempotencyKey:, so a retry never records an event twice, and a key or an app may send 600 events a minute.contacts->listEvents(),listAllEvents()anditerateEvents()read the events of one contact. WebhookEventsgainedAUTOMATION_ENTERED,AUTOMATION_EXITED,AUTOMATION_PAUSEDandAUTOMATION_WEBHOOK. Likeform.*, they belong to the whole workspace, have to be named ineventTypes, and never reach an endpoint limited to some addresses or domains.- On
threads->list(),listAll()anditerate()the newcategory:argument sits beforeaddress:,limit:,cursor:andapiKey:, and on the request log readers inkeysthe newstatuses:andpath:sit afterfailedOnly:and beforesince:,until:,limit:,cursor:andapiKey:. Pass the arguments after them by name. - A sign-up form field can fill more of the contact:
mapsToalso takesphone,company,jobTitle,websiteandbirthday, saved on the contact card when the sign-up is accepted and only where the card has nothing yet. A single contact now comes withsignUps, its last 20 sign-ups with their answers, for a caller that holdsforms: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()anddeleteCard()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()anddeleteFeed()manage secret links that publish the calendar to Google Calendar, Outlook or Apple Calendar, andlistSubscriptions(),createSubscription(),updateSubscription(),refreshSubscription()anddeleteSubscription()manage the calendars you subscribe to by link.respondToEvent()can propose a new time, anddeclineProposal()keeps the original one.- Every event carries
subscriptionId,rdatesandrecurrenceId, 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()andsendMailAppSetup()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()takessenderName, the name mail from a domain goes out under when the sending address has no name of its own.knowledgeis 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()takesreplySuggestions,semanticSearchandknowledgeLearningto turn those features off for the workspace.threads->list(),listAll()anditerate()takesemanticto match the words of a query by meaning, andaddressto keep the threads delivered to one address.tracking->list(),listAll(),iterate()andgetStats()takeaddresstoo.emails->compose()takesdraft,withSubject,from,bcc,send,timeZone,template,templatesandfiles, so one description can write or change a whole email.tools->readVerificationCode()finds the sign-in or verification code in a message, andtools->estimateCost()prices every plan for a usage. Neither needs a scope.settings->get()andsettings->update()reach every setting the app has:@domainsets a whole domain, andnulllets a scope inherit again.drafts->create()takesforwardOfto forward a message, andtemplates->publish()takesexpectedVersion.threads->counts()counts drafts and labels too, and the new constant classImportFormatsholds every format a mailbox import reports.- The messages
threads->get()returns carrysentWith,forwardedFromandverificationCode, andsenders->get()andsenders->research()share what is known about a domain with every workspace. drafts->update()keeps the attachments and the forward of a draft,settings->update()withworkspaceNamerenames the workspace, and thesecurity->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\OpenEmailhas a property for every namespace, fromemails,threadsandtemplatestoproviderImports,dnsConnectionsandaccount, 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:andapiKey:. A response comes back as the decoded associative array. - Every paginated resource has
list, which returns onePage,listAll, which returns every item as an array, anditerate, which returns a Generator that fetches the next page only when it gets there.PeoplePage,TempMessagesPage,AddressBookPage,AddressBook,BatchResultandTemplateSendscarry the answers that hold more than a list. - An API refusal throws
ApiExceptionor its subclass for the error type, withstatus,type,errorCode,param,docUrl,requestId,retryAfterSeconds,fieldsandbody, and methods such asisValidation()andisStepUpRequired(). No response at all throwsNetworkException, withisTimeout()when the deadline passed. A mistake in the call itself throwsInvalidArgumentExceptionbefore anything is sent. Every one implementsOpenEmailException. new OpenEmail()andOpenEmail::createClient()readOPENEMAIL_API_KEY,OPENEMAIL_ACCESS_TOKENandOPENEMAIL_BASE_URLfor anything you leave out, andOpenEmail::init(),OpenEmail::getClient()andOpenEmail::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 takesapiKey:to act with another key for one call, and thetempMailmethods takeinboxToken:. 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()andisRtlLanguage()are the helpers of the TypeScript SDK, and every value set its package root exports is a class inOpenEmail\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, andtimeout:bounds a whole attempt.Psr18HttpClientsends 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-Afteris 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 on0.0.0.0, refuses a header that would split a request, and never letsraw->request()leave the base URL’s origin.