Connect and follow a mailbox
A mailbox is the email connection you send from or receive into. Read it first. A connected state alone does not mean it can send.
Read its state
import { Colding } from "colding";
const colding = new Colding("cd_your_key");
const mailboxId = "22222222-2222-4222-8222-222222222222";
const mailbox = await colding.mailboxes.get({ mailboxId });
console.log(mailbox.state, mailbox.ownership.state);mailboxes.get needs read and changes nothing. Replace mailboxId with an ID from mailboxes.list() or a thread's mailboxIds. The first guide shows how to get an API key.
connected proven
connected- The mailbox is not paused and its logins have not been marked as needing replacement. Address confirmation and other sending checks still apply.
needs_reconnect- The provider refused access. Ask the owner to update the logins in the console, using the request below.
paused- Outgoing mail is paused. Receiving continues while the logins work. Check
syncto see whether mail is being read.
The full answer also holds provider, purpose, dailyCap, minGapSeconds, problems, domainAuth, sync, warmup and sendState. ownership says whether the sender address is confirmed. sendState is a campaign hold, not permission for a direct message.
Passwords never come back. A send-only mailbox also hides its SMTP username, because that username can itself be a secret. smtp and imap can be null, depending on how the mailbox connects.
Ask an owner to connect
Before you ask the owner, look the address up. mailboxes.detectSettings says who runs its email (name), which servers it uses and which password the owner needs (password): normal, app_password (a separate password the provider makes for one app), app_password_with_2fa (an app password only when two-step sign-in is on) or secure_mail_key (AT&T's name for its app password). It needs read, reads DNS only and never logs in.
import { Colding } from "colding";
const colding = new Colding("cd_your_key");
const found = await colding.mailboxes.detectSettings({ email: "[email protected]" });
console.log(found.name, found.password);Google app_password
accessSwitch names a setting the owner turns on at the provider first: imap (IMAP access) or third_party (access for other email apps). signIn names a provider whose own sign-in connects the mailbox instead, and unavailable says why none of our logins can reach it. When nothing is found, servers is null and source is none. Then unresolved says why: dns_unanswered (DNS didn't answer, so try again) or not_published (the owner types the servers in the console). Each workspace can look up 20 addresses a minute.
SMTP sends mail and IMAP reads it. An agent asks for this connection with a handoff: a request that gives the owner a console link. Creating it needs operate. Give url to the owner. The owner signs in and enters the logins in the console, not in an agent chat.
const request = await colding.handoffs.create({
kind: "connect_mailbox",
});
console.log(request.url);Read the same request with handoffs.get, which needs read. A completed request names the mailbox in resultMailboxId. Read that mailbox to see its current state.
const now = await colding.handoffs.get({ handoffId: request.id });
console.log(now.status);
if (now.status === "completed" && now.resultMailboxId) {
const mailbox = await colding.mailboxes.get({ mailboxId: now.resultMailboxId });
console.log(mailbox.ownership);
}A request is pending, completed, cancelled or expired. Pending requests expire after 24 hours. If it expires, create a new request. To withdraw one you no longer need, call handoffs.cancel with handoffId. Canceling needs operate and never disconnects a mailbox already connected.
To replace logins, use update_mailbox_credentials and name the mailboxId. The owner enters the new ones in the console. A passing check replaces the logins but does not undo a mailbox pause.
const request = await colding.handoffs.create({
kind: "update_mailbox_credentials",
mailboxId,
});
console.log(request.url);Before saving a connection or new logins, the worker checks the logins over encrypted connections. A refused login saves nothing and answers 400 VALIDATION_ERROR, with the field and reason in details. The owner fixes the named login and retries in the console. A passing check proves the logins worked then, not that mail will arrive or that the address is yours.
If the check cannot finish within 15 seconds, or the worker is unavailable, it answers retryable 503 SERVICE_UNAVAILABLE with messageKey: serverErrors.mailboxCheckUnavailable. Nothing is saved. There is no Retry-After header. The owner can retry the check in the console.
The public SDK has no method to pass raw logins, change a password, enter a confirmation code or disconnect. Those are signed-in owner steps in the console. A request link does not approve a send.
Use SMTP for your app's email
A send-only mailbox uses a sending service's SMTP login, with no IMAP. In the connection request, ask the owner to choose Sending service (send only). It reads as provider: smtp_only and purpose: transactional.
It sends your app's transactional email, such as a receipt or sign-in link. It never sends campaigns, starts a new direct message or warms up. It needs a plan with transactional email and counts toward the plan's mailboxes. The service must ask for an SMTP login before we save it.
We cannot read mail sent to that address. Replies go wherever the rest of your domain's mail goes. Set replyTo on the mailbox with mailboxes.update, or on the transactional email, to choose an address someone reads. Connect a receiving mailbox if you need those replies in this inbox.
Later bounces and spam reports need the sending service's report hook, set up by the owner in the console. The mailbox's missing inbox cannot supply them. To ask the owner for it, create a manage_report_hook handoff with the mailbox's id: the owner pastes the service's secret in the console, so you never see it. The SDK reads the hook's state with mailboxes.reportHook and received reports with mailboxes.reports.
Confirm the sender address
Working logins are not proof of the sender address. ownership.state is unproven (not confirmed), pending (a code is waiting) or proven (confirmed). No mail sends while it is unproven or pending, including replies, tests, transactional email and warmup. Reading still works.
The owner confirms the address in the console, under Sending address in the mailbox's Overview. We email a code to that address, and the owner presses the button in the email or types the code in. If the address can't receive email, ask our team to confirm it another way.
Ask our team to confirm an address
If a code is pending, ownership includes expiresAt and mail, the state of that code's email. Pending does not mean confirmed. There is no address-confirmation handoff or public SDK code method.
Even with proven, sending still needs the applicable permission, the owner's key limits, the acceptable use policy, the plan, payment and recipient checks. Mailbox pauses and other holds still apply. Follow the send guide for the message you want to send.
Check reading and old-mail import
resync is Check now. It asks the worker to look for mail sooner, rather than waiting for the regular check (currently about every 60 seconds). It needs operate. Its answer is a mailbox snapshot, not proof that the new pass finished. Read mailboxes.get again later.
await colding.mailboxes.resync({ mailboxId });
const mailbox = await colding.mailboxes.get({ mailboxId });
console.log(mailbox.sync);
const folders = await colding.mailboxes.folders({ mailboxId });
console.log(folders);sync.startedAt is when reading first listed the folders. lastSuccessAt is the last pass that finished with nothing held back. Null means no such pass has finished, not an empty inbox. nextAt says when the next pass is due. failures counts failed passes in a row, and pending counts messages found but not stored yet.
sync.import describes the one-off import of existing mail, newest first. Its status is pending, running or complete. since and limit are the time window and message ceiling fixed when it starts. imported counts what it has stored, and completedAt says when it ended. Complete does not mean the provider's entire history was imported.
sync.folders counts discovered and watched folders. mailboxes.folders lists each one's provider path, role, watched, excludedReason, pending and importDone. An excluded folder says why, rather than silently pretending it was read. This call needs read.
If reading stopped after a refused IMAP login, Check now tries the saved logins first. A login still refused answers 409: ask the owner to replace it. If checks to that server are on hold, a 429 says when to try again. A send-only mailbox answers 409 because it has no incoming mail to read.
Check the sending domain
The sending domain is the part after the @. DNS records tell receiving servers who may send from it. checkDomain reads the records now and saves the result on this workspace's mailboxes using that domain. It needs operate.
const mailbox = await colding.mailboxes.checkDomain({ mailboxId });
console.log(mailbox.domainAuth);SPF and DMARC decide whether the domain is passing or failing. A send-only mailbox is judged on DMARC alone. DKIM is found or not verified, never called missing: DNS cannot list every selector, the name under which a DKIM key is published. When SPF or DMARC is missing, domainAuth.dnsHost says where the domain's DNS lives, read from its name servers: hosts lists the DNS providers they match (empty when we do not know it), and the whole value is null when DNS did not answer.
If the required records keep failing for 72 hours, campaigns and warmup stop using these mailboxes until a check passes. failingSince is when that grace began. Fix the records with whoever manages your domain, then call checkDomain again.
unknown means no verdict, not a pass or a failure. unknownReason says whether DNS did not answer or the domain is a reserved name. A timeout does not clear an existing failure. Check again when DNS answers. These records never promise delivery.
Some DNS providers can add the missing DMARC record for the owner: the owner sees the record on the provider's own page and approves it. This uses a standard called Domain Connect. domainConnectLink returns that link. It needs operate.
const link = await colding.mailboxes.domainConnectLink({ mailboxId });
console.log(link.url ?? link.reason);The answer has the url and the provider's name in provider, or a null url and a reason: not_set_up (we haven't turned one-click links on), not_offered (the domain's DNS provider doesn't offer our record), unanswered (DNS or the provider didn't answer in time, so try again) or nothing_to_add (a DMARC record already covers the domain). No provider offers our records yet, so for now every answer has a reason and no link. Give the link to the owner. It opens the provider's page, and the provider sends them back to the console, or to the console page you name in returnTo (a path like /acme/mailboxes). Coming back proves nothing, so call checkDomain after. Each workspace can ask for 30 links a minute.
A tracking domain's CNAME record has the same kind of link: trackingDomains.connectLink takes a trackingDomainId and answers the same way, with nothing_to_add once the domain is verified.
Pause sending or disconnect
mailboxes.pause stops outgoing work from the mailbox. Campaigns and warmup wait, and new sends are refused. Mail still arrives and is read while the logins work. Settings and logins stay. It cannot recall mail already on its way. It needs operate.
const mailbox = await colding.mailboxes.pause({ mailboxId });
console.log(mailbox.state, mailbox.sync.lastSuccessAt);mailboxes.resume with mailboxId removes that pause. It also needs operate. It does not repair a refused login, confirm an address or lift a separate hold.
Disconnect is an owner-console action, with no public SDK method or handoff kind. It deletes colding's connection, stored logins, settings and stored messages from that mailbox. Mail at your provider is not deleted. It cannot be undone. Warmup holds attached to the address remain, so reconnecting is not a way to clear one.
Start and read warmup
Warmup sends and receives email with a group of mailboxes, called the pool. It sends real email, so startWarmup needs send. The owner must also have accepted the acceptable use policy. It can be enabled before the sender address is confirmed, but no warmup sends until then. Key limits on mailboxes and streams still apply. A key limited to transactional email cannot start warmup. Send-only mailboxes and Gmail API mailboxes never warm up.
const mailbox = await colding.mailboxes.startWarmup({ mailboxId });
console.log(mailbox.warmup.state, mailbox.warmup.blockedBy);
console.log(mailbox.warmup.sentToday, mailbox.warmup.todayCap);
const days = await colding.mailboxes.warmupStats({ mailboxId });
console.log(days);Read warmup.state, blockedBy, sentToday and todayCap from the answer. Running means enabled, not sending right now. todayCap is today's actual cap after pool size, days and health are taken into account. dailyTarget is the ramp's target, not a promised number of messages.
day counts time warmed, not calendar days since connection. rampHeldUntil says when a spam-placement hold ends. health holds the band and its readings, or null when there is no reading. A missing complaint reading is no data, not zero. health.probation can slow warmup after a quarantine or block even when the band reads healthy.
blockedBy names the first reason to fix: a workspace suspension, our team's hold, an unconfirmed address, a refused login, a mailbox pause, a health hold, the domain or no partners. Our team's mailbox hold has its reason in staffBlock. Ask for review with mailboxes.appealWarmup, passing mailboxId and reason. It needs operate and does not lift the hold by itself. Only one appeal can wait at a time.
mailboxes.pauseWarmup with mailboxId stops warmup sending but keeps the mailbox in the pool and preserves its ramp. Partners can still write to it. Resume with startWarmup. mailboxes.stopWarmup leaves the pool. The next run starts its ramp from the first day. Both pause and stop need operate, and keep the saved hours, days and reply rate. Neither clears an address's health or staff hold.
await colding.mailboxes.pauseWarmup({ mailboxId });
await colding.mailboxes.stopWarmup({ mailboxId: otherMailboxId });
const appealed = await colding.mailboxes.appealWarmup({
mailboxId,
reason: "We fixed the domain's records on 5 October.",
});
console.log(appealed.warmup.blockedBy);receiveOnly: true on the first start lets partners write to the mailbox without it sending warmup. Resume with the same value. To change modes, stop first. window and days use the workspace's clock.
warmupStats needs read and returns 30 days by default, today included. Pass days for up to 90. Each day says what went, its cap, what partners found in spam and what arrived here. Warmup does not appear in the inbox or raise send.* events. It does not promise where email will land.
Read where your mail lands
deliverability.get needs read and answers in one call, with no score. Pass days as 7, 14, or 30, today included, on the workspace's calendar. Left out, it is 7. It reads every mailbox and campaign of the workspace, so a key limited to some mailboxes is refused with 403 keyReads.
const answer = await colding.deliverability.get({ days: 14 });
for (const domain of answer.placement.domains) {
for (const row of domain.providers) {
console.log(domain.domain, row.provider, row.counts.arrived, row.counts.spam);
}
}
for (const mailbox of answer.attention.mailboxes) {
const worst = mailbox.reasons[0];
console.log(mailbox.email, worst?.reason, worst?.stops, worst?.until);
}placement is warmup, by sending domain and receiving provider. A provider row counts the emails that arrived, the ones the partner found in spam (part of arrived), the ones that neverArrived, the ones your mailbox's server refused (bounced), the ones still tooSoon to judge and the ones we keep out (leftOut). They add up to sent. atTestInboxes counts how many of the arrived emails reached a test inbox we run.
fromOurServers is what receiving servers answered about email that left our servers, by stream (cold or transactional) and provider. Accepted means the server took the email, not that it reached the inbox. noWordYet counts emails with no answer yet. It is null when the workspace has no agent inbox or sender address and sent nothing from our servers in the window.
attention lists the mailboxes and campaigns that one of our rules paused or slowed. Each has its reason, what the reason stops (sending, first_contact, campaigns, warmup, nothing_yet or slows_warmup) and when it began and ends. A pause a person made is not listed. The lists hold 100 items at most, and more counts the rest.
A share is { pct, of, bound }, and bound is under1 or over99 when the figure is close to 0 or 100 but not exactly there. A share shows only from 20 warmup emails (placement.floor) or 50 messages (floor). Under that you get { tooFew, floor }.
Spam reports are a count and never a share, because most providers send none. source: false means a provider that tells the sender nothing, which is not 0. days has one entry for each day of the window, with zeros where nothing happened.
Answer a review
Our team can put a mailbox under review, for example after reports about its mail. While it lasts, review on mailboxes.get, mailboxes.list and the agent inbox reads is not null. Without a review, it is null.
Read review.caseNumber first and quote it when you write to us. since says when the review began, and until is the date we said we would review by. Nothing lifts when until passes, and overdue turns true. reason has our team's words, or null when the system put the mailbox under review. answeredAt is null until you tell us what happened.
const mailbox = await colding.mailboxes.get({ mailboxId });
if (mailbox.review) {
const review = await colding.mailboxes.answerReview({
mailboxId,
whatHappened: "Our 3 September campaign went to a list we bought. Some people had not asked.",
whatChanged: "We deleted that list and paused the campaign.",
howItStaysFixed: "From now on we only write to people who signed up on our site.",
});
console.log(review.caseNumber, review.answeredAt);
}Tell us what happened with mailboxes.answerReview. Pass mailboxId and three answers in your own words: whatHappened, whatChanged and howItStaysFixed. Each is 1 to 2000 characters. It needs operate, and a key limited to some mailboxes can answer only for those. The answer is the review, with answeredAt filled in.
Our team reads the three answers with the case. Sending them again replaces the last answer. This lifts nothing: only our team ends a review. A mailbox that is not under review answers 409, so read review first. It also works while the workspace waits to be deleted.
It works for an agent inbox too. Pass the inbox's id as mailboxId. Agent inbox reads carry the same review field.
Follow mailbox changes
Read mailbox.failed, mailbox.recovered, mailbox.address_confirmed and mailbox.warmup_health_changed through events.list or a webhook. They name the mailbox and the change, without logins. An arrival event is for new mail after reading started, not old imported mail.
const page = await colding.events.list({
types: [
"mailbox.failed",
"mailbox.recovered",
"mailbox.address_confirmed",
"mailbox.warmup_health_changed",
],
});
console.log(page.items);